一、UDP

1.UDP协议的段格式
UDP报头大小为8字节,其中16位UDP长度代表着报文的长度,意味着如果UDP报文长度最多为,2^16字节。
2.报头和有效载荷的分离方法
UDP报头定长,恒为8字节。可据此直接将报头和报文分离。
3.如何将有效载荷交付给对应上层协议
源端口号由服务端指定,而目的端口号在收到客户端请求后即可得到,从而指定交付对象。
4.周边知识
(1)五元组
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n命令查看);
(2)端口号范围划分
0-1023: 知名端口号,HTTP,FTP,SSH这些广为使用的应用层协议,他们的端口号都是固定的
1024-65535: 操作系统动态分配的端口号和客户端程序的端口号就是由操作系统从这个范围分配的
(3)UDP面向数据报的特点
应用层发给UDP多长的报文,UDP会原样发送,既不拆分也不合并。假如用UDP传输100个字节的数据,如果发送端调用一次sendto发送100个字节,那么接收端也必须调用对应的一次recvfrom接收100个 字节,不能循环调用10次recvfrom每次接收10个字节。
(4)UDP缓冲区的特点
UDP没有真正意义上的发送缓冲区,因为调用sendto会直接将数据交给内核,再有由内核将数据传给网络层协议完成传输。
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致 (即便数据包乱序也会向上交付),如果缓冲区满了,后续接收的UDP数据会直接丢弃,不通知发送方。
(5)报文管理

大量的UDP报文放在缓冲区里面需要进行管理,所以报文在编程上设计为结构体 ,其中也包含了报头结构体。其中还包含指向缓冲区头尾的指针start和end,以及当前报文在内存中末端的位置pos。各个报文结构体用链式结构连接,所以UDP缓冲区每次接收新的报头相当于对报文链表做尾插操作。
二、TCP传输控制协议
1.对"传输控制"的理解
网络传输和文件传输有很大的共通之处 。比如每建立一个连接,服务端和客户端就要创建一对收发缓冲区,而操作系统做文件操作时也是通过缓冲区进行的。write/read这些操作本质就是拷贝函数------将用户缓冲区的内容拷贝到这些收发缓冲区中。
数据什么时候发送,发送多少,出错了怎么办,和文件缓冲区的刷新机制一样,由内核自主决定,这就是控制的体现。
2.TCP协议的段格式

(1)基本构成
TCP报文由标准报头、选项和报文构成。标准报头4位首部长度代表的时报头和选项的长度,而非整个报文。4位首部长度的单位是四字节,4 bit最大值为15,即四位首部长度可代表15*4=60字节。
标准报头恒为20字节,意味着选项长度不超过60-20=40个字节。所以拿到报文以后先读个20字节,剩下的值即选项长度,从而实现报头和有效载荷的分离。
(2)16位窗口大小、确认应答机制与流量控制
添加TCP报头,到了服务器那边就只需要摘掉TCP的报头了,既然必须做这个操作,数据必然携带完整的TCP报头。
数据发送端需要控制自己的发送速度以防止服务器因缓冲区被写满来不及接收新的报文导致的大面积丢包 (虽然TCP有丢包重传的性质,但浪费毕竟会导致效率降低)具体慢多少由接收方缓冲区的剩余空间大小决定,这就是流量控制。
当一端收到报文后,即便没有数据要给另一端,也至少要发一个确认应答让对方知道自己已经收到了数据,这就是确认应答机制 。前文提到的数据收发必然被携带的完整的TCP报头就携带这个确认应答,接收方在接收数据后,为了调整发送端的发送速度,需将自身接收缓冲区的剩余空间大小发给发送端,这就是报头里的16位窗口大小。
(3)序号和确认序号
收到应答的一方能确认对方收到了自己的数据,然而发送应答的一方却无法得知应答是否被接收,这样最新消息的发送方总是无法得到应答,因此TCP无法保证单次发送的消息是百分百可靠的。
TCP的通信只在局部上从两端各自的角度能够保证发送的消息被对方接收 ,即服务端得到了应答,说明服务端向客户端发送的消息被接收;客户端得到了应答,说明客户端向服务端端发送的消息被接收。假如在一段时间如果没收到应答就要重传。
当然应答大部分时候都和要发送的数据一起发过去,分开发送成本太高了,这就是捎带应答。实际上数据大部分时候也不是一条条收发的,而是分批次收发。
一批数据发送必然有序,而接收却不一定有序,这就是数据包乱序问题。
报头中的序号用于解决此类问题,保证数据能够按序到达。序号具体是什么呢?

