《网络编程》非阻塞 I/O

概述

        在前面文章中,我们介绍了 I/O 的五种模型《I/O 模型》。从那里可以知道,非阻塞式的 I/O 是进程调用 I/O 操作时,若数据未准备就绪,则立即返回一个 EWOULDBLOCK 错误,在数据准备就绪之前,应用进程采用轮询的方式检查数据是否准备就绪。直到数据准备就绪,则内核把该数据复制到应用进程的缓冲区,完成数据复制之前进程处于阻塞状态,直到数据复制完成后才返回。即 I/O 操作第一阶段处于轮询检查状态,第二阶段处于阻塞状态。

套接字的 I/O 操作默认状态是采用阻塞式。即当不能立即完成套接字调用时,其进程会处于阻塞状态,直到相应操作完成。阻塞套接字大致可分为以下四种类型:

  1. 输入操作引起的阻塞状态,包括 read、readv、recv、recvfrom 和 recvmsg 函数。若某个进程对一个阻塞的 TCP 调用这些输入函数,且此时套接字的接收缓冲区没有可读数据,则该进程会处于阻塞状态,直到有一些数据到达。对于字节流协议的 TCP 来说,只要有一些数据到达,哪怕是单个字节数据,或者是完成的 TCP 报文段数据,该进程都会被唤醒;对于数据报协议的 UDP 来说,若一个阻塞的 UDP 套接字接收缓冲区为空,则该进程也会处于阻塞状态,直到完整 UDP 数据报到达;对于非阻塞的套接字,若不能满足输入操作要求,则会立即返回一个 EWOULDBLOCK 错误;
  2. 输出操作引起的阻塞状态,包括 write、writev、send、sendto 和 sendmsg 函数。对于一个阻塞 TCP 套接字,内核将从应用进程的缓冲区复制数据到该套接字的发送缓冲区,若该套接字的发送缓冲区没有存储空间,则进程会进入阻塞状态,直到有空间为止;对于一个非阻塞的 TCP 套接字,若其发送缓冲区没有存储空间,则输出函数调用立即返回一个 EWOULDBLOCK 错误,当发送缓冲区有空间时,则内核把应用进程缓冲区的数据复制到发送缓冲区中,并返回已复制的字节数;由于 UDP 套接字不存在真正的发送缓冲区,内核只是将应用进程的数据复制并把它沿协议栈向下传送,因此对于一个阻塞 UDP 套接字调用输出函数时不会和 TCP 一样的原因而阻塞,是因为其他的原因(在书本上没有说明是什么原因);
  3. 接受连接请求引起的阻塞状态,即 accept 函数。当阻塞套接字调用 accept 函数时,若没有新的连接请求,则进程就会进入阻塞状态;非阻塞套接字调用 accept 函数时,且不存在新的连接请求,则 accept 函数调用立即返回一个 EWOULDBLOCK 错误;
  4. 发出连接请求引起的阻塞状态,即用于 TCP 的 connect 函数。由于 TCP 的连接建立需要三次握手过程,且 connect 函数一直等待客户端收到对自己的 SYN 的应答响应 ACK 才返回,因此,每个 connect 发起的连接请求在 RTT 时间内处于阻塞状态;对于 非阻塞 TCP 套接字调用 connect 函数时,若连接不能立即建立,则连接请求正常发出,但是会返回一个 EINPROGRESS 错误,但此时已经发起的 TCP 连接请求三次握手过程会继续进行;


非阻塞读写

        在非阻塞读写的编程中,维护两个缓冲区:to 容纳从标准输入到服务器去的数据,fr 容纳自服务器到标准输出来的数据,这两个缓冲区具体结构如下图所示:

        其中 toiptr 指针指向标准输入读入的数据可以存放的下一个字节,tooptr 指向下一个必须写到套接字的字节。一旦 tooptr 移动到 toiptr,则这两个指针就一起恢复到缓冲区开始处。




/* include nonb1 */
#include <sys/select.h>
#include <sys/socket.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXLINE 4096
inline int Max(int a, int b)
{
    return(a >= b?a:b);
}

extern void err_sys(const char *, ...);
extern void err_quit(const char *, ...);

static void set_fl(int fd, int flags);

