TCP/IP协议笔记2-TCP编程重要选项含义解析

前文再续,书接上一回,这篇主要总结TCP编程中那些重要选项的含义解析,毕竟没有研究过linux的网络这块的源码,大都是从现象来分析,如有错漏,请大家指正,linux内核版本为4.4.0,系统为ubuntu16.04,macos版本为10.12。

1 SO_REUSEADDR选项解析

SO_REUSEADDR选项大家不会陌生,在编程中会经常看到有设置为1。至于为什么要设置这个选项,要从之前提到的TCP关闭连接的四次握手说起,我们知道,在TCP关闭连接的时候主动关闭的一方会有2MSL(Maximum Segment Life,报文最大生存时间)的时间处于TIME_WAIT状态,并不会马上关闭。TCP连接在2MSL等待期内,定义这个连接的插口四元组(客户端IP, 客户端端口,服务端IP,服务端端口)不能再被使用。而且在大多数的TCP实现中,要求更为严格,在2MSL内的等待期内,插口中使用的本地端口在默认情况下也不能再次使用。那如果要使用,就需要用到SO_REUSEADDR选项了。

对于SO_REUSEADDR选项,一些文章中有点语焉不详,这里来细细探究一下。对于这个选项的解释,大部分书籍是以Unix为例子的,也就是说只适用于Unix系列的系统,比如MacOS,BSD等。

先看看Linux下面这个选项的实际作用。注意说的是TIME_WAIT状态的端口可以再次使用,如果端口只是在监听中,并不是TIME_WAIT状态,是不能再次使用的。还是用之前的代码,服务端的设置了SO_REUSEADDR选项,并加了一个host参数,如下:

#server.py
import socket

def start_server(ip, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        sock.bind((ip, port))
        sock.listen(1)

        while True:
            conn, cliaddr = sock.accept()
            print 'server connect from: ', cliaddr

            while True:
                data = conn.recv(1024)
                if not data:
                    print 'client closed:', cliaddr
                    break

                conn.send(data.upper())
            conn.close()
    except Exception, ex:
        print 'exception occured:', ex
    finally:
        sock.close()

if __name__ == "__main__":
    import sys
    host = '127.0.0.1'
    if len(sys.argv) >= 2:
          host = sys.argv[1]
    print 'host:%s' % host
    start_server(host, 7777)


#client.py
from socket import *
import sys

def start_client(ip, port):
    try:
        sock = socket(AF_INET, SOCK_STREAM, 0)
        sock.connect((ip, port))
    print 'connected'
        while True:
            data = sys.stdin.readline().strip()
            print 'input data:', data
            if not data: break

            sock.send(data)
            result = sock.recv(1024)
            if not result:
                print 'other side has closed'
            else:
                print 'response from server:%s' % result
        sock.close()
    except Exception, ex:
        print ex

if __name__ == "__main__":
    start_client('127.0.0.1', 7777)

第一个实验
先在一个终端运行python server.py,这个时候你再开一个终端运行python server.py或者python server.py 0.0.0.0都是不行的,因为端口已经在使用了,前一个是因为是IP和端口都相同不行,而后一个是因为0.0.0.0是指代本机所有的IP,linux下面是不能让0.0.0.0和其他ip一起在同一个端口监听的,而macos是可以的。由于之前占用端口的连接不处于TIME_WAIT状态,即便你设置了SO_REUSEADDR选项也不能再次使用,当然有个例外就是,如果你指定本机的ip10.0.2.15是可以同时监听的

第二个实验
先在一个终端运行python server.py,再开一个终端运行python client.py,接着CTRL+C终止第一个终端脚本,这个时候服务端这个连接会处于FIN_WAIT2状态,而客户端连接则是CLOSE_WAIT状态。然后CTRL+C关掉第二个终端脚本,这时服务端的连接处于TIME_WAIT状态,此时我们另外开启一个终端,运行python server.py或者python server.py 0.0.0.0发现,是可以正常运行的,因为设置了SO_REUSEADDR标记,所以可以将TIME_WAIT状态的端口进行再次使用。这一点linux和macos表现一致。

2 backlog的含义

backlog参数的解释在网上查资料很是混乱,各种解释都有。当然大部分可能还是依照的《Unix网络编程》上面的说明,实际上根据我在macos和Linux上面测试的情况,是有所不同的。backlog队列的设计一般而言是有两种方式的:

  • 1)第一种方式是只用一个队列,队列大小是listen函数里面的backlog大小。当服务端接收到一个SYN包的时候,则回复一个SYN-ACK包并将这个连接加入到队列中,此时连接状态为SYN-RECV。当接收到ACK包的时候,则将该连接的状态从SYN-RECV改为ESTABLISHED,此时这个连接才可以移交给上层应用。也就是说,这个队列中的连接有两种状态,SYN-RECV和ESTABLISHED,只有处于ESTABLISHED的连接才会通过accept函数调用提交给上层应用处理。

  • 2)第二种方式是用两个队列。一个是syn queue,另一个是accept queue。处于SYN-RECV状态的连接会被加入到syn queue,等到ACK包到达完成三次握手了会将其状态改为ESTABLISHED,并将连接移到accept queue。正如队列名字中所示,accept函数调用就是从accept queue中取连接即可。

