一個真正的客戶端非阻塞的 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 的使用罷了. 你知道了也許就少趟點坑, 多無可奈何些~

 

后記 - 感悟

  代碼還是少點注釋好, 那些老人說的代碼即注釋好像有些道理

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM