网络编程中Select的学习记录
主要记录个人学习过程中,对select函数的理解和使用
为什么要使用Select
通过socket(),bind(),listen(),accept()就可以创建一个简单服务器时,但这种服务器在同一时间,只能有一个客户端与之连接。
为了能够使得一个服务器能够同时和多个服务器连接,可以通过多进程或多线程的方式。多进程的方式,会频繁的发生进程的创建和销毁,增加cpu和内存的的开销,线程与之类似。
因此可以使用I/O复用技术来解决这个问题,简单来说,I/O复用就是可以使用一个进程来处理多个I/O操作,具体解释,如下大佬说明:
关于select函数的基本结构
这里只是简述,很多大佬已经解释的很详细了
int ret = select(int nfds,
fd_set* readfds,
fd_set* writefds,
fd_set* exceptfds,
const struct timeval* timeout)
nfds:一般传入监听的文件描述符中,最大的文件描述符+1
readfds:读 文件描述符监听集合 传入传出参数
writefds:写 文件描述符监听集合 传入传出参数
exceptfds:异常 文件描述符监听集合 传入传出参数
timeout: >0: 设置监听时常。注意:这里传入的不是一个整型变量,而是一个结构体,因此如果想设置监听时常,需定义一个结构体变量
NULL: 阻塞监听
0: 非阻塞监听,轮询
关于select函数的使用过程中,须知得几个函数
fd_set rset; //定义一个监听集合
void FD_ZERO(fd_set *set); //清空一个文件描述符集合
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); //将待监听得文件描述符,添加到监听集合中
FD_SET(3, &rset);
void FD_CLR(int fd, fd_set *set); //将一个文件描述符从监听集合中移除
FD_CLR(3, &rset);
int FD_ISSET(int fd, fd_set *set); //判断一个文件描述符是否在监听集合中
FD_ISSET(3, &rset);
select服务器的实现过程
select重点在设置的监听集合。
fd_set rset; //通过该设置一个监听集合
在使用select函数前,和普通服务器的创建方法一样,通过socket,bind,listen创建出一个监听的文件描述符lfd。
int lfd;
struct sockaddr_in ser_addr,cli_addr;
socklen_t cli_addr_len;
memset(&ser_addr,0,sizeof(ser_addr)); //结构体初始化
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(SERV_PORT);
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd < 0)
sys_err("socket error");
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //端口复用
if(bind(lfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)))
sys_err("bind error");
if(listen(lfd,128))
sys_err("listen error");
由于select的形参 readfds 是一个传入传出参数,因此在这个服务器中,设置了两个集合,一个为传入集合allset,一个为传出集合,rset。
在lfd创建之后,创建出了两个集合。
解释:为什么一个叫传入集合,一个叫传出集合。
传入集合,allset是:传给select函数的集合,表示有多少文件描述符被监听。
传出集合,rset是:select返回的集合,表示被被监听的文件描述符中,哪几个文件描述符有事件发生。这里集合的本质是位图。
传入集合初始化,并将lfd添加到传入集合中,从此以后,lfd不会从传入集合中移除。
然后传出集合初始化(此时的传出集合刚被传入集合初始化,所以作为参数传给select,当select返回后,传出集合就变成了真正的传出集合),并传给select函数,来记录被监控的文件描述符中哪些有事件发生。
lfd的作用是,当有新的客户端连接到服务端,lfd会被加入到传出集合中,然后通过遍历传出集合,操作accept函数,创建出新的文件描述符cfd,用于客户端与服务端的数据传输。
fd_set rset,allset; //allset为传入集合 ,表示有多少文件描述符需要监听,rset为传出集合,表示监听到了有事件发生的文件描述符。
FD_ZERO(&allset); //清空监听集合
FD_SET(lfd,&allset); //将待监听的fd添加到监听集合中
rset = allset; //传出集合通过传入集合初始化,然后传入到select函数中,根据事件发生改变
ret = select(maxfd+1,&rset,NULL,NULL,NULL); //使用select监听,返回值ret表示监听的集合中有几个事件发生,
//例如,被监听的文件描述符有10个,而真正有事件发生的文件描述符只有5个,ret=5
if(FD_ISSET(lfd,&rset)) //有客户端请求链接,rset集合中有lfd
{
cli_addr_len = sizeof(cli_addr);
cfd = accept(lfd,(struct sockaddr *)&cli_addr,&cli_addr_len); //调用accept创建链接,返回cfd
if(cfd < 0)
sys_err("accept error");
FD_SET(cfd,&allset); //将返回的fd加入到传入集合中,表示这个文件描述符被监听
整体代码如下(里面解释最详细):
嗯,注释尽量写的详细,主要看注释
/*
select,多路I/O转接服务器
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>
#define SERV_PORT 9090
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int lfd,cfd;
int ret,i,n;
char buf[BUFSIZ];
struct sockaddr_in ser_addr,cli_addr;
socklen_t cli_addr_len;
memset(&ser_addr,0,sizeof(ser_addr)); //结构体初始化
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(SERV_PORT);
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd < 0)
sys_err("socket error");
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //端口复用
if(bind(lfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr)))
sys_err("bind error");
if(listen(lfd,128))
sys_err("listen error");
fd_set rset,allset; //allset为传入集合 ,表示有多少文件描述符需要监听,rset为传出集合,表示监听到了有事件发生的文件描述符。
int maxfd = 0; //定义最大文件描述符
maxfd = lfd;
FD_ZERO(&allset); //清空监听集合
FD_SET(lfd,&allset); //将待监听的fd添加到监听集合中
while(1)
{
rset = allset;
ret = select(maxfd+1,&rset,NULL,NULL,NULL); //使用select监听,返回值ret表示监听的集合中有几个事件发生,
//例如,被监听的文件描述符有10个,而真正有事件发生的文件描述符只有5个,ret=5
if(ret < 0)
{
sys_err("select error");
}
/*
**注意:此时的rset为传出集合,如果lfd在传出集合rset中,表示有连接事件发生,如果lfd不在传出集合中,表示没有连接事件发生。
allset为传入集合,传入集合存储的是被监控的文件描述符,当某个文件描述符对应的事件发生了,则传出集合rset中会有该文件描述符,
如果某个文件描述符对应的事件没有发生,则传出集合中就没有这个文件描述符,我们只需要处理传出集合rset中的事件即可。
例如lfd,在传入集合allset中,一定会有lfd,表示lfd一定会被监听,至于监听了有没有连接事件发生,就不一定了。
如果有新的客户端连接进来,则传出集合rset中会有lfd,然后通过accept函数,产生新的文件描述符cfd,然后将cfd加入到allset中(之所以
加入allset中而不是rset中,是因为此时表示某个客户端连接进来,但是并没有发生数据的读写,加入到allset中表示这个客户端会受到监控)
在本次循环中,会处理rset中的cfd,表示在这次循环中,这些cfd有数据的读写。**
*/
if(FD_ISSET(lfd,&rset)) //有客户端请求链接,rset集合中有lfd
{
cli_addr_len = sizeof(cli_addr);
cfd = accept(lfd,(struct sockaddr *)&cli_addr,&cli_addr_len); //调用accept创建链接,返回cfd
if(cfd < 0)
sys_err("accept error");
FD_SET(cfd,&allset); //将返回的cfd加入到传入集合中,表示这个文件描述符被监听
if(maxfd < cfd) //更改最大的cfd的值
maxfd = cfd;
//if(ret == 1) //说明select的返回值为1,并且是lfd,表示该次while循环只有新客户端连接进来,而并没有发生客户端和服务端之间
//的读写操作,因此接下来的for循环就不必在经历了,直接进行下次while循环。
//continue;
}
for(i=lfd+1;i<maxfd+1;i++) //处理满足读事件的cfd,此时表示有客户端和服务端之间的读写操作,遍历查找,i是文件描述符
{
if(FD_ISSET(i,&rset)) //找到满足读事件的cfd
{
n = read(i,buf,sizeof(buf));
if(n == 0) //检测到客户端关闭链接
{
close(i); //关闭cfd
FD_CLR(i,&allset); //将关闭的cfd,从传入集合中除去,表示不再对此文件描述符进行监听
}
else if(n == -1)
sys_err("read error");
for(int j = 0;j<n;j++)
buf[j]=toupper(buf[j]);
write(i,buf,n);
write(STDOUT_FILENO,buf,n);
}
}
}
return 0;
}
在代码中,maxfd的作用是为了select第一个参数的传递,同时减少在rset中的遍历次数,算是一种优化,如果不设置,就得从0循环到1023来查找哪些文件描述符有事件发生。
上述代码中,在判断select得返回值,ret的大小时,我的理解是,如果ret == 1,也有可能表示没有连接事件发生,只是有一个读写事件发生,因此我把源代码中的这个语句注释掉了,如果我理解有误,望大佬们不吝赐教。
以上为我对select函数的个人理解,如有错误,希望大佬们帮忙指正,提前感谢!!!!