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 代码的童鞋可能会有点头大,所以本人也是把代码都提供出来给家参考,希望对大家有帮助哈。