首頁 > 軟體

Go微服務閘道器的實現

2022-07-11 14:01:52

Go微服務閘道器

從核心原理理解閘道器的本質

閘道器具備的基本功能:

  • 支援多種協定代理:tcp/http/ websocket/grpc
  • 支援多種負載均衡策略:輪詢,權重輪詢,hash一致性輪詢
  • 支援下游的服務發現:主動探測 / 自動服務發現
  • 支援橫向擴充套件: 加機器就能解決高並行

藉助閘道器處理高可用,高並行

  • 限流:請求QPS限制
  • 熔斷:錯誤率達閾值則服務熔斷
  • 降級:確保核心業務可用
  • 許可權認證:請求攔截

網路基礎大綱

OSI七層網路協定

經典協定與封包

http 協定

GET/HTTP/1.1
Host:www.baidu.com
User-Agent:curl/7.55.1
Accept:*/*

Websocket握手協定

三次握手 與 四次揮手

三次握手的最主要的目的是保證連線是全雙工的,可靠更多的是通過重傳機制來保證的

因為連線是全雙工的,雙方必須都收到對方的FIN包及確認才可關閉

TCP報文格式:

其中比較重要的欄位有:

(1)序號(sequence number):Seq序號,佔32位元,用來標識從TCP源端向目的端傳送的位元組流,發起方傳送資料時對此進行標記。

(2)確認號(acknowledgement number):Ack序號,佔32位元,只有ACK標誌位為1時,確認序號欄位才有效,Ack=Seq+1。

(3)標誌位(Flags):共6個,即URG、ACK、PSH、RST、SYN、FIN等。具體含義如下:

URG:緊急指標(urgent pointer)有效。ACK:確認序號有效。PSH:接收方應該儘快將這個報文交給應用層。RST:重置連線。SYN:發起一個新連線。FIN:釋放一個連線。

需要注意的是:

不要將確認序號Ack與標誌位中的ACK搞混了。確認方Ack=發起方Seq+1,兩端配對。

三次握手連線:

(1)首先使用者端向伺服器端傳送一段TCP報文,其中:

  • 標記位為`SYN,表示“請求建立新連線”;
  • 序號為Seq=X(X一般為1);
  • 隨後使用者端進入SYN-SENT階段。

(2)伺服器端接收到來自使用者端的TCP報文之後,結束LISTEN階段。並返回一段TCP報文,其中:

  • 標誌位為SYN和ACK,表示“確認使用者端的報文Seq序號有效,伺服器能正常接收使用者端傳送的資料,並同意建立新連線”(即告訴使用者端,伺服器收到了你的資料);
  • 序號為Seq=y;
  • 確認號為Ack=x+1,表示收到使用者端的序號Seq並將其值加1作為自己確認號Ack的值;隨後伺服器端進入SYN-RCVD階段。

(3)使用者端接收到來自伺服器端的確認收到資料的TCP報文之後,明確了從使用者端到伺服器的資料傳輸是正常的,結束SYN-SENT階段。並返回最後一段TCP報文。其中:

  • 標誌位為ACK,表示“確認收到伺服器端同意連線的訊號”(即告訴伺服器,我知道你收到我發的資料了);
  • 序號為Seq=x+1,表示收到伺服器端的確認號Ack,並將其值作為自己的序號值;
  • 確認號為Ack=y+1,表示收到伺服器端序號Seq,並將其值加1作為自己的確認號Ack的值;
  • 隨後使用者端進入ESTABLISHED階段。

伺服器收到來自使用者端的“確認收到伺服器資料”的TCP報文之後,明確了從伺服器到使用者端的資料傳輸是正常的。結束SYN-SENT階段,進入ESTABLISHED階段。

在使用者端與伺服器端傳輸的TCP報文中,雙方的確認號Ack和序號Seq的值,都是在彼此Ack和Seq值的基礎上進行計算的,這樣做保證了TCP報文傳輸的連貫性。一旦出現某一方發出的TCP報文丟失,便無法繼續"握手",以此確保了"三次握手"的順利完成。

四次揮手:

(1)首先使用者端想要釋放連線,向伺服器端傳送一段TCP報文,其中:

  • 標記位為FIN,表示“請求釋放連線“;
  • 序號為Seq=U;
  • 隨後使用者端進入FIN-WAIT-1階段,即半關閉階段。並且停止在使用者端到伺服器端方向上傳送資料,但是使用者端仍然能接收從伺服器端傳輸過來的資料。注意:這裡不傳送的是正常連線時傳輸的資料(非確認報文),而不是一切資料,所以使用者端仍然能傳送ACK確認報文。

(2)伺服器端接收到從使用者端發出的TCP報文之後,確認了使用者端想要釋放連線,隨後伺服器端結束ESTABLISHED階段,進入CLOSE-WAIT階段(半關閉狀態)並返回一段TCP報文,其中:

  • 標記位為ACK,表示“接收到使用者端傳送的釋放連線的請求”;
  • 序號為Seq=V;
  • 確認號為Ack=U+1,表示是在收到使用者端報文的基礎上,將其序號Seq值加1作為本段報文確認號Ack的值;
  • 隨後伺服器端開始準備釋放伺服器端到使用者端方向上的連線。使用者端收到從伺服器端發出的TCP報文之後,確認了伺服器收到了使用者端發出的釋放連線請求,隨後使用者端結束FIN-WAIT-1階段,進入FIN-WAIT-2階段

前"兩次揮手"既讓伺服器端知道了使用者端想要釋放連線,也讓使用者端知道了伺服器端了解了自己想要釋放連線的請求。於是,可以確認關閉使用者端到伺服器端方向上的連線了

(3)伺服器端自從發出ACK確認報文之後,經過CLOSED-WAIT階段,做好了釋放伺服器端到使用者端方向上的連線準備,再次向用戶端發出一段TCP報文,其中:

  • 標記位為FIN,ACK,表示“已經準備好釋放連線了”。注意:這裡的ACK並不是確認收到伺服器端報文的確認報文。
  • 序號為Seq=W;
  • 確認號為Ack=U+1;表示是在收到使用者端報文的基礎上,將其序號Seq值加1作為本段報文確認號Ack的值。

隨後伺服器端結束CLOSE-WAIT階段,進入LAST-ACK階段。並且停止在伺服器端到使用者端的方向上傳送資料,但是伺服器端仍然能夠接收從使用者端傳輸過來的資料。

(4)使用者端收到從伺服器端發出的TCP報文,確認了伺服器端已做好釋放連線的準備,結束FIN-WAIT-2階段,進入TIME-WAIT階段,並向伺服器端傳送一段報文,其中:

  • 標記位為ACK,表示“接收到伺服器準備好釋放連線的訊號”。
  • 序號為Seq=U+1;表示是在收到了伺服器端報文的基礎上,將其確認號Ack值作為本段報文序號的值。
  • 確認號為Ack=W+1;表示是在收到了伺服器端報文的基礎上,將其序號Seq值作為本段報文確認號的值。隨後使用者端開始在TIME-WAIT階段等待2MSL

為什麼要使用者端要等待2MSL呢?見後文。

伺服器端收到從使用者端發出的TCP報文之後結束LAST-ACK階段,進入CLOSED階段。由此正式確認關閉伺服器端到使用者端方向上的連線。

使用者端等待完2MSL之後,結束TIME-WAIT階段,進入CLOSED階段,由此完成“四次揮手”。

後“兩次揮手”既讓使用者端知道了伺服器端準備好釋放連線了,也讓伺服器端知道了使用者端了解了自己準備好釋放連線了。於是,可以確認關閉伺服器端到使用者端方向上的連線了,由此完成“四次揮手”。

與“三次揮手”一樣,在使用者端與伺服器端傳輸的TCP報文中,雙方的確認號Ack和序號Seq的值,都是在彼此Ack和Seq值的基礎上進行計算的,這樣做保證了TCP報文傳輸的連貫性,一旦出現某一方發出的TCP報文丟失,便無法繼續"揮手",以此確保了"四次揮手"的順利完成。

為什麼使用者端在TIME-WAIT階段要等2MSL?

  • 為的是確認伺服器端是否收到使用者端發出的ACK確認報文
  • 保證TCP協定的全雙共連線能夠可靠關閉
  • 保證這次連線的重複資料段從網路中消失

當用戶端發出最後的ACK確認報文時,並不能確定伺服器端能夠收到該段報文。所以使用者端在傳送完ACK確認報文之後,會設定一個時長為2MSL的計時器。MSL指的是(最大的生命週期)Maximum Segment Lifetime:(30秒–1分鐘)一段TCP報文在傳輸過程中的最大生命週期。2MSL即是伺服器端發出為FIN報文和使用者端發出的ACK確認報文所能保持有效的最大時長。

伺服器端在1MSL內沒有收到使用者端發出的ACK確認報文,就會再次向用戶端發出FIN報文;

如果使用者端在2MSL內,再次收到了來自伺服器端的FIN報文,說明伺服器端由於各種原因沒有接收到使用者端發出的ACK確認報文。使用者端再次向伺服器端發出ACK確認報文,計時器重置,重新開始2MSL的計時;否則使用者端在2MSL內沒有再次收到來自伺服器端的FIN報文,說明伺服器端正常接收了ACK確認報文,使用者端可以進入CLOSED階段,完成“四次揮手”。

所以,使用者端要經歷時長為2SML的TIME-WAIT階段;這也是為什麼使用者端比伺服器端晚進入CLOSED階段的原因

為啥會出現大量的close_wait

  • 首先close_wait一般出現在被動關閉方
  • 並行請求太多導致
  • 被動關閉方未及時釋放埠資源導致
func main() {
	//1、監聽埠
	listener, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("listen fail, err: %vn", err)
		return
	}
	//2.建立通訊端連線
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept fail, err: %vn", err)
			continue
		}
		//3. 建立處理協程
		go func(conn net.Conn) {
			defer conn.Close() //思考題:這裡不填寫會有啥問題?
            //伺服器端就有一個close,wait狀態,使用者端就有一個finally 狀態
			for {
				var buf [128]byte
				n, err := conn.Read(buf[:])
				if err != nil {
					fmt.Printf("read from connect failed, err: %vn", err)
					break
				}
				str := string(buf[:n])
				fmt.Printf("receive from client, data: %vn", str)
			}
		}(conn)
	}
}

TCP為啥需要流量控制

  • 由於通訊雙方,網速不同。通訊方任一方傳送過快都會導致對方的訊息處理不過來,所以就需要資料放到緩衝區中
  • 如果緩衝區滿了,傳送方還在瘋狂傳送,那接收方只能把封包丟棄,因此我們需要控制傳送速率
  • 我們緩衝區剩餘大小稱之為接收視窗,用變數win表示,如果win=0,則傳送方停止傳送

TCP 為啥需要擁塞控制

  • 流量控制與擁塞控制是兩個概念,擁塞控制是調節網路的負載
  • 接收方網路資源繁忙,因未及時響應ACK導致傳送方重傳大量的資料,這樣將會導致網路更加的擁堵
  • 擁塞控制是動態調整win大小,不只是依賴緩衝區大小去確定視窗大小

TCP 擁塞控制

  • 慢開始和擁塞避免
  • 快速重傳和快速恢復

優化步驟3到步驟4:因為網路擁塞,有24直接降到1 ,會造成堵塞

為啥會出現粘包,拆包,如何處理

粘包、拆包表現形式

現在假設使用者端向伺服器端連續傳送了兩個封包,用packet1和packet2來表示,那麼伺服器端收到的資料可以分為三種,現列舉如下:

第一種情況,接收端正常收到兩個封包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論範圍內。

第二種情況,接收端只收到一個封包,由於TCP是不會出現丟包的,所以這一個封包中包含了傳送端傳送的兩個封包的資訊,這種現象即為粘包。這種情況由於接收端不知道這兩個封包的界限,所以對於接收端來說很難處理。

第三種情況,這種情況有兩種表現形式,如下圖。接收端收到了兩個封包,但是這兩個封包要麼是不完整的,要麼就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。

產生tcp粘包和拆包的原因

我們知道tcp是以流動的方式傳輸資料,傳輸的最小單位為一個報文段(segment)。tcp Header中有個Options標識位,常見的標識為mss(Maximum Segment Size)指的是,連線層每次傳輸的資料有個最大限制MTU(Maximum Transmission Unit),一般是1500位元,超過這個量要分成多個報文段,mss則是這個最大限制減去TCP的header,光是要傳輸的資料的大小,一般為1460位元。換算成位元組,也就是180多位元組。

tcp為提高效能,傳送端會將需要傳送的資料傳送到緩衝區,等待緩衝區滿了之後,再將緩衝中的資料傳送到接收方。同理,接收方也有緩衝區這樣的機制,來接收資料。

發生TCP粘包、拆包主要是由於下面一些原因:

  • 應用程式寫入的資料大於通訊端緩衝區大小,這將會發生拆包。
  • 應用程式寫入資料小於通訊端緩衝區大小,網路卡將應用多次寫入的資料傳送到網路上,這將會發生粘包。
  • 進行mss(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>mss的時候將發生拆包。
  • 接收方法不及時讀取通訊端(socket)緩衝區資料,這將發生粘包。

如何解決拆包粘包

既然知道了tcp是無界的資料流,且協定本身無法避免粘包,拆包的發生,那我們只能在應用層資料協定上,加以控制。通常在制定傳輸資料時,可以使用如下方法:

  • 使用帶訊息頭的協定、訊息頭儲存訊息開始標識及訊息長度資訊,伺服器端獲取訊息頭的時候解析出訊息長度,然後向後讀取該長度的內容。
  • 設定定長訊息,伺服器端每次讀取既定長度的內容作為一條完整訊息。
  • 設定訊息邊界,伺服器端從網路流中按訊息編輯分離出訊息內容。

如何獲取完整應用資料包文

  • 使用帶訊息頭的協定,頭部寫入包長度,然後在讀取包內容
  • 設定定長訊息,每次讀取定長內容,長度不夠時空位補固定字元
  • 設定訊息邊界,伺服器端從網路流中按訊息邊界分離出訊息內容,一般使用 ‘n’
  • 更為複雜的協定:json,protobuf

如何獲取完整的資料包文

func main() {
	//類比接收緩衝區
	bytesBuffer := bytes.NewBuffer([]byte{})
	// 傳送
	if err := Encode(bytesBuffer, "hello world 0 !!"); err != nil {
		panic(err)
	}
	if err := Encode(bytesBuffer, "hello world 1 !!"); err != nil {
		panic(err)
	}
	//讀取
	for {
		if bt, err := Decode(bytesBuffer); err == nil {
			fmt.Println(string(bt))
			continue
		}
		break
	}
}

如何獲取完整的資料包文

tcp_server

func main() {
	//simple tcp server
	//1.監聽埠
	listener, err := net.Listen("tcp", "127.0.0.1:9090")
	if err != nil {
		fmt.Printf("tcp Listen fail,err: %vn", err)
		return
	}
	//2.接受請求
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("tcp Accept fail,err: %vn", err)
			continue
		}
		//3.建立協程
		go process(conn)
	}
}

//4.建立的協程裡面實現解碼的功能
func process(conn net.Conn) {
	defer conn.Close()
	for {
		bt, err := unpack.Decode(conn)
		if err != nil {
			fmt.Printf("read from connect failed, err: %vn", err)
			break
		}
		str := string(bt)
		fmt.Printf("receive from client, data: %vn", str)
	}
}

tcp_client

func main() {
	//1.連線tcp伺服器
	conn, err := net.Dial("tcp", "localhost:9090")
	defer conn.Close()
	if err != nil {
		fmt.Printf("connect failed, err : %vn", err.Error())
		return
	}
	//2.實現編碼
	unpack.Encode(conn, "hello world 0!!!")
}

**unpack ** : 實現編碼(encode)和解碼(docode)功能

const Msg_Header = "12345678"
// 編碼
func Encode(bytesBuffer io.Writer, content string) error {
	//msg_header+content_len+content
	//8+4+content_len
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(Msg_Header)); err != nil {
		return err
	}
	clen := int32(len([]byte(content)))
    //  binary.BigEndian 大端位元組實現的加密 , 
	if err := binary.Write(bytesBuffer, binary.BigEndian, clen); err != nil {
		return err
	}
	if err := binary.Write(bytesBuffer, binary.BigEndian, []byte(content)); err != nil {
		return err
	}
	return nil
}
// 解碼
func Decode(bytesBuffer io.Reader) (bodyBuf []byte, err error) {
	MagicBuf := make([]byte, len(Msg_Header))
    //先讀取header的大小
	if _, err = io.ReadFull(bytesBuffer, MagicBuf); err != nil {
		return nil, err
	}
	//比較得到的header和實際的Msg_Header 是否相同
	if string(MagicBuf) != Msg_Header {
		return nil, errors.New("msg_header error")
	}

	lengthBuf := make([]byte, 4)
	if _, err = io.ReadFull(bytesBuffer, lengthBuf); err != nil {
		return nil, err
	}
    //  binary.BigEndian 大端位元組實現的解密 ,得到實際資料的長度
	length := binary.BigEndian.Uint32(lengthBuf)
	bodyBuf = make([]byte, length)
	if _, err = io.ReadFull(bytesBuffer, bodyBuf); err != nil {
		return nil, err
	}
	return bodyBuf, err
}

基於golang 實現TCP,UDP,Http伺服器端與使用者端

golang 實現UDP 伺服器端與使用者端

UDP伺服器端:

func main() {
	//1.監聽埠
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 9090,
	})
	if err != nil {
		fmt.Printf("listen udp failed ,err:%vn", err)
		return
	}
	//2.迴圈讀取訊息內容
	for {
		var data [1024]byte
		n, addr, err := listen.ReadFromUDP(data[:])
		if err != nil {
			fmt.Printf("read failed from addr :%v,err%vn", addr, err)
			break
		}
		go func() {
			//3.回覆資料
			fmt.Printf("addr:%v data:%v count:%vn", addr, string(data[:n]), n)
			_, err = listen.WriteToUDP([]byte("received success!"), addr)
			if err != nil {
				fmt.Printf("write failed,err :%vn", err)
				return
			}
		}()
	}
}

udp使用者端

func main() {
	//1. 連線udp伺服器
	conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 9090,
	})
	if err != nil {
		fmt.Printf("connect failed ,err %vn", err)
		return
	}
	for i := 0; i < 100; i++ {
		// 2.傳送資料
		_, err := conn.Write([]byte("hello " +
			"server"))
		if err != nil {
			fmt.Printf("send data failed,err: %vn", err)
			return
		}
		// 3. 接收資料
		result := make([]byte, 1024)
		n, remoteAddr, err := conn.ReadFromUDP(result)
		if err != nil {
			fmt.Printf("read data failed,err:%vn", err)
			return
		}
		fmt.Printf("receive from addr:%v data:%vn", remoteAddr, string(result[:n]))
	}
}

golang實現tcp的伺服器端和使用者端

tcp 伺服器端

func main() {
	//1、監聽埠
	listener, err := net.Listen("tcp", "0.0.0.0:9090")
	if err != nil {
		fmt.Printf("listen fail, err: %vn", err)
		return
	}

	//2.建立通訊端連線
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept fail, err: %vn", err)
			continue
		}

		//3. 建立處理協程
		go process(conn)
	}	
}

func process(conn net.Conn) {
	defer conn.Close()	//思考題:這裡不填寫會有啥問題?
	for {
		var buf [128]byte
		n, err := conn.Read(buf[:])

		if err != nil {
			fmt.Printf("read from connect failed, err: %vn", err)
			break
		}
		str := string(buf[:n])
            fmt.Printf(" from client, data: %vn", str)
	}
}

tcp使用者端

golang實現Http的伺服器端和使用者端

http伺服器端

var (
	Addr = ":8000"
)

// http的伺服器
func main() {
	//1.建立路由器
	mux := http.NewServeMux()
	// 2. 設定路由規則
	mux.HandleFunc("/bye", sayBye)

	// 3.建立伺服器
	server := &http.Server{
		Addr:         Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	// 4. 監聽埠並提供服務
	log.Println("starting httpServer at" + Addr)
	log.Fatal(server.ListenAndServe())
}
func sayBye(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)
	w.Write([]byte("bye bye,this is httpserver"))

}

http使用者端

func main() {
	//1. 建立連線池
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 超時時間
			KeepAlive: 30 * time.Second, //長連線時間
		}).DialContext,
		MaxIdleConns:          100,              //最大空閒連線數
		IdleConnTimeout:       90 * time.Second, // 空閒超時時間
		TLSHandshakeTimeout:   10 * time.Second, // tls握手超時時間
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue 狀態碼超時時間
	}
	//2. 建立使用者端
	client := &http.Client{
		Timeout:   30 * time.Second,
		Transport: transport,
	}
	//3.請求資料
	resp, err := client.Get("http://127.0.0.1:8000/bye")
	if err != nil {
		fmt.Println("client get url failed ", err)
		return
	}
	defer resp.Body.Close()
	//4.讀取內容
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Read body failed ", err)
		return
	}
	fmt.Println(string(b))

}

Http 伺服器原始碼解讀

  • 閱讀原始碼的原則:先整體在區域性,先看腦圖在逐一分析
  • 註冊路由:理解函數是一等公民以及註冊原理
  • 開啟服務
  • 處理連線

函數是一等公民

type HandleFunc func(http.ResponseWriter, *http.Request)

func (f HandleFunc) ServerHTTP(w http.ResponseWriter, r *http.Request) {
	f(w, r)
}

//函數是一等公民
func main() {
	hf := HandleFunc(HelloHandler)
	resp := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/", bytes.NewBuffer([]byte("test")))

	hf.ServerHTTP(resp, req)
	b, _ := ioutil.ReadAll(resp.Body)
	fmt.Println(string(b))
}
func HelloHandler(res http.ResponseWriter, req *http.Request) {
	res.Write([]byte("hello youMe "))
}

到此這篇關於Go微服務閘道器的實現的文章就介紹到這了,更多相關Go微服務閘道器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com