UNIX网络编程卷一:第十六章 非阻塞I/O

套接字的默认状态是阻塞的。当发出一个套接字调用,但是不能立即完成时,该进程被投入睡眠。

可阻塞的套接字调用有4类:

1)输入操作

     read  readv recv recvfrom  recvmsg

     某进程对一个阻塞的TCP套接字调用这些输入函数,并且该套接字的接受缓冲区种没有数据可读,该进程被投入睡眠,直到有一些数据到达。

     因为TCP是字节流协议,该进程的唤醒就是只有一些数据到达(可能不是所有数据)。

     如果想等到某个固定数目的数据,那么可以使用unpvol1提供的readn函数,或者使用MSG_WAITALL标志。

     UDP是数据报协议,如果一个阻塞的UDP套接字的接收缓冲区为空,那对它调用的进程被投入睡眠,直到有UDP数据报到达。

     对于非阻塞套接字,如果输入操作不能被满足(对于TCP套接字即至少有一个字节数据可读,对于UDP即有一个完整的数据报可读),

     相应调用立即返回一个 EWOULDBLOCK错误。


readn函数:

ssize_t						/* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
	size_t	nleft;
	ssize_t	nread;
	char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;		/* and call read() again */
			else
				return(-1);
		} else if (nread == 0)
			break;				/* EOF */

		nleft -= nread;
		ptr   += nread;
	}
	return(n - nleft);		/* return >= 0 */
}
/* end readn */

read被信号处理中断了, errno为EINTR。中断返回后继续调用read即可。

2)输出操作

     write  writev  send  sendto  sendmsg

     对于TCP套接字,内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据。

     对于阻塞的套接字,如果发送缓冲区中没有空间,进程被投入睡眠,直到有空间为止。

     对于非阻塞的TCP套接字,发送缓冲区没有任何空间,函数立即返回一个EWOULDBLOCK错误。

     如果发送缓冲区有一些空间,返回值时内核能复制到该缓冲区的字节数(不足计数,short count)。

     UDP不存在真正的发送缓冲区。内核只是复制应用进程数据并把它沿协议栈向下传送,以此加上udp首部和IP首部。

     所以UDP阻塞的原因与上面TCP阻塞的原因不同。

3) 接受外来连接

     accept

      对一个阻塞的套接字调用accept,并尚无新的连接到达,调用进程被投入睡眠。

       对于非阻塞的,无新连接到达时,accept立即返回一个EWOULDBLOCK错误。

4)发起连接

     connect

     TCP连接的建立涉及三次握手,connect一直等到客户收到对于自己的SYN的ACK为止才返回。

     这意味着TCP的每个connect总会阻塞其调用进程至少一个到服务器的RTT时间。

     对于非阻塞的TCP调用connect,并且连接不能立即建立,那么连接的建立能照样发起(譬如发出TCP三次握手的第一个分组),

     不过会返回一个EINPROGRESS错误。


非阻塞+select

/* 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;

	// 设置描述符为非阻塞模式
	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;

	// 使用select监控这些描述符
	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) {
#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 */
				}
			}
		}
	}
}
/

维护了两个缓冲区,to和fr。结合select。实现比较复杂。但是效率高。


客户端通过fork,处理不同的功能。

#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);

		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(sockfd, SHUT_WR);	/* EOF on stdin, send FIN */
	pause();
	return;
}

/*
	使用shutdown的原因:
	fork之后,父进程和子进程共享sockfd,sockfd的引用计数为2.
	如果此处使用close,仅仅是将sockfd的引用计数减1,那么sockfd的引用计数变为1,仍然不为0。
	不会发送FIN分节。
	而shutdown一定会发送FIN分节。

        使用kill的原因:
        有可能服务器提前终止了,子进程将在套接字上读到EOF,这样子进程必须告诉父进程:不要再往套接字写入数据了。
        所以通过kill,杀死父进程。
 *



UNIX网络编程卷一:第十六章 非阻塞I/O_第1张图片

你可能感兴趣的:(UNIX网络编程卷一:第十六章 非阻塞I/O)