协议
协议:通信双方需要遵守的规则
TCP协议
TCP协议(Transmission Control Protocol,传输层控制协议)是一种面向连接、可靠的、字节流的传输层协议。
面向连接
连接
TCP连接的本质是客户端和服务端各自维护一个数据结构(11个状态的状态机)和对端信息,来记录和维护这个连接的状态的。
数据结构
对端信息
- Socket:由IP地址和端口号组成
- 序列号:用来解决乱序问题
- 窗口大小:用于流量控制
如何确定一个连接
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
建立连接
三次握手之后,维护连接和对端信息,如果确认了该信息,接收端和发送端可以相互通信。
- 建立连接时为什么需要三次握手?
- 防止旧的重复连接初始化造成混乱,网络延迟的历史连接会再次建立连接
- 同步双方初始序列号,用于可靠传输
- 避免资源浪费,两次握手可能会建立多个无效的连接
- SYN攻击
攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
解决方法
- 增加半连接队列
- 减少SYN+ACK的重传次数
断开连接
四次挥手来确保双方都知道且同意对方断开连接,然后remove为对方维护的连接。
- 断开连接时为什么需要四次挥手?
- 客户端像服务端发送
FIN,表示客户都安不再发送数据但仍可以接受数据。 - 服务端收到
FIN,回复ACK报文,但仍可能需要数据处理和发送,等待服务端不再发送数据时,才发送FIN报文来同意关闭连接。
- 为什么需要等待2MSL时间?
MSL是Maximum Segment Lifetime,报文最大生存时间,报文在网络中存在超过这个时间就会被丢弃。网络中可能存在来自发送方的数据包,当发送方的数据包被接收方处理又会向对方发送响应,所以一来一回需要等待2倍的时间,这样设置意味着允许报文丢失一次,如LAST-ACK在一个MSL中丢失,重发的LAST-ACK会在第2个MSL内达到。
- 服务端是否能够主动断开连接?
主动断开连接的一方会进入TIME-WAIT状态,持续2MSL的时间,对于服务器来说,有点浪费,所以服务器一般来说不会主动断开连接。但必要情况下,服务器也可以主动断开连接。
可靠
TCP协议需要在IP的基础上构建可靠的传输层协议,而IP协议是一种无连接、不可靠的协议,那么这就需要一个复杂的机制来保障可靠性。
- 序列号和确认应答
- 超时重传,处理数据丢失的情况。发送数据时,如果超过指定时间没有收到对方的
ACK报文,就会重发该数据。 - 滑动窗口,处理确认应答效率低的问题。引入窗口,窗口大小就是无需等待确认应答,而可以继续发送数据的最大值。
- 流量控制,处理发送方处理数据超出接收方接受能力,超时重传进而导致网络流量浪费。提供让发送方根据接受方的实际接受能力控制发送的数据量的机制,即流量控制。
- 拥塞控制,避免发送方的数据填满整个网络。引入拥塞窗口,会根据网络的拥塞程度动态变化。
字节流
TCP协议是一种字节流协议,流表示发送的报文没有固定的报文边界。
底层原因
- 在发送端调用send函数,数据从用户区拷贝到了内核区,并没有进行发送。真正的发送取决于发送窗口、拥塞窗口、当前发送缓冲区大小等条件,即每次调用send函数,消息并不是作为一个整体进行发送的。
- 接收端如果不知道消息长度,即消息边界,无法读取出有效的消息。
解决方法
建立自定义协议,确定消息边界
报文解析
- 序列号,在建立连接时由计算机生成的随机数作为初始值,通过SYN报文发给对端,用于解决乱序问题。
- 确认应答号,指的下一次期望收到数据的序列号,用于解决丢包问题。
- 控制位
适用场景
- 文件传输
- 电子邮件传输
- 网页浏览
- 数据库访问
UDP协议
UDP协议(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、面向数据报的协议。
- 无连接,通信双方传输数据前不需要建立连接,也不需要维护连接状态。
- 不可靠,不提供数据报确认和重传机制,也不保证数据报的顺序性,传输过程中可能会出现丢包、重复和乱序等情况,可靠性由应用层保证。
- 面向数报,以数据报作为基本单位进行通信,每个数据报是一个独立、完整的消息。发送方的UDP对用户的报文,只添加首部后就直接交给IP层。
报文解析
- 源端口号和目标端口号,告知UDP协议应该把报文发给哪个进程
- 包长度,保存UDP首部长度和数据长度之和
- 校验和,防止收到网络传输中受损的数据报
适用场景
由于不需要建立连接和维护状态,UDP传输速度较快,适用于实时性要求较高的应用场景。
- 在线游戏
- 实时音视频,视频会议、网络直播
- 简单请求,DNS快速解析域名
Socket编程
Socket编程可用于TCP协议、UDP协议和本地不同进程间的通信。
TCP协议 socket编程
全连接队列和半连接队列
在TCP三次握手阶段,Linux内核会维护两个数据结构
- 半连接队列,又称SYN队列
- 全连接队列,又称accept队列
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
基本函数
- socket(),创建一个socket文件对象,返回引用的文件描述符。
- bind(),将IP地址和端口号与一个套接字进行关联,使其他网络设备能够通过这个IP地址和端口来进行通信。服务端必须绑定,客户端最好不要绑定(防止暴露ip)。
- connect(),建立连接,发起第一次握手,如果服务端处于监听状态listen就建立tcp连接,不然就报错。
- listen(),将文件对象中的发送缓冲区和接收缓冲区摧毁,变为全连接队列和半连接队列。connect发起第一次握手,当server收到第一次握手时,将连接放入到半连接队列中,server收到第三次握手时,将连接从半连接队列中取到全连接队列中。
- accept(),从全连接队列中取出一条连接,构建新的文件对象,分配新的文件描述符。这个文件对象有发送和接收缓冲区,和客户端进行交互。
- send(),将数据从buffer写入缓冲区,并不真正发送。
- recv(),将数据从缓冲区写入buffer,并不真正接受。
- close(),关闭套接字,套接字是用文件描述符表示的,需要及时回收。
常见问题
- 粘包问题,没有消息边界
- 原因:TCP为流式协议
- 解决方案:自定义协议,增加消息的长度
- 半包问题,自定义协议,消息内容和消息长度混淆
- 原因:recv函数并不确保接受到固定的字节数
- 解决方案:采用WAIT_ALL信号、设置循环的sendn()函数
- 死循环问题,客户端提前终止,服务端会陷入死循环。
- 原因:管道的特点,读端关闭,写端继续写,会触发SIGPIE信号,导致子进程终止。相当于sockpair管道写端关闭,epoll-wait监听管道一直是可读的,每一次读都返回0,父进程的epoll一直处于就绪状态,所以陷入死循环。
- 解决方法:设置MSG_NOSIGNAL,无视SIGPIE信号。
- 断开连接后,无法重新建立连接。
- 原因:TIMEWAIT状态避免客户端和服务端建立重复的连接。
- 解决方案:设置套接字属性为setsockpot。
UDP socket编程
UDP协议是没有连接的,所以不需要三次握手,也就没有listen、connect、accept的过程,但是UDP的交互依然需要IP和端口号,所以需要bind。不需要维护连接,也就没有发送方和接受方,只要有一个socket多台机器之间就可以进行通信,因此每一方的UDP都需要bind。另外,每次通信调用sendto、recvfrom的时候,都需要传入目标主机的IP地址和端口号。