非阻塞式 I/O

非阻塞式 I/O

概述

非阻塞式 I/O, 相对复杂, 增加了很繁琐的输入输出缓冲区, 通常讲解此类 I/O 会用下面这张图来描述, 对于非阻塞式的涉及来说, 光这一张图的讲解太过敷衍.

非阻塞式 I/O IO

先大致描述以下这张图, 进程调用 recvfrom 方法, 向内核获取 I/O 数据(也就是输入输出, 缓冲流的数据), 如果内核有数据, 则复制数据并返回结果, 如果没有, 则返回 BWOULDBLOCK 标志(不同系统标志不尽相同). 仅单纯的使用非阻塞式系统调用, 性能不会太好, 需要有很多复杂的缓冲流维护.

相较于阻塞式 I/O

了解非阻塞式 I/O, 必然要对 阻塞式 I/O 的痛点有一定的了解.

    int         maxfdp1, stdineof;
    fd_set      rset;
    char        buf[MAXLINE];
    int     n;

	// 标识符, 标志是否处理客户输入
    stdineof = 0;
    FD_ZERO(&rset);
    for ( ; ; ) {
        if (stdineof == 0) // 当且仅当标识符为默认状态下, 才服务客户端输入的数据
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;
        Select(maxfdp1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(sockfd, &rset)) {  /* socket 可读 */
            if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                if (stdineof == 1) return;     /* 正常终端 */
                else err_quit("str_cli: server terminated prematurely");
            }
            Write(fileno(stdout), buf, n);
        }

        if (FD_ISSET(fileno(fp), &rset)) {  /* 标准输入可读(用户开始输入数据) */
			// 处理完用户的所有数据, 将 stdineof 标志符激活, 此时不再处理客户数据
            if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
                stdineof = 1;
                Shutdown(sockfd, SHUT_WR);  /* 发送 FIN 标志, 终止对套接字的写操作 */
                FD_CLR(fileno(fp), &rset);
                continue;
            }
            Writen(sockfd, buf, n);
        }
    }
  1. 当标准输入可读, 本进程便向标准输入索要数据, 并将得到的数据通过 writen 写入 sockfd 发送缓冲区(buf)中, 但是, 如果 sockfd 发送缓冲区已经被写满(网络过慢, 还来不及发送给服务器), 此时, 进程阻塞于 writen 操作.

  2. 当套接字缓冲区有数据可读, sockfd 可读, 进程将获取到的数据通过 write 写到准输出, 但是如果 write 操作的速度甚至慢于网络传输, 那么进程将阻塞于写操作, 无法顾及服务端新发送的数据.

由于以上两点原因, 我们可以将 io 操作拆的更细一点, 这里将从标准输入读, 写入套接字, 套接字读, 套接字写入标准输入四部分全部拆出来, 只要有任意一部分 I/O 就绪, 则执行. 为了监听套接字缓冲区是否占满或者是否为空, 我们需要在添加两个缓冲区分别缓冲套接字发送缓冲区和套接字接收缓冲区.

从标准输入到服务器的数据

从标准输入到服务器的数据

从套接字到标准输出的数据

非阻塞式 I/O 时间线


这里需要使用非阻塞式 I/O, 即尝试读写, 失败则跳过继续, 防止进程阻塞.

#include	"unp.h"

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

	// 监听套接字, 并设置为非阻塞
	val = Fcntl(sockfd, F_GETFL, 0);
	Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
	// 监听标准输入, 并设置为非阻塞
	val = Fcntl(STDIN_FILENO, F_GETFL, 0);
	Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
	// 监听标准输出, 并设置为非阻塞
	val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
	Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

	toiptr = tooptr = to;	/* 初始化 fr, to 两个缓冲区 */
	friptr = froptr = fr;
	stdineof = 0;

	maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
	for ( ; ; ) {
		FD_ZERO(&rset);
		FD_ZERO(&wset);
		// 当输入缓冲区(to)有空闲位置可以存放标准输入的数据, 则监听 STDIN_FILENO
		if (stdineof == 0 && toiptr < &to[MAXLINE])
			FD_SET(STDIN_FILENO, &rset);	/* 从标准输入开始读 */
		// 当套接字缓冲区(fr)有空闲位置可以接收 socket 数据, 监听套接字的读操作
		if (friptr < &fr[MAXLINE])
			FD_SET(sockfd, &rset);			/* 读 socket */
		// 当存在需要写入套接字的数据, 开始监听套接字的写操作
		if (tooptr != toiptr)
			FD_SET(sockfd, &wset);			/* 写入 socket */
		// 当发往标准输出区域有数据, 则开始监听标准输出的写操作
		if (froptr != friptr)
			FD_SET(STDOUT_FILENO, &wset);	/* 写入数据到标准输出 */

		// 此时, 根据上述条件开始选择性的监听套接字的读和写操作.
		Select(maxfdp1, &rset, &wset, NULL, NULL);

		// 当输入缓冲区(to)有空闲位置可以存放标准输入的数据, 则监听 STDIN_FILENO
		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) {
#ifdef	VOL2
				fprintf(stderr, "%s: EOF on stdin\n", gf_time());
#endif
				stdineof = 1;			/* all done with stdin */
				if (tooptr == toiptr)
					Shutdown(sockfd, SHUT_WR);/* send FIN */

			} else {
#ifdef	VOL2
				fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);
#endif
				toiptr += n;			/* # just read */
				FD_SET(sockfd, &wset);	/* try and write to socket below */
			}
		}

		// 当套接字缓冲区(fr)有空闲位置可以接收 socket 数据, 监听套接字的读操作
		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) {
#ifdef	VOL2
				fprintf(stderr, "%s: EOF on socket\n", gf_time());
#endif
				if (stdineof)
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");

			} else {
#ifdef	VOL2
				fprintf(stderr, "%s: read %d bytes from socket\n",
								gf_time(), n);
#endif
				friptr += n;		/* # just read */
				FD_SET(STDOUT_FILENO, &wset);	/* try and write below */
			}
		}
		
		// 当存在需要写入套接字的数据, 开始监听套接字的写操作
		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 {
#ifdef	VOL2
				fprintf(stderr, "%s: wrote %d bytes to stdout\n",
								gf_time(), nwritten);
#endif
				froptr += nwritten;		/* # just written */
				if (froptr == friptr)
					froptr = friptr = fr;	/* back to beginning of buffer */
			}
		}

		// 当发往标准输出区域有数据, 则开始监听标准输出的写操作
		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 {
#ifdef	VOL2
				fprintf(stderr, "%s: wrote %d bytes to socket\n",
								gf_time(), nwritten);
#endif
				tooptr += nwritten;	/* # just written */
				if (tooptr == toiptr) {
					toiptr = tooptr = to;	/* back to beginning of buffer */
					if (stdineof)
						Shutdown(sockfd, SHUT_WR);	/* send FIN */
				}
			}
		}
	}
}

以上便实现了单进程的 I/O 复用且非阻塞式的套接字编程.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值