前面我们学习了应用层的协议,接下来我们进入下层传输层协议的学习;
1.UDP协议
udp协议是用户数据报协议,这样的协议的优点是简单,但这样的协议不会进行传输控制,对于传输的可靠性没有提供保证;我们前面使用udp协议进行过通信,我们也看到过代码实现时udp协议的实现非常简单;接下来我们来看看udp协议的结构;
1.1 UDP协议报文结构
我们可以看到udp报文的结构非常简单:
1.16位端口号和源端口号:它们是用来标识本主机进程端口号与目标主机进程端口号的
2.16udp长度:用来标识整个报文的长度2^16是64kb,所以udp报文的最大长度是64kb,当我们要单次传输的数据超过64kb时我们需要分段进行发送,避免报文过长被丢弃的情况出现;
3.udp校验和:这个是用来验证通信数据的正确性的,这是通过数学原理验证的
4.数据部分:其实就是有效载荷部分;
当我们了解了udp报文的结构后,我们再来看udp报文,来理解udp协议的特点;
1.2 UDP协议的特点
1.udp协议是无连接的
udp协议不需要建立连接就可以直接进行通信,只管给目标主机发送数据即可,而这样的行为一定会导致这样的通信是不可靠的;
2.udp协议不可靠
由于通信的双方没有建立连续,所以双方都不能确保自己的信息成功传输到对端,可能出现如:乱序,丢包,重复传输等情况;
3.udp协议面向数据报
udp协议是面向数据报的协议,udp协议只有接收缓冲区,没有发送缓冲区,对于上层应用层交付下来的报文,udp会加上自己的报头后直接向下交付,所以假如上层有一个https报文,这个完整的https报文会直接打包为一个udp报文然后再向下传输,接收方接收的udp报文中也只会包含一个完整的https报文;所以我们看到的现象就是一个个应用层报文的传输;
1.3 UDP的缓冲区
udp没有发送缓冲区,所以udp无法进行传输控制,所以udp无法保证传输的可靠性,所以udp是面向数据报的协议,上层协议直接打包向下交付;
udp有接收缓冲区的,到达udp接收缓冲区中的udp报文会想链表一样先描述再组织的管理在udp缓冲区中;
2.TCP协议
tcp是传输控制协议,所以tcp的传输是保证了传输的可靠性的,如果传输要保证可靠性要做的事情一定会比前面udp不保证可靠性的要多,所以tcp协议的结构一定会更复杂;
在我们了解tcp报文的结构之前,我们需要先了解基本的tcp通信;
2.1 确认应答机制(ACK)
我们知道tcp协议具有可靠性,那么tcp协议是如何保证可靠性的呢?在基本的tcp通信中,tcp具有确认应答机制,当发送端将信息发送给接收端时,接收端收到了信息后,会向发送端回信告诉发送端我接收到了信息,这样发送端就可以知道自己的信息是否发送成功,保证了可靠性;应答保证了前一次通信的可靠性
真实情况:
此外,应答也不一定是作为独立报文来发送,假设主机1向主机2发送了一个网页页面的申请的请求,主机2收到后肯定得返回网页的响应,那么就可以在响应的时候将报文捎带的发送给主机1,这样极进行了通信又证明了上次请求通信的可靠性,这也是最贴近实际的一种情况;
2.2 捎带应答
这就是我们现实生活中真正通信的情况;
我们了解了基本的通信方式后,我们再回过头来看看tcp报文的结构,来了解tcp的细节;
2.3 tcp协议结构
2.3.1 窗口大小(流量控制)
再理解16位窗口大小的作用前,我们先看看下面的两点:
1.tcp通信一定是完整的tcp报文传输,所以每次通信都是有完整的报文结构的报头+有效载荷
2.tcp是通过确认应答的方式来保证通信的可靠的(信息的成功到达),所以每次通信都一定会有相应的应答;
由上面的两点我们可以知道每次通信一定都是完整的报文进行发送,那报头一定也是完整的,那么每次通信一定都会发送这个16位的窗口大小,发送信息与应答都会发(应答也是通信);
作用:
窗口大小的作用是进行流量控制,什么是流量控制呢,就是数据传输的快慢控制;窗口大小代表的是本主机的tcp接收缓冲区剩余的空间大小,对方主机再通信时可以时刻知道通信主机还可以接收多少的数据,从而进行发送数据多少的控制,这就是16位窗口大小起到的作用;
2.3.2 序号与确认序号
(通信的起始序号不一定为0,使用随机数来尽量避免网络中残留数据的影响)
我们前面知道了每次发送信息都需要应答来证明信息是成功到达的,如何证明呢?
当一段发送tcp报文时发送的数据应该是把这个缓冲区中的一部分数据打包发送出去,那发送的情况应该是这样的:
当接收端接收到这份报文时通过报文中的32位序号字段就可以获得发送的序号,接收端既可以通过这部分序号来对接收到的序号进行排序(保证有序性)也可以通过这个序号进行相应序号的确认应答让发送端知道是哪条信息应答了(设置32位确认序号);
下面是报文批量发送的情况(由于16位窗口大小的存在,我们知道了对方的接收能力,所以可以同时发送多条报文给对端,而多条报文到达对端的时间是不确定的所以需要序号来排序,确认序号则是返回发送端的ACK告诉发送端某条信息已经送达了)
这样的序号与确认序号还有一个好处,就是可以允许部分的应答丢失,比如:
上面就是32位序号与确认序号的作用;
面试题:为什么不将序号与确认序号合并,都发送一样的内容?
由于现实生活中的应答一般是捎带的,所以序号必须和确认序号区分开才好进行稍带;
2.3.3 四位首部长度与选项长度
其中四位首部长度代表的是报头的长度2^4=16可以代表0-15,首部长度的单位为4字节,就是说报头的长度最长为4*15=60字节,所以选项长度最长为为60-20=40字节;
2.3.4 TCP通信过程
2.3.5 6个标志位
报文具有类型,而标识类型的就是这6个标志位,可以通过上面的通过过程辅助理解;
1.ACK:当ack标志位为1时代表32位确认序号有效,此报文是具有应答作用的;
2.SYN:这是请求建立连接的标志位,双方开始建立连接时第一条报文(同步报文段)
3.FIN:这是通知对方主机本端要关闭,想要断开连接的报文标识;
4.PSH:当报文中此标志位为1时代表,发送端收到对方的窗口大小很小,而发送端想要发送的数据很多,这就会导致发送端发送阻塞,为了让自己的信息可以发送提高效率,发送端会将自己发送的报文的PSH标志位置为1,接收到接收到这样的报文后,会快速将接收缓冲区中数据向上读取,来空出位置让发送端可以继续发送,接收端向上读取的行为是TCP自己进行的;
5.RST:当连接建立失败时(TCP允许连接建立失败),先得知连接建立失败的一方会向对方发送带有这一标志位的报文来重新建立连接,这个报文也叫(复位报文段)
6.URG:当发生某些紧急情况时,例如客户端发现自己向服务器发送的请求一直没有响应,但是服务器又是可以成功ping上,证明服务器还在运行,客户端想要了解服务器发生了什么,客户端就会发送一条报文这条报文的URG标志位为1,服务器在收到这条报文时会发现URG标志位为1从而去查看相应的16位紧急指针指向的位置(紧急数据最后一个字节后面一个字节)来查看客户端想要询问服务器的问题,URG起到的时将某些紧急数据优先处理的作用
2.3.6 紧急指针
接上面URG标志位说到的作用,紧急指针会指向紧急数据(应用层叫外带数据out-of-band-data) 紧急指针一般也是字节序号,假如报文的序号为1001(1001-2000),紧急指针值为50,所以紧急数据的范围为1001-1050(1050=1001+50-1)紧急指针指向的是紧急数据最后位置的后面一个字节,所以1001-1050为紧急数据;
以上就是TCP结构的基本内容,下面我们来看看TCP到底做了哪些工作来对通信进行控制保证通信效率与可靠性;
2.4 超时重传(接确认应答)
当发送端发送出去一条消息时,发送端是不清楚它发送的消息的状态的,这个时候我们通过确认应答机制收到应答,就可以证明消息成功到达;但如果发送端发送出去了消息,过了一段时间后迟迟没有收到应答,这个时候发送端依旧无法判断,它发送的消息是在网络中阻塞了,还是丢包了,又或者是对方返回给发送端的应答丢失了;但不知道没有关系,超时了重传就好了,必须要收到应答了才能保证消息是成功到达了,这就是超时重传的作用与原理;
那么超时,这个时间如何设置呢?
这个超时的时间是动态变化的,与网络环境有关,当网络情况良好时超时时间需要设置短一些,网络情况差时需要设置长一些;
在linux与windows中是这样控制超时时间的,超时时间以500ms为单位,当发送的消息在500ms后没有收到应答,发送端就会进行重发,第二次等待500ms*2^1=1000ms还是没有收到应答时,发送端再次进行重发,第三次等待500ms*2^2=2000ms还是没有收到应答时,发送端继续重发,依此类推,超时的时间以指数增加,知道重传的次数到达某个设定值时,发送端会直接断开连接;
2.5 TCP通信过程
随着上面通信过程的图,我们来理解一下TCP通信;
2.5.1 建立连接过程(三次握手)
客户端想要和服务器进行通信,所以它们需要建立连接检查是否可以正常通信,所以建立连接的过程其实也是验证全双工的过程;
那TCP是如何建立连接的呢?
客户端首先向服务器发起SYN请求(第一次握手),服务器ACK应答,并且服务器也马上回应客户端,我要为你服务我也要和你建立连接发送SYN请求,这里服务器的SYN与ACK合并发送(第二次握手),客户端收到服务器的SYN后进行应答ACK,此时客户端连接建立成功(第三次握手),服务器收到客户端的ACK应答,服务器的连接建立成功;
通过上面的三次握手连接成功建立,那为什么一定是三次握手呢?
假设是一次握手就建立好连接,客户端可以不断向服务器发起请求,服务器只要接收到了请求就建立好连接,而客户端收不到应答客户端是不一定建立了连接的,如果此时客户端对服务器不断的发送建立连接请求,服务器不断维护连接,导致服务器资源不断被消耗,从而挂机,这就是SYN洪水问题;
如果两次握手就可以建立连接,依旧会导致服务器先建立连接,如果服务器回信(第二次握手)的消息丢包,这样也会导致客户端没有建立连接,而服务器建立了连接,从而浪费服务器资源;为了避免客户端连接建立失败浪费服务器资源的情况,引入三次握手,让服务器在第三次收到客户端ACK应答时才建立连接,将对方连接建立失败的风险转嫁给客户端,当客户端的ACK丢失时,服务器不会建立连接,从而不会浪费服务器资源;
其实三次握手也可以是四次握手,只不过服务器的ACK被SYN捎带应答了,所以这两次握手合并了;
2.5.2 listen第二个参数与全连接半连接队列
我们在之前的tcp服务器代码中的listen函数调用时,我们知道这个函数的第二个参数backlog不能太大也不能太小;现在我们来解释一下原因;
首先,我们需要知道三次握手是由tcp自动完成的,与服务器是否accpet无关:
服务器run函数代码:
(下面代码的意义只在于告诉我们服务器没有使用accept函数来服务监听到的进程)
void run(){while (true){sleep(1);//不进行任何操作,不accept}}
我们的服务器只创建了socket套接字并listen监听,没有对连接的进程进行任何操作;但是我们使用netstat查看还是可以发现服务器与客户端是成功建立连接,都进入了ESTABLISHED状态的;
所以此时在还没有调用accept函数之前,客户端与服务器就已经建立好了连接了:
这里是成功建立连接的情况,但是如果我们将listen的第二个参数修改为1,我们的全连接队列的长度则最大为backlog+1=2;说明全连接队列长度为2,那么什么是全连接队列呢?
而listen的第二个参数就是我们的全连接队列的长度,最大为backlog+1;
半连接队列则是在服务器接收到了SYN,返回给客户端SYN+ACK的时候维护的等待确认的连接:
所以我们可以将上面两个队列情况合并:
在我们现实生活中,真正的SYN洪水攻击,就是这里半连接队列和全连接队列被占满的情况,其他的主机申请时无法进入队列无法进行通信;
总结:
现在我们就知道了为什么listen的第二个参数backlog不能太大也不能太小的原因,不能太大是因为全连接队列长度太长维护的没有进行服务的连接太多会占用那些正在服务的连接资源,增加资源消耗;而不能太小是因为全连接队列是作为候补连接的,当服务器空闲时可以立马补充连接进行服务,减少服务器空闲而没有连接来服务的情况,提高资源利用率(类似餐厅候补的情况)
2.5.3 tcp断开连接过程 (四次挥手)
一般这个过程是客户端先向服务器发送断开连接请求(这个动作在现实中可以看作关闭网页和退出APP的动作),此时客户端为FIN_WAIT_1状态,服务器接收到了客户端断开连接请求会这个请求进行应答ACK,此时客户端进入FIN_WAIT_2状态,服务器为CLOSE_WAIT状态,如果此时服务器还有在前面数据传输阶段的报文没有发送完,服务器不会发送断开连接请求,当数据传输阶段的报文发送完后,服务器也发送断开连接请求,此时服务器进入LAST_ACK状态;当客户端收到了服务器发送的断开连接请求后,客户端才会进入TIME_WAIT状态,并返回应答ACK给服务器,服务器收到后直接进如CLOSED状态彻底退出,而客户端还会等待2MSL(报文的最大存活时间),之后才会彻底退出;
上面就是四次挥手的完整过程,四次挥手不能合并为三次握手的原因是因为,客户端发送断开连接请求时,服务器不一定可以发送,它还有前面的数据报文没有发送完,如果服务器恰好与客户端同时想要断开连接,那么其实是可以将四次挥手合并为三次挥手的;
2.5.4 TIME_WAIT等待一段时间原因
断开连接的一方会进入TIME_WAIT状态,而进入TIME_WAIT状态后需要等待2MSL,这是因为信息在网络传输中,如果阻塞在了网络中还没有到达,你如果直接退出,如果你马上重新启动,这个新启动的进程是有可能收到上次阻塞在网络中的信息的,这个信息在这个新进程中是错误的信息,可能会影响新进程的通信;所以TIME_WAIT需要等待一段时间让阻塞在网络中的数据消散;其次,等待一段时间也可以用来确保其发送的ACK到达对端,如果没有到达你处于TIME_WAIT状态没有结束进行还是可以重发ACK来保证最后的ACK的完成;
1.消耗掉阻塞在网络中的数据
2.保证最后的ACK到达
现在我们也可以解释之前服务器进程主题ctrl+c结束时无法立即重启的原因,这就是因为服务器进入的TIME_WAIT状态,端口号被占用了的原因,需要使用setsockopt函数来解决;
2.5.5 setsockopt函数的原理
int opt=1;
setsockopt(_socketfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))
_socketfd指的是socket套接字,SOL_SOCEKT指的是套接字层(设置的选项所在的协议层),SO_REUSEADDR | SO_REUSEPORT代表的是端口号可复用的意思,&opt是选项值的指针;这样设置后,端口号可以复用,服务器自然也可以快速重启了;
sockfd
:套接字的文件描述符。level
:设置的选项所在的协议层,如SOL_SOCKET
、IPPROTO_TCP
等。optname
:要设置的选项(如SO_REUSEADDR
、SO_RCVBUF
等)。optval
:指向选项值的指针。optlen
:optval
指向的缓冲区的大小
2.6 滑动窗口(流量控制)
2.6.1 滑动窗口如何滑动
在上面的2.3.1中我们知道了流量控制的其中一个条件是拥有16位的窗口大小来通知对方主机本端的接收能力,从而达到流量控制的效果;接下来我们来看看约定好了接收能力后,发送端是如何操作控制发送数据量的大小的;
首先,我们知道发送端是有发送缓冲区的,发送缓冲区中的数据就是要发送给对端的,既然如此我们可以使用双指针算法,在发送缓冲区上移动,控制需要发送的数据的数量,这就是滑动窗口,下图中的win_begin与win_end之间的就是滑动窗口;
2.6.2 丢包时滑动窗口情况(快重传)
当滑动窗口中的报文没有收到应答时,这条报文就不会被覆盖,会一直保存在发送缓冲区滑动窗口中,等待重新发送;这也是超时重传的行为,超时重传一般是发生在网络状态不好的情况下发生的丢包行为;而当网络状况好的时候,某条丢失了应答的报文它之后的报文都是成功应答时,这些后面成功应答的报文返回的应答序号都会是丢失的那条应答的序号,当发送端接收到三条相同序号的应答时,这个时候就会触发快重传机制,发送端不会再等待丢失了应答的报文超时后再重传,而是直接重传,这就是快重传;
2.7 延迟应答
(TCP协议层稍微等一等)
在接收端接收到了报文时,接收端不会立即返回应答,而是会等待上一段时间,这一段时间中,接收端的上层可以尽量的从接收缓冲区中取走报文,以提供给TCP更大的接收缓冲区用来接收数据,从而让发送端的滑动窗口增大,可以发送更多的数据,减少IO次数,提高通信效率;而在我们的代码编写中,也是尽快调用recv,read这样的函数来尽快读取缓冲区中的数据的;
2.8 拥塞控制
网络通信时可能出现网络状态较差的情况,而网络又是公共资源,当网络非常拥塞,可使用资源少时,这个时候,就需要网络中的所有主机都对通信的数据进行控制,不再继续消耗网络资源,缓解网络的阻塞情况;下面我们看看我们的主机是如何进行拥塞控制的;
2.8.1 慢启动算法(控制拥塞窗口)
在开始通信时,拥塞窗口最小,但进行指数型的快速增长,直到遇到阈值时改为线性增长;这样的算法可以让数据通信慢慢试探网络情况,如果网络状态良好就会快速增长;慢启动,初始速度慢,增长速度非常快;当网络状态非常差时,会重新开始慢启动算法,并将阈值除以2;
滑动窗口大小=min(拥塞窗口大小,接收缓冲区大小,数据量)
2.9 面向字节流
对于TCP,它拥有发送缓冲区与接收缓冲区,是可以对用户上层交付下来的数据进行自主的传输控制;而其对于用户数据进行传输控制的方式,就是通过将用户的数据识别为一个个的字节(而不是报文的形式),当用户数据到达TCP层时,TCP识别的是一个个的字节,至于TCP向下传递的每个报文有多少字节,这是TCP层自己决定的;面向字节流就是TCP层对于数据只有字节的概念;
2.10 粘包问题
前面在延迟应答讲解时,我们就说过用户层应该尽快的将TCP接收缓冲区中的数据快速读取到上层,从而提供给TCP更大的窗口大小;而当用户一次读取了大量数据时,由于TCP是面向字节流的,所以用户并不清楚这一长串字节是几个报文,这个报文是否完整(这里的报文是用户层报文);那么如何区分开这些报文的问题就是粘包问题;
其实这个粘包问题,早在我们前面的博客协议定制:
应用层协议编写,序列化反序列化,网络版计算器,Json序列反序列化工具,条件编译,结合网络协议>网络协议栈理解协议定制-CSDN博客
这里的打包与解包处就有操作过;
这里打包的报头是增加一个有效载荷长度和一个空行作为报头,之后通过相同的方式将有效载荷解析出来;
其实还有在http协议博客:
http协议-CSDN博客
我们应该读取到空行和Content-Length属性后的有效载荷长度来分离出完整的报文来解决粘包问题;
所以粘包问题我们早就接触并且解决过只是之前没有比较清晰的认识;
如何解决粘包问题?
定协议!下面是定协议的方式:
2.11 主机不同情况TCP行为
1.主机正常重启或关机进程终止(不管异常还是正常终止):
正常的进行四次挥手,因为四次挥手开始是由套接字(文件)关闭时开始的,当进程想要进行四次挥手时只需要关闭相应的套接字(文件描述符),TCP就会自动完成四次挥手,不管进程主动还是异常关闭都会将套接字文件也关闭,从而进行四次挥手;
2.主机断电/网络断开:
对端主机发送消息或者一段时间后发现链接异常,就会进行reset,如果链接还是无法重新建立,那么对端主机也会断开(销毁)链接;(保活机制与发送信息发现链接异常情况)
2.12 文件与socket的关系
(这个点的讲解有些模糊,是提供给我自己复习的)
对于方法集多态那里我还是有些没有理解等理解了再回来进行添加;
图片太大,通过下面的链接查看:
图片/socket与文件关系.png · future/my_road - Gitee.com
一个请求或者响应想要通过网络进行传递,就需要通过文件找到相应的sokect数据结构,再添加到数据结构中的发送缓冲区队列中(udp没有发送缓冲区直接向下交付);而这样的缓冲区在实际的内核数据结构中,其实是像链表一样的形式被组织起来的:
上面这样的一个sk_buffer结构就是一个报文,在网络协议>网络协议栈中每次向上或者向下传递,都是移动head指针来增加或者去除报头;而这样的一个个sk_buffer又如链表的节点一样一样被组织在sk_buffer_head这样的数据结构中;
所以我们可以有这样一个模糊的认知:用户层的一个请求(用户自己定义想要发送的数据)不断向下交付就是通过文件找到相应的socket结构,在从相应的socket结构中找到sock数据结构中的发送队列,用户的请求会作为一个节点进入这个队列中,之后请求不断向下传递,就是在这个节点上进行指针的移动来添加报头,接收响应就是反过来理解;