socket编程套接字选项分析

socket编程套接字选项分析

1.前言

我们讨论的套接字选项,范围是这样的:

  • Linux平台下
  • TCP协议
  • 服务端

套接字选项通过不同的组合,可以达成不同的效果,本着够用就行的原则,本文仅探讨其中的一种设定方式。

基本的参考资料,可以在《UNIX网络编程 卷1》里找到,这里不再截图和赘述。

2.选项

2.1 设定方式

方式1:调用socket接口创建服务端侦听套接字的时候指定,函数原型如下:

其中type字段可以指定如下两个选项:

第一个显而易见,用来设定是否创建非阻塞套接字。

第二个选项在man-page中的解释如下:

这个选项跟fork和exec这两个系统调用有关,相关参考资料为:

  • 《UNIX网络编程 卷1》4.7章 fork和exec函数
  • 《UNIX环境高级编程》8.10章 函数exec

简单理解就是:

1.父进程fork之后,文件描述符也被复制了,父子进程可以对同一文件描述符进行读写,没有问题,有时候我们甚至会利用这种特性来实现一些功能,比如父进程从套接字收,子进程往套接字发等。(补充知识:假设父进程打开了一个文件A,父进程fork之后,相当于子进程又打开了一次文件A,父子进程可以各自独立关闭这个文件A,对对方没有影响,进程打开的文件可以用lsof -p PID命令查看)

2.但是一旦子进程调用exec执行了另外一个程序,子进程的几乎所有资源都将被换出(正文、数据、堆、栈等),只有少数一些资源会被新进程继承(如进程id等,详细列表见《UNIX环境高级编程》8.10章 函数exec),对于子进程打开的文件,除非被显示指定close-on-exec标识,否则在子进程执行exec后就丢了,只有在程序退出后才会被操作系统回收。

3.简而言之就是:如果没有指定close-on-exec标识,那么这个文件描述符会在进程fork+exec后泄漏,只有程序退出才会被关闭回收。

我们的程序虽然不涉及fork和exec,但指定了第二个标识也没有什么坏处,而且这是一个良好的习惯,所以我们默认指定:

方式2:调用accept创建客户端套接字的时候指定,函数原型如下:

flags的可选值也是只有两个:

具体作用相同,不再赘述,我们的程序也是默认指定:

方式3:调用setsockopt函数指定,函数原型如下:

可以设置的套接字选项列表,见《UNIX网络编程 卷1》-7.2 getsockopt和setsockopt函数一节。

我们实际能用到的有以下几个:

对象leveloptname备注
acceptfdSOL_SOCKETSO_REUSEADDR地址重用
SOL_SOCKETSO_REUSEPORT端口重用
clientfdSOL_SOCKETSO_SNDBUF发送缓冲
SOL_SOCKETSO_RCVBUF接收缓冲
SOL_SOCKETSO_KEEPALIVE保活心跳(可选)
SOL_TCPTCP_KEEPIDLE
SOL_TCPTCP_KEEPINTVL
SOL_TCPTCP_KEEPCNT
IPPROTO_TCPTCP_NODELAY禁用Nagle算法
SOL_SOCKETSO_LINGER延迟关闭

下面的章节将详细描述这几个套接字选项的作用。

2.2 侦听套接字选项

侦听套接字,除了一开始创建的时候指定的SOCK_NONBLOCKSOCK_CLOEXEC之外,还需要指定SO_REUSEADDRSO_REUSEPORT

1)SO_REUSEADDR

如果未指定,当你快速重启程序时,系统会提示你地址已被占用(bind失败),导致程序无法启动,过一段时间,等那些未关闭的老连接断开,相关资源被操作系统回收后才能启动,这一过程可能长达数分钟,这在生产系统上是不可接受的。

另外一个问题是,你无法在同一个端口上(比如80端口)上启动两个TCP服务器程序,来实现负载均衡,这会影响服务器的极限吞吐量。

例如,当你的服务器有两个本地地址(网卡IP,比如192.168.1.1和192.168.1.2),你想启动两个服务器程序,绑定在同一个端口(比如80端口)上,分别服务从两个地址发起的TCP连接时,就必须指定SO_REUSEADDR选项。

2)SO_REUSEPORT

如果指定了该选项,会带来两个好处:一是你可以在同一个地址(即IP+端口)上启动多个服务器程序,二是当这个地址上有新连接时,操作系统会自动帮你把这些连接均衡地分配给这些服务器程序,自动实现负载均衡。

综上,两个选项我们都指定。

2.3 客户端套接字选项

客户端套接字,除了一开始创建的时候指定的SOCK_NONBLOCKSOCK_CLOEXEC之外,还需要指定如下几个套接字选项。

1)SO_SNDBUF/SO_RCVBUF

这两个用来设置客户端套接字发送和接收缓冲区的大小,有条件就设大点,能更好地应对突发流量,减少TCP丢包重传问题。

2)SO_KEEPALIVE等四个选项

SO_KEEPALIVE是操作系统级实现的保活心跳机制,默认关闭。

一般情况下,我们都需要在应用层设计一个心跳检测协议,但如果由于某些原因没有设计心跳协议,或者在某些不重要的程序中想偷懒,就可以使用SO_KEEPALIVE。

