客户端TIME_WAIT的解决方法与思考

本文探讨了解决客户端TIME_WAIT状态的方法,对比了Windows和Linux环境下SO_REUSEADDR和SO_LINGER选项的不同表现,阐述了如何有效避免TIME_WAIT带来的连接复用问题。

客户端TIME_WAIT的解决方法与思考

本文目的是为了记录在工作中遇到的问题及解决方法,便于以后学习总结,文中参考了以下两篇博客的内容:


TIME_WAIT

TIME_WAIT状态是指在SOCKET连接主动断开后,客户端会进入2MSL的等待期,这个机制的目的有两个:1)保证FIN的ACK能到达;2)确保迷路数据不会影响新的连接。具体在网上很多介绍。

现象

因为工作需要,最近写了一个客户端测试程序,其中客户端需要BIND本地端口,第一次向服务端发送数据正常。连接断开后,第二次重新发送,连接失败。
测试的环境是在Windows下,看了Netstat后,发现相应socket状态为TIME_WAIT。最开始想到的方式是设置socket选项,SO_REUSEADDR尝试客户端的socket句柄复用。在修改了客户端代码后,connect时报错。
这个问题困扰了很久,后来尝试使用SO_LINGER选项(使用RST代替FIN,避免TIME_WAIT的产生),这个选项暂时解决了问题,但是仍然有问题,这个问题在后面详细说明。

思考

虽然程序暂时可用,但是为什么SO_REUSEADDR没有效果。在阅读了以上两篇博客后,才发现这么多的需要注意的东西。

SO_REUSEADDR

UNPv1中有详细的说明:
SO_REUSEADDR可以用在以下四种情况下。

  • 1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。

  • 2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP
    Alias技术的机器可以测试这种情况。

  • 3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。

  • 4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

值得注意的是在百度百科中的一段话:
- 一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用 SO_REUSEADDR 选项。

也就是说,对于此选项来说,仅对本地IP及端口有效。也就是说如果TCP五元组的对端IP及端口仍然没变,那么仍然无法摆脱TIME_WAIT对socket的影响,这也就解释了第二次connect报错的原因,因为connect的时候,涉及到了对端的IP及端口,这样五元组完全一样,这是不允许的。

WINDOWS及LINUX区别

初步测试也验证了这一说法。但是问题出现了,在Linux端测试,发现使用SO_REUSEADDR,五元组完全一致的情况下,connect没有报错。
在引用的第二篇博客也有说明,原因在于:在Linux下socket API中的socket选项SO_REUSEADDR完全不同于TCP/IPv1中描述(那么就应该不同与大部分UNIX下的功能).

在UDP下:

  • 在LINUX下, 绑定相同端口(port)不同IP不需要任何额外的操作; 而在书中指出, 绑定相同端口不同IP需要在除了第一次绑定外的所有后续绑定前声明SO_REUSEADDR, 并且在这种情况下可以绑定通配IP地址(例如绑定了192.168.1.2:12345 后使用SO_REUSEADDR就可以绑定*:12345, 而在linux这是不可行的).
  • 在LINUX下, 绑定相同端口相同IP需要在每一次绑定(包括第一次)前声明SO_REUSEADDR, 绑定后如果收到多播或者广播的数据报, 那么每一个绑定该IP和端口的socket都可以收到这个数据报, 如果是收到单播的话就只有一个程序收到这个数据报(据说是最后一个绑定该IP:port的socket); 这和书中指出的做法基本相同, 除了某些unix系统(4.4BSD)使用的是SO_REUSEPORT, 而LINUX下无此选项, 另外如果linux需要绑定某个接口的IP和通配IP和相同端口(如192.168.1.2:12345和*:12345), 那么应该每次绑定前都声明SO_REUSEADDR这个选项.