void str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, stdineof;
	ssize_t		n, nwritten;
	fd_set		rset, wset;
	char		to[MAXLINE], fr[MAXLINE];
	char		*toiptr, *tooptr, *friptr, *froptr;

    /* 设置套接字描述符为非阻塞 */
    set_fl(sockfd, O_NONBLOCK);
    /* 设置标准输入为非阻塞 */
    set_fl(STDIN_FILENO, O_NONBLOCK);
    /* 设置标准输出为非阻塞 */
    set_fl(STDOUT_FILENO, O_NONBLOCK);

    /* 初始化两个缓冲区指针 */
	toiptr = tooptr = to;	/* initialize buffer pointers */
	friptr = froptr = fr;
	stdineof = 0;/* 标准输入键入EOF的标志 */

	maxfdp1 = Max(Max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
	for ( ; ; ) {
        /* 初始化,为调用select函数做准备 */
		FD_ZERO(&rset);
		FD_ZERO(&wset);
		if (stdineof == 0 && toiptr < &to[MAXLINE])
			FD_SET(STDIN_FILENO, &rset);	/* read from stdin */
		if (friptr < &fr[MAXLINE])
			FD_SET(sockfd, &rset);			/* read from socket */
		if (tooptr != toiptr)
			FD_SET(sockfd, &wset);			/* data to write to socket */
		if (froptr != friptr)
			FD_SET(STDOUT_FILENO, &wset);	/* data to write to stdout */

		if(select(maxfdp1, &rset, &wset, NULL, NULL) < 0)
            err_sys("select error");
/* end nonb1 */
/* include nonb2 */
        /* 若标准输入在rset有效,则从标准输入读取数据到发送缓冲区 */
		if (FD_ISSET(STDIN_FILENO, &rset))
        {
			if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0)
            {
				if (errno != EWOULDBLOCK)
					err_sys("read error on stdin");

			}
            else if (n == 0)
            {
				stdineof = 1;			/* all done with stdin */
				if (tooptr == toiptr)
					if(shutdown(sockfd, SHUT_WR) < 0)/* send FIN */
                        err_sys("shutdown error");

			}
            else
            {
				toiptr += n;			/* # just read */
				FD_SET(sockfd, &wset);	/* try and write to socket below */
			}
		}

        /* 若套接字在rset有效,则从套接字读取数据到接收缓冲区中 */
		if (FD_ISSET(sockfd, &rset))
        {
			if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0)
            {
				if (errno != EWOULDBLOCK)
					err_sys("read error on socket");

			}
            else if (n == 0)
            {
				if (stdineof)
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");

			}
            else
            {
				friptr += n;		/* # just read */
				FD_SET(STDOUT_FILENO, &wset);	/* try and write below */
			}
		}
/* end nonb2 */
/* include nonb3 */
        /* 若标准输出在wset有效,则从接收缓冲区写数据到标准输出 */
		if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0))
        {
			if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0)
            {
				if (errno != EWOULDBLOCK)
					err_sys("write error to stdout");

			}
            else
            {
				froptr += nwritten;		/* # just written */
				if (froptr == friptr)
					froptr = friptr = fr;	/* back to beginning of buffer */
			}
		}

        /* 若套接字在wset有效,则从发送缓冲区写数据到套接字中 */
		if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0))
        {
			if ( (nwritten = write(sockfd, tooptr, n)) < 0)
            {
				if (errno != EWOULDBLOCK)
					err_sys("write error to socket");

			}
            else
            {
				tooptr += nwritten;	/* # just written */
				if (tooptr == toiptr) {
					toiptr = tooptr = to;	/* back to beginning of buffer */
                    /* 若套接字接收来自标准输入的数据,则关闭套接字写端,相当于从标准输入键入EOF */
					if (stdineof)
						if(shutdown(sockfd, SHUT_WR) <0)	/* send FIN */
                            err_sys("Shutdown error");
				}
			}
		}
	}
}
/* end nonb3 */

static void set_fl(int fd, int flags)
{
    int val;
    /* 获取描述符状态标志 */
    if( (val = fcntl(fd, F_GETFL, 0)) < 0)
        err_sys("fcntl get error");
    /* 添加描述符状态标志flags*/
    val |= flags;

    /* 设置描述符状态标志 */
    if(fcntl(fd, F_SETFL, val) < 0)
        err_sys("fcntl set error");
}

非阻塞 connect 


非阻塞 connect 有以下好处:

  1. 发起连接请求 connect时,在处于 TCP 连接三次握手过程的时间,即 RTT 时间内,套接字可以处理其他事务,不必处于阻塞状态;
  2. 在等待连接建立成功期间,套接字可以发起多个连接请求;
  3. 在非阻塞 connect 中使用到 select 函数,因此可以通过 select 参数自由设置超时时间,不必由系统限制 connect 的超时时间;

select 判断规则:

  1. 如果 select 返回 -1,表示 select 出错,可以关闭 socket 套接字,重新发起连接过程;
  2. 如果 select 返回 0,表示在 select 超时,超时时间内未能成功建立连接,也可以再次执行 select 进行检测,如若多次超时,需返回超时错误给用户;
  3. 如果 select 返回大于 0 的值,则说明检测到可读或可写的套接字描述符。源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:
    • 1)当套接字连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写);
    • 2)当套接字连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写);
        因此,当发现套接口描述符 可读或可写时,可进一步判断是连接成功还是出错。这里必须将 2)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时 select 同样会返回非阻塞 socket 描述符既可读又可写。
        对于 Unix 环境,可通过调用 getsockopt 来检测描述符集合是连接成功还是出错,但是该方法在 Linux 环境上测试是无效的。因为在 Linux 下无论网络是否发生错误,getsockopt 始终返回 0,不返回-1。若采用 getsockopt 来检查:
  1. 如果连接建立是成功的,则通过 getsockopt(sockfd,SOL_SOCKET,SO_ERROR, &error,&len) 获取的 error 值将是 0;
  2. 如果建立连接时遇到错误,则 errno 的值是连接错误所对应的 errno 值,比如ECONNREFUSED,ETIMEDOUT 等;
        在 Linux 环境下可以使用以下方法进行测试连接是成功还是出错:再次调用connect,相应返回失败,如果错误 errno 是EISCONN,表示 socket 连接已经建立,否则认为连接失败。即在一次 select 调用之后,若发现此时套接口描述字 可读或可写,则再次执行 connect 调用,此时 errno 始终仍为 EINPROGRESS,则再次执行 select 和 connect 函数,直到 errno 被置为EISCONN,表示 connect 成功。

