一直用go編寫tcp、http、websocket伺服器,得空總結一些簡單的正規化,供參考。**在github上都可以看到。
之前用c++寫tcp server,一般兩種模式:
- 1個listener執行緒 + n個processor執行緒
- 通過reuseport機制,n個listener執行緒,tcp包的處理可以直接在listener執行緒中做,也可以起processor執行緒處理。
用go寫tcp的server,做併發簡單多了,因為goroutine比執行緒輕量很多,在一定的併發下,建立以及銷毀乙個goroutine的效能損耗可以不用過多的考慮。所以通常的做法是:乙個listener goroutine,來了新連線就啟動兩個goroutine,乙個負責讀乙個負責寫。至於其他的邏輯處理,可以在讀的goroutine中處理,也可以在其他業務邏輯的goroutine中處理。
我的這個正規化中,先定義個處理tcp包的handler
type messagehandler struct
func (this *messagehandler)waitingforread()
func (this *messagehandler)waitingforwrite()
tcp server的**片段一般如下:
serveraddr, err := net.resolvetcpaddr("tcp", hostandport)
if err != nil
listener, err := net.listentcp("tcp", serveraddr)
if err != nil
log.printf("start tcp server %s\n", hostandport)
for
handler := newmessagehandler(conn)
go handler.waitingforread()
go handler.waitingforwrite()
}}
每乙個新的連線,new乙個messagehandler,然後分別啟動乙個讀和寫的goroutine來等待socket的讀寫事件。
一般我們都要定義好服務端跟客戶端之間的協議,封好應用層的包寫到socket中。但是tcp是流式傳輸位元組流,通過tcp傳輸資料,存在粘包情況,例如:一端write兩個包,另一端某次read,可能read到0.5個包,也有可能read到1.5個包。所以對於tcp的讀,需要判斷什麼時候讀到了完整的應用層包。一般做法有兩個:
- 每乙個應用層的包以特定字串結尾,比如」/r/n/r/n」。
這樣的話,read的時候需要對讀到的每乙個字串做比較,以判斷是否到了結束符。這個字串比較也是不小的開銷。
- 將應用層的包設計成head+body樣式,比如head為固定的2個位元組,表示body的長度。read的時候,先取2個位元組,解析出body的長度,然後再取該長度的位元組流解析出body。
這種做法,read的時候,需要知道約定的訊息格式,**中socket的讀需要跟應用層的協議耦合在一起。但是避免了頻繁的字串比較,所以我們一般都選擇這種做法。
比如我們約定訊息格式如下:
8
162432|
----
----
|---
----
-|--
----
--|-
----
---|
|bodylen
|magic||
----
----
|---
----
-|--
----
--|-
----
---|
|seq||
----
----
|---
----
-|--
----
--|-
----
---|
|body||
|
那read的**如下:
func (this *messagehandler)waitingforread()
if bodylen ==0
needread = int(bodylen) - (endpos - startpos -8)
log.printf("startpos:%d, endpos:%d, bodylen:%d, magic:%d, seq:%d, needread:%d", startpos, endpos, bodylen, magic, seq, needread)
if needread >0 else
startpos += int(bodylen) +8
bodylen =0
needread =0
if startpos == endpos }}
//[注釋4]迴圈處理完後,如果緩衝區中還有剩餘的資料未處理,則挪到緩衝區最左端,避免緩衝區滿無法再讀資料
if startpos < endpos && startpos >0
case syscall.errno(0xb): // try again
log.printf("read need try again\n")
continue
default:
log.printf("read error %s\n", err.error())
goto disconnect}}
disconnect:
//......
}
這裡有幾處可能存在問題:
-[注釋1]的地方,緩衝區應該根據實際設定大一點,粘包情況還是比較頻繁的。
-[注釋2]的地方,這裡用簡單的阻塞read。也可以用io.readfull,每次讀滿一定的位元組,比如先讀8個位元組的head,解析出body長度,再讀出該長度的資料處理,這樣不存在邊界問題。但是跟read相比,可能會多出很多readfull的系統呼叫。因為如果對端資料比較頻繁,read可以一次從緩衝區讀出多個應用層包的資料。
-[注釋3]的地方,要特別注意,slice作為傳參只是傳的引用,緩衝區的資料可能被覆蓋,所以如果handlemessage是在其他goroutine中處理,一定要先把資料copy出去,否則極有可能前乙個包的資料被覆蓋造成解包錯誤。
-[注釋4]的地方,涉及到緩衝區移動資料,太頻繁的readat呼叫可能有效能損耗。可以採用環形緩衝區減少位元組移動,或者在大緩衝區情況下,當緩衝區快到右邊邊界時才挪動資料。
以上幾點有興趣的童鞋可以自己改進。具體**例項如下:tcpserver
client端的**讀寫部分可以參考server部分的**,這裡就不做過多描述。具體**例項如下:tcpclient
go語言網路程式設計 TCP程式設計
一.tcp socket程式設計 go的tcp服務端流程分為三步 1 監聽埠 2 接收客戶端請求連線,返回conn 3 建立goroutine處理請求 乙個例項如下 tcp服務端 package main import fmt net bufio func process conn net.conn...
go語言網路程式設計之tcp
go語言網路程式設計之tcp go語言網路程式設計需要匯入包 net如下 import fmt net 重要函式 func listen net,laddr string listener,error func accept cconn,errerror func read b byte n int...
Go語言 基於TCP的Sockets程式設計
簡介 做乙個簡單的通訊,從服務端建立連線,建立套接字也就是127.0.0.1 1021 我用的是這個,埠號可以自己設定 然後客戶端發起連線到127.0.0.1 1021.從而實現客戶端與服務端之間的通訊 服務端 package main import fmt net 處理連線 func proces...