选项TCP_NODELAY是禁用Nagle算法,即数据包立即发送出去,而选项
上右图显示的选项TCP_CORK的理论情况,但是在各个具体协议栈的实际实现中,有一些机制会打破选项TCP_CORK的这个“完全”堵塞(即数据包长度不到一个MSS则不允许发送)特性。以linux
3.4.4版本的内核代码实现为例,正常的tcp数据发送流程为(调用片段:仅从tcp层往ip层发送的函数调用关系):
tcp_push() -> __tcp_push_pending_frames() ->
tcp_write_xmit()
如果函数tcp_write_xmit()有发送数据成功,不论发送了多少个数据包,它都将返回0;但如果一个数据包也未发送,比如可能受当前拥塞窗口和发送窗口的限制,也可能是受选项TCP_CORK(函数tcp_write_xmit()内会调用tcp_nagle_test()函数做数据包发送判断)等的影响,导致数据包暂不能发送,此时就可能返回1:
由于TCP_CORK选项是Linux特有的,在其他比如BSD平台上,与此对应的是TCP_NOPUSH选项,所以在nginx内部通过函数ngx_tcp_nopush()/ngx_tcp_push()来分别对应启用/禁用TCP_CORK选项,达到各个平台的一致性封装。
第152行的if判断给出了需设置TCP_CORK选项的前提条件,变量c->tcp_nopush的值为NGX_TCP_NOPUSH_UNSET则表示TCP_CORK选项当前处于禁用状态,所以才需要进入到if块内去执行函数ngx_tcp_nopush()启用TCP_CORK选项;值得注意的是,禁用状态并不是表示nginx不使用TCP_CORK选项,如果设置为不使用该选项,那么对应该变量的值则为NGX_TCP_NOPUSH_DISABLED。第153-155的判断为真则表示响应头和响应体同时存在,并且响应体在文件内;为什么要把“响应体在文件内”作为一个是否启用TCP_CORK选项的条件,原因在后面《数据读/写传输方式》一节对系统函数writev()进行描述时有讲到,在这里简单的说一句就是:如果待发送数据全部都在内存缓冲区,那么使用系统函数writev()可达到更好的效果,从而无需使用TCP_CORK选项。另外,由于nginx对选项TCP_CORK和TCP_NODELAY是互斥使用,所以有底189行的if判断。开启TCP_CORK选项发送完响应数据后,在连接结束的其中一个处理函数,也就是ngx_http_set_keepalive()内又将禁用TCP_CORK选项,即拔掉塞子,让阻塞的数据可以发送出去,但是否立即发送出去还需由选项TCP_NODELAY以及Nagle算法决定。
对于选项TCP_CORK和TCP_NODELAY,除了前面提到的这些使用逻辑之外,在nginx的upstream模块也有对应的使用,不过都比较简单而不多累述,但要注意的就是,nginx始终是在互斥使用这两个选项,也正因为如此,为了避免错误的认为这两个选项必须互斥使用,下面就介绍一下这两个选项的混合使用情况。
对于一个套接口描述符,选项TCP_NODELAY和TCP_CORK可以同时存在,这是无容置疑的。看一下内核里设置两个选项时所对应的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2129: Filename :
\linux-3.4.4\net\ipv4\tcp.c
2130: static int do_tcp_setsockopt(struct
sock *sk, int level,
2131:
int optname, char __user *optval, unsigned int optlen)
2132: {
2133: …
2268:
case TCP_NODELAY:
2269:
if (val) {
2270: …
2278: tp->nonagle |= TCP_NAGLE_OFF|TCP_NAGLE_PUSH;
2279: tcp_push_pending_frames(sk);
2280: } else {
2281: tp->nonagle &= ~TCP_NAGLE_OFF;
2282: }
2283:
break;
2284: …
2299:
case TCP_CORK:
2300: …
2311:
if (val) {
2312: tp->nonagle |= TCP_NAGLE_CORK;
2313: } else {
2314: tp->nonagle &= ~TCP_NAGLE_CORK;
2315:
if (tp->nonagle&TCP_NAGLE_OFF)
2316: tp->nonagle |= TCP_NAGLE_PUSH;
2317: tcp_push_pending_frames(sk);
2318: }
2319:
break;
前面已经提到过,如果选项TCP_CORK存在,那么选项TCP_NODELAY的作用将被弱化,这在函数tcp_nagle_check()里能看到这一点,而对应的TCP_NAGLE_CORK旗标正好在第2312行里设置,也就是启用TCP_CORK选项时打上该标记;从第2315-2317代码可以看出,只有当选项TCP_CORK被清除后,选项TCP_NODELAY的作用才会体现出来,但是存在一个特别的时间点,也就是在开启选项TCP_NODELAY时,如第2278-2279行所示,此时会设置TCP_NAGLE_PUSH旗标,而后调用tcp_push_pending_frames()函数,把当前发送队列的数据包强制发送(即PUSH)出去,即便当前设置有选项TCP_CORK,在前面提到的函数tcp_nagle_test()里对TCP_NAGLE_PUSH旗标的特殊处理论证了这一点。当有新的数据包被加入到发送队列时,会调用函数skb_entail()清除TCP_NAGLE_PUSH标记,对于此时的这些数据包(新加进来的数据包以及在这个新数据包加进来之前还没发送完的旧数据包),选项TCP_CORK才又占主导地位:
1
2
3
4
5
6
7
535: Filename
: \linux-3.4.4\net\ipv4\tcp.c
536:
static inline void skb_entail(struct
sock *sk, struct sk_buff *skb)
537: {
538: …
549:
if (tp->nonagle & TCP_NAGLE_PUSH)
550: tp->nonagle &= ~TCP_NAGLE_PUSH;
551: }