在nginx中使用了send_file 并且配合TCP_CORK/TCP_NOPUSH进行操作,我们一般的操作是这样子的,首先调用tcp_cork,阻塞下层的数据发送,然后调用send_file发送数据,最后关闭TCP_CORK/TCP_NOPUSH.而在nginx中不是这样处理的,前面两步都是一样的,最后一步,它巧妙的利用的http的特性,那就是基本都是短连接,也就是处理完当前的request之后,就会关闭当前的连接句柄,而在linux中,如果不是下面两种情况之一,那么关闭tcp句柄,就会发送完发送buf中的数据,才进行tcp的断开操作(具体可以看我以前写的那篇 “linux内核中tcp连接的断开处理”的 blog) :

1 接收buf中还有未读数据。

2 so_linger设置并且超时时间为0.

而如果调用shutdown来关闭写端的话,就是直接发送完写buf中的数据,然后发送fin。

ok,通过上面我们知道每次处理完请求,都会关闭连接(keepalive 会单独处理),而关闭连接就会帮我们将cork拔掉,所以这里就可以节省一个系统调用,从这里能看到nginx对细节的处理到了一个什么程度。

接下来还有一个单独要处理的就是keepalive的连接,由于keepalive是不会关闭当前的连接的,因此这里就必须显式的关闭tcp_cork。

然后我们来看代码,首先来看TCP_CORK/TCP_NOPUSH相关的配置,我们知道在nginx中 TCP_CORK/TCP_NOPUSH是默认关闭的,除非我们显示的打开(tcp_nopush on).而在nginx中对于TCP_CORK/TCP_NOPUSH选项的打开关闭是有两个位置,一个是ngx_http_core_loc_conf_t的tcp_nopush(这个对应配置文件里面的tcp_nopush选项),一个是r->connection->tcp_nopush,它默认是NGX_TCP_NOPUSH_UNSET,可是如果没有设置tcp_nopush on,则这个值就会被设为NGX_TCP_NOPUSH_DISABLED,也就是关闭tcp_cork.而这里主要使用的就是r->connection->tcp_nopush。这里多出来的NGX_TCP_NOPUSH_DISABLED主要就是针对keepalive的情况,后面我们会详细分析.

下面就是tcp_nopush的可选值.

1
2
3
4
5
6
7
8
9
10
  
typedef enum {

NGX_TCP_NOPUSH_UNSET = 0,

NGX_TCP_NOPUSH_SET,

NGX_TCP_NOPUSH_DISABLED

} ngx_connection_tcp_nopush_e;

来看core loc conf的值:

1
2
      
ngx_conf_merge_value(conf->tcp_nopush, prev->tcp_nopush, 0);

可以看到他的默认值是0T,也就是tcp_nopush没有打开的情况。

然后是r->connection->tcp_nopush 的设置,它的值依赖于clcf->tcp_nopush,如果clcf->tcp_nopush为(默认值),则关闭tcp_nopush,否则就是默认值打开。而connection中的tcp_nopush默认是NGX_TCP_NOPUSH_UNSET,也就是0。

1
2
3
4
5
6
7
8
9
10
      
if (!clcf->tcp_nopush) {

/\* disable TCP_NOPUSH/TCP_CORK use \*/

//设置关闭

r->connection->tcp_nopush = NGX_TCP_NOPUSH_DISABLED;

}

ok,接下来我们就直接来看nginx中如何使用TCP_CORK/TCP_NOPUSH,主要的代码是在ngx_linux_sendfile_chain(我们通过分析tcp_cork来分析,bsd的tcp_nopush和linux的处理类似)中,这个函数我前面的blog有详细分析过,这次主要是分析tcp_cork部分,我们主要来看,第一次调用sendfile的时候,会先设置tcp_cork.而设置之后就会设置c->tcp_nopush为NGX_TCP_NOPUSH_SET,也就是接下来的操作都不需要再次设置了。而最终当buf处理完毕,就直接返回。

这里有一个要注意的就是tcp_nodelay和tcp_nopush是互斥的。不过如果你同时设置了两个值的话,将会在第一个buf发送的时候,强制push数据,而第二个buf时,将会调用tcp_cork来打开nagle算法,也就是后面的都会应用tcp_nopush.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
  
