第六章笔记
(1)需要IO复用的原因:
echo客户端同时需要读终端和套接字会遇到这样的问题:如果客户端面向两个文件描述符,控制和连接套接字,那么就不能同时收到终端和套接字的数据,如果客户端阻塞在终端,那即使服务器发来了close也不能立即处理。
这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个IO条件就绪(也就是说输入已准备好被读取,或者描述符已能承担更多的输入),它就通知进程。这个能力成为IO复用。
(2)IO复用的特点:
使用IO复用,使进程不再阻塞在真正的IO系统调用上,而是阻塞在select poll epoll这其中某一个系统调用之上,而它们的优势在于它们可以帮助我们同时等待多个描述符就绪。
(3)IO复用适用的场景:
IO复用是IO模型的一种,共有五种IO模型:阻塞IO、非阻塞IO、IO复用、信号驱动式IO、异步IO
以套接字recvfrom为例:recvfrom操作包括两个阶段(1)等待内核缓冲区的数据是否准备好。(2)从内核向进程复制数据。而这几种IO模型的主要区别就在于当内核缓冲区的数据已经准备好之后以何方方式通知用户进程
默认情况下,所有套接字都是阻塞的。这种模型表现为:如果数据没有准备好,调用recvfrom的当前线程进入阻塞状态,直到数据准备好并复制到应用程序缓冲区才返回。它的缺点是:当应用程序遇到多个文件描述符时,前面的文件描述符会阻塞后面的文件描述符,即:即使有某个文件描述符可读,也要等到前面的文件描述符变成可读才能处理。
每个线程处理一个IO,这样即使有多个文件描述符也可同时处理。这种方式可以处理上面那种非阻塞IO不能有效处理多个文件描述的情况。《unix网络编程笔记(三)》服务器代码就是这种模型。但缺点是:当需要成千上万设置更多文件描述符,多进程非常耗费资源。多线程的切换会影响性能。
如果数据没有准备好,调用recvfrom会返回错误,使用返回值告诉应用程序数据是否可读且已经复制到应用程序缓冲区中,因此应用程序需要不断轮询查看数据是否准备好。缺点为:轮询往往耗费大量CPU时间
使进程不再阻塞在真正的IO系统调用上,而是阻塞在select poll epoll这其中某一个系统调用之上,虽然与阻塞IO一样如果数据没有准备好还是阻塞,但是其优势为可以同时等待多个文件描述符,只要有任何一个准备好就返回。之后应用进程调用read读取数据。特点:当同时处理大量描述符时性能由于多线程+阻塞IO。
使用信号SIGIO来通知应用程序数据已准备好,用户需要捕获信号并在信号处理程序中读取数据
支持POSIX异步IO模型的系统较罕见。工作机制为:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。与信号驱动模型的主要区别是:信号驱动IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成,即通知我们时数据已经复制到应用程序缓冲区内了。
阻塞IO、非阻塞IO、IO复用、信号驱动式IO前四种IO模型有一个共同点:对于recvfrom的第二个阶段即把数据从内核缓冲区复制到调用缓冲区期间,进程都阻塞与recvfrom调用。即都有阻塞。而异步IO模型复制操作由异步IO函数完成,不需要因为调用recvfrom而阻塞。即:全程没有阻塞,异步IO参与了recvfrom的两个阶段。
linux支持三种IO复用,select poll epoll
int select(int maxfdpl,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval* timeout);
FD_ZERO、FD_SET、FD_CLR、FD_ISSET
来清空文件描述符集、添加描述符、删除描述符、判断描述符是否位于文件描述符集中。也可用赋值语句将某个描述符集的值赋值给另一个描述符集。注意:在设置描述集时,必须给描述集初始化,否则可能发生不可预期的结果。 maxfdp1为检测的最大描述值+1,不要忘加1;描述符集是输入输出参数再次调用select时忘记再次设置描述符。
(1)select返回的条件为:等待的文件描述符有一个或多个就绪,或者指定的时间到达。
(2)套接字读描述符就绪的条件:
(3)套接字写描述符就绪的条件:
(1)不能在读输入读到eof时就认为程序处理完毕
问题描述:
当实现echo客户端,如果从终端读数据,当读数据读到eof时,我们认为数据传送完毕close套接字是没有问题的。但如果把从终端读数据换成从文件读数据,当我们read到eof后close套接字就会发现从服务器传回来的数据少于我们发送的数据。
问题原因:
这是因为从终端读数据,发送数据和读取数据是停等模式的,我们只有等待对端发来了数据才向对端发送数据,所以当从终端输入了eof时我们可以确认之前发送对端的数据对端已经全部收到,而且对端发来的数据我们也全部收到。但把终端换成文件,那么向服务器写完一行数据后会立即写一行,两个写动作之间几乎没有时间延迟,这样当读文件读到了eof,可能连接通道中还有数据连接通道中可能还有其他的请求和应答,或者是去往服务器的请求数据,或者是返回客户端的应答数据。
也就是说当从文件读到了eof,只意味着我们已经写完了数据,但我们并不知道全部数据有没有全部发往对端,或者对端是否还有数据发给我们。
解决方案:我们需要一种只关闭Tcp写的方式,也就是说我们告诉服务器我们已经完成了数据发送,但还可以继续接受对端发送的数据,只有读到对端发来了eof才认为数据传送结束。而因为本端发送的eof和对端发送的eof是本端和对端最后发送的消息,如果两端都收到了对端发送的eof,则可以保证两端之前发送的消息肯定都收到了。这可以调用shutdown完成。
(2)不要让select和stdio和其他带有缓冲区的函数混合使用
**原因:**stdio具有自己的缓冲区,虽然fgets只返回一行,但是其他输入行的数据仍然在stdio缓冲区中。这样再次调用select等待新的输入时,由于select不知道stdio的缓冲区数据,它只单纯从read系统调用的角度判断是否有数据可读,而不是从fget角度考虑。这样可能就会产生虽然fgets能读到数据,但read却发现数据已经读完,而select返回后发现无数据可读的问题。
int shutdown(int sockfd,int howto);
shutdown与close的区别:close只有当文件描述符引用计数为0才发送FIN,而shutdown不管引用计数就发送FIN。close终止读和写,而shutdown可用于只终止读半步或写半步。当告知对端我们已经完成了数据发送,但还可以读取对端发送的数据,可让howto设置为SHUT_WR。调用shutdown(fd,SHUT_WR)后,当前留在套接字发送缓冲区中的数据将被发送掉,后跟Tcp的FIN终止序列。
#include "unp.h"
void str_cli(FILE* fp,int sockfd);
int main(int argc,char* argv[])
{
if(argc != 3)
{
err_quit("usage: ip port\n");
}
int sockfd = Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
Inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
servaddr.sin_port = htons(atoi(argv[2]));
Connect(sockfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
str_cli(stdin,sockfd);
exit(0);
}
void str_cli(FILE* fp,int sockfd)
{
int fd = fileno(fp);
char buf[MAXLINE] = {0};
fd_set rds;
FD_ZERO(&rds);
int eof = 0;
int maxfds = -1;
ssize_t size = 0;
for(;;)
{
FD_SET(sockfd,&rds);
if(!eof)
{
FD_SET(fd,&rds);
maxfds = (fd > sockfd ? fd : sockfd) + 1;
}
else
{
maxfds = sockfd + 1;
}
Select(maxfds,&rds,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rds))
{
if( (size = Read(sockfd,buf,MAXLINE)) == 0)
{
if(eof)
{
break;
}
else
{
err_quit("str_cli:server terminate prematurely");
}
}
else
{
Write(fd,buf,size);
}
}
else if(FD_ISSET(fd,&rds))
{
if( (size = Read(fd,buf,MAXLINE)) == 0)
{
eof = 1;
Shutdown(sockfd,SHUT_WR);
FD_CLR(fd,&rds); //不要忘记清除fd
}
else
{
writen(sockfd,buf,size);
}
}
}
}
(1)select只能检测文件描述符,fileno用于把标准IO文件指针转换为对应的描述符
(2)使用eof标记是否标准输入了eof,当eof为1时不再把它放入文件描述符集中
#include "unp.h"
int main(int argc,char* argv[])
{
if(argc != 3)
{
err_quit("usage:port listen_backlog");
}
int listenfd = Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(atoi(argv[1]));
Bind(listenfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
Listen(listenfd,atoi(argv[2]));
char buf[MAXLINE] = {0};
int clientfd[FD_SETSIZE];
for(int i = 0;i < FD_SETSIZE; i++)
{
clientfd[i] = -1;
}
int maxfd = listenfd;
int maxi = -1;
ssize_t size;
fd_set rds,allds;
FD_ZERO(&allds);
FD_SET(listenfd,&allds);
for(;;)
{
rds = allds;
int n = select(maxfd + 1,&rds,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&rds))
{
struct sockaddr_in clientaddr;
socklen_t addrlen;
int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&addrlen);
int i;
for(i = 0;i<FD_SETSIZE;i++)
{
if(clientfd[i] < 0)
{
clientfd[i] = connfd;
break;
}
}
if( i >= FD_SETSIZE)
{
err_msg("too many client");
continue;
}
FD_SET(connfd,&allds);
if(maxfd < clientfd[i])
maxfd = clientfd[i];
if(maxi < i)
maxi = i;
n--;
if(n <= 0)
continue;
}
for(int i = 0; i<= maxi;i++)
{
if(clientfd[i] < 0)
continue;
if(FD_ISSET(clientfd[i],&rds))
{
ssize_t size = Read(clientfd[i],buf,MAXLINE);
if(size == 0)
{
close(clientfd[i]);
FD_CLR(clientfd[i],&allds);
clientfd[i] = -1;
}
else if(size > 0)
{
writen(clientfd[i],buf,size);
}
n--;
if(n == 0)
break;
}
}
}
}
使用clientfd数组来存储已连接的客户端fd,使用maxi标记clientfd数组中有效fd最小index,这样select后,就不用从0到maxfd开始遍历,而是从0到maxi遍历,减少遍历时间。
(1)select文件描述符集:
FD_ZERO、FD_SET、FD_CLR、FD_ISSET
来清空文件描述符集、添加描述符、删除描述符、判断描述符是否位于文件描述符集中。(2)maxfd:
(3) select会被信号中断,需要做好返回EINTR的准备
(4) 套接字读描述符就绪条件之一:
该套接字接收缓冲区的数据字节数>=套接字接收缓冲区低水位标记,可以使用SO_RCVLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为1。
(5)套接字写描述符就绪的条件之一:
该套接字发送缓冲区的可用空间字节数>=套接字发送缓冲区低水位标记,可以使用SO_SNDLOWAT套接字选项设置套接字低水位标记。对于Tcp和Udp套接字,默认值为2048。
(6)使用select实现echo客户端注意两个问题:
(7)fileno用于把标准IO文件指针转换为对应的描述符