0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

没有accept,能建立TCP连接吗?

电子工程师 来源:小林coding 作者:小林coding 2022-08-05 10:37 次阅读

正文

下面这个动图,是我们平时客户端和服务端建立连接时的代码流程。

df3b975e-1464-11ed-ba43-dac502259ad0.gif握手建立连接流程

对应的是下面一段简化过的服务端伪代码。

intmain()
{
/*Step1:创建服务器端监听socket描述符listen_fd*/
listen_fd=socket(AF_INET,SOCK_STREAM,0);

/*Step2:bind绑定服务器端的IP和端口,所有客户端都向这个IP和端口发送和请求数据*/
bind(listen_fd,xxx);

/*Step3:服务端开启监听*/
listen(listen_fd,128);

/*Step4:服务器等待客户端的链接,返回值cfd为客户端的socket描述符*/
cfd=accept(listen_fd,xxx);

/*Step5:读取客户端发来的数据*/
n=read(cfd,buf,sizeof(buf));
}

估计大家也是老熟悉这段伪代码了。

需要注意的是,在执行listen()方法之后还会执行一个accept()方法。

一般情况下,如果启动服务器,会发现最后程序会阻塞在accept()里。

此时服务端就算ok了,就等客户端了。

那么,再看下简化过的客户端伪代码。

intmain()
{
/*Step1:创建客户端端socket描述符cfd*/
cfd=socket(AF_INET,SOCK_STREAM,0);

/*Step2:connect方法,对服务器端的IP和端口号发起连接*/
ret=connect(cfd,xxxx);

/*Step4:向服务器端写数据*/
write(cfd,buf,strlen(buf));
}

客户端比较简单,创建好socket之后,直接就发起connect方法。

此时回到服务端,会发现之前一直阻塞的accept方法,返回结果了

这就算两端成功建立好了一条连接。之后就可以愉快的进行读写操作了。

那么,我们今天的问题是,如果没有这个accept方法,TCP连接还能建立起来吗?

其实只要在执行accept() 之前执行一个 sleep(20),然后立刻执行客户端相关的方法,同时抓个包,就能得出结论。

df544aa6-1464-11ed-ba43-dac502259ad0.png不执行accept时抓包结果

从抓包结果看来,就算不执行accept()方法,三次握手照常进行,并顺利建立连接。

更骚气的是,在服务端执行accept()前,如果客户端发送消息给服务端,服务端是能够正常回复ack确认包的。

并且,sleep(20)结束后,服务端正常执行accept(),客户端前面发送的消息,还是能正常收到的。

通过这个现象,我们可以多想想为什么。顺便好好了解下三次握手的细节。

三次握手的细节分析

我们先看面试八股文的老股,三次握手。

df63c2ce-1464-11ed-ba43-dac502259ad0.pngTCP三次握手

服务端代码,对socket执行bind方法可以绑定监听端口,然后执行listen方法后,就会进入监听(LISTEN)状态。内核会为每一个处于LISTEN状态的socket 分配两个队列,分别叫半连接队列和全连接队列

df74a92c-1464-11ed-ba43-dac502259ad0.png每个listen Socket都有一个全连接和半连接队列

半连接队列、全连接队列是什么

df826e68-1464-11ed-ba43-dac502259ad0.png半连接队列和全连接队列
  • 半连接队列(SYN队列),服务端收到第一次握手后,会将sock加入到这个队列中,队列内的sock都处于SYN_RECV 状态。
  • 全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的sock取出,放到全连接队列中。队列里的sock都处于 ESTABLISHED状态。这里面的连接,就等着服务端执行accept()后被取出了。

看到这里,文章开头的问题就有了答案,建立连接的过程中根本不需要accept() 参与, 执行accept()只是为了从全连接队列里取出一条连接。

我们把话题再重新回到这两个队列上。

虽然都叫队列,但其实全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表

df8f7cd4-1464-11ed-ba43-dac502259ad0.png半连接全连接队列的内部结构

为什么半连接队列要设计成哈希表

先对比下全连接里队列,他本质是个链表,因为也是线性结构,说它是个队列也没毛病。它里面放的都是已经建立完成的连接,这些连接正等待被取走。而服务端取走连接的过程中,并不关心具体是哪个连接,只要是个连接就行,所以直接从队列头取就行了。这个过程算法复杂度为O(1)

半连接队列却不太一样,因为队列里的都是不完整的连接,嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了,则需要从队列里把相应IP端口的连接取出,如果半连接队列还是个链表,那我们就需要依次遍历,才能拿到我们想要的那个连接,算法复杂度就是O(n)。

