用Bollger记录技术之路的点滴...

关注高性能linux网络编程,NoSQL, c/c++/java ~~~ weibo @语_行 http://weibo.com/201281062~~~ twitter @JerryVector https://twitter.com/JerryVector
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

非阻塞socket调用connect, epoll和select检查连接情况示例

Posted on 2013-03-08 17:08  语行  阅读(26570)  评论(5编辑  收藏  举报

我们知道,linux下socket编程有常见的几个系统调用:

对于服务器来说, 有socket(), bind(),listen(), accept(),read(),write()

对于客户端来说,有socket(),connect()

这里主要要讲的是客户端这边的connect函数。

对于客户端来说,需要打开一个套接字,然后与对端服务器连接,例如:

 1 int main(int argc, char **argv) 
 2 {
 3         struct sockaddr_in s_addr;
 4         memset(&s_addr, 0, sizeof(s_addr));
 5         s_addr.sin_family = AF_INET;
 6         s_addr.sin_addr.s_addr = inet_addr("remote host");
 7         s_addr.sin_port = htons(remote port);
 8         socklen_t addr_len = sizeof(struct sockaddr);
 9         int c_fd = socket(AF_INET, SOCK_STREAM, 0);
10         int ret = connect(c_fd, (struct sockaddr*)&s_addr, addr_len);                                 
11         ......
12 }

 

当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;
}

 

ps:文中如有不妥的地方,望各位博友指正。