传输层协议
在TCP/IP协议中,用"源IP","目的IP","源端口","目的端口","协议号"这样的五元组来表示一个通信,udp和tcp都是全双工的(在接收信息的时候可以同时发送消息)
TCP协议
有连接,可靠传输,面向字节流,但维护成本高,因为在通信途中没有确认传输成功之前,TCP就需要把数据存在传输层维护起来。
在tcp传输层中有MSS,这个是在tcp三次握手的时候进行协商的,因为数据链路层最多可以发送1500字节的数据,而这个数据是有包含tcp报头和网络层报头的,所以最大的可传输的数据长度为MSS,所以tcp原本是可以直接把数据一次性拷贝过去的,但tcp在滑动窗口做了分段,就是这个原因。



发送数据实际上和把数据刷新到磁盘一模一样,在用户层都有缓冲区,然后用户层缓冲区把数据交给操作系统内核的缓冲区,操作系统决定把数据交给底层的磁盘,就叫刷新到磁盘,如果交给底层的网卡,就叫做网络通信,但本质都是拷贝。
TCP的文件描述符只有一个,但这一个文件描述符既可以读,也可以写,因为TCP的两端的文件描述符对应的struct file都有两个缓冲区,一个用于读,一个用于写,所以TCP是可以控制发送的,等操作系统觉得什么时候可以发送了,数据才能发送,而udp做不到这一点,因为udp没有发送缓冲区
可靠性
报文格式

前20字节属于tcp的标准报头
数据偏移(首部长度) :表示报头长度+选项长度是多少,如果没有选项,那么这个值就是20,但首部长度只有4位,表示范围是0-15,但计算的单位是4字节,所以表示的范围是0-60字节,而标准报头是20字节,所以选项最多是40字节,然后通过固定长度分离
16位窗口大小 :基于确认应答机制,当服务器给客户端确认应答的时候会发送完整报文或者报头,服务器会在窗口填上服务器接收缓冲区的剩余大小,同理,当客户端确认应答的时候也可以使用同样的方法
序号 :为了解决数据报乱序问题,可以使用序号,保证数据的按序到达,http等应用层协议在传输层看来也只是一个大字符串而已,放在传输层的一个char数组,数组天然就有下标,每一次传输层都会发送一批数据,就会把这一批数据里数组下标最大的数组下标作为序号填到字段里,序号刚开始是随机的,这是为了避免新连接接收到老数据
确认序号 :是用于应答的,里面填充的是收到报文的序号+1,表示确认序号之前的数据已经全部收到了,下一次被应答方发送就要从确认序号作为开始,用于填充序号,通过这样的规定,表示我们允许应答有少量的丢失,比如说确认序号已经收到301了,但101和201都没有收到,那么我们依旧认为301之前的报文全部收到了
关于为什么需要两个序号:客户端给服务器发信息,服务器也可能给客户端发消息,如果服务器使用捎带应答,那么服务器既要应答,也就是使用确认序号,这里的确认序号等于收到的报文的序号+1,而服务器同时也要发信息,也就是使用序号,序号就是收到的报文的确认序号,所以在这种条件下只使用一个序号就会出现冲突。
六个标记位 :tcp建立连接,正常的数据通信和断开连接都需要tcp报文的,服务端收到不同类型的tcp报文就会做出不同的动作
ACK:判断确认序号是否有效,当三次握手建立成功后,绝大部分报文的ACK默认都置为1的,表示当前报头具有应答属性,至于有没有数据需要看有没有有效载荷,为0就表示当前报文不包含确认信息,确认序号就被忽略
SYN:请求建立连接,我们把携带SYN标识的称为同步报文段
FIN:通知对方,连接要关闭了,和close很像
PSH:当置为1的时候,提示接收端应用程序立刻从TCP缓冲区把数据读走,比如说服务端一直不读数据,而接收缓冲区数据越来越多,数据就放不下了,就可以通过这个位置提醒服务器提取数据
RST:对方请求重新连接,我们把携带RST标识的称为复位报文段,有时候会有特殊情况,比如说客户端认为连接建立成功了,而服务端认为连接没建立成功,当客户端请求服务器信息的时候,服务器会在报文里添加RST字段,请求重连,客户端在第三次ACK发送出去后,就认为连接建立完成,但如果最后一个报文丢失的话,服务器就认为连接没建立成功,但客户端就认为连接建立好了
URG:紧急指针是否有效,如果是0标识紧急指针无效,如果是1则标识这个报文含有紧急数据,代表16位紧急指针有效,需要高优先级处理
紧急指针 :如果我们需要让一些数据优先,那么就设置URG标志位,紧急指针里面的数字表示紧急数据在正文中的偏移量,这个数据就可以被高优先级处理,关于为什么没有指定紧急数据的大小,是因为TCP这个协议里,紧急数据只能携带一个字节,可以用于机器卡顿,无法处理外来数据的时候,对机器发起询问,读取软件状态编号,就可以进行修改优化
如果我们想发送紧急数据,可以使用sendto,在flags这个参数传递MSG_OOB参数,就可以发送紧急数据,要接收紧急数据的话,在recv的flags设置MSG_OOB即可


