ASIO是一个跨平台,主要用于实现异步网络和其他一些底层I/O操作的C++库
可以基于ASIO实现Echo服务端,在这之前,学习一些基础的知识和概念
1:IO多路复用
简单的来说,一个线程同时监听多个I/O事件就是I/O多路复用。任何一个I/O流操作不需要阻塞等待每个I/O流的完成,即非阻塞
2:并发、并行、串行
并发(Concurrency),并发偏重于多个任务交替执行,并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象
并行(Parallelism),并行指的是多个任务同时在多个处理单元上同时执行的能力,并行的"同时"是同一时刻可以多个进程在运行(处于running)
串行(Sequential),当任务按照固定的顺序依次执行,每个任务的开始都要等待上一个任务的完成,这就是串行执行
3:文件描述符(file descriptor,简称FD)
Linux 系统中,把一切都看做是文件(一切皆文件),当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。
FD也可以被称为文件句柄(file handle)、文件指针(file pointer)或文件引用(file reference)。简单来说,它是操作系统为了管理 I/O 操作而维护的一个表中的索引,代表着系统中打开的文件的一个“门牌号”。
4:服务器套接字(Socket)
是在网络编程中用于实现网络通信的一种抽象接口,它提供了一种统一的编程接口,使得应用程序可以在网络上进行数据传输和通信。套接字的实现通常涉及到操作系统内核、网络协议栈和网络硬件设备等多个层面
Socket的过程形容为打电话,流程就如下:
-
创建Socket:就像拿起一部电话,准备开始通话。
-
绑定地址和端口:类似于确定你要打电话的号码。
-
监听连接请求(对于服务器端):准备接听来自其他人的电话。
-
接受连接(对于服务器端):接听来自其他人的电话。
-
连接到远程主机(对于客户端):拨打某个号码开始通话。
-
发送和接收数据:你可以通过电话传递信息了。
-
关闭连接:挂断电话,结束通话。
4:select(80年代),epoll(多用),poll
①Select的实现:
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
// 创建 TCP 服务器套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0); //AF_INET: 是一个宏,表示使用 IPv4 地址族(Address Family),套接字将使用 IPv4 地址来标识主机和端口,也可以使用AF_INET6表示IPv6
//SOCK_STREAM: 是一个宏,表示创建一个基于流的套接字,用于 TCP 协议,如果是SOCK_DGRAM,就是使用UDP协议
//0: 是套接字的类型标志,通常为 0
if (server_socket == -1) { //-1表示没有成功创建socket
perror("socket failed");
exit(EXIT_FAILURE); //exit() 函数用于终止程序的执行,并返回一个整数参数作为程序的退出状态码。
//EXIT_FAILURE 是一个宏,表示程序执行失败的状态码,通常定义为非零值。
}
// 设置服务器地址和端口
struct sockaddr_in server_address; //用于存储服务器地址信息的数据结构
memset(&server_address, 0, sizeof(server_address)); //将 server_address 结构体中的所有字节都设置为零的操作,初始化结构体
server_address.sin_family = AF_INET; //这行代码设置了地址族为 AF_INET,表示使用 IPv4 地址族。AF_INET 是一个常量,代表 IPv4 地址族。
server_address.sin_addr.s_addr = htonl(INADDR_ANY); //这行代码设置了 IP 地址为 INADDR_ANY,表示服务器将接受来自任意网络接口的连接请求
server_address.sin_port = htons(8080); //这行代码设置了端口号为 8080,并将其从主机字节序转换为网络字节序
// 绑定地址和端口
if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) == -1) { //bind() 函数的第二个参数是一个指向 struct sockaddr 类型的指针
//&server_address 返回的是指向server_address 结构体变量的指针,也就是指向该变量在内存中的地址
//server_address 的地址转换为一个指向通用地址结构体的指针,以便能够传递给网络编程函数
perror("bind failed");
close(server_socket);
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(server_socket, 5) == -1) {
perror("listen failed");
close(server_socket);
exit(EXIT_FAILURE);
}
std::cout << "Server started, waiting for connections..." << std::endl;
// 创建要监视的文件描述符集合,并将服务器套接字加入集合中
std::vector<int> client_sockets;
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(server_socket, &read_fds);
int max_fd = server_socket;
while (true) {
// 使用 select 函数等待文件描述符就绪
fd_set tmp_fds = read_fds; //fd_set 是一个位图(bitmap),它将每个文件描述符映射到一个位(bit)。在 fd_set 中,每个位代表一个文件描述符
//当位被设置为 1 时,表示相应的文件描述符是待监听的;当位被设置为 0 时,表示相应的文件描述符不需要监听
if (select(max_fd + 1, &tmp_fds, NULL, NULL, NULL) == -1) { //max_fd 表示监视的文件描述符集合中的最大文件描述符
//fd_set 结构的指针,select() 函数将检查 tmp_fds 集合中的文件描述符,判断是否有文件描述符处于就绪状态
//第三个参数用于指定要监视的写和异常事件的文件描述符集合
//第四个参数用于指定 select() 函数的超时时间。在这里传入 NULL 表示 select() 函数将一直阻塞,直到有文件描述符就绪或者出错为止
//第五个参数用于指定 select() 函数的超时时间精度
perror("select failed");
close(server_socket);
exit(EXIT_FAILURE);
}
// 遍历就绪的文件描述符
for (int fd = 0; fd <= max_fd; ++fd) {
if (FD_ISSET(fd, &tmp_fds)) { //FD_ISSET() 是一个宏,用于检查指定的文件描述符是否在给定的 fd_set 集合中被设置
if (fd == server_socket) { 如果是服务器套接字,表示有新的连接请求
int client_socket = accept(server_socket, NULL, NULL); //调用 accept() 函数来接受客户端的连接请求,并创建一个新的套接字用于与客户端进行通信。
//第二个参数表示指向 struct sockaddr 类型的指针,用于获取客户端的地址信息。在这里传入 NULL 表示不获取客户端的地址信息。
//第三个参数指向 socklen_t 类型的指针,用于获取客户端地址结构体的大小。在这里传入 NULL 表示不获取客户端地址结构体的大小
if (client_socket == -1) {
perror("accept failed");
close(server_socket);
exit(EXIT_FAILURE);
}
std::cout << "New connection" << std::endl;
client_sockets.push_back(client_socket);
FD_SET(client_socket, &read_fds); //将 client_socket 添加到 read_fds 集合中,以便在调用 select() 函数时监视它的就绪状态。
max_fd = std::max(max_fd, client_socket);
} else {
// 如果是客户端套接字,表示有数据可读
char buffer[1024];
ssize_t bytes_received = recv(fd, buffer, sizeof(buffer), 0);
if (bytes_received <= 0) {
// 客户端关闭连接
std::cout << "Connection closed" << std::endl;
close(fd);
FD_CLR(fd, &read_fds);
client_sockets.erase(std::remove(client_sockets.begin(), client_sockets.end(), fd), client_sockets.end());
} else {
buffer[bytes_received] = '\0';
std::cout << "Received from client: " << buffer << std::endl;
}
}
}
}
}
return 0;
}
所以我们能看出select的缺点:
bitmap缺点,只能是1024
FDset不可重用,每次都需要新声明
用户态到内核太切换要开销
select()函数每次都要重新遍历文件描述符
②poll
struct pollfd{
int fd;
short events;
short revents;
}; //fd 表示文件描述符,events 表示要监视的事件,revents 表示实际发生的事件
for(i = 0; i<5,i++)
{
memset(client,0,sizeof(client)); //memset 是 C/C++ 标准库中的一个函数,用于将一块内存区域的内容设置为指定的值。
addrlen = sizeof(client);
pllfds[i] = accept(sockfd,(struct sockaddr*)&client,&addelen);
pooldfs[i].events = POLLIN; //设置 poolfds[i].events 为 POLLIN,表示要监视该文件描述符的可读事件。
}
sleep(1);
while(1){
puts("round again");
poll(poolfds,5,50000); //阻塞函数,等待文件描述符有数据
for(i = 0;i<5;i++) //遍历 poolfds 数组,检查每个文件描述符的事件。如果 revents 中包含 POLLIN 事件,表示该文件描述符有数据可读。
{
if(poolfds[i].revents & POOLIN){
poolfds[i].revents = 0; //清空 revents,就是置位
memset(buffer,0,MAXBUF);
read(poolfds[i].fd,buffer,MAXBUF); //读取数据
puts(buffer);
}
}
}
③epoll
struct epoll_event events[5];
int epfd = epoll_create(10); //参数 10 表示 epoll 实例的大小,但是在实际中这个参数并不会限制 epoll 实例的大小,内核会根据需要调整大小。
..
..
for(i = 0;i<5;i++)
{
static struct epoll_event ev;
memset(&client,0,sizeof(client));
addlen = sizeof(client);
ev.data.fd = accept(sockfd,(struct sockaddr*(&client, &addrlen); //向 epoll 实例中添加了 5 个文件描述符,这些文件描述符是通过 accept() 函数接受客户端连接而得到的
ev.events = EPOLLIN; //每个事件的类型都设置为 EPOLLIN,表示监听可读事件
epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev); //epoll_ctl() 函数将其添加到 epoll 实例中
}
while(true){
puts("round again");
nfds = epoll_wait(epfd,events,5,10000);
for(i == 0;i<nfds;i++){
memset(buffer,0,MAXBUF);
read(events[i].data.fd,buffer,MAXBUF);
puts(buffer);
}
}
epoll中最重要的函数就是epoll_wait()函数
他的原型是
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:是 epoll 实例的文件描述符,即通过 epoll_create() 创建的 epoll 实例。
events:是一个结构体数组,用于存储发生的事件信息
maxevents:是 events 数组的大小,即最多能够存储多少个事件。timeout:是超时时间,以毫秒为单位。如果设置为 -1,表示永远等待,直到有事件发生;如果设置为 0,表示立即返回,不阻塞;如果大于 0,表示等待指定时间后返回