之前學習HTTP和TCP請求的時候經常看到一個名詞就是長串連,之前一直很好奇怎麼去實現,最近偶爾看到一篇文章寫的IM系統,想轉載學習一下。
IM系統,那麼必然需要TCP長串連來維持,由於Golang本身的基礎庫和外部依賴庫非常之多,我們可以簡單引用基礎net網路程式庫,來建立TCP server。一般的TCP Server端的模型,可以有一個協程【或者線程】去獨立執行accept,並且是for迴圈一直accept新的串連,如果有新串連過來,那麼建立串連並且執行Connect,由於Golang裡面協程的開銷非常之小,因此,TCP server端還可以一個串連一個goroutine去迴圈讀取各自串連鏈路上的資料並處理。當然, 這個在C++語言的TCP Server模型中,一般會通過EPoll模型來建立server端,這個是和C++的區別之處。
關於讀取資料,Linux系統有recv和send函數來讀取發送資料,在Golang中,內建有io庫,裡面封裝了各種讀寫方法,如io.ReadFull,它會讀取指定位元組長度的資料
為了維護串連和使用者,並且一個串連一個使用者的一一對應的,需要根據串連能夠找到使用者,同時也需要能夠根據使用者找到對應的串連,那麼就需要設計一個很好結構來維護。我們最初採用map來管理,但是發現Map裡面的資料太大,尋找的效能不高,為此,最佳化了資料結構,conn裡麵包含user,user裡麵包含conn,結構如下【只包括重要欄位】。
// 一個使用者對應一個串連type User struct { uid int64 conn *MsgConn BKicked bool // 被另外登陸的一方踢下線 BHeartBeatTimeout bool // 心跳逾時}type MsgConn struct { conn net.Conn lastTick time.Time // 上次接收到包時間 remoteAddr string // 為每個串連建立一個唯一識別碼 user *User // MsgConn與User一一映射}
建立TCP server 程式碼片段如下
func ListenAndServe(network, address string) { tcpAddr, err := net.ResolveTCPAddr(network, address) if err != nil { logger.Fatalf(nil, "ResolveTcpAddr err:%v", err) } listener, err = net.ListenTCP(network, tcpAddr) if err != nil { logger.Fatalf(nil, "ListenTCP err:%v", err) } go accept()}func accept() { for { conn, err := listener.AcceptTCP() if err == nil { // 包計數,用來限制頻率 //anti-attack, 黑白名單 // 建立一個串連 imconn := NewMsgConn(conn) // run imconn.Run() } }}func (conn *MsgConn) Run() { //on connect conn.onConnect() go func() { tickerRecv := time.NewTicker(time.Second * time.Duration(rateStatInterval)) for { select { case <-conn.stopChan: tickerRecv.Stop() return case <-tickerRecv.C: conn.packetsRecv = 0 default: // 在 conn.parseAndHandlePdu 裡面通過Golang本身的io庫裡面提供的方法讀取資料,如io.ReadFull conn_closed := conn.parseAndHandlePdu() if conn_closed { tickerRecv.Stop() return } } } }()}// 將 user 和 conn 一一對應起來func (conn *MsgConn) onConnect() *User { user := &User{conn: conn, durationLevel: 0, startTime: time.Now(), ackWaitMsgIdSet: make(map[int64]struct{})} conn.user = user return user}