Go实现网关服务

前言

基本功能

  1. 支持多种协议代理:tcp/http/websocket/grpc

  2. 支持多种负载均衡策略:轮询/加权轮询/一致性Hash

  3. 支持下游服务发现:主动探测/自动服务发现

  4. 支持横向扩容:加机器就能解决高并发

借助网关处理高可用、高并发

  1. 限流:请求QPS限制

  2. 熔断:错误率达阈值服务熔断

  3. 降级:确保核心业务可用

  4. 权限认证:拦截没有权限的用户

计算机网络基础

OSI七层网络协议与TCP/IP协议

image-20201014193343592

经典协议与数据包

数据包

image-20201014193624753

在物理层传输的是以太网帧,它由以太网首部(主要包含机器mac信息等)和IP数据包构成。

在网络层传输的是IP数据包,它由IP首部和TCP段构成。

在传输层传输的是TCP段,它由TCP首部和应用数据构成。如果应用数据过大则会进行拆包,那么需要在应用数据中数据的包定义起点、终点。

在应用层传输的是应用数据,它由应用头部和用户数据构成。

HTTP协议

HTTP协议是明文传输的。

如下是HTTP的请求报文和响应报文

image-20201014194613130

Websocket握手协议

基于HTTP协议实现,完成连接后以二进制流方式进行传输

image-20201014194822267

Websocket数据包

image-20201014195012535

三次握手与四次挥手

三次握手

image-20201014195350112

四次挥手

image-20201014195447121

抓包分析三次握手和四次挥手

命令:tcpdump -n -S -i eth0 host www.baidu.com and tcp port 80

image-20201014200427763

为什么关闭方要等待2MSL?

  1. 保证连接可靠关闭

  2. 保证所有数据都传输完成

为什么会大量close_wait?

  1. 并发请求数太多导致的

  2. 被动关闭方未及时释放端口资源导致

close_wait一般出现在被动关闭方

TCP流量控制

流量控制来控制通讯双方数据发送的频率

由于通讯双方网速不同。任一方数据发送过快都会导致对方消息处理不过来,一般会先把数据放到缓存区中。如果缓冲区满了,发送方还在疯狂发送数据,数据包将会被丢弃。因此我们需要控制发送频率,TCP则是通过流量控制来控制发送频率。

image-20201014201934777

TCP拥塞控制

拥塞控制用来调节网络的负载

接收方网络资源繁忙,因未及时响应ACK导致发送方重传大量数据,将是的网络更加拥堵。拥塞控制可以动态调整窗口大小,不只是依赖缓冲区大小确定窗口。

慢启动和拥塞避免

image-20201014203306434

在慢启动阶段从1开始以2倍扩大的方式指数增长至慢启动阈值,接着进入拥塞避免加法增大(每次增大1),如果遇到网络拥塞的情况则会直接将窗口置为1,慢启动阈值置为原来的一般,继续进入慢启动阶段。

快速重传和快速恢复

image-20201014203836336

最开始仍然采用慢启动和拥塞避免算法。不同的是在收到3个重复ACK执行快重传,快重传(乘法减小)是将拥塞窗口降为原来的一半,将拥塞避免的阈值也降为原来的一半,接着进入快速恢复阶段。快速恢复对数据进行重传,重传完毕后,进入拥塞避免(加法增大)。

为啥会出现粘包、拆包,如何处理?

image-20201014205744465

应用程序写入的数据大于套接字缓冲区大小,将会发生拆包。

进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度 - TCP头部长度 > MSS时将发生拆包。

应用程序写入的数据小于套接字缓存区大小,网卡将应用多次写入的数据发送到网络上,将发送粘包。

接收方法不及时读取套接字缓冲区数据,将发生粘包。

如何获取完整的应用数据报文?

  1. 使用带消息头的协议,头部写入包长度,然后读取包内容。

  2. 设置定长消息,每次读取定长内容,长度不够时空位补固定字符。

  3. 设置消息边界,服务端从网络流中按消息边界分离出消息内容,一般使用

  4. 在应用层我们还可以直接选择如protobufjson等协议。

