> 文档中心 > golang 原生 tcp setsocketopt

golang 原生 tcp setsocketopt

最近新项目上线,自己写了一个测试程序来做压测。

测试刚开始使用小并发的请求没有啥问题,但是加大并发后,发现每次请求到1.6 万笔左右的时候就连接不上服务器了,而监控服务器这边显示 cpu、内存都很正常,所以猜测是客户端这边某些资源到达了瓶颈。

再次发起测试,同时在客户端这边通过 netstat -anpt 命令 查看 tcp网络状态,发现存在大量 time_wait 状态 的连接。问题找到了,因为压测程序这边使用 tcp 短连接,每完成一次请求后就 主动断开 了连接。

我们知道正常调用 close 会经历 tcp 四次挥手,同时主动断开连接的一方会维护 time_wait 状态来保证连接正确的断开,time_wait 状态会持续 2MSL 时间。而每台物理机的端口是有限的,在大并发的情况下,如果端口一直被这些状态的连接占用,就没有端口可以用于新的连接。

问题找到了,怎么解决?

现在问题是客户端这边维持的 time_wait 状态的连接过多,导致端口都被占用无法用于新的请求。

从客户端的角度解决 time_wai 状态过多可以用以下方法:

  • 设置 tcp_tw_reuse 开启端口重用
  • 设置 SO_LINGER 选项 跳过 time_wait状态

想着我这只是一个测试程序,就不去改系统参数了,因此,选择第二种方法。

TCP 提供的 SO_LINGER 选项可以用来改变调用 close 后默认的行为:

  • l_onoff 为 0,则该选项关闭,l_linger 的值被忽略;
  • l_onoff 非 0,l_linger 为 0,调用 close 将会直接发送 RST 分组来关闭该连接,不会经历四次挥手过程;
  • l_onoff 非 0,l_linger 非 0,调用 close 将会拖延 l_linger 大小的时间用来将发送缓冲区中残留的数据都发送给对端。

golang 对应的结构体在 syscall 包:

type Linger struct {    Onoff  int32    Linger int32}

net 包好似没有提供相关的方法来设置这一选项,因此只能采用 syscall 包提供的原生 tcp 来实现。

1、首先创建 socket,设置 SO_LINGER 选项

syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)// 设置SO_LINGER linger := syscall.Linger{Onoff: 1, Linger: 0}syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &linger)

2、连接服务器

sa := &syscall.SockaddrInet4{Port: port, Addr: inet_addr(ip)}syscall.Connect(fd, sa)

3、发送消息(本人是 windows 平台)

func Send(fd syscall.Handle, msg string) error{     var buf syscall.WSABuf    var written uint32    buf.Buf,_ = syscall.BytePtrFromString(msg)    buf.Len = uint32(len(msg))    err := syscall.WSASend(fd, &buf, 1, &written,0, nil, nil)    if err != nil{ log.Printf("write error [%s]\n", err) return err    }    return nil}

4、接收消息

func Recv(fd syscall.Handle)([]byte, error){var ( buffer = make([]byte, 1024*4) readBytes uint32 flags uint32 buf syscall.WSABuf    )      buf.Buf = &buffer[0]    buf.Len = uint32(len(buffer))     err := syscall.WSARecv(fd, &buf, 1, &readBytes, &flags, nil, nil)    if err != nil { log.Printf("recv error [%s]\n", err) return nil, err    }    n := int(readBytes)    if n <= 0 { return []byte{}, errors.New("recv 0 byte")    }    return buffer[:n], nil    }

5、关闭连接

func Close(fd syscall.Handle){    if err := syscall.Closesocket(fd); err!=nil{ log.Printf("close error [%s]\n", err)    }    log.Printf("close success\n")}

完整代码如下(注:这里只是一个测试客户端,有需要生产使用的请酌情修改)

package clientimport (    "errors"    "log"    "net"    "strconv"    "strings"    "syscall"    "time")func init(){    var wsadata syscall.WSAData    if err := syscall.WSAStartup(MAKEWORD(2, 2), &wsadata); err != nil { log.Printf("Startup error, [%s]\n", err) return    }}func MAKEWORD(low, high uint8) uint32 {    var ret uint16 = uint16(high)<<8 + uint16(low)    return uint32(ret)}func inet_addr(ipaddr string) [4]byte {    var ( ips = strings.Split(ipaddr, ".") ip  [4]uint64 ret [4]byte    )    for i := 0; i < 4; i++ { ip[i], _ = strconv.ParseUint(ips[i], 10, 8)    }    for i := 0; i < 4; i++ { ret[i] = byte(ip[i])    }    return ret}func Connect(ip string, port int) (syscall.Handle, error){    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)    if err != nil{ log.Printf("init socket error [%s]\n", err) return syscall.InvalidHandle, err    } // 直接关闭,不经历四次挥手    err = syscall.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 1, Linger: 0})    if err != nil{ log.Printf("SetsockoptLinger error [%s]\n", err) return syscall.InvalidHandle, err    } sa := &syscall.SockaddrInet4{Port: port, Addr: inet_addr(ip)}    err = syscall.Connect(fd, sa)    if err != nil{ log.Printf("connect error [%s]\n", err) return syscall.InvalidHandle, err    }  return fd, nil}func Send(fd syscall.Handle, msg string) error{var buf syscall.WSABuf    var written uint32    buf.Buf,_ = syscall.BytePtrFromString(msg)    buf.Len = uint32(len(msg))    err := syscall.WSASend(fd, &buf, 1, &written,0, nil, nil)    if err != nil{ log.Printf("write error [%s]\n", err) return err    }    return nil}func Recv(fd syscall.Handle)([]byte, error){var ( buffer = make([]byte, 1024*4) readBytes uint32 flags uint32 buf syscall.WSABuf    ) buf.Buf = &buffer[0]    buf.Len = uint32(len(buffer)) err := syscall.WSARecv(fd, &buf, 1, &readBytes, &flags, nil, nil)    if err != nil { log.Printf("recv error [%s]\n", err) return nil, err    }    n := int(readBytes)    if n <= 0 { return []byte{}, errors.New("recv 0 byte")    }    return buffer[:n], nil    }func Close(fd syscall.Handle){    if err := syscall.Closesocket(fd); err!=nil{ log.Printf("close error [%s]\n", err)    }    log.Printf("close success\n")}

目前网上对于 golang 原生 tcp 的介绍还是比较少的, 好在函数定义和 C 标准库没有太大区别,只要去源码包找到对应的方法即可。当然,没有写过 C 代码的童鞋可能会有点头大,所以本人也是把代码都提供出来给家参考,希望对大家有帮助哈。

最后,卑微求个赞。