• UNPv1笔记9 非阻塞IO 我爱美女的日志 网易博客


    UNPv1笔记9 -- 非阻塞IO - 我爱美女的日志 - 网易博客

    UNPv1笔记9 -- 非阻塞IO   

    2009-06-02 23:32:15|  分类: tcpipsocket |  标签: |字号 订阅



    非阻塞读写

    默认 socket 是阻塞的,读写函数 read, readv, recv, recvfrom, recvmsg 以及 write, writev, send, sendto, sendmsg 都有可能会阻塞。可以将 socket 描述字设为非阻塞,这样,当 socket 描述字未就绪时,调用以上读写函数将会返回 EWOULDBLOCK 或 EAGAIN 。

    UNPv1 给出了一个 非阻塞socket + select 的例子。有人对此提出疑问:


    假如fd1是一个阻塞socket,我将它加入select的readset中,然后用select去侦听fd1上是否有数据到来。我感觉这和非阻塞 socket的性质是一样的,因为它不会阻塞在fd1的recv 函数上,因为之前select已经判定到fd1可读,所以recv 就会返回不会阻塞。 那么为什么大家总要还要创建一个非阻塞的socket加入select中呢

    有人给出的答案是:
    select 只能说明 socket 可读或者可写,不能说明能读入或者能写出多少数据。比如,socket 的写缓冲区有 10 个字节的空闲空间,这时监视的 select 返回,然后在该 socket 上进行写操作。但是如果要写入 100 字节,如果 socket 没有设置非阻塞,调用 write 就会阻塞在那里。而更为要紧的是,在多个 socket 的情况下,读写一个socket 时阻塞,会影响到其他的 socket 。



    UNPv1 上的例子如下:

    1 #include     "unp.h"

    2 void
    3 str_cli(FILE *fp, int sockfd)
    4 {
    5 int maxfdp1, val, stdineof;
    6 ssize_t n, nwritten;
    7 fd_set rset, wset;
    8 char to[MAXLINE], fr[MAXLINE];
    9 char *toiptr, *tooptr, *friptr, *froptr;

    10 val = Fcntl(sockfd, F_GETFL, 0);
    11 Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

    12 val = Fcntl(STDIN_FILENO, F_GETFL, 0);
    13 Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);

    14 val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
    15 Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

    16 toiptr = tooptr = to; /* initialize buffer pointers */
    17 friptr = froptr = fr;
    18 stdineof = 0;

    19 maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
    20 for ( ; ; ) {
    21 FD_ZERO(&rset);
    22 FD_ZERO(&wset);
    23 if (stdineof == 0 && toiptr < &to[MAXLINE])
    24 FD_SET(STDIN_FILENO, &rset); /* read from stdin */
    25 if (friptr < &fr[MAXLINE])
    26 FD_SET(sockfd, &rset); /* read from socket */
    27 if (tooptr != toiptr)
    28 FD_SET(sockfd, &wset); /* data to write to socket */
    29 if (froptr != friptr)
    30 FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */

    31 Select(maxfdp1, &rset, &wset, NULL, NULL);

    32      if (FD_ISSET(STDIN_FILENO, &rset)) {
    33 if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
    34 if (errno != EWOULDBLOCK)
    35 err_sys("read error on stdin");

    36 } else if (n == 0) {
    37 fprintf(stderr, "%s: EOF on stdin\n", gf_time());
    38 stdineof = 1; /* all done with stdin */
    39 if (tooptr == toiptr)
    40 Shutdown(sockfd, SHUT_WR); /* send FIN */

    41 } else {
    42 fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(),
    43 n);
    44 toiptr += n; /* # just read */
    45 FD_SET(sockfd, &wset); /* try and write to socket below */
    46 }
    47 }

    48 if (FD_ISSET(sockfd, &rset)) {
    49 if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
    50 if (errno != EWOULDBLOCK)
    51 err_sys("read error on socket");

    52 } else if (n == 0) {
    53 fprintf(stderr, "%s: EOF on socket\n", gf_time());
    54 if (stdineof)
    55 return; /* normal termination */
    56 else
    57 err_quit("str_cli: server terminated prematurely");

    58 } else {
    59 fprintf(stderr, "%s: read %d bytes from socket\n",
    60 gf_time(), n);
    61 friptr += n; /* # just read */
    62 FD_SET(STDOUT_FILENO, &wset); /* try and write below */
    63 }
    64 }

    65      if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0)) {
    66 if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
    67 if (errno != EWOULDBLOCK)
    68 err_sys("write error to stdout");

    69 } else {
    70 fprintf(stderr, "%s: wrote %d bytes to stdout\n",
    71 gf_time(), nwritten);
    72 froptr += nwritten; /* # just written */
    73 if (froptr == friptr)
    74 froptr = friptr = fr; /* back to beginning of buffer */
    75 }
    76 }

    77 if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0)) {
    78 if ( (nwritten = write(sockfd, tooptr, n)) < 0) {
    79 if (errno != EWOULDBLOCK)
    80 err_sys("write error to socket");

    81 } else {
    82 fprintf(stderr, "%s: wrote %d bytes to socket\n",
    83 gf_time(), nwritten);
    84 tooptr += nwritten; /* # just written */
    85 if (tooptr == toiptr) {
    86 toiptr = tooptr = to; /* back to beginning of buffer */
    87 if (stdineof)
    88 Shutdown(sockfd, SHUT_WR); /* send FIN */
    89 }
    90 }
    91 }
    92 }
    93 }



    1 #include "unp.h"
    2 #include <time.h>

    3 char *
    4 gf_time(void)
    5 {
    6 struct timeval tv;
    7 static char str[30];
    8 char *ptr;

    9 if (gettimeofday(&tv, NULL) < 0)
    10 err_sys("gettimeofday error");

    11 ptr = ctime(&tv.tv_sec);
    12 strcpy(str, &ptr[11]);
    13 /* Fri Sep 13 00:00:00 1986\n\0 */
    14 /* 0123456789012345678901234 5 */
    15 snprintf(str + 8, sizeof(str) - 8, ".%06ld", tv.tv_usec);

    16 return (str);
    17 }
    这个例子中用到的两个 buffer 如下:


    UNPv1笔记9 -- 非阻塞IO - 我爱美女 - wangzws home



    UNPv1笔记9 -- 非阻塞IO - 我爱美女 - wangzws home

    由于上面这个例子 buffer 管理 太过复杂,作者又给出了 多进程 方式来替代 上面的 非阻塞 + select 。
    对于同一个 socket ,一个进程读,另一个进程写。


    UNPv1笔记9 -- 非阻塞IO - 我爱美女 - wangzws home


    1 #include     "unp.h"

    2 void
    3 str_cli(FILE *fp, int sockfd)
    4 {
    5 pid_t pid;
    6 char sendline[MAXLINE], recvline[MAXLINE];

    7 if ( (pid = Fork()) == 0) { /* child: server -> stdout */
    8 while (Readline(sockfd, recvline, MAXLINE) > 0)
    9 Fputs(recvline, stdout);

    10 kill(getppid(), SIGTERM); /* in case parent still running */
    11 exit(0);
    12 }

    13 /* parent: stdin -> server */
    14 while (Fgets(sendline, MAXLINE, fp) != NULL)
    15 Writen(sockfd, sendline, strlen(sendline));

    16 Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
    17 pause();
    18 return;
    19 }



    非阻塞 connect

    TCP socket 被设为非阻塞后调用 connect ,connect 函数会立即返回 EINPROCESS ,但 TCP 的 3 次握手继续进行。之后可以用 select 检查 连接是否建立成功。非阻塞 connect 有3 种用途:

    1. 在3 次握手的同时做一些其他的处理。
    2. 可以同时建立多个连接。
    3. 在利用 select 等待的时候,可以给 select 设定一个时间,从而可以缩短 connect 的超时时间。

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


    UNPv1 给出的 非阻塞 connect 的例子:

    1 #include     "unp.h"

    2 int
    3 connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
    4 {
    5 int flags, n, error;
    6 socklen_t len;
    7 fd_set rset, wset;
    8 struct timeval tval;

    9 flags = Fcntl(sockfd, F_GETFL, 0);
    10 Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    11 error = 0;
    12 if ( (n = connect(sockfd, saptr, salen)) < 0)
    13 if (errno != EINPROGRESS)
    14 return (-1);

    15 /* Do whatever we want while the connect is taking place. */

    16 if (n == 0)
    17 goto done; /* connect completed immediately */

    18 FD_ZERO(&rset);
    19 FD_SET(sockfd, &rset);
    20 wset = rset;
    21 tval.tv_sec = nsec;
    22 tval.tv_usec = 0;

    23 if ( (n = Select(sockfd + 1, &rset, &wset, NULL,
    24 nsec ? &tval : NULL)) == 0) {
    25 close(sockfd); /* timeout */
    26 errno = ETIMEDOUT;
    27 return (-1);
    28 }

    29 if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
    30 len = sizeof(error);
    31 if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
    32 return (-1); /* Solaris pending error */
    33 } else
    34 err_quit("select error: sockfd not set");

    35 done:
    36 Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */

    37 if (error) {
    38 close(sockfd); /* just in case */
    39 errno = error;
    40 return (-1);
    41 }
    42 return (0);
    43 }

    注意事项:
    1. 如果在调用 select 之前,连接已经建立成功,并且有数据发送过来了,这时套接字将是即可读又可写,和连接失败时是一样的。所以我们必须用 getsockopt 来检查套接字的状态。
    2. 由于 socket 可写并不能说明连接是否成功建立,可以用以下几种方法取代 getsockopt 来检查连接到底是不是成功建立。
    1)调用 getpeername ,如果调用失败,返回 ENOTCONN ,表示连接失败。可以用 getsockopt (SO_ERROR) 获取 socket 上待处理的错误。
    2)调用 read ,长度参数为 0 , 如果read 失败,表明 connect 失败,而且 read 返回的 errno 指明了连接失败的原因。如果连接成功,read 返回 0 。
    3)在调用 connect 一次,这是应该失败,如果错误号为 EISCONN ,表明连接已经成功建立。


    被中断的 connect

    在阻塞的 socket 上 调用 connect ,在 TCP 3 次握手完成之前被信号中断,如果 connect 不被重启,将返回 EINTR 。但是不能再调用 connect 来完成连接,这样做会返回 EADDRINUSE 。
    这时需要做的是调用 select ,如同非阻塞 socket 上调用 select 一样。select 返回时表明连接成功(socket 可写)或连接失败(socket 可读可写)。




    非阻塞 accept

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

    问题是:accept 时,将 listening socket 设置为 非阻塞 的必要性是什么?


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

    UNPv1笔记9 -- 非阻塞IO - 我爱美女 - wangzws home


    一个连接被异常终止时执行的动作取决于实现:
    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 。
  • 相关阅读:
    07组 Beta冲刺 (2/5)
    第07组 Beta冲刺 (1/5)
    第07组 Alpha冲刺 总结
    ES相关
    集群与分布式
    idea使用总结
    Tomcat配置与启动与访问
    Web基础
    B/S与C/S架构
    一周视频学习总结
  • 原文地址:https://www.cnblogs.com/lexus/p/2857746.html
Copyright © 2020-2023  润新知