Csocket非阻塞模式
一、前言
初期学习socket的时候,为了方便理解,使用默认的阻塞模式比较多。而实际做项目时,我们必须考虑程序的并发性,非阻塞模式在其中担任着很重要的角色,是必会的点之一。本文不对阻塞IO和非阻塞IO的概念做说明,不了解的请自行了解。下文代码以linux平台为例。
二、设置非阻塞模式
设置非阻塞模式,通过fcntl方法设置,为了保存socket其他设置,一般选择先获取 status flags, 并在其基础上设置O_NONBLOCK属性, 代码如下:int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK);
fcntl失败返回值为-1, 同时errno会被设置成对应的错误码。(errno在此不做说明,不了解的自行了解。) 考虑失败的情况,个人注意到网上有些例子(包括ss-libev项目)在 F_GETFL 失败后,给了flags默认值,代码如下:int flags; if (-1 == (flags = fcntl(fd, F_GETFL, 0))) { flags = 0; } fcntl(fd, F_SETFL, flags | O_NONBLOCK);
经过测试,默认情况下,flags得到的值为2,也就是O_RDWR 读写, 而 0 对应的相关宏为O_RDONLY只读,明显不合理。个人感觉,对于一个正常的socket来说,F_GETFL 出错的机会不大吧, 至少我是没遇到过。如果实在出错了,还是建议走错误流程而不是给个默认值。
三、 非阻塞server
server端通常在accept后,我们为客户端连接的fd设置为非阻塞。设置O_NONBLOCK后,recv和send发生了变化。默认阻塞模式下,recv在没有数据可以接收(对方未发数据,或者缓冲区的数据已读完对方没有继续发)情况下,recv会阻塞等待,直到下次有数据发送过来。而非阻塞模式下,recv在没有数据可以接收的时候, recv会直接返回-1, 同时errno会被设置为EAGAIN/EWOULDBLOCK 。同理,非阻塞send也会在对方缓冲区满的情况下直接返回-1并设置errno, 而不是阻塞等待。 非阻塞模式下server代码大致如下:int cli_fd = accept(listen_fd, NULL, NULL); //... //省略出错判断和设置非阻塞 //... while (true) { int n = recv(cli_fd, buf, buf_len, 0); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { //无数据,做其他业务逻辑或继续下一轮逻辑, 这里sleep一秒并接着等数据 sleep(1); continue; } else { //错误,可利用errno判断出错原因,这里直接结束 close(cli_fd); break; } } else if (n == 0) { //对方关闭 close(cli_fd); break; } //正常读到数据,处理buf }
四、非阻塞client
client除了在send/recv, 还可以在connect前设置非阻塞模式,这样在connect时候可以直接返回。
client 非阻塞connect的时候,如果返回0表示连接成功,如果返回-1, 则需要判断errno 是否为EINPROGRESS,EINPROGRESS表示非阻塞连接不能立刻获取connect结果,后面可使用select/poll/epoll等对socket 可写性进行判断,如果socket已可写,使用getsockopt(iSocket, SOL_SOCKET, SO_ERROR ,&err, &len)进行判断…好像挺麻烦是不是,但是我还是建议在大部分项目中connect前设置非阻塞(小工具之类的就无所谓了,项目中一定要保证效率)。如果使用阻塞模式,有可能的问题: 如果是桌面程序,你的程序可能会卡住无响应。如果你单独为connect开一个线程,可能加大资源消耗,特别是需要connect多个对象的时候。如果你使用线程池,非阻塞connect会导致线程处理效率下降。
下面是个非阻塞connect的部分代码, 使用select, 至于poll/epoll请自行搜索代码,跟非阻塞逻辑无关:int fd; //...省略N行代码,socket的初始化,准备服务器信息等 set_nonblocking(fd); int ret = connect(fd, (struct sockaddr*)&serv_addr, serv_len); if (0 == ret) { //连接成功,直接进入正常业务逻辑 } else { if (EINPROGRESS != errno) { //出错,结束 close(fd); return; } //EINPROGRESS 继续判断 } //使用select对fd进行可写判断 struct timeval tv; tv.tv_sec = timeout; tv.tv_usec = 0; fd_set sets; FD_ZERO(&sets); FD_SET(fd, &sets); ret = select(fd + 1, NULL, &wds, NULL, &tv); if (ret > 0) { if (FD_ISSET(fd, &sets)) { int err = -1; socklen_t len = sizeof(int); ret = getsockopt(fd, SOL_SOCKET, SO_ERROR ,&err, &len) if (ret < 0 || err ) { close(fd); return; } //连接成功 //其实个人觉得如果可写后需要直接send,不需要getsockopt直接通过send也可以知道结果 } } else if (ret == 0) { //超时逻辑 } else { //select出错逻辑 }