GO语言TCP编程范式

一直用go编写TCP、HTTP、websocket服务器,得空总结一些简单的范式,供参考。代码在github上都可以看到。

1、TCP server

之前用c++写TCP server,一般两种模式:
- 1个listener线程 + N个processor线程
- 通过REUSEPORT机制,N个listener线程,TCP包的处理可以直接在listener线程中做,也可以起processor线程处理。

用go写TCP的server,做并发简单多了,因为goroutine比线程轻量很多,在一定的并发下,创建以及销毁一个goroutine的性能损耗可以不用过多的考虑。所以通常的做法是:一个listener goroutine,来了新连接就启动两个goroutine,一个负责读一个负责写。 至于其他的逻辑处理,可以在读的goroutine中处理,也可以在其他业务逻辑的goroutine中处理。

我的这个范式中,先定义个处理TCP包的handler

type MessageHandler struct {

    conn net.Conn 
    ExitCmd chan bool
    writeChan chan []byte
}

func (this *MessageHandler)WaitingForRead(){
    ......
}

func (this *MessageHandler)WaitingForWrite(){
    ......
}

tcp server的代码片段一般如下:

serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
    if err != nil {
        log.Printf("Resolving %s failed: %s\n", hostAndPort, err.Error())
        return err
    }

    listener, err := net.ListenTCP("tcp", serverAddr)
    if err != nil {
        log.Printf("listen %s failed: %s", hostAndPort, err.Error())
        return err
    }
    log.Printf("start tcp server %s\n", hostAndPort)
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("accept error: %s , close this socket and exit\n", err.Error())
            listener.Close()
            return err
        }
        handler := NewMessageHandler(conn)
        //this.clientList = append(this.clientMap, handler)

        go handler.WaitingForRead()
        go handler.WaitingForWrite()
    }
}

每一个新的连接,new一个MessageHandler,然后分别启动一个读和写的goroutine来等待socket的读写事件。
一般我们都要定义好服务端跟客户端之间的协议,封好应用层的包写到socket中。但是TCP是流式传输字节流,通过TCP传输数据,存在粘包情况,例如:一端write两个包,另一端某次read,可能read到0.5个包,也有可能read到1.5个包。所以对于tcp的读,需要判断什么时候读到了完整的应用层包。一般做法有两个:
- 每一个应用层的包以特定字符串结尾,比如”/r/n/r/n”。
这样的话,read的时候需要对读到的每一个字符串做比较,以判断是否到了结束符。这个字符串比较也是不小的开销。
- 将应用层的包设计成head+body样式,比如head为固定的2个字节,表示body的长度。read的时候,先取2个字节,解析出body的长度,然后再取该长度的字节流解析出body。
这种做法,read的时候,需要知道约定的消息格式,代码中socket的读需要跟应用层的协议耦合在一起。但是避免了频繁的字符串比较,所以我们一般都选择这种做法。

比如我们约定消息格式如下:

         8        16       24       32 
|--------|--------|--------|--------|
|      bodyLen    |        magic    |
|--------|--------|--------|--------|
|                seq                |   
|--------|--------|--------|--------|
|                 body              |
|                                   |

那read的代码如下:

func (this *MessageHandler)WaitingForRead(){
    //......
    var ibuf []byte = make([]byte, 1024)//[注释1] 1024字节大小的缓冲区

    var needRead int = 1024
    var bodyLen uint16 = 0
    var endPos int = 0
    var startPos int = 0
    var magic uint16 = 0
    var seq uint32 = 0
    for {
        //[注释2]开始等待读,每次读到缓冲区endPos指定的位置
        length, err := this.conn.Read(ibuf[endPos:])
        log.Printf("read data: %d\n", length)
        switch err {
        case nil:
            endPos += length
            //有可能一次读到了多个应用层的包,所以要循环处理
            for {
                if endPos-startPos < 8 {
                    break
                }
                if bodyLen == 0 {
                    bodyLen = binary.BigEndian.Uint16(ibuf[startPos : startPos+2])
                    magic = binary.BigEndian.Uint16(ibuf[startPos+2 : startPos+4])
                    seq = binary.BigEndian.Uint32(ibuf[startPos+4 : startPos+8])
                }
                needRead = int(bodyLen) - (endPos - startPos - 8)
                log.Printf("startPos:%d, endPos:%d, bodyLen:%d, magic:%d, seq:%d, needRead:%d", startPos, endPos, bodyLen, magic, seq, needRead)
                if needRead > 0 {
                    break
                } else {
                    //[注释3]读到完整的消息后,处理
                    res := this.handleMsg(8, bodyLen, ibuf[startPos:], magic, seq)
                    if res == -1 {
                        log.Printf("handle msg error, close the connection\n")
                        goto DISCONNECT
                    }
                    startPos += int(bodyLen) + 8
                    bodyLen = 0
                    needRead = 0
                    if startPos == endPos {
                        startPos = 0
                        endPos = 0
                    }
                }
            }
            //[注释4]循环处理完后,如果缓冲区中还有剩余的数据未处理,则挪到缓冲区最左端,避免缓冲区满无法再读数据
            if startPos < endPos && startPos > 0 {
                reader := bytes.NewReader(ibuf)
                reader.ReadAt(ibuf, int64(endPos-startPos))
                startPos = 0
                endPos -= startPos
            }
        case syscall.Errno(0xb): // try again
            log.Printf("read need try again\n")
            continue
        default:
            log.Printf("read error %s\n", err.Error())
            goto DISCONNECT
        }

    }
DISCONNECT:
    //......

}

这里有几处可能存在问题:
- [注释1] 的地方,缓冲区应该根据实际设置大一点,粘包情况还是比较频繁的。
- [注释2] 的地方,这里用简单的阻塞Read。也可以用io.ReadFull,每次读满一定的字节,比如先读8个字节的head,解析出body长度,再读出该长度的数据处理,这样不存在边界问题。但是跟Read相比,可能会多出很多ReadFull的系统调用。因为如果对端数据比较频繁,Read可以一次从缓冲区读出多个应用层包的数据。
- [注释3] 的地方,要特别注意,slice作为传参只是传的引用,缓冲区的数据可能被覆盖,所以如果HandleMessage是在其他goroutine中处理,一定要先把数据copy出去,否则极有可能前一个包的数据被覆盖造成解包错误。
- [注释4] 的地方,涉及到缓冲区移动数据,太频繁的ReadAt调用可能有性能损耗。可以采用环形缓冲区减少字节移动,或者在大缓冲区情况下,当缓冲区快到右边边界时才挪动数据。

以上几点有兴趣的童鞋可以自己改进。具体代码实例如下:tcpserver

2、TCP client

client端的代码读写部分可以参考server部分的代码,这里就不做过多描述。具体代码实例如下:tcpclient

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值