写在前面:本篇介绍多路IO转接服务器实现的三种方式,select、poll、epoll,下面开始一一介绍,本篇文字叙述会比较少,代码的量会大点。
Linux 网络编程 全解(一)--------网络基础协议
Linux 网络编程 全解(二)--------套接字socket
Linux 网络编程 全解(三)--------TCP三次握手、数据传输、四次挥手、滑动窗口
Linux 网络编程 全解(四)--------多进程并发服务器和多线程并发服务器
Linux 网络编程 全解(五)--------TCP状态切换
正文:
一、select实现多路IO转接服务器
1、select函数简介:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
作用: 借助内核,select来监听客户端连接、数据通信事件。
函数参数:
nfds:监听的描述符里面的最大描述符+1.
readfds,传入传出参数,监听的读数据的文件描述符的集合。传入:要监听的文件描述符的集合。传出:实际有事件发生的文 件描述符集合。
writefds:写文件描述符集合。传入传出参数。
exceptfds:异常文件描述符集合。传入传出参数。
timeout:三种情况:
(1)、NULL,永远等下去
(2)、设置 timeval,等待固定时间
(3)、设置 timeval 里时间均为 0,检查描述字后立即返回,轮询
返回值:
>0:所有集合中,满足对应事件的总数。
=0:没有满足监听事件的描述符。
-1:异常
相关函数:
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里 fd 清 0,将fd文件描述符从监听集合中清除
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里 fd 是否置 1,即判断fd这个文件描述符是否在set集合中
//返回值:1,fd在集合中;0,fd不在集合中
void FD_SET(int fd, fd_set *set); //把文件描述符集合里 fd 位置 1,将监听的文件描述符fd,添加到监听集合中
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清 0
2、下面就是代码实践部分,里面注释已经非常详细了。
#include "stdio.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SER_PORT (6666)
/* 如下:fd 是递增的,如下:
*
* fd数值 fd含义
*
* ... ...
* 5 cfd2
* 4 cfd1
* 3 lfd
* 2 STANDERR
* 1 STANDOUT
* 0 STANDIN
*/
int main(void)
{
int lfd = -1, max_fd = 0,sel_nums = -1,cfd = -1, i = 0,rd_size;
struct sockaddr_in ser_ip, cli_ip;
fd_set rd_set,all_set; //rd_set是监听的文件描述符集合,all_set是所有的文件描述符集合
char cli_ip_addr[INET_ADDRSTRLEN];
char rd_buf[1024] = {0};
socklen_t cli_ip_len = sizeof(struct sockaddr_in);
lfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&ser_ip,0,sizeof(struct sockaddr_in));
ser_ip.sin_family = AF_INET;
ser_ip.sin_port = htons(SER_PORT);
ser_ip.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (const struct sockaddr *)&ser_ip,sizeof(struct sockaddr_in));
listen(lfd, 128);
max_fd = lfd;
FD_ZERO(&all_set); //清空文件描述符集合
rd_set = all_set; //将all_set赋值给rd_set
FD_SET(lfd,&all_set); //将连接的文件描述符lfd添加到监听集合中
//设置端口复用
int opt = 1;// 1:设置端口复用,0:设置端口不复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
while(1)
{
rd_set = all_set; //将all_set赋值给rd_set
sel_nums = select(max_fd + 1, &rd_set, NULL,NULL, NULL);
if(sel_nums < 0) //select 错误
{
printf("select error\n");
return;
}
if(sel_nums > 0)
{
if(FD_ISSET(lfd, &rd_set)) //如果FD_ISSET返回值>0,说明满足lfd
{ //的条件在rd_set中,即有新的客户端发生了连接
//server调用accept与client连接,这里不会阻塞了,因为select已经监听到了连接事件
cfd = accept(lfd, (struct sockaddr *)&cli_ip, &cli_ip_len);
if(cfd > 0)
{
memset(cli_ip_addr,0,sizeof(cli_ip_addr));
inet_ntop(AF_INET, &cli_ip.sin_addr.s_addr, cli_ip_addr, sizeof(cli_ip_addr));
printf("client connected:client ip:%s\n",cli_ip_addr);
//获取最大的fd,保存到max_fd中
max_fd = max_fd > cfd ? max_fd : cfd;
printf("max_fd = %d\n",max_fd);
//将cfd添加到监听集合中
FD_SET(cfd,&all_set);
}
//注意:这里很重要,即select的返回值是1,并且lfd在监听的集合当中,也就是只返回了lfd
//这意味着只有连接事件发生,并没有通信读写事件发生
if(1 == sel_nums)
{
continue;
}
}
//(1):因为文件描述符是递增的,所以第一个文件描述符是lfd,最大的文件描述符是max_fd
//(2):既然程序到了这里 ,说明发生了客户端的通信读写事件
//(3):TODO:代码优化,这里轮询其实效率是很低的,优化方法:单独定义一个
// 数组来存储监听到的fd,每次轮询这个数组的有效区即可
for(i = lfd +1;i < max_fd + 1;i++)
{
memset(rd_buf,0,sizeof(rd_buf));
if(FD_ISSET(i, &rd_set)) //找到满足读事件的那个fd
{
rd_size = read(i, rd_buf, sizeof(rd_buf));
if(rd_size == 0)
{
close(i);
FD_CLR(i, &all_set);//这里很重要,从监听的文件描述符中移除,不移除会出问题
continue;
}
else
{
printf("read client buf:%s\n",rd_buf);
}
if(sel_nums == 1)
break;
}
}
}
}
close(lfd);
return 0;
}
3、测试结果:
4、select的优缺点
缺点:监听上限受文件描述符限制,最大1024.
有点:跨平台。windows、linux、micOS、unix、mips都支持select。
二、poll实现多路IO转接服务器
1、poll函数简介
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作用: poll函数的作用也是实现多路IO转接服务器。
函数参数:
struct pollfd {
int fd; /* file descriptor */ 文件描述符
short events; /* requested events */ 传入的事件,待监听的文件描述符对应的事件,读POLLIN、写 POLLOUT、异常POLLERR等
short revents; /* returned events */传出的事件:读、写、异常等,传入时给0.满足对应事件会返回非0。
};
fds:监听的文件描述符的数组,传入传出参数。
nfds:监听数组的实际有效监听个数。
timeout:阻塞时长,时间ms。
=0:不阻塞;
>0: 阻塞时长
<0: 阻塞
返回:返回满足对应监听事件的文件描述符总个数。
2、代码实践,注释也很详细
#include "stdio.h"
#include
#include
#include
#include
#include
#include
#define SER_PORT (6666)
//定义一个server的 poll函数监听管理的结构体
typedef struct
{
struct pollfd monitor_set[1024]; //最大可以poll可以监听1024个客户端
int maxIndex; //记录monitor_set 数组使用的最大下标值
}tMonitorManager;
tMonitorManager g_ser_monitor_mgr ;
//初始化server 监听的结构体
void serMonitorManagerInit(void)
{
int i = 0;
for(i = 0;i < 1024; i++)
{
g_ser_monitor_mgr.monitor_set[i].fd = -1; //将fd都初始化为-1
g_ser_monitor_mgr.monitor_set[i].events = 0;
g_ser_monitor_mgr.monitor_set[i].revents = 0;
}
g_ser_monitor_mgr.maxIndex = 0;
return ;
}
int main(void)
{
int lfd = -1,cfd = -1,i = 0,rd_size;
int listen_nums = -1;
struct sockaddr_in ser_addr,cli_addr;
socklen_t cli_addr_len = sizeof(struct sockaddr_in);
char cli_ip[16];
char rd_buf[1024];
lfd = socket(AF_INET, SOCK_STREAM, 0);
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr));
listen(lfd,20);
//设置端口复用
int opt = 1;// 1:设置端口复用,0:设置端口不复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
serMonitorManagerInit();
//将监听数组的0号下标位置设置为监听连接的fd
g_ser_monitor_mgr.monitor_set[0].fd = lfd;
//将监听数组的0号下标位置的监听事件设置为读事件
g_ser_monitor_mgr.monitor_set[0].events = POLLIN;
while(1)
{
//调用poll阻塞监听事件
listen_nums = poll(g_ser_monitor_mgr.monitor_set, g_ser_monitor_mgr.maxIndex + 1, -1);
if(listen_nums < 0)
{
printf("poll error\n");
return -1;
}
if(listen_nums > 0)
{
//查看监听数组0号下标的revents是否发生了连接事件
if(g_ser_monitor_mgr.monitor_set[0].revents & POLLIN )
{
//发生了连接事件,server调用accept函数跟client连接
memset(&cli_addr,0,sizeof(cli_addr));
cfd = accept(lfd, (struct sockaddr *)&cli_addr, &cli_addr_len);
memset(cli_ip,0,sizeof(cli_ip));
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cli_ip, 16);
printf("client connected,client ip : %s\n",cli_ip);
//将client的fd也添加到监听数组中去,监听读事件
for(i = 1; i < 1024; i ++)
{
if(g_ser_monitor_mgr.monitor_set[i].fd < 0)
{
g_ser_monitor_mgr.monitor_set[i].fd = cfd;
g_ser_monitor_mgr.monitor_set[i].events = POLLIN;
break;
}
}
if(i >= 1024)
{
printf("monitor arrey full\n");
return -1;
}
//下标大的哪一个赋值给g_ser_monitor_mgr.maxIndex
g_ser_monitor_mgr.maxIndex = i > g_ser_monitor_mgr.maxIndex ? i : g_ser_monitor_mgr.maxIndex ;
printf("g_ser_monitor_mgr.maxIndex =%d\n",g_ser_monitor_mgr.maxIndex);
//poll的返回值是1,并且lfd在监听的集合当中,也就是只返回了lfd
//这意味着只有连接事件发生,并没有通信读写事件发生
if(1 == listen_nums )
{
continue;
}
}
//程序到了这里 ,说明发生了客户端的通信读写事件
//就要遍历监听的有效数组区看看哪一个发生了读事件
for(i = 1; i < g_ser_monitor_mgr.maxIndex + 1; i ++)
{
if(g_ser_monitor_mgr.monitor_set[i].fd < 0)
continue;
if(g_ser_monitor_mgr.monitor_set[i].revents & POLLIN)
{
memset(rd_buf,0,sizeof(rd_buf));
rd_size = read(g_ser_monitor_mgr.monitor_set[i].fd, rd_buf, 1024);
if(0 == rd_size)//对端关闭
{
close(g_ser_monitor_mgr.monitor_set[i].fd);
//从监听集合中移除
g_ser_monitor_mgr.monitor_set[i].fd = -1;
g_ser_monitor_mgr.monitor_set[i].events = 0;
g_ser_monitor_mgr.monitor_set[i].revents = 0;
//g_ser_monitor_mgr.maxIndex --;
//g_ser_monitor_mgr.maxIndex = g_ser_monitor_mgr.maxIndex <= 0 ? 0: g_ser_monitor_mgr.maxIndex ;
continue;
}
if(rd_size > 0)
{
printf("receive from client:%s\n",rd_buf);
}
if(listen_nums == 1)
break;
}
}
}
}
close(lfd);
return 0;
}
3、测试结果
4、poll函数优缺点
优点: 自带数组结构,可以将监听事件集合和返回事件集合分离。可以拓展监听上限,可以超出1024个,select 最多1024。
缺点:不能跨平台,只能在linux中使用。无法直接定位满足监听事件的文件描述符,需要轮询数组。
三、epoll实现多路IO转接服务器
1、epoll相关函数简介
(1) int epoll_create(int size);
作用:创建一个epoll句柄,实际上创建了一个监听红黑二叉树。
参数:size:创建的红黑树的监听节点数量,仅供内核参考,即告诉内核要监听的文件描述符的个数。
返回值:成功返回文件描述符,即指向新创建的红黑二叉树的fd。
失败: -1
(2) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数作用:操作监听红黑树, 控制某个 epoll 监控的文件描述符上的事件:添加、修改、删除。
参数:
epfd: epoll_create()返回值;
op: 对该监听红黑树所做的操作,
EPOLL_CTL_ADD,添加fd到监听红黑树;
EPOLL_CTL_MOD,修改fd在监听红黑树上的事件;修改已经监听的fd的监听事件。
EPOLL_CTL_DEL,将一个fd从监听红黑树上删除,即取消监听。
fd:监听的fd;
event:
typedef union epoll_data {
void *ptr; //泛型指针
int fd; //对应监听事件的fd
uint32_t u32; //不用
uint64_t u64; //不用
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */ // 读事件EPOLLIN;写事件EPOLLOUT;异常事件EPOLLERR;
epoll_data_t data; /* User data variable */ //
};
返回:成功 0;失败 -1;
(3)epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
作用:阻塞监听,等待所监控文件描述符上有事件的产生,类似于 select()调用;
参数:
epfd:epoll_create()返回值;
events:用来存内核得到事件的集合,可以看做是个数组,这是一个传出参数,传出满足监听条件的那些fd结构体。
maxevents:表示数组events元素的总个数。
timeout:阻塞时长,时间ms。
=0:不阻塞;
>0: 阻塞时长
<0: 阻塞
返回值:>0:满足监听的总个数,可以用作循环上限。
=0:没有fd满足监听事件
=-1:异常。
2、代码实践,同样,注释部分已很详细
#include "stdio.h"
#include
#include
#include
#include
#include
#include
#include
#define SER_PORT (6666)
//server的监听连接的结构体类型
typedef struct
{
struct epoll_event levent;
}tListen;
//定义一个epoll 管理的结构体
typedef struct
{
struct epoll_event epoll_set[1024]; //epoll 监听的文件描述符的集合
int maxIndex; //这个变量此工程中未用到
tListen lsn;
}tEpollMgr;
tEpollMgr g_tEpollMgr;
void ePollMgrInit(void)
{
int i = 0;
for(i = 0;i < 1024;i ++)
{
g_tEpollMgr.epoll_set[i].data.fd = -1; //将监听的fd都设为-1
}
g_tEpollMgr.maxIndex = 0;
}
int main(void)
{
int lfd = -1,epfd = -1,ret = -1,ep_wait_nums = -1,i = 0,cfd = -1,rd_size = -1;
struct sockaddr_in ser_ip, cli_ip;
socklen_t cli_ip_len = sizeof(cli_ip);
char cli_ip_addr[16] = {0};
struct epoll_event temp;
char rd_buf[1024] = {0};
lfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&ser_ip,0,sizeof(ser_ip));
ser_ip.sin_family = AF_INET;
ser_ip.sin_port = htons(SER_PORT);
ser_ip.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd, (const struct sockaddr *)&ser_ip,sizeof(ser_ip));
listen(lfd, 20);
//设置端口复用
int opt = 1;// 1:设置端口复用,0:设置端口不复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
//初始化epoll的管理结构体
ePollMgrInit();
//创建epoll红黑树,设置红黑树的监听节点最大为500
epfd = epoll_create(500);
if(epfd < 0)
{
printf("epoll_create error\n");
return -1;
}
//给监听连接的fd赋值
g_tEpollMgr.lsn.levent.data.fd = lfd;
//设置监听事件为读事件
g_tEpollMgr.lsn.levent.events = EPOLLIN;
//添加监听事件到监听树中
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &g_tEpollMgr.lsn.levent);
if(ret < 0)
{
printf("epoll_ctl error\n");
return -1;
}
while(1)
{
//阻塞监听epoll事件
ep_wait_nums = epoll_wait(epfd, g_tEpollMgr.epoll_set,1024, -1);
if(ep_wait_nums < 0)
{
printf("epoll_wait err\n");
return ;
}
if(ep_wait_nums > 0)//有监听的事件发生,并且客户端的个数是ep_wait_nums
{
for(i = 0;i < ep_wait_nums;i ++ )
{
//如果数组相应的位置没有读事件,继续下一个循环
if(!(g_tEpollMgr.epoll_set[i].events & EPOLLIN))
{
continue;
}
//如果客户端发生了连接事件
if(g_tEpollMgr.epoll_set[i].data.fd == lfd)
{
//server 调用 accept函数与client连接
memset(&cli_ip,0,sizeof(cli_ip));
cfd = accept(lfd, (struct sockaddr *)&cli_ip, &cli_ip_len);
if(cfd > 0)
{
memset(cli_ip_addr,0,16);
inet_ntop(AF_INET, &cli_ip.sin_addr.s_addr, cli_ip_addr, 16);
printf("client connected,client ip addr:%s\n",cli_ip_addr);
temp.events = EPOLLIN;
temp.data.fd = cfd;
//并将连接上来的client添加到监听树中
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
}
}
else //发生了读事件
{
memset(rd_buf,0,1024);
rd_size = read(g_tEpollMgr.epoll_set[i].data.fd, rd_buf, 1024);
if(rd_size == 0)//对端已关闭
{
//将其从监听树中删除
//删除的话 epoll_ctl的第三个参数传NULL就可以了
epoll_ctl(epfd, EPOLL_CTL_DEL, g_tEpollMgr.epoll_set[i].data.fd, NULL);
close(g_tEpollMgr.epoll_set[i].data.fd);
}
if(rd_size > 0)
{
printf("receive from client:%s\n",rd_buf);
}
}
}
}
}
close(lfd);
return 0;
}
3、测试结果
4、 epoll优缺点总结:
优点:高效
缺点: 不能跨平台,只支持linux。