golang中怎麼處理socket長連接?

我是golang新手,看了下goroutine+channel,我認為很適合伺服器端並發編程。
我在網上看到的demo都是千篇一律的accept到一個client fd後,開一個goroutine處理client fd。是一問一答式的,client發來一個請求,server回應,都是在一個goroutine裡面處理的。

但是伺服器應該是可以主動push message的。我想的事為每個client fd開兩個goroutine,一個recv,一個send。同時還有加2個channel,一個用於recv routine向邏輯主線程傳送收到的數據,一個用於邏輯主線程向send goroutine傳送待發送的數據,是這樣的么?

還有,如果我開2個goroutine的話,client斷開連接了,假設recv goroutine先發生err並且close(fd),那在send goroutine中該如何處理呢?有可能不應該這樣處理,那應該怎麼處理呢?


GitHub - davyxu/cellnet: 簡單,方便,高效的Go語言的遊戲伺服器底層
直接看答案吧


我想的事為每個client fd開兩個goroutine,一個recv,一個send。同時還有加2個channel,一個用於recv routine向邏輯主線程傳送收到的數據,一個用於邏輯主線程向send goroutine傳送待發送的數據,是這樣的么?

實際上需要 3 個 goroutine,一個 read,一個 send,還有一個 handle。

read goroutine 讀,然後寫入 recevice chan。

write goroutine 把 send chan 的東西寫。

handle goroutine 是 conn 的主要處理邏輯,負責把 recevice chan 的東西讀出來 call 業務邏輯。

業務邏輯中要寫數據就直接寫入 send chan。

這樣就可以保證,業務邏輯的讀寫都是在 handle goroutine 上處理,而避免 race 產生。

如果需要定時任務(比如心跳),就在 handle goroutine 上加上一個 timer.C;

如果需要 goroutine 下發任務,在 handle goroutine 增加一個 task chan,hanlde 收到 task 後處理業務;

如果需要輸出結果,那就增加 result chan,業務邏輯把數據輸出即可。

----------------------------

還有,如果我開2個goroutine的話,client斷開連接了,假設recv goroutine先發生err並且close(fd),那在send goroutine中該如何處理呢?有可能不應該這樣處理,那應該怎麼處理呢?

如果 net.Conn Close() 了,不論 Read() 阻塞還是 Write() 阻塞都會立即收到 err 返回。

一般來說,Write() 是不可能主動知道連接斷開的,除非是 SetDeadline() 猜測對方斷掉了,指定時間內沒有寫成功就認為是斷開。Read() 是可以主動收到對方發來的斷開(TCP FIN),但也沒辦法知道異常的斷開(當然也可以設置超時)。

無論是誰,是確實收到 FIN 還是 Deadline 猜測斷開,只要 Close() 大家就知道連接斷開了。

handle goroutine 還有一個用處就是:你的程序主動結束的時候,能正確的 close conn,讓對方知道你是真的斷開了,而不用去猜。

----------------------------

補充閱讀:

淺談TCP/IP網路編程中socket的行為
Go 語言使用 TCP keepalive


如果要主動推送的話應該是不夠的,因為我們會有一個goroutine阻塞在讀取client的request上,而且為了高效處理,收到一個請求可以開一個goroutine去處理,該goroutine處理完之後可以直接發送(但是這樣的話可能會有多個goroutine同時發送,所以要加鎖),或者發送處理結果給一個channel,某個goroutine阻塞在該channel上,一收到數據就可以發送給client。但是如果client需要等到一個請求回包才會再次發送的話,也不需要開多個goroutine處理。 至於第二個問題,可以close channel,那麼阻塞在channel上讀取的goroutine就會出錯,可以退出。


@南靖男 的回答已經很好了,我們項目里用的有點不同,每個client fd只開兩個goroutine:writeRoutine、readRoutine。

作為兩個後台線程:
writeRoutine平常阻塞在&<-tcpConn.writeChan讀數據這裡(邏輯線程通過SendMsg介面往tcpConn.writeChan &<- buf寫數據),chan有數據即取出寫入tcpConn;
readRoutine則阻塞在io.Read處,有數據到來即被喚醒,處理粘包、分發。

消息分發處理上沒有再加handle goroutine,直接是readRoutine調msgDispatcher,主要是避免開過多goroutine,且HandleMsgFunc中並無耗時或阻塞操作,readRoutine能夠很快處理完,不會影響tcp數據接收(readRoutine里的io.Read用的bufio緩衝,消息響應函數若有耗時/io操作會單獨開goroutine)

至於關閉的問題,我們都是通過readRoutine觸發的,設置tcpConn.conn.SetReadDeadline,超時/讀取報錯,即結束readRoutine的循環,再開啟關閉流程:
(1)向writeChan中寫入一個nil buf,觸發writeRoutine結束;
(2)tcpConn.conn.Close()
(3)設置closeflag
(4)調用註冊onNetClose函數指針,裡面清理些邏輯相關的東東,比如連接管理啥的

代碼參考:Sundry/tcp_conn.go at master · 3workman/Sundry · GitHub


https://github.com/qianlnk/superservice/blob/master/longsocket/longsocket.go
說那麼多太無力,直接看代碼


someonegg/bdmsg · GitHub
看下這個有沒有幫助


一個go肉體為什麼不能往下push?
只要push的觸發方能找到對應conn,還不能write?
當然了你單機並發連接要求低的話可以開多個routine……


如何recv這邊close了,send這邊肯定也是需要退出的。
覺得可以這樣處理,client定義一個exitChan chan bool變數,send和recv gorouting遇到錯誤時,close它,並且send和recv gorouting都同時select,接收到信號時,退出
send gorouting
for {
select {
case data := client.sendChan:
//process
if err != nil {
close(client.exitChan)
goto end
}
case &<- client.exitChan:
goto end
}
}
end:
client.Lock()
if client.exitChan != nil {
close(client.exitChan)
client.exitChan = nil
}
client.Unlock()
===================================================================
recevive gorouting
for {
select {
case client.exitChan:
goto end
}
buf, err := conn.Read()
if err != nil {
close(client.exitChan)
}
}
end:
client.Lock()
if client.exitChan != nil {
close(client.exitChan)
client.exitChan = nil
}
client.Unlock()


我想問你用的什麼應用協議?http?還是其他?如果是http的話,client和server要互相通信用websocket就挺好的,如果對協議沒要求,可以考慮mqtt


推薦閱讀:

TAG:長連接 | Socket | Go語言 |