//如果tcp_nopush为0(也就是配置文件中tcp_nopush on),则进入tcp_nopush的处理。

if (c->tcp_nopush == NGX_TCP_NOPUSH_UNSET

&& header.nelts != 0

&& cl

&& cl->buf->in_file)

{

……………………………….

//如果nodelay有设置,则进入相关处理,也就是关闭nagle算法。

if (c->tcp_nodelay == NGX_TCP_NODELAY_SET) {

tcp_nodelay = 0;

if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY,

(const void *) &tcp_nodelay, sizeof(int)) == -1)

{

…………………

}

else {

//如果设置成功,则设置nodelay为NGX_TCP_NODELAY_UNSET.这样下次就不会进入nodelay的处理。

c->tcp_nodelay = NGX_TCP_NODELAY_UNSET;

ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,

"no tcp_nodelay");

}

}

//如果tcp_nodelay没有设置,则进入tcp_nopush的处理。

if (c->tcp_nodelay == NGX_TCP_NODELAY_UNSET) {

//设置tcp_nopush(tcp_cork)

if (ngx_tcp_nopush(c->fd) == NGX_ERROR) {

err = ngx_errno;

/*

* there is a tiny chance to be interrupted, however,

* we continue a processing without the TCP_CORK

*/

if (err != NGX_EINTR) {

wev->error = 1;

ngx_connection_error(c, err,

ngx_tcp_nopush_n " failed");

return NGX_CHAIN_ERROR;

}

} else {

//如果设置成功,则将c->tcp_nopush设置为set,这样当再次需要调用sendfile的时候,就跳过设置tcp_nopush的部分

c->tcp_nopush = NGX_TCP_NOPUSH_SET;

ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,

"tcp_nopush");

}

}

………………………………………..

}

通过上面我们看到nginx最终会调用sendfile发送数据,然后当前的请求结束,最终会调用tcp_close或者tcp_shutdown(设置linger_timeout)来关闭连接,而当关闭连接的同时,内核会将写buf中缓存的数据发送出去,这样,就不需要我们关闭tcp_cork,因为内核会帮我们做这个,于是减少了一次系统调用。

到了这里,会有一个问题,那就是keepalive的情况,这时就会有问题了,因为keepalive是不会关闭连接的,这样,就需要我们显示的调用tcp_push来将数据push出去。接下来我就来分析上篇介绍keepalive的文章中没有涉及到的部分。

接下来的代码就在ngx_http_set_keepalive中。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
  
//如果tcp_nopush为NGX_TCP_NOPUSH_SET,则说明我们需要关闭tcp_cork,也就是将数据push出去.

if (c->tcp_nopush == NGX_TCP_NOPUSH_SET) {

//push数据

if (ngx_tcp_push(c->fd) == -1) {

ngx_connection_error(c, ngx_socket_errno, ngx_tcp_push_n " failed");

ngx_http_close_connection(c);

return;

}

//reset tcp_nopush

c->tcp_nopush = NGX_TCP_NOPUSH_UNSET;

tcp_nodelay = ngx_tcp_nodelay_and_tcp_nopush ? 1 : 0;

} else {

tcp_nodelay = 1;

}

//如果tcp_nodelay存在,则说明我们并没有使用tcp_nopush,此时如果clcf->tcp_nodelay被设置,则此时需要重新设置tcp_nodelay,也就是关闭nagle算法。

if (tcp_nodelay

&& clcf->tcp_nodelay

&& c->tcp_nodelay == NGX_TCP_NODELAY_UNSET)

{

ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "tcp_nodelay");

if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY,

(const void *) &tcp_nodelay, sizeof(int))

== -1)

{

#if (NGX_SOLARIS)

/\* Solaris returns EINVAL if a socket has been shut down \*/

c->log_error = NGX_ERROR_IGNORE_EINVAL;

#endif

ngx_connection_error(c, ngx_socket_errno,

"setsockopt(TCP_NODELAY) failed");

c->log_error = NGX_ERROR_INFO;

ngx_http_close_connection(c);

return;

}

//reset tcp_nodelay

c->tcp_nodelay = NGX_TCP_NODELAY_SET;

}