套接字的默认状态是阻塞的。当发出一个套接字调用,但是不能立即完成时,该进程被投入睡眠。
可阻塞的套接字调用有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 */ } } } } } /
客户端通过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,杀死父进程。 *