(一)select函数实现多路并发服务器
(1) select()函数的基础知识
Select()函数:用于监视文件描述符的变化情况——读写或是异常
#include <sys/select.h> //头文件包含
#include<sys/types.h> //头文件包含
#include<sys/time.h> //头文件包含
#include<unistd.h> //头文件包含
int select (int max_fd + 1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout);
参数 | 功能 |
---|---|
max_fd+1 | 指待测试的fd的总个数,它的值是待测试的最大文件描述符加1 |
fd_set *readset,fd_set | 用于检查可读性(不需要:NULL) |
fd_set *writeset | 用于检查可写性(不需要:NULL) |
fd_set *exceptset | 用于检查带外数据(不需要:NULL) |
const struct timeval * timeout | 一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。 |
返回值:
大于0:就绪描述字的正数目
等于0:超时
等于-1:出错
拓展:
①带外数据:使用与普通数据不同的通道独立传送给用户,是相连的每一对流套接口间一个逻辑上独立的传输通道。(linux系统的套接字机制支持低层协议发送和接受带外数据。但是TCP协议没有真正意义上的带外数据。)
1.1 struct fd_set结构体
struct fd_set:可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄(这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内。)。
①fd_set集合可以通过下面四个宏由人为来操作。
②在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。(FD_SET有限,select()默认同时处理1024个链接)
fd_set feset; //fd_set的数据类型
FD_ZERO(fd_set* fds) //清空集合(一如果不初始化,会导致不可预期的后果)
FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除
1.2 struct timeeval结构体
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
}
(2) select()多路复用实现服务器多路并发
2.1 服务器多路并发的流程图
2.2 服务器多路并发代码
2.2.1 长选项命令解析&判断命令参数的导入
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#include<time.h>
#include<pthread.h>
#include<getopt.h>
#include<libgen.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) //字节长除于首字节=个数
static inline void msleep(unsigned long ms);
int socket_server_init(char *listen_ip,int listen_port);
static inline void print_usage(char *progname);
int main(int argc,char **argv)
{
int daemon_run=0;
int serv_port=0;
int listen_fd;
char *progname=NULL;
int opt;
int fds_array[1024]; //select 只能寻1024
int i,k;
int conn_fd;
int found;
int max_fd=0;
char buf[1024];
int rv;
fd_set rdset;
//一、命令帮助提示
//另外:打印命令提示进行函数抽象
progname=basename(argv[0]); //从arg[0]中截取文件名
//1.opts结构体定义
struct option opts[]=
{
{"daemon",no_argument,NULL,'b'}, //守护进程
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{0,0,0,0}
};
//2.命令获取与解析
while((opt=getopt_long(argc,argv,"d:p:h",opts,NULL))!=-1)
{
switch(opt)
{
case'd':
daemon_run=1;
break;
case'p':
serv_port=atoi(optarg);
break;
case'h':
print_usage(progname);
return EXIT_SUCCESS;
//出错
default:
break;
}
}
//3.判断输入值是否正确
//serv_port置-1,还需要if(!...)吗
if(!serv_port)
{
print_usage(progname);
return -1;
}
//判断是否需要守护进程(即是否要后台运行)
if(daemon_run)
{
daemon(0,0);
}
//判断服务器是否监听端口
if((listen_fd=socket_server_init(NULL,serv_port))<0)
{
printf("ERROR:%s server listen on port %d failure",argv[0],serv_port);
return -2;
}
//将数组中所有元素初始化为-1;
for(i=0;i<ARRAY_SIZE(fds_array);i++) //ARRAY_SIZE
{
fds_array[i]=-1;
}
fds_array[0]=listen_fd;
简析:
①命令解析这里就不用再多说了,之前都有提到过。
②宏定义一个类型:#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
我们知道,FD的数据类型是整型。那么现在我想知道传入FD的数目,即用(全部/首位)即可。
③下面这段代码的作用
for(i=0;i<ARRAY_SIZE(fds_array);i++)
{
fds_array[i]=-1;
}
fds_array[0]=listen_fd;
首先,我们知道,这时候还没有进行FD的接收。我们对这个fds_array数组中所有元素进行一个初始化为-1,这样当有FD接收就可以知道了。
当然刚开始的listen_fd也需要被监听。
2.2.2 select()函数实现
//二、select()
for( ; ;)
{
FD_ZERO(&rdset); //清空集合
//用for循环判断是否有fd进入,没有则继续等待
for(i=0;i<ARRAY_SIZE(fds_array);i++)
{
if(fds_array[i]<0)
{
continue; //等待
}
max_fd=fds_array[i]>max_fd?fds_array[i]:max_fd; //listen_fd的个数
FD_SET(fds_array[i],&rdset); //将给指定的listen_fd加入集合
}
//阻塞ing、等待FD_SET放入的指定FD
rv=select(max_fd+1,&rdset,NULL,NULL,NULL); //0超时,-1出错
if(rv<0)
{
printf("select failure:%s\n",strerror(errno));
break;
}
else if(rv==0)
{
printf("select get timeout\n");
continue;
}
//rv>0
//判断listen_fd是否在fd集合中:是则进行连接,不是则可能已连接
if(FD_ISSET(listen_fd,&rdset)) //fd在集合中则返回非零值
{
if((conn_fd=accept(listen_fd,(struct sockaddr *)NULL,NULL))<0)
{
printf("accept new client failure:%s\n",strerror(errno));
continue;
}
else
{
//判断当前连接客户端数量,是否达到上限
found=0;
for(i=0;i<ARRAY_SIZE(fds_array);i++)
{
if(fds_array[i]<0)
{
printf("accept new client[%d] and add it into arry\n",conn_fd);
fds_array[i]=conn_fd;
found =1;
break;
}
}
//当fds_array达到上限,则found=0
if(!found)
{
printf("accepting a new client[%d] reach the ceiling\n",conn_fd);
close(conn_fd);
}
}
}
else //已连客户:1.数据请求 2.中断连接
{
for(i=0;i<ARRAY_SIZE(fds_array);i++)
{
//判断是否集合中有FD,判断指定FD是否在集合中
if(fds_array[i]<0||!FD_ISSET(fds_array[i],&rdset))
{
continue; //结束循环
}
//IO操作
if((rv=read(fds_array[i],buf,sizeof(buf)))<=0)
{
printf("socket[%d] read failure or get disconnect.\n",fds_array[i]);
close(fds_array[i]);
fds_array[i]=-1;
}
else
{
printf("socket[%d] read get %d bytes data \n",fds_array[i],rv);
//字符:小转大字母
for(k=0;k<rv;k++)
{
buf[k]=toupper(buf[k]);
}
if(write(fds_array[i],buf,rv)<0)
{
printf("socket[%d]write failure :%s\n",fds_array[i],strerror(errno));
close(fds_array[i]);
fds_array[i]=-1;
}
}
}
}
}
CleanUp:
close(listen_fd);
return 0;
}
2.2.3 命令提示打印函数
static inline void print_usage(char *progname) //了解static inlin 意义
{
printf("Usage:%s [OPTION]...\n",progname);
printf("%s is a socket server program\n",progname);
printf("Mandatory arguments:long or short options\n");
printf("-b[daemon] set program running on background\n");
printf("-p[port] socket server port address\n");
printf("-h[help] display this help information\n");
printf("\nExample: %s -b -p 1111 \n",progname);
return ;
}
2.2.4 socket server四步骤
int socket_server_init(char *listen_ip,int listen_port)
{
int on=1;
int rv=0;
int listen_fd;
struct sockaddr_in servaddr;
//1.socket
if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket () create a TCP socket failure:%s\n",strerror(errno));
return -1;
}
//端口重用
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//初始化操作
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(listen_port);
//判断IP地址,并初始化。意义:这里if else相当于两用。监听特定IP,或监听所有IP。
if(!listen_ip)
{
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
printf("listen other IP address [%d]",listen_ip);
}
else
{
if(inet_pton(AF_INET,listen_ip,&servaddr.sin_addr)<=0)
{
printf("inet_pton() set listen IP address failure.\n");
rv=-2;
goto CleanUp;
}
}
//2.bind
if(bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
rv=-3;
goto CleanUp;
}
//3.listen
if(listen(listen_fd,13)<0)
{
printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
rv=-4;
goto CleanUp;
}
CleanUp:
if(rv<0)
{
close(listen_fd);
}
else
{
rv=listen_fd;
return rv;
}
}
2.2.5 select等待I/O的时间函数
static inline void msleep(unsigned long ms)
{
struct timeval tv;
tv.tv_sec=ms/1000;
tv.tv_usec=(ms%1000)*1000;
select(0,NULL,NULL,NULL,&tv);
}