TCP连接中TCP_NODELAY
TCP/IP协议中针对TCP默认开启了Nagle算法。Nagle算法通过减少需要传输的数据包,来优化网络。在内核实现中,数据包的发送和接受会先做缓存,分别对应于写缓存和读缓存。
int nodelay = 1;
int ret = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char*)&nodelay, sizeof(nodelay));
if (ret == SOCKET_ERROR)
{
log_error("_SetNoDelay failed, err_code=%d, fd=%d", _GetErrorCode(), fd);
}
- 启动TCP_NODELAY,就意味着禁用了Nagle算法,允许小包的发送。
对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。比如,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来说,绝对不是在一个量级上的,所以数据传输非常少;而又要求用户的输入能够及时获得返回,有较低的延时。如果开启了Nagle算法,就很可能出现频繁的延时,导致用户体验极差。
-
**对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到一定量之后,才会被发送出去,**这样明显提高了网络利用率(实际传输数据payload与协议头的比例大大提高)。但是这又不可避免地增加了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。当然这个问题只有在连续进行两次写操作的时候,才会暴露出来。
连续进行多次对小数据包的写操作,然后进行读操作,本身就不是一个好的网络编程模式;在应用层就应该进行优化。
-
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。 -
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。 -
举个例子,一开始client端调用socket的write操作将一个int型数据(称为A块)写入到网络中,由于此时连接是空闲的(也就是说还没有未被确认的小段),因此这个int型数据会被马上发送到server端,接着,client端又调用write操作写入‘/r/n’(简称B块),这个时候,A块的ACK没有返回,所以可以认为已经存在了一个未被确认的小段,所以B块没有立即被发送,一直等待A块的ACK收到(大概40ms之后),B块才被发送。 -
还隐藏了一个问题,就是A块数据的ACK为什么40ms之后才收到?这是因为TCP/IP中不仅仅有nagle算法,还有一个ACK延迟机制 。当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假设为t),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。在我之前的时间中,t大概就是40ms。这就解释了为什么’/r/n’(B块)总是在A块之后40ms才发出。 -
如果你觉着nagle算法太捣乱了,那么可以通过设置TCP_NODELAY将其禁用 。当然,更合理的方案还是应该使用一次大数据的写操作,而不是多次小数据的写操作。
Socket中SO_REUSEADDR
-
如果在绑定一个socket之前设置了SO_REUSEADDR,除非两个socket绑定的源地址和端口号都一样,那么这两个绑定都是可行的。也许你会疑惑这跟之前的有什么不一样?关键是SO_REUSEADDR改变了在处理源地址冲突时对通配地址(“any ip address”)的处理方式。 -
当没有设置SO_REUSEADDR的时候,socketA先绑定到0.0.0.0:21,然后socketB绑定到192.168.0.1:21的时候将会失败(EADDRINUSE错误),因为0.0.0.0意味着"任意本地IP地址”,也就是"所有本地IP地址“,因此包括192.168.0.1在内的所有IP地址都被认为是已经使用了。但是在设置SO_REUSEADDR之后socketB的绑定将会成功,因为0.0.0.0和192.168.0.1事实上不是同一个IP地址,一个是代表所有地址的通配地址,另一个是一个具体的地址。注意上面的表述对于socketA和socketB的绑定顺序是无关的,没有设置SO_REUSEADDR,它们将失败,设置了SO_REUSEADDR,它将成功。 -
下面给出了一个表格列出了所有的可能组合:
SO_REUSEADDR socketA socketB Result
---------------------------------------------------------------------
ON/OFF 192.168.0.1:21 192.168.0.1:21 Error (EADDRINUSE)
ON/OFF 192.168.0.1:21 10.0.0.1:21 OK
ON/OFF 10.0.0.1:21 192.168.0.1:21 OK
OFF 0.0.0.0:21 192.168.1.0:21 Error (EADDRINUSE)
OFF 192.168.1.0:21 0.0.0.0:21 Error (EADDRINUSE)
ON 0.0.0.0:21 192.168.1.0:21 OK
ON 192.168.1.0:21 0.0.0.0:21 OK
ON/OFF 0.0.0.0:21 0.0.0.0:21 Error (EADDRINUSE)
上面的表格假定socketA已经成功绑定,然后创建socketB绑定给定地址在是否设置SO_REUSEADDR的情况下的结果。Result代表socketB的绑定行为是否会成功。如果第一列是ON/OFF,那么SO_REUSEADDR的值将是无关紧要的。
现在我们知道SO_REUSEADDR对通配地址有影响,但这不是它唯一影响到的方面。还有一个众所周知的影响同时也是大多数人在服务器程序上使用SO_REUSEADDR的首要原因。为了了解其它SO_REUSEADDR重要的使用方式,我们需要深入了解TCP协议的工作方式。 一个socket有一个发送缓冲区,当调用send()函数成功后,这并不意味着所有数据都真正被发送出去了,它只意味着数据都被送到了发送缓冲区中。对于UDP socket来说,如果不是立刻发送的话,数据通常也会很快的发送出去,但对于TCP socket,在数据加入到缓冲区和真正被发送出去之间的时延会相当长。这就导致当我们close一个TCP socket的时候,可能在发送缓冲区中保存着等待发送的数据(由于send()成功返回,因此你也许认为数据已经被发送了)。如果TCP的实现是立刻关闭socket,那么所有这些数据都会丢失而你的程序根本不可能知道。TCP被称为可靠协议,像这种丢失数据的方式就不那么可靠了。这也是为什么当我们close一个TCP socket的时候,如果它仍然有数据等待发送,那么该socket会进入TIME_WAIT状态。这种状态将持续到数据被全部发送或者发生超时。 在内核彻底关闭socket之前等待的总时间(不管是否有数据在发送缓冲区中等待发送)叫做Linger Time。Linger Time在大部分系统上都是一个全局性的配置项而且在默认情况下时间相当长(在大部分系统上是两分钟)。当然对于每个socket我们也可以使用socket选项SO_LINGER进行配置,可以将等待时间设置的更长一点儿或更短一点儿甚至禁用它。禁用Linger Time绝对是一个坏主意,虽然优雅的关闭socket是一个稍微复杂的过程并且涉及到来回的发送数据包(以及在数据包丢失后重发它们),并且这个过程还受到Linger Time的限制。如果禁用Linger Time,socket可能丢失的不仅仅是待发送的数据,而且还会粗暴的关闭socket,在绝大部分情况下,都不应该这样使用。如何优雅的关闭TCP连接的细节不在这里进行讨论,如果你想了解更多,我建议你阅读: http://www.freesoft.org/CIE/Course/Section4/11.htm 。而且如果你用SO_LINGER禁用了Linger Time,而你的程序在显式的关闭socket之前就终止的话,BSD(其它的系统也有可能)仍然会等待,而不管已经禁用了它。这种情况的一个例子就是你的程序调用了exit() (在小的服务器程序很常见)或者进程被信号杀死(也有可能是进程访问了非法内存而终止)。这样的话,不管在什么情况下,你都无法对某一个socket禁用linger了。 问题在于,系统是怎样看待TIME_WAIT状态的?如果SO_REUSEADDR还没有设置,一个处在TIME_WAIT的socket仍然被认为绑定在源地址和端口,任何其它的试图在同样的地址和端口上绑定一个socket行为都会失败直到原来的socket真正的关闭了,这通常需要等待Linger Time的时长。所以不要指望在一个socket关闭后立刻将源地址和端口绑定到新的socket上,在绝大部分情况下,这种行为都会失败。然而,在设置了SO_REUSEADDR之后试图这样绑定(绑定相同的地址和端口)仅仅只会被忽略,而且你可以将相同的地址绑定到不同的socket上。注意当一个socket处于TIME_WAIT状态,而你试图将它绑定到相同的地址和端口,这会导致未预料的结果,因为处于TIME_WAIT状态的socket仍在"工作",幸运的是这种情况极少发生。 对于SO_REUSEADDR你需要知道的最后一点是只有在你想绑定的socket开启了地址重用(address reuse)之后上面的才会生效,不过这并不需要检查之前已经绑定或处于TIME_WAIT的socket在它们绑定的时候是否也设置这个选项。也就是说,绑定的成功与否只会检查当前bind的socket是否开启了这个标志,不会查看其它的socket。
Socket中SO_REUSEPORT
- SO_REUSEPORT的含义与绝大部分人对SO_REUSEADDR的理解一样。基本上说来,SO_REUSEPORT允许你将多个socket绑定到 相同的地址和端口只要它们在绑定之前都设置了SO_REUSEPORT 。如果第一个绑定某个地址和端口的socket没有设置SO_REUSEPORT,那么其他的socket无论有没有设置SO_REUSEPORT都无法绑定到该地址和端口直到第一个socket释放了绑定。
- SO_REUSEPORT并不表示SO_REUSEADDR。这意味着如果一个socket在绑定时没有设置SO_REUSEPORT,那么同预期的一样,其它的socket对相同地址和端口的绑定会失败,但是如果绑定相同地址和端口的socket正处在TIME_WAIT状态,新的绑定也会失败。当有个socket绑定后处在TIME_WAIT状态(释放时)时,为了使得其它socket绑定相同地址和端口能够成功,需要设置SO_REUSEADDR或者在这两个socket上都设置SO_REUSEPORT。当然,在socket上同时设置SO_REUSEPORT和SO_REUSEADDR也是可行的。
|