Echo服务器学习__01(基础)

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的过程形容为打电话,流程就如下:

  1. 创建Socket:就像拿起一部电话,准备开始通话。

  2. 绑定地址和端口:类似于确定你要打电话的号码。

  3. 监听连接请求(对于服务器端):准备接听来自其他人的电话。

  4. 接受连接(对于服务器端):接听来自其他人的电话。

  5. 连接到远程主机(对于客户端):拨打某个号码开始通话。

  6. 发送和接收数据:你可以通过电话传递信息了。

  7. 关闭连接:挂断电话,结束通话。

**4:**select(80年代),epoll(多用),poll

①Select的实现:

cpp 复制代码
#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

cpp 复制代码
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

cpp 复制代码
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,表示等待指定时间后返回

相关推荐
今麦郎xdu_26 分钟前
【Linux系统】命令行参数和环境变量
linux·服务器·c语言·c++
还不秃顶的计科生29 分钟前
linux下conda未安装的解决方法(离线安装linux下的conda)
linux·运维·服务器
饮浊酒1 小时前
Python学习-----小游戏之人生重开模拟器(普通版)
python·学习·游戏程序
QT 小鲜肉1 小时前
【个人成长笔记】在Ubuntu中的Linux系统安装 anaconda 及其相关终端命令行
linux·笔记·深度学习·学习·ubuntu·学习方法
QT 小鲜肉1 小时前
【个人成长笔记】在Ubuntu中的Linux系统安装实验室WIFI驱动安装(Driver for Linux RTL8188GU)
linux·笔记·学习·ubuntu·学习方法
急急黄豆1 小时前
MADDPG学习笔记
笔记·学习
BullSmall1 小时前
《道德经》第十七章
学习
爱学习饼1 小时前
CentOS下安装配置JDK24和tomcat11
linux·运维·centos
Super Rookie1 小时前
Tomcat 自动化脚本安装方案
运维·tomcat·自动化
qinxue7222 小时前
Jenkins自动化配置--CICD流水线
运维·自动化·jenkins