文章目录
- 1. 重新理解端口号
- 端口号划分
- netstat指令
- pidof
- 2. UDP协议
- 2.1 UDP协议端格式
- 2.2 UDP的特点
- 2.3 UDP的注意事项
- 2.4 基于UDP的应用层协议
- 3. TCP协议(传输控制协议)
- 3.1 TCP协议的格式和报头字段
- 3.2 如何解包和分用
- 3.3 理解TCP协议报头
- 3.4 TCP协议的可靠性
- 3.4.1 可靠性和不可靠性的理解
- 3.4.2 保证可靠性的机制——确认应答机制
- 3.4.3保证可靠性的机制——超时重传机制
- 3.4.4保证可靠性的机制——连接管理机制&&TCP连接创建和销毁过程
- 3.4.5 保证可靠性的机制——滑动窗口
- 3.5 TCP协议的高效性
- 3.5.1体现高效性的机制——流量控制
- 3.5.2 体现高效性的机制——拥塞控制
- 3.5.3 体现高效性的机制——捎带应答
- 3.5.4 体现高效性的机制——延迟应答
- 3.6 TCP面向字节流
- 3.6.1 理解TCP面向字节流
- 3.6.2 面向字节流导致的问题——粘包问题
- 3.7 TCP异情况分析
- 3.8 TCP中listen第二个参数的理解
- 3.9 TCP小结
写在前面:在之前的学习中,我们从socket编写到http协议,搞懂了网络协议栈中应用层要做的事情。在网络协议栈四层模型中,传输层和网络层都是属于OS kennel 的,所以这就决定了应用层和传输层之前是会存在系统调用的,我们之前在手写socket编程的时候,使用的read/write/recv/send/recvfrom/sendto…都是系统调用。接着再往下,本片文章将会讲解传输层的协议UDP/TCP,借助这两个传输层协议来理解传输层做的事情
1. 重新理解端口号
端口号是用来标识一个主机上通信的不同应用程序的。在TCP/IP协议中,用“源IP”,“源端口号”,“目的IP”,“目的端口号”,“协议号”这样的一个五元组来标识一个通信(可以通过netstat n查看)
这样就能够标识网络中的唯二的两个进程进行通信。
端口号划分
端口号是一个16位的无符号整型,取值范围为0-65535
,其中有一些端口号是固定的知名端口号
-
0-1023:知名端口号,HTTP,FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的
ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443
-
1024-65535:OS动态分配的端口号,客户端进程的端口号就是由OS从这个范围内分配的
执行下面的命令就可以看到知名的端口号
cat /etc/services
我们自己写的程序在使用端口号的时候要避开这些端口号
一个进程是否可以bind多个端口号,一个端口号是否可以被多个进程bind
一个进程可以bind多个端口号,一个端口号只能被一个进程bind,因为端口号和进程bind的目的是为了通过端口号找到唯一进程,所以需要 端口号–>进程 方向一定是唯一的,反过来没有这个需求
netstat指令
net指令是一个用来查看网络状态的重要工具
语法:netstat [选项]
功能:查看网络状态
常用选项:
- n 拒绝显示别名,能显示数字的全部转化为数字
- l 只列出在Listen状态的服务状态
- p 显示建立连接的相关进程(process)
- t(tcp) 仅显示tcp相关的
- u(udp) 仅显示udp相关的
- a(all) 显示所有选项的,默认不显示LISTEN相关
pidof
查看服务进程对应的pid
语法:pidof [进程名]
功能:通过进程名,查看进程id
2. UDP协议
2.1 UDP协议端格式
- 这里16位UDP长度,标识的是整个数据报(UDP首部+UDP数据)的最打长度
- 如果校验和出错,数据就会直接丢弃
理解Linux下协议报头
我们知道协议报头本质上是一种结构化数据,传输层协议是属于Linux内核的,Linux内核是C语言实现的==>协议报头在内核中就是一个C语言结构体
struct udp_hdr{
uint16_t src_port;
uint16_t dst_port;
uint16_t udp_len;
uint16_t check;
};
最终组织起来的报文就是协议报头+数据
2.2 UDP的特点
UDP的传输过程类似于寄信:
- 无连接:知道对方的IP和端口号就能直接进行传输,不需要建立连接(寄信的时候知道地址即可)
- 不可靠:没有确认机制,没有重传机制,如果因为网络故障导致该段无法发送到对方,UDP协议层也不会给应用层返回任何错误信息
- 面向数据报:不能够灵活的控制读写数据的次数和数量
面向数据报的理解
应用层交给UDP多长的报文,UDP原样发送,既不会拆分也不会合并
举个例子:如果发送端调用一次sendto,发送100字节的数据,那么接收端也必须调用一次对应的recvfrom,接收100个字节,不能循环调用10次recvfrom,每次接收10个字节
UDP的缓冲区
- UDP没有真正意义上的发送缓冲区,调用sendto会直接将数据交给kennel,由kennel将数据传输给网络层协议后进行后续的传输动作
- UDP具有接收缓冲区,但是这个接收缓冲区并不能保证收到的UDP报的顺序和发送的顺序一致(如果缓冲区满了,再到达的数据就会被丢弃)
UDP的socket既能读也能写,这个概念叫做全双工
2.3 UDP的注意事项
在上文中,我们看到UDP的协议格式里面UDP长度是使用16位的无符号整型表示的,这就意味着一个UDP传输的数据最大长度就是64K(包括UDP首部)。如果需要传输的数据超出这个带下,就需要在应用层下手动的分包进行多次发送,同时在接收端手动拼装
2.4 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
3. TCP协议(传输控制协议)
3.1 TCP协议的格式和报头字段
- 16位源端口号:用来标识发送此报文的源的端口号
- 16位目的端口号:含义同上
- 32位序号:每个报文都会携带一个序号,标识当前报文
- 32位确定序号:表示对方收到的最后一条连续报文的下一个序号
- 4位首部长度:表示协议报头的长度(单位:4字节)
- 保留6位:我们不考虑
- 6个标识位:是TCP协议报文的属性标识,包括
- URG:标识紧急指针,为1时紧急指针才有效
- ACK:表示当前报文包含应答内容
- PSH:催促接收方上层尽快取走接收缓冲区的数据
- RST:复位,强制关闭一个异常连接
- SYN:请求建立一个连接
- FIN:请求断开一个连接
- 16位窗口大小:是报文发送方的接收缓冲区剩余空间大小
- 16位检验和:判断当前报文的有效性(通过将报文的某些字段的内容经过某种算法得到的一个结果,可以类比于数据摘要理解)
- 16位紧急指针:标识在数据中的紧急内容的首个字节的偏移量
- 选项:这里会包含一些可选信息和功能扩展,例如:最大报文长度、扩大窗口因子等
3.2 如何解包和分用
我们知道,要让一个协议能够被使用,那么最关键的就是:数据如何进行解包和分用
1. 如何解包
TCP协议的报头是有一个标准长度的:20字节。这20字节的内容包括:16位源端口号,16位目的端口号,32位序号,32位确认序号,首部长度+保留6位+6位标志位,16位窗口大小,16位检验和,16位紧急指针
所以TCP的解包过程就是
- 首先拿到标准长度的20字节,访问首部长度的4个位
- 对应的值*4得到数据报头的大小[20,60],通过首部长度拿到报头长度
- 通过报头长度确认选项内容,读取选项,然后在对应的时候处理选项
- 剔除所有报头数据,剩下的就是有效载荷数据
2. 如何分用
我们收到一个报文,是如何给到对应进程的?
在之前的理解中我们知道是通过端口号找到对应进程的(理解端口号),其实也就是在网络通信开始之前初始化socket的时候给指定进程bind端口号的
-
那么kennel是怎么通过port快速定位到指定进程的呢?
我们在Linux系统编程的时候接触到进程,OS kennel管理进程是先描述,再组织。描述使用的就是PCB,在Linux下也就是struct task_struct描述的,里面存放了进程的相关信息,组织使用的是一个双向链表给所有的PCB串起来的。
实际上OS kennel组织PCB并不只是简单的使用了一个双链表组织PCB的,还有很多其他的结构来管理(推测应该是管理PCB指针)
OS kennel在内部维护了一个hash表,使用port作为hash key,里面存放对应的PCB指针,通过协议报头的目的端口号找到hash表中对应的位置,拿到对应的PCB指针,就能够找到指定PCB了
在PCB中有一个文件结构(files_struct)指针,里面有一个文件描述符表(fd_array),其中就对应了socket的sockfd,能够找到对应的文件结构体(Linux下一切皆文件),将传输层的数据读到(这里的读取过程比较复杂,因为OS内部对socket做了很多的封装,这里不过多解释)对应的文件缓冲区。
3.3 理解TCP协议报头
TCP的协议报头和UDP的协议报头本质上是同样的,都是一个内核结构体(结构化数据)
struct tcp_hdr
{
uint32_t src_port:16; // 位段结构
uint32_t dst_port:16;
uint32_t seq;
uint32_t check_seq;
uint32_t header_length:4;
// ...
}
在通信的时候把有效载荷和协议报头组织起来丢给网络层传输即可。
3.4 TCP协议的可靠性
3.4.1 可靠性和不可靠性的理解
在最开始了解TCP的时候,我们就说过可靠性和不可靠性在这里是一个中性词,是一个特性的描述,并没有褒义和贬义之分。因为如果要保证可靠性就要做更多的工作,这样的话效率就会降低。
不可靠性的体现:
网络传输的距离很长,在这个传输的过程中,就有可能出现顺序错误、丢包、重复传输、校验错误等情况
那么我们说TCP协议是可靠的,它是怎么保证可靠性的呢?通过几个机制来保证
3.4.2 保证可靠性的机制——确认应答机制
如果什么时候能保证别人听到我们说话,当对方给我们一个响应的反馈的时候就能确认对方收到了上一条消息,所以TCP为了保证可靠性就引入了确认应答机制
所谓的确认应答机制,就是指当我们收到对方的报文的时候,检测报文的内容,就能确定对方收到了那些报文。
那么具体是怎么做的呢?我们已经知道TCP协议中存在序号和确认序号。
当我们收到一条TCP报文的时候,会检查报头里面的确认序号X
,这个确认序号X
的含义是**X序号之前的所有数据我都已经收到啦(包括X-1),下一次你发报文的时候从X位置开始发**
3.4.3保证可靠性的机制——超时重传机制
网络传输不稳定的很大因素就是会有丢包问题的情况(数据包在传输的过程中出现丢失),那么相应的TCP肯定给出了一些解决方案来解决这个问题,TCP为了避免丢包问题给出的解决方案就是超时重传机制
所谓的超时重传机制就是:在确认应答机制的前提下,如果在规定的时间内没有收到对方的确认应答报文,就会重新发送报文
实际上,没有收到对方的应答报文ACK的情况可能有两种
1. 情况1:在发送报文的时候丢包,对方没收到报文
- 对于这种情况,我们一定需要重新发送报文
2. 情况2:在对方收到报文并发送ACK报文的时候出现丢包,我方没收到报文
-
对于这种情况,我们采取的措施也是重新发送报文,但是会出现发送报文重复的问题,这个问题怎么解决?毕竟收到重复数据也是不可靠的表现
这个时候TCP给出的解决方案就是接收方会按照收到报文的序号进行去重
超时时间怎么确定
由于数据传输和当时的网络环境有关,所以重传时间不能设定为固定值,而是由操作系统动态的调整重传时间。
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。(我们PC上的QQ就是这种策略)
为什么在TCP协议报头中有两组序号
TCP是全双工的,对于同一个套接字,能读也能写,所以发送的报文有可能是我放的内容(携带序号),也有可能是对方的应答报文(携带确认序号)
3.4.4保证可靠性的机制——连接管理机制&&TCP连接创建和销毁过程
我们知道,TCP是建立在连接的基础上的,不像UDP在传输的时候不用创建和管理连接。
正常情况下TCP连接的建立需要三次握手保证连接建立成功,需要四次挥手保证连接断开成功
当对方处于listen状态(经过了分配文件描述符,bind address和port,调用listen)之后,就会阻塞的等待别人的连接。此时我们认为客户端处于CLOSED状态(这是一个虚拟的状态,标识没有连接),服务端处于LISTEN状态。此时如果要建立连接就要进行三次握手
1. 三次握手的过程
- 客户端发送一条报文,把SYN对应标志位置为1,表示想建立一个连接,发送后客户端处于SYN_SENT状态
- 服务端收到这条报文之后表示“我知道你想建立连接了,我同意你建立连接”,此时会发送一条报文,表示收到(应答报文)同时有一个建立连接请求的标识位(把SYN和ACK置为1),然后把自己的状态设置为已经收到连接建立请求的状态SYN_RECV
- 客户端收到服务端的应答报文之后才能确认其收到了之前发的连接建立申请报文,然后客户端认为连接已经建立成功,状态变成ESTABLISHED,然后发送一条ACK报文,表示收到并同意客户端的连接建立请求
- 服务端收到客户端发送的ACK报文,此时服务端才能确认客户端收到了服务端上一次发送的连接建立请求,此时服务端才认为自己的连接建立成功
2. 四次挥手的过程
- 当一方先决定断开连接之后,会向另一方发送一条连接断开的申请报文(FIN置为1),然后自己的状态变成FIN_WAIT_1
- 对方收到FIN的报文之后把自己的状态设置为CLOSE_WAIT状态,然后向这一方发送一个ACK报文,当这一方收到之后能够确定对方收到了断开连接请求,然后就把状态设置为FIN_WAIT_2状态
- 由于TCP是全双工的,所以对方也要断开连接,所以也会向我方发送一条连接断开的申请(FIN),然后把自己的状态设置成LAST_ACK状态
- 我方收到这个FIN的报文之后确定了对方也要关闭连接,然后就把自己的状态置为TIME_WAIT,然后向对方发送ACK报文,在一定时间内没有收到这个连接发送的新报文,就会认定连接断开已经成功,然后就恢复到CLOSED状态
在讲解下面的内容之前,我们先确定两个共识:
1. 三次握手和四次挥手不一定非得成功,由于确认应答机制,我们只担心最后一条报文的丢失,但是对于这种丢失TCP给出了配套的解决方案
2. 建立的连接是需要被OS管理起来的,管理就需要“先描述,再组织”,维护是需要时空成本的
1. 为什么非得三次握手,其他次数不行吗?
TCP是传输层的,属于OS内核,如果一次握手就认为连接建立成功的话,那么一个客户端可以向服务端发送很多个建立连接的请求,这样就有可能引发SYN洪水攻击,同时也不能验证服务端的发送能力
如果两次握手的话,同样也有引发SYN洪水攻击的可能性
TCP是全双工的通信策略,采用三次握手是使用最少的通信成本,确认TCP通信双方都能够正常进行收发。同时采用三次握手当最后一次客户端发送ACK报文的时候才认为连接建立成功,这样客户端和服务端在建立一条连接的消耗是同等的,可以有效的规避单主机的SYN洪水攻击(当然TCP协议本身并不是用来规避攻击的,TCP协议的主要任务是用来进行网络通信)
如果采用其他更多次数的握手,需要的成本就变高了,没有意义,同时如果采用其他偶数次也有SYN洪水攻击的可能性。
2. 为什么非得四次挥手,其他次数不行吗?
TCP是全双工的通信策略,所以断开是需要双方同意的。4次挥手是成本最低的确认双方都断开连接的方式。是稳定的
-
TIME_WAIT状态
在第一次发起连接断开的一方接收到对方的连接断开请求之后,状态会变成TIME_WAIT状态,并持续一段时间然后才变成关闭状态,这是因为己方不确定对方已经收到了对应的ACK应答,如果等待一段时间之后没有其他状况产生,那么就认为连接断开成功,才能够变成关闭状态。如果对方没有收到ACK应答的话,在对方触发了超时重传机制后,己方会再次收到FIN报文
若服务器先关闭,一段时间内重启服务器将不能再绑定上一次绑定的端口,必须等上一次服务器状态变为CLOSED后,那个端口才可以重新用于连接。对于访问量大的服务器,如果挂了,那么大量的端口在一段时间内无法再次被绑定,所以这是不合理的。使用setsockopt()
设置允许地址重用,也就是允许创建端口号相同但IP地址不同的socket描述符。
3.4.5 保证可靠性的机制——滑动窗口
在上文中,我们说到TCP为了保证报文被对方收到,就采用了确认应答机制,当收到对方的ACK报文才认为我们发的报文被对方收到了,但是这样一条一条的发送实在是有些慢了。那么能不能连续发送很多条报文,维护一个等待队列,当收到队列中对应报文的ACK报文,就认为这个报文已经被收到,然后把原报文从队列中移除,当然是可以的,TCP就采用了这种策略,把这个等待队列叫做滑动窗口
滑动窗口也能够体现出TCP的高效性,因为把很多条报文的等待应答时间放在一起了
1. 滑动窗口的本质
滑动窗口本质上是在发送缓冲区的一段空间,滑动窗口的定义就是已经发送但是没有收到对应的ACK报文的内容
滑动窗口移动的本质就是下标的改变(OS内部维护了一个环形队列)
2. 滑动窗口的作用
- 流量控制:TCP滑动窗口机制允许接收方控制发送方的发送速率,以确保接收方能够在处理能力范围内接收和处理数据。接收方通过调整滑动窗口的大小来告知发送方,它还能够根据接收方的处理能力动态地调整滑动窗口的大小。滑动窗口一定程度上提高了TCP对数据的传输效率。
- 拥塞控制:TCP滑动窗口机制能 够帮助控制网络中的拥塞情况。当网络发生拥塞时,接收方可以减小滑动窗口的大小以减缓数据的发送速率,从而避免对网络造成进一步的负载压力。通过动态调整滑动窗口的大小,TCP能够根据网络的拥塞程度自适应地控制数据的传输速率。
- 可靠性传输:TCP滑动窗口机制提供了可靠的数据传输。发送方只有在收到接收方确认的数据后才会发送下一个滑动窗口的数据,接收方同时也会对接收到的数据进行确认。如果发送方没有收到确认或者接收方收到失序的数据,发送方可以通过滑动窗口机制选择性重传这些数据,以确保数据的可靠传输。
3. 一些问题
-
窗口的大小是怎么规定的,未来怎么变化
滑动窗口的大小取决于对方的接收能力(对方的接收缓冲区剩余空间大小,我们通过收到的报文中的16位窗口大小来确定,在建立连接的三次握手期间交换初始的大小),未来会变化,未来的变化取决于对方的窗口大小和网络拥塞程度
-
窗口一定会向右滑动吗,会不会向左滑动
当收到对应的ACK报文的时候,会向右滑动,表示当前报文已经被确认收到了,当没有收到报文的时候,会不动,但是不会向左滑动
-
窗口的大小会一直不变吗,如果变,未来怎么变化,依据是什么
不会,窗口大小是会变的。窗口大小取决于最近收到的报文中的16位窗口大小和网络拥塞程度
-
收到ACK报文的时候,如果收到的不是最左侧的报文的确认信息,而是中间报文的确认信息怎么办,滑动吗?
我们之前说过在TCP报头中有一个确认序号的32位数据,这个数据表示的含义就是比这个数小的所有序号的报文都收到了,下一次从这个序号的内容开始发送,所以收到任意序号都可以认为之前的报文都收到了,所以会滑动到对应的位置
-
滑动窗口一定要滑动吗,会不会不动了或者大小变为0?
当没有收到对应的ACK报文的时候,窗口不会滑动。当收到报文的16位窗口大小为0 的时候就表示对方无法再次接收型报文了,此时窗口大小就是0
-
会一直滑动吗,如果空间不够了怎么办
窗口是需要一直滑动的,所以被设计成了环形队列的数据结构
3.5 TCP协议的高效性
3.5.1体现高效性的机制——流量控制
我们知道接收端处理数据的速度是有限的,如果发送端发送的太快,那么就会导致接收缓冲区被填满,这个时候如果继续发送,就会出现丢包重传等一系列问题,因此TCP根据接收端的处理能力来决定发送端的发送速度,这个机制就叫做流量控制
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
3.5.2 体现高效性的机制——拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍
然可能引发问题.因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
这里引入一个概念:拥塞窗口
从发送开始的时候,定义拥塞窗口的大小为1,每次收到一个ACK应答, 拥塞窗口进行指数增长(每次乘2),每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。但是拥塞窗口的大小的慢启动阈值(Slow Start Threshold)的时候,就开始进行线性增长,直到发现网络拥塞(发现产生大量的丢包),此时更新慢启动阈值为拥塞时的拥塞窗口大小的一般,同时重置拥塞窗口大小为1,再次进行指数级增长,然后周而复始
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
当发生少量丢包时,触发超时重传机制,但是发生大量丢包的时候,就认为发生了网络拥塞
最终产生的现象就是TCP在刚启动的时候传输吞吐量比较低,然后逐渐上升(快速),发生网络拥堵之后,吞吐量立刻下降,目的时为了尽快的把数据传输给对方,但是又要避免给网络造成太大压力,最终采用的折中方案。
3.5.3 体现高效性的机制——捎带应答
我们知道,在TCP协议中,只有收到这条报文对应的ACK报文的时候,才认为这个报文已经被对方收到了,也就是“一发一收”对应着一次传输,但是在客户端给服务端发送报文的时候,服务端也是希望给客户端发送报文的,所以可以把这个ACK消息和对应的服务端报文结合在一起进行发送,就能够减少通信成本,提高通信效率。这种把ACK报文和数据报文结合在一起进行发送的方式就叫做捎带应答
3.5.4 体现高效性的机制——延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小,因为数据还没有来得及被处理,如果再稍微等待一点点时间,上层有可能就会拿出去一部分数据处理,接收缓冲区的空闲位置变大,窗口就变大了。窗口越大,网络吞吐量越大,传输效率就越高,我们最终想要保证的就是在网络不拥塞的前提下,尽量的提高传输效率
那么所有的包都可以延迟应答么? 肯定也不是
- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次
体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
3.6 TCP面向字节流
3.6.1 理解TCP面向字节流
在我们之前写的TCP协议的socket编程的时候,我们使用write和read进行通信,实际上调用write的时候,只是把应用层的数据拷贝到TCP发送缓冲区中,调用read的时候,就是把数据从接收缓冲区中拷贝出来。并没有进行真正的发送,真正的发送数据是由传输层的TCP协议控制的,所以什么时候开始发送,发送什么内容都是由TCP自己决定的,TCP协议的读和写并需要匹配,上层给多少或者想要多少以及如何将这些字节组合成一个个报文的,TCP根本不关心,想要组合成报文,应用层自己想办法,这种现象就是TCP面向字节流。
和TCP面向字节流相对应的是UDP协议面向数据报,每次发送的都是一个数据报,如果一次性发送了100个字节,那么接收就要与之相对应一次接收100个字节,但是TCP如果一次性发送100个字节,可以接收10次,每次10个字节。
3.6.2 面向字节流导致的问题——粘包问题
由于TCP是面向字节流的,所以在应用层需要控制将不同的报文给分开,否则就会造成粘包问题,也就是报文分不开
要解决粘包问题,本质就是将报文和报文之间分开
1、定长读取策略:对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
2、报头约定包长策略:对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
3、特殊字符分隔策略:对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)
http协议使用的就是特殊字符分隔策略(报头和报文之间有一个
\r\n
)
3.7 TCP异情况分析
1. 进程终止
进程在终止的时候会释放文件描述符,仍然会发送FIN,和正常关闭没有区别
2. 机器重启
机器在重启的时候,会首先终止各个进程,然后再终止操作系统,和进程终止一样
3. 机器掉电/网线断开
被掉电的一端根本来不及给对端发消息就寄了,对端会定期发起询问,多次无应答后将会断开连接。(对于连接的管理机制,大都是应用层来管理的(HTTP、HTTPS等)应用层的某些协议, 也有一些这样的检测机制,例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ在断线之后, 也会定期尝试重新发起TCP三次握手进行连接)
3.8 TCP中listen第二个参数的理解
在创建TCP连接的时候,服务端需要调用listen,将对应的sockfd设置为监听状态,此时需要传入第二个参数backlog,这是因为TCP协议要为上层维护一个全连接队列,队列里面是已经完成三次握手的待连接对象,这个队列不能没有,它的存在就好比去外面吃饭,需要排队的场景,上一个顾客走了,在排队的用户马上可以接上来,大大提高TCP的速度,当然这个队列也不能太长,因为队伍太长你还愿意等吗,TCP也一样,队列太长没必要,因为维护这个队列是需要开销的。这个全连接队列受listen的第二个参数的影响。
int listen(int sockfd, int backlog);
如果listen的第二个参数设置为2,在第四次建立连接的时候(客户端36648),可以看到客户端认为自己建立连接成功了,但是在服务器端,连接建立的状态是SYN_RECV。(客户端发起建立连接的请求,服务器收到后处于SYN_RECV状态并向客户端发送SYN+ACK应答,使客户端建立成功连接,但是客户端发起第三次握手的时候会无视该客户端的ACK,说话以服务器并不会建立连接,仍处于SYN_RECV的状态称为半连接状态)
tcp底层最多允许backlog+1个全连接,后续来的全部都是半连接。(半连接如果没有尽快完成握手,会被server自动关掉)所以我们一般不会把这个参数设置的特别大
半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
3.9 TCP小结
TCP的可靠性和高效性的实现原理:
可靠性:
校验和、序列号(按序到达)、确认应答、超时重传、连接管理、流量控制、拥塞控制
提高性能:
滑动窗口、快速重传、延迟应答、捎带应答、流量控制、拥塞控制
使用TCP的应用层协议
使用TCP通信的应用层协议有:HTTP,HTTPS,SSH,Telnet,FTP,SMTP等等,当然自定义的应用层协议也可以是
本节完…