UNIX网络编程三.非阻塞式I/O

本文介绍非阻塞IO技术在客户端编程中的应用,包括非阻塞读写、非阻塞connect与accept的实现细节。同时提供了一种利用多进程简化非阻塞IO复杂性的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.非阻塞读和写

主要是针对str_cli函数的改写

1.1 阻塞IO代码

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: tcpcli <IPaddress>");

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);				//服务端服务端口9877
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);	//命令行传入服务端地址

	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

	str_cli(stdin, sockfd);		/* 改写!! */

	exit(0);
}


void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, stdineof;
	fd_set		rset;
	char		buf[MAXLINE];
	int		n;

	stdineof = 0;						//标志位
	FD_ZERO(&rset);
	for ( ; ; ) {
		if (stdineof == 0)				//标准输入stdin只要没有遇到EOF,就关注stdin的可读性
			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 is readable */
			if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
				if (stdineof == 1)					/* 已经在标准输入遇到过EOF了,是正常终止*/
					return;							/* normal termination */
				else								/*服务器过早终止*/
					err_quit("str_cli: server terminated prematurely");
			}

			Write(fileno(stdout), buf, n);	//正常情况往stdout写
		}

		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
			if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
				stdineof = 1;				/* stdin遇到EOF*/
				Shutdown(sockfd, SHUT_WR);	/* send FIN 关闭写一半*/
				FD_CLR(fileno(fp), &rset);  /* stdin清0 */
				continue;
			}

			Writen(sockfd, buf, n);			//正常情况往服务器写
		}
	}
}

1.2 更新为非阻塞IO

2个客户端缓冲区
在这里插入图片描述在这里插入图片描述

/* 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;
	/*
		套接字sockfd, stdin, stdout均设置为nonblock
	*/
	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);

		if (FD_ISSET(STDIN_FILENO, &rset)) { /* stdin满足可读条件  */
			/*               描述符        地址起始    长度                 */
			if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
																/*从标准输入读,且填入缓冲区对应区域,但是遇到错误*/
				if (errno != EWOULDBLOCK)							/* #define	EWOULDBLOCK	EAGAIN */
					err_sys("read error on stdin");

			} else if (n == 0) {									/* 遇到EOF */
				stdineof = 1;										/* 标志位1 */
				if (tooptr == toiptr)								/* 没有要发往服务器的数据了 */
					Shutdown(sockfd, SHUT_WR);						/* stdin遇到了EOF,send FIN */

			} else {												/* 正常 */
				toiptr += n;										/* toiptr指针往后移动n(从stdin读了n个字节) */
				FD_SET(sockfd, &wset);								/* 让wset中socketfd这一位是打开的(从标准输入读入数据了,
																	   本次循环肯定是可以往socket写数据的) */
			}
		}

		if (FD_ISSET(sockfd, &rset)) {		/* sockfd可读(有从服务端回射回来的数据)*/
			if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("read error on socket");

			} else if (n == 0) {
				if (stdineof)			/* 正常终止:stdin遇到EOF */
					return;		
				else
					err_quit("str_cli: server terminated prematurely");

			} else {
				friptr += n;					/* friptr指针往后移动n(从sockfd读了n个字节) */
				FD_SET(STDOUT_FILENO, &wset);	/* 让wset中stdout这一位是打开的(从sockfd读入数据了,
												   本次循环肯定是可以往stdout写数据的) */
			}
		}
		/* 满足往stdout可写且有数据可以写*/
		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 */
			}
		}
		/* 满足往sockfd可写且有数据可以写*/
		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 */
					if (stdineof)			
						Shutdown(sockfd, SHUT_WR);	/* stdin遇到EOF,则send FIN */
				}
			}
		}
	}
}


1.3 更简单的多进程方法替代复杂的非阻塞法

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	pid_t	pid;
	char	sendline[MAXLINE], recvline[MAXLINE];
	/*子进程负责从套接字sockfd读,并写到stdout*/
	if ( (pid = Fork()) == 0) {		/* child: server -> stdout */
		while (Readline(sockfd, recvline, MAXLINE) > 0)
			Fputs(recvline, stdout);
		/*套接字sockfd无数据可读,子进程要结束了,所以要终止父进程 */
		kill(getppid(), SIGTERM);	/* in case parent still running */
		exit(0);
	}
	/* 父进程负责从stdin读入,往套接字sockfd写 */
		/* parent: stdin -> server */
	while (Fgets(sendline, MAXLINE, fp) != NULL)
		Writen(sockfd, sendline, strlen(sendline));
	/*stdin遇到EOF了,所以调shutdown发送FIN*/
	Shutdown(sockfd, SHUT_WR);	/* EOF on stdin, send FIN */
	/*父进程等待子进程发出的SIGTERM信号*/
	pause();
	return;
}

2.非阻塞connect

有关于select和非阻塞connect的以下2个规则:1)当连接成功建立时,描述符变为可写;2)当连接遇到错误时,描述符变为既可读又可写

描述符就绪条件

2.非阻塞accept

当一个已完成的连接准备好被accept的时候,select会把监听socket标记为可读。因此,如果用select等待外来的连接时,应该不需要把监听socket设置为非阻塞模式,因为如果select告诉我们连接已经就绪,accept就不应该被阻塞。不过这样做的时候有一个BUG:当客户端 在跟服务器建立连接之后发送了一个RST包,这个时候accept就会阻塞,直到有下一个已完成的连接准备好被accept为止。

代码引发rst

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	struct linger		ling;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: tcpcli <IPaddress>");

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

	ling.l_onoff = 1;		/* cause RST to be sent on close() */
	ling.l_linger = 0;
	Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
	Close(sockfd);

	exit(0);
}

struct linger的l_onoff标志设为1,l_linger设为0。这个时候,如果关闭TCP连接时,会先在socket上发送一个RST包。这个时候会出现下面的问题:
A: select向服务器返回监听socket可读,但是服务器要在一段时间之后才能调用accept;
B: 在服务器从select返回和调用accept之前,收到从客户发送过来的RST;
C: 这个已经完成的连接被从队列中删除,我们假设没有其它已完成的连接存在;
D: 服务器调用accept,但是由于没有其它已完成的连接存在,因而服务器被阻塞了;
注意,服务器会被一直阻塞在accept调用上,直到另外一个客户建立一个连接为止;但是如果一直没有其它客户建立连接,那么服务器将仍然一直被阻塞在accept调用上,不处理任何其他已就绪的socket;(本问题和之前的拒绝服务攻击多少有点类似,不过对于这个新的缺陷,一旦有客户建立一个连接,服务器就会脱出阻塞中的accept)

解决这个问题的办法是:
A:如果使用select来获知何时有链接已就绪可以accept时,总是把监听socket设置为非阻塞模式
B: 在后面的accept调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现在客户放弃连接时出现的错误)、 ECONNABORTED(Posix.1g的实现在客户放弃连接时出现的错误)、EPROTO(SVR4的实现在客户放弃连接时出现的错误)和 EINTR(如果信号被捕获).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值