而如果将半连接队列设计成哈希表,那么查找半连接的算法复杂度就回到O(1)了。

因此出于效率考虑,全连接队列被设计成链表,而半连接队列被设计为哈希表。

怎么观察两个队列的大小

查看全连接队列

#ss-lnt
StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
LISTEN0128127.0.0.1:46269*:*

通过ss -lnt命令,可以看到全连接队列的大小,其中Send-Q是指全连接队列的最大值,可以看到我这上面的最大值是128Recv-Q是指当前的全连接队列的使用值,我这边用了0个,也就是全连接队列里为空,连接都被取出来了。

当上面Send-QRecv-Q数值很接近的时候,那么全连接队列可能已经满了。可以通过下面的命令查看是否发生过队列溢出

#netstat-s|grepoverflowed
4343timesthelistenqueueofasocketoverflowed

上面说明发生过4343次全连接队列溢出的情况。这个查看到的是历史发生过的次数

如果配合使用watch -d 命令,可以自动每2s间隔执行相同命令,还能高亮显示变化的数字部分,如果溢出的数字不断变多,说明正在发生溢出的行为。

#watch-d'netstat-s|grepoverflowed'
Every2.0s:netstat-s|grepoverflowedFriSep1709452021

4343timesthelistenqueueofasocketoverflowed

查看半连接队列

半连接队列没有命令可以直接查看到,但因为半连接队列里,放的都是SYN_RECV 状态的连接,那可以通过统计处于这个状态的连接的数量,间接获得半连接队列的长度。

#netstat-nt|grep-i'127.0.0.1:8080'|grep-i'SYN_RECV'|wc-l
0

注意半连接队列和全连接队列都是挂在某个Listen socket上的,我这里用的是127.0.0.1:8080,大家可以替换成自己想要查看的IP端口

可以看到我的机器上的半连接队列长度为0,这个很正常,正经连接谁会没事老待在半连接队列里。

当队列里的半连接不断增多,最终也是会发生溢出,可以通过下面的命令查看。

#netstat-s|grep-i"SYNstoLISTENsocketsdropped"
26395SYNstoLISTENsocketsdropped

可以看到,我的机器上一共发生了26395次半连接队列溢出。同样建议配合watch -d 命令使用。

#watch-d'netstat-s|grep-i"SYNstoLISTENsocketsdropped"'
Every2.0s:netstat-s|grep-i"SYNstoLISTENsocketsdropped"FriSep1708382021

26395SYNstoLISTENsocketsdropped

全连接队列满了会怎么样?

如果队列满了,服务端还收到客户端的第三次握手ACK,默认当然会丢弃这个ACK。

但除了丢弃之外,还有一些附带行为,这会受 tcp_abort_on_overflow 参数的影响。

#cat/proc/sys/net/ipv4/tcp_abort_on_overflow
0
  • tcp_abort_on_overflow设置为 0,全连接队列满了之后,会丢弃这个第三次握手ACK包,并且开启定时器,重传第二次握手的SYN+ACK,如果重传超过一定限制次数,还会把对应的半连接队列里的连接给删掉。
df9c5cc4-1464-11ed-ba43-dac502259ad0.pngtcp_abort_on_overflow为0
  • tcp_abort_on_overflow设置为 1,全连接队列满了之后,就直接发RST给客户端,效果上看就是连接断了。

这个现象是不是很熟悉,服务端端口未监听时,客户端尝试去连接,服务端也会回一个RST。这两个情况长一样,所以客户端这时候收到RST之后,其实无法区分到底是端口未监听,还是全连接队列满了

dfaa62a6-1464-11ed-ba43-dac502259ad0.pngtcp_abort_on_overflow为1

半连接队列要是满了会怎么样

一般是丢弃,但这个行为可以通过 tcp_syncookies 参数去控制。但比起这个,更重要的是先了解下半连接队列为什么会被打满。

首先我们需要明白,一般情况下,半连接的"生存"时间其实很短,只有在第一次和第三次握手间,如果半连接都满了,说明服务端疯狂收到第一次握手请求,如果是线上游戏应用,能有这么多请求进来,那说明你可能要富了。但现实往往比较骨感,你可能遇到了SYN Flood攻击

所谓SYN Flood攻击,可以简单理解为,攻击方模拟客户端疯狂发第一次握手请求过来,在服务端憨憨地回复第二次握手过去之后,客户端死活不发第三次握手过来,这样做,可以把服务端半连接队列打满,从而导致正常连接不能正常进来。

dfcd5ec8-1464-11ed-ba43-dac502259ad0.pngsyn攻击

