目录
[1.1.3知名端口号(Well-Know Port Number)](#1.1.3知名端口号(Well-Know Port Number))
2.5理解TIME_WAIT状态和其引起的bind失败解决方法。
[4.TCP dump抓包](#4.TCP dump抓包)
1.UDP
传输层负责数据能够从发送端传输接收端
1.1.端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在TCP/IP协议中,用"源IP","源端口号","目的IP","目的端口号","协议号"这样一个五元组来标识一个通信(可以通过netstat -n查看);
1.1.2端口号范围划分
0-1023 :知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。
1024-65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
1.1.3知名端口号(Well-Know Port Number)
有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:
ssh服务器,使用22端口
ftp服务器,使用21端口
telnet服务器,使用23端口
http服务器,使用80端口
https服务器,使用443
执行下面的命令,可以看到知名端口号
bashcat /etc/services我们自己写一个程序使用端口号时,要避开这些知名端口号
另外,一个进程可以bind多个端口号,一个端口号理论上不可以被多个进程bind
1.2UDP协议
1.2.1UDP协议端格式
如果校验和出错,就会直接丢弃;
报头其实就是个双方约定好个结构,一方能把数据填入结构,另一方能完整的把填入的数据正确提取出来。
源端口和目的端口,可以这样理解,就是为了当数据传到对方主机之后,一路解包,到了传输层后,把传输层的报头分开,读取报头里的源端口和目的端口,就可以知道下一步应该把数据交给应用层的哪个进程。
16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度,也就是说,接收方分离报头和数据,是先读取固定长度8字节的报头,检查检验和,检验和没出错,再提取里面的16位udp长度,这个长度减去8字节,就是有效载荷的长度,然后传输层再读取目的端口,决定将数据发给应用层的哪个进程。
1.2.2UDP的特点
UDP传输的过程类似于寄信.
无连接 :知道对端的IP和端口号就直接进行传输,不需要建立连接;
不可靠 :没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
面向数据报:不能够灵活的控制读写数据的次数和数量;
1.2.3面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节;而不能循环调用10次recvfrom,每次接收10个字节;
如何理解?事实上,我们前面讲了,通过16位udp长度是可以确认完整的传输层有效载荷长度的,因此一个完整的应用层数据是可以完整的发给应用层的,应用层可以保证只要拿到了数据,那就一定是完整的,因此是面向数据报的。
1.2.4UDP的缓冲区
UDP没有真正意义上的发送缓冲区.调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区.但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
UDP的socket既能读,也能写,这个概念叫做全双工
传输层将数据交给应用层,本质上就是os将传输层的缓冲区中的数据,传给应用层的进程,而端口号和进程会有一个联系(不同os做法不同,可以理解为哈希,key是port,value是pcb),传输的过程就是把文件传到pcb对应的文件缓冲区里面,进程的接口识别到缓冲区有内容了,就会把缓冲区的内容写入容器里面(数组等等)。
1.2.5UDP使用注意事项
我们注意到,UDP协议首部中有一个16位的最大长度.也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).
然而64K在当今的互联网环境下,是一个非常小的数字.
如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
1.2.6基于UDP的应用层协议
NFS :网络文件系统
TFTP :简单文件传输协议
DHCP :动态主机配置协议
BOOTP :启动协议(用于无盘设备启动)
DNS:域名解析协议当然,也包括你自己写UDP程序时自定义的应用层协议;
2.TCP协议

TCP 全称为"传输控制协议(Transmission Control Protocol").人如其名,要对数据的传输
进行一个详细的控制;
2.1TCP协议段格式
源/目的端口号 :表示数据是从哪个进程来,到哪个进程去;
32位序号/32位确认号 :后面再说;
4位TCP报头长度 :表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15*4=60
6位标志位:
URG :紧急指针是否有效
ACK :确认号是否有效
PSH :提示接收端应用程序立刻从TCP缓冲区把数据读走
RST :对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN :请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
因此标记位的作用就是,区分不同类型的tcp报文。
16位窗口大小 :后面再说
16位校验和 :发送端填充,CRC校验。接收端校验不通过,则认为数据有问题.此处的检验和不光包含TCP首部,也包含TCP数据部分。
16位紧急指针 :标识哪部分数据是紧急数据;
40字节头部选项:暂时忽略;
另外,传输层的这些协议也是要序列化和反序列化的,但是都是由内核已经写好了的,采用的是最朴素的结构化字段,直接转换为二进制流,当然了前面也说了,这些转换方式有很多地方要处理,比如不同os,大小端,但是内核已经处理好了,所以我们也不用太担心。
对于4位首部长度和40位选项的理解,标准的tcp报头长度只有20个字节(没有选项),但tcp报头是可变长度,又因为4位首部长度中,基本单位是4字节,也就是说,一个tcp报头的长度是[20,60]字节。当只有20字节的时候,也就是说首部长度应该是0101即5。另外,因为首部长度的基本单位是4字节,因此,每个选项都需要4字节对齐(也就是说一个选项占4个字节)。其实不管标志位究竟有没有,只要明确报头最少有20个字节,且一定有首部长度,就可以把报头(包括选项)和有效载荷分离了。
如何理解封装和解包?
事实上,当我们应用层在序列化和反序列化的时候,底层的传输层、网络层、数据链路层一定会有很多正在处理以及未处理的报文,这些报文有的是发送有的是接受。而这么多的报文肯定是需要被管理的,如何管理?先描述再组织。
而linux的内核中,报文是以struct sk_buffer描述管理的,链表形式进行组织管理。sk_buffer中会有指向当前报文的有效载荷的指针以及指向当前报文的报头的指针(指向的是同一块内存空间的不同地方),还有链表必须的下一跳指针,还有很多别的。对报文的管理就变成了对链表的增删查改。
所谓的报头其实就是一个结构体对象,所以封装的过程,最初就是只有来自上层发下来的有效载荷,head和data都指向有效载荷第一个字节,封装就是让head指针向前移动一个结构体大小的偏移量,然后直接head->成员的方式填充结构体成员的值即可。
同理,解包也是类似,比如当前是传输层的udp,那么解包就只需要(udphdr*)head->成员,就可以获得报头值,至于有效载荷,那就是head+=sizeof(udphdr)即可。
因此,在内核中,实际上的封装和解包,只需要移动指针即可。
而一层层的上下交付,本质就是一个sk_buffer对象的指针不断的赋予不同的层,而封装和解包,就是sk_buffer对象里的各个指针进行移动。
2.2确认应答(ACK)机制
我们先聊聊可靠性的问题,之所以需要有可靠性,在于网络传输距离边远,比较形象的理解就是,2个人拿着2台直接拿线连接的电脑,双方进行通信,距离不超过3米,任何一方发送了消息后,另一方的电脑收到后,可以马上嘴上大喊告诉对方,自己收到了消息,但是当距离变很远,连人都看不到,那就不可能这样了,在这种情况下,该怎么保证自己的消息被对方接受到了呢?这就是tcp可靠性最主要解决的问题
而这个问题的方案其中一部分就是应答机制,当发送方发生消息给接收方,接收方收到消息后需要发送一个应答给发送方,这样发送方可以保证,自己发送的消息被对方接收到了,而对于接收方来说,也是在明确收到了消息后才发送的应答,也就是说,起码发送方发送的这条消息,是双方都明确知道已经送达了的。
而这个时候,接收方发送的应答是只有发送方明确知道自己送达了,接收方不知道,这时候同理,发送方也可以发送应答给接收方,在这种循坏下,可以明确保证的就是,历史消息一定是可靠的(即送达了的),只有最新的消息(应答(这里我们暂时只认为只有应答,没有别的数据))是只有单方面明确送达的。
没有在特定时间内收到应答的一方,就可以认为自己上一次发送的消息丢失了。
这个机制的核心就是,只要收到了应答,就可以确定自己上一次发送的消息是送达了的,没收到应答,可能消息送达了但应答丢失或阻塞了,也可能是消息本身就丢失或阻塞了,但默认都是认为上一次的消息没有送达。
发送数据和发送应答,一般是双方os自动完成的。也就是说,我们应用层只负责接受数据后处理数据,和准备要发送的数据即可,通信的细节,os内部,比如tcp会自动完成这个应答机制,我们应用层不需要关心。
仔细思考,会发现,上面的应答机制效率不行,如果发送方必须等到应答之后才能发送下一条消息的话,那效率就很低,所以实际tcp是一次性发送多条消息,然后等接受方发送应答。这时候又有新的问题了,如何确定收到的应答是对应哪几条报文呢?
要知道,报文在网络中传输,意外因素有很多(路由转发、阻塞等等),都会导致发送了123条报文,接受方收到的顺序很可能是213、312等等,而接受方假如按照顺序一一发送应答(比如213),发送方收到的应答很可能是312等等,也就是说这在情况下,无法确定报文的顺序,而且如果有报文丢失了,不管是接受方收到消息,还是发送方收到应答,都需要确定,是哪些报文丢失了,哪些收到了。
因此,需要引进序号和确定序号。序号就是对报文进行编号32位,这样接受方收到消息后,会按照序号顺序,依次处理发送应答,而确定序号就是对应答也进行编号(2个序号是同一个序列),一般是对应的报文序号+1。
发送方收到了确定序号之后,可以知道的是,确定序号之前的报文数据已经被接收方收到了,下次发送从确定序号开始发送。
这时候又延伸出新的问题了,为什么序号和确定序号要分开呢?
tcp是全双工的,也就是说接受方在发送应答的同时,他有时候也会想发送数据,因此消息和确认应答会合在一起,也就是'捎带应答',而如果序号只有32位,那么消息和应答就不能同时发送了,所以tcp的报头中既有序号(标志自己要发送的数据报文的序号)字段又有确认序号(即对方某条消息报文中的序号+1的序号,即对应的确认序号)字段。
TCP将每个字节的数据都进行了编号.即为序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发.
我们可以把tcp的缓冲区想象成一个数组,每一个字节都天然的就有了一个下标。因此发送的序号就等于确认序号+要发送的字节数。
2.3超时重传机制
主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果
那么,如果超时的时间如何确定?
最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返回"
但是这个时间的长短,随着网络环境的不同,是有差异的.
如果超时时间设的太长,会影响整体的重传效率;
如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,
如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传.
如果仍然得不到应答,等待4*500ms进行重传.依次类推,以指数形式递增。
累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
2.4连接管理机制
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
先注意,这里并不是说单单发送什么标记,而是指这些报头中相应的标记位置1。三次握手指的是SYN->SYN+ACK->ACK。注意,三次握手一般是从客户端先发起SYN,而我们前面写tcp服务的时候也发现了,客户端要调用connect接口,才能进行连接,connect接口就是主动发起SYN的动作,但具体的三次握手细节,全部由双方os自动完成。
而服务端必须处于listen状态才能够受理SYN,否则就会直接丢弃。这也是为什么我们写服务端的时候,需要先调用listen接口,让套接字处于listen状态。
当客户端最后发送一个ACK给服务端之后,服务端的accept接口才会真正的获取这个连接供上层使用。
维护连接是需要成本的,对服务端而言,连接有很多,因此对这些连接需要管理,而管理的本质依旧是先描述再组织,描述意味着必须有对应的内核数据结构,而这些内核数据对象在创建的时候需要时间、存储也需要空间,粗略的理解下,这就是连接的主要成本。
而四次挥手,指的是FIN->ACK->FIN->ACK。四次挥手的本质就是双方都向对方发出断开连接的请求并收到对方的应答。因此,我们前面写服务端的时候,close(fd)内部就是在做2次挥手,双方都调用了close,就是四次挥手。
服务端状态转化:
[CLOSED->LISTEN] 服务器端调用listen后进入LISTEN状态,等待客户端连接;
[LISTEN->SYN_RCVD] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文.
[SYN_RCVD->ESTABLISHED] 服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了.
[ESTABLISHED->CLOSE_WAIT] 当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT->LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK->CLOSED] 服务器收到了对FIN的ACK,彻底关闭连接.
客户端状态转化:
[CLOSED->SYN_SENT] 客户端调用connect,发送同步报文段;
[SYN_SENT->ESTABLISHED] connect调用成功,则进入ESTABLISHED状态,开始读写数据;
[ESTABLISHED->FIN_WAIT_1] 客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1;
[FIN_WAIT_1->FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段;
[FIN_WAIT_2->TIME_WAIT] 客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK;
[TIME_WAIT->CLOSED] 客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态.
CLOSED是一个假想的起始点,不是真实状态;
bash你(客户端)想给朋友(服务器)打电话: 1. 你拨号(SYN)→ 等待对方接听 2. 对方接起电话说"喂?"(SYN+ACK)→ 确认有人 3. 你说"喂,我是XXX"(ACK)→ 对话开始 4. 你们聊天(ESTABLISHED状态)→ 数据传输 5. 你说"好了,我说完了"(FIN)→ 你这边说完 6. 对方说"嗯,知道了"(ACK)→ 确认收到你的结束信号 7. 对方说"那我也说完了"(FIN)→ 对方也说完了 8. 你说"好的,拜拜"(ACK)→ 双方确认结束 9. 你等几秒再挂电话(TIME_WAIT)→ 防止对方还有话注意,三次握手不是说100%建立好连接,而是经历三次握手,cs都认为连接建立好了。因此三次握手不能保证最后一次发送的ack真的被对方接收了,但是因为前2次握手都成功了,所以第三次握手成功的几率会非常高。
当然,如果第三次握手真的失败了,导致客户端已经进入连接状态ESTABLISHED,但是服务端还在处于SYN_RCVD状态,这时候客户端会私自继续发送数据报文给服务端,而服务端收到了这个数据报文后,不会对数据处理,而是直接发送一个RST标志位置为1的报文,告诉客户端,让客户端重新建立连接,也就是重新开始三次握手。注意,不只是这种情况要重新建立连接,事实上还有很多种。RST处理的就是连接出现异常的情况,前2次握手就算失败了,双方都不会进入连接状态,所以RST重点在于处理在第三次握手出现的问题,只要第三次握手的问题解决了,三次握手就没什么问题了。
为什么是三次握手,而不是更少??一个比较次要的原因,是1次、2次握手本身问题更大,更容易收到SYN洪水攻击(也就是仅靠单个客户端就可以不停地发SYN,但是不真正建立连接),三次握手也无法避免洪水攻击,但是三次握手决定了,先建立连接的一定是客户端,也就是说攻击的成本会变高(建立连接、维持连接是需要消耗客户端计算机资源的),三次握手无法避免的是多台客户端(典型的就是肉鸡)同时发起握手,挤占服务端资源,导致正常客户端无法访问服务端。
第一个理由 :需要保证信道即网络的健康 ,在三次握手中,CS双方都可以确定收发是正常的(比如客户端发送了SYN,收到了SYN+ACK,就是确定了发送是正常的,接收也是正常的,服务端同理)。因此,三次握手可以说是最小成本的确认CS的全双工。
第二个理由:确保双方tcp是健康且愿意通信的。健康意味着报文可以正常的收发,而愿意通信,就是说双方都向对方发送了SYN,也都收到了ACK应答,说明双方都愿意通信,跟四次挥手很像,四次挥手也是明确的,双方都跟对方说自己要断开连接FIN,也都收到了对方的同意ACK。只是对于服务端来说,有客户端来连接,那肯定是非常乐意的,因此相比断开连接,建立连接的时候,服务端会直接捎带应答,从原来的4次握手压缩成3次握手(服务端第一次发送的就是SYN+ACK)。
至于为什么不是4次、5次、6次握手,是因为没必要,3次已经确认了信号的健康和双方的通信共识,再多就是浪费带宽和时间。
为什么是四次挥手?跟3次握手在思想上是一致的,理由一样。
客户端发FIN除了表明断开连接,也表示没有用户数据要发送了(上层调用了close(fd)了且缓冲区里的数据已经都发完了,但要注意,此时客户端依旧可以发送应答),服务端也是类似。注意,调用close不意味着立刻关闭连接,而是上层不会再拷贝数据到缓冲区里,双方的OS会自动进行接下来的步骤,即,把缓冲区剩下的数据发出去然后进行四次挥手。
注意,四次挥手之所以是强调4次而不是跟3次握手一样进行捎带应答,是因为断开连接的情况更多的是,其中一方决定断开连接,进行2次握手(发送FIN,收到ACK),但是另一方还有数据要发(可能是没close,也可能是close了之后缓冲区还有数据),直到这一方的数据也发完了,才会进行2次握手。也就是说前二次和后二次之间还可能会有数据发送,只有恰好双方都发送完数据且决定断开连接从而出现3次挥手(捎带应答)。这也是为什么说握手和挥手的思想是一致的,但是描述的次数却不一样。
但是还有个问题,我们前面也说了,另一方可能还不想关闭,如果直接调用close,意味着,另一方传来的数据,就不能读取了,所以实际上还是有这个接口的
SHUT_WR,意味着关闭写,只保留读。至于SHUT_RDWR等同于close。
直接用close也没错,因为实际上双方调用close,意味着该做的工作前面都已经做好了,正常来说就是可以直接close的。
2.5理解TIME_WAIT状态和其引起的bind失败解决方法。
TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximumsegmentlifetime)的时间后才能回到CLOSED状态
假如我们使用Ctrl+C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口;
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos7/Ubuntu上默认配置的值是60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout查看msl的值;
为什么是TIME_WAIT的时间是 2MSL??
MSL是 TCP 报文的最大生存时间,因此 TIME_WAIT 持续存在 2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能
会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN.这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重LAST_ACK);
在 server的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求).
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接.
由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip,源端口,目的ip,目的端口,协议).其中服务器的ip和端口和协议是固定的.如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题,
使用 setsockopt ()设置 socket 描述符的选项 SO_REUSEADDR 为1,表示允许创建端口号相同但IP地址不同的多个 socket 描述符
cppint opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2.6理解CLOSE_WAIT状态
对于服务器上出现大量的CLOSE_WAIT 状态,原因就是服务器没有正确的关闭
Socket,导致四次挥手没有正确完成.这是一个BUG.只需要加上对应的close即可解决问题.
下面我把我之前的一份tcp echo服务的代码改了下,去掉了服务端的close。
图中,我目前只开了一个客户端,客户端和服务端都在一个机子上。可以看到8888接口被监听,且客户端跟服务端、服务端和客户端都有一个ESTABLISHED,也就是说双方目前都建立了连接。
假如服务端此时在察觉到客户端退出后,却没有马上close,那么就会处于close_wait状态。还有一种就是有大量客户端连接上后,然后马上退出,但是服务端并没有close,此时就会出现大量的close_wait状态的连接,而当服务端强行直接退出ctrl+c之后,内核会回收文件描述符,也就是close,因此之后会变成LAST_ACK状态,然和此时的客户端的FIN_WAIT_2状态也因为长时间收不到FIN,从而主动进入closed状态,此时的服务端的LAST_ACK状态就会持续一会儿,然后才因为重传超时的问题,进入CLOSED状态。
2.7流量控制
接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应,
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度.这个机制就叫做流量控制(FlowControl);
接收端将自己可以接收的缓冲区剩余空间大小放入TCP首部中的"窗口大小"字段,通过ACK端通知发送端(就是在应答的报文里,填写16位窗口大小字段);
窗口大小字段越大,说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后,就会减慢自己的发送速度;
如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
我们上面说了是通过tcp报头的16位窗口大小字段表示窗口大小的。
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位;
另外,窗口探测和主动通报,是同时生效的,可以保证在主动通报的报文丢包后,仍然可以确认缓冲区大小。
如果接受方一直不更新窗口大小(比如一直在忙着别的事情不读,或者接受别的发送方),这时候发送方在发送了很多次窗口探测之后,会选择发送一个把标记位PSH置为1的报文,PSH就是催促接受方快点读取缓冲区的内容。另外,平时如果有些数据很急的话,也会在报文里把PSH置为1,比如我们平时用的shell工具,命令发送后要及时的反馈。(不要担心接受方一直不肯读,一直不肯读,这意味着接受方的程序本身就有bug,这是接收方的问题)。而所谓的催促,其实就是在接受方的read、scanf等接口等待缓冲区数据大小达到一定读取标准前,只要有数据马上读取,而不是等待。
最后,关于URG标志位,是用于表示报文中的紧急指针是否有效,而紧急指针指的是报文数据中某一个特定的字节的偏移量或者说地址,当URG被置为1,接受方收到这个报文后,会优先读取这个紧急指针指向的字节(默认缓冲区就像是个队列,先进先出)。URG的应用场景,核心是把紧急指针指向的字节当做状态码、错误码来用,比如上传文件到网盘里,接受方缓冲区里有很多已经上传但还没被接受方读取的字节,而当用户想取消上传,点击了取消后,就会发送URG置为1,紧急指针指向的字节置为某个约定好的数字,这样接收到收到后会优先处理这个紧急数据,然后直接丢弃前面已经在缓冲区里的数据,以及已经存在接受方数据存储位置的数据。
如果想发送带有紧急数据的报文,可以用send,然后flag里面填MSG_OOB(意味着带外数据)
同理,接受方也可以用的recv,把flag填MSG_OOB。
2.8滑动窗口
对每一个发送的数据段,都要给一个ACK确认应答.收到ACK后再发送下个数据段.这样做有一个比较大的缺点,就是性能较差.尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.上图的窗口大小就是4000个字节(四个段).(跟流量控制那边的不一样,这里的是滑动窗口的大小,而且滑动窗口大小跟接受方的缓冲区剩余空间大小也是有关的,暂时可以理解为滑动窗口大小随着上面那个窗口大小变化,保持同步)
发送前四个段的时候,不需要等待任何ACK,直接发送;
收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
窗口越大,则网络的吞吐率就越高;
因为滑动窗口的存在,发送缓冲区里的数据从左往右被划分成了已发送&&已确认数据、滑动窗口(发且没确认的数据&&没发没确认的数据都有可能存在)、未发送数据、没使用空间。
也就是说,对发送且未收到应答的报文保持在滑动窗口中,方便重传。
滑动窗口可以变大(比如接受方的剩余缓存空间突然变大了),也可以变小(比如接收方的剩余缓冲空间变小了),自然也可以变成0。
而滑动窗口的在实际上,就是2个整形变量维护的一段数组范围[start,end)。滑动窗口的变化,实际上就是靠ACK的确认序号来确定start,end就是start+窗口大小。
又有个新的问题,如果出现了丢包,该怎么办,要怎么重传?
情况一:数据包已经抵达,ACK被丢了。这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认;(确认序号的意义就是确认序号之前的都已经收到了)
情况二:数据包就直接丢了。
当某一段报文段丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"---样;
如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001-2000重新发送;
这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为"高速重发控制"(也叫"快重传").
注意,就算丢包的不止是1001~2000,后面的通信过程中也会不断地快重传。
假设当滑动窗口最左侧的报文丢失了之后,滑动窗口不会移动,而且就算因为ACK丢失或者干脆返回ACK数量达不到3次,也有tcp的超时重传兜底。
假设滑动窗口中间的报文丢失,说明前面收到了ACK,滑动窗口的左边界最后会移到这个丢失的报文上,然后就转化成了滑动窗口最左侧报文丢失的问题了。
假设滑动窗口最右侧的报文丢失了,但也是一样,随着前面的ACK,滑动窗口一直右移,最后又回到了滑动窗口最左侧报文丢失的问题。
简而言之,滑动窗口中的所有问题,最后都会转化为最左侧丢包问题。
如果ack的顺序是乱的呢?首先这在情况基本不会出现,因为接收方虽然可能收到的报文是乱序的,但是他会依序返回ACK,其次,就算乱序了,因为确认序号的意义是确认序号之前的报文都已经收到了,所以就算后面的ack先来了,滑动窗口也是按照这个ack的确认序号右移,哪怕慢一步但是确认序号先的ack到了,也不影响,直接无视即可。
最后,如果接收方收到的数据报文是乱序的,放在接收缓冲区里,上层读取又是顺序读取,那岂不是读的乱序数据?不,前面也说了,接受方会对收到的报文进行排序,保证接收缓冲区里的数据是按照正确的报文顺序排列的。
注意,上面是把发送缓冲区默认当成了线性的数组空间,但考虑到溢出、内存碎片等等问题,实际上的存储方式多种多样,有些只是逻辑上环状的数组空间,物理上是分散的内存块等等。
前面流量控制说了,根据接受方的窗口大小,tcp会控制传输速度,怎么控制?就是滑动窗口
总结,滑动窗口就是流量控制的具体实现,也是重传策略的具体实现。
2.9拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据.但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
此处引入一个概念称为拥塞窗口,当发送方发送的报文数据大于窗口,可能引起网络拥塞,但不超过不会拥塞。
发送开始的时候,定义拥塞窗口大小为1;
每次收到一个ACK应答,拥塞窗口加1;
每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口;即滑动窗口大小=min(接受方的窗口,拥塞窗口)。
"慢启动"只是指初使时慢,但是增长速度,是指数级别的,符合tcp的需求,刚开始少量报文检测网络状态,如果网络状态好,那就尽快恢复通信,也就是快速增加报文量。为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。
此处引入一个叫做慢启动的阀值,当拥塞窗口超过这个阀值的时候,不再按照指数方式增长,而是按照线性方式增长。
当TCP开始启动的时候,慢启动阈值等于窗口最大值 ;
· 在每次超时重发的时候,慢启动阈值会变成原来拥塞窗口的一半,同时拥塞窗口置回1 ,拥塞窗口核心作用的探测网络拥塞,滑动窗口的大小在网络通畅的情况下,是由接受方的窗口大小决定的,拥塞窗口的大小是不会停止增长的,直到网络拥塞,这时候依靠min,就强行让滑动窗口的大小减少,从而减少发送量,缓解网络拥塞。
少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。另外,因为网络中TCP协议是主流,所以当识别到网络拥塞,所有识别到的主机都会进行拥塞控制,这样才能尽可能的减缓网络的压力。
另外,拥塞窗口就算是线性增长也是有上限的,另外,实际的滑动窗口大小还要考虑到网络带宽的上行和下行速度。
2.10延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,
假设接收端缓冲区为1M.一次收到了500K的数据;如果立刻应答,返回的窗口就是500K;
但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M;
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高.我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么?肯定也不是;
数量限制:每隔N个包就应答一次;
时间限制:超过最大延迟时间就应答一次;
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms;
2.11捎带应答
前面讲过了,这里再稍微讲下
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是"一发一收"的.意味着客户端给服务器说了"How are you",服务器也会给客户端回一个"Fine,thank you";
那么这个时候ACK就可以搭顺风车,和服务器回应的"Fine,thankyou"一起回给客户端。
2.12面向字节流
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区和一个接收缓冲区;
调用write时,数据会先写入发送缓冲区中;
如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据.这个概念叫做全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次;
2.13粘包问题
首先要明确,粘包问题中的"包",是指的应用层的数据包
在TCP的协议头中,没有如同UDP一样的"报文长度"这样的字段,但是有一个序号这样的字段
站在传输层的角度,TCP是一个一个报文过来的.按照序号排好序放在缓冲区中.
站在应用层的角度,看到的只是一串连续的字节数据.
那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包.
如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。
对于定长的包,保证每次都按固定大小读取即可;例如我之前文章写的代码里Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是咱们自己来定的,只要保证分隔符不和正文冲突即可);
对于UDP协议来说,是否也存在"粘包问题"呢?
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层.就有很明确的数据边界。
站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出
现"半个"的情况。
2.14TCP异常情况
进程终止 :进程终止会释放文件描述符,仍然可以发送FIN.和正常关闭没有什么区别.
机器重启 :和进程终止的情况相同.
机器掉电/网线断开 :接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。(这在保活策略效果一般,而通常时间不会短,都是分钟和小时为单位的,因此实际上保活更多的是由应用层来做,比如定期发送约定好的数据,应用层知道该以什么间隔进行询问更合适)。另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态,例如QQ,在QQ断线之后,也会定期尝试重新连接。
2.15小结
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
可靠性 :
校验和
序列号(按序到达、去重)
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能 :
滑动窗口
快速重传
延迟应答
梢带应答
其他 :
定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)
2.16基于TCP应用层协议
HTTP、HTTPS、SSH、Telnet、FTP、SMTP以及我们自己写tcp程序的时候自定义的应用层协议(像是我前面写的网络计算器)。
2.17TCP/UDP对比
我们说了TCP是可靠连接,那么是不是TCP一定就优于UDP呢?TCP和UDP之间的优点和缺点,不能简单,绝对的进行比较
TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等.另外UDP可以用于广播;
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。
用UDP实现可靠传输?
参考TCP的可靠性机制,在应用层实现类似的逻辑;
例如:
引入序列号,保证数据顺序;
引入确认应答,确保对端收到了数据;
引入超时重传,如果隔一段时间没有应答,就重发数据;
2.18其他
我们想想,为什么前面距离的例子里面,大多都是以1000字节为单位的报文发送,而不是一个报文里塞了整个滑动窗口大小的数据呢?
是因为数据链路层的帧限制了其有效载荷不能超过MTU,而MTU一般就是1500字节。
之所以不直接1500,是因为从传输层到网络层到数据链路层,都需要额外封装,因此传输出的报文不会太接近1500,而是保留一些空位给网络层和数据链路层。
首次发数据的时候,怎么确定接受方的窗口大小呢?实际上三次握手的时候就已经在发报文了,意味着接受方在三次握手的时候,就可以把窗口大小通过报文告诉发送方。
而且捎带应答在第三次握手,也是可以实现的,因为前2次握手,对于发送方来说,他已经确认了对方可以允许通信,也就是说第三次握手开始,他就能发数据了。
doff (Data Offset) :这实际上是TCP头部长度(以32位字为单位),而不是一个标志。它指示了TCP头部
有多少32位字(4字节)。由于TCP头部可能包含可选项,因此这个字段用于告诉接收
方TCP头部有多长。通常,没有可选项的TCP头部长度是20字节(即doff为5,因为
5*4=20字节)。
resl :这是保留位,通常被设置为0。它们用于未来的协议扩展。
cwr (Congestion Window Reduced) :当发送方收到一个带有ECN(Explicit Congestion Notification,显式拥塞通知)标志
的ACK时,它可能会设置CWR标志来响应。这通常用于ECN(ExplicitCongestion
Notification,显式拥塞通知)机制,一种改进TCP拥塞控制的机制。
ece (ECN-Echo) :当TCP的接收方检测到网络拥塞时,它可能会发送一个带有ECE标志的ACK给发送方。
ECE标志告诉发送方,接收方已经检测到拥塞,并且可能希望发送方减少其发送速率。
3.TCP全连接队列
三次握手建立连接和用户是否accept无关,也就是说就算没有accept,也可以建立established状态的连接。
云服务器的公网ip是提供商虚拟出来的,本质上只是一个内网ip(172.******什么的)。所以我们bind不能bind云服务器的公网ip,只能拿来当地址用(提供商会把虚拟的公网ip加入路由)。
Linux内核协议栈为一个tcp连接管理使用两个队列:1.半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
2.全连接队列(accpetd队列)(用来保存处于established状态,但是应用层来不及调用accept取走的请求)
而全连接队列的长度会受到listen第二个参数的影响.
全连接队列满了的时候,就无法继续让当前连接的状态进入established状态了,只能待在半链接队列里.
这个队列的长度是listen的第二个参数+1。
全连接队列本质是个生产者消费者模型,accept取走连接,就是给一个特定的文件描述符来指向这个连接(连接本质上也是个数据结构或者说结构体),客户端建立连接成功就进入这个队列,如果生产者(应用层)有空,就会把连接取走。
这个全连接队列是必须有的,本身就像个缓冲区,只要应用层闲下来了,就可以马上取走连接,而不是被服务器统统拒绝,导致当应用层闲下来后还得等新的连接。同样,因为这个原因,全连接队列也不应该为空,导致增加服务器的闲置率和减少服务的效率和用户体验。也不应该太长,太长意味着客户端很久才能被取走,而客户端等久了是会直接不等的,这样的话,增加的长度就没有意义了,浪费空间,客户端也不愿意等太久(体验不好)。
当三次握手成功后,会创建一个tcp_sock,链入listen_sock文件描述符对应一层层下来的全连接队列里。当accept的时候,即假设是个accept_sock,会在文件描述符表里占据一个位子(比如这里是4)然后创建一个struct file以及内部的struct socket,所谓的取走,就是把listen_sock里全连接队里的tcp_sock指针剪切到accept_sock的struct file的struct socket里的struct sock*sk里。
题外话,将sock和fd映射是有个函数sock_map_fd的。
4.TCP dump抓包
TCP Dump是一款强大的网络分析工具,主要用于捕获和分析网络上传输的数据包。
4.1安装
tcpdump通常已经预装在大多数Linux发行版中。如果没有安装,可以使用包管理器进行安装。例如Ubuntu,可以使用以下命令安装:
sudo apt-get update
sudo apt-get install tcpdump
在RedHat或CentOS系统中,可以使用以下命令:
sudo yum install tcpdump
4.2常见使用
抓到的报文中,S就是SYN,seq就是序号,win就是窗口大小,len就是长度,falg里额外的.意味着ack,R是RST,P是PSH,F是FIN。
4.2.1捕获所有网络接口上的TCP报文
使用以下命令可以捕获所有网络接口上传输的TCP报文
bashsudo tcpdump -i any tcp-i any 指定捕获所有网络接口上的数据包,,tcp指定捕获TCP协议的数据包。i可以理
解成为 interface 的意思
4.2.2捕获指定网络接口上的TCP报文
如果你只想捕获某个特定网络接口(如eth0)上的TCP报文,可以使用以下命令
bashsudo tcpdump -i eth0 tcp
4.2.3捕获特定源或目的IP地址的TCP报文
使用host关键字可以指定源或目的IP地址。例如,要捕获源IP地址为192.168.1.100
的TCP报文,可以使用以下命令:
bashsudo tcpdump src host 192.168.1.100 and tcp要捕获目的IP地址为192.168.1.200的TCP报文,可以使用以下命令
bashsudo tcpdump dst host 192.168.1.200 and tcp同时指定源和目的IP地址,可以使用and关键字连接两个条件
bashsudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp
4.2.4捕获特定端口的TCP报文
使用port关键字可以指定端口号。例如,要捕获端口号为8θ的TCP 报文(通常是HTTP 请求),可以使用以下命令
bashsudo tcpdump port 80 and tcp
4.2.5保存捕获的数据包到文件
使用选项可以将捕获的数据包保存到文件中,以便后续分析
bashsudo tcpdump -i eth0 port 80 -w data.pcap这将把捕获到的HTTP流量保存到名为data.pcap的文件中。
pcap后缀的文件通常与PCAP(Packet Capture)文件格式相关,这是一种用于捕获网络数据包的文件格式
4.2.6从文件中读取数据包进行分析
使用-r选项可以从文件中读取数据包进行分析
bashtcpdump -r data.pcap这将读取data.pcap文件中的数据包并进行分析。
使用tcpdump时,请确保你有足够的权限来捕获网络接口上的数据包。通常,你需要以root用户身份运行tcpdump。
使用tcpdump的时候,有些主机名会被云服务器解释成为随机的主机名,如果不想要,就用-n选项
主机观察三次握手的第三次握手,不占序号
5.wireshark分析TCP通信流程
wireshark是windows下的一个网络抓包工具.虽然Linux命令行中有tcpdump工具同样能完成抓包,但是tcpdump是纯命令行界面,使用起来不如wireshark方便
安装包:https://1.na.dl.wireshark.org/ 要梯子。如果要安装包可以评论区留言。
安装之后过滤器设置参考
wireshark 实用过滤表达式(针对ip、协议、端口、长度和内容)_wireshark过滤端口-CSDN博客
三次握手:
确认应答:
我的echo服务的客户端输入一个字符。
其实正常来说只有3条的,解释可以参考AI:
四次挥手:
呃呃呃,我本地端口不要在意,我中间重新测试了下,这个本地客户端端口不一样
这里测试的四次挥手实际上是3次挥手,中间2次通过捎带应答合并了,主要是因为我服务端close的太快了,tcp为了效率才合并的,如果我等了一秒才close,就是4次挥手了。
































