閱讀Skynet源碼的過程中,發現一種非阻塞connect方式。以前不知道,這次好好學習一下。
文章參考自:非阻塞connect編寫方法介紹(董的博客)
TCP連接的建立涉及到一個三次握手的過程,鑑於RTT波動範圍很大,從局域網的幾個毫秒到幾百個毫秒甚至廣域網上的幾秒。這段時間內,我們可以執行其他處理工作,以便做到並行。因此,非阻塞connect可以爲我所用。
1、fcntl
fcntl函數可執行各種描述符的控制操作,對於socket描述符,可以設置其IO爲阻塞或非阻塞。默認情況下connect爲阻塞,現修改爲非阻塞:
int flag = fcntl(fd, F_GETFL); //獲取當前flag
if ( -1 == flag )
{
return;
}
fcntl(fd, F_SETFL, flag | O_NONBLOCK); //設置爲非阻塞
2、connect
對於阻塞式套接字,調用connect函數將激發TCP的三次握手過程,而且僅在連接建立成功或者出錯時才返回;對於非阻塞式套接字,如果調用connect函數返回-1(表示出錯),且錯誤爲EINPROGRESS,表示連接建立中,尚未完成;如果返回0,則表示連接已經建立。
3、epoll
epoll作爲Linux下出色的IO多路複用工具,它可以讓內核監控多個socket。使用epoll與非阻塞connect相互配合,可以達到異步建立TCP連接的目的。需要注意:
[1] 當連接成功建立時,connect描述符變成可寫;
[2] 當連接建立遇到錯誤時,描述符變爲即可讀,也可寫,遇到這種情況,可調用getsockopt函數查看連接狀態。
於是,非阻塞connect步驟如下:
1)創建socket,設置爲非阻塞;
2)調用connect函數,如果返回0,則連接建立;如果返回-1,檢查errno ,如果值爲 EINPROGRESS,表明連接正在建立,否則表明連接出錯;
3)將connect用的fd置於epoll監控下;
4)待epoll監測到fd可寫,同時用getsockopt查看連接狀態,如沒有錯誤,表明連接建立成功。
一個簡單的例子如下:
客戶端:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/epoll.h>
int main()
{
struct addrinfo ai_hints;
struct addrinfo *ai_list = NULL;
struct addrinfo *ai_ptr = NULL;
const char *host = "127.0.0.1";
const char *port = "8888";
memset(&ai_hints, 0, sizeof(ai_hints));
ai_hints.ai_family = AF_UNSPEC;
ai_hints.ai_socktype = SOCK_STREAM;
ai_hints.ai_protocol = IPPROTO_TCP;
if (getaddrinfo(host, port, &ai_hints, &ai_list))
{
printf("error\n");
return 0;
}
int status = -1;
int connect_fd = -1;
for (ai_ptr = ai_list; ai_ptr != NULL; ai_ptr = ai_ptr -> ai_next)
{
connect_fd = socket(ai_ptr -> ai_family, ai_ptr -> ai_socktype, ai_ptr -> ai_protocol);
if (connect < 0)
{
close(connect_fd);
printf("error\n");
return 0;
}
// 查看getaddrinfo函數後返回的目的IP和PORT,注意各種轉換。inet_ntop、ntohs。
char buffer[100];
struct sockaddr * addr = ai_ptr->ai_addr;
void * sin_addr = (ai_ptr->ai_family == AF_INET) ? (void*)&((struct sockaddr_in *)addr)->sin_addr : (void*)&((struct sockaddr_in6 *)addr)->sin6_addr;
if (inet_ntop(ai_ptr->ai_family, sin_addr, buffer, sizeof(buffer))) {
printf("sin_addr: %s, port: %d\n", buffer, ntohs(((struct sockaddr_in *)addr)->sin_port));
}
int flag = fcntl(connect_fd, F_GETFL);
if (-1 == flag)
{
close(connect_fd);
printf("fcntl error\n");
return 0;
}
fcntl(connect_fd, F_SETFL, flag | O_NONBLOCK);
status = connect(connect_fd, ai_ptr -> ai_addr, ai_ptr -> ai_addrlen);
if(status != 0 && errno != EINPROGRESS)
{
close(connect_fd);
printf("fcntl error\n");
return 0;
}
break;
}
if (0 == status)
{
printf("Connected!\n");
sleep(20);
close(connect_fd);
return 0;
}
else
{
int efd = epoll_create(10);
struct epoll_event event;
struct epoll_event wait_event[1];
event.data.fd = connect_fd;
event.events = EPOLLOUT | EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, connect_fd, &event);
int ret = 0;
while (1)
{
printf("epoll...\n");
ret = epoll_wait(efd, wait_event, 1, -1);
int err = 0;
int errlen = sizeof(err);
printf("ret: %d\n", ret);
for (int i = 0; i < ret; ++i)
{
printf("%d %d\n", (wait_event[i].events & EPOLLOUT) == 0, (wait_event[i].events & EPOLLIN) == 0);
getsockopt(wait_event[i].data.fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
if (err)
{
close(connect_fd);
close(efd);
printf("err: %d\n", err);
return 0;
}
if (wait_event[i].data.fd == connect_fd && (wait_event[i].events & EPOLLOUT))
{
printf("Connected!\n");
sleep(20);
close(connect_fd);
close(efd);
return 0;
}
}
}
}
}
服務器:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const int SERVER_PORT = 8888;
int main()
{
int server_socket;
int n;
server_socket = socket(AF_INET, SOCK_STREAM, 0);
assert(server_socket != -1);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
assert(listen(server_socket, 5) != -1);
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
while(1)
{
printf("waiting...\n");
int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if(connfd == -1)
continue;
printf("Connected..\n");
sleep(20);
close(connfd);
break;
}
close(server_socket);
return 0;
}
客戶端運行結果:
sin_addr: 127.0.0.1, port: 8888
epoll...
ret: 1
0 1
Connected!
可以發現,開始第一次 connect 的結果表明連接建立中,後面通過 epoll 監控到TCP連接建立完成。
PS:getsockopt 函數很重要!在不啓動服務器的情況下,客戶端代碼是可以正常運行到 getsockopt 函數所在地方的,而且此時epoll反饋connect_fd是可讀可寫的,其實已經出錯了。通過該函數的 err 參數返回錯誤碼 111 可知已經從出錯。
Linux下常見的socket錯誤碼:
EACCES, EPERM:用戶試圖在套接字廣播標誌沒有設置的情況下連接廣播地址或由於防火牆策略導致連接失敗。
EADDRINUSE 98:Address already in use(本地地址處於使用狀態)
EAFNOSUPPORT 97:Address family not supported by protocol(參數serv_add中的地址非合法地址)
EAGAIN:沒有足夠空閒的本地端口。
EALREADY 114:Operation already in progress(套接字爲非阻塞套接字,並且原來的連接請求還未完成)
EBADF 77:File descriptor in bad state(非法的文件描述符)
ECONNREFUSED 111:Connection refused(遠程地址並沒有處於監聽狀態)
EFAULT:指向套接字結構體的地址非法。
EINPROGRESS 115:Operation now in progress(套接字爲非阻塞套接字,且連接請求沒有立即完成)
EINTR:系統調用的執行由於捕獲中斷而中止。
EISCONN 106:Transport endpoint is already connected(已經連接到該套接字)
ENETUNREACH 101:Network is unreachable(網絡不可到達)
ENOTSOCK 88:Socket operation on non-socket(文件描述符不與套接字相關)
ETIMEDOUT 110:Connection timed out(連接超時)
此外,這次用了與之前不一樣的初始化addr的方式,getaddrinfo 函數,該函數能夠處理名字到地址以及服務到端口這兩種轉換,返回的是一個addrinfo的結構(列表)指針。