关于TCP同时打开-无需Listener的TCP连接建立过程

六一儿童节的大清早,竟然用这么一篇技术博客来总结童真,也挺好。我把早上的发的朋友圈文字附于文后,以应景。


周中写了一篇关于socket查找的文章,次日,也就是昨天上午,收到一封反馈邮件,好快,十分高兴能和大家一起进行技术讨论。

在这封邮件里,一位朋友提出了一种查询socket的优化方案,瞄准的是__inet_lookup函数,我以3.10内核为例,将该函数贴如下:

static inline struct sock *__inet_lookup(struct net *net,
                     struct inet_hashinfo *hashinfo,
                     const __be32 saddr, const __be16 sport,
                     const __be32 daddr, const __be16 dport,
                     const int dif)
{
    u16 hnum = ntohs(dport);
    // 1.首先查询establish hash表
    struct sock *sk = __inet_lookup_established(net, hashinfo,
                saddr, sport, daddr, hnum, dif);
    // 2.在查询establish hash表未果时再查询Listener hash表
    return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport,
                         daddr, hnum, dif);
}

考虑到一台服务器支撑10万+的并发连接(所谓的并发连接均在establish hash表中)并不是什么难事,那么每一次新建连接都要先去10万+数量的hash表中去查询一遍,如果出现过长的hash冲突链表(如果你不使用理想中的完美hash,这几乎是不可避免的),便会严重影响scalable特性,于是能不能如下进行优化呢:

static inline struct sock *__inet_lookup(struct net *net,
                     struct inet_hashinfo *hashinfo,
                     const __be32 saddr, const __be16 sport,
                     const __be32 daddr, const __be16 dport,
                     const int dif,
                     // 新增一个TCP头参数
                     const struct tcphdr *th)
{
    u16 hnum = ntohs(dport);
    struct sock *sk;

    if (!th->syn) // 如果有syn标识则直接查询Listener hash,不再查询establish hash!
         sk = __inet_lookup_established(net, hashinfo,
                saddr, sport, daddr, hnum, dif);

    return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport,
                         daddr, hnum, dif);
}

非常不错的想法,深入到了细节。但是如果你对TCP状态机有深入的理解,就会发现这里的这个优化是有问题的。

问题出在下面的两个细节上:

  • TCP连接主动断开后会进入timewait状态,该状态的连接依然在establish hash中;
  • TCP可以支持双向打开,没有Listener的情况下同时发送syn包建立连接。

在查询Listener hash之前先查询establish hash正是为了支持上述两种场景的,具体的支持方法如下:

  • 如果一个syn包命中了timewait状态的连接,必须检查其timestamp确认连接可重用,要么允许建立连接要么重置;
  • 如果一个syn包命中了一个已经发送过syn包的连接,则执行“同时打开”握手流程。

所以说,虽然这位朋友的这个优化思路是没有问题的,但是涉及到TCP协议的具体细节时却不可行。

嗯,结论就是这样,即在为一个TCP数据包查询socket的时候,必须首先查询establish hash表,然后再查Listener hash表。


有了结论并不意味着本文的结束,接下来的篇幅我将实验展示TCP同时打开的过程,给出一个观感上的印象。所需工具很简单,netcat和systemtap即可,能不编程就不编程,毕竟我编程编的不好…

好了,开始实验,准备两台机器,我这里分别是下面两台VMWare虚拟机:

Host 1:192.168.44.138/24
Host 2:192.168.44.248/24

两台机器直连在同一个网段,这样最简单,因为这个实验只是TCP层面的语义测试,与IP层无关。两台机器同时安装netcat这个“瑞士军刀”,然后在两台机器上分别确认没有TCP 1234这个端口在侦听,之后,两台机器上执行下面的命令:

