UNP卷一chapter16 非阻塞式I/O

时间:2022-04-25 22:49:57

阻塞套接字意味着当发出一个不能立即完成的套接字调用时,其进程将投入睡眠,等待相应操作完成。

非阻塞套接字,如果输入操作不能被满足(对于tcp套接字即至少有一个字节的数据可读,对于udp套接字即有一个完整的数据报可读),相应调用将立即返回一个EWOULDBLOCK错误。

注意:selectc通常结合非阻塞式I/O一起使用

1、非阻塞读和写:str_cli函数的开发

如果套接字发送缓冲区已满,writen调用将会阻塞。在进程阻塞于writen调用期间,可能有来自套接字接收缓冲区的数据区的数据可供读取(偏偏由于进程被阻塞而读不了)。同理,如果从套接字中有一行输入文本可读,那么照样可能阻塞于后续的write调用。这个时候就显示出非阻塞读和写的好处,但非阻塞式I/O的加入str_cli函数的缓冲区管理显著地复杂化。于是可简化版本就是把应用程序任务划分到多个进程(使用fork)或多个线程。

首先介绍一下容纳从标准输入到套接字的数据的buff和从套接字到标准输入出的buff,如下所示:

UNP卷一chapter16 非阻塞式I/O

相应代码此处贴出(实在是有点长,增加了复杂性)

/* include nonb1 */
#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;

	//以下是利用fcntl函数设置套接字为非阻塞的惯用手法
	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;	/* initialize buffer pointers */
	friptr = froptr = fr;
	stdineof = 0;

	maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
	for (; ; ) {
		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 */

		Select(maxfdp1, &rset, &wset, NULL, NULL);
		/* end nonb1 */
		/* include nonb2 */
		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) {//遇见EOF结束符
#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 */
			}
		}

		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 */
			}
		}
		/* end nonb2 */
		/* include nonb3 */
		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 */
				}
			}
		}
	}
}
/* end nonb3 */

str_cli的较简单版本(把应用程序任务划分到多个进程(使用fork)

UNP卷一chapter16 非阻塞式I/O

#include	"unp.h"

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

	if ((pid = Fork()) == 0) {		/* child: server -> stdout */
		while (Readline(sockfd, recvline, MAXLINE) > 0)
			Fputs(recvline, stdout);
		/*当子进程读到EOF时,表明服务器进程终止,必须终止父进程从stdin到sockfd复制data*/
		kill(getppid(), SIGTERM);	/* in case parent still running */
		exit(0);
	}

	/* parent: stdin -> server */
	while (Fgets(sendline, MAXLINE, fp) != NULL)
		Writen(sockfd, sendline, strlen(sendline));
	//此处shutdown关闭写半部分,因为子进程还可能存在读,但不能用close(还出于存在引用计数问题考虑)
	Shutdown(sockfd, SHUT_WR);	/* EOF on stdin, send FIN */
	pause();
	return;
}

2、非阻塞connect

当在一个非阻塞的tcp套接字上调用connect时,connect将立即返回一个EINPROGESS错误,另如果已将发起的tcp三路握手继续进行。于是关天select和非阻塞connect的两个规则i、当连接成功建立时,描述符变为可写;ii、当连接建立遇到错误时,描述符变为既可读又可写。

#include	"unp.h"

int
connect_nonb(int sockfd, const SA *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;
	if ((n = connect(sockfd, saptr, salen)) < 0)//期待错误EINPROGRESS,表示建立已连接但未完成
		if (errno != EINPROGRESS)               //此是non-block socket connect的特点
			return(-1);

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

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

	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	tval.tv_sec = nsec;
	tval.tv_usec = 0;

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

	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {//如果套接字变为可读或可写时,调用getsockopt取得套接字的待处理错误
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)//error值为0表示禁止option,非0表示启用option
			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);
}

3、非阻塞accept

很矛盾哈,这是因为select告诉我们该套接字上已有连接就绪,那么随后的accept调用不应该阻塞,因为此时阻塞完全没必要。但有时有特定的条件时,还是需要非阻塞accept的,由于情况比较特殊,此处就不,贴代码了,详情见书上P363页。

以上知识点来均来自steven先生所著UNP卷一(version3),刚开始学习网络编程,如有不正确之处请大家多多指正。