socket编程 —— 非阻塞socket

来源:互联网 发布:php读取文件 编辑:程序博客网 时间:2024/05/02 23:31

    在上一篇文章 《socket编程——一个简单的例子》 http://blog.csdn.net/wind19/archive/2011/01/21/6156339.aspx 中写了一个简单的tcp socket通信程序,可以进行数据的交互,但有一个问题是这个程序是阻塞的,任何socket函数都要等返回后才能进行下一步动作,如果recv一直没有数据,那么就一直不会返回,整个进程就阻塞在那。所以我们要进行改造一下,让程序不再阻塞在那,而是在有数据到来的时候读一下数据,有数据要写的时候发送一下数据。

 

    设置阻塞模式的函数一般由两个fcntl 和 ioctl

 

先放源程序,服务器端还是阻塞的,客服端改成非阻塞的,只是作为一个例子

 

 

在服务器端,要关注的一个东西是O_REUSEADDR,在程序里调用了(setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &flags, flaglen)对socket进行设置。

1.   可以对一个端口进行多次绑定,一般这个是不支持使用的; 
2.   对于监听套接字,比较特殊。如果你定义了SO_REUSEADDR,并且让两个套接字在同一个端口上进行接听,那么对于由谁来ACCEPT,就会出现歧义。如果你定义个SO_REUSEADDR,只定义一个套接字在一个端口上进行监听,如果服务器出现意外而导致没有将这个端口释放,那么服务器重新启动后,你还可以用这个端口,因为你已经规定可以重用了,如果你没定义的话,你就会得到提示,ADDR已在使用中。

  在多播的时候,也经常使用SO_REUSEADDR,也是为了防止机器出现意外,导致端口没有释放,而使重启后的绑定失败~。一般是用来防止服务器在发生意外时,端口未被释放~可以重新使用~

 

关于errno值的定义在errno.h中

 

 

接下来我们关注client.c

1. 把socket设置为非阻塞模式

    fdflags = fcntl(sock_fd, F_GETFL, 0);
    if(fcntl(sock_fd, F_SETFL, fdflags | O_NONBLOCK) < 0)
    {
        syslog(LOG_ERR, "%s:%d, fcntl set nonblock failed", __FILE__, __LINE__);
        close(sock_fd);
        exit(1);
    }

当然ioctl也可以,这个函数更为强大,这里不做详细说明。

 

2. 对于connect的处理

首先我们看一下非阻塞模式的I/O模型

对于一个系统调用来说,如果不能马上完成会返回-1(一般都是-1,具体的函数可以看详细说明),并设置errno,不同的系统会不一样,一般是EWOULDBLOCK, EAGAIN等。如果系统调用被中断,则返回EINTR错误。

 

那么对于connect来说,如果是返回值 <0,那么就需要对errno进行判断和处理,这里有几种情况

1)errno == EISCONN,说明这个socket已经连接上了

 

2)(errno == EINPROGRESS || errno == EALREADY || errno == EWOULDBLOCK), 表明connect正在进行但没有完成,因为connect需要花费一点时间,而socket又被设置成了非阻塞,所以这些错误时正常的。但如果不是这些错误(errno != EINPROGRESS && errno != EALREADY && errno != EWOULDBLOCK),那么connect就出错了。

 

3)接下来就是用select对connect进行等待

对于conncet来说,如果是阻塞的,那么它会一直等到连接成功或失败,这个时间一般是75秒到几分钟之间,这个时间对于我们的程序来说太长了,所以我们用selcet。

int select(int maxfdp1,fd_set *readset, fd_set *writeset,fd_set *exceptset, const struct timeval *timeout);

函数返回值Returns: positive count of ready descriptors, 0 on timeout, –1 on error。其中的参数

maxfdp1表示我们关注的所有套接字的最大值+1, 如果这个值是5,那么select只关注0~4的描述符,这样可以减少范围提高效率。

readset, writeset 和exceptset是selcet关注的可读,可写和异常错误的描述符集合

timeout是超时时间,如果设为NULL则永远不超时,直到有感兴趣的描述符在I/O上准备好;如果设为0则马上返回;如果是其他值,则如果在这个时间段里还没有感兴趣的描述符在I/O上准备好则返回,且返回值为0

这里还要说明的一点是每次select之后,都会把readset, writeset 和exceptset除了准备好I/O的描述符清掉,所以如果循环select的话每次都要重新设置描述符集合。

 

对于select如果返回值<0,并且errno == EINTR,说明系统调用被中断;返回值 ==0,说明超时,这两种情况都继续select。如果返回值 >0,说明有描述符的I/O准备好了,进行处理,在这里我们要看sock_fd是否可读或可写。connect连接成功则可写,如果在select之前连接成功并收到数据则又可读。但是connect异常也会出现可读(socket 对应的连接读关闭(也就是说对该socket 不能再读了。比如,该socket 收到 FIN ))或可写(socket 对应的连接写关闭(也就是说对该socket不能再写。比如,该socket 收到 RST))的情况。我们可以通过

