目录
一. 对于端口号的理解
1.1 网络通信五元组
1.2 端口号的划分策略
二. 网络通信中常用的指令
2.1 netstat指令
2.2 pidof指令
三. udp协议
3.1 udp的概念及特点
3.2 udp协议端格式
3.3 对于面向数据报及应用层发送与读取数据的理解
四. tcp协议的概念及特点
五. tcp报头
5.1 tcp协议端格式
5.2 tcp报头各部分的含义
六. 三次握手和四次挥手
6.1 三次握手和四次挥手的流程
6.2 三次握手解析
6.3 四次挥手解析
七. tcp协议保证可靠性和提高效率的机制
7.1 确认应答机制
7.2 超时重传机制
7.3 去重和按序到达机制
7.4 流量控制机制
7.5 滑动窗口机制
7.6 快重传机制
7.7 网络拥塞控制机制
7.8 延时应答机制
7.9 捎带应答机制
八. 对listen接口的第二个参数的理解
九. 对于tcp一些概念的理解
9.1 面向字节流
9.2 粘包问题
9.3 tcp异常情况
9.4 udp实现可靠通信的方法
十. 总结
一. 对于端口号的理解
1.1 网络通信五元组
在网络通信中,假设主机A要给主机B的发送数据,那么本质上就是主机A的某个进程要与主机B的某个进行实现进程间通信。主机A发数据时,通过全网中唯一的IP地址,可以定位到主机B,再通过目的端口号,即可获取主机B上要接受主机A发送信息的进程。通过 IP地址 + 端口号,可以确定全网中唯一的一个进程,实现网络环境下的进程间通信。
网络通信五元组:源IP、目的IP、源端口号、目的端口号、协议号。
这里的协议号,我们可以理解为传输层所使用的协议,即:udp(面向数据报协议)或tcp(传输控制协议)。
如图1.1所示,假设两个客户端都打开了某一浏览器,那么客户端就要与服务器建立通信。对于服务器而言,需要属于其自身的独立端口号,以便客户端确定接受信息的进程。而客户端的可能打开多个浏览器画面,每个浏览器画面都是一个独立的进程,拥有独立的端口号。
在图1.1中,服务器就是目的端,其ip和提高服务的进程的端口号,就是目的端口号。而客户端的ip和打开浏览器界面的进程的端口号,就是源端口号。传输层使用的tcp/udp协议,可以理解为协议号。
一个进程可以绑定多个端口号,但一个端口号不能绑定多个进程。
1.2 端口号的划分策略
端口号用16位的数据表示,范围是0~65535,根据编号数值,将端口号划分为知名端口号和操作系统动态分配的端口号:
- 0 ~ 1023:知名端口号。一些知名的服务,如http、https、ssh等使用的端口号,这些知名服务使用的端口号是固定的,这样能够方便客户端进行连接。
- 1024 ~ 65535:由操作系统动态分配的端口号,应用层软件的进程,所使用的的都是这些端口号,如果使用知名端口号则很容易引发冲突。
常见的知名端口号:http(80)、https(443)、ftp(21)、ssh(22)、telnet(23)。
二. 网络通信中常用的指令
2.1 netstat指令
netstat指令的功能为查看OS中的网络状态。
语法:netstat [选项]
netstat常见选项及功能:
- n:尽量以数字的形式显示网络信息。
- t:显示使用tcp协议的网络通信。
- u:显示使用udp协议的网络通信。
- l:显示处于监听状态的服务器。
- p:显示进行网络通信的进程名称。
- a:显示所有的网络通信(默认不显示监听状态,选项a会显示)
2.2 pidof指令
pidof可以直接获取进程对应的pid,避免使用ps axj | grep ... 来查看进程pid。
语法:pidof [进程名称]
三. udp协议
3.1 udp的概念及特点
udp协议,全程面向数据报协议,是在网络通信中传输层用到的协议。udp协议具有以下的特点:
- 无连接,在通信之前,客户端和服务端不需要预先建立连接即可通信。
- 不可靠,在传输中可能出现丢包、报文乱序等问题。
- 面向数据报,不能够灵活的控制发送和读取数据的次数,数据见具有明确的边界。
- 编码简单,由于不需要可靠性控制,udp的实现成本要远低于tcp。
3.2 udp协议端格式
udp协议的报头固定8字节长度,其中16位目的端口号用于目的主机找的接受数据的进程,16位源端口号用于目的主机向源主机发回数据时确认端口号。
udp协议是面向数据报的,每次发送和接受的数据大小固定,而面向数据报就是由udp报头中的16位窗口大小支持的。
16位窗口大小是整个udp报文(数据端)的大小,有效数据的大小 = 16位窗口大小 - 8字节。
如果16位校验喝出错,报文就会直接被丢弃。
udp的报头,是通过结构体(位段)来定义的,使用位段是由于网络资源相对紧张,使用位段能够有效减少报头的长度,节约网络资源,在tcp协议中报头也是使用位段来组织的。下面为一段udp报头定义的demo代码。
struct udp_header
{
uint32_t src_port : 16;
uint32_t dst_port : 16;
uint32_t udp_size : 16;
uint32_t udp_check : 16;
};
对于任何协议的报文,都需要结局两个关键问题:
- 如何封装数据(添加报头)。
- 如何向上交付数据(解包)。
对于udp协议的报文,我们看到,其报头的大小是固定的。因此,通过一下的操作,可以提取出报文中的有效数据并向上交付:
- 提取出8字节的报头。
- 提取报头中的16位窗口大小。
- 有效数据大小 = 16位窗口大小 - 8字节报头。
- 通过指针偏移,获取指向有效数据起始位置的指针。
3.3 对于面向数据报及应用层发送与读取数据的理解
对于udp协议面向数据报的理解:udp协议报文的长度是可以确定的,而由于其报头长度又是8字节,这样就可以确定有效数据的大小。接受方通过获取有效数据的大小,就可以读取固定长度的数据,这样也就实现了面向数据报。在udp中,一次发生的数据,接收方不可以分为多次读取。
对于应用层发送数据和读取数据的理解:在udp/tcp协议中,要维护发生缓冲区和接受缓冲区,udp协议由于是面向数据报的,一次发送固定长度的数据,所以udp没有发送缓冲区,但是udp有接收缓冲区。而tcp协议则既有发送缓冲区也有接受缓冲区。
- udp协议没有发送缓冲区,有接收缓冲区。
- tcp协议既有发送缓冲区也有接受缓冲区。
应用层调用send/sendto/recv/recvfrom这一类网络IO接口发送和接受数据,本质上并不是将数据直接传到网络中,而是将数据拷贝到内核中协议维护的缓冲区里,或者从缓冲区中拷贝数据到应用层,至于数据什么时候发送的网络,则是由OS关心的,与上层用户和协议无关。
四. tcp协议的概念及特点
tcp协议,全称传输控制协议,属于网络通信中的传输层协议,传输控制协议,顾名思义就是可以保证网络通信可靠性的协议。
tcp协议具有以下特点:
- 需要连接:在两台主机正式通信之前,需要三次握手建立连接,结束通信时需要四次挥手断开连接。
- 通信可靠:一般不会出现丢包、报文乱序等问题。
- 面向字节流:读写不需要一一匹配,应用层只负责向下交付数据,至于具体怎么发送数据,则完全由tcp控制。
五. tcp报头
5.1 tcp协议端格式
图5.1为tcp协议端的一般格式,分为三部分组成:20字节定长报头 + 选项 + 数据。
5.2 tcp报头各部分的含义
如图5.1所示,选项之前的20字节数据为tcp协议的定长报头,其中每一部分的含义和功能为:
- 16位源端口号和目的端口号:用于确定通信双方主机上,是哪两个进程在进行进程间通信。
- 32位序号和确认序号:用于确保网络通信的可靠性,每当一条报文发出,就要携带序号,而tcp为了保证传输数据的可靠性,就要求每一条发出去的报文要收到与之对应的确认收到的响应,确认序号表示该序号之前的报文以及被接收端接收了。确认序号和序号一定要区分,不可以使用同一序号来确认应答,因为tcp是全双工的,一条响应信息也有可能携带要发给对方的数据,而这条信息也需要被对方确认收到。
- 4位首部长度:以4字节为基本单位,表示20字节报头 + 选项的总长度,4字节的数据可表示的范围是0 ~ 15,20字节定长报头 + 选项长度 = 4位首部长度 * 4字节,我们可以将tcp报文中的前部20位定长数据 + 选项 统称为报头,那么tcp报头长度范围是 20bytes ~ 60bytes。
- 6个标志位:
- URG:紧急状态标志位,配合16位紧急指针使用,表示该报文不排队直接被接收方接收。
- ACK:确认应答标志位,表示该报文是对某一条接收到的报文的响应,在tcp协议网络通信中,大部分的报文ACK标志位都是1。
- PSH:催促标志位,配合16位窗口大小使用,如果发现对端迟迟没有接收数据的能力(接收缓冲区空间不足),那么就设置PSH标志位为1,催促对方尽快向上交付数据。
- PST:复位报文段,表示需要重新建立连接。如果因为某种异常原因,客户端与服务端的连接断开了,但此时客户端并不知道连接已经断开,仍然正常向服务器发送数据,服务器在收到数据时发现源客户端没有链接,那么就会向客户端发送PST标志位为1的报文,告知客户端需要重新进行三次握手建立连接。
- SYN:请求建立连接标志位,将带有SYN标识的报文称为同步报文段。
- FIN:连接关闭标志位。用于通知对端,连接要关闭了,携带FIN的报文称为结束报文段。
- 16位窗口大小:用于获取对端的接收能力,即:对端的接收缓冲区还有多少剩余空间可用。
- 16为检验和:如果检验不成功,就认为数据存在问题,需要客户端重发。
- 16位紧急指针:配合URG标准位使用,表示要被对端优先紧急接受的内容相对于报文中数据起始位置的偏移量,注意对端只能接受一个字节的紧急内容,无法给定对端需要紧急接受的数据的长度。紧急指针一般用于检查服务器的状态,假设某台主机向服务器发送的数据迟迟得不到应答,那么就需要发送一个带有URG标识为的数据,获取对端相应,以确定是因为网络故障还是服务器本身崩溃引发的问题。在一些大型互联网厂商,会有一台控制主机控制多台服务器,这台控制设备会定期向每台服务器发送带有URG标识为的报文,以检验每台服务器的工作状态是否正常。
六. 三次握手和四次挥手
6.1 三次握手和四次挥手的流程
图6.1位三次握手和四次挥手通信双方(主机A和主机B)向对方发送的报文标识位信息以及双方主机所处的状态。当三次握手/四次挥手全流程结束后,通信双方的连接才正式的建立/断开。
6.2 三次握手解析
如图6.1所示,三次握手建立连接时,主机A和主机B进行的操作流程为:
- 第一次握手:主机A的进程通过connect申请与主机B建立连接(主机B处于LISTE状态),即:主机A向主机B发送带有SYN标志位的报文。
- 第二次握手:主机B应答主机A,发送带有ACK标志位的报文,主机A收的主机B的应答后,即处于ESTABLISH状态,确认建立连接。
- 第三次握手:主机A建立好连接后向主机B发生ACK报文,主机B收到报文后随机处于ESTABLISH状态,完成建立连接的操作。
SYN -> SYN + ACK -> ACK的过程称为三次握手。
问题:为什么不能是一次握手,两次握手或四次握手?-- 从安全性、可靠性两方面考虑。
三次握手能在一定程度上提高安全性:
- 连接是需要管理的,管理连接需要消耗资源,OS会通过struct结构体描述连接,通过特定的数据结构组织连接,这需要消耗内存资源和CPU资源。
- 如果采用一次握手:此时客户端只要大量向服务器发送SYN请求,就可以在短时间内让服务器充斥大量的连接,从而造成服务器瘫痪,这种状态被称为SYN洪水。
- 如果采用二次握手:问题与一次握手本质上相同,主机A只需要丢掉主机B发回的ACK,就可以只在主机B建立连接而主机A中没有连接,引发SYN洪水。
- 如果采用四次握手:那么主机A在第一次收到主机B的ACK应答后并不会建立连接,而收到两次ACK应答才会建立连接,那么主机A丢掉第二次的ACK报文,即可引发SYN洪水。
- 三次握手的主要目的不是为了解决安全性,假设一个黑客同时控制数万台电脑,通过提前设置好的指令让这数万台电脑在同一时刻向某服务器发送SYN连接请求,那么一样可以瘫痪服务器,SYN三次握手,知识可以避免计算机小白攻击服务器。因为服务器建立一个连接,客户端就要对应的建立一个连接,客户端与服务器需要承受同等级别的资源消耗,而服务器配置一般远高于单台计算机,这样计算机小白就无法通过单台计算机攻击服务器。
三次握手保证可靠性(验证全双工):
- 全双工,即通信双方同时具有收数据和发数据的能力。
- 主机A向主机B发生连接请求,主机B会给主机A一个ACK应答,这样就可以检验主机A具有收数据发数据的能力。
- 主机B在向主机A发送ACK应答时,也会收到来自主机A的ACK应答,这样就验证了主机B具有收数据和发数据的能力。
6.3 四次挥手解析
如图6.1所示,通信双方需要通过四次挥手断开连接,四次挥手过程中,主机A和主机B所执行的操作流程为:
- 第一次挥手:主机A调用close(fd)主动关闭连接,向主机B发送带有FIN标志位的报文。
- 第二次挥手:主机B收到主机A的FIN报文后,向主机A发送ACK应答,确认其收到了主机A的FIN请求。
- 第三次挥手:主机B在发送完ACK后,调用close(connfd)关闭连接,向主机A发送FIN报文。
- 第四次挥手:主机A收到主机B的FIN报文后,向主机B发送ACK,主机B再次收到ACK后结束四次挥手,断开连接。
在四次挥手中,有两个状态值的关注,第一个是TMIE_WAIT,另一个是CLOSE_WAIT。
TIME_WAIT状态的理解:
- 当主动断开连接的主机(主机A)收到对端连接的FIN报文后,并不会立即将连接断开,而是会等待一定的时间才将连接状态设置为CLOSE,当连接处于CLOSE状态时,才算是真正的断开连接。
- 在TIME_WAIT状态下,虽然连接理论上处于断开状态,但是其ip和端口号依旧处于占用状态,不能重新建立连接。
- 如果一个服务器在关闭后处于TIME_WAIT状态,那么他就不能被立马重启,而是需要等过一段时间后,连接状态变为CLOSE才能够重启。
- TIME_WAIT状态存在的目的是为了清空网络中残存的数据,网络中残存数据,可能是由于丢包等问题产生的,如果下次建立连接后网络中中的残余数据还没有被清理掉,那么下次连接的SYN请求可能会失败,还可能对通信造成其他的一些干扰。
- 一般来说,TIME_WAIT时间是2MSL,即:报文单向传输的最大时间,这样就可以基本保证通信双方在TIME_WAIT阶段能够进行一次数据的发送和应答操作,最大程度的保证网络中的数据被清空,但是无论如何也不可能确保网络中的数据100%清空,2MSL的等待时间可以极大程度确保清空网络数据。
某些时候,如果服务器挂掉不能立刻重启,会造成很大的损失,因此需要能够在TIME_WAIT状态下重启,通过setsockopt函数可以实现上面的功能。
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
CLOSE_WAIT状态的理解:
如果在网络通信中,某连接长期处于CLOSE_WAIT状态,或者有大量连接处于CLOSE_WAIT状态,那么则是服务器程序写出了BUG。
造成大量CLOSE_WAIT状态的原因:服务器没有close(connfd)。这种情况下,服务器就不会向客户端发送FIN报文,也就无法将状态变换为LAST_ACK状态,造成服务器中存在大量的连接,消耗CPU和内存资源,最终导致服务器崩溃。
七. tcp协议保证可靠性和提高效率的机制
7.1 确认应答机制
在tcp协议中,主机每接收到一条报文,就要向对端发送一个ACK应答,告知对方收到了报文。
如图5.1所示,在tcp报头中,存在32位序号和32位确认序号,每一条带有ACK标志位的报文,其携带的确认序号的意义在于:告知对端该序号之前的报文已经收到了,并且告知对端下一次发送数据的起始序号。
确认序号的两个作用:
- 告知对端确认序号之前的报文全部被接收了。
- 告知对端下次发送报文的起始序号。
在网络状况良好的情况,几乎不存在丢包问题,确认序号 = 序号 + 1。
如图7.1所示,我们可以将发送缓冲区看做是一个char类型的数组,其中每个待发送的char类型数据都有一个对应的下标位置,tcp协议单次发送数据的下标范围记为 [left, right],其中我们可以将发送的最后一个字符对应的下标right记为序号,right+1即是确认序号。
在tcp协议通信的过程也如图7.1所示,我们可以暂时将其视为是串行的过程,假设第一次发送报文的确认序号为1000,表示发生了下标为1~1000的数据,那么就会收到确认序号1001,下次从下标1001的位置开始发送数据,以此类推就是确认应答机制的工作原理。
7.2 超时重传机制
如图7.2所示,假设主机A发送给主机B的数据发生了丢包,或者主机B发送给主机A的确认应答报文发生了丢包,就会导致主机A迟迟收不到来自主机B的应答,那么主机A就会判定刚发出去的数据可能丢包,会触发超时重传机制,再次发送没有收到确认应答的报文。
7.3 去重和按序到达机制
- 去重机制:在tcp协议中,如果主机B发送给主机A的确认应答报文ACK发生了丢包,那么即使是主机B收到了对应的数据,主机A依然会按照数据丢包处理,进而触发超时重传机制。在这种情况下,主机B会收到两份同样的报文,为了保证tcp通信的可靠性,主机B就需要根据报文的序列号来进行去重,以避免上层应用收到两份相同的报文。
- 按需到达机制:tcp协议可靠性的一点体现就是主机B接收报文的顺序与主机A发送报文的数据保持一致,即按序到达。但是,实际的网络环境十分复杂,会存在先发送的报文后到达的情况,此时作为接收端主机B,就需要根据报文中的序号来对报文进行排序,以保证报文的按序到达。
7.4 流量控制机制
- 流量控制:通过获取对端接收能力,动态调整发送数据的速度。
接收端接受数据的能力是有限的,这是因为接受缓冲区的剩余大小是有限的。换句话说,就是主机从网络中接受数据的能力取决于接受缓冲区的剩余空间大小。
如果接受缓冲区的剩余空间过小,而又有大量的数据从网络中发送到主机,那么就会造成接受缓冲区溢出的问题,溢出的这部分数据就会被丢弃,引发丢包造成通信过程不可靠。
但是,在保证可靠性的前提下,还应当注重网络通信效率的提升。我们希望达到这样的目的:对端接收能力强发送数据就快,接受能力弱发送数据就慢。为此,数据发送方就必须要获取对端接受缓冲区剩余空间的大小。
在tcp协议的报头中,有16位窗口大小,用于表示对端接收数据的能力(对端接收缓冲区大小),数据发送方根据窗口大小动态调整发送数据的速度。如果检测到16位窗口大小为0,则表示对端接收缓冲区没有剩余空间,会暂时停止数据的发送。
当由于检测到窗口大小为0而停止发送数据后,数据发送方会每隔一定的时间间隔向对端发送探测信息,以确定对端接收缓冲区是否有了剩余空间可以接收数据,如果检测到了窗口大小更新,那么数据发送就会恢复。
图7.3为流量控制机制的工作原理图。
提问:第一次发送数据时还没有收到应答,怎样确认对端的接收能力?在三次握手建立连接时,通信双方向对端发送的报文中,会携带窗口大小。
7.5 滑动窗口机制
如果tcp协议按照图7.1中表示的串行方式发送数据,那么一个数据在发出后,只有收到对端的应答,才可以发送下一条数据,这样tcp协议相对于udp协议,效率就会十分低下。
为提高效率,tcp引入滑动窗口,即:在滑动窗口内的数据,不需要获取对端的应答即可发送。
滑动窗口的大小,由对端接收能力和网络状况共同决定,一般我们认为:滑动窗口大小 = min(16位接收窗口大小, 网络拥塞窗口大小)。
滑动窗口,是位于发送缓冲区的滑动窗口,图7.6位滑动窗口的物理结构示意图,在存在滑动窗口的情况下,可以将发送缓冲区分为三块来看待:
- 在滑动窗口左侧的数据,是已经被发出去并且收到确认应答的数据。
- 在滑动窗口内部,是不需要收到对端应答就能发送的数据。
- 在滑动窗口右侧,是暂时还不能发送的数据。
当数据发送出去且收到应答,并且对端窗口的大小不变或者变大时,滑动窗口的右边界会向右移动,数据收到对端的确认应答后左边界也会向右移动。滑动窗口内的数据,是不断动态变化的。
如图7.5所示,主机A检测到主机B接受能力为4000,那么滑动窗口最初的范围就是[1,4000],其中的数据在没有收到确应答的时候就可以发出。假设主机A收到了确认序号为1001的报文,表示1~1000的数据被对端获取到了,且对端的接收能力变为4000不变,那么滑动窗口的左右边界都会向后移动,变为[1001,5000]。
提问:滑动窗口向后移动,是否会出现越界问题?
答案是不会的。因为虽然发送缓冲区的物理结构是线性数组,但是其逻辑结构实际上为环形数组,假设滑动窗口的右边界为indexRight,那么其在实际的线性数组中对应的下标是 indexRight % N,其中N为发送缓冲区的最大容量。
这种情况下,如果主机B的某个确认应答发生了丢包,如图7.7所示,假设主机B的2001和3001确认应答在传输过程中丢包,但是4001号确认应答被主机收到了,这并不会影响后序数据的发送,因为确认序号的含义为:告知对端在确认序号之前的所有报文都被成功接收了。
因此,只要主机A收到4001确认应答,就会自动认为4001之前的所有数据都被对端接收了,哪怕没有收到之前的确认应答。
7.6 快重传机制
- 快重传:如果主机三次收到确认应答序号相同的报文,那么就触发重传机制。
还是以滑动窗口的场景为例,在图7.5中,假设数据1001~2000发生了丢包,那么即使主机B收到了3001~4000、4001~5000等报文,只要1001~2000还没有收到,主机B就只能给主机A发送1001确认应答,在这种情况下,主机A就会收到连续3个1001号确认应答,此时就会判定1001~2000可能发生了丢包,该数据会自动重传。
而主机B在收到重传后的1001~2000数据后,就会直接将确认序号更新到接收的序号最大的数据。
快重传机制与超时重传机制是相互配合的,如果一个报文丢失,但对端在一定时间内没有向源主机发送三个一样的确认应答,那么就必须由超时重传机制触发重传数据。
7.7 网络拥塞控制机制
虽然tcp协议有滑动窗口以及流量控制机制可以提高数据传输的效率,但是,这只是在通信双方的主机层面来考虑的,在实际的网络数据传输中,还要考虑网络状况的好坏来动态调整发送数据的速度。为此,tcp协议引入了网络拥塞控制机制来根据网络状态调整数据发送频率。
图7.8为网络拥塞控制及调整的策略,遵循慢启动机制,即:在通信开始的时候,先发送少量的报文探测网络情况,如果能够顺利接受到确认应答,再不断加大数据大发送量,最多不能超过对端的接收能力。
慢启动机制,只是在发送数据的初期启动缓慢,其增长速度遵循指数级别增长,因此发送数据几次后,就会快速增长。
慢启动机制的控制策略可以总结为:
- 通信开始阶段,先发送单位数据探测网络情况,此时记网络窗口大小为1。
- 每一批发送的报文,如果全部收到了应答,那么网络窗口的大小就翻倍,我们可以理解为每收到一个ACK,网络窗口大小就+1。
- 将网络窗口的大小与对端接受能力进行对比,取较小的值为滑动窗口大小。
由此,我们可以认为,慢启动机制在在初期遵循指数级别的增长,而指数级别增长由于后期会增长过快,导致网络中可能一瞬间塞满大量数据引发网络拥堵,为此引入一个阈值,当网络窗口大小达到这个阈值之后就不再符合指数增长,而是遵循线性增长策略。当网络窗口大小增长到发生网络拥塞时,就会重新开始慢启动机制,并将新的阈值更新为拥塞窗口大小的1/2。
总结网络窗口动态调整的策略为:
- 前期慢增长阶段,遵循指数级别的增长。
- 当网络窗口的大小达到某一阈值后,就变为线性增长,直到发送网络拥塞。
- 发生网络拥塞就将窗口大小变回1,重新开始慢启动。
- 将阈值更新为发生网络拥塞时窗口大小的1/2。
7.8 延时应答机制
- 延时应答:收到报文不立马应答,而是等一段时间后应答。
假设主机A和主机B在进行通讯,主机B收到主机A发送过来的消息后,如果马上给主机A确认应答,那么新发过来的数据不会被上层应用取走,这样就会造成主机B接收缓冲区的剩余空间变少,将变少的剩余空间填入ACK报文的16位窗口大小处,就会降低主机A发送数据的速度。
如果在主机B收到报文后,不立即做出应答,而是等一段时间,让上层应用取走一部分数据,此时接收缓冲区就有了更多的剩余空间,这是后做出应答,对端就不会认为接收能力减弱了。
从接收报文个数N和接受后的间隔时间T来控制延时应答:
- 当接收方连续接受N个报文后,就进行确认应答。
- 收到报文后,经过时间T后应答。
N和T在不同的操作系统中有所不同,一般取N=2,T=200ms,图7.10为延时应答策略的实现原理。
7.9 捎带应答机制
- 捎带应答:在确认应答的同时携带其它数据/标志位,以减少报文发送次数的策略。
如图7.11所示,tcp协议通信在执行四次挥手断开连接时,主机B应答主机A断开连接请求的ACK报文和其自身的FIN请求可以合并为一个报文发送给对端,从而在事实上执行“三次挥手”。
八. 对listen接口的第二个参数的理解
listen函数 -- 设置监听状态,等待对端连接
函数原型:int listen(int sockfd, int backlog)
函数参数:
- socked:socket文件描述符。
- backlog:连接队列的长度。
返回值:成功返回对应的监听文件描述符,失败返回-1。
我们知道,在tcp通信时,服务器在接收客户端的连接请求是,是通过accept函数来实现的,但是accept本质上不参与三次握手建立连接的过程。
accept的功能:将连接从内核层拿到应用层进行通信。
如果不调用accept,那么连接依旧可以被建立,只是没有拿到应用层中。而listen的第二个参数,我们可以称之为全连接队列的最大长度。如果服务器收到了大量的请求,但是应用层来不及调用accept来取走连接,那么就会在内核层暂时将这些连接使用队列的形式维护起来,等待有连接关闭的时候,就可以立即accept取走内核中连接队列中维护的连接。
backlog用于指定全连接队列的最大长度,这个参数不能太大也不能太小。
用生活中的场景来理解这个参数,假设一家很火爆的餐馆在高峰期店内桌子全部被占用了,这是再有顾客希望来就餐时,店家就会在店门口准备若干桌椅,将这些客户暂时“保存维护”起来,如果店内有客人离开,就立马可以有排队的客户进店用餐,这样能够最大程度上保证店内资源的利用率。
如果“等待队列”维护的太短或者干脆不维护,那么就有可能存在店内客户离开,空桌在一段时间内没有被利用起来,造成经济损失。如果维护的太长,那么队列后面的连接就需要等待很长时间才能够被上层拿走,不符合实际需求,况且可以将“等待队列”的一部分资源用于扩充店内桌椅。
综上,backlog不能太小是为了保证资源在任何时候都能够被充分利用,不能太大是为了避免全连接队列尾部的连接等待较长时间才能够被accept取走。
在OS内核中,会维护两种关于连接的队列:
- 全连接队列:已经完成三次握手建立好的,但还没有被上层accept取走的连接。
- 半连接队列:完成了三次握手的一部分,但还没有全部完成的连接。
全连接队列的长度由listen的第二个参数决定,如果全连接队列的长度达到了最大值,那么就无法让更多的连接进入到ESTABLISHED状态,进而处于半连接状态。处于半连接状态的连接有一定的生命周期,如果在一定时间内不能完成三次握手进入ESTABLISHED状态,那么半连接就会被销毁。
九. 对于tcp一些概念的理解
9.1 面向字节流
建立起一个基于tcp协议的通信,会在操作系统内核中创建一个发送缓冲区和一个接收缓冲区,对于发送缓冲区/接受缓冲区及面向字节流的理解如下:
- 应用层调用网络IO接口,本质上并不是将数据写到网络中去,而是将数据拷贝到内核中的发送缓冲区或者从内核中的接收缓冲区拷贝数据到应用层。
- 如果一次发送的数据太长,那么就可能分多次拷贝到发送缓冲区,同理如果接收缓冲区内的数据过多,则有可能分多次拷贝到应用层。
- 在面向字节流通讯中,发送数据和接受数据不是一一对应的,而在udp协议(面向数据报)通信中,发送数据和接受数据必须一一对应。
面向字节流,就好比通过自来水管取水,我们一次拿到的水,可能是供水商分多次供给的,也可能是供水商一次供水量的一部分。面向数据报,就好比取快递,我们那N个快递,那发送方就一定发了N个快递,不能看发快递的和取快递的次数不匹配。
在tcp协议中,应用层只负责交付数据,至于怎么发送、什么时候发送,则完全由tcp协议控制,应用层完全不需要关系,用于层也无需关系数据的格式,只管向下交付数据即可。
9.2 粘包问题
什么是粘包问题:
- 由于tcp协议是面向字节流的,因此每个报文数据之间是没有明确边界的。
- 这样就可以导致在某一时刻,发送缓冲区中同时存在多组数据。
- 应用层从内核接收缓冲区读取数据时,可能将多个数据包混在一起,这就是粘包问题。
由此可见,粘包问题主要是由于数据包之间边界不明确引起的,那么要解决粘包问题,就必须要明确不同数据包之间的边界,解决粘包问题是应用层的责任:
- 如果数据包是定长的,那么每次只需要读取定长数据即可。
- 可通过特定的标识符来明确数据包之间的边界。
- 可以在数据的起始位置显示的给出数据包的长度。
9.3 tcp异常情况
- 进程终止:向对端发送FIN请求,四次挥手断开连接,与正常的断开连接没有区别。
- 计算机重启:重启关机前会发生FIN,与进程终止相同。
- 计算机断电或网络故障:接收端认为连接还在, 一旦接收端有写入操作,接收端发现连接已经不在了,发送报文请求连接重置。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。
9.4 udp实现可靠通信的方法
仿照tcp实现可靠通信的方法即可,只不过是把实现可靠通信的手段从内核层移动至应用层。
可以考虑的方法有:
- 在应用层引入序号和确认序号。
- 引入确认应答,确保不丢包。
- 引入超时重传。
- 按照应用层序列化对报文进行排序去重。
- ... ...
十. 总结
- 网络通信时基于五元组实现的:源IP、目的IP、源端口号、目的端口号、协议号。根据端口号的编号值,可将端口号划分为知名端口号和操作系统动态分配端口号。
- netstat指令可用于查看网络状态,pidof指令可用于根据进程名获取进程pid。
- udp协议全程用户数据报协议,具有无连接、不可靠、编码简单的特定;而tcp协议全程传输控制协议,具有有连接、传输可靠、编码复杂的特定。
- tcp协议建立连接需要三次握手,断开连接需要四次挥手,在建立和断开连接的过程中,通信双方的主机状态是不断在变化的,主动断开连接的一方处于TIME_WAIT状态时无法重新建立连接,而如果存在大量连接处于CLOSE_WAIT状态则说明服务端代码没有close(connfd)。通过setsockopt可以解决TIME_WAIT状态无法重新连接的问题。
- tcp通过引入:超时重传机制、确认应答机制、去重机制、按序到达机制等来保证数据传输的可靠性;通过引入:流量控制机制、快重传机制、网络拥塞控制机制、延时应答机制、捎带应答机制等来提高通信效率。其中流量控制和快速应答机制既有保证可靠通信的功能,也有提高效率的功能。
- listen的第二个参数表示底层全连接队列的最大长度,这个参数既不能太大也不能太小。
- tcp协议是面向字节流的,数据发送和接收不需要一一对应,这样就可能引发粘包问题,解决粘包问题的关键在于明确区分数据包之间的边界。tcp协议的异常情况可以分为大致分为:进程终止、计算机重启、计算机断点、网络故障等,这几种状态连接都会断开。
- udp协议可以在应用层引入保证tcp保证可靠性的机制,从而达到使用udp协议一定程度实现可靠通信的目的。