概述
套接字的默认状态是阻塞的。这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待响应操作完成。可能阻塞的套接字调用可分为以下四类:
- 输入操作,包括read、readv、recv、recvfrom和recvmsg共5个函数。如果某个进程对一个阻塞的TCP套接字(默认设置)调用这些函数之一,而且该套接字的接收缓冲区中没有数据可读,该进程将被投入睡眠,直到有一些数据到达。如果想等到有固定数目的数据可读位置,那么可以调用readn函数或者指定MSG_WAITALL标志;而对于非阻塞的套接字,如果输入操作不能被满足,相应调用将立即返回一个EWOULDBLOCK错误;
- 输出操作,包括write、wrtiev、send、sendto和sendmsg共5个函数。内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。对于一个非阻塞的套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区有一些空间,返回值将是内核能够复制到该缓冲区的字节数;
- 接受外来连接,即accept函数。如果对一个阻塞的套接字调用该函数,并且无新的连接到达,调用进程将被投入睡眠。如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept调用将立即返回一个EWOULDBLOCK错误;
- 发起外出连接,即调用TCP的connect函数(UDP使用connect,不能使一个“真正”的连接建立起来,它只是使内核保存对端的IP地址和端口号)。TCP连接经历“三次握手”过程,并且connect函数一直要等到客户收到对于自己的SYN的ACK为止才返回。若对一个非阻塞的TCP套接字调用connect,并且连接不能建立,那么连接的建立照样发起,不过会返回一个EINPROGRESS错误;
非阻塞读和写:str_cli函数
我们维护这两个缓冲区:
- to:容纳从标准输入到服务器去的书;
- fr:容纳来自服务器到标准输出来的数据;
下图展示了to缓冲区的组织和指向该缓冲区中的指针:
下图展示了fr缓冲区相应的组织:
str_cli的第一部分:
#include "unp.h"
void str_cli(FILE *fp, int sockfd){
int macfdpl, val, stdineof;
ssize_t n, nwritten;
fd_set rset, wset;
char to[MAXLINE], fr[MAXLINE];
char *toiptr, *tooptr, *friptr, *froptr;
//fcntl把所用3个描述符都设置为非阻塞,包括林艾尔到服务器的套接字、标准输入和标准输出
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);
//初始化两个缓冲区的指针,并把最大描述符号+1,用作select的第一个参数
toiptr = tooptr = to;
friptr = froptr = fr;
stdineof = 0;
maxfdpl = 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);
if(friptr < &fr[MAXLINE])
FD_SET(sockfd, &rset);
if(tooptr != toiptr)
FD_SET(sockfd, &wset);
if(froptr != friptr)
FD_SET(STDOUT_FILENO, &wset);
select(maxfdpl, &rset, &wset, NULL, NULL);
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){
fprintf(stderr, "%s: EOF on stdin\n",gf_time());
stdineof = 1;
if(tooptr == toiptr)
shutdown(sockfd,SHUT_WR);
}else{
fprintf(stderr, "%s: read %d bytes from stdin\n",gf_time(), n);
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 bytes from socket\n",gf_time(), n);
friptr += n;
FD_SET(sockfd, &wset);
}
}
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 on stdin");
}else{
fprintf(stderr, "%s: wrote %d bytes from stdin\n",gf_time(), nwritten);
froptr += nwritten;
if(froptr == friptr)
froptr = friptr = fr;
}
}
if(FD_ISSET(scokfd, &wset) && ((n = toiptr - tooptr) > 0)){
if((nwritten = write(sockfd, tooptr, n)) < 0){
if(errno != EWOULDBLOCK)
err_sys("write error on socket");
}else{
fprintf(stderr, "%s: wrote %d bytes from stdin\n",gf_time(), nwritten);
tooptr += nwritten;
if(tooptr == toiptr){
tooptr = toiptr = to;
if(stdineof)
shutdown(sockfd, SHUT_WR);
}
}
}
}
}
-
非阻塞connect
当在一个非阻塞TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三路握手继续进行。我们接着使用select检测这个连接或成功或失败的已建立条件。非阻塞的connect的三个用途是:
- 可以把三路握手叠加在其他处理上。完成一个connect需要一个RTT,而RTT根据环境的不同波动比较大,所以波动的这段时间内我们可以执行我们想要处理的工作;
- 可以使用这个技术同时建立多个连接;
- 既然使用select等待连接的建立,我们可以给select指定一个时间限制,使得我们能够缩短connect的超时;
非阻塞connect需要我们处理的细节:
- 尽管套接字是非阻塞的,如果连接到的服务器在同一主机上,那么当我们调用connect时,连接通常立刻建立;
- 源自Berkeley的实现(和POSIX)有关于select和非阻塞connect的以下两个规则:
- 当连接成功建立时,描述符变为可写;
- 当连接建立遇到错误时,描述符变为既可读又可写;
-
非阻塞accept
当有一个已完成的连接准备好被accept时,select将作为可读描述符返回该连接的监听套接字。因此,如果我们使用select在某个监听套接字上等待一个外来连接,那就没有必要把该监听套接字设置为非阻塞,这是因为如果select告诉我们该套接字上已有连接就绪,那么随后的accept调用不应该阻塞。
不幸的是,这里存在一个可能让我们掉入陷阱的定时问题。问题例子如下:
#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));
//这样设置导致连接被关闭时在TCP套接字上发送一个RST
ling.l_onoff = 1;
ling.l_linger = 0;
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
close(sockfd);
exit(0);
}
我们在TCP回射程序中添加如下:
if(FD_ISSET(listenfd, &rset)){
printf("listening socket readable"); //+
sleep(5): //+
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *) &cliaddr, &cliaddr);
}
上述代码模拟了一个繁忙的服务器,它无法在select返回监听套接字的可读条件后就马上调用accept。通常情况下服务器的这种迟钝不成问题(实际上这就是要维护一个已完成连接队列的原因),但是结合上连接建立之后到达的来自客户的RST,问题就出现了。
我们知道当客户在服务器调用accept之前中止某个连接时,源自Berkeley的实现不把这个中止的连接返回给服务器,而其他实现应该返回ECONNABORTED错误,却往往代之以返回EPROTO错误。考虑一个源自Berkeley的实现上的如下例子:
- 客户建立一个连接并随后中止它;
- select向服务器进程返回可读条件,不过服务器要过一小段时间才调用accept;
- 在服务器从select返回到调用accept期间,服务器TCP接收到来自客户的RST;
- 这个已完成的连接被服务器TCP驱除出队列,我们假设队列中没有其他已完成的连接;
- 服务器调用accept,但是由于没有任何已完成的连接,服务器于是阻塞;
【解决办法】
- 当使用select获悉某个监听套接字上何时有已完成连接准备好被accept时,总是把这个监听套接字设置为非阻塞;
- 在后续的accept调用中忽略一下错误:
- EWOULDBLOCK(Berkeley实现,客户中止连接时);
- ECONNABORTED(POSIX实现,客户中止连接时);
- EPROTO(SVR4实现,客户中止连接时);
- EINTR(如果有信号被捕捉);