# Host 1
root@debian:/home/zhaoya# while true; do nc -p 1234 192.168.44.248 1234 -v;done
192.168.44.248: inverse host lookup failed: No address associated with name
(UNKNOWN) [192.168.44.248] 1234 (?) : Connection refused
192.168.44.248: inverse host lookup failed: No address associated with name
# 连接建立!
(UNKNOWN) [192.168.44.248] 1234 (?) open
aaaa # 在一个终端敲入字符会在另一个机器的终端显示出来
bbbb
# Host 2
[root@localhost ~]# while true; do nc -p 1234 192.168.44.138 1234 -v;done
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connection refused.
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connection refused.
Ncat: Version 7.50 ( https://nmap.org/ncat )
# 连接建立!
Ncat: Connected to 192.168.44.138:1234.
aaaa # 在一个终端敲入字符会在另一个机器的终端显示出来
bbbb

这个时候,我们看一下连接状态:

[root@localhost ~]# ss -antp
State      Recv-Q Send-Q                                       Local Address:Port                                                      Peer Address:Port              
...
ESTAB      0      0                                           192.168.44.248:1234                                                    192.168.44.138:1234                users:(("nc",pid=7925,fd=3))

可见,两边同时都是运行的TCP客户端在调用connect,两边均bind到了1234这个端口同时均没有Listen这个端口,最终连接还是建立了,状态为ESTABLISH。两边的时序图如下:
这里写图片描述
状态转换图如下:
这里写图片描述

似乎比较简单和清晰,然而这一切是怎么发生的,有必要确认一下以加深印象。

其实,现在就一个问题,作为TCP客户端调用connect系统调用主动打开连接,发送SYN包,该连接会在什么时候加入到establish hash表中呢?我们来detect一下究竟。依然基于3.10内核:

[root@localhost ~]# uname -r
3.10.0-862.2.3.el7.x86_64

静态分析代码的话,我们可以看到tcp_v4_connect中我们感兴趣的逻辑:

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    ...
    /* Socket identity is still unknown (sport may be zero).
     * However we set state to SYN-SENT and not releasing socket
     * lock select source port, enter ourselves into the hash tables and
     * complete initialization after this.
     */
    tcp_set_state(sk, TCP_SYN_SENT);
    err = inet_hash_connect(&tcp_death_row, sk);
    ...
}

于是我们在inet_hash_connect处打桩,看看实验过程中是不是调用了这里,如果是的话__inet_hash_connect将会被调用,这个函数将socket加入到了establish hash表中。

systemtap是一个好工具,和crash侧重于静态分析不同,systemtap可以动态插桩,甚至可以搞一个trick,实现类似内核热补丁的功能,简直太强大,虽然其底层依赖的kprobe有点不太易用看起来让人觉得没什么玩头,但systemtap的封装完美解决了可玩性问题。

我们先看一下inet_hash_connect函数都有什么变量可用:

[root@localhost ~]# stap -L 'kernel.function("inet_hash_connect")'
kernel.function("inet_hash_connect@net/ipv4/inet_hashtables.c:593") $death_row:struct inet_timewait_death_row* $sk:struct sock*

然后我们确认逻辑进入到了这个函数,另起一个终端,执行下面的systemtap命令:

[root@localhost ~]# stap -e 'probe kernel.function("inet_hash_connect") {printf("OK\n");}'

重新做实验,观测systemtap命令终端的输出:

[root@localhost ~]# stap -e 'probe kernel.function("inet_hash_connect") {printf("OK\n");}'
OK
OK
OK
OK
OK
OK
OK
OK

嗯,确实印证了静态分析的结论,即socket在调用connect的时候就会把自己加入到establish hash表,虽然它此时连syn都还没有发送

为了实际确认,编写下面的systemtap脚本,探测tcp_v4_connect调用inet_hash_connect前后的sk->sk_hash值的变化。

#!/usr/bin/stap -g

// 在inet_hash_connect调用前探测sk_hash的值
probe kernel.function("inet_hash_connect")
{
        printf("pre sk_hash value:%d\n", @cast($sk, "struct sock")->__sk_common->skc_hash);
}

// 在inet_hash_connect调用结束返回前再次探测sk_hash的值
probe kernel.function("inet_hash_connect").return
{
        printf("post sk_hash value:%d\n", @cast($sk, "struct sock")->__sk_common->skc_hash);
}

重新做实验,以下是systemtap的结果:

[root@localhost network]# stap -u ./detect_skhash.stp 
WARNING: confusing usage, consider @entry($sk) in .return probe: identifier '$sk' at ./init_cwnd:14:42
 source:        printf("post sk_hash value:%d\n", @cast($sk, "struct sock")->__sk_common->skc_hash);
                                                        ^
pre sk_hash value:0
post sk_hash value:610642400

确认了确实就是在inet_hash_connect中将该socket加入到establish hash表的,确切的讲,应该是在它调用的__inet_check_established函数中。


解释结束。这就是Linux关于TCP同时打开的实现细节的一部分,可以看出,整个流程没有Listener socket的参与,并且正确地处理了syn握手。

最后,我来展示一个好玩的TCP连接,TCP咬尾蛇,即自己连接自己bind的端口:

[root@localhost ~]# nc -p 2234 127.0.0.1 2234
aaaaa # 无论你敲入什么
aaaaa # 当前的终端就会回显什么...


wwwwwwwwwwwwww
wwwwwwwwwwwwww

用ss命令确认一下:

[root@localhost ~]# ss -ntp
# 注意,只有一个2234到2234的TCP establish连接,没有Listener。
State      Recv-Q Send-Q        Local Address:Port      Peer Address:Port   
...
ESTAB      0      0             127.0.0.1:2234          127.0.0.1:2234                users:(("nc",pid=3576,fd=3))
...                                                  

