架构学习之路(四)--|架构学习之路(四)-- IM系统初探

之前学习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 { uidint64 conn*MsgConn BKickedbool // 被另外登陆的一方踢下线 BHeartBeatTimeout bool // 心跳超时 }type MsgConn struct { connnet.Conn lastTicktime.Time // 上次接收到包时间 remoteAddr string// 为每个连接创建一个唯一标识符 user*User// MsgConn与User一一映射 }

【架构学习之路(四)--|架构学习之路(四)-- IM系统初探】建立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 }

    推荐阅读