流量控制
服务器是有接收缓冲区和发送缓冲区的,当客户端不断向服务器发送消息,但服务器来不及处理的时候,会选择让客户端发慢一点,从而有时间处理接收缓冲区里的内容,这就是流量控制,否则会导致大面积丢包。而tcp是可以重传的,即使不进行流量控制,丢包之后也可以让客户端重新发送报文,但这样会消耗大量的网络带宽资源,造成低效率的问题。
其实流量控制是一种类似于生产者消费者模型的机制,用于将生产者消费者的速度匹配,当服务器的接收缓冲区快满了的时候,TCP报头里的PSH会置为1,提醒服务器的应用程序把数据读走,如果一直不取走,客户端会一直往发送缓冲区写数据,直到缓冲区写满了,就阻塞了。
在三次握手期间就已经协商了双方缓冲区的接收能力等内容,第三次握手发送ACK的报头的报文是可以携带数据的

如果发送方一直发送数据,把接收方的缓冲区打满后,接收方发送后来的响应报文说窗口大小为0,发送方就不能再发送,发送方就开始等待接收方的窗口更新报文,但发送方并不会一直等,而是在一段时间后发送一个窗口探测报文,窗口探测报文并没有携带数据,因为我们不知道对方缓冲区是否已经满了,无法处理数据,只携带报头就不会使用到缓冲区。接收方接收到窗口探测报文就必须响应,把窗口大小通过报头带回去。
一端通过16位窗口字段把窗口大小告诉另外一端,2的16次方就是65535,也就是说缓冲区的最大是60KB,但tcp报头里还有选项字段,包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位
确认应答(ACK)机制
tcp是通过确认应答机制保证数据的可靠性的,也就是说客户端在给服务器发送消息后,服务器确认接收到正确的消息,然后就会确认应答,而发送消息和确认应答实际上都会携带完整的TCP报文或者报头(应答+发送TCP数据=捎带应答),客户端需要收到应答才能确定数据没有丢失,所以客户端有一个定时机制,当一段时间后没有收到应答,客户端就认为数据丢失,进行重传。
但每一次都只发一条消息应答的话,效率太低,所以现实中的TCP客户端会并行发送一批消息,但这会出现数据报乱序问题,这就需要序号的作用
超时重传机制
主机A把数据发给主机B的时候,可能数据还没有到达主机B,也可能数据包丢失,也有可能是应答丢失了(这种情况会让主机B收到重复的报文,但报文具有序号,可以进行去重),无论如何,主机A在一定时间没有收到主机B的确认应答之后,都会把数据报重发。
关于超时的时间间隔 :如果设置的时间太长,出现丢包,就会影响我们重传的效率,如果设置的时间太短,可能会发送大量重复的包,所以这个时间间隔必须是动态的,而且要和网络状况是强相关的。
策略 :Linux里每次都以500ms为一个单位进行控制,如果重发一次收不到应答,就等待2500ms再次重发,如果依旧收不到应答,就等到4500ms再次重发,以此类推,直到积累到一定的次数之后,TCP认为网络或者对端主机出现问题,就会强制关闭连接
连接管理机制