在TCP下:

  • TCP要求正在使用的IP:port(处于TIME_WAIT状态的除外)不能通过任何形式来重用, 以防止端口盗用(TCP是面向链接的协议, 通过一个socket pair四元组确定一个链接, 如果链接的一端两个socket都能绑定同一个IP:port, 那么就破坏了四元组关系, 到底哪个socket应该收到数据就无法确定). 虽然某些系统能够这样做, 但是我们不应该违反约定而去做一些不兼容的coding. LINUX不支持这种操作.
  • 对于绑定不同IP相同端口的情况, LINUX也不需要任何操作, 这种情况和UDP的第一种完全一样, 和TCP/IPv1中的区别也和UDP的第一种完全一样, 因此, 在linux下TCP不能同时绑定某个接口的IP和通配地址的相同端口.
  • 对于处于TIME_WAIT的链接的复用, TCP 要求的是可以复用构成TIME_WAIT socket pair中的本机IP:port, 当不可使用这个IP:port去与TIME_WAIT socket pair 的另外一端的IP’:port’建立链接. 而在linux下, 这么做是可行的, 只要在每次绑定之前声明SO_REUSEADDR, 那么就可以重用正在TIME_WAIT的socket pair, 反之如果没有每次绑定前都声明好SO_REUSEADDR, 那么无论如何都无法重用正在TIME_WAIT的本机IP:port(即使这个IP:port和另外一端的IP:port构成的socket pair不是TIME_WAIT的socket pair, 并且必须每次都声明SO_REUSEADDR, 即使除了第一次之外都声明了SO_REUSEADDR, 都会绑定失败); 而在TCP/IPv1中描述的是, 只要在后续的绑定前声明好SO_REUSEADDR, 那么就可以绑定正在处于TIME_WAIT的本机IP:port, 只要不要再去connect那个和本机IP:port构成TIME_WAIT socket pair的就好(如果这么做connect会失败).

SO_REUSEADDR 使用结论

也就是说LINUX的socket中SO_REUSEADDR的实现与WINDOWS及大多数UNIX不同。TCP的五元组可以完全复用。那么为了解决TIME_WAIT,使客户端可以复用同样五元组socket,不受其影响。

  • 在WINDOWS端使用SO_REUSEADDR无效,可以尝试SO_LINGER,使用RST避免TIME_WAIT。
  • 在LINUX端,SO_REUSEADDR可以正常解决TIME_WAIT问题。

另一种解决方法

SO_LINGER作用

设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
利用此选项,可以将此缺省行为设置为以下两种

  • 立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求。
  • 将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。 在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。 如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。

SO_LINGER选项使用如下结构:
typedef struct linger {
u_short l_onoff; //开关,零或者非零
u_short l_linger; //优雅关闭最长时限 } linger;

l_onoffl_lingerclosesocket行为发送队列底层行为
忽略立即返回。保持直至发送完成。系统接管套接字并保证将数据发送至对端。
非零立即返回。立即放弃。直接发送RST包,自身立即复位,不用经过2MSL状态。对端收到复位错误号。
非零非零阻塞直到l_linger时间超时或数据发送完成。(套接字必须设置为阻塞zhuan)在超时时间段内保持尝试发送,若超时则立即放弃。超时则同第二种情况,若发送完成则皆大欢喜。

总的来说,SO_LINGER选项,需要注意:

  • 服务端对RST的处理,RECV返回10054
  • 使用延时时间==0时,要考虑缓存队列是否会被放弃;
  • 使用延时时间>0时,考虑数据队列是否能发完;
<think>好的,我现在需要回答用户关于`epoll_wait`函数在Linux系统编程中的作用的问题。首先,我要回忆一下之前的对话内容,用户之前询问了关于`epoll_ctl`的作用,而我的回答详细解释了它的功能、参数、使用场景和注意事项,并引用了相关文献。现在用户转向`epoll_wait`,我需要确保回答结构清晰,符合用户的要求,同时参考之前提供的引用内容。 首先,用户希望了解`epoll_wait`的作用,所以我应该从核心功能开始阐述。根据引用[1]和[4],`epoll`机制包括三个主要函数:`epoll_create`、`epoll_ctl`和`epoll_wait`。其中,`epoll_wait`用于等待事件的发生,类似于`select`或`poll`函数,但更高效。需要强调它如何高效处理大量并发连接,这是`epoll`的优势所在。 接下来,参数解析部分需要详细说明函数的各个参数,如`epfd`、`events`数组、`maxevents`、`timeout`等。引用[4]提到了`epoll_wait`的函数原型和参数,可以引用这部分内容。同时,需要解释`timeout`的不同取值对行为的影响,比如-1表示阻塞,0表示立即返回,正数表示超时时间。 使用场景方面,可以结合用户之前的回答,提到高并发服务器中的应用,比如网络服务器如何处理多个客户端连接。边缘触发(ET)和水平触发(LT)模式的区别也需要解释,这部分在之前的回答中可能提到过,但需要在此处再次强调,特别是`epoll_wait`在不同模式下的行为差异。 性能优势部分需要对比`select`和`poll`,引用[2]和[3]指出`select`的效率问题,比如时间复杂度为O(n),而`epoll`是O(1),因此`epoll_wait`在处理大量文件描述符时更高效。这里可以引用相关部分来支持观点。 注意事项部分应该包括错误处理、事件数组的正确使用,以及文件描述符的管理。比如,确保`events`数组足够大以避免溢出,正确处理返回值,以及在ET模式下需要非阻塞IO等。引用[2]提到`epoll_create`后需要关闭句柄,可能也需要提醒用户在使用`epoll_wait`时注意资源管理。 此外,用户希望生成相关问题,我需要考虑`epoll_wait`相关的常见问题,比如`select`/`poll`的区别、ET和LT模式的选择、性能优化等。这些问题应该自然延伸自当前主题,并帮助用户进一步学习。 在组织内容时,需要遵循用户提供的格式:核心功能、参数解析、使用场景、性能优势、注意事项,最后相关问题。要使用中文,正确使用LaTeX格式,行内公式用`$...$`,独立公式用`$$...$$`,但当前问题可能不需要数学公式,所以可能用不到。但如果有涉及性能分析的部分,比如时间复杂度,可以用O(n)这样的表示。 需要确保引用正确,每个引用标识对应之前的引用内容。例如,引用[4]提到了`epoll_wait`的函数原型,引用[2]和[3]讨论了`select`的局限性,这些都应该在适当的位置引用。 最后,检查是否有遗漏的重要点,比如`epoll_wait`的返回值含义,如何处理就绪事件,以及可能的错误码。同时,是否需要代码示例来演示`epoll_wait`的使用,比如在一个事件循环中调用该函数,并处理返回的事件列表。 总结思考步骤:明确问题,分解回答结构,参考已有引用,详细解释每个部分,注意格式和引用标识,最后生成相关问题。确保回答准确、详细,符合用户的需求和之前的回答风格。</think>### epoll_wait函数在Linux系统编程中的作用详解 #### 一、核心功能 `epoll_wait` 是 Linux epoll 机制中**事件等待的核心函数**,主要用于**监控注册到 epoll 实例中的文件描述符(FD)的事件状态**,其核心作用包括: 1. **事件监听返回** 阻塞进程(或线程)直到注册的 FD 发生指定事件或超时,返回已触发的事件列表[^4]。 2. **高效事件分发** 仅返回实际触发事件的 FD,时间复杂度为 $O(1)$(活跃 FD 数量相关),可支撑数万级并发连接[^2][^3]。 3. **支持两种触发模式** 通过 `EPOLLET` 标志实现**边缘触发(Edge-Triggered, ET)**,默认采用**水平触发(Level-Triggered, LT)**,满足不同场景需求[^4]。 #### 二、参数解析 ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` 1. **epfd** 由 `epoll_create` 创建的 epoll 实例句柄,标识要监控的事件集合。 2. **events** 输出参数,指向**存放就绪事件的结构体数组**。每个 `epoll_event` 包含触发的事件类型(如 `EPOLLIN`)及用户数据(如关联的 FD)。 3. **maxevents** 指定 `events` 数组的容量,必须大于 0,避免缓冲区溢出。 4. **timeout** 超时时间(毫秒): - **-1**:阻塞直到有事件发生 - **0**:立即返回(非阻塞模式) - **>0**:等待指定时间后返回,即使无事件触发[^4] #### 三、典型使用场景 1. **网络服务器事件循环** 在事件驱动模型中,通过循环调用 `epoll_wait` 处理客户端请求: ```c while (1) { int nready = epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i = 0; i < nready; i++) { if (events[i].events & EPOLLIN) { // 处理可读事件(如接收数据) } if (events[i].events & EPOLLOUT) { // 处理可写事件(如发送数据) } } } ``` 2. **边缘触发模式(ET)** 需一次性处理完所有可用数据,通常非阻塞 I/O 结合使用: ```c event.events = EPOLLIN | EPOLLET; // 启用 ET 模式 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); ``` #### 四、性能优势 1. **高效的事件通知机制** `select`/`poll` 的 $O(n)$ 轮询不同,`epoll_wait` 直接通过内核事件表获取就绪 FD,时间复杂度为 $O(1)$[^3]。 2. **零拷贝优化** 内核通过共享内存(`mmap`)将就绪事件列表返回用户空间,避免数据复制开销[^2]。 #### 五、注意事项 1. **事件数组容量** `maxevents` 必须实际分配的 `events` 数组大小一致,否则可能导致内存越界。 2. **错误处理** 检查返回值: - **>0**:返回就绪事件数量 - **0**:超时且无事件触发 - **-1**:错误(需检查 `errno`,如 `EINTR` 被信号中断)[^4] 3. **ET模式下的非阻塞I/O** 使用 ET 模式时,必须将 FD 设为非阻塞模式,并循环读写直到返回 `EAGAIN` 或 `EWOULDBLOCK`。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值