一个真正的客户端非阻塞的 connect
前言 - 一个简短开场白
winds 的 select 和 linux 的 select 是两个完全不同的东西. 然而凡人喜欢把它们揉在一起.
非阻塞的connect业务是个自带超时机制的 connect. 实现机制无外乎利用select(也有 epoll的).
本文是个源码软文, 专注解决客户端的跨平台的connect问题. 服务器的connect 要比客户端多考虑一丁点.
有机会再扯. 对于 select 网上资料太多, 几乎都有点不痛不痒. 了解真相推荐 man and msdn !!!
正文 - 所有的都需要前戏
那开始吧 . 一切从丑陋的跨平台宏开始
#include <stdio.h> #include <errno.h> #include <stdint.h> #include <stddef.h> #include <stdlib.h> #include <signal.h> // // IGNORE_SIGPIPE - 管道破裂,忽略SIGPIPE信号 // #define IGNORE_SIGNAL(sig) signal(sig, SIG_IGN) #ifdef __GNUC__ #include <fcntl.h> #include <netdb.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/tcp.h> #include <sys/un.h> #include <sys/uio.h> #include <sys/select.h> #include <sys/resource.h> /* * This is used instead of -1, since the * SOCKET type is unsigned. */ #define INVALID_SOCKET (~0) #define SOCKET_ERROR (-1) #define IGNORE_SIGPIPE() IGNORE_SIGNAL(SIGPIPE) // connect链接还在进行中, linux显示 EINPROGRESS,winds是 WSAEWOULDBLOCK #define ECONNECTED EINPROGRESS typedef int socket_t; #elif _MSC_VER #undef FD_SETSIZE #define FD_SETSIZE (1024) #include <ws2tcpip.h> #undef errno #define errno WSAGetLastError() #define IGNORE_SIGPIPE() // connect链接还在进行中, linux显示 EINPROGRESS,winds是 WSAEWOULDBLOCK #define ECONNECTED WSAEWOULDBLOCK typedef int socklen_t; typedef SOCKET socket_t; static inline void _socket_start(void) { WSACleanup(); } #endif // 目前通用的tcp udp v4地址 typedef struct sockaddr_in sockaddr_t; // // socket_start - 单例启动socket库的初始化方法 // socket_addr - 通过ip, port 得到 ipv4 地址信息 // inline void socket_start(void) { #ifdef _MSC_VER # pragma comment(lib, "ws2_32.lib") WSADATA wsad; WSAStartup(WINSOCK_VERSION, &wsad); atexit(_socket_start); #endif IGNORE_SIGPIPE(); }
此刻再封装一些, 简化操作.
inline socket_t socket_stream(void) { return socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); } inline int socket_close(socket_t s) { #ifdef _MSC_VER return closesocket(s); #else return close(s); #endif } inline int socket_set_block(socket_t s) { #ifdef _MSC_VER u_long mode = 0; return ioctlsocket(s, FIONBIO, &mode); #else int mode = fcntl(s, F_GETFL, 0); if (mode == SOCKET_ERROR) return SOCKET_ERROR; if (mode & O_NONBLOCK) return fcntl(s, F_SETFL, mode & ~O_NONBLOCK); return 0; #endif } inline int socket_set_nonblock(socket_t s) { #ifdef _MSC_VER u_long mode = 1; return ioctlsocket(s, FIONBIO, &mode); #else int mode = fcntl(s, F_GETFL, 0); if (mode == SOCKET_ERROR) return SOCKET_ERROR; if (mode & O_NONBLOCK) return 0; return fcntl(s, F_SETFL, mode | O_NONBLOCK); #endif } inline int socket_connect(socket_t s, const sockaddr_t * addr) { return connect(s, (const struct sockaddr *)addr, sizeof(*addr)); }
全局的测试主体main 函数部分如下
extern int socket_addr(const char * ip, uint16_t port, sockaddr_t * addr); extern int socket_connecto(socket_t s, const sockaddr_t * addr, int ms); extern socket_t socket_connectos(const char * host, uint16_t port, int ms); // // gcc -g -O2 -Wall -o main.exe main.c // int main(int argc, char * argv[]) { socket_start(); socket_t s = socket_connectos("127.0.0.1", 80, 10000); if (s == INVALID_SOCKET) { fprintf(stderr, "socket_connectos is error!!\n"); exit(EXIT_FAILURE); } puts("socket_connectos is success!"); return EXIT_SUCCESS; }
int socket_addr(const char * ip, uint16_t port, sockaddr_t * addr) { if (!ip || !*ip || !addr) { fprintf(stderr, "check empty ip = %s, port = %hu, addr = %p.\n", ip, port, addr); return -1; } addr->sin_family = AF_INET; addr->sin_port = htons(port); addr->sin_addr.s_addr = inet_addr(ip); if (addr->sin_addr.s_addr == INADDR_NONE) { struct hostent * host = gethostbyname(ip); if (!host || !host->h_addr) { fprintf(stderr, "check ip is error = %s.\n", ip); return -1; } // 尝试一种, 默认ipv4 memcpy(&addr->sin_addr, host->h_addr, host->h_length); } memset(addr->sin_zero, 0, sizeof addr->sin_zero); return 0; }
这里才是你要的一切, 真正的跨平台的客户端非阻塞 connect.
int socket_connecto(socket_t s, const sockaddr_t * addr, int ms) { int n, r; struct timeval to; fd_set rset, wset, eset; // 还是阻塞的connect if (ms < 0) return socket_connect(s, addr); // 非阻塞登录, 先设置非阻塞模式 r = socket_set_nonblock(s); if (r < 0) { fprintf(stderr, "socket_set_nonblock error!\n"); return r; } // 尝试连接一下, 非阻塞connect 返回 -1 并且 errno == EINPROGRESS 表示正在建立链接 r = socket_connect(s, addr); if (r >= 0) goto __return; // 链接不再进行中直接返回, linux是 EINPROGRESS,winds是 WASEWOULDBLOCK if (errno != ECONNECTED) { fprintf(stderr, "socket_connect error r = %d!\n", r); goto __return; } // 超时 timeout, 直接返回结果 ErrBase = -1 错误 r = -1; if (ms == 0) goto __return; FD_ZERO(&rset); FD_SET(s, &rset); FD_ZERO(&wset); FD_SET(s, &wset); FD_ZERO(&eset); FD_SET(s, &eset); to.tv_sec = ms / 1000; to.tv_usec = (ms % 1000) * 1000; n = select((int)s + 1, &rset, &wset, &eset, &to); // 超时直接滚 or linux '异常'直接返回 0 if (n <= 0) goto __return; // 当连接成功时候,描述符会变成可写 if (n == 1 && FD_ISSET(s, &wset)) { r = 0; goto __return; } // 当连接建立遇到错误时候, winds 抛出异常, linux 描述符变为即可读又可写 if (FD_ISSET(s, &eset) || n == 2) { socklen_t len = sizeof n; // 只要最后没有 error那就 链接成功 if (!getsockopt(s, SOL_SOCKET, SO_ERROR, (char *)&n, &len) && !n) r = 0; } __return: socket_set_block(s); return r; } socket_t socket_connectos(const char * host, uint16_t port, int ms) { int r; sockaddr_t addr; socket_t s = socket_stream(); if (s == INVALID_SOCKET) { fprintf(stderr, "socket_stream is error!\n"); return INVALID_SOCKET; } // 构建ip地址 r = socket_addr(host, port, &addr); if (r < 0) return r; r = socket_connecto(s, &addr, ms); if (r < 0) { socket_close(s); fprintf(stderr, "socket_connecto host port ms = %s, %u, %d!\n", host, port, ms); return INVALID_SOCKET; } return s; }
每一次突破都来之不易. 如果需要在工程中实现一份 nonblocking select connect. 可以直接用上面思路.
核心就是不同平台的select api 的使用罢了. 你知道了也许就少趟点坑, 多无可奈何些~
后记 - 感悟
代码还是少点注释好, 那些老人说的代码即注释好像有些道理