基于go实现TCP、UDP、HTTP服务器与客户端

TCP

服务端

// 1. net.Listen 监听端口,选择协议和端口
// 2. 通过listener.Accept() 建立套接字连接,并挂起
// 3. go func 创建处理协程并处理消息

func main() {
	// 1. 监听端口
	var (
		listener net.Listener
		err      error
		conn     net.Conn
	)
	if listener, err = net.Listen("tcp", "0.0.0.0:8989"); err != nil {
		log.Fatalf("listen failed, err: %s \n", err.Error())
	}

	// 2. 建立连接并挂起
	for {
		if conn, err = listener.Accept(); err != nil {
			log.Fatalf("create conn failed, err: %s \n", err.Error())
		}

		// 3. 处理消息
		go handle(conn)
	}
}

func handle(conn net.Conn) {
	defer conn.Close()
	for {
		var buf [128]byte
		var n, err = conn.Read(buf[:])
		if err != nil {
			fmt.Printf("read from connect failed, err: %v\n", err)
			break
		}
		var str = string(buf[:n])
		fmt.Printf("receive from client, data: %v\n", str)
	}
}

客户端

// 1. 建立连接器
// 2. 读取命令行输入
// 3. 挂起,一直读取,知道读到\n
// 4. 读取Q时停止
// 5. 发送信息到服务器
func main() {
	sendMessage()
	fmt.Println("send over")
}

func sendMessage() {
	// 1. 连接服务器
	var (
		conn net.Conn
		err  error
	)

	if conn, err = net.Dial("tcp", "localhost:8989"); err != nil {
		log.Fatalf("connect failed, err: %s", err.Error())
	}
	defer conn.Close()

	var (
		inputReader  *bufio.Reader
		input        string
		trimmedInput string
	)
	// 2. 读取命令行输入
	inputReader = bufio.NewReader(os.Stdin)

	for {
		// 3. 直到读取到\n
		if input, err = inputReader.ReadString('\n'); err != nil {
			fmt.Printf("read from console failed, err: %s \n", err.Error())
			break
		}

		// 4. 读取Q时停止
		if trimmedInput = strings.TrimSpace(input); trimmedInput == "Q" {
			break
		}

		// 5. 发送消息
		if _, err = conn.Write([]byte(trimmedInput)); err != nil {
			fmt.Printf("write failed , err : %s\n", err.Error())
			break
		}
	}
}

UDP

服务端

package main

import (
	"fmt"
	"log"
	"net"
)

// 1. 监听端口
// 2. 循环读取消息
// 3. 打印消息(接收到的客户端消息)
// 4. 响应消息(回复确认消息)
func main() {
	var (
		conn *net.UDPConn
		err  error
	)
	// 监听端口
	if conn, err = net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 8989,
	}); err != nil {
		log.Fatalf("listen failed, err: %s\n", err.Error())
	}

	// 2. 循环读取消息内容
	for {
		var (
			data [1024]byte
			n    int
			addr *net.UDPAddr
		)
		if n, addr, err = conn.ReadFromUDP(data[:]); err != nil {
			log.Fatalf("read failed from addr: %v, err: %s\n", addr, err.Error())
		}

		go func() {
			// 3. 打印消息
			fmt.Printf("addr: %v data: %v  count: %v\n", addr, string(data[:n]), n)
			// 4. 响应消息
			_, err = conn.WriteToUDP([]byte("received success!"), addr)
			if err != nil {
				fmt.Printf("write failed, err: %v\n", err)
			}
		}()
	}
}

客户端

package main

import (
	"log"
	"net"
	"strconv"
)

