单线程非io多路复用
server.cpp
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
addr.sin_addr.s_addr = 0;
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
if (ret == -1)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(0);
}
while (1)
{
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &clilen);
if (cfd == -1)
{
perror("accept");
exit(0);
}
char ip[24] = {0};
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip));
int port = ntohs(cliaddr.sin_port);
cout << ip << ":" << port << endl;
while(1){
char buf[1024];
memset(buf, 0, sizeof(buf));
int len=read(cfd,buf,sizeof(buf));
if(len>0){
cout<<"client say:"<<buf<<endl;
write(cfd,buf,len);
}else if(len==0){
cout<<"end connect"<<endl;
break;
}else{
perror("read");
break;
}
}
close(cfd);
}
close(lfd);
return 0;
}
client.cpp
#include <iostream>
#include <unistd.h>
#include<string.h>
#include<arpa/inet.h>
using namespace std;
int main(){
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1){
perror("socket");
exit(0);
}
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(10000);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr.s_addr);
int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){
perror("connect");
exit(0);
}
int number=0;
while(1){
char buf[1024];
sprintf(buf, "你好, 服务器...%d\n", number++);
write(fd,buf,strlen(buf)+1);
memset(buf,0,sizeof(buf));
int len=read(fd,buf,sizeof(buf));
if(len>0){
cout<<"server say"<<buf<<endl;
}else if(len==0){
cout<<"connect exit"<<endl;
}else
{
perror("read");
break;
}
sleep(1);
}
close(fd);
return 0;
}
单线程一次只能接待一个client
线程在accept/read/write可以阻塞,就无法处理下一个连接。
io多路复用
select:可以处理的最大连接数1024,可以跨平台
epoll:linux,红黑树,快
poll:可处理的最大连接数无上限(视资源多寡而定),不可跨平台。
同时检测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪(可以读或者写)组赛就被解除,并基于这(一个或者多个)就绪的文件描述符进行通信。
多线程并发处理:
- 服务器:
主线程:监测客户端连接请求,处理accept连接,无连接则阻塞,有则唤醒建立连接
子线程:和客户端通信
read()/recv接收数据,无数据则阻塞(处理其他连接中有数据的),有则唤醒处理
write()/send()给客户端发送数据,写满了则阻塞,否则等可以写了唤醒并将带发送数据写入写缓冲区中。 - io多路复用:
使用io多路转接函数委托内核检测服务器所有的文件描符号(通信和监听),这个监测会导致线程阻塞,如果检测到已就绪的文件描述符组赛接触,就将其传出
监听文件描述符:和客户端建立连接
此时调用accept()不会阻塞程序,因为文件描述符以及就绪(不用等待)
通信的文件描述符:调用读写。
io多路复用为什么有效:在可处理时唤醒调用资源去处理,而不是空闲等待其就绪,把资源一直利用起来。在可以执行的时候唤醒资源去处理。
线程方式,用其他线程去监测是否就绪,单线程只有一个不能用它去监测是否就绪,于是把监测交给内核检测就绪的文件描述符。
优势:系统开销小,不必创建进行/线程,因此也不用维护进程线程,因此开销小。
select
特点:跨平台(Linux,Mac,Windows)
#include <sys/select.h>
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval * timeout);
1.nfds:委托内核检测的这三个集合中最大的文件描述符+1
2.readfds:文件描述符的集合,内核之监测这个集合中文件描述符对应的读缓冲区
3.writefds:文件描述符的集合,监测其中对应的写缓冲区
4.exceptfds:监测该文件描述集合中是否有fd处于异常状态
5.timeout:超时时长,用来强制解除select函数的组赛的
NULL:函数检测不到就绪的文件描述符会一直阻塞。
等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回 0
不等待:函数不会阻塞,直接将该参数对应的结构体初始化为 0 即可。
函数返回值:0:成功
-1:调用失败
0:超时,没有检测到就绪的文件描述符
初始化fd_set
// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
sizeof(fd_set) = 128 字节 * 8 = 1024 bit // int [32]
0代表不检测该fd,1表示监测该fd的状态。
select的io多路复用实现并发流程:
1.创建监听的套接字socket,int lfd=socket();
2.lfd绑定本地ip和端口
3.listen
4.创建监听文件描述符集合fd_set,
- FD_zero 初始化未为0
- FD_Set 将坚挺的文件描述符放入集合
5.循环调用select找就绪的文件描述符
6.select()解除阻塞,得到内核传出的满足条件的fd集合
通过fd_isset判断是我们关注的监听fd,则用accept与client建立连接,(此时得到通信的cfd,放入检测集合)
如果是通信fd则通信,如果断开连接则把监听fd从检测集合删除,不断开则正常通信
7.重复6
select:
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
addr.sin_addr.s_addr = 0;
bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
listen(lfd, 128);
int maxfd = lfd;
fd_set rdset;
fd_set rdtemp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
while (1)
{
rdtemp = rdset;
int num = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);
if (FD_ISSET(lfd, &rdtemp))
{
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
FD_SET(cfd, &rdset);
maxfd = maxfd > cfd ? maxfd : cfd;
}
for (int i = 0; i <= maxfd; i++)
{
if (i != lfd && FD_ISSET(i, &rdtemp))
{
char buf[10] = {0};
int len = read(i, buf, sizeof(buf));
if (len == 0)
{
cout << "client close" << endl;
FD_CLR(i, &rdset);
close(i);
}
else if (len > 0)
{
cout << "client say:" << buf << endl;
write(i, buf, len);
}
else
{
perror("read");
}
}
}
}
return 0;
}
#include <iostream>
#include <unistd.h>
#include<string.h>
#include<arpa/inet.h>
using namespace std;
int main(){
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1){
perror("socket");
exit(0);
}
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(10000);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr.s_addr);
int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){
perror("connect");
exit(0);
}
int number=0;
while(1){
char buf[1024];
sprintf(buf, "你好,服务器...%d\n", number++);
write(fd,buf,strlen(buf)+1);
memset(buf,0,sizeof(buf));
int len=read(fd,buf,sizeof(buf));
if(len>0){
cout<<"server say"<<buf<<endl;
}else if(len==0){
cout<<"connect exit"<<endl;
}else
{
perror("read");
break;
}
sleep(1);
}
close(fd);
return 0;
}
epoll
是 linux 内核实现 IO 多路转接 / 复用(IO multiplexing)的一个实现。IO 多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。epoll 是 select 和 poll 的升级版,相较于这两个前辈,epoll 改进了工作方式,因此它更加高效。
- select,poll对于其基于线性方式处理,epoll基于红黑树来管理待检测集合
- select,poll每次线性扫描整个带检测集合,集合越大速度越慢,epoll使用回调机制,效率高,处理效率也不会随着检测集合的变大而下降
- select,poll工作工程存在内核和用户空间数据的频繁拷贝问题,而epoll中内存和用户去使用共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
- select,poll返回的集合进行判断才可以知道哪些文件描述符是就绪的,epoll可以直接得到已就绪的文件描述符集合,无需再次检测。
- epoll无最大文件描述符的限制,仅受系统中进程可以打开的最大文件数目的限制
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
select/poll低效的原因是将添加/维护待检测任务和阻塞进程/线程两个步骤合二为一。每次都需要这两部,大多数场景中需要检测的socket个数相对固定,不需要每次都修改。
epoll将其分开,先用epoll_ctl()维护等待队列,再用epoll_wait()阻塞进程。
int epoll_create(int size);
创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
epoll_ctl() 函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。
// 联合体, 多个变量共用同一块内存
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数:
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
op:这是一个枚举值,控制通过该函数执行什么操作
EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
events:委托 epoll 检测的事件
EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
EPOLLERR:异常事件
data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
函数返回值:
失败:返回 - 1
成功:返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
监测创建的epoll实例中有无就绪的文件描述符。
函数参数:
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
maxevents:修饰第二个参数,结构体数组的容量(元素个数)
timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
-1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
成功:
等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
大于 0:检测到的已就绪的文件描述符的总个数
失败:返回 - 1
#include <iostream>
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
using namespace std;
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
struct sockaddr_in addr;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
// port reuse
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 64);
if (ret == -1)
{
perror("listem error");
exit(0);
}
int epfd = epoll_create(64);
if (epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
struct epoll_event ev;
ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
exit(0);
}
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
for (int i = 0; i < num; i++)
{
int curfd = evs[i].data.fd;
if (curfd == lfd)
{
int cfd = accept(curfd, NULL, NULL);
ev.events = EPOLLIN;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
else
{
char buf[1024];
memset(buf,0,sizeof(buf));
int len=recv(curfd,buf,sizeof(buf),0);
if (len == 0)
{
printf("客户端已经断开了连接\n");
// 将这个文件描述符从epoll模型中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if (len > 0)
{
printf("客户端say: %s\n", buf);
send(curfd, buf, len, 0);
}
else
{
perror("recv");
exit(0);
}
}
} // for
}
return 0;
}
当在服务器端循环调用 epoll_wait() 的时候,就会得到一个就绪列表,并通过该函数的第二个参数传出:
epoll的工作模式
水平模式:LT(level triggered)
- 读事件:如果文件描述符对应的都缓冲区还有数据,读就会被出发
- 因为读数据是被动的,必须通过它知道有数据到达,所以对读的检测必须的。
- 写事件:缓冲区可写就会触发
- 因为写时主动的,所以对写事件的检测不是必须
边沿模式:ET(edge-triggered)
高速工作模式,文件已经就绪就不会再发更多通知,减少epoll事件被重复触发的次数。
读:数据没有被全部取走且有无数据进入都不再次触发。
写:可写时只触发一次。
使用边沿模式那么需要保证内存足够大,可以一次把数据从缓冲区读出。或者把套接字设置成非组赛模式。