架构学习之路(四)-- 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 {
    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
}

你可能感兴趣的:(架构学习之路(四)-- IM系统初探)