TCP/IP协议笔记2-TCP编程重要选项含义解析_第1张图片
图1 Unix网络编程一书中关于backlog的图示

大多数的BSD系列系统应该是采用的第一种方式,即只用一个队列。但是在《Unix网络编程》中提到的是一些BSD系列系统也是用的两个队列,但是两个队列大小之和不超过backlog大小,也就是说从表现上来看,跟第一种方式是类似的,比如macos就是类似第一种方式。比如指定backlog为1,则accept queue的大小为1,也就是说除了accept已经处理的那个连接,accept queue中还可以有最多1个连接,一共最多可以有2个连接同时处于ESTABLISHED状态。这时,如果有其他连接进来,macos里面会忽略SYN包,不做任何处理,其他的那个连接会在重试75秒左右(先是每隔一秒尝试5次,然后是隔2,4,8,16,32秒等)放弃。

而Linux从2.2之后,是采用的第二种方式,而且accept queue的大小是backlog+1和系统配置文件/etc/sysctl.conf中的somaxconn+1的较小值(也就是说如果你指定一个很大的backlog,但是somaxconn小于backlog的话,最终的backlog会被设置为somaxconn),syn queue的大小是backlog和tcp_max_syn_backlog的较小值(即syn队列大小不会超过tcp_max_syn_backlog大小)。如果backlog设置为0,则Linux会自动设置为一个默认大小,我的系统里面默认大小是1。而前面之所以要加1,是因为Linux里面默认是从0开始计数队列大小,也就是保证accept队列里面至少可以有1个连接,那么我们如果指定backlog=1,而somaxconn使用默认的128的话,那么最终的accept queue大小为2,也就是说除了已经被accept处理的那个连接,accept queue里面还可以有2个连接,结果是最终最多有3个连接同时处于ESTABLISHED状态(1个已经被accept处理+2个在accept queue中,当然通常情况下我们accept后正在处理的并不止一个连接)。

2.1 Linux环境下测试

首先有几个配置要说明下,Linux的sysctl.conf里面的几个相关配置如下,macos的配置保持默认。

net.ipv4.tcp_max_syn_backlog = 128
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1
net.core.somaxconn = 128
net.ipv4.tcp_abort_on_overflow = 0

测试代码还是用第一节中的,先开一个终端运行python server.py,然后开启3个终端,运行python client.py,这个时候通过命令netstat -alnp|grep 7777 可以看到3个客户端和服务端的连接都建立成功(双向连接一共6个,还有1个处于监听状态的服务端连接),处于ESTABLISHED状态如下:

root@ssj-VirtualBox:/home/ssj/network# netstat -alpn|grep 7777  
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               LISTEN      24027/python
tcp        0      0 127.0.0.1:45786         127.0.0.1:7777          ESTABLISHED 24049/python
tcp        0      0 127.0.0.1:7777          127.0.0.1:45786         ESTABLISHED -
tcp        0      0 127.0.0.1:7777          127.0.0.1:45784         ESTABLISHED 24027/python
tcp        0      0 127.0.0.1:45784         127.0.0.1:7777          ESTABLISHED 24040/python
tcp        0      0 127.0.0.1:7777          127.0.0.1:45788         ESTABLISHED -
tcp        0      0 127.0.0.1:45788         127.0.0.1:7777          ESTABLISHED 24062/python

通过ss -l|grep 7777命令也可以看到当前Recv-Q为2,也就是accept queue大小为2,而Send-Q显示的是backlog值的大小。这跟我们的预期是一致的。也就是说,这个时候,有1个连接被accept函数调用返回,另外2个连接处于ESTABLISHED状态,在accept queue中。

root@ssj-VirtualBox:/home/ssj/network# ss -l|grep 7777
tcp    LISTEN     2      1      127.0.0.1:7777                  *:*  

接下来,我们继续开启新的终端,运行python client.py,可以看到,虽然客户端那边新的连接是处于ESTABLISHED状态的,服务端这边新的连接会处于SYN_RECV状态,也就是在syn queue中,如果前面的连接没有关闭的,那么这个连接会一直处于SYN_RECV状态中,而此时服务端会重发SYN-ACK包5次(重试次数由net.ipv4.tcp_synack_retries参数决定)后,如果还是建立不了连接,会将该连接从syn queue中移除,这个重试时间分别间隔1,2,4,8,16秒,最后还要等32秒才移除,所以一共耗时1+2+4+8+16+32=63秒。

#处于SYN_RECV状态的连接
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               LISTEN      24027/python
tcp        0      0 127.0.0.1:45804         127.0.0.1:7777          ESTABLISHED 29314/python
tcp        0      0 127.0.0.1:45786         127.0.0.1:7777          ESTABLISHED 24049/python
tcp        0      0 127.0.0.1:7777          127.0.0.1:45786         ESTABLISHED -
tcp        0      0 127.0.0.1:7777          127.0.0.1:45784         ESTABLISHED 24027/python
tcp        0      0 127.0.0.1:7777          127.0.0.1:45804         SYN_RECV    -
tcp        0      0 127.0.0.1:45784         127.0.0.1:7777          ESTABLISHED 24040/python
tcp        0      0 127.0.0.1:7777          127.0.0.1:45788         ESTABLISHED -
tcp        0      0 127.0.0.1:45788         127.0.0.1:7777          ESTABLISHED 24062/python

