山东前网站建设,网站制作将栏目分类,在线生成logo图标免费,做网站的图片字虚#x1f320; 作者#xff1a;阿亮joy. #x1f386;专栏#xff1a;《学会Linux》 #x1f387; 座右铭#xff1a;每个优秀的人都有一段沉默的时光#xff0c;那段时光是付出了很多努力却得不到结果的日子#xff0c;我们把它叫做扎根 目录 #x1f449;TCP协议 作者阿亮joy. 专栏《学会Linux》 座右铭每个优秀的人都有一段沉默的时光那段时光是付出了很多努力却得不到结果的日子我们把它叫做扎根 目录 TCP协议TCP协议段格式确认应答机制窗口大小六个标记位连接管理机制三次握手四次挥手 超时重传流量控制滑动窗口拥塞控制延迟应答捎带应答面向字节流粘包问题TCP异常情况TCP小结基于TCP应用层协议TCP与UDP的对比用UDP实现可靠传输 TCP相关实验理解listen的第二个参数 总结 TCP协议
TCP 协议Transmission Control Protocol 传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。那 TCP 协议如何对数据传输进行控制的呢接下来我们一起来探讨。
TCP协议段格式 TCP 报文由首部和数据两部分组成。首部一般由 20 到 60 字节Byte构成长度可变。其中前 20 个字节格式固定后 40 个字节为可选。
TCP 报文中每个字段的含义如下
源端口和目的端口字段源端口Source Port是源计算机上的应用程序的端口号目的端口Destination Port是目标计算机的应用程序端口号都占 16 位。序列号字段序列号Sequence Number占 32 位。它表示本报文段所发送数据的第一个字节的编号。确认号字段确认号Acknowledgment NumberACK Number占 32 位。它表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。数据偏移字段4 位首部长度数据偏移是指 TCP 报文中的有效载荷数据相对于 TCP 报文起始位置的字节偏移量占 4 位。保留字段保留Reserved占 6 位。为 TCP 将来的发展预留空间目前不需要关心该字段。标志位字段包括 URG、ACK、PSH、RST、SYN、FIN 等标志位。 URG紧急指针是否有效ACK确认号是否有效PSH提示接收端应用程序立刻从 TCP 缓冲区把数据读走RST对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段SYN请求建立连接我们把携带 SYN 标识的称为同步报文段FIN通知对方本端要关闭了我们称携带 FIN 标识的为结束报文段 窗口大小字段窗口大小Window Size占 16 位。它表示从 Ack Number 开始还可以接收多少字节的数据量也表示当前接收端的接收窗口还有多少剩余空间。TCP 校验和字段校验位TCP Checksum占 16 位。它用于确认传输的数据是否有损坏。校验和由发送端填充CRC校验接收端校验不通过则认为数据有问题. 此处的检验和不光包含 TCP 首部也包含 TCP 数据部分。紧急指针字段紧急指针Urgent Pointer仅当前面的 URG 控制位为 1 时才有意义。它指出本数据段中为紧急数据的字节数占 16 位。可选项字段选项Option长度不定但长度必须是 4 字节的整数倍。 4 位首部长度的意义 4 位首部长度是用来表示 TCP 报头的长度的也能表示数据相对于 TCP 报文起始位置的偏移量其单位是 4 字节所以其能表示的范围是 0 到 60 字节。因为 TCP 固定的报头长度是 20 字节那么 4 位首部长度的最小值就是 5 了。 TCP 是如何解包并将有效载荷数据向上交付的呢 首先提取 TCP 报文的前 20 个字节的固定报头根据固定报头中的 4 位首部长度来判断报文中是否有可选项字段如果有可选项字段也需要将其提取出来。如果 4 位首部长度等于 5则说明报文中没有可选项字段如果 4 位首部长度不等于 5则说明报文中的可选项字段占 4 位首部长度大小 * 4 - 20 个字节。将报头提取完后剩下的就是有效载荷了。那么再根据固定报头中的目的端口就可以向上交付数据给特定的进程了。 TCP 报文中是没有整个报文的大小或者有效载荷的大小那是如何得知有效载荷的大小的呢 当 TCP 报文被封装在 IP 报文中传输时IP 首部中有一个字段叫做总长度它表示整个 IP 报文的长度。由于 IP 报文包括 IP 首部和 TCP 报文因此可以通过减去 IP 首部长度来得到 TCP 报文的总长度。然后再减去 TCP 首部长度就可以得到 TCP 有效载荷的长度了。
确认应答机制
我们都知道 TCP 协议是可以保证可靠性的协议那我有两个问题就是是什么原因导致传输的不可靠的呢TCP 协议是如何可靠性的呢 是什么原因导致传输的不可靠呢 其实传输的不可靠单纯就是因为传输的距离变长了。就好比两个人相隔百米互相喊话那么这两个的通信就无法保证可靠无法保证自己说的话已经被对法接收到了。 那 TCP 协议是如何保证可靠性的呢 TCP 保证可靠性的一个重要的机制就是确认应答机制。当发送端发送数据时它会等待接收端的确认应答ACK。如果接收端成功接收到数据它会返回一个确认应答给发送端。如果发送端在一定时间内没有收到确认应答它会认为数据丢失并重新发送数据。那么这种机制就称为 TCP 协议的确认应答机制而这种机制可以保证数据的可靠传输。 深入了解 TCP 的确认应答机制 客户端向服务端请求时实际并不是只发送一个请求注只发送一个请求效率过低可能是多个请求而客户端也可能会给客户端回复多个应答。需要注意的是客户端一次向服务端发送多个请求时发送的顺序不一定是服务端接收的顺序也就是报文乱序问题。那么 TCP 是如何解决以上这两个问题的呢想要知道这个我们需要了解 TCP 报头中的序号和确认序号。 序号和确认序号 序号是发送方给每个发送的报文分配的一个编号TCP 将每个字节的数据都进行了编号用来标识该报文在数据流中的位置。接收方可以根据序号对接收到的报文进行排序以确保数据按顺序传输。注这里的报文指的是携带完整 TCP 报头的 TCP 报文。
确认序号是接收方返回给发送方的一个编号表示该编号前的的所有报文都已经接收到了也可以用来表示接收方期望接收的下一个数据包的序号。发送方可以根据确认序号判断哪些数据包已经被接收方成功接收哪些数据包需要重新发送。 通过序号和确认序号请求和应答就可以一一对应起来了。因为确认序号的含义是该序号前的所有报文都已经接收到了所以就会出现部分确认应答和不给应答两种情况。
部分应答应答假设客户端给服务端发送序号为 1000、2000 和 3000 三个报文服务端也成功接收到了这三个报文那服务端就可以给客户端回复一个确认序号为 3001 的报文表示该序号前的报文都接收到了而不需要回复 1001 或 2001 的报文这就是部分确认应答。
不给应答假设客户端给服务端发送序号为 1000、2000 和 3000 三个报文服务端只接受到序号为 1000 和 3000 的报文没有接收到序号为 2000 的报文。那么服务端只能给客户端回复一个确认序号为 10001 的报文尽管服务端已经接收到了序号为 3000 的报文这就是不给应答的情况。而这种情况就可能会涉及报文的重传了稍后会介绍到。 只用序号一个字段就可以表示序号和确认序号两个字段那么为什么只用序号一个字段呢 TCP 协议是全双工的通信发送也就是说 TCP 协议中的通信双方既可以收数据也可以发数据。那么就可能会出现这种情况服务端在给客户端应答的时候也想给客户端发送数据。发送数据就需要有序号那么这就要求了 TCP 报头中要有序号和确认序号两个字段。只有序号一个字段无法区分该序号究竟是序号还是确认序号也无法实现全双工的通信方式。
窗口大小 TCP 的接收缓冲区和发送缓冲区 TCP 本身是具有接收缓冲区和发送缓冲区的
接收缓冲区用来暂时保存接收到的数据发送缓冲区用来暂时保存还未发送的数据这两个缓冲区都是在 TCP 传输层内部实现的 TCP 发送缓冲区当中的数据是由上层应用层程序进行写入的。当上层调用 write / send 这样的系统调用接口时就会将应用层的数据拷贝到 TCP 的发送缓冲区中并不是将数据直接发送到网络中。TCP 接收缓冲区当中的数据也是有上层应用层程序来进行读取的。当上层调用 read / recv 这样的系统调用接口时就会将 TCP 接收缓冲区的数据拷贝到应用层中并不是直接从网络中获取数据。 当数据拷贝到 TCP 中的发送缓冲区中后对应的 write / send 函数就能够返回无须关系数据如何发送以及如何发送这些问题都是 TCP 协议来解决的。 TCP 发送缓冲区和接收缓冲区存在的意义 发送缓冲区和接收缓冲区的作用
数据在网络传输的过程中可能会遇到一些错误导致对端无法接收到数据这时就需要进行数据重传因此 TCP 必须提供一个发送缓冲区来暂时保存发送出去但未收到确认应答的数据。当收到对端的确认应答后发送缓冲区中的数据才能够被覆盖。接收端接收数据的速度是有限的为了保证没来得及接收的数据不会被迫丢弃因此 TCP 提供了一个接收缓冲区来保存接收到但未来得及读取的数据。因为数据的传输是非常耗时我们不能够随意丢弃经过网络传输过来的数据。 经典的生产者消费者模型
对于发送缓冲区来说上层应用不断将数据拷贝到发送缓冲区中下层网络层不断从发送缓冲区中拿出数据进行进一步的封装。在这个过程中上层应用充当着生产者的角色下层网络层充当着消费者的角色而发送缓冲区就是交易场所。对于接收缓冲区来说下层网络层不断将解包后的数据写入到接收缓冲区中上层应用不断从接收缓冲区中拿出数据进行进一步的处理。在这个过程中下层网络层充当着生产者的角色上层应用充当着消费者的角色而接收缓冲区就是交易场所。引入接收缓冲区和发送缓冲区就相当于引入了两个生产者消费者模型生产者消费者模型将上层应用与底层通行细节进行了解耦同时也支持并发来提高通信的效率。 窗口大小 当发送端将数据发送给对端时其本质就是将自己发送缓冲区中的数据发送到对端的接收缓冲区中。但是缓冲区是有大小的当接收端处理数据的速度小于发送端发送数据的数据缓冲区就有可能会被填满。这时候发送端再发送过来的数据就无法放入到接收缓冲区中导致数据丢失从而引发数据重传等连锁反应。
为了解决这个问题TCP 报头中包含了 16 位窗口大小16 位窗口大小中填充的是自己的接收缓冲区剩余空间的大小也就是通过 16 位窗口大小告知对方自己的接收能力。
接收端在对发送端发送过来的数据进行响应时可以通过 TCP 的报头中的 16 位窗口大小来告知发送端自己当前接收缓冲区剩余空间的大小。此时发送端就可以根据这个窗口大小来调整自己发送数据的速度。
窗口大小字段越大说明接收端处理数据的能力越强发送端可以适当地提高数据的发送速度。窗口大小字段越小说明接收端处理数据的能力越弱发送端需要适当地降低数据的发送速度。当窗口大小字段等于 0 时说明接收端的接收缓冲区已经没有剩余空间了发送端应该停止发送数据直到接收端处理完一些数据接收缓冲区中有空间剩余。通过 16 位窗口大小告知对方自己的接收能力这样就可以做到流量控制了。
理解本质
在进行套接字编程时我们调用 write / send 函数向套接字中写入数据时可能会因为套接字的发送缓冲区已经被写满而被阻塞住了其本质就是 TCP 中的发送缓冲区已经被写满了所以 write / send 函数就需要阻塞到发送缓冲区有足够的空间来存储数据。我们调用 read / recv 函数从套接字中读取数据时可能会因为套接字中的接收缓冲区中没有数据而被阻塞住了其本质就是 TCP 中的接收缓冲区中没有数据所以 read / recv 就需要阻塞到接收缓冲区中有一定数量的数据。调用 write / send 和 read / recv 函数会被阻塞注本质就是生产者消费者模型中的临界资源没有就绪需要阻塞等待直到条件满足。
六个标记位 为什么需要多个标记位 TCP 报文的种类是多种多样的除了正常通信的常规报文还有建立连接时发送的建立连接报文以及断开连接时发送的断开连接报文等等。服务端接收到大量的不同类型的报文时服务端就要根据报文的类型来进行相应的处理。例如服务端接收到正常通信的常规报文时会将该报文放入到接收缓冲区中等待上层应用来读取。当服务端接收到建立连接和断开连接的报文时操作系统会在 TCP 层内进行三次握手和四次挥手的动作。正是因为不同类型的报文需要对应不同的处理逻辑我们就需要通过某种策略来区分报文的类型。而 TCP 就是通过报头中的六个标记位来区分报文的类型这六个标记位都只占一个比特位。其中1 表示真0 表示假。例如如果 TCP 报头中的 ACK 的比特位为 1说明该报文是一个确认报文。 SYN SYNSynchronize Sequence Numbers表示同步序列号是建立连接时使用的握手信号。如果 TCP 报头中的 SYN 被设置为 1则说明该报文是一个请求建立连接的报文。ACK 只会在建立连接阶段被设置为 1在正常通信阶段时均被设置为 0。 FIN FINFinish是断开连接时使用的结束信号。如果 TCP 报头中的 FIN 被设置为 1则说明该报文是一个请求断开连接的报文。FIN 只会在断开连接阶段被设置为 1在正常通信阶段时均被设置为 0。 ACK ACKAcknowledge Character是确认应答的标记位该标记位被设置为 1表示对收到的报文进行确认应答。大部分的 TCP 报文中的 ACK 标记位都会被设置为 1只有建立和断开连接的报文的 ACK 标记位不会被设置为 1。因为在正常网络通信中自己发送出去的数据本身对对方发送的数据就有一定的应答能力因此大多数 TCP 报文的 ACK 都会被设置为 1。而建立和断开连接会分别因为之前没有消息需要应答和不再进行数据通信因为这两个类型的报文的 ACK 标记位都不会被设置为 1。注除了有些报文可能是 FIN ACK 报文。 RST RSTReset是用于非正常关闭连接的标记位该标记位被设置为 1 时表示该 TCP 连接出现异常需要关闭该连接。在 TCP 连接中RST 数据包通常是由接收方发送的用于表示接收方无法处理接收到的数据包或者表示接收方已经关闭了连接。当发送方收到 RST 数据包时会立即关闭连接并且不会发送任何数据。进行三次握手时如果客户端收到服务端发送的 SYN ACK 数据包后向服务端发送 ACK 数据包并认为连接已经建立好了。但是 ACK 数据包丢失了服务端无法接收到 ACK 数据包。那么服务端就会认为自己发送的 SYN ACK 数据包丢失了需要进行重传。这就会存在一种情况服务端还没来得及进行 SYN ACK 数据包的重传客户端就向服务端发送数据而服务端会认为怎么连接还没有建立好就给我发送数据呢这时服务端会向客户端发送 RST 数据包表示关闭该连接并重新进行三次握手建立连接。RST 攻击攻击者可以伪造一个 TCP 数据包将 RST 标志位设置为 1然后发送给另一端这样就可以终止连接。攻击者可以通过这种方式来终止正常的连接从而使另一端无法正常工作。 PSH PSHPush是用来告知对方是否立即读取缓冲区的数据的标记位如果 PSH 标记位被设置为 1则表示发送端要求接收端立即读取缓冲区中的数据。接收端给发送端应答时会在 TCP 报头中填充窗口大小告诉发送端自己接收缓冲区剩余空间的大小。如果窗口大小为 0则说明接收缓冲区没有剩余空间那么发送端会过一段时间再给接收端发送一个 TCP 报头来得知接收端接收缓冲区剩余空间的大小。如果缓冲区剩余空间多次为 0发送端会发送 PSH 数据包要求接收端立即从接收缓冲区中读取数据。 URG 双方使用 TCP 协议进行网络通信时TCP 协议是保证报文按序发送和按序到达的即便到达接收端的接收缓冲区的顺序和发送时的顺顺序不一样也可以根据序列号来排序从而实现按序到达。
一般情况下TCP 按序到达是我们所希望的要求接收方从接收缓冲区中按序读取数据。但是有些特殊情况发送方会给接收方发送一些紧急数据要求接收方优先读取这些数据。那该怎么办呢 此时就需要用到 TCP 报头中的 URG 标记位和 16 位紧急指针了。
URGUrgent是标记紧急指针是否有效的标记位如果 URG 标记位被设置为 1则说明该报文的紧急指针有效该报文是紧急报文。16 位紧急指针的大小表示紧急数据在有效载荷中的偏移量紧急指针所指向的位置的一个字节的数据就是紧急数据需要上层优先读取该数据。
连接管理机制 如何理解连接 TCP 协议是一种面向连接的可靠的通信协议。使用 TCP 协议通信前客户端和服务器之间需要建立一个连接并维护这个连接的状态才能进行数据的传输。
客户端和服务端使用 TCP 协议通信前需要建立连接是因为 TCP 的各种可靠性如超时重传、流量控制和拥塞控制等都是建立在连接的基础之上的。因此保证传输数据的可靠性的前提就是先建立号连接。
每个客户端将来都有可能连接同一个服务端那么服务端中一定会存在大量的连接此时操作系统就需要对这些链接进行管理。
操作系统通过先描述再组织的方式来管理连接。在操作系统内部里一定会有一个描述连接的结构体结构体有着各种字段来描述着连接的各种属性最终所有定义出来的连接结构体会以某种数据结构的管理起来。此时操作系统对连接的管理就转换成对该数据结构的增删查改了。建立连接实际上就是在操作系统内部用该结构体定义一个结构体变量然后填充该结构体变量的各种属性字段然后将变量插入到数据结构中。断开连接实际上将该连接对应的结构体变量从数据结构中移除然后释放该连接占用的各种资源。操作系统对连接进行管理是有成本的这个成本包括管理连接的时间成本和存储连接的空间成本。
三次握手 三次握手的过程 客户端和服务端进行 TCP 通信前需要建立好连接而建立的连接的过程就称之为三次握手。 以客户端和服务端为例客户端想要和服务端进行通信。客户端主动向服务端发起建立连接的请求然后客户端和服务端的 TCP 层就自动三次握手建立好连接。
第一次握手客户端发送一个 SYN 标记位为 1 的数据包给服务端其中包含了客户端随机生成的初始序列号。第二次握手服务端收到客户端的 SYN 数据包后向客户端发送一个 SYN 和 ACK 标记位都为 1 的数据包表示确认收到了客户端的 SYN 数据包并同意建立连接。SYN ACK 数据包中包含了服务器随机生成的初始序列号和确认序号客户端的初始序列号 1。第三次握手客户端收到服务端的 SYN ACK 数据包后会向服务端发送一个 ACK 标记位为 1 的数据包表示确认收到了服务端的 SYN ACK 数据包并且客户端认为建立连接成功ACK 数据包中包含了确认序号服务端的初始序列号 1。三次握手的状态变化 客户端向服务器发送 SYN 包客户端进入 SYN_SENT 状态。服务器收到 SYN 包后向客户端发送SYN ACK 包服务器进入 SYN_RCVD 状态。客户端收到 SYN ACK 包后向服务器发送 ACK 包客户端进入 ESTABLISHED 状态。服务器收到 ACK 包后服务器进入 ESTABLISHED 状态。
三次握手完成后客户端和服务端之间的连接就建立起来了从此客户端和服务端就可以通过该连接进行通信直至其中一方断开连接。 三次握手一定能保证成功吗 三次握手不一定能够保证成功因为在网络传输中可能会出现各种各样的问题如网络延迟、丢包、服务端关机等等。以第三次握手为例如果服务端发出的 SYN ACK 数据包超过一段的时间没有收到应答服务端会认为该数据包丢失并进行数据包重传。重传次数根据 /proc/sys/net/ipv4/tcp_synack_retries 来指定默认是 5 次。如果重传次数超过了这个值服务端就会认为连接建立失败关闭连接。 为什么是三次握手呢而不是一次、两次、四次等次数呢 如果是一次握手的话服务端接收到客户端的 SYN 包就认为建立连接成功的话这样会带来 SYN 洪水攻击问题。SYN 洪水攻击是攻击者向被攻击主机发送大量的伪造的 TCP 连接请求从而使得被攻击主机服务器的资源耗尽CPU 满负荷或内存不足的攻击方式。如果是两次握手的话也无法避免 SYN 洪水攻击。因为服务端收到客户端的 SYN 数据包后会给客户端发送 SYN ACK 数据包但服务端无法保证该数据包一定能够被客户端接收到所以服务端只能在发出 SYN ACK 数据包后就把连接建立好。如果黑客向服务端发送大量的 SYN 数据包服务端就只能把连接建立好进而导致资源耗尽。如果采取的是一次握手和两次握手服务端中肯定会存在大量的连接而客户端可能没有什么连接这样会存在 SYN 洪水攻击。而如果采取的是三次握手只有当服务端接收到客户端的 ACK 数据包才会进行连接的建立而客户端在发出 ACK 数据包时就把连接建立好了。此时如果客户端对服务端进行 SYN 洪水攻击那么客户端中也会存在大量的连接它也是要付出代价的。三次握手可以将连接建立异常的成本嫁接到客户端一定程度上保证了服务端的安全。除了这个作用三次握手还有验证全双工的作用。客户端通过发送 SYN 数据包和接收 SYN ACK 数据包来证明自己即能收也能发服务端通过发送 SYN ACK 数据包和接收 ACK 数据包来证明自己即能收又能发而一次握手和两次握手都无法验证 TCP 是全双工的。偶数次握手会将连接建立异常的成本嫁接到服务端因此不会采取偶数次的握手。而三次以上的奇数次握手会浪费时间和资源因为三次握手已经能够将连接建立成功了。
四次挥手
客户端和服务端结束通信时需要断开连接断开连接的过程就是四次挥手。 以客户端主动断开连接为例 第一次挥手客户端主动向服务端发送 FIN 报文请求断开连接表明客户端不会再向服务端发送数据了但可以接收服务端发送过来的数据。 第二次挥手服务端接收到客户端发送的 FIN 报文后会给客户端发送 ACK 报文表明服务端收到了客户端的 FIN 报文。而服务端可能还有数据需要进行处理和发送连接并没有真正关闭。 第三次挥手服务端处理完数据后便向客户端发送 FIN 报文。 第四次挥手客户单收到服务端的 FIN 报文后向服务端发送 ACK 报文表明确认关闭连接。服务端收到该 ACK 报文时也就关闭了连接。 注如果收到客户端的 FIN 报文时服务端没有数据需要进行处理那么 ACK 和 FIN 会在同一个报文中同时设置为 1此时四次挥手就变成了三次挥手。 四次挥手的状态变化 客户端向服务端发送 FIN 报文后进入 FIN_WAIT_1 状态。服务端向客户端响应 ACK 报文后立马进入 CLOSE_WAIT 状态。客户端收到服务端发送的 ACK 报文后客户端的状态会从 FIN_WAIT_1 变成 FIN_WAIT_2。服务端向客户端发送 FIN 报文后服务端的状态会从 CLOSE_WAIT 变成 LAST_ACK。客户端收到服务端发送的 FIN 报文后客户的状态会从 FIN_WAIT_2 变成 TIME_WAIT并给服务端发送 ACK 报文。服务端收到客户端发送的 ACK 报文后服务端的状态会从 LAST_ACK 变成 CLOSED。主动关闭连接的一方进行第四次挥手后需要维持 TIME_WAIT 状态一段时间这个时间的大小是 2 MSLMax Segment Lifetime, 报文最大生存时间然后才能进入 CLOSE 状态。 四次挥手一定能够成功吗 四次挥手不能够保证一定成功。四次挥手可能会失败的情况包括客户端或服务器发送的 FIN 数据包丢失客户端或服务器发送的 ACK 应答报文丢失。如果出现这些情况客户端或服务器会重传丢失的数据包直到连接断开或达到最大重传次数。 在什么情况下服务端会存在大量 CLOSE_WAIT 状态呢 服务端会在被动关闭连接的情况下在接收到 FIN 数据包但尚未发送自己的 FIN 数据包时进入 CLOSE_WAIT 状态。通常CLOSE_WAIT 状态在服务器上的停留时间应该很短。但是如果服务器上出现大量的 CLOSE_WAIT 状态那么可能意味着被动关闭的一方没有及时发出 FIN 数据包也就是说应用层没有调用 close 函数关闭文件描述符。 验证 CLOSE_WAIT 状态 Sock.hpp
#pragma once#include iostream
#include string
#include cstring
#include cerrno
#include cassert
#include unistd.h
#include memory
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h
#include ctype.hclass Sock
{
private:const static int gbacklog 20;public:Sock() {}int Socket(){int listensock socket(AF_INET, SOCK_STREAM, 0);if (listensock 0){exit(2);}int opt 1;// setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt));return listensock;}void Bind(int sock, uint16_t port, std::string ip 0.0.0.0){struct sockaddr_in local;memset(local, 0, sizeof local);local.sin_family AF_INET;local.sin_port htons(port);inet_pton(AF_INET, ip.c_str(), local.sin_addr);if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) 0){exit(4);}}int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len sizeof(src);int servicesock accept(listensock, (struct sockaddr *)src, len);if (servicesock 0){return -1;}if(port) *port ntohs(src.sin_port);if(ip) *ip inet_ntoa(src.sin_addr);return servicesock;}bool Connect(int sock, const std::string server_ip, const uint16_t server_port){struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_port htons(server_port);server.sin_addr.s_addr inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)server, sizeof(server)) 0) return true;else return false;}~Sock() {}
};Test.cc
#include Sock.hppint main()
{Sock sock;int listensock sock.Socket();sock.Bind(listensock, 8080);sock.Listen(listensock);while(true){std::string clientip;uint16_t clientport;int sockfd sock.Accept(listensock, clientip, clientport);if(sockfd 0){std::cout [ clientip : clientport ]# sockfd std::endl;;}}return 0;
}验证流程
启动服务端使用 netstat 查看服务器状态。 使用 telnet 工具充当客户端连接服务端后再使用 netstat 来查看服务器状态。 客户端输入 quit 断开连接后使用 netstat 来查看 CLOSE_WAIT 状态。 验证 TIME_WAIT 状态 #include Sock.hppint main()
{Sock sock;int listensock sock.Socket();sock.Bind(listensock, 8080);sock.Listen(listensock);while (true){std::string clientip;uint16_t clientport;int sockfd sock.Accept(listensock, clientip, clientport);if (sockfd 0){std::cout [ clientip : clientport ]# sockfd std::endl;;}sleep(10);close(sockfd);std::cout sockfd closed std::endl;}return 0;
}如果想让服务器能够立即重新启动可以使用 setsocketopt 函数的 SO_REUSEADDR 选项。SO_REUSEADDR 选项允许在同一端口上快速重新启动服务器而不必等待 TIME_WAIT 状态的套接字释放。
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);sockfd要设置选项的套接字描述符。level指定所要设置的选项的协议层常用的协议层包括SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP等通常设置为 SOL_SOCKET 表示Socket选项。optname要设置的选项名称具体的选项可以是以下任意一个 SO_REUSEADDR允许重用本地地址SO_KEEPALIVE启用 TCP 的 KeepAlive 机制。如果启用了此选项则在套接字上的连接空闲一段时间后将自动发送一个探测报文以检测对端是否仍然存在。TCP_NODELAY禁用 Nagle 算法。如果启用了此选项则当套接字收到数据时它将立即发送而不是等待大量数据一起发送。这在某些情况下可以提高性能。SO_SNDBUF设置发送缓冲区大小SO_RCVBUF设置接收缓冲区大小SO_ERROR获取socket上的错误信息SO_BROADCAST允许广播传输SO_LINGER控制关闭 socket 的行为SO_REUSEPORT允许多个进程或线程绑定同一个 IP 地址和端口号。 optval指向包含选项新值的缓冲区的指针。optlen缓冲区长度。
如果想让服务器能够立即重新启动可以设置 SO_REUSEADDR 选项并将 optval 参数设置为一个 int 类型的指针指向一个值为 1 的整数表示开启地址重用功能。代码示例如下
int optval 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, optval, sizeof(optval)) 0)
{perror(setsockopt);exit(EXIT_FAILURE);
}这将允许在服务器退出后立即重新启动而无需等待TIME_WAIT 状态的套接字释放。请注意使用 SO_REUSEADDR 选项可能会导致套接字地址重用因此应谨慎使用。 为什么要保持 TIME_WAIT 状态 2 MSL 的时间呢 MSL 是 Max Segment Lifetime报文最大生存时间报文从一端到另一端所需时间的最大值它是任何报文在网络上存在的最长时间超过这个时间报文将被抛弃。TIME_WAIT 状态保持两倍的 MSL 时间是因为防止历史连接中的数据被相同四元组源 IP、源端口、目的 IP、目的端口的连接错误的接收。如果在关闭连接之前的某个报文被网络延迟了没有送到对端可能这个报文已经被重传过了然后客户端和服务端经过一段时间后关闭连接。但是客户端和服务端立即建立了四元组相同的连接曾经被网络延迟的报文刚好达到对端且刚好在对端的接收窗口内对端会正常接收这个报文但是这个数据报文是上一个连接残留下来的这样就会导致数据错乱等严重问题。因此TIME_WAIT 状态需要至少保持 2 MSL 的时间保证历史的数据完全从网络中消消失。TIME_WAIT 状态保持两倍的 MSL 时间的另一个原因是保证被动关闭连接的一方能够正确地关闭连接。如果客户端主动关闭放最后一次 ACK 报文第四次挥手在网络中丢失了那么根据 TCP 可靠性原则服务端被动关闭方会重传 FIN 报文。假设客户端主动关闭连接放没有 TIME_WAIT 状态而是在发完最后一次 ACK 报文就直接进入 CLOSED 状态。如果该 ACK 报文丢失了服务端则重传 FIN 报文而此时客户端已经进入关闭状态了在收到服务端重传的 FIN 报文后就会回复 RST 报文。 服务端收到这个 RST 报文并将其解释为一个错误Connection Reset By Peer这对于一个可靠的协议来说不是一种优雅的断开连接的方式。为了防止以上情况的出现客户端必须等待足够长的时间确保服务端能够收到 ACK 报文。如果服务端没有收到 ACK 报文那么服务端就会重传 FIN 报文这样一去一来刚好两个 MSL 的时间。注客户端在收到服务端重传的 FIN 报文时TIME_WAIT 状态的等待施加你会被重置回 2 MSL。如果服务端重传次数超过规定的次数时服务端会自动关闭连接。 注MSL 在 RFC1122 中规定为两分钟但是各操作系统的实现不同在 Centos7 上默认配置的值是 60s可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看 MSL 的值。 超时重传 TCP 的超时重传机制是指在 TCP 协议中当发送数据的一方在规定的时间内未收到接收方的确认信号时就会触发超时重传机制。具体来说TCP 将每个发送的数据包都标记一个时间戳如果在规定的时间内未收到确认信号就会重新发送该数据包。这个时间戳是根据之前的数据包的发送时间、传输距离以及网络拥塞程度等因素计算得出的。 超时重传机制可以保证数据的可靠传输但也会影响网络的传输效率。因此TCP 协议的实现通常会根据网络状况动态地调整超时时间以达到最佳的传输效率和可靠性。 需要注意的是TCP 的超时重传机制是通过代码逻辑来实现的并不是 TCP 报头来实现的。当发送方发送数据包时会启动一个定时器如果在规定的时间内没有收到接收方的确认消息发送方会认为该数据包已经丢失并重新发送该数据包。超时重传机制的实现需要在代码中设置一个合适的超时时间以及对超时事件的处理逻辑。 超时重传的两种情况 超时重传分为两种情况一种是发送方发送的报文丢失了此时发送方在一定的时间内无法收到对应的应答会对该报文进行重传。 超时重传的另一种情况是接收方收到了发送方的报文但是接收方给发送方的应答报文丢失了此时发送方也会因为一定时间内没有收到对应的应答进而对该报文进行重传。 当出现超时重传时发送方无法判别是因为发送的报文丢失了还是因为接收方发送的应答报文丢失了。但是发送方也不关心这个只要发送方在一定的时间内没有对应的应答发送方就会对该报文进行重传。如果是因为接收方发送的应答报文丢失而引起超时重传的话那么接收方就收到多个重复的报文。但是这不需要担心可以根据报文的序列号来对报文进行去重。当发送缓冲区中的数据被发送出去后操作系统并不会立即将刚发送出去的数据从缓冲区中移除或者覆盖掉。只有当接收方回复了对应的应答后我们才能认为对方收到了该数据否则该数据必须保留在发送缓冲区中等待超时重传。 超时重传的时间 如果超时重传的时间过长会导致丢包后发送方长时间收不到对应的应答而一直在等待影响了整体重传的效率。如果超时重传的时间过短会导致发送方频繁地重传数据。因为应答报文有可能正在网络中传输并没有丢包但是超时重传的时间过短此时发送方就开始重传数据了这样会导致发送方重传了大量的重复数据浪费网络资源降低了网络传输的效率。
最理想的情况就是找到一个最小的时间保证确认应答一定能在这个时间内返回。但是这个时间是不固定的其应该根据网络状况和数据传输的要求进行调整。当网络状况好的时候超时重传的时间可以设置成短一点提高网络传输的效率而当网络状况差时超时重传的时间可以设置成长一点降低重传的概率和避免网络拥塞。
TCP 为了保证无论在任何环境下都能比较高性能的通信因此会动态计算这个最大超时时间。
Linux 中BSD Unix 和 Windows 也是如此超时以 500ms 为一个单位进行控制每次判定超时重发的超时时间都是 500ms 的整数倍。如果重发一次之后仍然得不到应答等待 2 * 500ms 后再进行重传。如果仍然得不到应答等待 4 * 500ms 进行重传。依次类推以指数的形式进行递增。当累计到一定的重传次数时TCP 认为网络或者对端主机出现异常进而强制关闭连接。
流量控制
接收端处理数据的速度是有限的如果发送端发的太快导致接收端的缓冲区被打满这个时候如果发送端继续发送就会造成丢包继而引起丢包重传等等一系列连锁反应。因此 TCP 支持根据接收端的处理能力来决定发送端的发送速度这个机制就叫做流量控制Flow Control。
接收端将自己接收缓冲区剩余空间的大小放入 TCP 报头中的窗口大小字段通过 ACK 报文通知发送端。窗口大小字段越大说明网络的吞吐量越高。接收端一旦发现自己的缓冲区快满了就会将窗口大小设置成一个更小的值通知给发送端。发送端接受到这个窗口之后就会减慢自己的发送速度。如果接收端缓冲区满了就会将窗口大小置为 0。这时发送方不再发送数据但是需要定期发送一个窗口探测数据段使接收端把窗口大小告诉发送端。还有一种策略就是接收端的缓冲区剩余空间更新后接收端会给发送端发送窗口更新通知来告知对方接收缓冲区剩余空间的大小。TCP 是全双工的可以在两个方向上进行流量控制。三次握手时会通过 TCP 报头中的窗口大小字段来告知对方自己的接收缓冲区剩余空间的大小。那么在正式进行网络通信时就已经得知对方的接收能力了可以根据对方的接收能力来发送数据。16位窗口字段最大能表示的值是 65535那么 TCP 窗口最大就是 65535 字节么并不是实际上TCP 报头 40 字节选项中还包含了一个窗口扩大因子 M实际窗口大小是窗口字段的值左移 M 位M 的取值范围是 0 到 14。 滑动窗口
我们都知道 TCP 是每发送一个数据都要进行一次应答。当上一个数据收到了应答再发送下一个数据。这样的模式有非常明显的缺点就是性能较差。当数据往返时间越长时网络的吞吐量会越低。 一次发送多条数据 双方在进行 TCP 通信时一次可以想对方发送多条数据这样可以将等待多个响应的时间重叠在一起进而提高通信效率。 需要注意的是虽然双方在进行通信时可以一次向对方发送多条数据但这样也是要考虑对方的接收能力的发送数据的总量不能超过对方接收缓冲区剩余空间的大小。
发送方给对方发送多条数据时这么多条数据当中会有部分数据是没有收到移动的。我们可以将发送缓冲区中的数据分为三部分
已经发送并且已经收到 ACK 确认的数据已经发送但未收到 ACK 确认的数据未发送的数据 如上图所示发送缓冲区第二部分数据所占的空间就是滑动窗口的空间。滑动窗口的大小是指无需等待 ACK 应答而可以继续发送数据的数量的最大值。 滑动窗口存在的最大意义就是提高通信效率
滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小之间的较小值拥塞窗口需要根网络状况有关暂时不需要考虑拥塞窗口可以认为滑动窗口的大小就等于对方窗口的大小。不考虑拥塞窗口的硬性假设滑动窗口的大小固定为 4000也就是说发送方不需要等待对方的应答而一次向其发送 4000 字节的数据。现在发送方可以直接向对方连续发送 1001 到 2000、2001 到 3000、3001 到 4000 和 4001 到 5000 这四个段的数据不需要等待对方的应答。当收到对方发送的确认序号为 2001 的报文时说明 1001 到 2000 这个段的数据已经被对方收到了那么此时滑动窗口可以向右移动。因为滑动窗口的大小固定为 4000对方窗口的大小所以 5001 到 6000 这段数据可以直接向对方发送不需要等待其他数据段的应答以此类推。滑动窗口越大则说明网络的吞吐率越高也说明对方的接收能力越好。
当发送方发送的数据陆陆续续地收到对应的应答时此时可以将应答所对应的数据段移出滑动窗口置于滑动窗口的左侧。而窗口是否会向右移动则取决于对方的接收能力窗口大小。如果对方可以接收更多的数据那么窗口可以向右移动将位于窗口右侧的数据包含进窗口中进行数据的发送。
TCP 超时重传机制要求发送缓存区暂时保存发送未被对方确认的数据而这部分数据就是在滑动窗口当中。位于滑动窗口左侧的数据都是已经被对方确认收到的数据这些数据能够被操作系统删除或者覆盖。因此滑动窗口不仅能够保证能向对方一次性发送多条数据而且还保证了数据的超时重传。
滑动窗口主要是解决数据传输的效率问题顺带保证超时重传的可靠性。 滑动窗口的本质 滑动窗口的本质就是指针或者下标而滑动窗口的移动就是指针或者下标增加。 滑动窗口一定能够向右移动吗滑动窗口如果一直向右移动会造成越界问题吗 滑动窗口不一定能够向右移动。因为滑动窗口的大小是受对方窗口大小的限制的如果对方应用层没有从接收缓冲区中拿出数据这样对方的窗口会越来越小因此滑动窗口会越来越小无妨向右移动。
滑动窗口并不一定是整体向右移动的可能是窗口的左边界进行移动而右边界不移动因为对方的窗口大小是不固定的随时在变化所以滑动窗口的大小也不是固定的也是随时都在变化。
滑动窗口一直向右移动并不会造成越界问题。因为发送缓存区是被看成环形队列的当滑动窗口向右移动时如果超出了缓冲区的范围那么就会进行模除运算重新回到缓冲区的起始位置进行向右移动。 滑动窗口的大小可以为零吗 滑动窗口的大小可以为零。当对方一直不从接收缓冲区中拿取数据会导致发送方的滑动窗口越来越小直至为零。 丢包问题 情况一数据已经被接收方接收但是 ACK 丢失了 这种情况下部分 ACK 丢了并不要紧因为可以通过后续的 ACK 进行确认。
情况二数据直接丢失了未被接收方接收到 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的 ACK就像是在提醒发送端我想要的是 1001 一样。如果发送端主机连续三次收到了同样一个 1001 这样的应答就会将对应的数据 1001 - 2000 重新发送。这个时候接收端收到了 1001 之后再次返回的 ACK 就是7001了因为 2001 - 7000 的数据接收方之前就已经收到了被放到了接收的接收缓冲区中。
这种重传机制就是快重传机制。快速重传机制的实际方式是当发送方收到三个相同的 ACK 时就认为是前一个数据包已经丢了并立即重传。这个机制提高重传的效率。 快重传 VS 超时重传 快重传能够进行数据的快熟重传当发送方连续收到三个相同的 ACK 时就会触发快重传机制。而快重传机制不需要向超时重传那样通过设置重传定时器在一定的时间后进行重传。虽然快重传机制能够实现数据的快速重传但是触发这个机制是有条件的就是连续收到三个相同的 ACK。这也是快重传无法取代超时重传的原因超时重传也是一种兜底的策略。如果接收方发送的 ACK 报文一直丢失的话就无法触发快重传了这时候就需要超时重传。
拥塞控制
双方通过 TCP 进行通信时出现丢包问题是非常正常的此时就通过超时重传或者快重传进行数据的重传。但是如果双方进行通信时出现了大量的丢包此时就不能认为这是正常现象了。
TCP 协议不仅考虑到了两个主机端到端的可靠性问题还考虑了网络状态的问题。
当出现大量丢包问题时就可能出现网络拥塞问题。当出现网络拥塞问题就不能立即将这些数据进行重传。因为一个网络是多个主机进行共用的并且这样主机使用的都是 TCP / IP 协议你重传了那别的主机要不要重传呢。所以当网络出现问题时不应该再向网络中发送大量数据。
网络拥塞影响的不只是一台主机影响的是该网络下的所有主机此时所有使用 TCP 协议的主机都需要执行拥塞避免算法来缓解网络拥塞的状态使得网络状态慢慢得以恢复。 拥塞控制 虽然 TCP 协议有了滑动窗口这个大杀器能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据仍然可能引发问题。因为网络上有很多的计算机可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下贸然地发送大量的数据是很有可能引起雪上加霜的。
TCP 协议引入了慢启动机制先发少量的数据探探路摸清当前的网络拥堵状态再决定按照多大的速度传输数据。 为了解决网络拥塞问题就引入了一个概念拥塞窗口。
拥塞窗口是可能引起网络拥塞的上限值如果一次性发送的数据量超过了拥塞窗口的大小就有可能引起网络拥塞。刚开始发送数据时拥塞窗口的大小被定义成 1,。每收到一个 ACK 应答拥塞窗口的大小就增加一。每次发送数据的视乎会将拥塞窗口的大小和接收方主机返回的窗口大小进行比较取较小值作为滑动窗口的大小。
每收到一个 ACK 应答拥塞窗口的大小就增加一那么拥塞窗口的大小是以指数的形式进行增长的。指数形式增长只是初始时增长较慢也就是所谓的慢启动但是越往后增长就后越快这时候就有可能短时间内又造成了网络拥塞的问题。
为了避免短时间内有造成网络拥塞的问题那么就不能让拥塞窗口大小一直以指数的形式增长下去。此时就引入了一个慢启动的阈值当拥塞窗口的大小超过这个阈值时拥塞窗口大小就不在意指数的形式进行增长了而是以线性的形式进行增长。当 TCP 刚开始启动的时候慢启动的阈值被设置为对方窗口大小的最大值。在每次超时重传的时候慢启动的阈值会变成当前拥塞窗口大小的一般同时拥塞窗口的大小变成一重新开始增长。 上图说明
指数增长刚开始发送数据时拥塞窗口大小被设置为 1并且以指数形式进行增长。因为指数前期增长较慢可以避免网络拥塞问题。而中后期时指数增长快此时网络也恢复了可以尽快恢复双方的通信效率。线性增长慢启动的阈值初始时被设置为对方窗口大小的最大值上图中的慢启动阈值为 16。当拥塞窗口的大小增长到慢启动阈值时就不再以指数形式进行增长而采用线性增长的方式。注意线性增长阶段时还未发生网络拥塞问题。乘法减小当拥塞窗口大小增长到 24 时发生了网络拥塞问题此时慢启动阈值就会变成当前拥塞窗口大小的一半同时拥塞窗口大小变成一。然后拥塞窗口再次以指数的形式进行增长周而复始。
少量的丢包我们仅仅是触发超时重传或快重传。大量的丢包我们就认为网络拥塞。当 TCP 通信开始后网络吞吐量会逐渐上升随着网络发生拥堵吞吐量会立刻下降。拥塞控制归根结底是 TCP 协议想尽可能快的把数据传输给对方但是又要避免给网络造成太大压力的折中方案。
延迟应答
如果接收数据的主机接收到数据立刻返回 ACK 应答此时返回的窗口可能比较小。
假设接收端缓冲区为 1M一次收到了 500K 的数据。如果立刻进行应答那么返回的窗口大小就是 500K。但实际上接收方处理数据的速度可能会很快10ms 之内就可以把 500K 数据从缓冲区消费掉了。在这种情况下接收端处理还远没有达到自己的极限即使窗口再放大一些也能处理过来。如果接收端稍微等一会再应答比如等待 200ms 再应答那么这个时候接收方返回的窗口大小就是 1M。
接收方在接收到数据后并不会立即发送 ACK 应答而且是等候一段时间一般是200ms等接收方上层处理完数据后再给发送方发送一个更大的窗口大小这种机制就是 TCP 协议的延迟应答机制。延迟应答机制并不是为了保证可靠性的而是为了提高效率的一种策略。
有了延迟应答机制发送给对方的窗口大小就会越大网络吞吐量就越大传输效率就越高。延迟应答机制的目标是在保证网络不拥塞的情况下尽量提高传输效率。 需要注意的是并不是所有的数据包都可以延迟应答。
数量限制每隔 N 个包就应答一次。时间限制超过最大延迟时间就应答一次这个时间不会导致超时重传。
具体的数量和超时时间依操作系统不同也有差异。一般 N 取2超时时间取 200ms。
捎带应答
捎带应答是双方进行 TCP 通信时最常见的一种方式。TCP 协议的捎带应答机制就是发送的同一个 TCP 数据包中即包含数据又包含 ACK 应答的一种机制。
就好比主机 A 给主机 B 发送一条消息主机 B 收到该消息后就回复给出 ACK 应答。但如果主机 B 也有消息需要给主机 A 发送那么这个 ACK 应答就可以和该消息放在同一个报文中而不需要再单独发送一个 ACK 应答了。此时主机 B 发送的这个既完成了对收到的数据的应答又完成了自己数据的发送。 捎带应答机制很明显能够减小网络通信的开销因为通信双方不再单独地发送 ACK 应答了。
面向字节流
创建一个 TCP 的套接字时会在内核中创建一个发送缓冲区 和一个接收缓冲区。
调用 write 或 send 函数时数据会先考到到发送缓冲区中。如果要发送的数据的字节数太多会被拆分成多个 TCP 的数据包发出。如果要发送的数据的字节数太少就会先发送缓冲区中保存着。等到缓冲区长度差不多了或者其他合适的时机再发送出去。接收数据的时候数据也是从网卡驱动程序到达内核的接收缓冲区然后应用程序可以调用 read 或 recv 函数从接收缓冲区拿取任意字节长度的数据。TCP 的一个连接既有发送缓冲区也有接收缓冲区。那么对于这一个连接既可以读数据也可以写数据这个概念叫做全双工。
由于缓冲区的存在TCP 程序的读和写不需要一一匹配。
写 100 个字节数据时可以调用一次 write 函数写 100 个字节也可以调用 100 次 write 函数每次写一个字节。读 100 个字节数据时也完全不需要考虑写的时候是怎么写的既可以一次 read 100 个字节也可以一次 read 一个字节, 重复 100 次。
对应 TCP 来说根本就不关心发送缓冲区中存储的是什么数据在它看来就是一个个字节的数据它只需要将这些数据可靠地发送到对方的接收缓冲区中就行了。而对方的上层也不会关心接收缓冲区的数据是什么数据只需要将其一个字节一个字节地读取上来就行了。像这种不关心数据格式的数据通信流程就被称为面向字节流。
而 UDP 的面向数据报就是将上层应用层交下来的数据看做一个独立的报文既不进行拆分也不进行合并添加上 UDP 报头后向下交付给网络层处理。而上层应用层收到数据时就认为该数据是一个完整的报文可以直接对其进行处理。面向数据包就好像是我们平时收快递的样子当我们收到一个快递时我们就知道对方发送了一个快递。当我们收到五个快递时对方就发送了五个快递。这种数据通信流程就被称为面向数据报。
粘包问题 什么是粘包 首先要明确的是粘包问题中的 “包”是指的应用层的数据包。在 TCP 的协议报头中没有向 UDP 协议一样的 “报文长度” 这样的字段但是有一个序号这样的字段。站在传输层的角度TCP 协议是一个一个报文传输过来的并且按照序号排好序放在缓冲区中。但是站在应用层的角度看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据并不知道从哪个部分开始到哪个部分是一个完整的应用层数据包这就是所谓的粘包。 如何解决粘包问题呢 解决粘包问题的本质就是明确两个包之间的边界只要知道包与包之间的边界就能够正确读取一个数据包了。
对于定长的包保证每次都按固定大小读取即可。 对于变长的包可以在包头的位置约定一个包总长度的字段从而就知道了包的结束位置。比如 HTTP 报头使用 Content-Length 字段来表示正文的长度。对于变长的包还可以在包和包之间使用明确的分隔符。应用层协议可以由程序员自己来定制的只要保证分隔符不和正文冲突即可。 UDP 协议是否存在粘包问题呢 对于 UDP协议如果还没有向上层交付数据UDP 的报文长度仍然存在。同时 UDP 协议是一个一个把数据交付给应用层就有很明确的数据边界。站在应用层的角度使用 UDP 协议进行通信的时要么是收到完整的 UDP 报文要么就是没有收到 UDP 报文不会出现读取到半个 UDP 报文的情况。
因此UDP 协议是不存在粘包问题的根本原因就是 UDP 报头中有 16 位 UDP 长度字段来明确数据之间的边界。而 TCP 是基于字节流没有明确的数据边界需要应用层定制协议来明确数据与数据之间的边界。
TCP异常情况 进程崩溃 / 进程退出 当客户端和服务端正常通信时客户端进程突然崩溃了那么建立好的连接会怎么办呢
TCP 的连接信息是有内核维护的所以当客户端进程崩溃后内核需要挥手该进程的 TCP 连接资源。于是内核会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核中完成的并不需要进程的参与。所以即使客户端进程退出了还是能与服务器完成 TCP 四次挥手释放连接的。 机器重启 当客户端和服务端正常通信时客户端机器重启那么建立好的连接会怎么办呢
当客户端选择机器重启时操作系统会把正在运行的所有进程杀掉再进行机器重启那么机器重启的情况就和进程崩溃、进程退出的情况一样了。操作系统自动帮客户端与服务器进行四次挥手正确释放连接。 机器掉电 / 网线断开 当客户端和服务端正常通信时客户端机器掉电或网线断开那么建立好的连接会怎么办呢
当客户端机器掉电或网线断开时客户端的连接会自动关闭掉但是服务端是无法感觉客户端已经关闭连接了因此服务端还会保持连接。但是这个连接并不会一直保持因为 TCP 协议具有保活机制。
保活机制定义一个时间段在这个时间段内如果没有任何连接相关的活动TCP 保活机制会开始作用。每隔一个时间间隔发送一个探测报文该探测报文包含的数据非常少。如果连续发送几个探测报文都没有得到应答则认为当前连接已经死亡系统内核会将错误信息通知上层应用程序进而关闭连接。客户端长期掉线服务端会发送探测报文来检查客户端的状态。如果发送多个探测报文都没有收到应答那么服务端会将该连接进行关闭。TCP 的保活机制是基于定时器实现的如果对方能够正常应答那么定时器将会被重置。
应用层的某些协议也有一些这样的检测机制。例如 HTTP 长连接中也会定期检测对方的状态。例如 QQ在 QQ断线之后也会定期尝试重新连接。
TCP小结
TCP 协议之所以设计得这么复杂是因为既要保证可靠性同时也要尽可能地提高性能。
可靠性
校验和序列号确认应答超时重发连接管理流量控制拥塞控制
提高性能
滑动窗口快速重传延迟应答捎带应答
其他
超时重传定时器用于实现可靠传输。当一个 TCP 数据包被发送出去后如果在一定时间内没有收到对方的确认应答则会触发超时重传机制即重传该数据包。而超时时间则是通过定时器来控制的。保活定时器TCP 协议中的保活定时器用于检测 TCP 连接是否已经失效如果失效则断开连接。TIME_WAIT 定时器主动断开连接的一方需要保持 TIME_WAIT 状态 2MSL 的时间确保被动关闭连接放能够正确关闭连接。
基于TCP应用层协议
基于 TCP 协议的应用层协议有很多常见的协议如下
HTTP超文本传输协议用于万维网上的数据传输。HTTPS超文本传输安全协议基于 HTTP 协议之上的安全协议用于万维网上的数据传输。SSH安全外壳协议用于远程登录主机和文件传输Telnet远程终端协议用于远程登录主机。FTP文件传输协议用于在网络上进行文件传输。SMTP简单邮件传输协议用于电子邮件的发送。DNS域名系统用于将域名转换为 IP 地址。
当然也包括我们自己写 TCP 程序时自定义的应用层协议。
TCP与UDP的对比
TCP 协议和 UDP 协议的区别主要有一下几个方面
连接TCP 是面向连接的协议而 UDP 是无连接的协议。可靠性TCP 协议提供可靠的数据传输即数据包在传输过程中会进行确认、重传和流量控制等机制而 UDP 协议则不提供可靠性保证。传输效率由于 TCP 协议提供了许多可靠性保证机制因此其传输效率相对较低而 UDP 协议则没有这些机制因此传输效率相对较高。
TCP 协议和 UDP 协议没有明显的好坏之分我们需要根据具体的应用场景来选择合适的协议。TCP 协议和 UDP 协议的具体应用场景
TCP 协议适用于需要可靠传输保障的应用场景如文件传输、电子邮件和网页访问等等。UDP 协议适用于要求传输效率高、实时性高的应用场景如实时音视频传输、游戏直播等等。
用UDP实现可靠传输
可以参考 TCP 协议保证可靠性的做法在应用层实现类似的功能。例如
引入序列号为每个数据包分配一个唯一的序列号接收方可以使用序列号来检测数据包的丢失和顺序错误并进行必要的处理。引入确认应答发送方在发送数据包后等待一段时间如果没有收到确认响应就会认为数据包丢失然后重新发送。接收方在收到数据包后发送确认响应。引入超时重传发送方在发送数据包后启动一个计时器。如果在一定时间内未收到确认响应就认为数据包丢失然后进行重传。…
TCP相关实验
理解listen的第二个参数
accept 函数不参与三次握手的过程而是当底层建立好连接后然后 accept 从底层获取已经建立好的连接。也就是说。就算我不调用 accept 函数底层的连接也可以建立好。
那如果上层来不及调用 accept 函数并且对端还来了大量的连接请求难道所有的连接都应该先建立好吗如果上层都来不及获取连接了那么就说明服务器现在已经很繁忙了。如果现在系统还有建立连接的话这就会导致服务器的资源更加吃紧甚至挂掉。
那系统是如何解决这个问题的呢系统为 TCP 连接管理维护了两个连接队列来解决这个问题。而这两个队列不能没有也不能太长。如果没有队列的话服务器闲下来的话没有连接可以获取这样就无法使服务器的资源充分发挥处理。而如果队列太长的话对端等的时间就会变长并且这些资源可以划分给服务器这样服务器就可以处理更多的连接请求更能提高效率。
当对端来了大量的连接请求时并不是所有的连接都会建立好。如果没有超过连接队列的长度连接就会建立好在队列中等待上层获取。而如果超过连接队列的长度连接请求就会被拒绝。而连接队列的长度与 listen 函数的第二个参数有关。
现在我们把上面代码中的 gbacklog 从 20 改成 1并且服务端不进行 accept 获取新连接。如下图所示 listen 的第二个参数决定了底层全连接队列的长度其长度等于 listen 第二个参数加一而全连接队列就是用来保存处于 ESTABLISHED 状态但是上层没有调用 accept 获取的连接请求。全连接队列也被称为 accept 队列。
还有一个队列就是半连接队列用来保存处于 SYN_SENT 和 SYN_RECV 状态的连接请求。半连接队列有明显的生命周期一段时间后服务端还是没有向客户端发送 SYNACK 包那么系统将该连接请求移除半连接队列。而如果服务端收到了客户端的 ACK 包后系统会将连接请求移入到全连接队列。而全连接队列能够将连接长时间的维持在 ESTABLISHED 状态等待上层来获取连接。当全连接队列满了时就无法继续让当前连接的状态进入 ESTABLISHED 状态了。
总结 本篇博客从 TCP 协议报头讲起讲解了 TCP 协议的确认应答机制、窗口大小、六个标记位、连接管理机制、超时重传机制、流量控制、滑动窗口、拥塞控制、延迟应答、捎带应答、面向字节流、粘包问题等等。以上就是本篇博客的全部内容了如果大家觉得有收获的话可以点个三连支持一下谢谢大家啦❣️