自己用前面描述的原理分析一下,应该能得知其所以然。


最后,除了TCP同时打开,之所以要先查establish hash还有一个原因,与timewait套接字相关,关于timewait的实验,请参见我在2013年写的一篇文章:
TCP的TIME_WAIT快速回收与重用https://blog.csdn.net/dog250/article/details/13760985
这个文章里也设计了一系列的实验,目的是确认timewait的影响。

附:童趣和童真

今天六一儿童节,上班路上,我来说说在古城安阳度过的我的童年。

小时候过六一儿童节,人民公园是必须要去的,我一直都比较喜欢动物,所以进了公园一般都是先去动物园,然后顺着东边的长廊走到花卉园,最里面有一棵很大的树,由于分叉比较低我一般会躺在枝丫处假装休息一会儿,出来花卉园再沿着长廊走一会儿,随后就到了游乐园,有旱冰场,游泳池(这两样我至今都不会…),还有滑梯,转圈升降的飞机,那个疯狂老鼠是后来才有的,至少我小学一年级是没有的。对了,还有人工湖,上面有游船,一般可以自己划桨,我没有坐过,好像是因为太贵了。。。

玩够了就回家,在人民公园门口有卖小金鱼的,我每次去都会买,然而养不了几天。。。大概走路不到半小时就能到家,沿着东环城护城河拐进红庙街,路过我的小学也会像小小一样很自豪的唠唠叨叨介绍“这是我的学校,这里是一年级3班,那里是三年级2班,这个房子后是后操场。。。”,然后经过老板娘和姨儿的小摊,很快就到家了,我父母现在还住在那一块,只是周围都被规划了,人民公园成了新东区的边缘,周边都是高楼大厦,再也没了那份寂静。

还在安阳的朋友估计没我这般感慨,对一座城市的印象,往往当你离开了,才能感知,每一座城市,都是围城。

小时候每过六一,我都特别想去农村玩,但最多也就到郊区,纱厂铁路大桥下面那个郭家湾,也就是现在的洹园,我和老婆谈恋爱的地方,那里有小蝌蚪可以抓,小时候我妈每周末都会骑自行车带我去玩一下午。。。我很羡慕那些农村的孩子,每天都能抓鱼,捉小蝌蚪,逮蜻蜓蝈蝈蚂蚱螳螂,钓青蛙。。。还能肆意田间奔跑,这些在城市里不存在,所以我只能假装去营造氛围,比如会找个坑边湖边逮蜻蜓,有时还挽起裤管假装要下去的样子,其实我心里是不敢的,傍晚路灯刚开,我就去路灯下蹲点了,手里拎个瓶子,在墙角旮旯偷清洁工阿姨一把扫帚,煞有其事的捣鼓着逮那些会飞的昆虫,显得自己很专业,其实都是装的。。。

在课外读物里知道农村可以抓蛇,我也想玩,既然没法抓蛇,去菜市场买总是可以吧,自己跑到健康路市场,还真有,但太大了有点害怕,后来很久,路过一家饭店看到有蛇逃了,我看机会来了,用手掐住蛇拔腿就跑,这个时候,发现后面一群小孩子跟着我跑,脑海里竟然出现了我在那些散文里读到的农村的场景,我的内心是满足的,一直跑到我家附近的后仓坑,把蛇放了。。。

后来我爸妈给我买了小霸王游戏机,那种必须用电视当显示器的盒子游戏机,有点像现在的外置机顶盒,但必须插入游戏卡,说白了小霸王游戏机就是一台普通的通用处理机,真正的那些好玩的游戏逻辑全部都在卡带的存储芯片里,所以说游戏机本身才100多块钱,而好玩的6合1卡带要好几百。。。炎热的假期就基本就游戏搞起了,还记得那些经典游戏吗?还记得上上下下左右左右BA吗?超级马里奥,魂斗罗,双截龙,坦克大战,赤色要塞,绿色兵团,松鼠大作战,忍者神龟,忍者龙剑传,沙罗曼蛇,波斯王子,冒险岛,龙牙。。。对了,还有俄罗斯方块,这个最经典了。不过最后我把游戏机摔了,好像是因为一直没能通关,也够任性的。

至于大街上游戏厅的街机,基本上也就街霸,格斗三人组,三国,抢不到机器时偶尔玩玩雷电。离我家最近的就是铁狮口那里的游戏厅,经常去。

伴随我童年成长的一个不得不提的东西,那就是《七龙珠》(together with《圣斗士》),直到现在还在追剧,已经伴随了我30年,据说《龙珠超》的续集7月份又要开播了,爱奇艺会员已经买好!
。。。。
愿所有人都能保持一份童趣和童真,祝大家六一儿童节快乐!!

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页