缓冲区本身是面向字节流的,存放的数据是按行排列的,在发送前报文在发送缓冲区的每个字节都被序号标识,也就是当前发送的这个字节,在整个数据流中的绝对偏移量。
每次建立新连接时,操作系统随机生成一个初始序号,此后各个字节从这个初始序号开始按序确认序号。这时为了防止重新连接服务器后,收到了网络的残留数据。
发送后每个报文的序号为当前字节流相对于初始序号(ISN)的偏移量 (或实际数值),发送的每个报文必然会收到响应,所以接收方用确认序号来保证数据被收到,填充的具体数字是收到报文的序号+1。这样设置确保在该数字之前的报文已被收到了,下一次发送端会从确认序号指定的数字开始发送 。比如发送方A发了四个包,每个包100 字节,序号分别为 100, 200, 300, 400(即覆盖 100-199, 200-299, 300-399, 400-499),接收方B如果没收到包200,且数据的起始序号是1000,则B发给A的应答序号为1000,确认序号为200,A收到这个包就知道500之前序号对应的报文接收方都收到了。
允许应答有少量丢失,依然认为最后的确认序号之前的应答都已收到,因为确认序号和序号一样都是累加的,这就是累积确认(Cumulative Acknowledgment),允许少量ACK丢失而不影响可靠性。仍以前面场景为例,A并没有收到101,201,301的确认序号,但只要收到了401,说明B已经读了400字节的消息,所以数据是没有丢失的。
通过上述机制,虽然看似只能保证单向数据传输的可靠性,但序号和确认序号的引入使得两个方向是互相监督的,从而确保了整体双向的数据传输的可靠性。这也是全双工的基础。
(4)标志位
ACK
Acknowledgment, 用于声明报文头中的"确认序号"字段是否有效。当该报文是为了回复对方已收到的数据时,ACK置为1,所以在整个TCP通信生命周期中,只有三次握手发起时的第一个SYN包(此时无任何数据需要确认)将ACK置为0。
SYN
Synchronize,建立连接时设为1,表示发送方希望同步初始序列号,调用connect的时候就会创建该标志位被设为1的报文。
FIN
Finish,请求关闭连接,调用close的时候就会创建该标志位被设为1的报文。
PSH
PUSH,告知对方读取消息,网络传输是具有同步机制,接收方缓冲区将满时发送信号让发送方停止发送。生产者会不断询问什么时候才能再发送,同时接收方也会告知缓冲区大小。如果接收方迟迟不将数据交付给上层,可将该标志位设为1时,通知接收方应立即将数据交付给上层应用,而不必等待缓冲区填满。
RST
Reset,表示需要强制重新连接,通常用于连接异常或拒绝连接请求。
TCP虽保证可靠性,但在建立连接时和正常通信中遇到不可抗因素导致的错误不能保证成功。
比如有的时候客户端认为连接已经建立好,而服务器却不这么认为。
在三次握手中的RST的应用场景:

