概念前情
阻塞:为了完成一个功能,发起调用,若不具备完成功能的条件,则调用一直阻塞等待
非阻塞:为了完成一个功能,发起调用,若不具备完成功能的条件,则立即返回一个
阻塞与非阻塞的区别:常用于讨论函数是否阻塞,表示这个函数无法立即完成功能时是否立即返回
同步:任务完成的流程通常是串行化的,并且任务由进程自身完成
异步:任务完成的流程通常是不确定的,并且任务由系统完成
同步与异步的区别:通常用于讨论功能的完成方式,表示一个功能是否是顺序化且是否由自己来完成
异步的种类:异步阻塞----等待系统完成功能。异步非阻塞----不等待系统完成功能
同步好还是异步好?:同步处理流程简单,同一时间占用资源少;而异步处理效率高,但占用资源多
IO完成过程:1、等待IO就绪(满足IO的条件)2、进行数据拷贝
阻塞IO
阻塞IO就是调用者发起IO请求调用,由于请求调用的条件不满足,就会一直等待,直到条件满足
用户线程通过系统调用recvfrom发起IO读操作,由用户空间转到内核空间。内核等到数据报,待数据报到达后,然后将接收的数据拷贝到用户空间,完成recvfrom操作。
优缺点:阻塞IO模型的流程非常简单,代码操作也简单,任务的操作都是顺序的。但是该模型也有很明显的缺点:无法充分利用资源,任务处理的效率比较低
非阻塞IO
每次用户询问内核是否有数据报准备好(文件描述符缓冲区是否就绪),当数据报准备好的时候,就进行拷贝数据报的操作。当数据报没有准备好的时候,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一次轮询
优缺点:与阻塞IO相比,充分利用了IO等待时间,提高了任务处理的效率。但是流程相对于阻塞IO较为复杂,需要循环访问处理,且响应不够实时,只有等待事情办完后才能循环回去重新发起IO
信号驱动IO
通过信号定义IO就绪的处理方式,当收到信号(SIGIO)时表示IO已就绪,此时就可以在处理方式中发起IO调用
优缺点:相对非阻塞IO,处理IO更加实时,资源也得到充分利用。但是处理流程更加复杂,需要定义信号的处理方式,且流程既有主控流程也有信号处理流程,也涉及到信号是否可靠的问题
异步IO
IO处理顺序不确定,IO(等待+数据拷贝)都由操作系统来完成。自定义IO完成信号处理方式,发起异步调用,告诉操作系统要完成指定功能,剩下的IO功能完全由操作系统完成,完成后通过信号通知进程
异步IO使用的不再是read和write的系统接口了,应用工程序调用aio_XXXX系列的内核接口。当应用程序调用aio_read的时候,内核一方面去取数据报内容返回,另外一方面将程序控制权还给应用进程,应用进程继续处理其他事务。这样应用进程就是一种非阻塞的状态。当内核的数据报就绪的时候,是由内核将数据报拷贝到应用进程中,返回给aio_read中定义好的函数处理程序。
优缺点:充分利用资源,高效率地处理任务。消耗大部分资源,流程非常复杂
多路转接IO
多路转接IO也称IO复用,主要用于对大量的描述符进行IO就绪事件监控,能够让我们的进程只针对就绪了的描述符进行IO操作
IO的就绪事件分为三种
1、可读事件:一个描述符对应的缓冲区中有数据可读
2、可写事件:一个描述符对应的缓冲区中有剩余空间可以写入数据
3、异常事件:一个描述符发生了某些特定的异常信息
只对就绪的描述符进行IO操作的好处----避免阻塞,且提高处理效率
1、在默认的socket中,例如tcp一个服务端只能与一个客服端的socket通信一次,因为我们不知道哪个客户端新建的socket有数据到来或者监听socket有新连接,有可能就会对没有没有新连接到来的监听socket进行accept操作而阻塞,或者对没有数据到来的普通socket进行recv阻塞
2、在tcp服务端中,将所有的socket设置为非阻塞,若没有数据到来,则立即报错返回,进行下一个描述符的操作,这种操作中,有一个不好的地方,就是也对没有就绪事件的描述符进行操作,降低了处理效率
在实现多路转接IO中,操作系统提供了三种模型,分别为select模型、poll模型和epoll模型
适用场景:适用于有大量描述符需要监控,但是同一时间只有少量描述符活跃的场景
select模型
使用流程
一、定义想要监控的事件的描述符集合初始化集合,将需要监控指定事件的描述符添加到指定事件的描述符集合中
集合:是一个
fd_set
结构体,结构体中只有一个成员,是一个数组,这个数组主要是作为位图进行使用。向集合中添加一个描述符,描述符就是一个数字,添加描述符其实就是将这个数字对应的比特位置1,表示置为1的位置对应的描述符被添加到集合中了。这个数组中有多少个比特位或者说select最多能监控多少描述符,取决于宏__FD_SETSIZE
,默认为1024
代码操作分为三步
1、定义指定事件的集合 fd_set rfds
----可读事件集合
2、初始化集合 void FD_ZERO(fd_set *set)
----清空指定的描述符集合
3、将需要监控的事件的描述符添加到集合中void FD_SET(int fd, fd_set *set)
----将fd描述符添加到set集合中
二、发起调用,将集合中的数据拷贝到内核中进行监控,监控采用轮询遍历判断方式进行
接口:int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask)
参数内容(nfds
:集合中最大描述符的数值+1,目的是为了减少内核中遍历次数;readfds
:可读事件集合;writefds
:可写事件集合;exceotfds
:异常事件集合;timeout
:select默认是一个阻塞操作< struct timeval{tv_sec; tv_usec} >,若timeout=NULL表示永久阻塞,直到有描述符就绪才返回;若timeout中的数据为0,则表示非阻塞,没有就绪就立即返回;若timeout有数据,若指定时间内没有描述符就绪则超时返回;返回值
:返回值小于0表示监控出错,等于0表示没有描述符就绪,大于0表示就绪的描述符的个数)
select会在返回前将所有集合中没有就绪的描述符都给从集合中移除出去(调用返回后,集合中保存的都是就绪的描述符)
三、select调用返回后,进程遍历哪些描述符还在哪些集合中,就可以知道哪些描述符就绪了哪些事件,进而进行对应操作
四、其他操作:void FD_CLR(int fd, fd_set *set)
从set集合中移除fd描述符;int FD_ISSET(int fd, fd_set *set)
判断fd描述符是否在set集合中
简单实例:通过对标准输入的监控,体会监控的作用
#include <stdio.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/select.h>
int main()
{
char buf[1024] = {0};
while (1)
{
//1.定义集合,每次监控都需要重新添加描述符
fd_set set;
FD_ZERO(&set);//初始化清空集合
FD_SET(0, &set);//将标准输入描述符添加到集合中
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
//2.发起调用
printf("start monitoring\n");
//select返回时会删除集合中没有就绪的描述符
int ret = select(1, &set, NULL, NULL, &tv);
if (ret < 0)
{
perror("select error");
return -1;
}
else if (ret == 0)
{
//没有描述符就绪的情况下返回的就是超时
printf("monitoring time out\n");
continue;
}
printf("descriptor ready or timeout waiting\n");
if (FD_ISSET(0, &set))//当描述符还在集合中,则表示该描述符已就绪
{
printf("start reading data\n");
char buf[1024] = {0};
ret = read(0, buf, 1023);
if (ret < 0)
{
perror("read error");
return -1;
}
printf("buf:%s\n", buf);
}
}
return 0;
}
运行截图:
进阶实例:实现封装一个并发的服务器。封装一个Select类,每一个实例化的对象都是一个监控对象,向外提供简单接口,可以监控大量的描述符,并且直接能够在外部获取到就绪的描述符;不需要让用户知道select是如何进行的,不需要用户在外部进行复杂的操作
这里tcp服务器我们用之前写过的代码
网络 TCP协议(C++代码|通过tcp协议实现客户端与服务端之间的通信)
#include <cstdio>
#include <unistd.h>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
//该值表示用一时间能够接收多少客户端连接
//并非指整个通信最多接收多少客户端连接
#define MAX_LISTEN 5
#define CHECK_RET(q) if((q) == false){return -1;}
class TcpSocket
{
public:
TcpSocket()
:_sockfd(-1)
{}
int GetFd()
{
return _sockfd;
}
void SetFd(int fd)
{
_sockfd = fd;
}
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
bool Bind(const string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = MAX_LISTEN)
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
perror("listen error");
return false;
}
return true;
}
bool Accept(TcpSocket *new_sock, string *ip = NULL, uint16_t *port = NULL)
{
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if (new_fd < 0)
{
perror("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if (ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(string *buf)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0)
{
perror("recv error");
return false;
}
else if (ret == 0)//默认阻塞,没有数据就会等待,返回0表示连接断开
{
printf("connection broken\n");
return false;
}
buf->assign(tmp, ret);
return true;
}
bool Send(const string &data)
{
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if (ret < 0)
{
perror("send error");
return false;
}
return true;
}
bool Close()
{
if (_sockfd > 0)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
bool Connect(const string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("connect error");
return false;
}
return true;
}
private:
int _sockfd;
};
对select进一步得封装
#include <iostream>
#include <vector>
#include <sys/select.h>
#include "tcpsocket.hpp"
using namespace std;
#define MAX_TIMEOUT 3000
class Select
{
public:
Select()
:_maxfd(-1)
{
FD_ZERO(&_rfds);
}
bool Add(TcpSocket& sock)
{
int fd = sock.GetFd();
FD_SET(fd, &_rfds);
//每次添加新的描述符都需要判断最大描述符
_maxfd = _maxfd > fd ? _maxfd : fd;
return true;
}
bool Del(TcpSocket& sock)
{
int fd = sock.GetFd();
FD_CLR(fd, &_rfds);
for (int i = _maxfd; i >= 0; --i)
{
//移除之后,从后往前判断第一个还在集合中的描述符,找到就是最大的
if (FD_ISSET(i, &_rfds))
{
_maxfd = i;
break;
}
}
return true;
}
//进行监控,并且直接向外提供就绪的tcpSocket
bool Wait(vector<TcpSocket> *list, int outTime = MAX_TIMEOUT)
{
struct timeval tv;
tv.tv_sec = outTime / 1000;//outTime以毫秒为单位
tv.tv_usec = (outTime % 1000) * 1000;//计算剩余的微妙
fd_set set;
set = _rfds;//不能直接使用本类中的_rfds,因为select会修改集合中的描述符
int ret = select(_maxfd+1, &set, NULL, NULL, &tv);
if (ret < 0)
{
perror("select error");
return false;
}
else if (ret == 0)
{
printf("wait timeout\n");
return true;
}
for (int i = 0; i < _maxfd+1; ++i)
{
if (FD_ISSET(i, &set))
{
//还在集合中表示已就绪
TcpSocket sock;
sock.SetFd(i);
list->push_back(sock);
}
}
return true;
}
private:
//可读事件集合,保存要监控的可读事件描述符
fd_set _rfds;
//每次输入都需要输入最大描述符
int _maxfd;
};
客户端也是之前的代码
#include <iostream>
#include <string>
#include <signal.h>
#include "tcpsocket.hpp"
using namespace std;
void sigcb(int no)
{
printf("recv no: %d\n", no);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: ./tcp_cli ip port" << endl;
return -1;
}
signal(SIGPIPE, sigcb);
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(srv_ip, srv_port));
while (1)
{
string buf;
cout << "client say: ";
cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(&buf);
cout << "server say: "<< buf << endl;
}
sock.Close();
return 0;
}
这是我们新写的主程序:
#include <iostream>
#include "select.hpp"
using namespace std;
int main(int argc, char * argv[])
{
if (argc != 3)
{
cout << "Usage: ./main ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip, port));
CHECK_RET(lst_sock.Listen());
Select s;
s.Add(lst_sock);
while (1)
{
vector<TcpSocket> list;
bool ret = s.Wait(&list);
if (ret == false)
{
continue;
}
for (auto sock : list)
{
if (sock.GetFd() == lst_sock.GetFd())
{
//就绪的描述符与监听套接字描述符一样,就表示需要获取新连接
TcpSocket new_sock;
ret = lst_sock.Accept(&new_sock);
if (ret == false)
continue;
s.Add(new_sock);//将新建套接字也添加监控
}
else
{
//就绪的描述符不是监听的套接字,那么就是通信套接字,则进行recv
string buf;
ret = sock.Recv(&buf);
if (ret == false)
{
sock.Close();
s.Del(sock);//关闭套接字则需要移除监控
continue;
}
cout << "client say: " << buf << endl;
cout << "server say: ";
buf.clear();
cin >> buf;
ret = sock.Send(buf);
if (ret == false)
{
sock.Close();
s.Del(sock);
}
}
}
}
lst_sock.Close();
return 0;
}
先运行服务端。服务端在等待新的客户端的连接
运行客户端并对服务端
服务端对客户端进行回复
创建一个新的客户端再对服务端说
服务端回复
新的客户端收到
select总结
select的缺点:
- select所能监控的描述符的描述符有最大上限的,取决于宏__FD_SETSIZE,默认是1024
- select进行监控,是在内核中进行轮询遍历判断,性能会随着描述符的增多而下降
- select返回时移除集合中未就绪的描述符,每次监控都需要重新添加,并且重新拷贝到内核
- select只能返回就绪的描述符集合,无法直接返回就绪的描述符,需要用户进行遍历判断哪个描述符还在集合中才能确定是否就绪
select的优点:
- select遵循posix标准,跨平台移植性好
- select的超时等待时间设置可以精细到微秒
poll模型
接口:int poll(struct pollfd *fds, nfds_t nfds, int timeout)
参数内容(fds:要监控的描述符事件结构体;nfds:实际第一个参数描述符事件结构体数量;timeout:超时等待时间-毫秒为单位)返回值:返回值大于0表示就绪的描述符个数;返回等于0表示监控超时;返回值小于0表示监控出错
struct pollfd
{
int fd;//要监控的描述符
short events;//描述符想要监控的事件
short revents;//实际就绪的事件
};
events
中,主要有两个事件,一个是POLLIN,表示可读,一个是POLLOUT,表示可写。
revents
中,当poll调用接口返回的时候,这个描述符实际就绪的事件就会被写入revents中,程序员就是通过这个成员进行判断
流程
1)定义描述符事件结构体数组,将需要监控的描述符以及对应的事件信息填充到数组中
例如 struct pollfd fsd[10];fds[0].fd=0;fds[0].events=POLLIN。则表示的是对标准输入监控可读事件
2)发起监控调用poll,将数组中数据拷贝到内核中进行轮询遍历监控,有描述符就绪或者等待超时后返回,返回时将这个描述符实际就绪的事件填充到对应结构体中的revents中,如果没有描述符就绪,则revents中数据为0
3)监控调用返回后,程序员在程序中遍历数组中每个结点的revents,确定当前结点描述符就绪了哪些事件,从而进行对应的操作
实例:对标准输入进行监控可读事件
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main() {
//定义描述符事件结构体数组
//但是这里只监控一个描述符,因此只定义一个结构体
struct pollfd poll_fd;
//要监控的描述符
poll_fd.fd = 0;
//对描述符进行监控的事件--可读
poll_fd.events = POLLIN;
for (;;) {
int ret = poll(&poll_fd, 1, 3000);//开始监控
if (ret < 0) {//小于0表示出错
perror("poll");
continue;
}
if (ret == 0) {//等于0表示超时
printf("poll timeout\n");
continue;
}
//调用返回后,通过每个结点的revents确定哪个描述符就绪了哪个事件
//revents不需要手动还原,因为下次监控的时候没有就绪就会自动置为0
if (poll_fd.revents == POLLIN) {
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}else if (poll_fd.revents == POLLOUT){
//可写事件
}
}
}
运行结果:
poll总结
poll的优点:
- poll通过描述符事件结构体的方式简化了多种描述符集合的操作流程
- poll所能监控的描述符数量是没有限制的,想要监控多少描述符就定义多大的数组即可
- poll每次监控不需要重新定义事件结构体
poll的缺点:
- 监控原理是在内核中进行轮询判断,会随着描述符的增多而性能下降
- 无法跨平台移植
- 每次监控调用返回后需要程序员在程序中进行遍历判断才能知道哪个描述符就绪了哪个事件
epoll----linux下最好用的多路转接模型
接口流程介绍
1)在内核中创建eventpoll结构体返回一个描述符作为代码中的操作句柄。int epoll_create(int size)
参数内容(size:表示要监控的描述符最大数量;但是在linux2.6.8之后被忽略,只要大于0即可)
2)对需要监控的描述符组织事件结构体,将描述符以及对应事件结构体添加到内核中的eventpoll结构体中int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
参数内容(epfd:创建eventpoll结构体返回的操作句柄;op:有三个可选项,EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL,分别表示添加/修改/删除描述符;fd:要监控的描述符;event:监控描述符对应的事件结构体信息)
event结构体:
struct epoll_event
{
//表示要监控的事件,以及监控调用返回后实际就绪的事件
uint32_t events;//EPOLLIN表示可读、EPOLLOUT表示可写
union epoll_data
{
int fd;//描述符
void* ptr;
}
}data;
每一个需要监控的描述符都会有一个对应的事件结构,当描述符就绪了监控的事件后,就会将这个事件结构体返回给程序员,供给程序员对描述符进行操作
3)开始监控,当有描述符就绪或者等待超时后监控调用返回
int epoll_wait(int epdf, struct epoll_event * evs, int maxevents, int timeout)参数内容(epdf:epoll的操作句柄,通过这个句柄找到你和中指定的eventpoll结构体;evs:epoll_event描述符的事件结构体数组的首地址,用于获取就绪的描述符对应的事件结构体;maxevents:evs数组中的结点数量,主要为了防止就绪事件太多,向evs中放置的时候越界;timeout:超时等待时间-毫秒)返回值大于0表示就绪的描述符个数;返回值等于0表示等待超时;返回值小于0表示监控出错。
该接口是的整个操作是异步阻塞操作,只是发起监控请求,也就是告诉操作系统哪些描述符需要监控,然后描述符的就绪监控过程由系统来完成。然后操作系统为红黑树中每一个需要监控的描述符设置了事件回调,一旦描述符就绪了监控的事件,则自动通过回调函数将就绪事件的描述符对应的epoll_event事件结构体添加到rellist双向链表中。但是我们发起的epoll_wait调用并不是立即返回的,而是进程每隔一会就检查双向链表是否为空,确定是否存在就绪的描述符,若rellist为空则阻塞,若rellist不为空则表示有就绪的描述符,则将就绪描述符的结构信息添加到epoll_wait传入的evs数组中才返回。在epoll_wait接口返回值res大于0的情况下,在epoll_wait传入的evs数组中前res结点就是就绪的描述符对应的事件结构体信息,只需要对evs数组进行遍历,逐个判断events是什么事件,然后对fd描述符进行操作即可
实例:实现封装一个并发的服务器,封装一个Epoll类,每实例化的对象都是一个监控对象
封装Epoll类代码
#include <cstdio>
#include <vector>
#include <unistd.h>
#include <sys/epoll.h>
#include "tcpsocket.hpp"
using namespace std;
class Epoll
{
public:
Epoll()
{
_epfd = epoll_create(1);
if (_epfd < 0)
{
perror("epoll create error");
_exit(0);
}
}
bool Add(TcpSocket& sock)
{
int fd = sock.GetFd();
//定义一个描述符对应的事件结构体
struct epoll_event ev;
ev.data.fd = fd;//fd描述符就绪后,返回的这个结构中的data.fd就是我们要操作的描述符
ev.events = EPOLLIN;//监控可读事件
int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0)
{
perror("epoll_ctl_add error");
return false;
}
return true;
}
bool Del(TcpSocket& sock)
{
int fd = sock.GetFd();
int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
if (ret < 0)
{
perror("epoll_ctl_del error");
return false;
}
return true;
}
bool Wait(vector<TcpSocket>* list, int timeout = 3000)
{
struct epoll_event evs[10];
//返回就绪的描述符个数
int ret = epoll_wait(_epfd, evs, 10, timeout);
if (ret < 0)
{
perror("epoll_wait error");
return false;
}
else if (ret == 0)
{
printf("epoll timeout\n");
return false;
}
for (int i = 0; i < ret; ++i)
{
//判断是否就绪了可读事件
//不能使用等号,因为有可能就绪了多个事件
if (evs[i].events & EPOLLIN)
{
TcpSocket sock;
sock.SetFd(evs[i].data.fd);
list->push_back(sock);
}
}
return true;
}
private:
int _epfd;
};
对select中需要对主程序进行修改
1、更换模型对象
2、引入封装了Epoll的头文件
#include <iostream>
#include "epoll.hpp"
using namespace std;
int main(int argc, char * argv[])
{
if (argc != 3)
{
cout << "Usage: ./main ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip, port));
CHECK_RET(lst_sock.Listen());
Epoll s;
s.Add(lst_sock);
while (1)
{
vector<TcpSocket> list;
bool ret = s.Wait(&list);
if (ret == false)
{
continue;
}
for (auto sock : list)
{
if (sock.GetFd() == lst_sock.GetFd())
{
//就绪的描述符与监听套接字描述符一样,就表示需要获取新连接
TcpSocket new_sock;
ret = lst_sock.Accept(&new_sock);
if (ret == false)
continue;
s.Add(new_sock);//将新建套接字也添加监控
}
else
{
//就绪的描述符不是监听的套接字,那么就是通信套接字,则进行recv
string buf;
ret = sock.Recv(&buf);
if (ret == false)
{
sock.Close();
s.Del(sock);//关闭套接字则需要移除监控
continue;
}
cout << "client say: " << buf << endl;
cout << "server say: ";
buf.clear();
cin >> buf;
ret = sock.Send(buf);
if (ret == false)
{
sock.Close();
s.Del(sock);
}
}
}
}
lst_sock.Close();
return 0;
}
运行结果:
运行服务端
运行客户端并对服务端说
服务端进行回复
客户端收到
创建新的客户端并对服务端说
服务端知道并回复
epoll的事件触发模式:如何触发IO就绪事件
我们上面有说过IO事件就绪有三种事件,分别是可读事件、可写事件和异常事件,我们现在再来进一步的了解这些就绪事件
可读事件指的是接收缓冲区中的数据大小大于低水位标记
可写事件指的是发送缓冲区的剩余空间大小大于低水位标记
这里的低水位标记其实就是一个基准值,通常默认都是1个字节,可配置
水平触发:默认触发模式。对于可读事件来说,接收缓冲区中的数据大小大于低水位标记时就会触发事件;对于可写事件来说,发送缓冲区的剩余空间大小大于低水位标记就会触发事件。select和poll只有这种触发方式
边缘触发:边缘触发是epoll独有的。需要在epoll_event结构体中的events成员设置EPOLLET。对于可读事件来说,每次新数据到来的时候,才会触发一次可读事件,不会关注接收缓冲区中是否存在数据,每次新数据到来最好能够一次性将所有数据读出,否则epoll的边缘触发不会触发第二次事件,只有等下一次有新数据到来的时候才会触发;但是我们也不知道缓冲区有多少数据,因此只能循环读取才能读取完所有数据,但是recv在读取数据的时候若是读取到没有数据的术后就会发送阻塞,这时候我们就必须使用非阻塞IO来结解决。在recv中的flag参数设置为MSG_DONTWAIT。还有一种解决方案是将描述符的属性设置为非阻塞。接口为int fcntl(int fd, int cmd, ...arg)
参数内容(fd:要设置的描述符;cmd:设置和获取属性 F_SETFL 、F_GETFL; arg:属性设置。O_NONBLOCK则是非阻塞)
对于可写事件来说,只有当剩余空间从无到有的时候才会触发事件
水平触发和边缘触发的适用场景
大多数情况下都使用水平触发,而边缘触发适用于一种一直触发情况,但并非每一次都需要进行操作的情况
epoll的惊群问题
一个执行流若添加了特别多的描述符进行监控,则轮询处理会比较慢,因此会在多个执行流中创建epoll,每个epoll监控一部分描述符,分摊压力,但是这种操作无法确定哪个描述符活跃,有可能存在均衡问题。
解决办法1:每个执行流的epoll都监控所有描述符,谁抢到事件谁处理,但是一个描述符有事件到来若是惊起了多个epoll的时候该怎么办(惊群问题)----使用mutex互斥锁解决这个问题
多线程多进行的tcp服务器实现并发并行和多路转接模型实现并发的区别
多路转接模型进行服务器并发处理指的是在单执行流中进行轮询处理就绪的描述符,当描述符多时,就很难做到负载均衡,因为后面的描述符必须等待前面的描述符处理完了才能被处理。但是可以对该模型进行优化,规定每个描述符只能处理指定的数量的数据,读取完后就处理下一个描述符
适用场景:适用于存在大量的描述符需要监控,但是同一时间只存在少量活跃的场景
多线程/多进程进行服务器并发处理指的是操作系统通过轮询调度执行流,实现每个执行流中描述符的处理,是系统内核层面的负载均衡,不需要用户态做过多的操作。
基于两种方式的特点,实际开发中通常搭配一起使用:多路转接监控大量的描述符,哪个描述符就绪了事件,再去创建执行流进行处理,防止直接为描述符创建执行流而描述符没有就绪事件的到来导致的浪费资源。
epoll总结
epoll的优点:
- 监控的描述符没有数量的上限
- 所有的描述符事件信息只需要向内核拷贝一次
- 监控采用异步阻塞,性能不会随着描述符的增多而下降
- 直接向程序员返回就绪的描述符信息,可以让程序员在程序中直接对就绪的描述符进行操作
epoll的缺点:
- 无法跨平台移植
- 监控的超时时间最多精确到毫秒
udp能不能使用epoll?
可以,只要有描述符想要监控IO事件,就可以使用多路转接模型。但是少量的描述符的监控更适用于select,因为不需要在内核中做过多的操作,并且轮询遍历处理也比较简单,性能也饿不会下降很多。