getsockopt来区分正常情况和异常情况。

除了getsockopt,也可以用一下方法区分异常和正常情况,但不同的系统不一样,一般unix上是可以的,但linux是否可以没有尝试过。

  (1).调用getpeername获取对端的socket地址.如果getpeername返回ENOTCONN,表示连接建立失败,然后用SO_ERROR调用getsockopt得到套接口描述符上的待处理错误;
  (2).调用read,读取长度为0字节的数据.如果read调用失败,则表示连接建立失败,而且read返回的errno指明了连接失败的原因.如果连接建立成功,read应该返回0;
  (3).再调用一次connect.它应该失败,如果错误errno是EISCONN,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的;

 

有的时候connect会马上成功,特别是当服务器和客户端都在同一台机器上的话,那么这种情况也是需要处理的,就不需要select了,在我们的代码里面是直接return了。

 

connect总结:

TCP socket 被设为非阻塞后调用 connect ,connect 函数如果没有马上成功,会立即返回 EINPROCESS(如果被中断返回EINTR) ,但 TCP 的 3 次握手继续进行。之后可以用 select 检查连接是否建立成功(但不能再次调用connect,这样会返回错误EADDRINUSE)。非阻塞 connect 有3 种用途:
(1). 在3 次握手的同时做一些其他的处理。
(2). 可以同时建立多个连接。
(3). 在利用 select 等待的时候,可以给 select 设定一个时间,从而可以缩短 connect 的超时时间。

使用非阻塞 connect 需要注意的问题是:
(1). 很可能 调用 connect 时会立即建立连接(比如,客户端和服务端在同一台机子上),必须处理这种情况。
(2). Posix 定义了两条与 select 和 非阻塞 connect 相关的规定:
     连接成功建立时,socket 描述字变为可写。(连接建立时,写缓冲区空闲,所以可写)
     连接建立失败时,socket 描述字既可读又可写。 (由于有未决的错误,从而可读又可写)


 另外对于无连接的socket类型(SOCK_DGRAM),客户端也可以调用connect进行连接,此连接实际上并不建立类似SOCK_STREAM的连接,而仅仅是在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象。

 

3. recv 和 send数据

这里的处理方式也是用select,并对其中的一些错误进行处理,和connect大同小异,不做详细的说明。这里有一个问题是,既然用了select,只有在有数据可读的时候才会调用recv,那么函数也就不会阻塞在那里,还有必要把它设置为非阻塞吗。这个问题我也没有想明白,有人这么解释:select 只能说明 socket 可读或者可写,不能说明能读入或者能写出多少数据。比如,socket 的写缓冲区有 10 个字节的空闲空间,这时监视的 select 返回,然后在该 socket 上进行写操作。但是如果要写入 100 字节,如果 socket 没有设置非阻塞,调用 write 就会阻塞在那里。

 

 

4. accept

我们虽然没有把服务器的socket设置为非阻塞模式,但我们可以说一下非阻塞的accept。

 

在select模式下,listening socket设置为非阻塞的原因是什么??

当用 select 监视 listening socket 时, 如果有新连接到来,select 返回, 该 listening socket 变为可读。然后我们 accept 接收该连接。


首先说明一下 已完成3次握手的连接在 accept 之前 被 异常终止(Aborted )时发生的情况,如下图:



一个连接被异常终止时执行的动作取决于实现:
(1). 基于 Berkeley 的实现完全由内核处理该异常终止的连接, 应用进程看不到。
(2). 基于 SVR4 的实现,在连接异常终止后调用 accept 时,通常会给应用进程返回 EPROTO 错误。但是 Posix 指出应该返回 ECONNABORTED 。Posix 认为当发生致命的协议相关的错误时,返回 EPROTO 错误。而 异常终止一个连接并非致命错误,从而返回 ECONNABORTED ,与 EPROTO 区分开来,这样随后可以继续调用 accept 。

 

现在假设是基于 Berkeley 的实现,在 select 返回后,accept 调用之前,如果连接被异常终止,这时 accept 调用可能会由于没有已完成的连接而阻塞,直到有新连接建立。对于服务进程而言,在被 accept 阻塞的这一段时间内,将不能处理其他已就绪的 socket 。

解决上面这个问题有两种方法:
(1). 在用 select 监视 listening socket 时,总是将 listening socket 设为非阻塞模式。
(2). 忽略 accept 返回的以下错误:
    EWOULDBLOCK(基于 berkeley 实现,当客户端异常终止连接时)、ECONNABORTED(基于 posix 实现,当客户端异常终止连接时)、EPROTO(基于 SVR4 实现,当客户端异常终止连接时)以及 EINTR 。

 