那这种情况怎么处理?有没有一种方法可以绕过半连接队列

有,上面提到的tcp_syncookies派上用场了。

#cat/proc/sys/net/ipv4/tcp_syncookies
1

当它被设置为1的时候,客户端发来第一次握手SYN时,服务端不会将其放入半连接队列中,而是直接生成一个cookies,这个cookies会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。

dfd804cc-1464-11ed-ba43-dac502259ad0.pngtcp_syncookies=1

会有一个cookies队列吗

生成是cookies,保存在哪呢?是不是会有一个队列保存这些cookies?

我们可以反过来想一下,如果有cookies队列,那它会跟半连接队列一样,到头来,还是会被SYN Flood 攻击打满。

实际上cookies并不会有一个专门的队列保存,它是通过通信双方的IP地址端口、时间戳、MSS信息进行实时计算的,保存在TCP报头seq里。

dff73630-1464-11ed-ba43-dac502259ad0.pngtcp报头_seq的位置

当服务端收到客户端发来的第三次握手包时,会通过seq还原出通信双方的IP地址端口、时间戳、MSS,验证通过则建立连接。

cookies方案为什么不直接取代半连接队列?

目前看下来syn cookies方案省下了半连接队列所需要的队列内存,还能解决 SYN Flood攻击,那为什么不直接取代半连接队列?

凡事皆有利弊,cookies方案虽然能防 SYN Flood攻击,但是也有一些问题。因为服务端并不会保存连接信息,所以如果传输过程中数据包丢了,也不会重发第二次握手的信息。

另外,编码解码cookies,都是比较CPU的,利用这一点,如果此时攻击者构造大量的第三次握手包(ACK包),同时带上各种瞎编的cookies信息,服务端收到ACK包以为是正经cookies,憨憨地跑去解码(耗CPU),最后发现不是正经数据包后才丢弃。

这种通过构造大量ACK包去消耗服务端资源的攻击,叫ACK攻击,受到攻击的服务器可能会因为CPU资源耗尽导致没能响应正经请求。

e0037ae4-1464-11ed-ba43-dac502259ad0.gifack攻击

没有listen,为什么还能建立连接

那既然没有accept方法能建立连接,那是不是没有listen方法,也能建立连接?是的,之前写的一篇文章提到过客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有listen,就能建立连接。

当时文章最后也留了个疑问,没有listen,为什么还能建立连接?

我们知道执行listen方法时,会创建半连接队列和全连接队列。

三次握手的过程中会在这两个队列中暂存连接信息。

所以形成连接,前提是你得有个地方存放着,方便握手的时候能根据IP端口等信息找到socket信息。

那么客户端会有半连接队列吗?

显然没有,因为客户端没有执行listen,因为半连接队列和全连接队列都是在执行listen方法时,内核自动创建的。

但内核还有个全局hash表,可以用于存放sock连接的信息。这个全局hash表其实还细分为ehash,bhash和listen_hash等,但因为过于细节,大家理解成有一个全局hash就够了,

在TCP自连接的情况中,客户端在connect方法时,最后会将自己的连接信息放入到这个全局hash表中,然后将信息发出,消息在经过回环地址重新回到TCP传输层的时候,就会根据IP端口信息,再一次从这个全局hash中取出信息。于是握手包一来一回,最后成功建立连接。

TCP 同时打开的情况也类似,只不过从一个客户端变成了两个客户端而已。

总结

  • 每一个socket执行listen时,内核都会自动创建一个半连接队列和全连接队列。
  • 第三次握手前,TCP连接会放在半连接队列中,直到第三次握手到来,才会被放到全连接队列中。
  • accept方法只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系
  • 出于效率考虑,虽然都叫队列,但半连接队列其实被设计成了哈希表,而全连接队列本质是链表。
  • 全连接队列满了,再来第三次握手也会丢弃,此时如果tcp_abort_on_overflow=1,还会直接发RST给客户端。
  • 半连接队列满了,可能是因为受到了SYN Flood攻击,可以设置tcp_syncookies,绕开半连接队列。
  • 客户端没有半连接队列和全连接队列,但有一个全局hash,可以通过它实现自连接或TCP同时打开。
审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 服务器
    +关注

    关注

    12

    文章

    8111

    浏览量

    82496
  • TCP
    TCP
    +关注

    关注

    8

    文章

    1270

    浏览量

    78282
  • 代码
    +关注

    关注

    30

    文章

    4555

    浏览量

    66746

原文标题:阿里二面:没有 accept,能建立 TCP 连接吗?

