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中取连接即可。

图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大小,更新:syn queue大小计算比较复杂,详见 https://juejin.cn/post/6844904071367753736)。如果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,可以看到,客户端这边新的连接会处于SYN_SENT状态,服务端直接丢弃了该syn包,客户端会不断重试,直到超时。

注意到,这里的实验室是以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 参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容