golang 原生 tcp setsocketopt

golang 原生 tcp setsocketopt,第1张

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

测试刚开始使用小并发的请求没有啥问题,但是加大并发后,发现每次请求到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 client

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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/990836.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存