TCP协议和UDP协议

106 阅读9分钟

协议

协议:通信双方需要遵守的规则

TCP协议

TCP协议(Transmission Control Protocol,传输层控制协议)是一种面向连接、可靠的、字节流的传输层协议。

面向连接

连接

TCP连接的本质是客户端和服务端各自维护一个数据结构(11个状态的状态机)和对端信息,来记录和维护这个连接的状态的。

数据结构 image.png

对端信息

  1. Socket:由IP地址和端口号组成
  2. 序列号:用来解决乱序问题
  3. 窗口大小:用于流量控制

如何确定一个连接

TCP 四元组可以唯一的确定一个连接,四元组包括如下:

  1. 源地址
  2. 源端口
  3. 目的地址
  4. 目的端口

建立连接

三次握手之后,维护连接和对端信息,如果确认了该信息,接收端和发送端可以相互通信。

image.png

  • 建立连接时为什么需要三次握手?
  1. 防止旧的重复连接初始化造成混乱,网络延迟的历史连接会再次建立连接
  2. 同步双方初始序列号,用于可靠传输
  3. 避免资源浪费,两次握手可能会建立多个无效的连接
  • SYN攻击

攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

解决方法

  1. 增加半连接队列
  2. 减少SYN+ACK的重传次数

断开连接

四次挥手来确保双方都知道且同意对方断开连接,然后remove为对方维护的连接。 image.png

  • 断开连接时为什么需要四次挥手?
  1. 客户端像服务端发送FIN,表示客户都安不再发送数据但仍可以接受数据。
  2. 服务端收到FIN,回复ACK报文,但仍可能需要数据处理和发送,等待服务端不再发送数据时,才发送FIN报文来同意关闭连接。
  • 为什么需要等待2MSL时间?

MSL是Maximum Segment Lifetime,报文最大生存时间,报文在网络中存在超过这个时间就会被丢弃。网络中可能存在来自发送方的数据包,当发送方的数据包被接收方处理又会向对方发送响应,所以一来一回需要等待2倍的时间,这样设置意味着允许报文丢失一次,如LAST-ACK在一个MSL中丢失,重发的LAST-ACK会在第2个MSL内达到。

  • 服务端是否能够主动断开连接?

主动断开连接的一方会进入TIME-WAIT状态,持续2MSL的时间,对于服务器来说,有点浪费,所以服务器一般来说不会主动断开连接。但必要情况下,服务器也可以主动断开连接。

可靠

TCP协议需要在IP的基础上构建可靠的传输层协议,而IP协议是一种无连接、不可靠的协议,那么这就需要一个复杂的机制来保障可靠性。

  1. 序列号和确认应答
  2. 超时重传,处理数据丢失的情况。发送数据时,如果超过指定时间没有收到对方的ACK报文,就会重发该数据。
  3. 滑动窗口,处理确认应答效率低的问题。引入窗口,窗口大小就是无需等待确认应答,而可以继续发送数据的最大值。
  4. 流量控制,处理发送方处理数据超出接收方接受能力,超时重传进而导致网络流量浪费。提供让发送方根据接受方的实际接受能力控制发送的数据量的机制,即流量控制。
  5. 拥塞控制,避免发送方的数据填满整个网络。引入拥塞窗口,会根据网络的拥塞程度动态变化。

字节流

TCP协议是一种字节流协议,流表示发送的报文没有固定的报文边界。

底层原因

  1. 在发送端调用send函数,数据从用户区拷贝到了内核区,并没有进行发送。真正的发送取决于发送窗口、拥塞窗口、当前发送缓冲区大小等条件,即每次调用send函数,消息并不是作为一个整体进行发送的。
  2. 接收端如果不知道消息长度,即消息边界,无法读取出有效的消息。

解决方法

建立自定义协议,确定消息边界

报文解析

image.png

  1. 序列号,在建立连接时由计算机生成的随机数作为初始值,通过SYN报文发给对端,用于解决乱序问题。
  2. 确认应答号,指的下一次期望收到数据的序列号,用于解决丢包问题。
  3. 控制位

