网站建设是如何寻找客户的,产品展示网站建设,开源免费企业网站系统,企业微网站怎么建设目录 #x1f308;前言#x1f338;1、基本概念#x1f33a;2、TCP协议报文结构#x1f368;2.1、源端口号和目的端口号#x1f369;2.2、4位首部长度#x1f36a;2.3、32位序号和确认序号#xff08;重点#xff09;#x1f36b;2.4、16位窗口大小#x1f36c;2.5、… 目录 前言1、基本概念2、TCP协议报文结构2.1、源端口号和目的端口号2.2、4位首部长度2.3、32位序号和确认序号重点2.4、16位窗口大小2.5、常见的6位标记位✨2.5.1、SYN和FIN标记位三次握手和四次挥手✱2.5.2、ACK标记位确认标记位✴2.5.3、PSH标记位数据推送标记位✵2.5.4、URG标记位紧急指针标记位和16位紧急指针 2.6、三次握手和RST标记位✶2.5.5、RST标记位复位标记位 3、TCP机制3.1、确认应答机制3.2、超时重传机制3.3、连接管理机制✨3.3.1、四次挥手✨3.3.2、连接管理机制状态的变化✨3.3.3、CLOSE_WAIT状态✨3.3.3、TIME_WAIT状态 前言
这篇文章给大家带来传输层中TCP协议学习 1、基本概念 [TCP协议 – 百度百科] TCP是隶属于传输层的协议它的主要功能是实现是让应用程序之间可以相互通信 TCP全称为 “传输控制协议Transmission Control Protocol”主要对数据的传输进行一个详细的控制 TCP协议是一个可靠的确认应答机制、超时重传等等、面向连接的通信前先建立连接、基于字节流流式IO进行网络数据传输的网络通信协议 TCP在传输过程中可以正确的处理丢包、数据包乱序的异常状况还能有效的利用宽带缓解网络拥堵 2、TCP协议报文结构 TCP协议是如何封包的呢 封包的本质是将TCP报头对象拷贝到应用层协议报文的前面即可这样就完成了封包了 后续添加新的报头时只要将缓冲区的指针移动到TCP头部然后加上新报头长度的大小最后填充就行了 TCP是如何解包的呢 解包的本质是将TCP报头和TCP选项在数据包中去除即可 TCP报头中有一个4位首部长度字段它标识了这个报头的长度报头 选项只要拿到首部长度然后根据首部长度去掉选项剩下的就是有效载荷了 有效载荷包含了上层应用所需传输的数据比如HTTP请求或响应内容 TCP协议是如何进行分用的呢 TCP报头的前32位属性字段名是跟UDP一样都是源端口号和目的端口号 分用的本质是通过TCP报头里面的目的端口号字段找到应用层的具体协议并且传递给上层应用程序进行处理 注意这里没有说IP地址因为IP地址是用来找网络中唯一主机的现在已经找到了通过目的端口号就能标定主机中唯一的进程了 下图为TCP报文的组成结构里面包含了不同的属性字段 2.1、源端口号和目的端口号 源/目的端口号: 表示数据是从哪个进程来到哪个进程去 16位源端口号标识发送端主机上进行网络通信的某个进程具有唯一性 16位目的端口号标识接收端主机上进行网络通信的某个进程具有唯一性 2.2、4位首部长度
概念 TCP报头的标准长度是20个字节不包括选项字段长度 那么我们如何确定选项的大小呢 答案就是4位首部长度 4位首部长度表示该TCP头部有多少个32位bit(4字节)4位的的取值范围是[0, 15]0000 - 1111可以得出TCP报头最多有15 * 4 60个字节的数据 TCP报头标准长度为20字节所以4位首部长度的值至少为50101 我们解包时就是通过四位首部长度来提取选项字段的 2.3、32位序号和确认序号重点
TCP可靠性问题 什么是不可靠网络数据传输过程中导致丢包、数据包乱序、校验失败等问题 什么是可靠性网络传输过程中不会出现丢包、乱序、校验失败等异常问题确保数据包完整的到达对端 那么TCP是如何解决丢包问题的呢 怎么确定一个报文是丢了还是没丢呢
确认应答机制 TCP中的32位序号和32位确认序号就是为了防止丢包准备的 确认应答机制数据在网络传输过程中没有发生丢包完整的被对端收到并且得到对端的应答 确认应答机制不能保证双方的应答因为这是不可能的永远只有一条消息是没有应答的只能保证单向的应答 32位序号和确认序号 首先建立一个共识TCP进行通信时发出去的报文一定携带TCP报头哪怕不携带数据 TCP是如何实现确认应答机制的呢 答案是通过序号和确认序号实现的 实现原理发送端给对方发送消息时会携带序号字段接收端收到后回复消息会更新确认序号回复给发送端后发送端根据自己的序号和对方的确认序号可以判断数据是否应答发送端到接收端的数据 通俗的话说就是网络传输时发送端发送的数据根据自身的序号和对方回复的确认序号可以确保单端的数据是否应答 比如发送端要发送100个字节数据携带的序号是1接收端收到数据后会更新自己的确认序号为101回复给发送端后发送端看到确认序号比自己的序号大于100表示没有丢包 为什么TCP报文中需要二个不同的序号来完成确认应答机制呢⭐⭐⭐⭐⭐ 按照上面的说法一个也能实现确认应答机制为什么出现了二个不同的序号 我们都知道TCP在通信时是全双工的意味着双方都能够进行发消息和收消息
假设如下图
如果发送端发送消息接收端应答消息并且发送新的消息呢 总结 只有单个序号只能保证一端到另外一端的数据应答 如果对端应答并且携带了新的消息那么就不能保证对端到发送端的应答了 可以得出发送端序号和对端确认序号可以保证发送端到对端数据的应答对端序号和发送端确认序号可以保证对端到发送端的应答 对端给发送端发送消息也要更新响应报文的序号发送端收到报文更新确认序号然后回复给对端对端比较自己的序号和发送端序号是否符合就能判断是否应答 2.4、16位窗口大小
发送缓冲区和接收缓冲区 首先建立一个共识这里的TCP协议发送缓冲区和接收缓冲区都是在内核中定义的 TCP需要保证报文的可靠性需要发送缓冲区做各种可靠性的策略而UDP不需要保证数据完整到达所以没有发送缓冲区 缓冲区本质是一段连续的内存它可以集中的处理数据刷新减少I/O次数从而达到提高整机的效率~ 发送缓冲区当我们在应用层调用write、send系统函数向套接字写入数据时进程会从用户态变为内核态并且会将数据拷贝到内核的发送缓冲区中用户的数据拷贝到内核中 接收缓冲区当我们在应用层调用read、recv系统函数向套接字接收数据时进程会从用户态变为内核态并且将内核中接收缓冲区的数据拷贝到用户所设置的buffer中存储着内核数据拷贝到用户中 数据被拷贝到内核的缓冲区中就归OS管了OS实现了传输层和网络层用户不会再过问什么时候传输是根据指定传输层协议来确定的 注意write、send、read、recv都是面向字节流的他们本身是不携带缓冲区的函数但是它们向指定的套接字写入或接收数据函数会根据传输层不同的协议放到缓冲区或从缓冲区中读取数据 16位窗口大小 缓冲区是有大小的如果发送的数据太快或数据太大超过缓冲区大小都会导致数据被丢弃丢包那么我们就要有个字段来获取对端的缓冲区接收能力剩余空间大小 16位窗口大小用于填充缓冲区剩余空间大小的属性字段可以让发送端智能的根据对端的接收能力来动态的调整发送的速度或数据的大小 如果服务器给客户端发报文那么只能由服务器填充自己的窗口大小对方就知道自己的缓冲区接收能力了 流量控制不管是服务器给客户端发信息还是客户端给服务器发信息只要有窗口大小存在就能解决发送数据太快或太大导致对端缓冲区已经接收不了数据还一直发的问题 2.5、常见的6位标记位 概念 我们都知道TCP协议在通信前要建立连接三次握手后才能正常通信通信完后要进行断开连接四次挥手 服务器每次要处理那么多报文连接报文、通信报文、断开连接报文等需要对报文进行类别的根据不同类型的报文用不同的逻辑处理它 6位标记位就是来标记报文类型的 ✨2.5.1、SYN和FIN标记位三次握手和四次挥手
概念 SYN只要是建立连接请求的报文SYN就要被设置为1携带SYN标记位的报文称为同步报文段连接请求报文 只要是断开连接请求的报文FIN就要被设置为1携带FIN标记位的报文称为结束报文段断开连接请求报文 SYN和FIN标记位不可能被同时设置为1因为不可能同时进行连接和断开连接 ✱2.5.2、ACK标记位确认标记位
概念 ACK确认标记位表示该报文是对历史报文的确认根据确认序号来进行确认表示发送端发送的报文已经被对端收到应答报文携带ACK标记位 历史报文发送端发送数据给对端对端应答发送端的报文就是”历史发送的报文“ 一般在大部分正式通信情况下ACK都是1 ✴2.5.3、PSH标记位数据推送标记位
概念 PSH提示接收端应用程序立刻从缓冲区把数据读走 应用层中的write、recv系统函数在读取内核缓冲区时会自动判断是否存在数据如果没有数据会一直阻塞反之读取数据到用户设置的缓冲区中 TCP缓冲区中有一个接收数据的低水位线比如有100字节低水位线为20字节只要传输的数据超过20字节就会被上层给读取
假设 假设应用层一直非常的忙没有时间读取TCP接收缓冲区里面的数据 如果TCP接收缓冲区满了并且应答了一个窗口大小为0的报文那么发送端只能等待对端读取完数据才能发送 如果等待了很久对端还是没有读取数据那么发送端可以发送一个带有PSH标记位的报文给对端催促对端应用层赶紧把缓冲区数据读取完 ✵2.5.4、URG标记位紧急指针标记位和16位紧急指针
前言 报文在传输的过程中是可能乱序到达的它是不可靠其中的一种行为TCP必须让我们发的报文按序到达 如果数据必须在TCP中进行按序到达那么如果有一部分TCP报文优先级更高PSH报文但是序号比较晚就无法做到报文被优先紧急处理 TCP是根据16位序号来实现按序达到的因为序号可以确定每个报文中数据发送了多少字节比如第一个报文发送了100字节那么第二个报文序号就是101
概念 URG只要是发送紧急数据就要把URG标记位置1URG标志设置为1时TCP首部紧急指针字段才有效默认为0时紧急指针无效 紧急指针该字段中保存着一个正的偏移量通过这个偏移量和序号相加可以找到数据有效载荷中的紧急数据 紧急报文发送给对端是不会经过缓冲区保存按序等待读取的会直接交付上层读取紧急数据只有一个字节其余数据要进入接收缓冲区 2.6、三次握手和RST标记位
概念 Server是服务器Client是客户端 TCP是面向连接的协议通信前需要建立连接三次握手 原理Client向Server发送连接请求报文携带SYN标记位Server收到报文后应答连接请求报文携带SYNACK标记位Client收到报文后创建连接对象并且发送应答报文携带ACKServer收到应答报文也创建连接对象到此就完成了三次握手 只要最后一次握手Server端收到Client的应答报文Server就会创建连接对象表明建立连接成功了
注意 TCP是保证数据完整的被对端收到但是三次握手不一定会成功 第三次握手时Client发送的应答报文可能会丢包没有被Server收到Client已经处于连接成功状态但是Server还未完成连接 原理 Client与Server建立好连接后Server需要对连接进行管理因为如果来了成千上万个连接Server就会分不清谁是谁了 管理的本质就是先描述连接的属性结构体再组织高效数据结构进行增删查改 可以得出双方建立连接需要花费时间和内存的特别是Server还要管理连接对象而Client只需创建好连接对象就行了 为什么要进行三次握手呢一次二次不行吗 一次握手 一次握手一次握手是完全不行的因为极其容易受到服务器攻击SYN泛洪攻击因为一次握手只要Client给我Server发送一个连接请求报文就能完成连接的建立了 但是如果Client发送完连接请求报文后就不管了也不创建连接对象但是Server会创建连接对象并且管理起来 如果Client循环式的发送SYN报文那么Server的内存就会一下子被填满了最后发生崩溃创建连接对象要消耗内存 二次握手 二次握手跟一次握手一样不行因为二次握手是由Server端最后应答连接报文给Client意味着要先创建连接对象 如果Client无视Server发送的应答连接报文或者直接丢弃那么Server端维护的连接对象也就没有意义了白白浪费资源 如果Client还是循环的发送SYN报文那么服务器内存也会被一下子填满导致崩溃 三次握手 前面的一、二次握手都不行是因为Server端先认为自己建立连接成功创建连接对象并且管理只要Client不建立连接或忽略应答连接报文那么Server就浪费资源了 三次握手第三次握手由Server来结束握手意味只要Server收到Client的应答报文Server才会认为自己连接成功但是Client在之前就认为自己建立连接成功了 好处就是Client如果想对Server进行攻击循环发送连接报文那么Client会先建立连接而Server最后建立连接双方都消耗了OS的内存资源 三次握手在Server收到ACK报文之前都维持着一个半连接的方式只有Client认为自己连接成功只要Server收到ACK报文那么就完成了一个完整的连接 ✶2.5.5、RST标记位复位标记位
概念 RST对方要求重新建立连接携带RST标识的称为复位报文段 作用发送带有RST标记位的报文表示叫对方关闭连接并且进行重新建立连接 用途双方在建立连接失败或出现严重丢包等等时就会给对端发送带有RST标记位的报文表明要求重新建立连接或连接复位
假设 如果Client在第三次握手中发送的ACK报文没有被Server收到但是Client认为自己建立连接成功了但是Server认为还未建立连接 Client认为自己建立连接成功向Server发送数据报文Server收到报文后想着自己还没有建立连接成功说明最后一次ACK报文丢包了 Server会给Client发送的数据报文进行应答并且把应答报文中的RST标记位也置为1表示要求Client断开当前连接并且重新建立连接 3、TCP机制
3.1、确认应答机制
概念 确认应答机制主机A向主机B发送数据主机B给主机A应答并且设置ACK标记位为1 ACK标记位设置为1的意思是对历史接收的报文的应答 序号问题 缓冲区是一段线性的内存发送端将发送的数据拷贝到缓冲区中也就意味着可以像数组一样用下标来进行访问 TCP每次发送报文都会设置序号数据从哪里开始发送也就是从哪个下标开始发送每一个ACK应答报文都有对应的确认序号已经接收到了哪些数据[序号, 确认序号]发送端会根据对端应答报文中确认序号来判断下一次从哪里开始发送 如下图所示一开始序号为1从第一个字节开始发送对端收到了1k字节应答报文中确认序号更新1001收到了1k字节10001就是后续发送端从1001下标继续发送的位置。发送端看到应答报文中确认序号为1001就知道从下标为1001的数据开始再次发送 3.2、超时重传机制
概念 主机A发生数据给主机B在网络传输过程中可能因为网络拥堵等原因数据无法到达主机B说明数据丢包了 如果主机A在特定的时间间隔内没有收到主机B的应答ACK报文就会进行超时重发报文 如果主机B收到了报文但是发送的应答ACK报文丢包了怎么办 因此得出主机B会收到很多重复数据那么TCP协议需要能够识别出哪些包是重复的包并且把重复的丢弃掉 这时候我们可以利用前面提到的序列号判断序号下标是否出现冗余就可以很容易做到去重的效果 超时重传时间如何确定 最理想的情况下找到一个最小的时间保证 “确认应答报文一定能在这个时间内返回” 但是这个时间的长短随着网络环境和状态的不同是有差异的 如果超时时间设的太长会影响整体的重传效率 如果超时时间设的太短有可能会频繁发送重复的包
解决方案 TCP为了保证无论在任何环境下都能比较高性能的通信将动态计算这个最大超时时间 Linux中(BSD Unix和Windows也是如此)超时以500ms为一个单位进行控制每次判定超时重发的超时时间都是500ms的整数倍 如果重发一次之后仍然得不到应答等待 2*500ms 后再进行重传 如果仍然得不到应答等待 4*500ms 进行重传. 依次类推以指数形式递增2N * 500ms 累计到一定的重传次数TCP认为网络或者对端主机出现异常强制关闭连接 3.3、连接管理机制
在正常情况下TCP要经过三次握手建立连接四次挥手断开连接 ✨3.3.1、四次挥手
概念 注意图中的时间轴走向图中Client先提出断开连接却是在Server断开之后才断开的反之Server先提出的也是一样的 TCP首先要进行三次握手建立连接随后正常数据通信双方断开连接时要进行四次挥手 四次挥手不管是Client还是Server都要与对方断开连接双方都要调用close也就是Client要跟Server断开连接Server也要跟Client断开连接 比如我和女朋友分手了是女方先提出的我同意了. 但是我还没提出跟她分手我还可以一直骚扰她发消息直到我给她提出分手女方也同意后双方才真正的分手了
原理 CLOSED状态就是断开连接状态 Client向Server发送断开连接报文并且将状态设置为FIN_WAIT_1. Server收到报文向Client发送应答报文并且将状态设置为CLOSE_WAIT. Client收到报文将状态由FIN_WAIT_1设置为FIN_WAIT_2 Server向Client发送FIN报文状态从CLOSE_WAIT变成LAST_ACK. Client收到报文并且应答从FIN_WAIT_2状态变成TIME_WAIT等待一段时间变成CLOSED状态Server收到应答后由LAST_ACK状态变成CLOSED ✨3.3.2、连接管理机制状态的变化 服务端状态的变化三次握手 [CLOSED, LISTEN]服务器端调用listen系统函数后进入LISTEN状态监听Client连接请求状态等待客户端连接 [LISTEN, SYN_RCVD]一旦监听到Client连接请求(同步报文段 – SYN)就将该连接放入内核等待队列中并向客户端发送SYN确认报文 [SYN_RCVD, ESTABUSHED]服务端收到客户端的确认报文就进入ESTABLISHED状态可以进行读写数据了
服务端状态的变化四次挥手 [ESTABLISHED, CLOSE_WAIT]当客户端主动关闭连接(调用close)服务器会收到结束报文段FIN服务器返回确认报文段并进入CLOSE_WAIT 状态 [CLOSE_WAIT, LAST_ACK]进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据)当服务器真正调用close关闭连接时会向客户端发送FIN报文此时服务器进入LAST_ACK状态等待最后一个ACK报文 到来(这个ACK报文是确认客户端收到了FIN) [LAST_ACK, CLOSED]服务器收到了对FIN的ACK应答报文彻底关闭连接 客户端状态的变化三次握手 [CLOSED, SYN_SENT]客户端调用connect系统函数发送连接请求报文同步报文段 – SYN [SYN_SENT, ESTABLISHED]connect调用成功则进入ESTABLISHED状态, 开始正常读写数据
客户端状态的变化四次挥手 [ESTABLISHEDFIN, WAIT_1]客户端主动调用close时向服务器发送结束报文段同时进入FIN_WAIT_1状态 [FIN_WAIT_1, FIN_WAIT_2]客户端收到服务器对结束报文段的确认ACK报文则进入FIN_WAIT_2开始等待服务器的结束报文段FIN [FIN_WAIT_2, TIME_WAIT]客户端收到服务器发来的结束报文段进入TIME_WAIT状态并发出LAST_ACK最后的确认应答报文 [TIME_WAIT, CLOSED]客户端要等待一个2MSL(Max Segment Life报文最大生存时间)的时间才会进入CLOSED状态 ✨3.3.3、CLOSE_WAIT状态
概念 CLOSE_WAIT状态先发出断开连接报文FIN的一端先调用了close对端收到了FIN报文但是自己没有真正的调用close所以会一直处于CLOSE_WAIT状态但会给先断开的一端进行ACK应答让他进入FIN_WAIT_2状态 只要调用了close函数关闭套接字就会进入下一个LAST_ACK状态 使用Server基本通信程序 和 telnet指令测试 #include iostream
#include string
#include cerrno
#include cstdlib
#include cstring
#include cassert
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.husing std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip ): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd 2)close(_listensockfd);}void InitInetData(){assert(_port 1025);_listensockfd socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd 0)exit(1);sockaddr_in serverData;socklen_t len sizeof(serverData);memset(serverData, 0, len);serverData.sin_family PF_INET;serverData.sin_port htons(_port);serverData.sin_addr.s_addr _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());if (bind(_listensockfd, (const sockaddr *)serverData, len) 0)exit(3);if (listen(_listensockfd, 2) 0)exit(4);}void StartServer(){while (true){int ServerSockfd accept(_listensockfd, nullptr, nullptr);if (ServerSockfd 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc 2){std::cout Format: ./可执行程序 [ip] port std::endl;exit(8);}std::string ip;uint16_t port;if (argc 3){ip argv[1];port std::stoi(argv[2]);}else{port std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
}测试
Server./test 8080telnettelnet 127.0.0.1 8080 总结 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket套接字, 导致四次挥手没有正确完成 这是一个 BUG. 只需要加上对应的 close函数 即可解决问题 ✨3.3.3、TIME_WAIT状态 模拟TIME_WAIT状态Client先关闭随后关闭Server代码不变还是上面的 模拟TIME_WAIT状态Server先关闭随后关闭Client代码不变还是上面的 大家应该都遇到过先关闭服务器然后关闭客户端服务器再重启启动就会bind出错这是为什么呢 现在做一个测试首先启动server然后启动client然后用Ctrl-C使server终止然后用Ctrl-C使Client终止这时马上重新运行server 运行结果是bind函数出错
#include iostream
#include string
#include cerrno
#include cstdlib
#include cstring
#include cassert
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.husing std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip ): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd 2)close(_listensockfd);}void InitInetData(){assert(_port 1025);_listensockfd socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd 0)exit(1);sockaddr_in serverData;socklen_t len sizeof(serverData);memset(serverData, 0, len);serverData.sin_family PF_INET;serverData.sin_port htons(_port);serverData.sin_addr.s_addr _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());if (bind(_listensockfd, (const sockaddr *)serverData, len) 0){std::cout bind error: strerror(errno) std::endl;exit(3);}if (listen(_listensockfd, 2) 0)exit(4);}void StartServer(){while (true){int ServerSockfd accept(_listensockfd, nullptr, nullptr);if (ServerSockfd 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc 2){std::cout Format: ./可执行程序 [ip] port std::endl;exit(8);}std::string ip;uint16_t port;if (argc 3){ip argv[1];port std::stoi(argv[2]);}else{port std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
} 原理解析 虽然server的应用程序终止了但TCP协议层的连接并没有完全断开因此不能再次监听同样的server端口 上图运行结果中使用netstat指令可以看到Server还处于TIME_WAIT未关闭连接的状态 TCP协议规定主动关闭连接的一方要处于TIME_ WAIT状态等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态 MSL在RFC1122中规定为两分钟但是各操作系统的实现不同在Centos7上默认配置的值是60s 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值
[rootLinux_Study]$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60为什么是TIME_WAIT的时间是2MSL? 服务器需要处理非常大量的客户端的连接每个连接的生存时间可能很短但是每秒都有很大数量的客户端来请求 这个时候如果由服务器端主动关闭连接比如某些客户端不活跃, 就需要被服务器端主动清理掉就会产生大量TIME_WAIT连接 由于我们的请求量很大就可能导致TIME_WAIT的连接数很多每个连接都会占用一个通信五元组(源ip、源端口、目的ip、目的端口、协议) 其中服务器的ip和端口和协议是固定的如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了就会出现问题 解决方案
使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1表示允许创建端口号相同但IP地址不同的多个socket描述符
#include sys/types.h
#include sys/socket.hint setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);sockfd套接字的文件描述符socket的返回值 level选项定义的层次支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6 optname需要设置的选项选项有SO_REUSEADDR 和 SO_REUSEPORT一般设置第一个即可 optval指针指向存放选项待设置的新值的缓冲区 optlenoptval缓冲区长度 作用主要用来禁止OS的判断和算法比如服务器处于TIME_WAIT状态重新启动就要判断状态
#include iostream
#include string
#include cerrno
#include cstdlib
#include cstring
#include cassert
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.husing std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip ): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd 2)close(_listensockfd);}void InitInetData(){assert(_port 1025);_listensockfd socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd 0)exit(1);sockaddr_in serverData;socklen_t len sizeof(serverData);memset(serverData, 0, len);serverData.sin_family PF_INET;serverData.sin_port htons(_port);serverData.sin_addr.s_addr _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());int opt 1;setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, (void*)opt, sizeof(opt));if (bind(_listensockfd, (const sockaddr *)serverData, len) 0){std::cout bind error: strerror(errno) std::endl;exit(3);}if (listen(_listensockfd, 2) 0)exit(4);}void StartServer(){while (true){int ServerSockfd accept(_listensockfd, nullptr, nullptr);if (ServerSockfd 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc 2){std::cout Format: ./可执行程序 [ip] port std::endl;exit(8);}std::string ip;uint16_t port;if (argc 3){ip argv[1];port std::stoi(argv[2]);}else{port std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
}