在三次握手的时候,会建立连接,协商起始序号,协商双方的接收缓冲区的大小
bash
netstat -ntp #可以用来查看连接情况
connect函数只负责发起三次握手,实际上就是客户端对服务器发起一次SYN,接下来就是一直阻塞,直到三次握手完成后,connect才返回。
accept本身不参与三次握手,只有在三次握手后连接建立好,accept把连接拿上去用,如果底层一直没有建立连接,accept就会一直阻塞。
四次挥手其实也可以看作是三次挥手,因为服务端发送的FIN+ACK是可以合在一起变成捎带应答,三次握手也可以看作是四次握手,和四次挥手也是一样的道理,缺任何一个环节就会导致连接的可靠性出问题。
三次握手其实是保证客户端和服务端都有一次发送消息和收到消息的经历,首先,这是为了验证客户端和服务端的全双工通路是否通畅,也就是能不能正常的收和发消息,其次,如果只有两次握手,服务器并不知道自己发出的消息客户端有没有收到,也就是不知道服务器发送消息的功能是否通畅,所以需要第三次握手,收到客户端发送的应答才能确定发送消息通畅。
还有一个原因,建立连接是有消耗的,如果只有一次握手,同一个客户端一直发送SYN信号,而服务端一直建立连接,就会导致浪费,如果只有两次握手,服务器在接收到客户端第一次发送的SYN信号后就建立一个连接结构体,客户端在收到服务器的ACK也会建立一个结构体,如果这个时候客户端崩掉了,没有第三次ACK的话,那么服务器会一直不知道客户端的情况,直到很久不使用这个连接才正常关闭,这种时间周期太长,非常占据资源,第三次握手其实可以把这种风险嫁接到客户端上,保证服务器的稳定性。
如果服务端的全连接队列已经满了,最后会导致服务器(或者被建立连接的一方)处于SYN_RECV状态,无法变成ESTABLISH状态,但客户端已经处于ESTABLISH状态了,说明客户端有发送ACK报头,但服务端会把这个报头丢弃,这样的连接就称为半连接,这种连接不会长时间保存,但半连接也是有限制长度的,如果半连接的队列满了,那么正常的客户端是连接不上服务器的,这就是SYN洪水。在半连接情况下,如果客户端能够发送消息的话,服务器会发送RST报头重新建立连接。
当客户端发送FIN报头(调用close)的时候,服务器会接收到,然后变为CLOSE_WAIT状态,客户端变为FIN_WAIT2状态,接下来向服务器发送2号信号,服务器在底层会自动完成剩下两次挥手,客户端就变为TIME_WAIT状态,如果反过来,服务器变为TIME_WAIT状态,IP和端口依旧被使用中,所以有时候我们关闭服务器后立刻重启,并且使用相同的端口,就会绑定失败,我们可以用setsockopt来设置套接字属性,让套接字可以复用端口,如果服务器还在正常使用,那么会重启失败,如果发现服务器是处于TIME_WAIT状态,就让服务器立即启动,客户端没有这个烦恼,因为客户端绑定的是随机端口号。
TCP协议规定,主动断开连接的一方要先处于TIME_WAIT状态,等待两个MSL(一个报文在网络中存在的最大的时间)的时间才能到达CLOSED状态,一般要持续60-120s的时间,这里有两个原因,第一是需要让通信双方历史数据得以消散,大部分是丢弃历史报文,否则再次重连的时候,容易让新的连接受到历史数据的影响,第二个是如果最后一个ACK报文丢失,处在LASK_ACK状态下的另一方会重新发送FIN报头,所以不能退出,只能一直处于TIME_WAIT状态,正常完成四次挥手

拥塞控制
其他机制是用于两端机器的,而拥塞控制机制是用于网络的,如果发送数据出了问题,不一定是主机出现了问题,也有可能是网络出了问题。
如果通信的时候出现了少量丢包,这是tcp的常规情况,如果发送方发现大量数据超时,出现了大量的丢包,这就是网络拥塞,那么就很有可能是网络的问题了(硬件设备出问题/数据量太大引起阻塞),在这种情况下我们是不能大面积重发丢失的报文的。
因为网络是共用的,我们让识别出网络拥塞的主机减少发的包的数量,而其他主机可以正常通信,就能大大降低网络的压力。
这里需要引入一个拥塞窗口,拥塞窗口的初始大小为1,每次收到一个ACK应答,拥塞窗口+1,每次发送数据包的时候,把拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为滑动窗口的大小,这被我们称为慢启动,为了不增长那么快,我们会引入一个称为慢启动的阈值,当拥塞窗口超过这个阈值的时候,就不会继续指数级增长(刚开始的时候是很慢的),而是变成线性增长,这样,当网络出现阻塞的时候,发送少量的报文,如果都能够发送,就说明网络已经趋于健康,我们就应该去关注另外一个窗口的大小了。

提高性能
滑动窗口

已经发送出去,但还没受到响应的报文,可能在发送方中存在多个,这些要被tcp保存在发送缓冲区里,我们只需要对缓冲区做划分区域即可。
已发送未应答区域是滑动窗口,因为滑动窗口的存在,我们才能一次性给对方发送一批数据,滑动窗口的最大的大小是对方接收缓冲区中剩余空间的大小。
如果丢包/未收到ACK的时候,发送了1000,2000,3000的数据,1000和3000的报文都发送成功,但2000这个报文没有收到ACK,我们并不会把滑动窗口往前移动,因为我们有序号和确认序号的概念,这里的确认序号最少都到3001,代表3001之前的数据我们都收到了,下一个数据序号应该从3001开始,无论是1000丢失,还是3000丢失,都是一样的,我们都会通过确认序号继续发送接下来的数据,这里说明tcp是允许少量的ACK丢失的。但数据并不会丢失,因为我们有一种快重传策略 ,确认序号的定义是序号之前的报文都收到了,能够保证我们收到的是丢的序号最小的报文,假设1000收到了,但2000的数据丢失,那么确认序号一直都是1001,当发送方收到了3次同样的确认序号后,就会重新发送丢的包。但快重传是有条件的,也就是说在发送数据的末期的时候,需要发送的数据很少,可能很难触发三次确认序号相同的条件,所以我们依旧需要超时重传。
滑动窗口不会向左移动,只会不动或者向右移动,窗口可能扩大,可能缩小,可能为0,也可能不变,start就是确认序号,end就是确认序号+窗口大小,如果数据没有那么多,那end就是数据尾,只有窗口为0,双方才会探测。
捎带应答
在接收方向发送方发送数据时,捎带着确认应答,告知发送方接收到的数据。这种方式可以减少数据传输中的往返次数,从而提高整体的传输效率。例如,在TCP协议中,捎带应答与延迟应答结合使用,能够有效降低通信成本。
延迟应答
如果我们需要发送的效率提高,可以让接收方给发送方通告一个更大的窗口,一次发送越多的数据,发送的效率越高,所以接收方可以晚一些应答,因为这样上层就有足够的时间把缓冲区数据取走,就能腾出更大的缓冲区空间。
面向字节流
由于缓冲区的存在,TCP的读和写不需要一一匹配,对方发一百个字节的数据,接收方可以读一百次,每次读一个字节,也可以一次性读100个字节,相应的,发送方也可以一次性发一百个字节,也可以一个字节发一百次,内核只认识有多少个字节,而不会对接收到的报文做区分,分析报文的任务交给用户层去做。
粘报问题
因为接收方会把接收缓冲区所有的数据一次性读到用户层,导致发送方发送的多个数据包在接收方接收时被粘合在一起,用户层没有对接收到的数据进行处理,导致无法正确区分每个数据包的边界。
如果想要解决粘报问题,可以在用户层定制协议,规定定长报文,或者使用特殊字符,来明确报文和报文的边界,或者使用定长报头+自描述字段,或者使用自描述字段+特殊字符
异常
进程终止 :连接是和文件相关的,文件的生命周期是随进程的,所以连接的生命周期也是随进程的,而在操作系统层面上,进程正常终止和异常终止是没区别的,所以连接会正常四次挥手,正常断开连接。
机器重启 :机器关机之前先要做的就是杀掉所有的进程,所以其实和上一个进程终止一样,正常断开连接,正常释放资源
机器掉电/网线断开:假设客户端网线断开,客户端就无法对另外一端发送消息,客户端重新联网后与服务器重新建立连接,对于服务器来说,连接还是旧的,而客户端的连接是新的,这就是连接认知不一致的问题,如果客户端一直没有重新连接,服务器发送出去的数据就不会有应答,时间一长,服务器就会把连接断开。
UDP协议
无连接,不可靠传输,面向数据报
UDP报头+有效载荷

16位UDP长度指的是整个报头的长度,16位有效载荷的长度指的是数据的长度,也就是UDP长度-8
不可靠传输:如果UDP检验失败,会直接把报文丢弃,并不会通知对方再发送一次,也就是没有重传机制
面向数据报:如果需要传输一个10kB的数据,sendto传一次,那么recvfrom也只能接收一次,而不能循环调用10次recvfrom,每次1kB
UDP是没有发送缓冲区的,调用sendto直接交给链路层,只有接收缓冲区,如果接收缓冲区满了,后来的数据报会直接被丢弃,而且UDP不保证可靠性,可能传输来的数据报顺序是乱的
UDP的长度最多是2^16B,也就是64KB,不能通过UDP发送超过64KB的数据,比较常用于直播,视频等内容
cpp
struct udp_header
{
uint16_t src_port;
uint16_t dest_port;
uint16_t udp_len;
uint16_t check;
};
udp的报文是用一个称为sk_buff的结构体描述的
cpp
struct sk_buff
{
//struct udp_header | 需要发送的数据 | 其他
char* start;//指向结构体的开头
char* pos;//指向报文的有效部分
char* end;//指向结构体的结尾
.....
struct sk_buff* next;//指向下一个报文
};
DNS等应用层协议底层就是基于UDP协议的,浏览器里内置了DNS服务器的IP地址,浏览器会把域名交给DNS服务器,然后DNS服务器会给浏览器返回域名对应的IP地址,这样浏览器才能去访问对应的网址