对客户端来说,发出ACK的那一刻就已经建立好连接了,接着向服务器发送数据。
对服务器来讲,假如客户端发出的ACK丢失且继续发送新的数据,服务器会回复RST被设置为1的报文,客户端收到该报文会重新进行发起连接。
URG
Urgent , 紧急需要优先处理的数据被称为紧急数据,此时可将URG设为1,要求接收方高优先处理。这里又涉及到TCP报头中的另一成员16位紧急指针。
(5)16位紧急指针
16位紧急指针即紧急数据的偏移量,偏移量指向报文需要紧急数据的位置,紧急数据大小最多为1字节。在调用send和recv接口时选项设置MSG_OOB可用于收发紧急数据。
使用场景:
客户端卡顿时会询问服务器卡顿原因,卡顿说明缓冲区已有大量报文待处理,询问报文从最后开始排队是不合理的。如果服务端有读取紧急数据的功能,就可以处理发送客户端的紧急询问,快速返回卡顿原因。
3.TCP如何保证效率和可靠性
(1)超时重传机制
发送方未收到应答无法充分证明数据丢失,因为无法排除传输过慢或接收方返回应答丢失的可能。
但发送端的确无法得知已发出报文的状态,于是在一定时间间隔内若未收到应答,则认定该报文已丢失并重传。这就是超市重传机制。
如果的确是传输时间过长导致发送方重传,那么接收方将收到两份报文。这是不可靠的,所以接收方会根据序号进行去重(重复报文序号一样)。
超时时间具体是多少?
理想情况是找到保证 "确认应答一定能在这个时间内返回"的最小时间。但该时间的长短随网络环境不同有所差异。如果时间设置过长,会影响重传效率;时间设置太短,可能频繁发送重复包。
TCP为了保证无论在任何环境下都能比较高性能的通信,会动态计算这个超时时间,机制如下:
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
- 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
(2)连接管理------三次握手与四次挥手详解

