TCP

作者: 红豆汤来两大碗 | 来源:发表于2020-11-19 22:44 被阅读0次

    [toc]

    TCP

    TCP总流程

    image-20201116221214607.png

    客户端、服务端:调用socket函数,创建套接字描述符

    服务端调用bind函数,绑定端口和ip

    • 端口:为了内核收到数据包知道交给那个应用进程

    • ip:机器可能有多个网卡,需要监听哪个网卡

    服务端调用listen函数,将套接字转换为被动套接字。不主动连接别人,需要别人联系我们。

    服务端阻塞在accept函数,等待客户端连接请求到来

    客户端调用connect函数,向服务器发动主动连接,在connect函数内部主动发起三次握手过程。

    客户端和服务端进行read和write数据。tcp是全双工通信的,read和write是双向的。

    通信完毕后,其中一方调用close函数向对方发送一个分包,表示不再发数据了。对方读到read函数返回0,感知到对方关闭连接请求,随后自己也调用close函数,通知对方我也关闭了。

    TCP头部

    image-20201116221846648.png

    源端口、目的端口:决定发送到哪个应用进程

    序列号:解决乱序问题

    确认号:每发送一个数据包需要对方的确认包,没有收到就会重新发送

    标志位:ACK为1,确认好就行。RST表示连接已经重置。SYN表示发起一个连接请求。FIN表示关闭一个连接。

    内核角度看TCP如何维护一个socket

    image-20201116222208687.png

    linux中一切皆文件,socket也以文件形式存在。

    调用socket函数立刻返回一个文件描述符。要在pcb里已打开的文件描述符列表中占有一项,该项的内容就是一个指针指向内核中的文件表,文件表最终找到inode

    对网络文件来说,inode并不是真正指向磁盘文件,而指向struct socket类型结构体。

    struct socket结构中有一个接受队列和一个发送队列。read函数数据写到发送队列中,从网络接到的TCP包放到接收队列中。从而实现全双工通信。

    TCP三次握手流程:

    image-20201116222634590.png

    TCP协议栈主动向服务端发送SYN包,告诉服务器自己的序列号是 i,客户端进去SYN-SENT状态。

    服务端内核协议栈接收到SYN包,给客户端回应ACK,同时发送自己的SYN序列号,服务端进去SYN-RCVD状态。

    客户端收到服务端的ACK后,从connect函数返回。

    服务端收到客户端的应答包,从accept函数返回。

    TCP过程中包的内容

    TCP头部的标志位:(1位bit)布尔域控制连接状态,三个重要标志位:SYN 创建连接,FIN 结束连接,ACK 确认接收数据

    序列号和确认号:32位,序列号用来跟踪该端发送的数据量,确认号用来通知发送端数据成功接收。初始序列号是随机的,通常使用相对序列号/确认号,跟踪更小的相对序列号/确认号会相对容易一些。

    TCP建立到发送信息间包的内容。

    1. 客户端主动发起连接请求。TCP会话的每一端的序列号(相对)和序列号都从0开始。此时通话未开始,不显示确认号。

    2. 服务端响应客户端的请求。响应中附带序列号0(服务端的第一个包),相对确认号1(表示收到了客户端的SYN包)。Note:尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位。含有SYN或FIN标志位的包并不携带有效数据。

    3. 客户端回应。用确认号1响应服务端的序列号0,同时发送自己的序列号。由于服务端发送的包确认收到客户端的SYN包,客户端的序列号由0变为1。完成TCP三次握手

    4. 客户端发第一个携带有效数据的包,HTTP请求。因为没有发送数据,序列号依然为1。因为没有接收数据,确认号也保持1不变。Note:准确来说是,客户端发送的HTTP请求。包中有效数据的长度为725字节。

    5. 服务器收到HTTP请求,回应ACK包。服务端发送ACK包来确认客户端在包4中发来的数据。服务端的序列号保持为1不变,确认号的值增加了725(包4的有效长度)变为726。告知客户端,当前总共收到了726字节的数据。

    6. 服务端返回HTTP响应的开始。由于服务端在该包之前返回的包中都不带有有效数据,序列号依然为1。该包带有1448字节的有效数据。

    7. 客户端回应。由于上个数据包的发送,客户端的序列号增长至726。从服务端接收了1448字节的数据,客户端的确认号由1增长至1449。

    8. 重复6、7。客户端的序列号一直是726(客户端除了最初的725字节数据没有再向服务端发送数据)。服务端的序列号持续增长(不断的发送HTTP响应)。Note:序列号为当前端成功发送的数据位数,确认号为当前端成功接收的数据位数,SYN标志位和FIN标志位也要占1位

    TCP关闭连接。

    1. 客户端认为通信已完成,决定结束连接,发FIN包。客户端发送设置了FIN标志位的包38,其确认号和之前的包37一样。

    2. 服务端回应客户端期望关闭连接的请求。服务端确认号加1,同时设置当前包的FIN标志位。(分两次发?)

    3. 客户端回应。序列号727,确认号加1的方式确认服务端的FIN包。

    包序号以几开始?

    image-20201116223000872.png

    为了防止网络中被延时的数据包以后又被传送,导致连接一方对他错误解释。所以客户端和服务器之间的包序号不能以 1 号开始变时。

    每一个连接都要有不同的起始同步序号

    image-20201117144122425.png

    listen函数内部建立了未完成三次握手队列,称之为SYN队列。已完成三次握手的队列,ACCRPT队列。

    accept函数实现就是阻塞等待,然后从ACCEPT队列取出一个连接,创建出新的socket,将原核目标IP和端口填入socket,然后返回。

    connect函数实现就是创建connect队列,然后将当前要发送的信息放入connect队列,服务端收到syn信息时放入SYN队列,同时回复一个SYN和ACK。客户端收到ACK,从connect队列取出,然后connect返回。

    服务端收到客户端的ACK的ACK后,从SYN队列删除,插入到ACCEPT队列,阻塞在pthread_cond_wait的accept函数,由于ACCEPT队列有数据而被唤醒,取走队头节点,然后accept函数返回。

    补充

    1. 序列号是“随机”的;
    2. ACK +1的问题;

    序列号应该不是随机的,而是由双方协商的,协商之前最开始的值由TCP运输层生成。 它可能是上一个该端口的序列号+1 ,原因是为了避免上次已断开的连接,还存在网络中延迟到达的TCP报文段的序号与当前连接中等待报文段的序号相同,以至于认为是这一次连接的包,发生错误。

    关于ACK+1,在文中“包2”中如下所述:

    “需要注意的是,尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位(并不会对有效数据的计数产生影响,因为含有SYN或FIN标志位的包并不携带有效数据)”这实际上是误解,因此误导了很多人。

    在停-等协议中,ACK确认的是当前包的序列号Seq=1 ,接收端会传回 ACK=1 。标识接收到了Seq=1的包,这是我们认为理所当然的。

    但是在TCP协议中不是这样, 当发生Seq=1的包,接受端会传回ACK=2, 也就是将接受到的Seq+1 , 代表的是期望接受到的下一个包是 Seq=2的包。 (换句话说,和头部的标志位并没有任何关系)

    为什么要这样呢?.. 这么做比较违反我们的直觉。

    因为TCP是以流水线发出的,比如发送端顺序的发出 Seq=1、Seq=2、Seq=3。

    那么如果ACK确认的序号和收到的包的序号一致的话,那么需要发回 ACK=1、ACK=2、ACK=3 共三个包。

    但是TCP协议对此进行了优化,只需要发送一个ACK包就能代表说自己已经收到了前面三个包,
    那就是发送ACK=4 (期望收到Seq为4的包)。这样节省了ACK确认的数量。

    另外TCP是的序号是根据数据流编码的, 假设最开始Seq=0 Len=3, 那么 ACK=4的时候:

    第一个意思是想表明期待收到下一个Seq为4的包。

    第二个意思实际上是说,想收到的包开始的那个比特位于数据流中的第四个比特。(下次从数据流中的第四个Byte开始发送)

    (实际上上面两个意思是一样的 = = ,同时也再次说明这个ACK+1和头部的标志位无关)

    为什么要三次握手

    image-20201117144828569.png

    最重要:为了满足在信道不可靠的情况下,建立双向连接。

    其他:为了减少恶意伪造数据包的用户对服务器攻击。减少异常的情况下,服务端资源占用情况。

    TCP四次挥手流程

    image-20201117145254085.png

    主动关闭的一方调用close函数,发送FIN包表示要关闭连接,之后进入FIN_WAIT_1状态。

    被动方接受FIN包,内核协议栈插入一个EOF标志,到接受缓冲区的数据之后,同时给主动方发送ACK包,被动方进入CLOSE_WAIT状态。

    主动方收到ACK后,进入FIN_WAIT_2状态。

    被动方的应用进程读到一个结束标志后,调用close函数发送FIN包给主动方。被动方进入LAST_ACK状态。

    主动方收到FIN包发起ACK,主动方进入TIME_WAIT状态。

    被动方收到ACK后进入CLOSED状态。

    主动方在2倍MSL后也进去CLOSED状态。linux中MSL时间为30s,所以TIME_WAIT时间为60s。

    为什么需要TIME_WAIT状态?为什么要是2倍MSL?

    如果没有TIME_WAIT,主动方的端口就可以用于新连接。这时如果被动方没有收到ACK,会重发FIN包,导致新连接被 关闭。

    2倍MSL:允许最后的ACK包丢失一次。如果丢失,重发的FIN包会在第二个MSL内到达。

    image-20201117150158293.png

    无论主动方还是被动方都需要一个FIN包,ACK包,所以关闭需要四次挥手。

    为什么建立连接是三次,端口连接是四次呢?

    TCP不允许处于半打开状态,单向传输数据。所以服务端把回应给客户端的ACK和SYN包一起发送给客户端。

    但是TCP允许在半关闭的状态下单向发送数据。

    TCP总结

    image-20201117150637103.png

    TCP真的可靠吗?

    tcp服务端宕机了怎么办?

    • TCP是一个可靠的协议,怎么保证可靠的?

    • TCP并不能保证他发送的数据能够准确的发送到最大的应用进程。

    TCP如何保证可靠

    差错:发送端发送的数据和接收端接收的数据不一致。TCP会在首部增加一个校验和,可以校验TCP首部和数据,接收到接收数据后发现校验和有错,就丢弃这个数据包,并且不给对端发送应答包。

    丢包:发送数据包后会启动一个定时器,如果得不到对方的应答就会超时重传。

    失序:TCP是承载于IP包之上的,IP发送时可能走不同路径,导致后发数据包先到达。TCP首部就做了序列号,接收端接收数据可以对它重新排序。

    重复发送:根据TCP首部的序列号。

    TCP发送过程情况

    1. 发送一次,应答一次。发送一次,应答一次。不能很好利用网络和机器的效率。

    2. 发送端尽可能发送,接收端接收后返回一个应答。

      1. 接收端告诉发送端,接收能力有多强。以免丢包,增加网络开销。

      2. 数据包在网络上多个路由器之间转发,网络转发数据能力。拥塞控制。先发送一个包,可以应答;发2个,应答,发4个。。增加到一定程度线性增长或者网络丢包了,降低网络发送速率。

    image-20201117151951256.png

    TCP的可靠机制仅仅是端到端的可靠。

    应用进程A发送数据到应用进程B。发送端会从协议栈从上往下层层传输,然后经过若干路由器到目标机器,再向上传输到达接收端。

    路由器之间是网络设备,并没有TCP,IP是一个不可靠协议。TCP的接收端应用进程B的内核协议栈TCP,能够保证收到的数据是按序的,未受损的。

    一个经典问题:

    B端的TCP向A发送了确认包,但是此时B宕机了,没有把已确认的数据包读走。那A收到了应答包,就认为B已经收到了。

    解决方案:增加应用层的应答包,

    image-20201117152821831.png

    设计双通道,向外发送数据包的模块,能收到对端应用进程的ACK。

    设计思路:发送一个数据包,然后启动一个定时器,在定时器到达前,不发送数据,直到对端应用层的ACK包收到后,才继续发下面的数据。如果超时没有收到对端的应答,就把进程结束掉(或数据重传)。

    TCP故障类型:

    1、不能收到FIN的故障

    • 网络掉线了或主机崩溃了,若此时刚好阻塞在read函数上,没法恢复。只能设置读超时,通过setsockopt。

    • 若先write再read,这时内核协议栈会不断重传,直到一定次数后,还收不到应答,就标记这个连接是异常的,走到超时错误。

    • 也有可能是阻塞在select或者epoll上,建议做心跳包,定时检测对端应用进程是否存活。

    • 对端操作系统崩溃了,在TCP还没有放弃连接前,重激起来了。这时能收到对端的RST。

    2、能收到FIN的故障

    • 对端应用进程崩溃、close、exit,都能收到FIN包。

    • 如果阻塞在read上,read直接返回0。

    • 如果此时在write,第一次write对端的内核协议栈会给一个RST,表示连接已经重置。如果收到RST,还继续发数据包,就会收到管道破裂。

    SYN泛洪攻击

    SYN cookie

    待补充

    相关文章

      网友评论

        本文标题:TCP

        本文链接:https://www.haomeiwen.com/subject/vjrriktx.html