func main() {
	// 1. 连接服务器
	// 2. 发送数据
	// 3. 接收数据
	var (
		conn       *net.UDPConn
		err        error
		rspMsg     = make([]byte, 1024)
		n          int
		remoteAddr *net.UDPAddr
	)
	if conn, err = net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 8989,
	}); err != nil {
		log.Fatalf("connect failed, err: %s\n", err.Error())
	}

	for i := 0; i < 16; i++ {
		// 发送数据
		if _, err = conn.Write([]byte("hello server, this is msg: " + strconv.Itoa(i))); err != nil {
			log.Printf("send data failed, err : %s\n", err.Error())
			return
		}

		// 接收数据(服务端响应消息)
		if n, remoteAddr, err = conn.ReadFromUDP(rspMsg); err != nil {
			log.Printf("receive data failed, err: %s\n", err.Error())
			return
		}

		log.Printf("receive from addr: %v  data: %v\n", remoteAddr, string(rspMsg[:n]))
	}
}

HTTP

服务端

var (
	addr = ":8989"
)

// 1. 创建路由器
// 2. 设置路由规则
// 3. 创建服务器
// 4. 监听端口并提供服务
func main() {
	var (
		mux    *http.ServeMux
		server *http.Server
	)
	// 1. 创建路由器
	mux = http.NewServeMux()
	// 2. 设置路由规则
	mux.HandleFunc("/bye", sayBey)
	// 3. 创建服务器
	server = &http.Server{
		Addr:         addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	// 4. 监听端口并提供服务
	log.Println("staring HTTPServer at ", addr)
	log.Fatal(server.ListenAndServe())
}

func sayBey(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)
	w.Write([]byte("bye bye, server addr: " + r.Host + " your addr: " + r.RemoteAddr))
}

客户端

// 1. 创建连接池
// 2. 创建客户端
// 3. 请求数据
// 4. 读取内容
func main() {
	var (
		transport *http.Transport
		client    *http.Client
		rsp       *http.Response
		err       error
		bds       []byte
	)
	// 1. 创建连接池
	transport = &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   time.Second * 30, // 连接超时
			KeepAlive: time.Second * 30, // 探活时间
		}).DialContext,
		MaxIdleConns:          100,              // 最大空闲连接数
		IdleConnTimeout:       time.Second * 90, // 空闲连接超时时间
		TLSHandshakeTimeout:   time.Second * 10, // tls握手超时时间
		ExpectContinueTimeout: time.Second,      // 100-continue状态码超时时间
	}

	// 2. 创建客户端
	client = &http.Client{
		Timeout:   time.Second * 30, // 请求超时时间
		Transport: transport,
	}

	// 3. 请求数据
	if rsp, err = client.Get("http://127.0.0.1:8989/bye"); err != nil {
		panic(err)
	}
	defer rsp.Body.Close()

	// 4. 读取内容
	if bds, err = ioutil.ReadAll(rsp.Body); err != nil {
		panic(err)
	}

	log.Println(string(bds))
}

net包阅读(有一定难度,建议看完如Gin等库实现后再看)

网络代理

网络代理和网络转发的区别

网络代理

用户不直接连接服务器,网络代理去连接。获取数据后返回给用户。

用户通过代理请求信息,请求通过网络代理完成转发到达目的服务器,目标服务器响应后再通过网络代理回传给用户。

image-20201015191826326

网络转发

是路由器对报文的转发操作,中间可能对数据包修改。

image-20201015191901825

网络代理类型

正向代理

是一种客户端的代理技术,帮助客户端访问无法访问的服务资源,可以隐藏用户真实IP。比如:浏览器web代理、VPN等。

正向代理示例:实现一个简单的浏览器代理

import (
	"io"
	"log"
	"net"
	"net/http"
	"strings"
)

type Proxy struct{}

func main() {
	log.Println("Serve on :8080")
	http.Handle("/", &Proxy{})
	http.ListenAndServe("0.0.0.0:8080", nil)
}

func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	log.Printf("received request method:%s, host:%s, remote:%s\n", req.Method, req.Host, req.RemoteAddr)
	transport := http.DefaultTransport
	// step 1,浅拷贝对象,然后就再新增属性数据
	outReq := new(http.Request)
	*outReq = *req
	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		if prior, ok := outReq.Header["X-Forwarded-For"]; ok {
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
		}
		outReq.Header.Set("X-Forwarded-For", clientIP)
	}

	// step 2, 请求下游
	res, err := transport.RoundTrip(outReq)
	if err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		return
	}

	// step 3, 把下游请求内容返回给上游
	for key, value := range res.Header {
		for _, v := range value {
			rw.Header().Add(key, v)
		}
	}
	rw.WriteHeader(res.StatusCode)
	io.Copy(rw, res.Body)
	res.Body.Close()
}