据我所知,SO_KEEPALIVE触发心跳超时后,操作系统会在底层默默关闭连接,并且不会在应用层唤醒程序的多路复用函数(比如epoll_wait),只有当你主动尝试在该连接上收、发数据时,才会触发错误(一般是触发Broken Pipe错误)。即:由SO_KEEPALIVE触发的连接关闭,对应用层不可见,不能方便地被应用层及时检测到以及时释放资源。

另外一个问题是,SO_KEEPALIVE不能检测程序假死,如果程序由于某种原因卡死了(比如卡在某个bug循环中出不来了),此时程序其实已经没有服务能力了,但由于程序没有退出,SO_KEEPALIVE不会判定连接异常,而应用层心跳则没有这个问题。

基于上述原因,我个人其实不太建议设置SO_KEEPALIVE,应用程序应该主动设计一个心跳协议,SO_KEEPALIVE只在十分有必要的场合才用。

3)TCP_NODELAY

该选项用于禁用TCP的Nagle算法,该算法默认是启动状态的,即系统会将你的小包延迟发送,跟其他包合并后或超时后再发送。

当你的协议中包含长度较小的协议包时(比如心跳包、时间包等),如果没有启用TCP_NODELAY(即关闭Nagle算法),会造成这些小包数据传输不及时,影响业务逻辑的正确性。

例如,当你的下游依赖你发时间包驱动做一些事情时,如果你的心跳包发送不及时,那么下游的业务逻辑可能就会出问题。

该选项建议打开:

4)SO_LINGER

这个选项用于定义关闭套接字时,对于发送缓冲区中残留的数据的处理行为,该选项默认是关闭的,程序调用close时会立即返回,残留在发送缓冲区的数据会被系统尝试继续发送出去。

这一默认行为的危害性在于,假如对方收的极慢,或者对端网络直接挂了,那么服务端的这些套接字资源没法被及时回收,影响服务端的服务能力。

而如果你打开了选项,又会有其他副作用。

如上图所示,这个选项有两个参数,第一个固定填1,表示打开该功能,第二个可以填0,也可以填1或其他正数值(这里以填1为例)。

填0的时候表示,进程调用close时,套接字发送缓冲区中的数据会被立即丢弃,同时发送一个RST(而不是FIN)到对端,套接字资源(包括文件描述符和端口号)会被立即回收。

填1的时候:

如果套接字是阻塞的,那么程序会进入休眠状态(相当于进程显式调用sleep(1)函数),直到缓冲区的数据发送完成(或出错,或超时),一个高性能的服务器,决不能是阻塞的,因此这个情况不考虑。

如果套接字是非阻塞的,close会立即返回,发送缓冲区的数据会被立即丢弃,其行为类似于填0的情况。也就是说,对于非阻塞套接字,这个超时参数是没用的,只要你打开linger选项,系统都会在你close的时候丢弃缓冲区的数据,同时给对端发送一个RST。

然而在某些场景下,我们不希望发送缓冲区的数据被丢弃,比如这个例子:对方发出了一个退出登录协议包,服务器收到后,会给他回一个退出登录成功协议包,并且在这个包中附带提供一些信息,发送后立即close掉连接。如果服务器打开了linger选项,那么这个退出登录应答包极大可能会被直接丢弃,发不到对端。

综上,我们其实不建议设置LINGER选项,我们目前的服务器对套接字和端口资源的使用量并不大,相比资源回收的速度,我们当然选正确性。因此我们的观点是,设置LINGER选项弊大于利。

2.4 其他设置

初学者可能会碰见这样一个场景:服务器程序运行得好好的,突然退了,也没有任何core文件产生。

原因就是SIGPIPE信号没有忽略。

当你尝试往一个已经被对端关闭的连接上写数据时(经常发生,比如连接已经被对端断开了,你的程序由于时序问题还没有走到recv函数处检测对端连接断开,而是先调用了send函数发送了一些数据,比如心跳数据),send函数并不会返回-1并且给你设置相应的错误码,而是直接触发程序的SIGPIPE信号。

如果你没有显式地捕获并处理这个信号,就会采用默认的处理方式-退出程序。这个退出是合理合法的,因此并不会产生core文件。如果你不知道这个机制,排查的时候就会非常头大。

3.建议汇总

建议1:程序忽略SIGPIPE信号

建议2:调用socket和accept函数创建套接字的时候,指定SOCK_NONBLOCK和SOCK_CLOEXEC

建议3:额外给侦听套接字和客户连接套接字设置如下选项:

对象leveloptname备注
acceptfdSOL_SOCKETSO_REUSEADDR设置
SOL_SOCKETSO_REUSEPORT设置
clientfdSOL_SOCKETSO_SNDBUF设置
SOL_SOCKETSO_RCVBUF设置
SOL_SOCKETSO_KEEPALIVE只有十分必要再设置
SOL_TCPTCP_KEEPIDLE
SOL_TCPTCP_KEEPINTVL
SOL_TCPTCP_KEEPCNT
IPPROTO_TCPTCP_NODELAY设置
SOL_SOCKETSO_LINGER不建议设置

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注