并发服务器
多路IO转接
单进程服务器会使客户端阻塞在输入上,可通过IO复用使其阻塞在某个函数上以达到多进程/多线程的效果。
一.select
1.相关函数
#include <sys/select.h>
//均为宏,fd_set类型为描述符集合
void FD_ZERO(fd_set *fdset) //清空fdset
void FD_SET(int fd, fd_set *fdset)) //在fdset中添加fd
void FD_ISSET(int fd, fd_set *fdset)) //判断对应位是否打开,就是判断对应事件是否发生
void FD_CLR(int fd, fd_set *fdset)) //从fdset中删去fd
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
}
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
调用select告知内核在某些情况发生时返回
nfds:最大描述符+1
readfds,wrirefds,exceptfds分别为读,写,异常事件描述符集合,且均为【传入传出参数】
timeout:等待时间
永远等下去(仅在有一个描述符发生对应事件时返回):NULL
等一段时间:指定timeout
不等待:指定timeout中成员为0
调用select后内核会对指定的描述符集合进行测试,如果发生对应事件就将该描述符对应位打开,在通过FD_ISSET()判断对应事件是否发生
3.基本流程
socket,bind,listen --> select -->FD_ISSET(lfd) --> accept -->select
FD_ISSET(lfd) --> accept
FD_ISSET(cfd) --> read,write
4.简单应用
#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
int main()
{
int lfd, cfd, maxfd, n, ret;
struct sockaddr_in saddr, caddr;
fd_set rset, allset;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; //BUFSIZ 1024, INET__ADDRSTRLEN 16
socklen_t clen;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
listen(lfd, 128);
FD_ZERO(&allset); //由于对应参数为传入传出所以需要设置总描述符集
FD_SET(lfd, &allset); //添加监听描述符
maxfd = lfd;
while(1)
{
rset = allset;
ret = select(maxfd+1, &rset, NULL, NULL, NULL);
if(FD_ISSET(lfd, &rset)) //检查是否有连接请求
{
clen = sizeof(caddr);
cfd = accept(lfd, (struct sockaddr *)&caddr, &clen);
printf("connection-- port:%d\tip:%s\n", ntohs(caddr.sin_port), inet_ntop(AF_INET, &caddr.sin_addr.s_addr, str, sizeof(str)));
FD_SET(cfd, &allset);
if(maxfd < cfd)
maxfd = cfd;
if(ret == 1) //只有连接请求就不需要往下继续
continue;
}
for(int i = lfd+1; i <= maxfd; i++) //检查除了连接请求外是否有相应事件发生
{
if(FD_ISSET(i, &rset))
{
n = read(cfd, buf, sizeof(buf));
if(n == 0) //read返回0即收到断开连接请求
{
close(i);
FD_CLR(i, &allset);
}
else
{
write(STDOUT_FILENO, buf, n);
for(int j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(i, buf, n);
}
}
}
}
close(lfd);
return 0;
}
5.select优缺点
优点:跨平台
缺点:可监听的描述符受1024限制,只能轮询查看满足事件的描述符
二.selectt
通过增加一个client数组来提高检测效率
#include <stdio.h>
#include <sys/types.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
int main()
{
int cfd, lfd, sfd, maxfd, maxi, ret, n, i;
struct sockaddr_in saddr, caddr;
socklen_t clen;
fd_set allset, rset;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
int client[FD_SETSIZE];
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
listen(lfd, 128);
FD_ZERO(&allset);
FD_SET(lfd, &allset);
maxfd = lfd;
maxi = -1;
for(i = 0; i < 1024; i++)
client[i] = -1;
while(1)
{
rset = allset;
ret = select(maxfd+1, &rset, NULL, NULL, NULL);
if(FD_ISSET(lfd, &rset))
{
clen = sizeof(caddr);
cfd = accept(lfd, (struct sockaddr *)&caddr, &clen);
printf("connection-- port:%d\tip:%s\n", ntohs(caddr.sin_port), inet_ntop(AF_INET, &caddr.sin_addr.s_addr, str, sizeof(str)));
FD_SET(cfd, &allset);
for(i = 0; i < FD_SETSIZE; i++)
{
if(client[i] == -1)
{
client[i] = cfd;
break;
}
}
if(cfd > maxfd)
maxfd = cfd;
if(maxi < i)
maxi = i;
if(ret == 1)
continue;
}
for(i = 0; i < maxi+1; i++)
{
sfd = client[i];
if(sfd < 0)
continue;
if(FD_ISSET(sfd, &rset))
{
n = read(sfd, buf, sizeof(buf));
if(n == 0)
{
close(sfd);
FD_CLR(sfd, &allset);
client[i] = -1;
}
else
{
for(int j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(STDOUT_FILENO, buf, n);
write(sfd, buf, n);
}
}
}
}
close(lfd);
return 0;
}
三.poll
相较于select优化了传入传出参数在一起的复杂情况,提升不大,且和select流程类似
1.相关函数
#include <poll.h>
struct pollfd
{
int fd;
short events; //POLLIN/POLLOUT/POLLERR...
short revents;
}
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作用和select相同
fds:指向struct pollfd的数组
nfds:fds的大小
timeout:等待时间
不等待:0
等待指定毫秒数:>0
永远等待:INFTIM(负值)/-1
2.基本流程
socket,bind,listen --> poll --> client[0].revents & POLLIN --> accept --> poll
client[0].revents & POLLIN --> accept
client[i].revents & POLLIN --> read,write
3.简单应用
跟select操作流程类似
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <poll.h>
#define OPEN_MAX 1024
int main()
{
int cfd, lfd, sfd, ret, n, i, maxi;
struct sockaddr_in caddr, saddr;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
struct pollfd client[OPEN_MAX];
socklen_t clen;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
listen(lfd, 128);
for(i = 0; i < OPEN_MAX ; i++)
client[i].fd = -1;
client[0].fd = lfd;
client[0].events = POLLIN;
maxi = 0;
while(1)
{
ret = poll(client, maxi+1, -1);
if(client[0].revents & POLLIN)
{
clen = sizeof(caddr);
cfd = accept(lfd, (struct sockaddr *)&caddr, &clen);
printf("connection--port:%d\tip:%s\n", ntohs(caddr.sin_port), inet_ntop(AF_INET, &caddr.sin_addr.s_addr, str, sizeof(str)));
for(i = 1; i < OPEN_MAX; i++)
{
if(client[i].fd < 0)
{
client[i].fd = cfd;
client[i].events = POLLIN;
break;
}
}
if(maxi < i)
maxi = i;
if(ret == 1)
continue;
}
for(i = 1; i < maxi+1; i++)
{
sfd = client[i].fd;
if(client[i].revents & POLLIN)
{
n = read(sfd, buf, sizeof(buf));
if(n == 0)
{
close(sfd);
client[i].fd = -1;
}
else
{
for(int j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(STDOUT_FILENO, buf, n);
write(sfd, buf, n);
}
if(ret == 1)
break;
}
}
}
close(lfd);
return 0;
}
4.优缺点
优点:自带数组,监听集合和返回集合分离,可突破1024限制
缺点:不能跨平台,只能轮询查看满足事件的描述符
四.突破1024限制
…
五.epoll
相较于select和poll性能上提升较大,通过红黑树和双向链表使得在高并发情况下依然保有效率,且可以突破最大文件描述符1024的限制,理论上可达到无限但受到硬件限制。
1.相关函数
#include <sys/epoll.h>
struct epoll_event {
__uint32_t events; // EPOLLIN/EPOLLOUT...
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
int epoll_create(int size);
创建一个红黑树
size:红黑树大小
return 红黑树句柄epfd:success
-1:error
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
对epfd所对应的红黑树进行增加、删除等操作
epfd:红黑树句柄
op:进行某些操作
EPOLL_CTL_ADD:往树上添加
EPOLL_CTL_MOD:改变某些设置
EPOLL_CTL_DEL:从树上删除
fd:要操作的描述符
event:fd所关联的结构体
return 0:success
-1:error
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
监听事件发生
epfd:红黑树句柄
events:【传出参数】有对应事件发生的fd所关联的结构体数组
maxevents:最大描述符
timeout:等待时间
-1:阻塞
0:立即返回
>0 :等待相应微秒
return 有对应事件发生的描述符总数: success
-1: error
2.基本流程
socket,bind,listen --> epoll_create() --> epoll_ctl()添加监听描述符 --> epoll_wait()监听
有连接请求 --> accept()
有数据发送 --> read() --> write()
3.简单应用
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/epoll.h>
#define OPEN_MAX 1024
int main()
{
int cfd, lfd, sfd, ret, n, i, maxi, efd, nready;
struct sockaddr_in caddr, saddr;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
socklen_t clen;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
listen(lfd, 128);
struct epoll_event tep, ep[OPEN_MAX];
efd = epoll_create(OPEN_MAX);
tep.events = EPOLLIN;
tep.data.fd = lfd;
ret = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
while(1)
{
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
for(i = 0; i < nready; i++)
{
if(!ep[i].events & EPOLLIN)
continue;
if(ep[i].data.fd == lfd)
{
clen = sizeof(caddr);
cfd = accept(lfd, (struct sockaddr *)&caddr, &clen);
printf("connection--port:%d\tip:%s\n", ntohs(caddr.sin_port), inet_ntop(AF_INET, &caddr.sin_addr.s_addr, str, sizeof(str)));
tep.data.fd = cfd;
tep.events = EPOLLIN;
ret = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
}
else
{
sfd = ep[i].data.fd;
n = read(sfd, buf, sizeof(buf));
if(n == 0)
{
ret = epoll_ctl(efd, EPOLL_CTL_DEL, sfd, NULL);
close(sfd);
}
else
{
for(int j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(STDOUT_FILENO, buf, n);
write(sfd, buf, n);
}
}
}
}
close(lfd);
return 0;
}
4.epoll 中的ET/LT模式
LT(水平触发):默认情况下为LT,当缓冲区有数据时,即使一次没有读完,也会一直触发epoll_wait()读数据直到读完为止
ET(边沿触发):当缓冲区有数据且一次没有读完时,不会继续读,会等下一次新的数据来时读上一次剩余数据,
epoll的et高效但只支持非阻塞模式
struct epoll_event tep;
tep.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
int flg = fctnl(cfd, F_GETFL)
flg |= O_NONBLOCK
fctnl(cfd, F_SETFL, flg);
5.优缺点
优点:高效,突破1024限制
缺点:不能跨平台,只支持linux
六.epoll反应堆模型(libevent 网络库核心思想)
epoll + 回调函数 + 自定义结构 + ET非阻塞
…