三次握手和四次挥手如上图所示,图中左右两侧的字符串如CLOSED代表状态,对于内核来说都是宏。其实三次握手和四次挥手一样做了四个步骤,只是捎带应答使得服务端发送的ACK和SYN放在一个报文里面了,所以叫三次握手。
对三次握手接口的理解
connect接口只负责发送SYN,然后阻塞直到三次握手完成并收到应答后才返回。
accept不参与三次握手,只负责将建立好的连接拿到上层,没有新连接就会阻塞。
为什么要进行三次握手?
1.验证全双工
客户端和服务端在通信前需要验证自己可以收发数据的,所以进行两次数据收发来确认双向通信能力。如果只有第一次收发,客户端能够知道自己收发正常------因为它既发出了SYN,又收到了服务端的SYN-ACK,说明自己既能发送也能接收;但服务端只知道自己可以收数据------它收到了客户端的SYN,却无法确认自己发出的SYN-ACK是否被客户端成功接收,也就无法确认自己是否具备发送能力。
2.成本嫁接与安全性考量
来看看不同握手次数分别有什么影响:
-
一次握手(SYN) :
客户端发完SYN后,服务器收到SYN的那一瞬间,服务器认为连接已经建立了 ,立刻进入ESTABLISHED状态,创建维护连接,服务器承担维护成本(因为客户端根本不会收到ACK保证,无法确认对端是否能正常接收)。 -
两次握手(SYN + SYN-ACK) :
服务器发完SYN-ACK的那一瞬间,客户端等服务端发ACK,所以服务器先认为连接建立了, 立刻进入ESTABLISHED状态,创建维护连接,成本依然在服务器。 -
三次握手(SYN + SYN-ACK + ACK) :
客户端发完最后的ACK那一瞬间,轮到服务端等客户端的发ACK,客户端会先认为连接已经建立, 立刻进入ESTABLISHED创建维护连接,成本转移到了客户端。
大多数情况下服务端数量远小于 客户端,如果服务端扛下连接成本,意味着维护大量连接,莫不如让各个客户端来承担 。另外之所以不考虑三次以上的握手次数,是因为三次是验证全双工的最小次数,没必要继续增加。
为什么四次挥手不通过捎带应答变为三次挥手呢?
只要客户端想连接,服务器必须无条件地接收,所以三次握手时应答可以捎带连接标志位同时发送给客户端。
发起断开连接的一方不能向对方发送消息了,而对方仍能继续发送消息。 如果用一方断开连接而另一方还有消息没有发送,就不会断开。因此不是一收到应答就主动断开,是不同时的。
为什么要进行四次挥手?
四次挥手本质是TCP对全双工通信的严谨体现。它允许一方在停止发送数据的同时,继续接收对方未发完的数据,从而保证了数据传输的绝对完整性和可靠性。
此外,主动断开方虽不能再发数据,但依然能给对方发送确认应答。
三次握手状态详细
当服务器的连接数量大于listen接口的第二个参数backlog+1 时,服务端处于SYN_RCVD状态 ,也就是尚未确认连接但已经发送了应答,此时客户端认为建立已被连接,这会造成什么影响呢?
实际上,服务端用队列管理已完成三次握手的连接,这个队列叫全连接队列 ,每当三次握手成功,队列里增加连接,accept接口则负责把队列中的连接取走。listen接口的第二个参数backlog+1的值正是该队列的最大容量。
当全连接队列满时,服务端会把客户端发回的ACK抛弃,三次握手无法完成**,服务端保持RECV状态,这种状态叫半连接**。半连接也有对应的队列,但服务器并不会长时间维护半连接队列节点,服务端维护一段时间后会把队列中的节点删去。
连接在进入全连接队列的前必然进入RECV状态,也就是进入半连接队列。由于半连接队列的长度也有限制,所以即便客户端不断发送连接也无法导致服务器崩溃,但过度占用半连接队列资源本身也是一个问题(比如导致其它链接无法进入)这就是真正意义的连接洪水问题,比如抢票时大部分用户甚至没有进入半连接队列。
面试题:为什么listen的第二个参数不能太长,又为什么不能没有?
如果上层来不及处理连接,服务器又不得不维护全连接队列,这时全连接队列中大部分节点都在占用了资源而没有创造价值,所以该队列没有必要太长。
没有全连接队列并不意味着服务器只能与一个客户端连接,而是每完成一次三次握手让accept接口直接拿走,多余的连接被忽略,这样不就没有任何资源浪费吗?然而,服务器是被动的,当服务器空闲时,如果不提前客户端排队,不可能让客户端主动连接,这时服务器同样没有创造价值,因此不能没有全连接队列。
四次挥手状态详细
无论是客户端还是服务器,主动断开的一方都会进入TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。报文在网络中的最大存活时长就是MSL,之所以要等两倍MSL,一方面是为了让通信双方的网络残留的历史数据消散,另一方面是确保最后一个ACK能被对方收到:假如客户端发出的ACK丢失,服务端会超时重传FIN包,从丢失到重传总时间不会超过2MSL,这样客户端在2MSL内可以收到返回ACK,然后重发ACK以确保连接正常关闭。注意MSL和最大传输时长不是一个东西,最大传输时长是根据网络情况决定的,而MSL一般是固定的。
setsockopt
当客户端处于TIME_WAIT状态,意味着连接没有正常断开。此时关闭服务器并再次绑定同样的端口号启动,会报错端口已经被占用。
此时可引入setsockopt接口,可用于设置连接状态标志位,如下方代码所示。
cpp
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
客户端不会出现这个问题,因为客户端并没有显式绑定,而是由系统分配随机端口,所以端口不会重复。
(3)流量控制深入
建立连接后第一次发数据时,如何保证数据量在对方接收缓冲区大小范围内?
三次握手前两次双方已经交换了报文,协商了双方的接受能力,第三次才可以携带接受能力范围内的数据量。
流量控制具体机制
接收方缓冲区满时,会发送报文告知发送方不要再发了。
此后,一方面当接收方缓冲区空出来的时候,向发送方发送窗口更新通知,说明缓冲区已有空闲。
当然这个通知是可能丢失,所以另一方面发送方也会不时向接收方发送窗口探测报文,假如探测报文得到回复,说明缓冲区已经有了空闲。
如果双方窗口探测报文和窗口更新通知都丢失,说明网络本身有问题,通信双方自然会主动关闭异常连接。
缓冲区(窗口)到底有有多大?
TCP首部中,有一个16位窗口字段存放了窗口大小信息,16位数字最大表示65535,那么TCP窗口最大就是65535字节吗?
默认情况下的确如此,但TCP首部选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位,从而可以进一步扩展窗口大小。
流量控制属于可靠性还是效率?
都有,防止丢包既能提升效率,也保证了可靠性。由此可见可靠性和效率不一定是负相关的。
(4)滑动窗口
已经发出去但没有收到应答的报文需要重发,因此报文不能刚发出去就丢掉了,而是被保留在发送缓冲区中等待应答。
于是,发送端缓冲区划分为为已发送并确认区、已发送但未确认区、可发送区 / 待发送区以及不可发送区 / 暂存区 ,其中第二、三个区域合起来就是滑动窗口。
由两个指针start、end分别定位滑动重口的头尾,两个指针右移就是窗口的滑动。根据区域定义,滑动窗口大小不应该超过对方接收缓冲区的大小(在后面拥塞窗口小节会说明两个指针和滑动窗口具体大小是如何界定的),所以会结合确认应答中的窗口大小进行收缩 。通常窗口越大,网络吞吐量越高(因为可以同时发送的数据量更大了)
如果应答丢失了,滑动窗口如何变化?
确认序号是累积的,比如前两个报文没有应答,第三个报文收到应答且确认序号包含了前两个报文,说明前三个报文都收到了。滑动窗口仍然可以右移。假如确认序号止步于先前的报文,滑动窗口不更新,补发丢失的报文。
如果是发送时数据包直接丢失呢?

