之前学习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
}