5. 异常情况处理

  当对端机器crash或者网络连接被断开(比如路由器不工作,网线断开等),此时发送数据给对端然后读取本端socket会返回ETIMEDOUT或者EHOSTUNREACH 或者ENETUNREACH(后两个是中间路由器判断服务器主机不可达的情况)。

  当对端机器crash之后又重新启动,然后客户端再向原来的连接发送数据,因为服务器端已经没有原来的连接信息,此时服务器端回送RST给客户端,此时客户端读本地端口返回ECONNRESET错误。

  当服务器所在的进程正常或者异常关闭时,会对所有打开的文件描述符进行close,因此对于连接的socket描述符则会向对端发送FIN分节进行正常关闭流程。对端在收到FIN之后端口变得可读,此时读取端口会返回0表示到了文件结尾(对端不会再发送数据)。 

  当一端收到RST导致读取socket返回ECONNRESET,此时如果再次调用write发送数据给对端则触发SIGPIPE信号,信号默认终止进程,如果忽略此信号或者从SIGPIPE的信号处理程序返回则write出错返回EPIPE。

  可以看出只有当本地端口主动发送消息给对端才能检测出连接异常中断的情况,搭配select进行多路分离的时候,socket收到RST或者FIN时候,select返回可读(心跳消息就是用于检测连接的状态)。也可以使用socket的KEEPLIVE选项,依赖socket本身侦测socket连接异常中断的情况。

 

6. 描述符的I/O什么时候准备好

这个问题在unix network programing中有详细说明

We have been talking about waiting for a descriptor to become ready for I/O (reading or writing) or to have an exception condition pending on it (out-of-band data). While readability and writability are obvious for descriptors such as regular files, we must be more specific about the conditions that cause select to return "ready" for sockets (Figure 16.52 of TCPv2).

   1. A socket is ready for reading if any of the following four conditions is true:
         a. The number of bytes of data in the socket receive buffer is greater than or equal to the current size of the low-water mark for the socket receive buffer. A read operation on the socket will not block and will return a value greater than 0 (i.e., the data that is ready to be read). We can set this low-water mark using the SO_RCVLOWAT socket option. It defaults to 1 for TCP and UDP sockets.(也就是说如果读缓冲区有大于等于设定的最低刻度线时可读,一般最低刻度线是1,也就是说只要有数据就可读,我们也可以通过设置改变这个值)
         b. The read half of the connection is closed (i.e., a TCP connection that has received a FIN). A read operation on the socket will not block and will return 0 (i.e., EOF).
         c. The socket is a listening socket and the number of completed connections is nonzero. An accept on the listening socket will normally not block, although we will describe a timing condition in Section 16.6 under which the accept can block.
         d. A socket error is pending. A read operation on the socket will not block and will return an error (–1) with errno set to the specific error condition. These pending errors can also be fetched and cleared by calling getsockopt and specifying the SO_ERROR socket option.


   2. A socket is ready for writing if any of the following four conditions is true:
         a. The number of bytes of available space in the socket send buffer is greater than or equal to the current size of the low-water mark for the socket send buffer and either: (i) the socket is connected, or (ii) the socket does not require a connection (e.g., UDP). This means that if we set the socket to nonblocking (Chapter 16), a write operation will not block and will return a positive value (e.g., the number of bytes accepted by the transport layer). We can set this low-water mark using the SO_SNDLOWAT socket option. This low-water mark normally defaults to 2048 for TCP and UDP sockets.
         b. The write half of the connection is closed. A write operation on the socket will generate SIGPIPE (Section 5.12).
         c. A socket using a non-blocking connect has completed the connection, or the connect has failed.
         d. A socket error is pending. A write operation on the socket will not block and will return an error (–1) with errno set to the specific error condition. These pending errors can also be fetched and cleared by calling getsockopt with the SO_ERROR socket option.


   3. A socket has an exception condition pending if there is out-of-band data for the socket or the socket is still at the out-of-band mark. (We will describe out-of-band data in Chapter 24.)

          Our definitions of "readable" and "writable" are taken directly from the kernel's soreadable and sowriteable macros on pp. 530–531 of TCPv2. Similarly, our definition of the "exception condition" for a socket is from the soo_select function on these same pages.

Notice that when an error occurs on a socket, it is marked as both readable and writable by select.

 

用更形象的图表来表示为

 

 

 

 

 

参考

http://hi.baidu.com/motadou/blog/item/02d506ef941421232df534fc.html

http://www.cnitblog.com/zouzheng/archive/2010/11/25/71711.html

《unix network programing volume 1》

 


 

 

 

原创粉丝点击