如果发送端主机连续三次 收到了同样的确认序号,该序号对应报文必然丢失,就会将对应的数据重新发送,这就是快速重传机制。
已经有了快重传为什么要有超时重传呢?
因为快重传是具有条件,用于增效的,而超时重传必然保证数据交到对端手中。
滑动窗口指针有可能向左移动吗?大小固定吗,会如何变化?
左右指针都不可能向左移动,因为滑动窗口之前的数据都是已经收到应答的数据。
对方上层一直不取数据而窗口一直在接受数据,窗口剩余空间不断减小,滑动窗口左指针不断右移,最终和右指针重叠。也可能对方边取数据边收数据,左右指针同时右移,可能变大可能不变。
滑动窗口会越界吗
滑动窗口采用环装算法,类似于环形队列,滑动窗口前的区域都是已经收到应答的数据,相当于新的空间,移动到末端后从头开始覆盖即可。
(5)延迟应答
我们已经知道,发送方一次发送的数据量越大,收发次数减少,效率越高。而发送方滑动窗口受限于接收方通知的窗口大小。如果接收方能通知一个更大的窗口大小,发送方的滑动窗口也会更大,一次发送的数据更多。
为了实现这一点,接收方收到数据以后不会立刻应答,而是等上一段时间,让上层把缓冲区的数据拿走,从而返回应答的窗口大小就会更大。这就是延迟应答机制。
延迟应答不一定能够提高效率,但无论如何都推荐接收方每次通过read/recv尽快把数据拿走,这样就可以更新出窗口供发送端发送消息,从而提高效率。
当然受最大传输时长约束,不是所有包都可以延迟应答,所以每隔N个包/超过最大延迟时间一定会应答一次(这个时间肯定不超过最大传输时长)
(6)拥塞控制
前面所有策略都是针对两端而非网络本身的,拥塞控制则是针对网络本身的。
少量丢包是正常的的,而大量丢包(滑动窗口大量数据都超时)则属于网络问题。
网络问题可分为两种:硬件设备出现问题或流动数据量过大引起阻塞 。作为发送方不应立即重发数据,如果是硬件问题重发毫无意义,如果是数据量过大,重发反而加重网络负担 。此时可选择少发或暂时不发。当然这不是应用层决定的事情,而是TCP将这种共识嵌入到各个主机内。
所有主机都不发数据了,拥塞自然逐渐缓解(当然实际上不是所有主机同时不发,比如有的主机发送的数据量大,受到网络堵塞的影响大,会先遵守这个规则)。由此可见,TCP不仅仅作用在两两主机中,实质上同一网络所有主机都被TCP协议约束着。
慢启动
当然将大量资源投入到一个堵住的网络中是不明智的,TCP使用慢启动机制机制判断网络的健康状况从而确定发送速度:
TCP维护一拥塞窗口 ,一开始拥塞窗口cwnd大小为4,意味着最多可以发4个报文,于是一口气发4个报文后,如果收到了4个确认应答,cwnd+4变为8,下一次一口气发8个报文......如此往复,这个过程叫慢启动 ,假如cwnd大于慢启动阈值ssthresh------这个值最开始通常会被操作系统设置为一个很大的值,就会从指数增长改为线性增长,也就是在一个 RTT(往返时间)内,无论收到多少个 ACK,cwnd 只允许增加1。 这就是拥塞避免**。**
上述过程是发送方不断探索最大发送速度的过程,当发送方发现丢包时,会立刻执行乘法减小,也就是将慢启动阈值砍掉一半,并重新进行慢启动(拥塞窗口置零重新开始指数增长),直到再次到达新的慢启动阈值,再次改为线性增长.......如此往复,从而能尽可能以适配网络速度的速度发送数据。
拥塞窗口与滑动窗口的关系
滑动窗口即便很大,如果网络状况不佳,发送再多的数据会进一步导致网络拥堵,所以滑动窗口必须受拥塞窗口大小限制。
滑动窗口的大小的具体计算方式:min(对端窗口大小,拥塞窗口)
滑动窗口start指针的计算方式:由对端应答中确认序号决定(见前文)
滑动窗口end指针的计算方式:start指针+min(对端窗口大小,拥塞窗口,有效数据个数(end不能扩展到缓冲区没有有效数据的地方))
4.TCP周边知识
(1)面向字节流
对tcp来说,无论对端发来多少报文,缓冲区只有字节的概念,最重要的任务是把字节完整的交付,至于如何从字节流中分割报文都是应用层的工作(如http报文)
正因如此,tcp将数据交付给用户的时候,也是按字节流发送的,用户可能只读到了一半的数据。所以用户那边也要定义一个缓冲区,将读取的字节流存储起来再进行拆分,把字节流变成一个个完整的请求。这也是为什么TCP只有首部长度,因为根本不需要知道报文的数据长度,而且TCP也无法保证每次读上来的有效载荷就是一个完整有效载荷,TCP报文中的校验和只能校验当时发出去报文大小,因为序号就能够保证字节流全部发送。所以用户需要反复读取直到读到完整的报文。
(2)粘包问题
正如前文所述,TCP没有包的概念,发给用户的只有字节流。如果用户不具备从这些字节流有效区分报文的能力,就可能多读或者少读报文,这就是粘包问题。
解决此问题方式诸多,下面分别介绍:
使用定长报文
如两端约定好报文长度为100字节,假如客户端在一次读操作中读到50字节,就继续再等50字节。
使用特殊字符
约定好当报文中出现某些特殊字符时,就读取到了完整的报文。
使用自描述字段+定长报头
就是像udp一样,用报头说明有效载荷的大小,但该报头必须定长,否则接收端连报头都无法区分,自然也无法知道哪些字段表示有效载荷的大小。
使用自描述字段+特殊字符
如http协议,读到空行说明说明报头读完,从而确定读到一完整报头,报头内包含contentlength属性用于确定报文大小。
如果用户确实读上来的数据是不完整的呢?
这是不可能的,当数据在网线或光纤中传输时,可能因为电磁干扰等原因发生比特翻转(比如把 0 变成了 1)。
- 底层校验 :当这个损坏的报文到达接收端时,网卡和操作系统内核会第一时间计算校验和。
- 静默丢弃 :如果校验和发现数据对不上,内核会直接把这、、、个损坏的报文悄悄丢弃 ,并且绝对不会回复 ACK。
- 应用层视角:因为损坏的报文被内核拦截了,应用层根本不知道这个报文曾经来过,自然也就不会收到"残缺"的数据。
(3)连接异常
TCP连接异常主要分为下面三种情况:
进程终止
连接是和文件相关的(套接字本质就是文件描述符,通信是基于文件的)而文件的生命周期是随进程的,所以连接也是随进程的。进程终止时,操作系统在回收该进程的所有资源之前,会执行一个"清理工作"。在这个清理过程中,操作系统会遍历该进程打开的所有文件描述符。当操作系统发现某个 Socket 文件描述符还关联着一个活跃的TCP连接时,它会触发底层的网络协议栈。
此时,是操作系统的内核代替死去的进程向对端发送 FIN 报文,发起正常的四次挥手。
机器重启
我们关闭电脑前,总是提示还有进程在运行,如果仍选择关机,操作系统会杀掉所有进程,所以四次挥手仍正常发起。
机器掉电/网线断开
该情况发生时,客户端没有机会和服务端挥手,服务器正常维护连接。
客户端启动后并不知道服务端还在维护,重新连接服务端,这时出现连接认知不一致的问题,服务端把维护的连接断开,发送reset让客户端重新连接。
如果客户端不重新连接,TCP内部有Keep-Alive(保活机制),即如果长时间客户端不发消息,服务器会主动断开。
(4)补充
三次握手所做的工作
- 建立连接
- 协商起始序号
- 协商双方的接受缓冲区大小
文件和socket的关系简单介绍
socket接口会创建struct file和socket结构体,71struct file内有一private指针指向socket;而socket结构体内又有一指向sock结构体的指针,sock结构体内有发送和接收队列指针,当应用层写入数据时,数据被封装成sk_buff并挂入发送队列;接收队列:当底层协议栈收到数据时,会将sk_buff挂入此队列,等待应用层读取。
已知struct file内提供操作方法表以调用不同的硬件,因此为了让文件系统能调用网络方法,内核在创建Socket时,会为其绑定一套专属的文件操作函数表(socket_file_ops)
当应用层对套接字对应fd执行write()时,虚拟文件系统会通过struct file找到f_op函数表,并调用其中的sock_write_iter(即回调文件描述符),最终控制网卡等相关硬件将数据发送出去。
从上述过程看来,每个socket文件描述符对应一组缓冲区,再次验证了"linux下一切皆文件"的思想,即便网络也不例外。
关于sk_buff这里再做额外说明,每个连接有大量报文,需要进行先描述再组织的管理工作。所以应用层的每个报文被内核用sk_buff做封装,用链表连接。具体封装机制如下:
Linux在分配 sk_buff 的时候,会一次性把空间留足。
- 第一步(应用层/传输层) :当 TCP 层准备发送数据时,它会在内存中申请一块空间(创建
sk_buff)。此时,它会在数据缓冲区的前面预留出 40 字节的空间,然后把应用层的数据拷贝到预留空间之后的区域。 - 第二步(传输层) :TCP 层直接在那个预留的 20 字节空间里填写 TCP 头部信息,然后把
sk_buff的数据起始指针往前推 20 字节,把整个sk_buff交给IP层。 - 第三步(网络层) :IP层拿到这个
sk_buff后,发现前面还有 20 字节空位,直接填上IP头部,再把"数据起始指针"往前推20字节,交给链路层。 - 第四步(链路层) :网卡驱动拿到
sk_buff,在最前面填上 MAC 头部,直接交给网卡硬件发送。
结论 :每一层只是往预留的空间 里填数据,并移动指针, 解包时也只是把指针往回推,最后推导报文起始位置从而读到报文。封包解包全程只有一个sk_buff,而不是每一层都拷贝一次。