概述
套接字默认是阻塞的,可能阻塞的套接字调用可以分为以下四类
-
读取操作。包括read,reav,recv,recvfrom和recvmsg五个函数。如果进程对阻塞的TCP套接字调用这些函数,而接收缓冲区没有数据可读,那么进程就会沉睡直到有数据可读(无论是一个字节还是说一个完整的TCP分节的数据)。如果需要等到足够数目的数据可读,那么可以用MSG_WAITALL状态或者调用我们自己的readn函数
对于UDP套接字而言,苏醒的条件是有数据报的到来。
对于非阻塞的套接字而言,调用这些函数如果不符合条件(TCP至少有一个字节数据可读,UDP至少有一个完整的数据报可读),那么会立刻返回EWOULDBLOCK
-
写入操作。包括write,writev,send,sendto和sendmsg。如果进程对阻塞的TCP套接字调用这些函数,而缓冲区没有空间,则投入睡眠直到有空间位置。而如果是非阻塞的TCP套接字,如果发送缓冲区没有任何空间,则立刻返回EWOULDBLOCK,如果有一些空间(但不足),函数返回值则是内核能够复制到该缓冲区中的字节数,称为不足计数
UDP套接字不存在真正的发送缓冲区,因此阻塞的UDP套接字不会因为和TCP套接字相同的原因而阻塞,但也可能因为其他原因而阻塞
-
接受外来连接,即accept函数。对于阻塞的套接字调用accept函数,如果没有新的连接到达,进程将会投入睡眠。如果是非阻塞的套接字,尚无新的连接到达,则立即返回EWOULDBLOCK错误
-
发起外出连接。即用于TCP的connect函数(应用于UDP的话不会建立连接,只会让内核保存对端的IP地址和端口号)。connect总会阻塞进程直到收到自己的SYN的ACK才返回(所以至少一个RTT时间)
对于非阻塞的TCP套接字调用connect,而且连接不能立刻建立,那么连接的建立依然发起(发出第一个分组),但是会返回一个EINPROGRESS错误。注意连接如果发生在同一个主机之间,是有可能立刻建立的(所以也要准备connect成功返回的情况)
非阻塞读和写:str_cli函数
#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;
friptr = froptr = fr;
stdineof = 0;
maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
for (;;)
{
FD_ZERO(&rset);
FD_ZERO(&wset);
//如果to缓冲区未满,则打开读描述集中标准输入的位置
if (stdineof == 0 && toiptr < &to[MAXLINE])
FD_SET(STDIN_FILENO, &rset);
//如果fr缓冲区未满,则打开读描述集中socket套接字的位
if (friptr < &fr[MAXLINE])
FD_SET(sockfd, &rset);
//如果to缓冲区有要写到socket的内容,则打开写描述集中socket套接字的位
if (tooptr != toiptr)
FD_SET(sockfd, &wset);
//如果fr缓冲区中有要写到标准输出的数据,那么就打开写描述符集中标准输出的位
if (froptr != friptr)
FD_SET(STDOUT_FILENO, &wset);
Select(maxfdp1, &rset, &wset, NULL, NULL);
if (FD_ISSET(STDIN_FILENO, &rset))
{
//试图读取标准输入,toiptr就是输入缓冲区
if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0)
{
if (errno != EWOULDBLOCK)
err_sys("read error on stdin.");
}
//收到eof,此时检查tooptr是否等于toiptr(是否还有未发出数据),如果没有数据
//则关闭
else if (n == 0)
{
fprintf(stderr, "%s:EOF on stdin \n", gf_time());
stdineof = 1;
if (tooptr == toiptr)
Shutdown(sockfd, SHUT_WR);
}
//正常收到数据,移动toiptr(注意tooptr没有移动)
else
{
fprintf(stderr, "%s:read %d bytes from stdin\n", gf_time());
toiptr += n;
FD_SET(sockfd, &wset);
}
}
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)
{
fprintf(stderr, "%s:EOF on socket\n", gf_time());
if (stdineof)
return;
else
err_quit("str_cli: server terminated prematurely");
}
else
{
fprintf(stderr, "%s :read %d bytesfrom socket\n", gf_time(), n);
friptr += n;
//这里打开了标准输出,导致下面的ISSET为真
FD_SET(STDOUT_FILENO, &wset);
}
}
if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0))
{
//注意由于上面会设置为真,所以此时调用write不一定不会阻塞,所以可能返回
//EWOULDBLOCK
if ((nwritten = write(STDOUT_FILENO, froptr, n)) < 0)
{
if (errno != EWOULDBLOCK)
err_sys("write error to stdout");
}
else
{
fprintf(stderr, "%s:wrote %d bytes to stdout\n", gf_time(), nwritten);
froptr += nwritten;
//如果fri==fro,那么意思是写出标准输出后,缓冲区已经没有数据要处理
//就设置回起点
if (froptr == friptr)
froptr = friptr = fr;
}
}
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
{
fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten);
tooptr += nwritten;
if (tooptr == toiptr)
{
toiptr = tooptr = to;
if (stdineof)
Shutdown(sockfd, SHUT_WR);
}
}
}
}
}
总结:为什么非阻塞会比select版本的更快一点?因为select版本中,我们在监听标准输入和socket,如果标准输入有信息,读取之后(这里因为select,是不会阻塞的)写入到socket(注意这里可能就出现阻塞等待)。而非阻塞的socket不会有这样的需要等待的问题。但是非阻塞的套接字不能保证自己已经能够写入,因此需要更加繁杂的检查。
fork版本的str_cli函数
非阻塞的速度更快毫无疑问,但是长度却大大增加,因此我们不妨引用第二种思路:我们用多个进程来处理多个输入输出流
#include "../unp.h"
void str_cli(FILE *fp,int sockfd)
{
pid_t pid;
char sendline[MAXLINE],recvline[MAXLINE];
if ( (pid=Fork())==0){
while (Readline(sockfd,recvline,MAXLINE)>0)
Fputs(recvline,stdout);
kill(getppid(),SIGTERM);//过早地终止,向父进程发送SIGTERM信号
exit(0);
}
while (Fgets(sendline,MAXLINE,fp)!=NULL)
Writen(sockfd,sendline,strlen(sendline));
Shutdown(sockfd,SHUT_WR);
pause();//让自己睡眠
return;
}
非阻塞connect
非阻塞的TCP套接字调用connect之后会立刻返回EINPROGRESS错误,然后我们可以用select检测连接成功或者失败。非阻塞的connect的作用有以下几点
- 三次握手需要花费一个RTT时间,RTT可能从几毫秒到几秒,我们也许希望能处理其他事务
- 我们可以用这个技术同时建立多个连接
- 我们可以select指定一个时间限制,使得我们能够缩短connect的超时(75秒钟)
但是我们也需要处理一些细节:
- 在特定情况下(如同一个主机),connect可能会立刻建立,我们需要处理这种情形
- 当连接成功时候,描述符变为可写。当连接遇到错误的时候,描述符变为即可读又可写
例子
#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)
if (errno != EINPROGRESS)
return (-1);
//我们可以在这个过程中做我们需要完成的其他事情
if (n == 0) //立刻完成
goto done;
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); //避免已经启动的三次握手继续
errno = ETIMEDOUT;
return (-1);
}
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
{
len = sizeof(error);
//下面的处理主要是移植性问题,berkeley的实现会在error中是设置,
//Solaris会返回-1然后设置了errno
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
return (-1);
}
else
{
err_quit("select error:sockfd not set");
}
done:
Fcntl(sockfd,F_SETFL,flags);
if (error){
close(sockfd);
errno=error;
return(-1);
}
return 0;
}
值得注意的是,非阻塞conenct面临很多移植性问题。其一,由于在我们调用connect之前可能连接已经完成并且对端发送了数据。所以”可读且可写“不足以判断连接失败,另一个是”可写而不可读“也同样不能充当连接成功的标志,这里我们统一使用了getsockopt
被中断的connect
假设一个阻塞套接字的connect调用在完成之前被打断(如信号)且没有自动重启,此时我们是不能重新调用connect的,会触发EADDRINUSE错误。此时我们能做的,使用select监听等待连接完成,然后用”可读且可写“判断失败,”可写而不可读“判断成功
非阻塞connect:web客户端程序
#include "web.h"
int main(int argc, char **argv)
{
int i, fd, n, maxnconn, flags, error;
char buf[MAXLINE];
fd_set rs, ws;
if (argc < 5)
err_quit("usage: web <#conns> <hostname> <homepage> <file1> ...");
maxnconn = atoi(argv[1]);
nfiles = min(argc - 4, MAXFILES);
for (i = 0; i < nfiles; i++)
{
file[i].f_name = argv[i + 4];
file[i].f_host = argv[2];
file[i].f_flags = 0;
}
printf("nfiles = %d\n", nfiles);
//homepage会读取主页
home_page(argv[2], argv[3]);
FD_ZERO(&rset);
FD_ZERO(&wset);
maxfd = -1;
nlefttoread = nlefttoconn = nfiles;
nconn = 0;
while (nlefttoread > 0)
{
while (nconn < maxnconn && nlefttoconn > 0)
{
for (i = 0; i < nfiles; i++)
if (file[i].f_flags == 0)
break;
if (i == nfiles)
err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
start_connect(&file[i]);
nconn++;
nlefttoconn--;
}
rs = rset;
ws = wset;
n = Select(maxfd + 1, &rs, &ws, NULL, NULL);
for (i = 0; i < nfiles; i++)
{
flags = file[i].f_flags;
if (flags == 0 || flags & F_DONE)
continue;
fd = file[i].f_fd;
if (flags & F_CONNECTING && (FD_ISSET(fd, &rs) || FD_ISSET(fd, &ws)))
{
n = sizeof(error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 ||
error != 0)
{
err_ret("nonblocking connect failed for %s", file[i].f_name);
}
printf("connection established for %s\n", file[i].f_name);
FD_CLR(fd, &wset);
write_get_cmd(&file[i]);
}
else if (flags & F_READING && FD_ISSET(fd, &rs))
{
if ((n = Read(fd, buf, sizeof(buf))) == 0)
{
printf("end-of-file on %s\n", file[i].f_name);
Close(fd);
file[i].f_flags = F_DONE;
FD_CLR(fd, &rset);
nconn--;
nlefttoread--;
}
else
{
printf("read %d bytes from %s\n",n,file[i].f_name);
}
}
}
}
exit(0);
}
void home_page(const char *host, const char *fname)
{
int fd, n;
char line[MAXLINE];
fd = Tcp_connect(host, SERV);//建立连接
n = snprintf(line, MAXLINE, GET_CMD, fname);//发送GET请求
Writen(fd, line, n);
for (;;)
{
//读取应答
if ((n = Read(fd, line, MAXLINE)) == 0)
break;
printf("read %d bytes of home page\n", n);
//这里可以用主页获取一些信息
}
printf("end-of-file on home page\n");
Close(fd);
}
void start_connect(struct file *fptr)
{
int fd, flags, n;
struct addrinfo *ai;
ai = Host_serv(fptr->f_host, SERV, 0, SOCK_STREAM);
fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
fptr->f_fd = fd;
printf("start_connect for %s,fd %d\n", fptr->f_name, fd);
flags = Fcntl(fd, F_GETFL, 0);
Fcntl(fd, F_SETFL, flags | O_NONBLOCK);
if ((n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0)
{
if (errno != EINPROGRESS)
err_sys("nonblocking connect error");
fptr->f_flags = F_CONNECTING;
FD_SET(fd, &rset);
FD_SET(fd, &wset);
if (fd > maxfd)
maxfd = fd;
}
else if (n >= 0)
write_get_cmd(fptr);
}
void write_get_cmd(struct file *fptr)
{
int n;
char line[MAXLINE];
n = snprintf(line, sizeof(line), GET_CMD, fptr->f_name);
Writen(fptr->f_fd, line, n);
printf("wrote %d bytes for %s\n", n, fptr->f_name);
fptr->f_flags = F_READING;
FD_SET(fptr->f_fd, &rset);
if (fptr->f_fd > maxfd)
maxfd = fptr->f_fd;
}
非阻塞accept
当一个套接字已经准备好被accept的时候,select将会作为可读描述符返回该连接的监听套接字。这听起来似乎是说我们没有必要为了accept设置套接字为非阻塞,然而这里有一个陷阱:如果连接成功,select返回,然而在调用accept之前(一个很小的时间差),对端发送了RST报文,来自Berkeley的实现会选择丢弃该连接,然后我们调用accept,此时进程就(意外的)阻塞了。
解决办法是:调用select来获悉哪个连接准备好被accpet时候,把这个监听套接字设置为非阻塞。然后在accept调用时候忽略以下错误:EWOULDBLOCK(源自berkeley实现,客户中止连接时),ECONNABORTED(POSIX实现,客户中止连接时),EPTOTO(SVR4实现,客户中止连接时)和EINTR