反向代理

是一种服务端的代理技术,帮助服务器做负载均衡、缓存、提供安全校验等,可以隐藏服务器的真实IP。比如:LVS技术、nginx等

image-20201015194635411

反向代理示例:简单的服务器代理

代理

package main

import (
	"bufio"
	"log"
	"net/http"
	"net/url"
)

var (
	proxyAddr = "http://127.0.0.1:2003"
	port      = "2002"
)

func main() {
	http.HandleFunc("/", handler)
	log.Println("Start serving on port " + port)
	err := http.ListenAndServe(":"+port, nil)
	if err != nil {
		log.Fatal(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	//step 1 解析代理地址,并更改请求体的协议和主机
	proxy, err := url.Parse(proxyAddr)
	r.URL.Scheme = proxy.Scheme
	r.URL.Host = proxy.Host

	//step 2 请求下游
	transport := http.DefaultTransport
	resp, err := transport.RoundTrip(r)
	if err != nil {
		log.Print(err)
		return
	}

	//step 3 把下游请求内容返回给上游
	for k, vv := range resp.Header {
		for _, v := range vv {
			w.Header().Add(k, v)
		}
	}
	defer resp.Body.Close()
	bufio.NewReader(resp.Body).WriteTo(w)
}

服务器

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	rs1 := &RealServer{Addr: "127.0.0.1:2003"}
	rs1.Run()
	rs2 := &RealServer{Addr: "127.0.0.1:2004"}
	rs2.Run()

	//监听关闭信号
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
}

type RealServer struct {
	Addr string
}

func (r *RealServer) Run() {
	log.Println("Starting HTTPServer at " + r.Addr)
	mux := http.NewServeMux()
	mux.HandleFunc("/", r.HelloHandler)
	mux.HandleFunc("/base/error", r.ErrorHandler)
	mux.HandleFunc("/test_http_string/test_http_string/aaa", r.TimeoutHandler)
	server := &http.Server{
		Addr:         r.Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	go func() {
		log.Fatal(server.ListenAndServe())
	}()
}

func (r *RealServer) HelloHandler(w http.ResponseWriter, req *http.Request) {
	//127.0.0.1:8008/abc?sdsdsa=11
	//r.Addr=127.0.0.1:8008
	//req.URL.Path=/abc
	//fmt.Println(req.Host)
	uPath := fmt.Sprintf("http://%s%s\n", r.Addr, req.URL.Path)
	realIP := fmt.Sprintf("RemoteAddr=%s,X-Forwarded-For=%v,X-Real-Ip=%v\n", req.RemoteAddr, req.Header.Get("X-Forwarded-For"), req.Header.Get("X-Real-Ip"))
	header := fmt.Sprintf("headers =%v\n", req.Header)
	io.WriteString(w, uPath)
	io.WriteString(w, realIP)
	io.WriteString(w, header)

}

func (r *RealServer) ErrorHandler(w http.ResponseWriter, req *http.Request) {
	uPath := "error handler"
	w.WriteHeader(500)
	io.WriteString(w, uPath)
}

func (r *RealServer) TimeoutHandler(w http.ResponseWriter, req *http.Request) {
	time.Sleep(6 * time.Second)
	uPath := "timeout handler"
	w.WriteHeader(200)
	io.WriteString(w, uPath)
}

演示

# 未开代理
[~] curl 'http://127.0.0.1:2004'                                      20:02:46
http://127.0.0.1:2004/
RemoteAddr=127.0.0.1:53942,X-Forwarded-For=,X-Real-Ip=
headers =map[Accept:[*/*] User-Agent:[curl/7.64.1]]
[~] curl 'http://127.0.0.1:2003'                                      20:02:53
http://127.0.0.1:2003/
RemoteAddr=127.0.0.1:53943,X-Forwarded-For=,X-Real-Ip=
headers =map[Accept:[*/*] User-Agent:[curl/7.64.1]]

# 2002只代理2003
[~] curl 'http://127.0.0.1:2002'                                      20:02:56
http://127.0.0.1:2003/
RemoteAddr=127.0.0.1:53963,X-Forwarded-For=,X-Real-Ip=
headers =map[Accept:[*/*] Accept-Encoding:[gzip] User-Agent:[curl/7.64.1]]
[~] curl 'http://127.0.0.1:2004'                                      20:03:22
http://127.0.0.1:2004/
RemoteAddr=127.0.0.1:53967,X-Forwarded-For=,X-Real-Ip=
headers =map[Accept:[*/*] User-Agent:[curl/7.64.1]]

HTTP代理

功能:

  1. 错误回调及错误日志处理

  2. 更改代理返回内容

  3. 负载均衡

  4. URL重写

  5. 限流、熔断、降级

  6. 数据统计

  7. 权限认证

ReverseProxy功能点

  1. 支持更改内容

  2. 错误信息回调

  3. 支持自定义负载均衡

  4. URL重写功能

  5. 连接池

  6. 支持Websocket服务(后续介绍)

  7. 支持HTTPS代理(后续介绍)

在net/http/httputil下定义了ReverseProxy

基于ReverseProxy实现简易的HTTP代理

package main

import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
)

var addr = "127.0.0.1:2002"

func main() {
	var (
		rs1  string
		url1 *url.URL
		err1 error
	)
	// IN:
	//	127.0.0.1:2002/xxx
	// OUT:
	//	127.0.0.1:2003/base/xxx
	rs1 = "http://127.0.0.1:2003/base"
	url1, err1 = url.Parse(rs1)
	if err1 != nil {
		log.Println(err1)
	}
	proxy := httputil.NewSingleHostReverseProxy(url1)
	log.Println("Starting HTTPServer at " + addr)
	log.Fatal(http.ListenAndServe(addr, proxy))
}

ReverseProxy更改内容支持

特殊的Header头

"X-Forwarded-For": 标记客户端地址每个方向服务器代理的IP

"X-Real-IP": 我的实际请求的标记

"Connection": 标记连接是关闭、长连接等状态

"TE": 标记传输类型是什么

"Trailer": 允许发送方在消息后面添加的一些源信息

拓展ReverseProxy功能

负载均衡

  1. 随机负载:随机挑选目标服务器IP

  2. 轮询负载:遍历服务器列表

  3. 加权负载:给目标设置访问权重,按权重轮询(参考Nginx的负载均衡)

    加权轮询

  4. 一致性Hash负载:请求固定URL访问指定IP

HTTPS代理

Websocket代理

TCP代理

中间件(可以参考Gin)

洋葱结构的原理

go中间件数据传递

中间件实现

中间件一般都封装在路由上,路由是URL请求分发的的管理器。

基于数据构建中间件,使用灵活,控制方便。基于链表实现不仅复杂,且难以调用。

责任链模式(基于链表构建)

方法切片模式(基于数组构建)

方法组装

  1. 构建中间件路由

  2. 构建URL的中间件方法数组

  3. 使用use方法整合路由与方法数组

image-20201016215511479

方法调用

  1. 构建方法请求逻辑

  2. 封装http.Handler接口与http.Server整合

image-20201016215820640

中间件的意义

  1. 提高复用、隔离业务。将公共逻辑剥离

    image-20201017113106390

  2. 调用清晰、组合随意

    image-20201017123125967

参考

Golang Web入门(3):如何优雅的设计中间件

限流和熔断降级

限流

高并发系统的三大利器:缓存、降级、限流

缓存:提升业务系统访问速度和增大处理容量,为相应业务增加缓存。

降级:当服务器压力剧增时,根据业务策略降级,以此释放服务资源保证业务正常。

限流:通过对并发限速,以达到拒绝服务、排队或等待、降级等处理。

限流原理

  1. 漏桶限流:每次请求时计算桶流量,超过阈值则降级请求。

    image-20201017160329470

  2. 令牌桶限流:每次请求时从令牌桶里取出令牌,取不到则降级请求。

    image-20201017160408401

网关集成限流

熔断降级

熔断器是当依赖的服务已经出现故障时,为了保证自身服务的正常运行不再访问依赖的服务,防止雪崩效应。

降级是当服务器压力剧增时,根据业务策略降级,以此释放服务资源保证业务正常。

熔断器的三个状态

  1. 服务关闭:服务正常,维护失败率统计,达到失败率阈值时,转到开启状态。

  2. 开启状态:调用fallback函数,一段时间后,进入半开启状态。

  3. 半开启状态:尝试恢复服务,失败率高于阈值,进入开启状态,低于阈值,进入关闭状态。

熔断降级原理

image-20201017164146851

网关集成熔断降级

选择go类库:hystrix-go

Websocket

WebSocket协议

image-20201017192553837

请求URL如ws://127.0.0.1:2002/echo,以ws开头表示协议是websocket

Websocket数据传输协议

image-20201017193117906

Connection Header头意义

标记请求发起方与第一代理的状态

决定当前事物完成后,是否关闭网络

  • Connection: keep-alive 不关闭网络

  • Connection: close 关闭网络

  • Connection: Upgrade 协议升级

Websocket代理服务器

HTTP、HTTPS、HTTP2

HTTPS与HTTP的区别

HTTPS是HTTP协议的安全版本,HTTP协议的数据传输是明文的,是不安全的,HTTPS使用了SSL/TLS协议进行了加密处理

HTTP默认端口是80,HTTPS默认端口是443。

HTTPS请求流程:

image-20201017212151871

HTTP2和HTTP

  1. HTTP2采用二进制格式而非文本格式。

  2. HTTP2使用一个连接实现多路复用。

  3. HTTP2压缩了报头,降低了开销。

  4. HTTP2让服务器可以将响应主动“推送”到客户端缓存。

HTTP2和HTTPS的关系

  1. HTTP2代表多路复用的传输协议

  2. HTTPS代表HTTP服务器使用了加密传输

  3. 一个启用HTTPS的服务器不一定使用HTTP2

  4. 但是使用HTTP2的服务器必须启动HTTPS(浏览器强制)

HTTP2的补充

HTTP2设计目标

  1. 大多数情况下的感知延迟要有实质上改进

  2. 解决HTTP1.1中“队首阻塞”问题

  3. 并行操作无需与服务器建立多个连接

  4. 保持HTTP1.1的语义,只是标准拓展非替代

HTTP2基本概念

  1. 流:流是连接中的一个虚拟信道,可以承载双向的消息

  2. 消息:是指逻辑上的HTTP消息,比如请求、响应等

  3. 帧:HTTP2通信的最小单位,没个帧包含帧首部,至少会标识出当前帧所属的流,承载着特定类型的数据。

HTTP2多路复用

HTTP2首部压缩

HTTP2协议磋商

  1. 询问服务器是否支持HTTP2,不支持则将为HTTP1.1

    • 发起带有HTTP2 Upgrade首部的HTTP1.1请求

    • 服务端拒绝升级,通过HTTP1.1返回响应

    • 服务器接受HTTP2升级,切换到新分帧

TCP代理

四层负载均衡和七层负载均衡

路由转发客户端和服务器之间有一次三次握手

反向代理客户端和服务端之间有两次三次握手

四层负载均衡(路由转发)

image-20201018181534845

NAT作用于内核运行的

七层负载均衡(反向代理)

image-20201018181646281

代理是用户程序运行的。代理的数据会进入程序buffer中

gRPC透明代理

数据库表整理与创建

设计原则:数据库三大范式

  1. 列不可再分:如服务名、服务描述等属性不可再进一步拆分了。

  2. 属性完全依赖于主键:服务名依赖于服务ID

  3. 属性直接依赖于主键

E-R图

最后更新于

这有帮助吗?