1. 程式人生 > >非阻塞socket呼叫connect, epoll和select檢查連線情況示例

非阻塞socket呼叫connect, epoll和select檢查連線情況示例

我們知道,linux下socket程式設計有常見的幾個系統呼叫:

對於伺服器來說, 有socket(), bind(),listen(), accept(),read(),write()

對於客戶端來說,有socket(),connect()

這裡主要要講的是客戶端這邊的connect函式。

對於客戶端來說,需要開啟一個套接字,然後與對端伺服器連線,例如:

int main(int argc, char **argv) 
{
        struct sockaddr_in s_addr;
        memset(&s_addr, 0, sizeof(s_addr));
        s_addr.sin_family = AF_INET;
        s_addr.sin_addr.s_addr = inet_addr("remote host");
        s_addr.sin_port = htons(remote port);
        socklen_t addr_len = sizeof(struct sockaddr);
        int c_fd = socket(AF_INET, SOCK_STREAM, 0);
        int ret = connect(c_fd, (struct sockaddr*)&s_addr, addr_len);                                 
        ......
}

當connect上對端伺服器之後,就可以使用該套接字傳送資料了。

我們知道,如果socket為TCP套接字, 則connect函式會激發TCP的三次握手過程,而三次握手是需要一些時間的,核心中對connect的超時限制是75秒,就是說如果超過75秒則connect會由於超時而返回失敗。但是如果對端伺服器由於某些問題無法連線,那麼每一個客戶端發起的connect都會要等待75才會返回,因為socket預設是阻塞的。對於一些線上服務來說,假設某些對端伺服器出問題了,在這種情況下就有可能引發嚴重的後果。或者在有些時候,我們不希望在呼叫connect的時候阻塞住,有一些額外的任務需要處理;

這種場景下,我們就可以將socket設定為非阻塞,如下程式碼:

int flags = fcntl(c_fd, F_GETFL, 0);
if(flags < 0) {
    return 0;      
}
fcntl(c_fd, F_SETFL, flags | O_NONBLOCK);

當我們將socket設定為NONBLOCK後,在呼叫connect的時候,如果操作不能馬上完成,那connect便會立即返回,此時connect有可能返回-1, 此時需要根據相應的錯誤碼errno,來判斷連線是否在繼續進行。

當errno=EINPROGRESS時,這種情況是正常的,此時連線在繼續進行,但是仍未完成;同時TCP的三路握手操作繼續進行;後續只要用select/epoll去註冊對應的事件並設定超時時間來判斷連線否是連線成功就可以了。

int ret = connect(c_fd, (struct sockaddr*)&s_addr, addr_len);
while(ret < 0) {
    if( errno == EINPROGRESS ) {
         break;
    }  else {
         perror("connect fail'\n");
         return 0;
    }
}

這個地方,我們很可能會判斷如果ret小於0,就直接判斷連線失敗而返回了,沒有根據errno去判斷EINPROGRESS這個錯誤碼。這裡也是昨天在寫份程式的時候遇到的一個坑。

使用非阻塞 connect 需要注意的問題是:
1. 很可能 呼叫 connect 時會立即建立連線(比如,客戶端和服務端在同一臺機子上),必須處理這種情況。
2. Posix 定義了兩條與 select 和 非阻塞 connect 相關的規定:
1)連線成功建立時,socket 描述字變為可寫。(連線建立時,寫緩衝區空閒,所以可寫)
2)連線建立失敗時,socket 描述字既可讀又可寫。 (由於有未決的錯誤,從而可讀又可寫)

不過我同時用epoll也做了實驗(connect一個無效埠,errno=110, errmsg=connect refused),當連線失敗的時候,會觸發epoll的EPOLLERR與EPOLLIN,不會觸發EPOLLOUT。

當用select檢測連線時,socket既可讀又可寫,只能在可讀的集合通過getsockopt獲取錯誤碼。

當用epoll檢測連線時,socket既可讀又可寫,只能在EPOLLERR中通過getsockopt獲取錯誤碼。

