【Linux】UDP/TCP协议

我们知道UDP/TCP这两个协议是在传输层上面的,传输层的主要功能是实现分布式进程通信。
下面我们来分别介绍一下UPD和TCP协议
一、UDP又叫用户数据报协议
UDP协议端格式:
这里写图片描述
解释说明:
16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度
如果校验和出错,直接丢弃
UDP的特点:
UDP的过程类似于寄信
(1)无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接
(2)不可靠:没有确认机制,没有重传机制;如果因为网络故障无法发到对方UDP协议也不会给应用层返回任何错误信息
(3)面向数据报:不能灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文,UDP原样发送,既不会拆分也不会合并:
举个例子:
用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用一次recvfrom,接受100个字节;而不能循环调用10次recvfrom,每次接收10个字节
UDP的缓冲区
(1)UDP没有真正意义上的缓冲区,调用sendto会直接发送给内核,由内核将数据传与网络层协议进行后续的传输动作
(2)UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的数据就会被丢弃
UDP的socket;
UDP的socket既能读也能写,这个概念叫做全双工
UDP使用注意事项:
我们注意到,UDP首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64k(包含UDP首部)
然而64k在当今的互联网环境中,是一个非常小的数字
如果我们需要传输的数据超过64k,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
基于UDP的应用层协议有:
(1)NFS:网络文件系统
(2)TFTP:简单文件传输协议
(3)DHCP:动态主机配置协议
(4)BOOTP:启动协议(用于无盘设备启动)
(5)DNS:域名解析协议
当然也包括你自己写UDP程序时自定义的应用层协议:
二、TCP全称叫做传输控制协议。人如其名,要对数据的传输进行一个详细的控制
TCP协议格式:
这里写图片描述
解释说明:
(1)源/目的端口号:表示数据是从哪个进程来,到哪个进程去
(2)32位序号/32位确认号
(3)4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15*4=60
(4)6位标志位
【1】URG:紧急指针是否有效
【2】ACK:确认号是否有效
【3】PSH:提示接收端应用程序立刻从TCP缓冲区把数据都走
【4】RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段
【5】SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
【6】FIN:通知对方,本端要关闭了,我们称携带FIN标识的称为结束报文段
(5)16位窗口大小
(6)16位校验和:发送端填充,CRC校验,接收端校验不通过,则认为数据有问题。此处的校验和不光包含TCP首部,也包括TCP数据部分
(7)16位紧急指针:标识哪部分数据是紧急数据
(8)40字节头部选项

连接管理机制:
正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
这里写图片描述
下面对服务器和客户端的状态转换情况进行说明:

服务器状态转化:
(1)[CLOSED->LISTEN]服务器端调用listen进入listen状态,等待客户端连接
(2)[LISTEN->SYN_RCVD],一直监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文
(3)[SYN_RCVD->ESTABLIAHED]服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,就可以进行读写数据了
(4)[ESTABLISHED->CLOSE_WAIT]当客户端主动关闭连接(调用close),服务器收到结束报文段,服务器返回确认报文段进入CLOSE_WAIT
(5)[CLOSED_WAIT->LAST_ACK]进入CLOSE_WAIT说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
(6)[LAST_ACK->CLOSED]服务器收到了对FIN的ACK,彻底关闭连接
客户端状态转化:
(1)[CLOSED->SYN_SENT]客户端调用connect发送同步报文段
(2)[SYN_SENT->ESTABLISHED]connect调用成功,则进入ESTABLISHED状态,开始读写数据
(3)[ESTABLISHED->FIN_WAIT_1]客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1
(4)[FIN_WAIT_1->FIN_WAIT_2]客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段
(5)[FIN_WAIT_2->TIMW_WAIT]客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK
(6)[TIME_WAIT->CLOSED]客户端要等待一个2MSL(报文最大生存时间)的时间才会进入CLOSED状态
对于TIME_WAIT状态的理解:
经过试验,我们先启动server,然后启动client,然后使用Ctr1l-C使server终止,这时马上在运行就会报错,错误就是:

bind error:Address already in use

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。
(1)TCP协议规定,注定关闭连接的一方要处于TIME_WAIT状态,等待两个MSL的时间,才能回到CLOSED状态
(2)我们使用Ctrl-C终止了server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口
(3)MSL在RFC1122中规定为两分钟,但在各操作系统的实现不同,在Centos7上默认的值是60s
(4)可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看msl的值
那么为什么TIME_WAIT的时间是2MSL呢?
MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存货2MSL的话,就能保证在两个传输方向上的尚未被接受或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到上一个进程的迟到的数据,但是TCP连接还在,任然可以重发LAST_ACK)
解决TIME_WAIT状态引起的bind失败的方法
我们知道在server的TCP断开之前不允许重新监听,在某些情况下显得不合理。
举个例子:
(1)当服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)
(2)这个时候如果由服务器主动关闭连接(比如客户端不活跃,就需要被服务器主动清理掉),就会产生大量TIME_WAIT连接
(3)由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多,导致服务器的端口不够用,无法处理新的连接
使用setsocketopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符,在server代码的socket()和bind()调用之间插入如下代码:

int opt = 1;
setsockopt(listefd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

就能有效解决上述问题。
TCP的确认应答机制:
这里写图片描述
TCP将每个字节的数据都进行了编号,即为序列号,又将每1000个分为一组,每一个ACK都带有对应的确认序号,意思是高作发送者,我已经收到了哪些数据,下一次你从哪里开始发。
TCP的超时重传机制
这里写图片描述
(1)主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B
(2)如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发
但是,主机A未收到B未来的确认应答,也可能是因为ACK丢失了
如图所示
这里写图片描述
因此主机B会收到很多重复数据,那么TCP协议需要能够识别哪些包是重复的包,并且把重复的包丢弃掉,这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果
那么·,超时的时间如何确定?
(1)最理想的情况下,找到一个最小的时间,保证“确认应答一定能在这个时间内返回”
(2)但是这个时间的长短,随着网络环境的不同,是有差异的
(3)如果超时的时间设的太长,会影响整体的重传效率
(4)如果超时时间设的太短,有可能会频繁发送重复的包
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
(1) Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍
(2)如果重发一次之后,仍然得不到应答,等待2*500ms进行重传
(3)如果仍然得不到应答,等待4*500ms进行重传,依次类推,以指数形式递增
(4)累积到一定的重传次数,TCP认为网络对端主机出现异常,强制关闭连接
TCP的滑动窗口机制
现在看这样一种传输过程
这里写图片描述
经过研究发现这样一发一收性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
这里写图片描述
上图就是滑动窗口机制的示意图:
(1)窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个段)
(2)发送前四个段的时候,不需要等待任何ACK,直接发送;
(3)收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推
(4)操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有那些数据没有应答;只有确认应答过得数据,才能从缓冲区删掉
(5)窗口越大,则网络的吞吐量越大
滑动窗口具体过程:
这里写图片描述
那么出现了丢包,如何进行重传?
第一种情况:数据已经抵达,ACK丢了
这里写图片描述
这种情况下,部分ACK丢了并不太影响,因为可以通过后续的ACK进行确认
第二种情况,数据包直接丢了
这里写图片描述
(1)当某一段报文段丢失之后,发送端会一直接收1001这样的ACK,就像在提醒发送端“我想要的是1001”一样
(2)如果发送端主机连续三次收到了同样一个“1001”这样的应答,就会将对应的数据1001-2000重新发送
(3)这个时候接收端收到了1001之后,再次返回的ACK就是7001了,(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中
这种机制被称为“高速重发控制”也叫快重传
TCP中的流量控制机制:
我们知道接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候发送端如果继续发送,就会导致丢包,继而引起丢包重传等等一系列连锁反应
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制、
(1)接收端将自己可以接收的缓冲区大小放入TCP首部中的窗口大小字段,通过ACK端通知发送端
(2)窗口大小字段越大,说明网络的吞吐量越高
(3)接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端
(4)发送端接受到这个窗口之后,就会减慢自己的发送速度
(5)如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
这里写图片描述
接收端如何把窗口大小告诉发送端呢,回忆我们的TCP首部当中有一个16位窗口字段,就是存放了窗口大小信息。
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节吗?
实际上,TCP首部40字节选项中还包含了一个扩大因子M,实际窗口的大小是窗口字段的值左移M位
TCP中的拥塞控制机制:
虽然TCP有了滑动窗口,能够高效可靠的发送大量的数据,但是如果刚开始阶段就发送大量的数据,仍然可能引发问题。
因为网络上有很多的计算机,可能当前的网络状态就已经非常拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据
这里写图片描述
(1)此处引入一个概念叫做拥塞窗口
(2)发送开始的时候,定义拥塞窗口大小为1
(3)每次收到一个ACK应答,拥塞窗口加1
(4)每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送的窗口
注意:像上面这样的拥塞窗口增长速度1,是指数级别的。“慢启动”只是指初始时慢,但是增长速度非常快
(5)为了不增长那么块,因此不能使拥塞窗口单纯的加倍
(6)此处引入一个叫做慢启动的阈值
(7)当拥塞窗口超过这个阈值的时候,不再按照指数的形式增长,而是按照线性方增长
将整个过程作图如下:
这里写图片描述
(1)当TCP开始启动的时候,慢启动阈值等于窗口最大值;
(2)在每次超时重发的时候,慢开始阈值会变成原来的一半,同时拥塞窗口置回1
少量的丢包,我们仅仅是触发超时重传,大量的丢包,我们就认为网络拥塞
当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降
拥塞控制,归根到底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案
TCP中的延迟应答机制:
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小
(1)假设接收端缓冲区为1M。一次收到了500k的数据;如果立刻应答,返回的窗口就是500k
(2)但实际上可能处理的速度更快,10ms之内就把500k数据从缓冲区消费掉了
(3)在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理古来
(4)如果接收端稍微等一会再应答,比如等200ms再应答,那么这个时候返回的窗口大小就是1M
注意:一定记得,窗口越大,网络吞吐量就越大,传输效率也就越高。我们的目标是在保证网络不拥堵的情况下,尽量提高传输效率
那么所有的包都延迟应答吗?肯定也不是。
就会有如下限制条件:
(1)数量限制:每隔N个包就应答一次
(2)时间限制:超过g最大延迟时间就应答一次
具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超过时间取200ms
这里写图片描述
TCP中的捎带应答机制
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是“一发一收”的,意味着客户端说了“How are you”,服务器也会给客户端回一个“Fine,thank you”
那么这个时候ACK就可以搭顺风车,和服务器回应的“Fine,thank you”一起回到客户端
过程如图所示:
这里写图片描述
TCP协议的特点:i
面向字节流:
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区
(1)调用write时,数据会先写入发送缓冲区中
(2)如果发送的字节数太长,会被拆分成多个TCP的数据包发出
(3)如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去
(4)接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
(5)然后应用程序可以调用read从接收缓冲区拿数据
(6)另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这样一个连接,既可以读数据,也可以写数据,这个概念叫做全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
(1)写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节
(2)读100个字节数据时,也可以不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次
TCP中的粘包问题:
(1)首先要明确,粘包问题中的“包”是指的应用层的数据包
(2)在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段
(3)站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中
(4)站在应用层角度,看到的只是一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界
(1)对于定长的包,保证每次按照固定大小读取即可;例如上面的request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)一次读取即可
(2)对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置
(3)对于变长的包,还可以在包和包之间使用明确的分隔符(应用·层协议,是程序员自己来定的,只要保证分隔符不和正文冲突即可);
对于UDP协议来说是否也存在“粘包”问题呢?
(1)使用UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层,就有很明确的数据边界
(2)站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现半个的情况
TCP异常情况:
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
机器重启:和进程终止的情况相同
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保洁定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ断线之后也会定期尝试重新连接

好了关于TCP的相关知识我们了解到这里,和其他的协议进行对比,我们发现TCP真的是太复杂了,这是因为,TCP要保证可靠性又要尽可能的提高性能
提供TCP可靠性的有:
(1)校验和
(2)序列号(按序到达)
(3)确认应答
(4)超时重发
(5)连接管理
(6)流量控制
(7)拥塞控制
提高性能的有:
(1)滑动窗口
(2)快速重传
(3)延迟应答
(4)捎带应答
其他:
定时器(超时重传定时器,保洁定时器,TIME_WAIT定时器等)
基于TCP的应用层协议有:
(1)HTTP
(2)HTTPS
(3)SSH
(4)Telnet
(5)FTP
(6)FTP
(7)SMTP
还有一些程序员写TCP程序时自己定义的应用层协议
TCP和UDP对比:
我们知道TCP是可靠连接,那么是不是TCP一定优于UDP呢?其实不然,它们不能简单地说谁好谁坏,各自在不同的应用场景下有各自的优势。
TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景
UDP英语对高速传输和实时性要求比较高的通信领域,例如早期的QQ,视频传输等,另外UDP可以用于广播
扩展:用UDP实现可靠连接:
我们知道UDP是不可靠的,但是我们知道一种已经非常成熟的可靠连接TCP
所以要实现可靠连接就尽量网TCP上去靠,比如:
(1)引入序列号,保证数据顺序
(2)引入确认应答,确保对端收到了数据
(3)引入超时重传,如果隔一段时间没有应答,就重发数据
等等

猜你喜欢

转载自blog.csdn.net/flowing_wind/article/details/80727954