• 一个真正的客户端非阻塞的 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!!
    ");
    		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.
    ", 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.
    ", 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!
    ");
    		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!
    ", 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!
    ");
    		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!
    ", host, port, ms);
    		return INVALID_SOCKET;
    	}
    
    	return s;
    }
    

    每一次突破都来之不易. 如果需要在工程中实现一份 nonblocking select connect. 可以直接用上面思路.

    核心就是不同平台的select api 的使用罢了. 你知道了也许就少趟点坑, 多无可奈何些~

    后记 - 感悟

      代码还是少点注释好, 那些老人说的代码即注释好像有些道理

  • 相关阅读:
    [Unity3D]Animation说明
    [Unity3D]Shader说明
    [AR+Vuforia]学习笔记
    [Android]ListView学习笔记
    [Android]优化相关
    [Android]学习笔记之布局
    [Android]快捷键
    [Android]学习笔记Activity_001
    [Unreal]学习笔记之灯光说明
    什么是Hystrix,Hystrix如何使用
  • 原文地址:https://www.cnblogs.com/life2refuel/p/7337070.html
Copyright © 2020-2023  润新知