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