什么是多路复用:类似C语言的switch。一句话简单讲就是:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。用多路复用可以实现监听多个客户端操作。
原理:
创建一个集合,这个集合保存你关心的文件描述符。用相关函数select对这个集合进行阻塞。当集合中任意一个文件描述符就绪的话,就解除阻塞,向下运行。
1、select函数
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:
多路复用,用来监听文件描述符
参数:
nfds:最大文件描述符值+1
readfds:所有要读的文件文件描述符的集合(读集合)
writefds:所有要的写文件文件描述符的集合(写集合)
exceptfds:其他要向我们通知的文件描述符(异常集合)
timeout:超时时间,结构体指针,设置为NULL阻塞
struct timeval {
long tv_sec; /* seconds */秒1
long tv_usec; /* microseconds */微秒
};
返回值:
成功返回所有文件描述符
失败返回-1,并设置错误码
宏:
void FD_CLR(int fd, fd_set *set); //将文件描述符fd移出fd_set集合
int FD_ISSET(int fd, fd_set *set);//判断fd是否在那个fd_set集合
void FD_SET(int fd, fd_set *set);//将fd加入到fd_set集合
void FD_ZERO(fd_set *set);//清空fd_set集合
示例:
//多路复用select
fd_set readfds;//多路复用函数要用到的读集合
int max = 0;//用来存储文件描述符最大值
char buf[50];//让我们可以看见效果
int cfd;
while(1){
FD_ZERO(&readfds);//清空fd_set集合
FD_SET(serfd,&readfds);//将serfd放进来,看有没有客户端连接
FD_SET(0,&readfds);//将文件描述符0,也就是键盘拿来测试是否多路
max = serfd;
ret = select(max+1,&readfds,NULL,NULL,NULL);
if(ret<0)//返回值判断
{
perror("select");
return -1;
}
if(FD_ISSET(0, &readfds)){//判断fd是否在那个fd_set集合
scanf("%s",buf);
printf("键盘输入:%s\n",buf);
}else if(FD_ISSET(serfd, &readfds)){//判断fd是否在那个fd_set集合
struct sockaddr_in cli_addr;
int addlen = sizeof(cli_addr);
cfd = accept(serfd,(struct sockaddr *)&cli_addr,&addlen);
if(cfd<0)//返回值判断
{
perror("accept");
return -1;
}
printf("客户%d连接进来了!\n",cfd);
}
}
2、poll函数
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
监听文件描述符哪个被触发,将fds结构体中revents改变
参数:
fds:监听的文件描述符“数组”
nfds:文件描述符的个数
timeout:超时时间 >0:阻塞对应时间 == 0:不阻塞 -1:一直阻塞
返回值:
成功返回0
失败返回-1,并设置错误码
struct pollfd {
int fd; /* file descriptor */ 0号事件 (是否键盘输入)
short events; /* requested events */ POLLIN(希望它被触发)
short revents; /* returned events */ (poll)触发了,置为POLLIN,0
};
示例:
int cfd; // 客户端socket描述符
struct pollfd fds[2]; // pollfd 结构体数组
char buf[50]; // 缓冲区
// 设置0号描述符为标准输入,1号描述符为服务器socket描述符
fds[0].fd = 0;
fds[0].events = POLLIN; // 监听可读事件
fds[1].fd = serfd;
fds[1].events = POLLIN; // 监听可读事件
while(1) // 无限循环
{
if(poll(fds, 2, -1) < 0) // 调用poll函数,监听两个描述符的事件,-1代表超时时间设置为无穷大
{
perror("poll"); // 出错处理
return -1;
}
if(fds[0].revents == POLLIN) // 检查0号描述符的事件是否为可读事件
{
printf("0号异步事件被触发了!\n");
scanf("%s", buf); // 从标准输入读取输入
printf("键盘输入:%s\n", buf);
fds[0].revents = 0; // 将0号描述符的revents置为0,表示处理完事件
}
if(fds[1].revents == POLLIN) // 检查1号描述符的事件是否为可读事件
{
struct sockaddr_in cli_addr; // 客户端地址结构体
int addlen = sizeof(cli_addr);
cfd = accept(serfd, (struct sockaddr *)&cli_addr, &addlen); // 接受客户端连接
if(cfd < 0) // 判断连接是否成功
{
perror("accept"); // 连接失败处理
return -1;
}
printf("客户端%d连接进来了!\n", cfd);
fds[1].revents = 0; // 将1号描述符的revents置为0,表示处理完事件;不置位则表示继续监听可读事件
}
}
3、并发服务器
并发服务器:
真正意义上做到一对多,但是,对系统的负担很大。
进程并发服务器:
服务器通过进程的方式开展可以存储很多客户端的信息,相比线程,开销比较大,每连接一个客户进来,就需要服务器开多一个进程存储该客户端
fork
线程并发服务器:
占用资源比较小,代码维护起来困难 pthread_create自动收尸,pthread_detach 不自动,pthread_join
测试:
fork函数执行并发服务器:
int ret;
void *recv_msg(int *cfd)//recv_msg = (*start_rtn) void *(*)(void *)
{
int fd = *cfd;//万能指针先强转成int *,再解引用
char buf[300];//接收每一个对应客户的信息
char buf1[100]="迎面走来的你让我如此蠢蠢欲动";
//将传入的参数存下来,通过它向客户端发送信息
while(1){
bzero(buf,sizeof(buf));
ret = recv(fd,buf,sizeof(buf),0);
if(ret==0)
{
printf("客户%d似乎不满意,离开了\n",fd);
close(fd);
break;
}else{
printf("客户%d说:%s\n",fd,buf);
send(fd,buf1,strlen(buf),0);//阻塞
}
}
}
//等待客户端连接
int cfd;
struct sockaddr_in cli_addr;
int addlen = sizeof(cli_addr);
while(1)
{
cfd = accept(serfd,(struct sockaddr *)&cli_addr,&addlen);
if(cfd<0)
{
perror("accept");
return -1;
}
printf("欢迎客户%d来访!\n",cfd);
printf("客户的IP地址是 %s 端口号是 %d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
if(fork()==0)//开辟子进程,不影响主进程
{
recv_msg(&cfd); //在子进程里执行需要操作的函数
}
}
线程执行并发服务器:
int ret;
void *recv_msg(void *cfd)//recv_msg = (*start_rtn) void *(*)(void *)
{
int fd = *(int *)cfd;//万能指针先强转成int *,再解引用
char buf[300];//接收每一个对应客户的信息
char buf1[100]="迎面走来的你让我如此蠢蠢欲动";
//将传入的参数存下来,通过它向客户端发送信息
while(1){
bzero(buf,sizeof(buf));
ret = recv(fd,buf,sizeof(buf),0);
if(ret==0)
{
printf("客户%d似乎不满意,离开了\n",fd);
close(fd);
break;
}else{
printf("客户%d说:%s\n",fd,buf);
send(fd,buf1,strlen(buf),0);//阻塞
}
}
}
//等待客户端连接
int cfd;
pthread_t tid;//开辟线程,保证while(1)中一直在做接收客户端连接的请求
//当连接进来一个客户端,我们就开辟线程为其提供服务,不影响主进程accept
while(1)
{
cfd = accept(serfd,NULL,NULL);
if(cfd<0)
{
perror("accept");
return -1;
}
printf("欢迎客户%d来访!\n",cfd);
printf("88号服务员为您提供服务!\n");
if(pthread_create(&tid,NULL,recv_msg,&cfd)<0)//开辟线程服务对应的客户
{
perror("pthread_create");
return -1;
}
pthread_detach(tid); //自动收尸
}