文章出处:【微信号:小林coding,微信公众号:小林coding】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    TCP和UDP建立连接的差异和可靠性的差异

    TCP 建立连接需要经过三次握手,同时 TCP 断开连接需要经过四次挥手,这也表示 TCP 是一
    发表于 09-02 16:10 603次阅读

    如何标识一个TCP连接

    tcp应用中,server事先在某个固定端口监听,client主动发起连接,经过三路握手后建立tcp连接。那么对单机,其最大并发
    的头像 发表于 10-10 10:33 2398次阅读

    STM32H7+FREERTOS+LWIP建立TCP连接连接不稳定怎么解决?

    利用ST的Demo建立TCP连接,但是当建立TCP连接超过4个时,
    发表于 04-25 06:05

    6678的sy***ios中的NDK的例程hello和clint中,使用TCP没有看到bing和listen accept等函数

    6678的sy***ios中的NDK的例程hello和clint中,使用TCP没有看到bing和listen accept等函数,请问为什么没有
    发表于 06-21 15:46

    为什么建立TCP连接有时成功有时失败?

    时开发板自带的里程改的,TCP这部分基本没有改,只是增加了自己的应用进去。首次使用以太网接口,还不太清楚建立连接的机理,有了解这块的朋友吗,请问建立
    发表于 09-19 04:36

    配置静态IP地址时,没有TCP连接可以建立,对ping没有响应

    新的,并且我正在尝试使用上面提到的具有集成PHY,25MHz的控制器来制造小型TCP服务器。外部OSC,没有外部的32 kHz OSC。我面临的几个问题是:1。当配置静态IP地址时,没有TCP
    发表于 11-04 07:03

    RT-Thread socket编程无法建立TCP连接是何原因

    脑socket调试工具无法建立TCP连接的问题,代码如下:SOCKET s,s2;struct sockaddr_in local_addr,foreign_addr;int len;s
    发表于 04-25 17:14

    大神告诉你TCP建立连接为什么是三次握手

    所谓三次握手(Three-Way Handshake)即建立TCP连接,是指建立一个TCP连接
    的头像 发表于 04-16 11:43 9985次阅读
    大神告诉你<b class='flag-5'>TCP</b><b class='flag-5'>建立</b><b class='flag-5'>连接</b>为什么是三次握手

    TCP通信通过网络调试助手与S7-1200建立TCP连接

    S7-1200 V4.5 版本开始支持网络视图组态开放式用户通信连接,不需要在程序中调用 TCON 等建立连接指令,只需要调用发送接收指令即可实现数据的收发。下面以 TCP 通信为例,
    的头像 发表于 07-06 15:39 9268次阅读

    要是没有一端进行监听是否可以建立TCP连接呢?

    TCP 网络通信过程中,我们都是先有 server 端调用 listen 监听某个端口号,然后 client 向 server 发起连接请求,最终建立连接
    的头像 发表于 11-16 17:42 1911次阅读
    要是<b class='flag-5'>没有</b>一端进行监听是否可以<b class='flag-5'>建立</b>起<b class='flag-5'>TCP</b><b class='flag-5'>连接</b>呢?

    TCP建立连接概述及三次握手、四次挥手的流程

    具备上述四个条件后A获取B的信息是有要求的,根本上的要求是数据信道可靠,就是平时所说的可靠连接,那么如何保证连接的可靠性呢,TCP协议就是靠确认应答机制、超时重传机制等保证连接可靠性的
    的头像 发表于 03-23 15:57 765次阅读
    <b class='flag-5'>TCP</b><b class='flag-5'>建立</b><b class='flag-5'>连接</b>概述及三次握手、四次挥手的流程

    什么是Socket连接?Socket与TCP连接的关系

    主机 A 的应用程序必须通过 Socket 建立连接才能与主机B的应用程序通信,而建立 Socket 连接需要底层 TCP/IP 协议来
    发表于 03-31 15:10 742次阅读

    TCP连接建立与中止

    TCP三次握手其实,网络上的传输是没有连接的, TCP 是一样的 TCP 所谓的 “连接”,其实
    的头像 发表于 10-08 16:52 455次阅读

    TCP连接建立中的异常

        建连接时SYN超时问题 如果 server 端因为某种情况没有收到 client 回来的 ACK,那么,这个连接处还处于一个未建立的状态。于是,server端如果在一定时间内
    的头像 发表于 10-08 17:01 518次阅读

    TCP的长连接和短连接

    TCP在真正开始进行数据传输之前,Server 和 Client 之间必须建立一个连接。当数据传输完成后,双方不再需要这个连接时,就可以释放这个连接
    的头像 发表于 11-13 10:46 511次阅读