另外一个有意思的问题是,如果在上面这种情况下,再起一个客户端会是什么结果呢?实验之后发现,再起一个客户端,新的连接在客户端看来开始会处于ESTABLISHED状态,也就是说服务端会完成三次握手,但是从服务端看来并没有建立连接,都没有放入到syn queue中,因为syn queue和accept queue都满了。所以等到客户端再输入数据 haha的时候,服务端会返回一个RST关闭该连接(如果之前的那个SYN_RECV的连接还在重试没有关闭,这个客户端连接则也会重试多次最后才会被服务端发送RST关闭)。

注意到,这里的实验室是以net.ipv4.tcp_abort_on_overflow = 0为前提的,这个设置关乎在accept queue满了的情况下,TCP对新来的连接如何处理,为0表示不做任何处理,也就是放入syn queue中,等重试超时再关闭连接。如果设置为net.ipv4.tcp_abort_on_overflow=1,并执行命令sysctl -p让配置生效,则在accept queue已经满的情况下(前面3个客户端连接成功),那么再运行新的客户端,这个时候服务端会马上返回RST,而不会将连接加入到syn queue中,也就是服务端不会有SYN_RECV状态的连接出现。

backlog设置为其他值时效果类比即可,通常来说,只要somaxconn值够大,处于 accept queue + 正在处理的连接 一共是backlog+2个,处于SYN_RECV的连接数目暂时没有发现设置规律。另外要强调的一点是,连接建立这个是TCP协议来完成的,比如Linux中就是内核协议栈完成的,并不依赖accept这个函数是否返回。比如我的实验中accept函数其实是阻塞在第一个连接的,并不妨碍其他连接建立。

2.2 Macos环境下测试

代码与Linux下面保持一样,先也是运行服务端,然后运行2个客户端,这个时候可以发现2个客户端可以正常连接处于ESTABLISHED状态,而当运行第三个客户端的时候可以发现,客户端连接一直处于SYN_SENT状态,并不断重试,直到一定次数后超时关闭(超时时间为75秒)。

ssj@ssj-mbp ~/Prog/network $ date; python client.py; date
2016年 8月29日 星期一 15时51分50秒 CST
[Errno 60] Operation timed out
2016年 8月29日 星期一 15时53分06秒 CST

这也就验证了Macos是采用类似第一种方案的队列设计,backlog为1则队列总大小为1,在队列中已经有一个连接的时候,其他的连接会不做任何处理,除非队列中的第一个连接已经处理并移除。

2.3 backlog总结

backlog参数在Linux和Macos中的含义并不一致,这一点需要区分。另外,backlog参数设置会影响服务器性能,要谨慎设置,apache和redis的网络部分的backlog的默认大小都是设置的511,对于一般的站点应该是足够的。

3 其他几个小点

如果客户端连接一个不可达的IP地址,在macos或Linux里面都会重试一定次数后提示timeout,注意如果你在客户端代码中设置了超时时间,如sock.settimeout(5),则超时时间以设置的为准,否则是系统默认超时时间。Linux的系统默认的超时约为为126秒,macos约为75秒。如修改client.py中连接的IP为123.0.0.1,即start_client('123.0.0.1', 7777),运行结果如下:

ssj@ssj-mbp ~/Prog/network $ date; python client2.py; date
2016年 8月29日 星期一 16时28分06秒 CST
[Errno 60] Operation timed out
2016年 8月29日 星期一 16时29分22秒 CST

而如果连接一个可达的IP地址,但是服务端对应的端口并没有处于监听状态,那么并不会重试,而是直接返回拒绝连接的错误。这两点在Linux和Macos上基本表现一致,除了超时时间略有不同外。

ssj@ssj-mbp ~/Prog/network $ date; python client2.py; date
2016年 8月29日 星期一 16时38分03秒 CST
[Errno 61] Connection refused
2016年 8月29日 星期一 16时38分04秒 CST

关于timeout再多说几句,服务端可以设置每个连接的timeout,如果在指定的timeout时间内没有收到数据,则像python语言会抛出timeout异常,这种情况下服务端就可以关闭客户端连接了。而客户端也可以设置timeout,这样在connect或者读取服务端数据的时候,如果超过指定时间没有连接成功或者读取到数据,也会抛出timeout异常。

4 总结

这篇笔记主要总结了一些常用的选项含义,并做了一些实验进行验证。第三篇准备总结TCP一些常见算法,第四篇为多进程网络编程和epoll等相关内容。

5 参考资料

  • how-tcp-backlog-works-in-linux
  • 耗子哥的TCP那些事儿
  • TCP/IP协议详解卷一部分章节

你可能感兴趣的:(TCP/IP协议笔记2-TCP编程重要选项含义解析)