Httpclient资源的释放与连接复用总结
最近修改同事代码时遇到一个问题,通过 httpclient 默认配置产生的 httpclient 如果不关闭,会导致连接无法释放,很快打满服务器连接(内嵌 Jetty 配置了 25 连接上限),主动关闭问题解决;后来优化为通过连接池生成 httpclient 后,如果关闭 httpclient 又会导致连接池关闭,后面新的 httpclient 也无法再请求,这里总结遇到的一些问题和疑问。
- 官网示例中的以下三个 close 分别释放了什么资源,是否可以省略,以及在什么时机调用,使用连接池时有区别么?
- 作为 RPC 通信客户端,如何复用 TCP 连接?
一、资源释放
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
try {
// do something useful
} finally {
instream.close();
}
}
} finally {
response.close();
}
// httpclient.close();
首先需要了解默认配置 createDefault
和使用了 custom 连接池(文章最后的 HttpClientUtil)两种情况的区别,通过源码可以看到前者也创建了连接池,最大连接20个,单个 host最大2个,但是区别在于每次创建的 httpclient 都自己维护了自己的连接池,而 custom 连接池时所有 httpclient 共用同一个连接池,这是在 api 使用方面需要注意的地方,要避免每次请求新建连接池、关闭连接池,造成性能问题。
The difference between closing the content stream and closing the response is that the former will attempt to keep the underlying connection alive by consuming the entity content while the latter immediately shuts down and discards the connection.
第一个 close 是读取 http 正文的数据流,类似的还有响应写入流,都需要主动关闭,如果是使用 EntityUtils.toString(response.getEntity(), "UTF-8");
的方式,其内部会进行关闭。如果还有要读/写的数据、或不主动关闭,相当于 http 请求事务未处理完成,这时通过其他方式关闭(第二个 close)相当于异常终止,会导致该连接无法被复用,对比下面两段日志。
第一个 close 未调用时,第二个 close 调用,连接无法被复用,kept alive 0。
o.a.http.impl.execchain.MainClientExec : Connection can be kept alive indefinitely
h.i.c.DefaultManagedHttpClientConnection : http-outgoing-0: Close connection
o.a.http.impl.execchain.MainClientExec : Connection discarded
h.i.c.PoolingHttpClientConnectionManager : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
第一个 close 正常调用时,第二个 close 调用,连接可以被复用,kept alive 1。
o.a.http.impl.execchain.MainClientExec : Connection can be kept alive indefinitely
h.i.c.PoolingHttpClientConnectionManager : Connection [id: 0][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely
h.i.c.DefaultManagedHttpClientConnection : http-outgoing-0: set socket timeout to 0
h.i.c.PoolingHttpClientConnectionManager : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 20]
第二个 close 是强行制止和释放连接到连接池,相当于对第一个 close 的保底操作(上面关闭了这个似乎没必要了?),结合上面引用的官方文档写到 immediately shuts down and discards the connection,这里如果判断需要 keep alive 实际也不会关闭 TCP 连接,因为通过 netstat 可以看到,第二段日志后在终端可以继续观察到连接:
# netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51003 ESTABLISHED
tcp4 0 0 127.0.0.1.51003 127.0.0.1.8080 ESTABLISHED
在 SOF 上可以搜到这段话,但是感觉和上面观察到的并不相符?
The underlying HTTP connection is still held by the response object to allow the response content to be streamed directly from the network socket. In order to ensure correct deallocation of system resources, the user MUST call CloseableHttpResponse#close() from a finally clause. Please note that if response content is not fully consumed the underlying connection cannot be safely re-used and will be shut down and discarded by the connection manager.
第三个 clsoe,也就是 httpclient.close 会彻底关闭连接池,以及其中所有连接,一般情况下,只有在关闭应用时调用以释放资源(补充:当 httpClientBuilder.setConnectionManagerShared(true)
时,并不会关闭连接池)。
二、连接复用
根据 http 协议 1.1 版本,各个 web 服务器都默认支持 keepalive,因此当 http 请求正常完成后,服务器不会主动关闭 tcp(直到空闲超时或数量达到上限),使连接会保留一段时间,前面我们也知道 httpclient 在判断可以 keepalive 后,即使调用了 close 也不会关闭 tcp 连接(可以认为 release 到连接池)。为了管理这些保留的连接,以及方便 api 调用,一般设置一个全局的连接池,并基于该连接池提供 httpclient 实例,这样就不需要考虑维护 httpclient 实例生命周期,随用随取(方便状态管理?),此外考虑到 http 的单路性,一个请求响应完成结束后,该连接才可以再次复用,因此连接池的最大连接数决定了并发处理量,该配置也是一种保护机制,超出上限的请求会被阻塞,也可以配合熔断组件使用,当服务方慢、或不健康时熔断降级。
最后还有一个问题,观察到 keepalive 的 tcp 连接过一段时间后会变成如下状态:
# netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51866 FIN_WAIT_2
tcp4 0 0 127.0.0.1.51866 127.0.0.1.8080 CLOSE_WAIT
可以看出服务器经过一段时间,认为该连接空闲,因此主动关闭,收到对方响应后进入 FIN_WAIT_2 状态(等待对方也发起关闭),而客户端进入 CLOSE_WAIT 状态后却不再发起自己这一方的关闭请求,这时双方处于半关闭。官方文档解释如下:
One of the major shortcomings of the classic blocking I/O model is that the network socket can react to I/O events only when blocked in an I/O operation. When a connection is released back to the manager, it can be kept alive however it is unable to monitor the status of the socket and react to any I/O events. If the connection gets closed on the server side, the client side connection is unable to detect the change in the connection state (and react appropriately by closing the socket on its end).
这需要有定期主动做一些检测和关闭动作,从这个角度考虑,默认配置产生的 HttpClient 没有这一功能,不应该用于生产环境,下面这个监控线程可以完成该工作,包含它的完整的 HttpUtil 从文章最后连接获取。
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(30 * 1000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
最后展示一个完整的示例,首先多线程发起两个请求,看到创建两个连接,30秒之后再发起一个请求,可以复用之前其中一个连接,另一个连接因空闲被关闭,随后最后等待 2 分钟后再发起一个请求,由于之前连接已过期失效,重新创建连接。
-
并发两个请求
16:54:44.504 [ Thread-4] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 150; total allocated: 0 of 150] 16:54:44.504 [ Thread-5] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 150; total allocated: 0 of 150] 16:54:44.515 [ Thread-5] : Connection leased: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 2 of 150; total allocated: 2 of 150] 16:54:44.515 [ Thread-4] : Connection leased: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 2 of 150; total allocated: 2 of 150] 16:54:44.517 [ Thread-5] : Opening connection {}->http://127.0.0.1:8080 16:54:44.517 [ Thread-4] : Opening connection {}->http://127.0.0.1:8080 16:54:44.519 [ Thread-4] : Connecting to /127.0.0.1:8080 16:54:44.519 [ Thread-5] : Connecting to /127.0.0.1:8080 16:54:44.521 [ Thread-5] : Connection established 127.0.0.1:52421<->127.0.0.1:8080 16:54:44.521 [ Thread-4] : Connection established 127.0.0.1:52420<->127.0.0.1:8080 .... 16:54:49.486 [ main] : [leased: 2; pending: 0; available: 0; max: 150] 16:54:49.630 [ Thread-4] : Connection can be kept alive indefinitely 16:54:49.630 [ Thread-5] : Connection can be kept alive indefinitely 16:54:49.633 [ Thread-4] : Connection [id: 0][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely 16:54:49.633 [ Thread-5] : Connection [id: 1][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely 16:54:49.633 [ Thread-4] : http-outgoing-0: set socket timeout to 0 16:54:49.633 [ Thread-5] : http-outgoing-1: set socket timeout to 0 16:54:49.633 [ Thread-4] : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 2 of 150; total allocated: 2 of 150] 16:54:49.633 [ Thread-5] : Connection released: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150] 16:54:54.488 [ main] : [leased: 0; pending: 0; available: 2; max: 150]
#netstat -n | grep tcp4 | grep 8080 tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED tcp4 0 0 127.0.0.1.8080 127.0.0.1.52420 ESTABLISHED tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 ESTABLISHED
-
下一个请求
16:55:14.489 [ Thread-6] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150] 16:55:14.491 [ Thread-6] : http-outgoing-1 << "[read] I/O error: Read timed out" 16:55:14.491 [ Thread-6] : Connection leased: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 2 of 150; total allocated: 2 of 150] 16:55:14.491 [ Thread-6] : http-outgoing-1: set socket timeout to 0 16:55:14.492 [ Thread-6] : http-outgoing-1: set socket timeout to 8000 ..... 16:55:19.501 [ main] : [leased: 1; pending: 0; available: 1; max: 150] 16:55:19.504 [ Thread-6] : Connection can be kept alive indefinitely 16:55:19.504 [ Thread-6] : Connection [id: 1][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely 16:55:19.505 [ Thread-6] : http-outgoing-1: set socket timeout to 0 16:55:19.505 [ Thread-6] : Connection released: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150] 16:55:24.504 [ main] : [leased: 0; pending: 0; available: 2; max: 150]
#netstat -n | grep tcp4 | grep 8080 tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED tcp4 0 0 127.0.0.1.8080 127.0.0.1.52420 ESTABLISHED tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 ESTABLISHED
复用了上面的连接,下面是随后逐步超时的日志。
16:55:39.513 [ main] : [leased: 0; pending: 0; available: 2; max: 150] 16:55:44.491 [ Thread-8] : Closing expired connections 16:55:44.492 [ Thread-8] : Closing connections idle longer than 30 SECONDS 16:55:44.492 [ Thread-8] : http-outgoing-0: Close connection 16:55:44.518 [ main] : [leased: 0; pending: 0; available: 1; max: 150] .... 16:56:09.535 [ main] : [leased: 0; pending: 0; available: 1; max: 150] 16:56:14.499 [ Thread-8] : Closing expired connections 16:56:14.499 [ Thread-8] : Closing connections idle longer than 30 SECONDS 16:56:14.499 [ Thread-8] : http-outgoing-1: Close connection 16:56:14.540 [ main] : [leased: 0; pending: 0; available: 0; max: 150]
分别对应状态如下,可以看到复用了 52421,随后 52420 空闲超时被回收,以及最后 52421 也被回收。
#netstat -n | grep tcp4 | grep 8080 tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 TIME_WAIT ... #netstat -n | grep tcp4 | grep 8080 tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 TIME_WAIT
-
最后一个请求后,日志省略,可以看到是新的连接 52443。
netstat -n | grep tcp4 | grep 8080 tcp4 0 0 127.0.0.1.8080 127.0.0.1.52443 ESTABLISHED tcp4 0 0 127.0.0.1.52443 127.0.0.1.8080 ESTABLISHED
文章所有演示用例和封装类链接:https://github.com/JeffreyPeng/http-client-case/
参考:
https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/fundamentals.html
https://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
https://www.baeldung.com/httpclient-connection-management
https://www.jianshu.com/p/56881801d02c
https://zhuanlan.zhihu.com/p/61423830
推荐阅读
-
Httpclient资源的释放与连接复用总结
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET