Go语言中如何开启 TCP keepalive?( 二 )


进入系统层面可以通过直接操作 socket 参数来实现 。我没有关注里面太多的细节,这纯粹是我的个人解释 。以下是我们如何设置空闲时间为 30 秒(我们可以通过SetKeepAlivePeriod设置,因为其他参数我们再另外设置),重试时间间隔设置为 5 秒,重试次数设置为 3 。我偷了(啊呸,是参考了)上面所引用的文章中的一些代码,多谢 。
conn.SetKeepAlive(true)conn.SetKeepAlivePeriod(time.Second * 30)// Getting the file handle of the socketsockFile, sockErr := conn.File()if sockErr == nil { // got socket file handle. Getting descriptor. fd := int(sockFile.Fd()) // Ping amount err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 3) if err != nil { Warning("on setting keepalive probe count", err.Error()) } // Retry interval err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 5) if err != nil { Warning("on setting keepalive retry interval", err.Error()) } // don't forget to close the file. No worries, it will *not* cause the connection to close. sockFile.Close()} else { Warning("on setting socket keepalive", sockErr.Error())}在这段代码之后的某一行我会写上dataLength, err := conn.Read(readBuf),这行代码会阻塞直到收到数据或者发生错误 。如果是 keepalive 引起的错误,err.Error()将会包含连接超时信息 。
关于文件描述符的坑上面的代码只有在你不频繁调用的前提下才运行良好 。在写完这篇文章之后,我以困难模式学习到了一个关于它的小问题 。。。
问题就隐藏在Fd[14]函数调用 。我们来看它的实现 。
func (f *File) Fd() uintptr { if f == nil { return ^(uintptr(0)) } // If we put the file descriptor into nonblocking mode, // then set it to blocking mode before we return it, // because historically we have always returned a descriptor // opened in blocking mode. The File will continue to work, // but any blocking operation will tie up a thread. if f.nonblock { f.pfd.SetBlocking() } return uintptr(f.pfd.Sysfd)}如果文件描述符处于非阻塞模式,会将它修改为阻塞模式 。根据stackoverflow 的这个回答[15],举例来说,当 Go 增加一个阻塞的系统调用,运行时调度器将该系统调用所属协程的所属系统线程从调度池中移出 。如果调度池中的系统线程数小于GOMAXPROCS,则会创建新的系统线程 。鉴于我的每一个连接都使用一个独立协程,你可以想象一下这个爆炸速度 。将很快到达 10000 线程的限制然后 panic 。
将它放入独立协程并不好使 。

译者yoko注,个人理解此处可做两层解释,如果是像原作者所描述的,每个连接都独占一个协程(直到连接关闭再退出协程),先使用系统调用设置文件描述符属性,再收发数据,那么系统线程会随连接数线性增长 。如果是在连接收发数据的协程之前,先弄一个协程处理完文件描述符属性的设置,那么系统调用完成后临时协程结束,线程还是会回收的 。但也毕竟不是一种好的模式 。
但是有一个方法是可行的 。注意,前提是 Go 版本高于 1.11 。看以下代码 。
//Sets additional keepalive parameters.//Uses new interfaces introduced in Go1.11, which let us get connection's file descriptor,//without blocking, and therefore without uncontrolled spawning of threads (not goroutines, actual threads).func setKeepaliveParameters(conn devconn) { rawConn, err := conn.SyscallConn() if err != nil { Warning("on getting raw connection object for keepalive parameter setting", err.Error()) } rawConn.Control( func(fdPtr uintptr) { // got socket file descriptor. Setting parameters. fd := int(fdPtr) //Number of probes. err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 3) if err != nil { Warning("on setting keepalive probe count", err.Error()) } //Wait time after an unsuccessful probe. err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 3) if err != nil { Warning("on setting keepalive retry interval", err.Error()) } })}func deviceProcessor(conn devconn) { //............ conn.SetKeepAlive(true) conn.SetKeepAlivePeriod(time.Second * 30) setKeepaliveParameters(conn) //............ dataLen, err := conn.Read(readBuf) //............}最新版本的 Go 提供了一些新接口,net.TCPConn实现了SyscallConn[16],它使得你可以获取RawConn[17]对象从而设置参数 。你所需要做的就是定义一个函数(就像上面例子中的匿名函数),它接收一个指向文件描述符的参数 。这是操作连接中的文件描述符而不造成阻塞调用的方法,可避免出现疯狂创建线程的情况 。
总结网络编程是复杂的 。并且时常是系统相关的 。这个解决方法只在 Linux 下有用,但是这是一个好的开始 。在其他操作系统中有类似的参数,它们只是调用方式不同 。


推荐阅读