尧图精选

Socket编程本质:TCP连接的内核级通信契约解析

🕒 发布时间:2026/9/17 1:23:15 📁 来源:尧图网络
1. 这不是“学个函数就完事”的事Socket编程的本质是构建通信契约你看到的socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()这九个函数表面是一串C语言API调用序列实际是一套网络通信的底层契约协议。它不教你怎么写网页也不告诉你怎么调API而是规定当两台机器要建立连接、交换数据、维持状态、最终断开时双方必须按什么顺序、在什么条件下、以什么方式完成每一步动作。这就像两个陌生人要在陌生城市见面——得先约定在哪条街bind()、是否开门迎客listen()、谁主动敲门connect()、谁开门放人进来accept()、进门后怎么递东西send()/recv()、中间要不要等对方select()最后怎么礼貌告别close()。漏掉任何一环见面就失败顺序错了可能直接被拒之门外。我带过几十个刚从学校出来的实习生他们背得下每个函数原型却在调试时卡在bind: Address already in use或Connection refused上一整天。问题从来不在代码语法而在于没真正理解bind()不是“把端口占住”而是向内核注册一个本地通信端点的身份声明listen()不是“开始监听”而是向内核申请一个连接队列缓冲区并声明自己愿意接受连接请求accept()更不是“接收连接”而是从已完成连接队列中取出一个已三次握手成功的连接生成一个新的、专属的文件描述符。这些函数背后没有魔法只有操作系统内核对网络协议栈的精确调度和状态机管理。你查到的那些热搜词——socket bind only one usage of each socket address、failed to connect to anthropic services、cannot connect to docker daemon——90%以上都源于对这套契约的误读或执行偏差。比如Address already in use不是端口被“占用”而是该地址端口组合已被另一个进程以SO_REUSEADDR未启用的方式独占注册Connection refused往往不是服务没启动而是客户端connect()发出SYN包后目标主机根本没有进程在对应端口上listen()导致内核直接回RST包。这些错误信息本身就在复述契约失败的环节而不是报错代码行号。所以这篇内容不是函数手册翻译而是带你站在内核视角看一次TCP连接如何从零诞生、流转、终结。你会看到socket()如何分配fd并初始化协议族结构体bind()怎样把IP端口映射到内核哈希表listen()如何设置backlog队列长度并触发状态机切换accept()在什么条件下阻塞、什么条件下返回、为什么需要单独的fd。所有操作都围绕一个核心让内核替你管理网络状态你只负责告诉它“我要做什么”而不是“怎么做到”。适合正在写网络服务、调试连接失败、或者想搞懂netstat -tuln输出含义的开发者。哪怕你用Python写Flask、用Node.js跑Express只要底层走TCP这些函数就是你代码背后的影子。2. 函数链路拆解每个调用背后的真实意图与内核动作2.1 socket()不是“创建套接字”而是向内核申请通信资源句柄socket()是整个链条的起点但它的作用常被严重低估。很多人以为它只是“创建一个socket对象”实际上它干了三件关键且不可替代的事第一分配文件描述符fd。在Linux中一切皆文件socket也不例外。socket()调用成功后返回的整数本质是进程打开文件表中的一个索引号。这个fd后续所有操作bind、connect、send都依赖它定位内核中的socket结构体。注意fd是进程级资源不同进程即使调用socket()返回相同数字如3指向的也是完全不同的内核对象。第二初始化协议族与类型。调用时传入的domain如AF_INET、type如SOCK_STREAM、protocol如IPPROTO_TCP参数决定了内核将为该fd关联哪一套协议栈逻辑。AF_INET表示IPv4地址族SOCK_STREAM表示面向连接、可靠传输的流式服务IPPROTO_TCP指定使用TCP协议。这三个参数共同确定了后续所有行为的基础——比如SOCK_DGRAM就不会触发三次握手AF_UNIX就根本不用IP地址。第三分配并初始化内核socket结构体。这是最核心的动作。内核会为每个socket分配一块内存其中包含sk_state当前TCP状态TCP_CLOSE、TCP_LISTEN、TCP_ESTABLISHED等sk_prot指向协议操作函数集的指针如tcp_prot中的tcp_connect()、tcp_accept()sk_write_queue/sk_receive_queue发送/接收缓冲区sk_bind_node用于哈希表查找的绑定节点提示socket()成功后socket处于TCP_CLOSE状态此时它只是一个“空壳”尚未绑定地址也不能收发数据。很多新手在socket()后直接send()得到EBADF错误就是因为没理解这个初始状态。实操中常见误区认为socket()失败一定是系统资源耗尽。其实更常见的是domain或type参数不被内核支持如某些嵌入式系统禁用IPv6或protocol值非法。调试时用strace -e tracesocket ./your_program可直接看到系统调用返回值及errno比查文档快得多。2.2 bind()不是“绑定端口”而是向内核注册本地通信身份bind()的语义常被简化为“把socket和IP地址端口绑定”这掩盖了它真正的技术内涵它是进程向内核声明“我将以这个地址和端口作为本地通信身份”的注册动作。关键点在于注册对象是“地址端口”组合而非单个端口。127.0.0.1:8080和0.0.0.0:8080是两个完全不同的注册项。前者只响应发给本机回环地址的请求后者响应所有网卡上的请求。bind()时指定INADDR_ANY即0.0.0.0意味着“监听所有可用网络接口”这是生产环境服务的常规做法。内核维护全局哈希表进行冲突检测。当调用bind()时内核会检查该IP, port组合是否已被其他socket同一进程或其他进程以SO_REUSEADDR未启用的方式注册。若已存在返回EADDRINUSE。这就是你搜到的bind: only one usage of each socket address的根源——不是端口被“占用”而是该地址组合已被独占注册。SO_REUSEADDR是解决TIME_WAIT问题的关键开关。TCP连接关闭后主动关闭方会进入TIME_WAIT状态持续2MSL通常60秒防止旧连接的延迟报文干扰新连接。若此时立即bind()相同地址会失败。设置setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on))后内核允许新socket重用处于TIME_WAIT的地址组合。但注意SO_REUSEADDR不允许两个LISTEN状态的socket同时绑定同一IP, port它只绕过TIME_WAIT限制。注意bind()对客户端socket非必需。若客户端不显式bind()内核会在connect()时自动为其分配一个临时端口ephemeral port通常在32768-65535范围并绑定到INADDR_ANY。这也是为什么多数客户端程序省略bind()步骤。一个典型调试场景开发Web服务时改完代码CtrlC结束进程立刻./server重启报错Address already in use。此时netstat -tuln | grep :8080很可能显示tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN说明前一个进程虽退出但socket仍处于TIME_WAIT。解决方案要么加SO_REUSEADDR要么等60秒要么改用不同端口测试。2.3 listen()不是“开始监听”而是向内核申请连接队列并切换状态listen()是服务端socket的转折点它彻底改变了socket的内核状态和行为模式。其核心动作有二第一将socket状态从TCP_CLOSE切换为TCP_LISTEN。这是质变此前socket不能接收任何连接请求此后内核协议栈开始处理发往该地址端口的SYN包并启动三次握手流程。第二初始化并配置全连接队列accept queue和半连接队列syn queue。这是listen()最易被忽视却最关键的参数——backlog。它并非单一队列长度而是两个队列容量的提示值半连接队列SYN queue存放已完成SYN_RECV状态即收到SYN、发出SYNACK、等待ACK的连接。长度由内核参数net.ipv4.tcp_max_syn_backlog控制backlog值仅作参考。全连接队列accept queue存放已完成三次握手、处于ESTABLISHED状态、等待accept()取出的连接。长度由min(backlog, net.core.somaxconn)决定net.core.somaxconn是系统级上限默认128。当全连接队列满时内核对新到达的SYN包有两种处理若net.ipv4.tcp_abort_on_overflow0默认则丢弃SYN包客户端超时重传若设为1则直接回RST包。这就是为什么高并发场景下listen()的backlog设太小会导致连接成功率骤降——不是服务挂了而是连接被内核静默丢弃。实操心得backlog值需根据预期并发量调整。例如Nginx默认backlog511Node.jshttp.createServer().listen(port)内部也设为511。若你的服务QPS达1000backlog至少设为2048并同步调大net.core.somaxconnsudo sysctl -w net.core.somaxconn2048。一个经典陷阱listen()后忘记调用accept()或accept()被阻塞如未设非阻塞模式导致全连接队列持续积压直至溢出。此时用ss -lnt查看Recv-Q列若数值接近backlog说明队列已满新连接必然失败。2.4 connect() 与 accept()客户端与服务端的“握手分工”connect()和accept()是TCP三次握手在应用层的镜像体现但分工截然不同connect()是客户端的主动发起者。它向目标地址发送SYN包并阻塞等待三次握手完成ESTABLISHED状态。成功返回意味着连接已建立socket可立即send()/recv()。若目标无服务监听内核直接回RSTconnect()返回ECONNREFUSED若网络不通超时后返回ETIMEDOUT。accept()是服务端的被动接收者。它不参与握手过程只从内核的全连接队列中取出一个已建立的连接为其创建新的socket fd。这个新fd与原始listenfd完全独立listenfd继续accept()新连接新fd负责与该客户端通信。这是实现“一个监听fd服务多个客户端”的基石。关键细节accept()返回的新fd继承原socket的部分属性如SO_KEEPALIVE但不继承SO_REUSEADDR、SO_LINGER等绑定相关选项。更重要的是新fd的远程地址peeraddr就是客户端的IP端口而listenfd的peeraddr为空因它不对应具体客户端。常见误区认为accept()的返回值是“客户端socket”实则它是服务端为该客户端创建的专用通信通道。一个listenfd可产生无数个acceptfd每个代表一个独立TCP连接。这也是为什么netstat -tn中能看到同一本地端口如:8080对应多个不同远程地址。调试技巧当connect()失败时不要只查服务端是否运行先用telnet target_ip port测试基础连通性。若telnet通说明网络层OK问题在应用层如防火墙、服务配置若telnet不通再查路由、防火墙、目标端口监听状态ss -tuln | grep port。2.5 recv() 与 send()数据流动的“缓冲区搬运工”recv()和send()看似简单却是性能瓶颈和bug高发区。它们的本质是在用户空间缓冲区与内核socket缓冲区之间搬运数据而非直接操作网络硬件。send()的真实行为将用户缓冲区数据拷贝到内核sk_write_queue然后返回拷贝的字节数。它不保证数据已发到网络更不保证对方收到。若内核缓冲区满如对方接收慢、网络拥塞send()会阻塞阻塞模式或返回EAGAIN/EWOULDBLOCK非阻塞模式。这就是为什么send()返回值必须检查——它可能小于请求发送的长度。recv()的真实行为从内核sk_receive_queue拷贝数据到用户缓冲区返回实际拷贝字节数。它不保证一次取完所有数据也不保证取到“一条完整消息”。TCP是字节流协议recv()只管搬数据不管业务逻辑的“包”边界。例如发送端send(HELLO,5)接收端可能分两次recv()到HE和LLO或一次收到HELLO甚至HELLO加上后续数据的开头部分。关键原则永远检查send()/recv()的返回值。伪代码如下int total_sent 0; while (total_sent len) { int sent send(sockfd, buf total_sent, len - total_sent, 0); if (sent -1) { if (errno EAGAIN || errno EWOULDBLOCK) continue; // 非阻塞模式 else break; // 其他错误 } total_sent sent; }一个高频问题recv()返回0表示对端已关闭连接FIN包到达这是正常终止信号不是错误。返回-1且errnoECONNRESET才表示异常断开RST包。2.6 select()单线程管理多连接的“轮询调度器”select()是I/O多路复用的元老它让单个线程能同时监控多个socket的读写就绪状态避免为每个连接开一个线程的资源消耗。其核心是内核提供的一次性状态查询服务。调用select()时你传入三个fd_set读、写、异常集合和超时时间。内核遍历所有被监控的fd检查其对应缓冲区状态读集合中fdsk_receive_queue是否有数据或对端关闭写集合中fdsk_write_queue是否有空间或连接建立完成异常集合中fd是否有带外数据OOB或错误select()返回后被修改的fd_set中置位的fd即为就绪fd。注意select()是水平触发Level-Triggered即只要条件满足如接收缓冲区有数据每次调用都会报告该fd就绪直到你recv()清空缓冲区。实操痛点select()有fd数量限制FD_SETSIZE通常1024且每次调用需复制整个fd_set到内核效率随fd数增长而下降。现代服务更倾向epollLinux或kqueueBSD但select()仍是理解多路复用原理的必经之路。一个典型用法服务端主线程select()监控listen_fd新连接和所有已连接client_fd数据到达。当listen_fd就绪调用accept()当某client_fd就绪调用recv()。这样单线程即可处理成百上千连接。2.7 close()不是“关闭连接”而是释放fd并触发四次挥手close()常被误解为“断开TCP连接”实则它是用户空间释放fd资源的操作间接触发内核的连接终止流程。调用close()时若该fd是socket的最后一个引用即无其他进程或线程持有此fd内核会将socket状态设为TCP_FIN_WAIT1发送FIN包给对端启动四次挥手流程若还有其他引用如fork()后父子进程共享fdclose()只减引用计数不发FIN。关键细节close()返回成功不代表连接已断开。它只表示用户空间放弃对该socket的控制。后续挥手过程由内核异步完成。若需确保数据发送完毕再关闭应先shutdown(sockfd, SHUT_WR)发FIN再recv()直到返回0收到对端FIN最后close()。注意close()对listenfd 和acceptfd 的影响不同。关闭listenfd 会使内核停止接受新连接但已建立的连接不受影响关闭acceptfd 则终止该特定连接。一个调试线索用ss -i查看连接状态。若看到fin-wait-1或time-wait说明close()已触发挥手若长期处于established但无数据可能是应用层未调用close()或shutdown()。3. 完整TCP服务端实操从零搭建可调试的Echo服务器3.1 基础版本阻塞式单连接Echo服务我们从最简版本开始逐步添加健壮性。以下为C语言实现Linux环境重点展示函数调用逻辑与错误处理#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); char buffer[BUFFER_SIZE]; // 1. 创建socket server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd -1) { perror(socket failed); exit(EXIT_FAILURE); } // 2. 设置SO_REUSEADDR避免TIME_WAIT端口占用 int opt 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) -1) { perror(setsockopt SO_REUSEADDR failed); close(server_fd); exit(EXIT_FAILURE); } // 3. 绑定地址 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; // 监听所有接口 server_addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { perror(bind failed); close(server_fd); exit(EXIT_FAILURE); } // 4. 开始监听backlog设为10 if (listen(server_fd, 10) -1) { perror(listen failed); close(server_fd); exit(EXIT_FAILURE); } printf(Server listening on port %d\n, PORT); // 5. 接受连接阻塞 client_fd accept(server_fd, (struct sockaddr*)client_addr, client_len); if (client_fd -1) { perror(accept failed); close(server_fd); exit(EXIT_FAILURE); } printf(Client connected from %s:%d\n, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 6. 循环收发数据 while (1) { ssize_t bytes_received recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (bytes_received 0) { printf(Client disconnected\n); break; } else if (bytes_received -1) { perror(recv failed); break; } buffer[bytes_received] \0; printf(Received: %s, buffer); // 回复客户端 if (send(client_fd, buffer, bytes_received, 0) -1) { perror(send failed); break; } } // 7. 关闭连接 close(client_fd); close(server_fd); return 0; }编译运行gcc -o echo_server echo_server.c ./echo_server测试新开终端telnet 127.0.0.1 8080输入文字回车观察回显。Ctrl]退出telnet。实操心得此版本虽简但已覆盖全部核心函数。setsockopt(SO_REUSEADDR)是生产必备否则改代码后重启必报Address already in useaccept()后打印客户端地址是调试连接来源的第一手信息recv()返回0判断客户端断开是优雅终止的关键。3.2 进阶版本非阻塞select多连接服务单连接服务无实用价值。我们升级为支持多客户端的版本引入非阻塞I/O和select()#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/select.h #include fcntl.h #define PORT 8080 #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); if (flags -1) return -1; flags | O_NONBLOCK; return fcntl(fd, F_SETFL, flags); } int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); fd_set read_fds, temp_fds; int max_fd, activity; int client_sockets[MAX_CLIENTS]; char buffer[BUFFER_SIZE]; int i, valread; // 初始化client_sockets数组 for (i 0; i MAX_CLIENTS; i) { client_sockets[i] 0; } // 1. 创建socket server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd -1) { perror(socket failed); exit(EXIT_FAILURE); } // 2. 设置SO_REUSEADDR int opt 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) -1) { perror(setsockopt SO_REUSEADDR failed); close(server_fd); exit(EXIT_FAILURE); } // 3. 绑定 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; server_addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { perror(bind failed); close(server_fd); exit(EXIT_FAILURE); } // 4. 监听 if (listen(server_fd, 10) -1) { perror(listen failed); close(server_fd); exit(EXIT_FAILURE); } printf(Server listening on port %d\n, PORT); // 5. 设置server_fd为非阻塞 if (set_nonblocking(server_fd) -1) { perror(set_nonblocking server_fd failed); close(server_fd); exit(EXIT_FAILURE); } // 6. 初始化fd_set FD_ZERO(read_fds); FD_SET(server_fd, read_fds); max_fd server_fd; while (1) { temp_fds read_fds; // 每次select前重置 // 7. select等待事件 activity select(max_fd 1, temp_fds, NULL, NULL, NULL); if (activity 0) { perror(select error); break; } // 8. 检查server_fd是否就绪新连接 if (FD_ISSET(server_fd, temp_fds)) { client_fd accept(server_fd, (struct sockaddr*)client_addr, client_len); if (client_fd 0) { if (errno EAGAIN || errno EWOULDBLOCK) { // 非阻塞无连接到达 } else { perror(accept error); } } else { // 添加新client_fd到数组和fd_set for (i 0; i MAX_CLIENTS; i) { if (client_sockets[i] 0) { client_sockets[i] client_fd; FD_SET(client_fd, read_fds); if (client_fd max_fd) max_fd client_fd; printf(New connection, fd: %d, ip: %s, port: %d\n, client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; } } if (i MAX_CLIENTS) { printf(Too many clients\n); close(client_fd); } } } // 9. 检查已连接client_fd是否就绪数据到达 for (i 0; i MAX_CLIENTS; i) { client_fd client_sockets[i]; if (client_fd 0 FD_ISSET(client_fd, temp_fds)) { valread recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (valread 0) { // 客户端断开 printf(Client %d disconnected\n, client_fd); close(client_fd); FD_CLR(client_fd, read_fds); client_sockets[i] 0; } else if (valread 0) { buffer[valread] \0; printf(Client %d: %s, client_fd, buffer); if (send(client_fd, buffer, valread, 0) -1) { perror(send failed); close(client_fd); FD_CLR(client_fd, read_fds); client_sockets[i] 0; } } else { if (errno ! EAGAIN errno ! EWOULDBLOCK) { perror(recv failed); close(client_fd); FD_CLR(client_fd, read_fds); client_sockets[i] 0; } } } } } // 清理 close(server_fd); for (i 0; i MAX_CLIENTS; i) { if (client_sockets[i] 0) close(client_sockets[i]); } return 0; }编译运行同上。现在可用多个telnet窗口同时连接服务端会轮流响应。关键升级点set_nonblocking()让accept()和recv()在无事件时立即返回EAGAIN避免阻塞主线程select()监控所有fdFD_ISSET()判断哪个fd就绪动态维护client_sockets[]数组和read_fds实现连接生命周期管理max_fd动态更新确保select()参数正确。3.3 生产级加固添加超时、日志与错误隔离真实服务需应对更多异常。我们在上版基础上加入连接超时防止恶意客户端占着不发数据请求超时单次recv()最长等待时间日志记录连接、断开、错误错误隔离单个客户端错误不影响其他#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/select.h #include fcntl.h #include time.h #include sys/time.h #define PORT 8080 #define MAX_CLIENTS 100 #define BUFFER_SIZE 1024 #define CLIENT_TIMEOUT 30 // 秒 typedef struct { int fd; time_t last_active; char addr_str[INET_ADDRSTRLEN]; } client_t; int set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); if (flags -1) return -1; flags | O_NONBLOCK; return fcntl(fd, F_SETFL, flags); } void log_message(const char* level, const char* fmt, ...) { va_list args; va_start(args, fmt); time_t now time(NULL); struct tm* tm_info localtime(now); char time_str[20]; strftime(time_str, sizeof(time_str), %Y-%m-%d %H:%M:%S, tm_info); fprintf(stderr, [%s] [%s] , time_str, level); vfprintf(stderr, fmt, args); fprintf(stderr, \n); va_end(args); } int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); fd_set read_fds, temp_fds; struct timeval timeout; int max_fd, activity; client_t clients[MAX_CLIENTS]; char buffer[BUFFER_SIZE]; int i, j, valread; time_t now; // 初始化clients数组 for (i 0; i MAX_CLIENTS; i) { clients[i].fd 0; } // 创建socket... server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd -1) { log_message(ERROR, socket failed: %s, strerror(errno)); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR... int opt 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) -1) { log_message(ERROR, setsockopt SO_REUSEADDR failed: %s, strerror(errno)); close(server_fd); exit(EXIT_FAILURE); } // 绑定... memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; server_addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { log_message(ERROR, bind failed: %s, strerror(errno)); close(server_fd); exit(EXIT_FAILURE); } // 监听... if (listen(server_fd, 128) -1) { // backlog增大 log_message(ERROR, listen failed: %s, strerror(errno)); close(server_fd); exit(EXIT_FAILURE); } log_message(INFO, Server listening on port %d, PORT); // 设置非阻塞... if (set_nonblocking(server_fd) -1) { log_message(ERROR, set_nonblocking server_fd failed: %s, strerror(errno)); close(server_fd); exit(EXIT_FAILURE); } // 初始化fd_set FD_ZERO(read_fds); FD_SET(server_fd, read_fds); max_fd server_fd; while (1) { temp_fds read_fds; timeout.tv_sec 1; // select超时1秒用于定期检查超时 timeout.tv_usec 0; activity select(max_fd 1, temp_fds, NULL, NULL, timeout); if (activity 0) { log_message(ERROR, select error: %s, strerror(errno)); break; } // 检查server_fd if (FD_ISSET(server_fd, temp_fds)) { client_fd accept(server_fd, (struct sockaddr*)client_addr, client_len); if (client_fd 0) { if (errno EAGAIN || errno EWOULDBLOCK) { // 无连接 } else { log_message(ERROR, accept error: %s, strerror(errno)); } } else { // 查找空闲slot for (i 0; i MAX_CLIENTS; i) { if (clients[i].fd 0) { clients[i].fd client_fd; inet_ntop(AF_INET, client_addr.sin_addr, clients[i].addr_str, sizeof(clients[i].addr_str)); clients[i].last_active time(NULL); FD_SET(client_fd, read_fds); if (client_fd max_fd) max_fd client_fd; log_message(INFO, New connection from %s:%d, fd%d, clients[i].addr_str, ntohs(client_addr.sin_port), client_fd); break; } } if (i MAX_CLIENTS) { log_message(WARN, Too many clients, rejecting new connection); close(client_fd); } } } // 检查client_fd now time(NULL); for (i 0; i MAX_CLIENTS; i) { client_fd clients[i].fd; if (
上一篇/下一篇内容由系统自动关联 返回资讯列表 →