Linux网络——传输层
目录
1. 再谈端口号
2. UDP
① UDP 协议的报文
② UDP 协议报头的本质
3. TCP
① TCP 缓冲区的理解
② TCP 协议的报文
1. 16位源端口号 && 16位目的端口号 && 4位首部长度
2. 16位窗口大小
3. 32位序号 && 32位确认序号
4. 六个标记位
③ TCP 的面向连接特性
④ TCP 的可靠传输特性
1. TCP 确认应答机制
2. TCP 超时重传机制
3. TCP 流量控制
4. TCP 滑动窗口
5. TCP 延迟应答
6. TCP 捎带应答
7. TCP 拥塞控制
⑤ TCP 可靠与效率小结
⑥ TCP 的面向字节流特性
粘包问题
⑦ TCP 的异常情况
4. 文件与 socket 的关系
我们之前已经谈到过传输层负责的是保证数据能够完整的从发送端发送到接收端
1. 再谈端口号
我们将 源IP、源端口号、目的IP、目的端口号、协议号 五元组标识为一个通信。我们来看看端口号的划分
0~1023:知名端口号,包含 HTTP、FTP、SSH 等广为人知的协议
1024~65535:OS 动态分配的端口号
我们来看看一些常见的知名端口号
SSH -> 22
FTP -> 21
telnet -> 23
HTTP -> 80
HTTPS -> 443
我们在使用端口号时,应该尽量避免使用这些较为知名的端口号,此外我们之前提到过,一个进程可以绑定多个端口号,但是一个端口号是不能被多个进程绑定的!
我们可以使用 netstat 来查看通信情况,如图
2. UDP
之前我们曾说过,在学习一个新层级时需要学习下面几个东西
1. 报头与有效载荷如何分离
2. 有效载荷应该交付给上层的哪个协议
3. 认识报头
4. 学习该协议的相关问题
① UDP 协议的报文
UDP 协议的报文如图所示
源端口号与目的端口号不必多说,我们将 UDP 长度减去一个8,就能得到正文长度,稍微计算一下正文的最大长度
2 ^ 16 Byte = 64 KB
那假如需要传输的数据大小超过了64KB,我们还能使用 UDP 来传输吗?——可以!不过我们要将自己的报文拆解成 64KB 然后再发送; 而我们用 UDP 检验和来检验整个报文是否完全送达,如果发现数据出错,就会将当前报文直接丢弃。
可以看到,整个 UDP 运作的模式基本上有三个特点,即
无连接
不可靠
面向数据报
注:面向数据报意为:应用层交给了 UDP 多长报文,UDP 照原样发送,既不拆分也不合并。举个例子,发送端调用一次 sendto 函数发送100字节的数据,接收端也只能一样操作,不能循环调用10次 recvfrom 函数,每次接收10字节。也是因此原因,我们可以说 UDP 没有真正意义上的发送缓冲区,如图
虽然 UDP 也是全双工的工作模式,但是如果有一方的缓冲区满了之后,再接收到 UDP 报文就会将其直接舍弃。
② UDP 协议报头的本质
我们需要知道,Linux 系统本身是使用 C 语言写的,也就是说整个报头在实际上是如下这种形式
// 报头
struct udp_header
{// 使用位段来划分各个部分uint32_t src_port:16; // 源端口号uint32_t dst_port:16; // 目的端口号uint32_t length:16; // UDP 长度uint32_t check_code:16; // UDP 检验和
}
而 OS 会为其提供一个缓冲区,也就是说实际使用如图所示
这样就通过先描述后组织完成了 UDP 的使用。
我们最后再介绍一些基于 UDP 协议的应用层协议
NFS: 网络文件系统协议,实现跨网络访问文件。
TFTP: 简单文件传输协议,用于设备固件升级。
DHCP: 动态主机配置协议,自动分配网络设置。
BOOTP: 引导程序协议,用于自动网络配置。
DNS: 域名系统,将域名解析为IP地址。
3. TCP
① TCP 缓冲区的理解
TCP 的全名为 Transmission Control Protocol,即传输控制协议,如图
传输控制协议在整个过程中决定数据在什么时候发送,发送多少,发送出错了怎么办?也就是说,这些问题全部由 TCP 协议自主决定!而上层在调用诸如 write, read, send, recv 等函数时,其本质并不是将数据发送到网络中,而是将数据拷贝到 Linux 内核中,由 TCP 决定数据何时发送等。而发送缓冲区本身也存在于 Linux 中,也就是由多个 4KB 的struct_page 组成的。所以,我们在学习网络时,将网卡更换为磁盘,整体与文件管理相似。
② TCP 协议的报文
TCP 协议的报文如图所示
接下来我们来看看各个字段的意义
1. 16位源端口号 && 16位目的端口号 && 4位首部长度
之前我们说过,要想学习一个协议,首先需要了解如何将有效载荷与报头分离?分离出来后如何将有效载荷交付给上层?
交付给上层很简单,我们在学习 UDP 协议的时候已经学过,只要有源端口号与目的端口号就可以有效交付。
接下来就是有效载荷如何与报头分离的问题,我们可以除有效载荷外,我们使用的是固定长度报头 + 自定义描述,而报头长度为20位,而选项长度为40位
4位首部长度就可以很好地解决这个问题,4个数位可以表示 [0, 15] 范围的数字,我们在计算的时候,会以4字节为单位,也就是说4个数位可以表示 [0, 60] 字节的范围
举几个例子
如果首部长度的数字为 0101 -> 5 -> 5*4字节 -> 20,意思是这个报文不带选项;
如果首部长度的数字为 1101 -> 13 -> 13*4字节 -> 52,意思是这个报文带32字节的选项。
而将整个报文减去这个长度就能得到有效载荷!
2. 16位窗口大小
传输过程如图所示
整个通信过程是基于 TCP 协议的,也就是说,互发消息时,任何一方发送的都是一个完整的 TCP 报文,即一定携带完整的 TCP 报头。那我们为什么说相对于 UDP 来说,TCP 是可靠的呢?
TCP 为了保证可靠性,最基本的一个特点就是确认应答机制!具体一点来说,就是不管是客户端还是服务端,在发送了任何消息后,都需要收到回应,如图所示
回到这个16位窗口大小,我们需要先了解一下流量控制机制
流量控制:接收方的接收缓冲区快满或已满时,会让发送方减缓发送频率或者不发,从而减少丢包率
那我们要让发送方发慢一点,依据是什么呢?——16位窗口大小中填写的是 自己的接收缓冲区 中 剩余的空间大小,我们先前提到过每次发送信息与确认应答都带有一个完整的 TCP 报文,而当发送方收到响应后,会根据接收方的接收缓冲区剩余空间大小来控制发送速度!所以,16位窗口大小填写的是接收方的接收缓冲区的剩余大小。
3. 32位序号 && 32位确认序号
我们来想想,世界上存在完全可靠的网络协议吗?肯定不是,那么我们可以退而求其次,即保证局部上的可靠即可,举个例子
在操场的百米跑道上站着两个人(A 与 B),A 向 B 询问“你吃了吗?”,B 向 A 回答“收到!”,B 向 A 再回答“吃了!”,A 向 B 回答“收到!”
在这个例子中
1. 只要收到了应答,就能确认自己最近发送的消息被对方收到了
2. 我们也能发现,如果没有应答的数据,我们就无法保证可靠性,因此最新的一条消息是没有应答的,所以无法保证发送出去的消息是 100% 可靠的
那为了保证可靠性,如果在一段时间内没有收到应答的话,发送方就会认为数据丢失,然后进行重传。而在操场传话的例子中,回答收到后再回答吃了这一行为效率有点低,因此我们可以将他们合在一起,也就是 捎带应答。在实际的 TCP 运行中,发起请求操作一般来说都是并行的,如图
而由于我们一次性发了很多个报文,经转多个设备,最终接收方接收到的顺序可能会不同(如4132),也就是说出现了乱序问题,这会导致 TCP 变得不可靠,因此我们需要使用32位序号来保证数据的按序到达(如1234等序号)!
那么32位序号是什么呢?如图所示
发送的数据块的最后一个字符的下标就是32位序号中要填入的内容!那32位确认序号又是什么?——填充的是序号报文的+1数字,为什么呢?
确认序号的意义是确认之前的数据,我现在已经收到了,下一次发送数据请从确认序号处开始发送
举个例子
如果在收到确认应答时,收到了3001与4001而没有收到1001与2001,我们认为前3000个数据都收到了,也就是说 TCP 允许应答有少量的缺失。这里还有一个疑问,我们只需要将序号+1填回序号中返回即可,那为什么还要有确认序号呢?——由于捎带应答的存在,可能接收方也有自己要发送的数据,所以需要使用序号来发送数据!
4. 六个标记位
TCP 在进行通信时,会有很多行为,比如:建立连接,进行通信,断开连接等,也就是说 TCP 收到的报文本身是有各种类型的。而不同的类型决定了接收方要做不同的动作!那接收方如何得知报文的类型呢?——查看六个标记位,也就是说六个标记位的存在意义就是区分 TCP 的报文类型。
六个标记位分别是:URG(Urgent Pointer), ACK(Acknowledgment), PSH(Push Function), RST(Reset), SYN(Synchronize), FIN(Finish)
具体来说
ACK:确认,表示确认序号字段是有效的,用于确认已成功接收的数据,即表示这是一个确认应答报文。
SYN:同步,表示请求建立连接,即表示这是一个同步报文。
FIN:结束,表示发送方已经发送完毕,请求关闭连接。
我们如何理解“连接”这一行为呢?——服务器对客户端进行先描述后组织!
PSH:推送,提示接收方立刻从 TCP 缓冲区中将数据读走
如果上层(如 CP 模型)一直不取数据的话,就会导致接收缓冲区变满,这样就会让发送端发送不了数据(在 OS 系统层面的体现是同步与互斥,OS 网络层面的体现则是流量控制),也就是说接收缓冲区越大,另一端发送的速度就越快。
RST:重置,表示要求重新建立连接,即表示这是一个复位报文。
三次握手如图所示
什么时候 C 与 S 认为自己已经完成连接了呢?——只要认为自己发出去了 ACK 就认为成功!TCP 虽然保证可靠性,但是 TCP 是允许建立连接失败的,也就是说 TCP 在进行三次握手时可能会出现问题,那就是万一发出去的 ACK 丢失,CS 对于建立连接的认知就会产生不一致(C 认为建立连接成功,S 认为没有建立连接)!
为了解决这个问题,如果此时 C 向 S 发送数据,S 给 C 的应答报文中带上 RST,C 就会意识到建立未成功,然后重新发送连接请求。
URG:紧急指针,表示16位紧急指针有效,即数据包中有紧急数据,需要优先处理
举个例子
假设现在有一个 TCP 数据段,序列号是 1000,并且该段的 URG 标志被设置为 1,紧急指针为 50。序列号 1000 表示这个数据段的起始位置。紧急指针 50 表示从序列号 1000 开始,紧急数据结束于序列号 1050。因为 URG 标志被设置为 1,接收方就知道数据段中的序列号范围 [1000, 1050) 这部分数据是紧急数据,应该优先处理。
我们再举一个实际中的例子,假如现在有一个服务器,当客户端多次向服务器发起请求后,服务器没有任何反应,此时就需要以紧急指针的方式询问服务器的状态!
③ TCP 的面向连接特性
如图,TCP 是一个基于连接的协议,整个通信的建立和断开又叫做 三次握手 和 四次挥手!
那么为什么需要三次握手呢?
1. 可靠地验证全双工工作模式
2. 只要是奇数次的握手都可以建立连接!
那我们为什么要采用三次握手呢?如果是一次握手的话,很容易将服务器的连接队列占满,也就是会导致 SYN 洪水;如果是两次握手也会导致 SYN 洪水的情况。这两种方式都优先让 Server 端做出建立连接的动作,而使用三次握手可以将这个动作嫁接到 Client 端,即三次握手能确保在一般情况下,握手失败后的连接成本是嫁接在 Client 端的,同时在这个握手过程中 C 与 S 都能可靠地保证一次报文的收与发,这也能验证全双工通路是否通畅!
那为什么要进行四次挥手呢?
如果想断开连接,意思就是没有数据给对方发送了,但是发送数据这个行为是双方都有可能会做的,因此需要断开两次!也就是所谓的四次挥手。
接下来让我们看看整个连接过程中的细节
1. 连接的建立与 accept 函数无关,也就是说三次握手是双方的 OS 自动完成的
2. listen(listenfd, backlog) 函数中,第二个参数是什么?
如图
backlog 表示这个全连接队列的最大长度,accept 函数实际就是向这个队列中添加一个新元素,这个参数为什么不能太长呢?
如果上层一直不取走,而下层却一直添加,就会导致资源被占据而没有创造价值,因此这个长度一般来说是视情况而定的!
那可不可以没有这个参数呢?——不行!
举个简单的例子,一般海底捞的店门口都会放上一些椅子,当店内没有满载时就可以直接进入,这样的设计可以让服务器充分利用资源。
而除了全连接队列以外,还存在一个队列
在服务端第一次接收到连接请求后会进入 SYN_RCVD 状态,也就是说被连接的一方处于 SYN_RCVD 状态,也称为半连接,而对于半连接队列来说,半连接节点并不会长时间进行维护。
3. 在四次挥手中,主动断开连接的一方,在四次挥手完成后,要进入 TIME_WAIT 状态,然后等待若干时间后,自动释放
也就是说,如果是服务器发起的断开连接,在一段时间内是无法使用同一端口号重启的!我们可以使用 setsockopt 来关闭这一行为。
那为什么要进入 TIME_WAIT 状态呢?它会持续多长时间呢?
TIME_WAIT 状态可以让双方的历史数据得以消散。从而防止对后续的通信造成伤害;还可以让我们断开连接(四次挥手)具有较好的容错性。举个例子
在 TCP 的四次挥手关闭过程中,如果客户端发送的最后一个ACK在网络中丢失,那么服务器端会因为超时未收到确认而重传FIN包。如果客户端不进入
TIME_WAIT
状态,而是直接关闭连接,那么当服务器端重传FIN包时,客户端将无法识别并响应这个FIN包,导致服务器端无法正常关闭连接。TIME_WAIT
状态确保了客户端在足够长的时间内能够接收到服务器端可能重传的FIN包,并发送相应的ACK确认,从而保证连接能够可靠地关闭
TIME_WAIT 的持续时间一般是 2个MSL(一个报文在网络中存在的最长时间),而一个 MSL 在 RFC1122 定制的标准中规定为 2 分钟,也就是说 TIME_WAIT 实际的持续时间大概在 60s ~ 120s 左右。
④ TCP 的可靠传输特性
1. TCP 确认应答机制
如图所示
确认应答机制就是在每次收到报文后,都会向发送方回应一个应答报文。
2. TCP 超时重传机制
如图所示
对于这个特定时间间隔来说
最理想的情况下,我们应该找到一个最小的时间,以保证"确认应答一定能在这个时间内返回"。但是这个时间的长短,随着网络环境的不同,会产生差异,也就是说:如果我们设置的超时时间太长,会影响整体的重传效率;但是如果设置的超时时间太短,有可能会频繁发送重复的包!
那我们如何解决这个问题呢?
TCP 为了保证无论在任何环境下都能比较高性能的通信,会动态计算这个最大超时时间。
在 Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传;如果仍然得不到应答,等待 4*500ms 进行重传。以此类推,超时时间以指数形式递增。累计到一定的重传次数,TCP 会认为网络或者对端主机出现异常,从而强制关闭连接。
现在让我们想想另一个问题:对于 S 端来说,若其被 C 端进行超时重传发送了重复报文怎么办?
报文是有序号的!S 端可以根据序号来对报文进行去重。
3. TCP 流量控制
我们在前面介绍 16位窗口大小时已经提到了流量控制,接下来我们更加深入的了解一下
流量控制如图所示
对接收端来说,一旦窗口更新通知在传送途中丢失,会导致双方无法继续通信,因此发送端主机时不时就会发送窗口探测包。接下来我们还有几个问题
1. 我们在第一次发送数据的时候,怎么保证发送的数据量是合理的呢?
三次握手做到的事不仅仅是握手,在握手这个过程中 CS 双方已经交换了报文,也就是已经协商好了双方的接收能力!
2. 由于已经协商好了接收能力,所以第三次握手的时候可以携带一部分数据!也就是我们所说的捎带应答
TCP 窗口大小最大为 65535字节吗?——并不是,TCP 选项中有一个 M 选项(窗口扩大因子),TCP 窗口的实际大小为 窗口大小左移 M 位。
3. 流量控制是属于 TCP 的可靠特性,还是属于效率特性呢?——可靠为主,效率为辅!
4. TCP 滑动窗口
实际的交流过程图如下
在这个数据传输过程中,已经发送出去的,但是暂时没有收到应答的报文,会被 TCP 暂时保存起来,这样的报文发送方可能会存在很多个,那这些报文会被保存到哪里呢?发送缓冲区如图所示
其中,对于已发送已确认的区域,我们会将其设置为可被覆盖的状态,这样就叫做从 TCP 发送缓冲区中移除它了!而我们将已经发/可以发送数据但是尚未收到应答的区域称为 滑动窗口!正是因为有滑动窗口区域,才能一次向对方发送最大的 TCP 报文。
它是否与我们学过的算法中的滑动窗口(算法——滑动窗口)有关系呢?——有的,窗口滑动本质上指的就是指针右移!那么我们还有几个疑问
1. 滑动窗口在哪里?——发送缓冲区的一部分
2. 滑动窗口的范围大小是多少?——目前我们认为是对方的接收窗口的大小
3. 如何理解发送缓冲区的区域划分?——通过指针/下标区分即可
滑动窗口如图所示
我们对于确认序号的定义如下
如果确认序号是 X,则 X 之前的报文我们都收到了,这个过程允许少量的 ACK 丢失
确认序号保证了滑动窗口线性地向后更新,不会出现跳跃的情况。 接下来我们加深一下对滑动窗口的理解
1. 如果在传输过程中出现了丢包问题,我们如何理解滑动窗口?
假如 1~1001, ... , 4001~5000 的 ACK 丢失了,但是 5001~6000 的 ACK 没丢失怎么办?——不怕有什么影响,只要收到了 5001~6000 的应答报文,就直接将滑动窗口的左端指针移动到 6001即可。
那假如 1~1001, ... , 4001~5000 的 ACK 没丢失,但是 5001~6000 的 ACK 丢失了怎么办?——将滑动窗口的左端指针移动到 4001,然后对 4001 其后的数据进行补发即可。
那假如在数据的传输过程中,接收方没有收到 1001~2000 的数据,而收到了1~1000 与 2001~6000 的数据怎么办?——如图所示
我们将连续收到3个同样的 ACK 后进行补发这一行为,称为快重传。那已经有了快重传这一机制,为什么我们还要有超时重传呢?——快重传是有条件的,它是为了提高效率而存在的,而超时重传是兜底的。
2. 如何理解滑动窗口中的指针向右移动?移动的时候,大小会变化吗?怎么变化?会变为0吗?
我们先回答后面的问题,大小是会变化的,一般来说大小是动态变化的,也可能会变成0。指针在右移的时候,一般有3种情况
1. 右指针不动,左指针移动 -> 范围缩小
2. 左右指针都移动 -> 范围扩大
3. 左右指针都移动 -> 范围缩小
左指针 = 收到的确认序号,右指针 = 确认序号 + min(窗口大小,有效数据),我们之前讲的流量控制就是通过滑动窗口来进行控制的,它不仅能将传输速率往小了控制,也能往大了控制。
3. 滑动窗口在发送缓冲区中移动指针会发生越界的情况吗?
不会的,TCP 的滑动窗口机制是基于一种“环形缓冲区”的思想设计的。通过这个机制,发送方和接收方在建立连接时会协商一个初始序号(例如 1234)。从这个初始序号开始,数据会被分配到序号加上数组下标的相应位置。由于滑动窗口使用环形缓冲区的方式,发送方的“发送指针”会随着数据的发送和确认逐步前进。如果发送窗口达到发送缓冲区的末尾,它会“回绕”到缓冲区的起始位置,继续发送未发送的数据。因此,滑动窗口的指针不会越界,它始终会保持在有效的缓冲区范围内。
5. TCP 延迟应答
在 CS 端互相发送数据的时候,我们已经知道,发送方一次发送更多的数据,发送的效率就会更高,也就是说发送方认为接收方能够接受更多的数据,这样的效率就会变高,意味着接收方应该尽量给发送方通报一个更大的窗口!
那我们如何让接收方给对方通告一个更大的窗口呢?——收到报文不着急应答,也就是延迟应答,这从根本上来说是一个赌概率的机会,赌上层能否尽快地取走数据,从而延迟应答时能回应一个更大的窗口。也是因此,如果我们要设计一个服务器,我们比较推荐的做法就是每次都尽快地通过read, recv 等函数将数据全部从 TCP 中拿上来!
那么所有的包都可以延迟应答么?当然不是,延迟应答有数量限制与时间限制,即
数量限制:每隔N个包就应答一次
时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,不同的操作系统有差异。一般来说 N 取 2,超时时间取 200ms。
6. TCP 捎带应答
捎带应答(piggybacking)是 TCP 协议中一个常见的机制,尤其是在数据传输阶段。这个机制具体来说是指在某个数据包的应答(ACK)中“捎带”回传数据,避免单独发送确认包,从而提高效率,减少网络负担。
7. TCP 拥塞控制
目前我们学习到的几乎所有策略,都是在两端的机器上起作用的。而 TCP 还为我们考虑了网络的问题——也就是拥塞控制!
如果任意主机在发送数据的时候出现了问题,这个问题可能不仅仅是对方主机出现了问题,也有可能是网络出现了问题。举个例子
- 如果通信的时候,出现了少量的丢包 -> 正常情况
- 如果通信的时候,出现了大量的丢包 -> 网络出现了问题
这个网络出现了问题只是一个笼统的概述,具体问题可能是硬件出现了问题,也有可能是数据量太大,引起了阻塞。
如果通信双方都出现了大量的数据丢包(大量数据超时)问题,那么 TCP 就会判断网络出问题了(网络拥塞了)。那我们作为发送方,应该怎么办呢?——暂缓一下,间歇性的发送少量数据,绝对不能立刻对报文进行超时重发!这样做只会加重网络的拥塞情况。
由于各台主机均使用 TCP/IP 协议,所以网络出现了拥塞对多台主机来说是“共识”,因此拥塞控制的策略是每台识别到当前网络已经拥塞的主机都要做的事。实际上,滑动窗口的大小其实也与拥塞窗口相关,即
滑动窗口大小 = min(接收窗口大小,拥塞窗口大小)
接收窗口大小考虑的是对方主机的接收能力,而拥塞窗口大小则考虑的是动态的网络的接收能力。所以我们现在可以重新定义一下滑动窗口的右指针了,即
右指针 = min(接收窗口,有效数据,拥塞窗口)
拥塞窗口是主机判断网络健康程度的指标,如果超过了拥塞窗口,会引发网络拥塞,反之则不会。如果网络出现了拥塞,当双方发送少量报文都表示可行的时候,网络应该趋于健康了,此时应当尽快恢复正常通信了!但是网络是动态的,所以拥塞窗口肯定不是静态的,如图所示
⑤ TCP 可靠与效率小结
目前我们可以看到 TCP 为了数据传输的可靠性,采取了如下方案
- 校验和
- 序列号(按序到达) -> 去重工作
- 确认应答 -> 保证可靠性的核心策略
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
而为了提供数据传输的效率,采取了如下方案
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
⑥ TCP 的面向字节流特性
面向字节流的意思是读写不需要一一匹配,对用户层来说,用户只关心应用层的协议;对于传输层来说,OS 负责具体的网络通信细节。由于缓冲区的存在,TCP 程序的读和写不需要一一匹配,举个例子
- 写 100 个字节数据时,可以调用一次 write 写 100 个字节,也可以调用 100 次 write ,每次写一个字节。
- 读 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节,也可以一次 read 一个字节,重复 100 次。
在 TCP 这一层,它并不关心上层协议,也不关心上层的报文格式,只有字节的概念!
在应用层这一层,用户对报文进行处理必须一个一个进行处理!将提取上来的字节流转换成一个一个完整的请求,这之后进行反序列化。
粘包问题
如果用户在提取字节流后没有将其处理成一个个请求或者少处理了几个请求,对于应用层来说就会产生粘包问题,而我们解决粘包问题最简单的做法就是定制协议!这也是我们在做网络版计算器时封装了 Encode 与 Decode 函数的原因(详见Linux网络——应用层中的网络计算器)。
所以,如何解决用户层粘包问题呢?——在应用层通过协议,明确报文与报文之间的边界。举几个例子
1. 使用定长报文
2. 使用特殊字符
3. 使用自描述字段 + 定长报头
4. 使用自描述字段 + 特殊字符(如 HTTP)
⑦ TCP 的异常情况
TCP 协议本身是基于连接的,而连接本身是和文件直接相关的!文件的生命周期是跟随进程的。如果在 CS 双方进程通信时,如果 C 端因为一些原因导致进程终止,那么连接就会正常终止。那要是 C 端直接断电/网线断开,那 S 端会一直维持住 TCP 吗?——TCP 有保活机制,即 S 端在一段时间内不会断开,而超过了一定时间会自动断开。
4. 文件与 socket 的关系
如图所示
对于我们目前学习的协议栈来说,OS 负责的是传输层与网络层,OS 在每一层中需要做的只有两件事
1. 使用特定的数据结构表述协议
2. 生成与特定协议相匹配的方法集
而对于每一层来说,均会收到多个报文,服务器需要对他们进行先描述后组织,我们查看 Linux 内核可以看到
事实上,我们在协议栈中向上向下交付,本质上就是在向头空间/尾空间添加报头,也就是说我们需要进行的封包与解包操作本质就是在移动指针!而我们在不同层间移动时,并不需要进行频繁的拷贝,只需要移动指针即可!