完整程式碼如下:


/* 
 * File:   main.cpp
 * Created on March 7, 2013, 5:54 PM
 */


#include <cstdlib>
#include <string>
#include <iostream>


#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>




using namespace std;


struct so {
    int fd;
    string val;
};


int select_version(int *fd) {
    int c_fd = *fd;
    fd_set rset, wset;
    struct timeval tval;
    FD_ZERO(&rset);
    FD_SET(c_fd, &rset);
    wset = rset;
    tval.tv_sec = 0;
    tval.tv_usec = 300 * 1000; //300毫秒
    int ready_n;
    if ((ready_n = select(c_fd + 1, &rset, &wset, NULL, &tval)) == 0) {
        close(c_fd); /* timeout */
        errno = ETIMEDOUT;
        perror("select timeout.\n");
        return (-1);
    }
    if (FD_ISSET(c_fd, &rset)) {
        int error;
        socklen_t len = sizeof (error);
        if (getsockopt(c_fd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
            cout << "getsockopt error." << endl;
            return -1;
        }
        cout << "in fire." << error << endl;
    }
    if (FD_ISSET(c_fd, &wset)) {
        int error;
        socklen_t len = sizeof (error);
        if (getsockopt(c_fd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
            cout << "getsockopt error." << endl;
            return -1;
        }
        cout << "out fire." << error << endl;
    }
    return 0;
}


int epoll_version(int *fd) {
    int c_fd = *fd;
    int ep = epoll_create(1024);
    struct epoll_event event;
    event.events = (uint32_t) (EPOLLIN | EPOLLOUT | EPOLLET);
    struct so _data;
    _data.fd = c_fd;
    _data.val = "test";
    event.data.ptr = (void*) &_data;
    epoll_ctl(ep, EPOLL_CTL_ADD, c_fd, &event);
    struct epoll_event eventArr[1000];
    int status, err;
    socklen_t len;
    err = 0;
    len = sizeof (err);
    int n = epoll_wait(ep, eventArr, 20, 300);
    for (int i = 0; i < n; i++) {
        epoll_event ev = eventArr[i];
        int events = ev.events;
        if (events & EPOLLERR) {
            struct so* so_data = (struct so*) ev.data.ptr;
            cout << so_data->val << ",err event fire." << endl;
            status = getsockopt(c_fd, SOL_SOCKET, SO_ERROR, &err, &len);
            cout << status << "," << err << endl;
        }
        if (events & EPOLLIN) {
            struct so* so_data = (struct so*) ev.data.ptr;
            cout << so_data->val << ",in event fire." << endl;
            status = getsockopt(c_fd, SOL_SOCKET, SO_ERROR, &err, &len);
            cout << status << "," << err << endl;
        }
        if (events & EPOLLOUT) {
            struct so* so_data1 = (struct so*) ev.data.ptr;
            cout << so_data1->val << ",out event fire." << endl;
        }
    }


}


int main(int argc, char** argv) {
    string ip = "127.0.0.1";
    int port = 25698;
    int c_fd, flags, ret;
    struct sockaddr_in s_addr;
    memset(&s_addr, 0, sizeof (s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(port);
    s_addr.sin_addr.s_addr = inet_addr(ip.c_str());


    if ((c_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("create socket fail.\n");
        exit(0);
    }
    flags = fcntl(c_fd, F_GETFL, 0);
    if (flags < 0) {
        perror("get socket flags fail.\n");
        return -1;
    }


    if (fcntl(c_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("set socket O_NONBLOCK fail.\n");
        return -1;
    }
    ret = connect(c_fd, (struct sockaddr*) &s_addr, sizeof (struct sockaddr));
    while (ret < 0) {
        if (errno == EINPROGRESS) {
            break;
        } else {
            perror("connect remote server fail.\n");
            printf("%d\n", errno);
            exit(0);
        }
    }
    //select_version(&c_fd);
    epoll_version(&c_fd);
    return 0;
}