Socket read()函数在隧道处理中不返回0的循环退出条件问题
代理服务器CONNECT隧道的无限循环退出问题
我正在为代理服务器实现CONNECT方法对应的隧道功能,当前代码可成功加载HTTPS网站,但存在无限while循环无法退出的问题,原因是Socket read()函数有时始终不返回0。
相关代码如下:
当端口为443时的处理逻辑:
if(port == 443){ // https strcpy(buffer, "HTTP/1.0 200 Connection established\r\n\r\n"); write(client, buffer, strlen(buffer)); tunnel(server, client, buffer); return; }
tunnel()函数实现:
void tunnel(int server, int client, char* buffer){ int servercon, clientcon; int x; x = fcntl(server, F_GETFL, 0); fcntl(server, F_SETFL, x|O_NONBLOCK); x = fcntl(client, F_GETFL, 0); fcntl(client, F_SETFL, x|O_NONBLOCK); while(true){ cout << "test" << endl; clientcon = read(client, buffer, BUFSIZE); write(server, buffer, clientcon); servercon = read(server, buffer, BUFSIZE); write(client, buffer, servercon); if(???) // 此处需设置退出条件 break; } close(client); close(server); }
我尝试过几种退出条件:
- servercon == 0(有时
read()永远不返回0) - servercon == -1(会出现安全连接失败)
- 添加计时器变量i,当i<100000时运行(有效但效率极低)
另外必须将Socket设置为非阻塞模式,否则程序会在read()处永久停滞,虽原因不明但该设置解决了停滞问题。
请问应如何设置正确的退出条件?
这个问题的核心是你没正确处理非阻塞Socket的read()返回值,以及没有考虑双向连接的关闭状态。非阻塞模式下read()的返回值有多种情况,不能只判断0或-1,得结合errno来分析:
先明确非阻塞read()的返回规则
- 返回值 > 0:成功读取了对应字节数的数据
- 返回值 = 0:对端已经关闭了连接(FIN包已收到)
- 返回值 = -1:此时要检查
errno:EAGAIN或EWOULDBLOCK:当前没有数据可读,不是错误,应该跳过继续循环- 其他错误码(比如
ECONNRESET、EPIPE):连接出现异常,需要退出循环
正确的退出条件逻辑
你需要同时检查客户端和服务端两个Socket的状态,并且处理read()的错误情况。另外,你现在的代码里直接把read()的返回值传给write(),如果read()返回-1(比如EAGAIN),write()会写入-1字节,这是错误的,得先过滤这种情况。
修改后的tunnel()函数逻辑大概是这样:
#include <errno.h> #include <unistd.h> void tunnel(int server, int client, char* buffer){ int servercon, clientcon; int x; bool client_closed = false; bool server_closed = false; x = fcntl(server, F_GETFL, 0); fcntl(server, F_SETFL, x|O_NONBLOCK); x = fcntl(client, F_GETFL, 0); fcntl(client, F_SETFL, x|O_NONBLOCK); while(true){ // 处理客户端到服务端的数据 if(!client_closed){ clientcon = read(client, buffer, BUFSIZE); if(clientcon > 0){ // 成功读取,写入服务端 write(server, buffer, clientcon); } else if(clientcon == 0){ // 客户端主动关闭连接 client_closed = true; // 给服务端发FIN,关闭写端 shutdown(server, SHUT_WR); } else { // 读取错误,判断是否是无数据 if(errno != EAGAIN && errno != EWOULDBLOCK){ // 真正的错误,退出 client_closed = true; shutdown(server, SHUT_WR); } } } // 处理服务端到客户端的数据 if(!server_closed){ servercon = read(server, buffer, BUFSIZE); if(servercon > 0){ // 成功读取,写入客户端 write(client, buffer, servercon); } else if(servercon == 0){ // 服务端主动关闭连接 server_closed = true; // 给客户端发FIN,关闭写端 shutdown(client, SHUT_WR); } else { // 读取错误,判断是否是无数据 if(errno != EAGAIN && errno != EWOULDBLOCK){ // 真正的错误,退出 server_closed = true; shutdown(client, SHUT_WR); } } } // 退出条件:两端都关闭,完成双向连接的终止 if(client_closed && server_closed){ break; } // 可选:添加短暂休眠,避免空转占用CPU(比计数器高效多了) usleep(1000); // 休眠1毫秒 } close(client); close(server); }
关键说明
- 分开关闭状态跟踪:分别记录客户端和服务端的连接是否关闭,因为可能一端先关闭,另一端还会发送剩余数据,不能直接退出。
- 处理
EAGAIN/EWOULDBLOCK:这两个错误码表示当前没有数据可读,不是连接故障,所以不能直接退出,应该继续循环等待。 - 调用
shutdown():当一端关闭时,要给另一端发送FIN包,告诉对方“我不再发数据了”,这样对方的read()会返回0,完成双向关闭的流程。 - 避免空转CPU:添加
usleep(1000)可以让循环在没有数据的时候休眠,不会占用过多CPU资源,比你之前的计数器方法高效太多。
另外,你之前的代码里直接把read()的返回值传给write(),如果read()返回-1(比如EAGAIN),write()会尝试写入-1字节,这是无效的甚至会触发错误,所以一定要先判断read()的返回值是否大于0再调用write()。
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