适用场景

  1. 文件传输
  2. 电子邮件传输
  3. 网页浏览
  4. 数据库访问

UDP协议

UDP协议(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、面向数据报的协议。

  1. 无连接,通信双方传输数据前不需要建立连接,也不需要维护连接状态。
  2. 不可靠,不提供数据报确认和重传机制,也不保证数据报的顺序性,传输过程中可能会出现丢包、重复和乱序等情况,可靠性由应用层保证。
  3. 面向数报,以数据报作为基本单位进行通信,每个数据报是一个独立、完整的消息。发送方的UDP对用户的报文,只添加首部后就直接交给IP层。

报文解析

image.png

  1. 源端口号和目标端口号,告知UDP协议应该把报文发给哪个进程
  2. 包长度,保存UDP首部长度和数据长度之和
  3. 校验和,防止收到网络传输中受损的数据报

适用场景

由于不需要建立连接和维护状态,UDP传输速度较快,适用于实时性要求较高的应用场景。

  1. 在线游戏
  2. 实时音视频,视频会议、网络直播
  3. 简单请求,DNS快速解析域名

Socket编程

Socket编程可用于TCP协议、UDP协议和本地不同进程间的通信。

TCP协议 socket编程

image.png

全连接队列和半连接队列

在TCP三次握手阶段,Linux内核会维护两个数据结构

  • 半连接队列,又称SYN队列
  • 全连接队列,又称accept队列

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

image.png

基本函数

  • socket(),创建一个socket文件对象,返回引用的文件描述符。
  • bind(),将IP地址和端口号与一个套接字进行关联,使其他网络设备能够通过这个IP地址和端口来进行通信。服务端必须绑定,客户端最好不要绑定(防止暴露ip)。
  • connect(),建立连接,发起第一次握手,如果服务端处于监听状态listen就建立tcp连接,不然就报错。
  • listen(),将文件对象中的发送缓冲区和接收缓冲区摧毁,变为全连接队列和半连接队列。connect发起第一次握手,当server收到第一次握手时,将连接放入到半连接队列中,server收到第三次握手时,将连接从半连接队列中取到全连接队列中。
  • accept(),从全连接队列中取出一条连接,构建新的文件对象,分配新的文件描述符。这个文件对象有发送和接收缓冲区,和客户端进行交互。
  • send(),将数据从buffer写入缓冲区,并不真正发送。
  • recv(),将数据从缓冲区写入buffer,并不真正接受。
  • close(),关闭套接字,套接字是用文件描述符表示的,需要及时回收。

常见问题

  • 粘包问题,没有消息边界
  1. 原因:TCP为流式协议
  2. 解决方案:自定义协议,增加消息的长度
  • 半包问题,自定义协议,消息内容和消息长度混淆
  1. 原因:recv函数并不确保接受到固定的字节数
  2. 解决方案:采用WAIT_ALL信号、设置循环的sendn()函数
  • 死循环问题,客户端提前终止,服务端会陷入死循环。
  1. 原因:管道的特点,读端关闭,写端继续写,会触发SIGPIE信号,导致子进程终止。相当于sockpair管道写端关闭,epoll-wait监听管道一直是可读的,每一次读都返回0,父进程的epoll一直处于就绪状态,所以陷入死循环。
  2. 解决方法:设置MSG_NOSIGNAL,无视SIGPIE信号。
  • 断开连接后,无法重新建立连接。
  1. 原因:TIMEWAIT状态避免客户端和服务端建立重复的连接。
  2. 解决方案:设置套接字属性为setsockpot。

UDP socket编程

image.png UDP协议是没有连接的,所以不需要三次握手,也就没有listen、connect、accept的过程,但是UDP的交互依然需要IP和端口号,所以需要bind。不需要维护连接,也就没有发送方和接受方,只要有一个socket多台机器之间就可以进行通信,因此每一方的UDP都需要bind。另外,每次通信调用sendto、recvfrom的时候,都需要传入目标主机的IP地址和端口号。