文章目录
- 1、再谈端口号
- 2、UDP协议
- 3、TCP协议
- 3.1 TCP协议段格式
- 3.2 TCP的三次握手和四次挥手(连接管理机制)
- 3.3 TCP的滑动窗口
- 3.4 TCP的流量控制
- 3.5 拥塞控制
- 3.6 延迟应答和捎带应答
- 3.7 面向字节流和粘包问题
- 3.8 TCP总结
1、再谈端口号
端口号port标识一个主机上的不同应用程序。
比如常见的服务器端口:
- HTTP服务器端口号是80
- HTTPS服务器端口号是443
- FTP服务器端口号是21
- SSH服务器端口是22
端口号范围划分
0 - 1023:是一些知名端口号,HTTP,FTP,SSH等这些应用层协议,他们的端口号是固定的。
1024-65532:操作系统动态分配的端口号。客户端程序的端口号就是这个范围里分配的。
通过下面的命令,可以看到所有知名端口号
cat /etc/services
值得注意的是
一个端口只能绑定一个进程,而一个进程可以绑定多个端口。
netstat
netstat是一个用来查看网络状态的重要工具
常用选项:
- n 拒绝显示别名,能显示数字的全部转换成数字
- l 仅列出在Listen(监听)状态的服务
- p 显示建立相关链接的程序名
- t 仅显示tcp相关选项
- u 仅显示udp相关选项
- a 显示所有选项,默认不显示LISTEN相关
pidof
在查看服务器进程id时非常方便。
通过进程名,查看进程id
pidof [进程名]
2、UDP协议
UDP协议端格式
UDP相关字段
16位的源端口号和目的端口号,保证了数据包的分发。源端口号表明了数据从哪个应用层进程来,到哪个应用层进程去。
16位UDP长度,表示整个数据报(报头+有效载荷)的最大长度。(UDP报文不是流式的,是有边界的,因此可能出现两个报文互相干扰,通过限定数据报大小就能很好解决这个问题)
16位UDP检验和,如果校验和出错,就会直接丢弃。
//报头结构可以理解成一堆字段
struct udp_hdr
{
size_t src_port:16
size_t dst_port:16
size_t udp_len:16
size_t udp_check:16
}
//添加报头本质就是在数据前面拷贝一个结构体对象
UDP的封装、解包和分用
UDP报文是定长的,传输层封装就直接在原来的应用层数据上通过指针添加一段结构体对象,另一端在解包时就直接通过定长的8字节找到报文大小,在通过报文减去8字节确定有效载荷数据完成解包。由于报头中有确定的目的端口,因此也能确定特定的应用层协议进程。
UDP的特点
- 无连接: 知道对方的IP和端口号就直接传输,不要建立连接。
- 不可靠: 没有确认机制,没有重传机制;如果无法发给对方,也不会有任何错误信息。
- 面向数据报:不能够灵活的控制读写数据的次数和数量。
无连接和不可靠不能看成是UDP的缺点而是特点,建立连接和保持可靠是需要代价的,在某些场景下恰恰无连接和不可靠才是好的。
其中面向数据报,应用层交给UDP多次的报文,UDP原样发送,既不拆分也不合并。
(比如发送端一次sendto发100个字节,接收端也一定是recvfrom,接收100个字节)
udp缓冲区
- udp没有真正的发送缓冲区(因为数据简单,并且不用维持连接和可靠性,不用暂存起来),调用sendto会交给内核,由内核将数据传给网络层协议进行后续的传输。
- udp具有接收缓冲区,因为udp不保证可靠性,所以这个接收缓冲区不能保证收到udp报顺序和发送udp顺序报顺序一样,并且如果缓冲区满了,再次到达的udp数据会被丢弃。
实际上,常用的write这类IO接口,实际上不是将数据写入文件,而是将数据拷贝至系统缓冲区,因为只有系统才了解硬件的情况,因此拷贝到系统缓冲区的数据什么时候发、怎么发都由OS决定。网络中的sendto也是如此。
这类udp的socket既能读也能写,这个概念叫全双工。
可以注意到,16位udp长最大也就64K,很小,如果传输的数据超过64K,就需要应用层手动的分包,多次发送,并在接收端手动拼装。
udp因为其不需要维护其连接和可靠性,在一些链接压力很大并且只需要进行传输数据的场景就非常合适。比如直播
3、TCP协议
tcp传输控制协议的特点是有连接和可靠性,因此谈论tcp协议往往需要讨论其可靠性和效率问题。
3.1 TCP协议段格式
- 16位源端口号和16位目的端口号和UDP一样,源端口号表明了数据从哪个应用层软件来,到哪个应用层软件去。
- 4为首部长度表示TCP报头有多少个4字节。因此如果显示是0101,那么报头大小为5*4 = 20字节。因此报头的范围为20-60.
TCP的封装、解包和分用:
封装:TCP的数据报大小是动态的,其报头也是通过字段组成。因此在封装的时候,也是在应用层数据的基础上添加一个结构体对象。
解包:在解包的时候通过4位首部长度*4减去20字节标准报头就可以确定还有多少剩余报头,提取完报头剩下的就是有效载荷。对于有效载荷的边界问题,后面再详细讨论。
分用:分用一样是通过目的端口号确定特定的应用层协议进程。
TCP可靠性问题:
- 什么是不可靠? 丢包、乱序、数据包检验失败
- 怎么确认一个报文是丢了还是没有丢? 如果收到应答就确认没丢,没有就不确定。
TCP不提供有效载荷大小的字段,而是采用一种确认应对机制,确保数据传输可靠。
那么如果面对多种请求,如何确定哪些是确认收到的呢?
对于发送的多个信息,只有收到的信息是和发送有对应关系的,才能确定发出去的数据是可靠的。
因此,TCP报头中有两个重要的字段:32位序号和32位确认序号。
其中序号标识传输的当前报文,确认序号代表确认接收到了当前所有之前序号的报文。因此序号和确认序号保证了两端通信数据的可靠性。
那为什么一个报文中要同时存在序号和确认序号两个字段呢?
因为tcp是全双工的,当server在确认回应的同时也可能向Client发送数据,因此就需要一个报文设置两份序号。
为了保证序号安全,序号生成不能有规律,即序号生成是随机生成的,并且与报文有关。溢出也会有方法进行回绕。
TCP发送缓冲区和接收缓冲区的关系
tcp协议传输需要保证其可靠性,因此在发送的时候需要有发送缓冲区(比如为了超时重传进行备份数据),另一端也需要有接收缓冲区暂存还未被应用层提取的数据。
实际上用户层上会有自己定义的缓冲区,并且IO类函数本质都是拷贝函数,将用户数据拷贝到内核,再由内核拷贝给用户,因为OS比用户更清楚底层的情况。
因此,数据怎么发,出错了怎么办,要不要添加提高效率的策略,这些都是由OS中TCP自主决定的,所以TCP对数据是具有传输和控制能力的,也就被称为传输控制协议。
其实在文件操作中,虽然用户拷贝到内核缓冲区的数据是由OS决定何时何样写到文件中,但也提供一些接口如fsync刷新缓冲区接口给用户带来确定性。网络更需要这样,不然在用户发送后,OS经过一些决定后才算发送成功,那么效率就太慢了。
TCP协议中有自己的发送缓冲区和接收缓冲区,因此一端在接收数据的时候也能向对方发数据。全双工的通信特点也因此。
16位窗口大小的应用
如果当client向server发消息速度过快,导致server缓冲区满了,那么后续的数据接收不了该怎么办呢?
让client重传这个做法显然是不太行的,因为client传递的数据是需要成本的,如果在到达后才知道server接收不了数据,这很影响效率。
有没有一种能提前知道对方接受能力的策略?报头中的窗口大小字段很好的解决。
client给server发消息后,server的应答报文中是会有窗口大小字段的。这个窗口大小就是接收缓冲区的剩余接收容量。
双方都可以通过传输报文的同时携带自身的窗口大小,让对方知道自己接收缓冲区的剩余接收容量,这一双向的行为也称流量控制。
6位标志位
两端进行TCP协议的整个通信过程中,有相互建立链接的过程,有通信的过程,有断开链接的过程。在这些过程中,发送的报文也有很多的类别,需要用不同逻辑区分这些报文。
- SYN:请求建立连接,把携带SYN标识的称为同步报文段。只要报文是建立链接请求,标识为1,证明这一次是建立连接的请求。
- FIN:通知对方,本端要关闭了。携带这种标识的称为结束报文段。
- ACK:确认标记位,标识该报文是对历史报文的确认。(发送的确认报文不仅仅可以是确认,也可以边确认边发消息)
- PSH:提示接收端应用立刻从TCP接收缓冲区内把数据读走。(可以理解为催促报文,一种就绪数据通知策略)
- URG:紧急指针标记位。TCP为了保证其数据可靠性,通过在缓冲区将序号进行排序,让发报文和收报文的顺序是一样的。但如果有一些数据优先级更高,但序号较晚,因此有优先被要求的需求。通过URG标记,让报文是优先被读的类型。(之后通过16位紧急指针在数据中获得特定的偏移量位置的数据,并且该数据只有1个字节。在一些极端情况下,常用于查看机器的状态)
- RST:对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。(如果Client回应ACK后Server没有收到,此时Client以为建立成功就直接开始通信,而Server看到没有建立链接就发信息,于是发一个RST的报文,要求让对方重新建立链接。)
3.2 TCP的三次握手和四次挥手(连接管理机制)
TCP的三次握手
tcp是面向连接的,如果多个客户端与TCP服务器建立链接,OS就需要管理这些链接,就需要管理维护这些链接的结构体。(花时间、花空间)
为什么是三次握手?
- 一次行不行?SYN一次不一定成功,并且很容易收到攻击(构造假的SYN请求,引发SYN洪水,使得服务端一直建立维护链接的结构体,不断消耗其资源)
- 两次行不行?SYN+SYN+ACK,server还是认为一次SYN就成功了,其实和一次是一样的。Client只要忽略你发的请求,就还是可以和一次的SYN攻击一样的。
- 三次?三次让客户端先建立维护链接的结构体,再回复给server。这样客户端就要和服务端受到同等代价,而服务器资源肯定是要比单主机资源多的。同时三次的ACK失败不影响server,而是影响client。(奇数次链接成本能够嫁接到客户端)
三次握手也用最小成本验证了全双工。
TCP的四次挥手
TCP是全双工的,通信双方地位是共有的,你关闭对应链接,我也要关闭对应链接。
客户端主动关闭连接,向服务端发送FIN结束报文,服务端返回确认报文段进入CLOSE_WAIT,处理完之前数据就close关闭连接,向客户端发送FIN结束报文,等待最后一个ACK到来后,就彻底关闭连接,客户端等待后,进入CLOSED。
进入CLOSE_WAIT状态的服务器此时因为没有close对应socket而占用资源。因此可能会有内存泄漏危险。
因为TCP是全双工的,在特殊情况下Server方的ACK和FIN可能会以一个报文的形式发送。
一个现象:为什么服务器关闭,客户端再关闭,客户端还要进入TIME_WAIT状态?
因为最后的ACK回应报文可能是丢失的,如果Client完成四次挥手直接进入CLOSED状态,可能Server还没有完成四次挥手最后以异常情况关闭链接,这有问题。因此,Client收到FIN后向Server发送ACK,如果之后没有发生Server因为超时重传FIN的情况,Client就正常退出。
那么这个TIME_WAIT等待的时长是多少呢?
Client->Server,Server->Client一来一回时长是要浮动的,其中最长的被称为TCP最大生存时间MSL,而TIME_WAIT等待时间如果设定为2*MSL,就能保证完成一次一来一回。
不过一般MSL都是被系统设置好了,这是为了保证两个传输方向上不收到尚未被接收或迟到的报文段。
[yzh@VM-4-8-centos ~]$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
在极端情况下
TIME_WAIT意义从宏观上来看,Client可能在传Data同时传了FIN,当FIN先到达时,TIME_WAIT就能很好保证剩余数据也能被处理。
不过在一些场景下,需要服务器及时启动,让其它不同IP或端口的客户端能够bind成功随后链接。
//通过setsockopt设置的socket,level一般设置为SOL_SOCKET代表socket层,选项optname有SO_REUSEADDR和SO_REUSEPORT,分别代表能以不同的IP和端口进行绑定链接。opt代表设置新值的缓冲区。
#include <sys/socket.h>
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3.3 TCP的滑动窗口
TCP的Listen和Accept
#include <sys/socket.h>
//socket是套接字。
//如果backlog上层不进行accept,底层建立好的链接数就是有上限的,上限是backlog+1。
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
在TCP三次握手之前,listen是一个等待链接的接口,在调用accept之前,建立链接后,会在底层建立一个链接好的队列。里面都是全链接(ESTABLISHED)状态的结构体。此时已经建立好链接。(在链接数超过backlog+1后,再次建立的链接就会处于SYN_RCVD半链接状态)
accept不参与三次握手,但底层会维护一个链接队列,accept将底层链接拿到上层,给用户看到。
重点是为什么要有这个队列呢?
1、为什么要排队?
可以让我们Server在有闲置的情况下,从底层拿去链接,进行链接处理。
2、为什么不能太长?
太长影响客户体验,太长过于占用资源,反而可能导致服务器效率低下。(与其增加更多的排队资源,不如腾出更多的资源给客户使用)
TCP的确认应答机制
之前的确认应答机制,主机间的通信发一次给一次应答,整个过程是串型的。这种做法效率很低。TCP采用它的思想,但不按照这样做。
通过并行的确认应答机制,接收一堆,响应一堆。TCP发送真正采用的是这种策略。
滑动窗口和缓冲区
发送出去的数据,在没有得到回应的情况下,必须被保留在发送缓冲区以便支持超时重传。
在发送缓冲区中,已经发送,但没有得到响应的区域,就是可能会出现超时重传的数据的区域。
它的整个区域大小也就决定了当前最多能发多少的数据给接收端,能发多少也就取决于接收端有多少的吞吐量,而衡量这一标准采用的就是接收端的16位窗口大小字段。在发送数据的过程中,这个区域的大小随着接收端的窗口大小而变化,因此形象称之为滑动窗口。
上图中,假设开始滑动窗口位4KB,同时也对应接收端接收缓冲区为4KB,当其中4KB数据全部传输过去,但接收端的用户进程一点都不取。这时接收缓冲区的剩余容量就是0,因此滑动窗口大小也就为0。(同时滑动窗口的移动不可能向左,并且不用担心越界问题,因为在实现角度上可以通过一个缓冲区数组的下标进行取模调整,因为滑动窗口之前的数据都是无用数据了。)
滑动窗口与超时重传策略
假设接收端接收缓冲区接收能力一直不变,当传输序号为1001~2000的报文,当收到2001的确认序号,滑动窗口起始就移到2001,代表之前数据发送成功。
那么在传多个报文的时候,中间报文丢失了怎么办?
情况1:数据包完整,ACK丢了。
如上图,假设同时发送4种报文,其中只收到了确认报文4001,其它的确认报文都丢失怎么办呢?
只要确认序号收到4001,那么就代表4001前的都收到,前面丢的都可以忽略,start_index就可以直接到4001。
情况2:数据包丢了
数据一直从1 ~ 1000发到6001 ~ 7000,接收端的接收缓冲区里除了1001 ~ 2000的数据包,其它的都收到了。因此返回确认报文时,确认序号总是1001,当发送端收到3个同样的确认应答时就会知道对应数据包丢失了,进行重发。之后如果接收端收到了1001 ~ 2000的数据包,就会返回确认序号为7001的报文,代表之前序号的报文都收到了。(对应滑动窗口start_index就到了7001)
这种机制被称为 “高速重发控制”(也叫 “快重传”)
综上:滑动窗口能在一定上提高传输的效率,同时也能很好的解决TCP的丢包问题。
3.4 TCP的流量控制
在前面介绍窗口大小字段的时候,为了让发送端知道接收端的剩余接收容量,在接收端发送报文给发送端的时候会附带一个窗口大小字段,代表当前还能接受多少的数据量。因为TCP是全双工的,双方都可以这么做。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制。
如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是发送方会定期发送一个窗口探测数据段(如果过了重发超时的时间还没收到窗口更新的通知,就会发一个窗口探测的包),再由接收端把窗口大小告诉发送端。(但这个返回的窗口通知是可能会丢失的,因此发送端主机会时不时发送窗口探测包。)
不过窗口大小是16位的,最大也就64K,那么数据超过这个怎么办?在TCP首部中选项中有对应的扩大因子,会将实际窗口大小字段左移。
3.5 拥塞控制
在之前,我们考虑了主机之间的数据可靠问题、效率问题、流量控制问题,但是这些都是主机和主机之间的。网络通信还必须考虑主机和网络之间的问题。因为网络上有很多的计算机,可能当前网络状态已经很拥堵,如果在不清楚网络的状态下,贸然发送大量数据,是很有可能引起雪上加霜的。
当发送方发送1K个报文,在接收端接收时发现少了2、3个报文。此时可能是发送端或接收端问题,这没事,大不了就超时重传一下。
如果发送方发送1K个报文,在接收端接收时发现少了大量的报文,此时就不应该是发送端接收端的问题,因为它们之间有策略保证了数据的可靠性问题。此时大概率就是网络的问题了。
因为网络拥塞问题出现大量的丢包,那能重传吗?
接入网络的主机是很多的,如果网络出问题,这么多的主机出现丢包如果超时重传的话,那么一定会有一定量的主机在同一时间段向网络发送数据,造成了大量的数据向网络里塞,网络压力本来就很大,因此这种方式不行。
那解决方案?拥塞控制!
TCP引入慢启动机制,识别到网络拥塞,先发少量的数据探探路,摸清当前的网络拥堵状态,再按照多大的速度传输数据。
拥塞窗口,发送的数据在这个窗口数据量内不会发生阻塞,超过这个量可能会导致网络拥塞问题。
在之前,一次向目标主机发送数据的量是对方窗口大小,现在考虑网络,一次向目标主机发送数据最大的量就应该是对方窗口大小和网络拥塞窗口的较小值。
在发生开始的时候,定义拥塞窗口大小为1,每次收到一个ACK应答,阻塞窗口以2倍增长。这种”慢启动“在初始时慢,但是增长速度很快,这也是为了想尽快把数据传给对方。当拥塞窗口超过某个阈值的时候,不再按照指数级方式增长,而是按照线性增长。如果再次遇到网络拥塞,就会再次降低拥塞窗口进行慢启动。
综上:拥塞控制是为了TCP协议尽快把数据传输给对方,但又要避免给网络造成拥塞的折中方案。
3.6 延迟应答和捎带应答
延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,因为接收发上层用户还没从接收缓冲区里取走数据,窗口大小就被返回了。
假设接收缓冲区的大小是1M,一次收到了500K的数据;如果立刻应答,返回的窗口就是500K。如果等待一段时间让上层用户将缓冲区中的500K数据取走,再返回的窗口就又是1M。
值得注意的是:窗口越大,网络的吞吐量就越大,传输效率就越高,我们目标是保证在网络不拥塞的前提下提高传输效率。
那么这个等待时间如何把握呢?毕竟等待是为了更好的提高传输效率,如果等待过长就得不偿失了。
- 数据限制:每隔N个包就应答一次。(一般N=2)(更多)
- 时间限制:超过最大延迟时间就应答一次。(一般200ms)
捎带应答
稍等应答很简单,在之前认识TCP两端进行通信的时候,大多数是”一发一收“的,在ACK的时候可以不仅是发送确认,同时也可以发送我想给你发的有效信息。
3.7 面向字节流和粘包问题
创建一个TCP的socket, 会同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
建立连接的双端,有两套缓冲区,彼此可以互相读写,因此构成了全双工。
在写入和读取缓冲区字节的时候,可以任意控制其数量以及次数。
TCP是面向字节流的,接收和发送不关系任何数据格式,但是应用层要正确使用这里的数据,是需要有特定数据格式的(在应用层就需要自己定制协议)。如果数据处理直接从流氏空间中以特定字节读取的话,就很可能读到多个数据包的内容,这就是TCP粘包问题。(就是蒸包子时还没分开直接拿的时候,可能会拿到其它包子的部分)
如果明确报文和报文的边界?
1、通过特定的标识符将其分开。(比如http中的空行、\r\n)
2、可以在报头位置,约定一个包总长度的字段。(比如http属性中的content-length)
UDP协议没有”粘包问题“,因为其报头中有明确的报文长度字段,这保证了其报文的边界。
3.8 TCP总结
TCP的复杂就是为了保证其可靠性以及尽可能提高性能。
可靠性:
- 校验和
- 序列号(确保发送和接收的顺序性)
- 确认应答(通过序号和确认序号)
- 超时重发
- 连接管理(三次握手,四次挥手)
- 流量控制(通过窗口大小,决定数据能被接收的量,同时也决定了发送方的速度)
- 拥塞控制(在保证网络不拥塞的情况下,维持TCP最大的传输速度)
提高性能:
- 滑动窗口( 让数据并行可靠的向对方发送)
- 快速重传(发送丢包时,发送方通过边传数据边识别多次收到相同确定序号报文的快重传策略)
- 延迟应答
- 捎带应答