处理多个输入的时候,需要使得内核一旦发现进程指定的一个或多个I/O条件就绪(即输入准备好被读取,或描述符已能承接更多输出),它就通知进程
此即为 I/O 复用
应用场合:
1、客户处理多个描述符时(交互式输入+网络套接字)
2、客户同时处理多个套接字
3、一个TCP服务器既要处理监听套接字,又要处理已连接套接字
4、服务器既要处理TCP,又要UDP
5、服务器处理多个服务或多个协议
因此我们将客户端读取函数改成这样:
批量输入:
采用一问一答的方式虽然很符合回射客户端/服务器的要求,但是既然套接字是全双工通信,我们可以将标准输入和标准输出替换为文件,充分利用。
但是,最后发现输出文件总是小于输入文件(对于回射服务器,他们理应相等)
问题在于我们对标准输入中的EOF的处理
str_cli函数就此返回到main函数,而main函数随后终止。然而在这种批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入:可能仍有请求在去往服务器的路上,或仍有请求在返回客户端的路上
我们需要的是一种关闭TCP连接其中一半的方法。也就是说,我们想给服务器TCP发送一个FIN,告诉服务器我们已经完成了发送数据,但是仍然保持该套接字描述符打开以便读取
P135 讲述了select 与缓冲区之间的微妙处理关系,但我没怎么看懂
终止网络连接常用close,不过close有两个限制,却可以使用shutdown函数来避免。
1、close把描述符的引用计数减1,仅在该计数为0时关闭描述符。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列(由FIN开始的4个字节)
2、close终止读和写两个方向的数据传输。既然TCP是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。即上面批量输入遇到的问题。
修改后,如下:
既然客户端用select函数进行了更改,服务器自然也可以:
关于 POLL
使用poll改写服务器:
此即为 I/O 复用
应用场合:
1、客户处理多个描述符时(交互式输入+网络套接字)
2、客户同时处理多个套接字
3、一个TCP服务器既要处理监听套接字,又要处理已连接套接字
4、服务器既要处理TCP,又要UDP
5、服务器处理多个服务或多个协议
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
(一)
struct timeval
{
long tv_sec ; //sec
long tv_usec; //micro-sec
};
1、为空: 永远等下去
2、等待一段固定时间
3、根本不等待 定时器值必须为 0
(二)
支持的异常条件:
1、某个套接字的带外数据到达
2、某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息
(三)
要用到的宏函数:
1、void FD_ZERO(fd_set *fdset) //clear all
2、void FD_SET(int fd,fd_set *fdset) //turn on
3、void FD_CLR(int fd,fd_set *fdset) //turn off
4、int FD_ISSET(int fd,fd_set *fdset) //test
(四)
该函数返回后,用FD_ISSET来测试描述符,描述符集内任何与未就绪描述符对应的位返回时均清成0.
为此,每次重新调用select函数,都得【再次】把所有所关心的描述符位置一
(五)
返回已就绪的总位数
因此我们将客户端读取函数改成这样:
void str_cli(FILE *fp, int sockfd)
{
int maxfdpl;
fd_set reset;
char sendling[MAX],receive[MAX];
FD_ZERO(reset);
for(;;)
{
FD_SET(fileno(fp),&reset);
FD_SET(sockfd, &reset);
maxfdlp = max(fileno(fp),sockfd)+1;
Select(maxfdlp, &reset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rerset))
{
if(Readline(sockfd,receive,MAX) == 0)
err_quit();
Fputs(buf,stdout);
}
if(FD_ISSET(fileno(fp),&reset))
{
if(Fgets() == NULL)
return;
Writen(sockfd,sending,strlen(sending));
}
}
}
批量输入:
采用一问一答的方式虽然很符合回射客户端/服务器的要求,但是既然套接字是全双工通信,我们可以将标准输入和标准输出替换为文件,充分利用。
但是,最后发现输出文件总是小于输入文件(对于回射服务器,他们理应相等)
问题在于我们对标准输入中的EOF的处理
str_cli函数就此返回到main函数,而main函数随后终止。然而在这种批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入:可能仍有请求在去往服务器的路上,或仍有请求在返回客户端的路上
我们需要的是一种关闭TCP连接其中一半的方法。也就是说,我们想给服务器TCP发送一个FIN,告诉服务器我们已经完成了发送数据,但是仍然保持该套接字描述符打开以便读取
P135 讲述了select 与缓冲区之间的微妙处理关系,但我没怎么看懂
终止网络连接常用close,不过close有两个限制,却可以使用shutdown函数来避免。
1、close把描述符的引用计数减1,仅在该计数为0时关闭描述符。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列(由FIN开始的4个字节)
2、close终止读和写两个方向的数据传输。既然TCP是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。即上面批量输入遇到的问题。
int shutdown(int sockfd, int how)
参数howto:
A、SHUT_RD
关闭连接的读这一半 ---- 套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数,对一个TCP套接字调用该操作后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。。。
B、SHUT_WR
关闭写这一半 ----- 对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区的数据将被发送掉,后跟TCP正常终止序列。不管套接字描述符的引用计数是否为0,写半部分关闭照样执行,不能再对这个套接字调用任何写函数
C、SHUT_RDWR
都关闭
修改后,如下:
void str_cli(FILE *fp, int sockfd)
{
int maxfdlp, stdineof;
fd_set rset;
int nbyte;
FD_ZERO(&rset);
stdineof = 0;
for(;;)
{
maxfdlp = max(fileno(fp),sockfd)+1;
if(stdinof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
if(select(maxfdlp,&rset,NULL,NULL,NULL) < 0)
{
perror("select");
exit(1);
}
if(FD_ISSET(sockfd, &rset))
{
if((nbyte=read(sockfd,buf,MAXLEN)) == 0)
{
if(stdineof == 1)
return; //normal end
else
{ //service send FIN to this client unexpectedly
fprintf(stderr,"Service Terminated Unexpected\n");
exit(1);
}
}
else if(nbyte < 0)
{
perror("Read Error");
exit(1);
}
if(write(1,buf,nbyte) != nbyte)
{
perror("Write Error");
exit(1);
}
}
if(FD_SET(fileno(fp),&rset))
{
if((nbyte=read(fileno(fp),buf,MAXLEN)) == 0)
{
stdineof = 1;
shutdown(sockfd, SHUT_WR); //send FIN to service to close half-TCP about write
//which means we would not send data anymore.
FD_CLR(fileno(fp),&rset);
continue;
}
else if(nbyte < 0)
{
perror("Read Error");
exit(1);
}
if(written(sockfd,buf,nbyte) != nbyte)
{
perror("Write Error");
exit(1);
}
}
}
}
既然客户端用select函数进行了更改,服务器自然也可以:
#include "unp.c"
int main(int ac, char *av[])
{
struct sockaddr_in serv_addr,clin_addr;
int serv_fd, clin_fd;
fd_set rset, allset;
char table[FD_SETSIZE]; //用一个队列将所有客户的套接字存储下来
int maxi = -1; //对应于拥有最大描述符的数组的索引
int maxfd = -1; //最大描述符(select第一个参数要用)
int nready; //select将返回的准备好的描述符个数
int i_loop;
socklen_t clin_len;
char comm_buf[MAXLEN];
int nread, nwrite;
if((serv_fd=socket(AF_INET,SOCK_STREAM,0)) < 0)
oops("socket error");
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(13000);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
oops("bind error");
if(listen(serv_fd, 10) < 0)
oops("listen error");
maxfd = serv_fd; //当前最大描述符当然是用于监听的套接字
for(i_loop=0; i_loop<FD_SETSIZE; i_loop++)
table[i_loop] = -1;
FD_ZERO(&allset);
FD_SET(serv_fd,&allset); //设置一个allset,且在循环中令一个变量等于它,就不需要每次都重置了
for(;;)
{
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1)
oops("select error");
if(FD_ISSET(serv_fd,&rset))
{
clin_len = sizeof(clin_addr);
if((clin_fd=accept(serv_fd, (struct sockaddr *)&clin_addr, &clin_len)) < 0)
oops("accept error");
for(i_loop=0; i_loop<FD_SETSIZE; i_loop++) //挑一个空位给这个新套接字
if(table[i_loop] < 0)
{
table[i_loop] = clin_fd;
break;
}
FD_SET(clin_fd,&allset); //并将这个新套接字设置进这个组中
if(maxfd < clin_fd)
maxfd = clin_fd;
if(maxi < i_loop) //取到最大的下标,用于接下来的循环
maxi = i_loop;
if(--nready == 0) //若等于0了,就无需再经历下面的循环
continue;
}
for(i_loop=0; i_loop<=maxi; i_loop++)
{
if(table[i_loop] < 0)
continue;
if(FD_ISSET(table[i_loop], &rset))
{
if((nread=read(table[i_loop], comm_buf, MAXLEN)) > 0)
if(written(table[i_loop], comm_buf, nread) != nread)
oops("write error");
if(nread < 0)
oops("read error")
else if(nread == 0) //connection closed by client
{
close(table[i_loop]);
FD_CLR(table[i_loop],&allset);
table[i_loop] = -1;
}
if(maxi == i_loop)
{
maxfd = -1;
for(i_loop=0; i_loop<=maxi; i_loop++)
if(maxfd < table[i_loop])
maxfd = table[i_loop];
}
if(--nready == 0) //a better solution
break;
}
}
}
}
// here I try pselect instead of select
// Given that one global variation "intr_flag" will be changed if signal "SIGINT" occures
// and the global variation will be used
/* sigset_t set,nset,oset;
sigemptyset(&nset);
sigemptyset(&set);
sigaddset(&nset,SIGINT);
sigprocmask(SIG_BLOCK,&nset,&oset);
if(int_flag)
handle_intr(); //handle the signal
if((nready = pslect(.....,&set)) == 0)
{
if(errno == EINTR)
if(intr_flag)
handle_intr();
}
*/
/*关于信号处理的部分,之前在APUE中已经有了一定的概念。
在测试intr_flag变量之前,我们阻塞SIGINT,当pselect被调用时,它先以空集代替进程的信号掩码,再检查描述符,并可能进入睡眠;当pselect返回时,进程的信号掩码又被重置为调用pselect之前的值*/
关于 POLL
int poll(struct pollfd *fdarray,long nfds,int timeout);
struct pollfd{
int fd;
short events;
short revents;
};
就TCP和UDP套接字而言,以下条件引起poll返回特定的revent:
1、所有正规TCP数据和所有UDP数据都被认为是普通数据
2、TCP带外数据被认为是优先级数据
3、当TCP连接的读半部关闭时,也被认为是普通数据,随后读操作返回0
4、TCP连接存在错误既可认为是普通数据,也可以是错误。无论如何,读操作返回-1,并设置errno。可用于处理RST或超时等条件
5、在监听套接字上有新的连接可用既可以是普通数据(大多数),也可以是优先数据
6、非阻塞式connect的完成被认为是使相应套接字可写
timeout:
1、INFTIM 永远等待
2、 0 立即返回
3、 >0 等待指定数目的毫秒
使用poll改写服务器:
#include "unp.h"
#define OPEN_MAX 100
#define INFTIM -1
int main(int ac, char *av[])
{
struct sockaddr_in serv_addr,clin_addr;
struct pollfd client[OPEN_MAX]; //poll要使用的参数
int serv_fd, clin_fd;
int nready; //select将返回的准备好的描述符个数
int maxi;
int i_loop;
socklen_t clin_len;
char comm_buf[MAXLEN];
int nread, nwrite;
if((serv_fd=socket(AF_INET,SOCK_STREAM,0)) < 0)
oops("socket error");
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(13000);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
oops("bind error");
if(listen(serv_fd, 10) < 0)
oops("listen error");
client[0].fd = serv_fd;
client[0].events = POLLRDNORM;
for(i_loop=0; i_loop<OPEN_MAX; i_loop++)
client[i_loop].fd = -1; //poll会自动忽略fd为-1的参数
maxi = 0;
for(;;)
{
if((nready=poll(client, maxi+1, INFTIM)) == -1)
oops("poll error");
if(client[0].revents & POLLRDNORM) //检测是否有普通数据
{
clin_len = sizeof(clin_addr);
if((clin_fd=accept(serv_fd, (struct sockaddr *)&clin_addr, &clin_len)) == -1)
oops("accept error");
for(i_loop=0;i_loop<OPEN_MAX;i_loop)
if(client[i_loop].fd < 0)
{
client[i_loop].fd = clin_fd;
client[i_loop].events = POLLRDNORM;
}
if(i_loop == OPEN_MAX)
{
fprintf(stderr,"too many descriptors\n");
exit(1);
}
if(i_loop > maxi)
maxi = i_loop;
if(--nready <= 0)
continue;
}
for(i_loop=1; i_loop<=maxi; i_loop++) //0固定为监听用的,之前select从0开始是因为数组元素全是存客户套接字的
{
if(client[i_loop].fd < 0)
continue;
if(client[i_loop].revents & (POLLRDNORM | POLLERR) )
{
if((nread=read(client[i_loop].fd, comm_buf, MAXLEN)) > 0)
if(written(client[i_loop].fd, comm_buf, nread) != nread)
oops("write error");
if(nread < 0)
{
if(errno == ECONNRESET)
{
close(client[i_loop].fd);
client[i_loop].fd = -1;
}
else
{
perror("read error");
exit(1);
}
}
else if(nread == 0) //connection closed by client
{
close(client[i_loop].fd);
client[i_loop].fd = -1;
}
}
if(--nready <= 0)
break;
}
}
return 0;
}
//这样看下来,其实差别不大,但之前select使用的数组不再需要了,简化了些