因為上一篇寫創建tcp服務端已經把很多重要介面分析過了,這一篇會寫的比較簡單,不過在分析代碼前,我想先說下非阻塞connect,因為這是本篇博客的核心,
以下的文字要么摘抄自《UNIX網路編程》:
當在一個非阻塞的TCP套接字上呼叫connect時,connect將立即回傳一個EINPROGRESS錯誤,不過已經發起的TCP三路握手繼續進行,我們接著使用select檢測這個連接或成功或失敗的已建立條件,既然使用select等待連接的建立(在libhv中未必是select,也可能是poll、epoll等其他io事件監視器),我們可以給select指定一個時間限制,使得我們能夠縮短connect的超時(libhv使用定時器實作的該功能),許多實作有著從75秒鐘到數分鐘的connect超時時間,應用程式有時想要一個更短的超時時間,非阻塞connect雖然聽似簡單,卻有一些我們必須處理的細節,盡管套接字是非阻塞的,如果連接的服務器在同一個主機上,那么當我們呼叫connect時,連接通常立刻建立(我在自己的環境測驗,感覺即使是在一個主機,也無法立刻建立),我們必須處理這種情況,源自Berkeley的實作(和POSIX)有關于select和非阻塞connect的以下兩個規則:(1)當連接成功建立時,描述符變為可寫(2)當連接建立遇到錯誤時,描述符變為即可讀又可寫,一個TCP套接字上發生某個錯誤時,這個待處理錯誤總是導致該套接字變為既可讀又可寫,
如果描述符變為可讀或可寫,我們就呼叫getsockopt取得套接字的待處理錯誤(使用SO_ERROR套接字選項),如果連接成功建立,該值將為0,如果連接建立發生錯誤,該值就是對應連接錯誤的errno值,我們之前說過,套接字的各種實作以及非阻塞connect會帶來移植性問題,首先,呼叫select之前有可能連接已經建立并有來自對端的資料到達,這種情況下即使套接字上不發生錯誤,套接字也是即可讀又可寫的,這和連接建立失敗情況下套接字的讀寫條件一樣,其次,我們不能假設套接字的可寫(而不可讀)條件是select回傳套接字操作成功條件的唯一方法,下一個移植性問題就是怎么判斷連接建立是否成功,張貼到Usenet上的解決方法各式各樣,這些方法可以取代getsockopt呼叫,
- 呼叫getpeername替代getsockopt,如果getpeername以ENOTCONN錯誤失敗回傳,那么連接建立已經失敗,我們必須接著以SO_ERROR呼叫getsockopt取得套接字上待處理的錯誤,
- 以值為0的長度引數呼叫read,如果read失敗,那么connect已經失敗,read回傳的errno給出了連接失敗的原因,如果連接建立成功,那么read應該回傳0.
- 再呼叫connect一次,它應該失敗,如果錯誤是EISCONN,那么套接字已經連接,也就是說第一次連接已經成功,
ok,開始創建客戶端了,,,,
與創建tcp服務端類似,創建一個客戶端的步驟如下:
hloop_t* loop = hloop_new(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); //創建一個loop
hio_t* sockio = hloop_create_tcp_client(loop, host, port, on_connect);
hio_setcb_close(sockio, on_close);
hio_setcb_read(sockio, on_recv);
hio_set_readbuf(sockio, recvbuf, RECV_BUFSIZE);
hloop_run(loop);
hloop_free(&loop);
hloop_new上一篇已經分析過了,創建一個loop,然后通過hloop_create_tcp_client,創建一個與服務端通信的io結構體,
hio_t* hloop_create_tcp_client (hloop_t* loop, const char* host, int port, hconnect_cb connect_cb) {
sockaddr_u peeraddr;
memset(&peeraddr, 0, sizeof(peeraddr));
//填充服務端網路地址結構體
int ret = sockaddr_set_ipport(&peeraddr, host, port);
if (ret != 0) {
//printf("unknown host: %s\n", host);
return NULL;
}
int connfd = socket(peeraddr.sa.sa_family, SOCK_STREAM, 0);
if (connfd < 0) {
perror("socket");
return NULL;
}
//創建io結構體
hio_t* io = hio_get(loop, connfd);
assert(io != NULL);
//設定對端地址資訊
hio_set_peeraddr(io, &peeraddr.sa, sockaddr_len(&peeraddr));
hconnect(loop, connfd, connect_cb);
return io;
}
除了hconnect,其他都比較簡單,hio_get在上一篇博客中也分析過了
hio_t* hconnect (hloop_t* loop, int connfd, hconnect_cb connect_cb) {
hio_t* io = hio_get(loop, connfd);
assert(io != NULL);
//設定connect回呼
if (connect_cb) {
io->connect_cb = connect_cb;
}
hio_connect(io);
return io;
}
int hio_connect(hio_t* io) {
//嘗試連接服務端
int ret = connect(io->fd, io->peeraddr, SOCKADDR_LEN(io->peeraddr));
#ifdef OS_WIN
if (ret < 0 && socket_errno() != WSAEWOULDBLOCK) {
#else
if (ret < 0 && socket_errno() != EINPROGRESS) {
#endif
perror("connect");
hio_close(io);
return ret;
}
//如果直接連接成功,呼叫回呼函式
if (ret == 0) {
// connect ok
__connect_cb(io);
return 0;
}
//如果沒有直接成功,設定超時時間,等待連接成功
int timeout = io->connect_timeout ? io->connect_timeout : HIO_DEFAULT_CONNECT_TIMEOUT;
io->connect_timer = htimer_add(io->loop, __connect_timeout_cb, timeout, 1);
io->connect_timer->privdata = io;
io->connect = 1;
return hio_add(io, hio_handle_events, HV_WRITE);
}
根據上一篇博客的說明,描述符已經被設定為非阻塞的,根據博客開始的文字說明,呼叫connect很可能無法直接成功,需要等待一段時間,為了防止一直不成功,這里設定了一個connect的定時器,如果在定時時間內沒有連接成功,__connect_timeout_cb回呼函式會被呼叫,因為需要等待連接成功,所以將該事件加入到io事件監視器中,由loop等待connect成功(其實就是文字說明中的select實作的功能),而connect的成功會使描述符成為可寫的,所以這里hio_add將該事件加入io事件監視器并設定感興趣的事件型別為可寫HV_WRITE,注冊了回呼函式hio_handle_events,等到可寫觸發時,該回呼函式會被呼叫,設定完這些后,就從hloop_create_tcp_client回傳,
呼叫hloop_create_tcp_client獲得了與服務端通信的io結構體,之后設定了一些回呼函式,設定了可讀緩沖區,這個緩沖區用戶可以自己設定,如果不設定會使用在hloop_new中初始化的那個緩沖區,可以參考上一篇博客,
設定完后,呼叫hloop_run等待事件的發生,關于hloop_run的細節也參考之前的博客,根據上面的分析,其實已經有兩個事件加入到loop了,一個是嘗試建立連接的io事件,一個是定時器事件,那么就有兩種可能,第一種是io事件觸發,第二種是定時器事件觸發,假設在定時器到期之前,連接建立成功,即可寫事件觸發,回呼上面注冊的回呼函式hio_handle_events:
static void hio_handle_events(hio_t* io) {
if ((io->events & HV_READ) && (io->revents & HV_READ)) {
if (io->accept) {
nio_accept(io);
}
else {
nio_read(io);
}
}
if ((io->events & HV_WRITE) && (io->revents & HV_WRITE)) {
// NOTE: del HV_WRITE, if write_queue empty
if (write_queue_empty(&io->write_queue)) {
iowatcher_del_event(io->loop, io->fd, HV_WRITE);
io->events &= ~HV_WRITE;
}
if (io->connect) {
// NOTE: connect just do once
// ONESHOT
io->connect = 0;
nio_connect(io);
}
else {
nio_write(io);
}
}
io->revents = 0;
}
這個函式在上一篇也有分析,不過只分析了讀事件,這次是寫事件,假設該客戶端連接上一篇博客的服務端,這時候服務端也會觸發這個函式,不過服務端會呼叫這里的nio_accept,而本客戶端呼叫的是nio_connect,有一個地方需要注意的是,在這里呼叫iowatcher_del_event清除了前面注冊的HV_WRITE事件型別,原因是如果不清除HV_WRITE,之后會一直觸發可寫,造成busy-loop,
static void nio_connect(hio_t* io) {
//printd("nio_connect connfd=%d\n", io->fd);
socklen_t addrlen = sizeof(sockaddr_u);
//獲取對端地址資訊
int ret = getpeername(io->fd, io->peeraddr, &addrlen);
if (ret < 0) {
io->error = socket_errno();
printd("connect failed: %s: %d\n", strerror(socket_errno()), socket_errno());
goto connect_failed;
}
else {
addrlen = sizeof(sockaddr_u);
getsockname(io->fd, io->localaddr, &addrlen);
if (io->io_type == HIO_TYPE_SSL) {
hssl_ctx_t ssl_ctx = hssl_ctx_instance();
if (ssl_ctx == NULL) {
goto connect_failed;
}
hssl_t ssl = hssl_new(ssl_ctx, io->fd);
if (ssl == NULL) {
goto connect_failed;
}
io->ssl = ssl;
ssl_client_handshark(io);
}
else {
// NOTE: SSL call connect_cb after handshark finished
__connect_cb(io);
}
return;
}
connect_failed:
hio_close(io);
}
根據博客開始的文字描述,可以知道即使套接字是可寫的,也有可能是因為有錯誤發生,這里的getpeername實際上也可以用來檢測connect是否成功,如果成功,呼叫__connect_cb,這里先忽略ssl相關的內容,之前分析心跳的博客就涉及到了__connect_cb介面,當時提到這里是設定心跳和keepalive的兩個位置之一,因為心跳部分之前博客分析過了,這里我把那部分代碼刪了,這里主要分析與連接相關的內容,
static void __connect_cb(hio_t* io) {
if (io->connect_timer) {
htimer_del(io->connect_timer);
io->connect_timer = NULL;
io->connect_timeout = 0;
}
if (io->connect_cb) {
io->connect_cb(io);
}
}
在__connect_cb中,會關閉之前設定的定時器,因為這個定時器就是防止connect長時間連接不成功的,這里既然connect已經成功了,當然要把這個定時器刪了,洗掉定時器后,呼叫用戶在呼叫hloop_create_tcp_client時注冊的回呼函式,在本次tcp客戶端示例程式中,回呼函式是這樣的:
void on_connect(hio_t* io) {
//使能讀
hio_read_start(io);
//向服務端發送一條訊息
static char buf[] = "PING\r\n";
hio_write(io, buf, 6);
}
這樣整個connect就成功了,
上面是io事件觸發的情況,還有一種就是io事件一直沒有觸發,可能一直在嘗試連接,那么會發生超時,這時候設定的定時器被觸發,__connect_timeout_cb被呼叫:
static void __connect_timeout_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io) {
char localaddrstr[SOCKADDR_STRLEN] = {0};
char peeraddrstr[SOCKADDR_STRLEN] = {0};
hlogw("connect timeout [%s] <=> [%s]",
SOCKADDR_STR(io->localaddr, localaddrstr),
SOCKADDR_STR(io->peeraddr, peeraddrstr));
io->error = ETIMEDOUT;
hio_close(io);
}
}
在定時器回呼中,會關閉這次的連接,連接服務器失敗,,,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/237990.html
標籤:其他
上一篇:R語言---Seewave包和tuneR在聲音分析中的應用①關于聲音及簡單分析
下一篇:雙線性變換法的帶通濾波器
