在我们之前的例子中,客户端把id发送过去,服务器端接收并处理这个id,总的来说客户端十分的简单,仅仅是,提前设定好一些消息,并且发送过去,然后等待响应。
但是当我们有一种这样的客户端——等待用户输入,然后把输入内容传递给服务端。这样的话,传递给服务端的内容是由客户自己决定的,而不是一条固定的消息。
有了这种场景以后,可以想象客户端程序要迭代地处理客户输入、服务器响应。我们把这看作两个读事件,就可以利用IO多路复用来解决。
一、select函数
void cli_input(FILE* fp,int sockfd){
int maxfdp1,stdineof=0;
fd_set rset;
const int buffersize=1024;
int n;
char buf[buffersize];
FD_ZERO(&rset);
while(1){
if(stdineof==0){
FD_SET(fileno(fp),&rset);
}
FD_SET(sockfd,&rset);
maxfdp1=max(fileno(fp),sockfd)+1;
select(maxfdp1,&rset, nullptr, nullptr, nullptr);
if(FD_ISSET(sockfd,&rset)){
if((n=read(sockfd,buf,buffersize))!=0){
buf[n]='\0';
cout<<"get server message:"<
首先我们要有一个事件的集合,这里面是rset,就是读事件的集合,可以“读”套接字的描述符,也可以读标准IO的描述符号(在这里面就是fileno(fp),可以返回FILE类的描述符)。使用之前先对rset清空(FD_ZERO),每次select返回后,都必须把要监听的描述符重新置位,也就是FD_SET方法,maxfdp1意味着select以循环扫描各个事件时候,范围有多大,一般来说,select最多监听1024个。
下面两个if是为了确认是哪个事件准备就绪了。
第一个if表明的是一个套接字可以读了,说白了就是服务器的数据已经接收到了,与前面不同的关键一点就是read是立即完成的,但read是个阻塞函数,如果数据不准备好会一直阻塞,在select已经通知了我们数据准备完毕后,会立即读取到数据。
第二个if表面是用户的输入准备就绪了,一个细节就是这里面的shutdown函数,当用户输入0个字节,说明已经要结束输入了,shutdown(sockfd,SHUT_WR)表明对此套接字执行关闭写的一端(也就是用户端)这样只会接收服务器数据但不会再发送数据了。最后,清理对此描述符的监听。
二、服务器中select的运用
前面我们提到了多线程多进程的使用,顺便说一句,多进程很可能导致系统崩溃,所以一般情况下用的都是多线程。多线程的好处很明显,那就是并发的处理多个客户端请求,但是实际上,我们看到15秒后面多出来的零点几秒就是处理的代价,因为要不断切换上下文,当线程非常多的时候,依然会造成大延迟。
考虑我们刚才提到的情况,我们利用多线程构造了大量对用户输入处理的线程,但用户输入可是随机的,这些线程,必须时时刻刻都存在着。但是实际上跟计算机相比,人的输入速度可就慢多了,所以可以说这种形式很浪费时间。
如果我们用一个线程循环地监听呢?这样就可以当有输入时候处理,没输入的时候循环等待。
void server4(){
const uint16_t listened_port=9000;
const char* localhost="127.0.0.1";
const int listening_queue_length=1024;
const int buffersize=1024;
int listen_fd=socket(AF_INET,SOCK_STREAM,0);
sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
in_addr temp;
inet_pton(AF_INET,localhost,&server_addr.sin_addr);
//server_addr.sin_addr.s_addr=htonl(temp.s_addr);
server_addr.sin_port=htons(listened_port);
bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
listen(listen_fd,listening_queue_length);
int maxfd=listen_fd,maxi=-1;
int nready;
vector clients(FD_SETSIZE,-1);
socklen_t len;
fd_set rset,allset;
FD_ZERO(&allset);
FD_SET(listen_fd,&allset);
char buf[buffersize];
while(1){
rset=allset;
nready=select(maxfd+1,&rset, nullptr, nullptr, nullptr);
if(FD_ISSET(listen_fd,&rset)){
int i;
int connect_fd=accept(listen_fd,(sockaddr*)&server_addr,&len);
for(i=0;i
在这里可以看到有两种事件,一个是listen_fd“可读”了,也就是说一个新的连接到达,这时候应立即调用accept,因为肯定会立即返回的。
另一种就是connect_fd“可读”了,也就是客户端数据到达,这里面做了个很简单的处理,就是把原数据回写到客户端。用长度为1024的数组来保存这些fd,nready的意思是,有的时候不一定一瞬间只有一个事件就绪,也有可能多个事件就绪,这个时候要做的是既要处理新连接,又要处理数据。
现在考虑一个常见问题,就是如果服务端的处理是一个耗时操作(比如我们之前写的3秒钟处理),那在下面的for循环中如果迭代处理那将严重损耗性能。正确的做法是用一个线程来处理任务,与上一节的处理方法一样。
二、poll
其实select的限制很大,每次只能监听至多1024个描述符,于是有了可以监听更多的函数——poll。
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout)
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
其中pollfd就是它要监听的一个结构,我们把感兴趣(等待)的事件写入events,然后返回后,会有事件出现,查看revents即可。
第二个参数可以自行设定监听的数量。下面用这个改写一下服务器端:
void server5(){
const uint16_t listened_port=9000;
const char* localhost="127.0.0.1";
const int listening_queue_length=1024;
const int buffersize=1024;
const int MAX_LISTENED=10240;
int listen_fd=socket(AF_INET,SOCK_STREAM,0);
sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
in_addr temp;
inet_pton(AF_INET,localhost,&server_addr.sin_addr);
//server_addr.sin_addr.s_addr=htonl(temp.s_addr);
server_addr.sin_port=htons(listened_port);
bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
listen(listen_fd,listening_queue_length);
socklen_t len;
pollfd clients[MAX_LISTENED];//10240,10 multiple .
clients[0].fd=listen_fd;
clients[0].events=POLLRDNORM;
for(int i=1;i
可以看到,逻辑基本差不多。POLLRDNORM表示事件是标准数据可读(tcp/udp正规数据都被认为是普通数据),由此可以看出和select几乎差不多的方法。但是可以处理的数量远大于select。