非阻塞 connect 编程步骤:

  1. 第一步:调用 socket 创建套接字,并使用 fcntl 函数使该套接字变为非阻塞式; 
  2. 第二步:调用 connect 函数请求建立连接,并判断连接是否成功建立;
    • 若 connect 调用返回 0,则表示连接请求成功建立;
    • 若 connect 调用返回 -1,首先检查 errno 错误类型,若不为 EINPROGRESS 错误,则直接退出,否则只是当前连接不能立即建立,但是已经发起的 TCP 连接请求三次握手过程会继续进行,此时调用 select 函数判断连接是否建立成功:
      • 若 select 调用返回 0,则表示 select 超时期限内不能成功建立连接,则此时返回一个超时错误,且关闭该链接,以防止 TCP 连接的三次握手过程继续进行;
      • 若 select 调用返回正值,则表示在超时期限内检查到套接字可读或可写或异常,若可读或可写,在 Unix 系统中,此时通过调用 getsockopt 函数检查连接状态,若连接成功,则该值为 0,若连接建立发生错误,则该值是对应连接错误的 errno 值;

        假设在调用 select 函数之前连接已经建立,并服务器发送的数据已到达客户端,此时非阻塞套接字处于即可读又可写状态。然而由 select 函数调用返回大于 0 值时,使用 getsockopt 检查到连接出错时,非阻塞套接字也是 既可读又可写。这样就会导致移植性问题,我们可以使用下列方法代替 getsockopt 调用:

  1. 调用 getpeername 代替 getsockopt。若 getpeername 以 ENOTCONN 错误失败返回,则表示连接建立失败,紧接着必须以 SO_ERROR 调用 getsockopt 取得套接字上待处理的错误;
  2. 以值为 0 的长度参数调用 read 函数。若 read 调用失败,表示 connect 连接失败,read 返回的 errno 给出连接失败的原因,若连接建立成功,则 read 返回 0;
  3. 再一次调用 connect 函数。如果返回错误 EISCONN,表示套接字已经连接,即连接建立成功; 

#include <sys/select.h>
#include <sys/socket.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern void err_quit(const char *, ...);
int
connect_nonb(int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec)
{
	int				flags, n, error;
	socklen_t		len;
	fd_set			rset, wset;
	struct timeval	tval;

    /* 将套接字设置为非阻塞状态 */
	flags = fcntl(sockfd, F_GETFL, 0);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

	error = 0;
    /* 发起连接请求,若返回EINPROGRESS,表示连接建立已经启动但是尚未完成 */
	if ( (n = connect(sockfd, saptr, salen)) < 0)
		if (errno != EINPROGRESS)/* 若是其他错误,则表示连接建立失败,直接退出 */
			return(-1);

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

    /* 连接建立成功 */
	if (n == 0)
		goto done;	/* connect completed immediately */

    /* 若返回EINPROGRESS错误,表示连接建立已经启动但是尚未完成;
     * 此时调用select函数检查连接状态;
     */
	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	tval.tv_sec = nsec;
	tval.tv_usec = 0;
    /* 若select返回0,表示连接请求超时,建立连接失败,关闭该链接,并设置errno错误类型 */
	if ( (n = select(sockfd+1, &rset, &wset, NULL,
					 nsec ? &tval : NULL)) == 0) {
		close(sockfd);		/* timeout */
		errno = ETIMEDOUT;
		return(-1);
	}
    /* 若select返回大于0,则此时套接字处于可读或可写状态;
     * 则此时调用getsockopt函数检查连接状态,判断是连接成功还是失败;
     */
	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
			return(-1);			/* Solaris pending error */
	} else
		err_quit("select error: sockfd not set");

done:
	fcntl(sockfd, F_SETFL, flags);	/* restore file status flags */

	if (error) {
		close(sockfd);		/* just in case */
		errno = error;
		return(-1);
	}
	return(0);
}

非阻塞 accept 

(1)使用 select 获悉某个监听套接字上何时有已完成连接准备好被 accept 时,总是把这个监听套接字设置为非阻塞。
(2)在后续的 accept 调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现,客户终止连接时),ECONNABORTED(POSIX实现,客户终止连接时),EPROTO(SVR4实现,客户终止连接时)和 EINTR(如果有信号被捕获)



参考资料:

《Unix 网络编程》

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值