• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    基于epoll实现 Reactor服务器的详细过程

    发布者: 嘉6148 | 发布时间: 2025-8-13 08:57| 查看数: 36| 评论数: 0|帖子模式

    了解epoll底层逻辑

    在我们调用epoll_create的时候会创建出epoll模型,这个模型也是利用文件描述类似文件系统的方式控制该结构。

    在我们调用epoll_create的时候,就会在内核管理中创建一个epoll模型,并且建管理模块地址给file结构体,file结构体也是连接在管理所有file结构体的数据结构中

    所以epoll也会给进程管理的files返回一个file地址保存在file_array中,并且将该地址在array中的下标值返回给上层。

    这样以同一的方式管理epoll模型。所以这就是epoll模型的好处,这和select和poll的方式不同,这两种并不使用文件描述符
    select还需要自己维护一个关心事件的fd的数组,然后再select结束以后,遍历该数组中的fd和输入输出型参数fd_set做查询关系(FD_ISSET),这其实是非常不方便的,在发生事件我们都需要遍历全部关心的事件,查看事件是否发生。并且因为是输入输出型(fd_set)参数,在响应后,之前设置的监视事件失效,所以每次监视事件前,都需要重新输入所有需要监听的事件这是非常不方便的事情
    poll在select上做了升级,不再需要额外的数组保存而是使用pollfd结构体保存fd和关心事件,但是在响应后我们依旧需要遍历所有关心的事件,假设1w个被监控的事件只有1个得到了响应,我们却需要遍历1w个事件一个一个检查是否响应,这是低效的算法。
    并且在操作系统中poll和epoll搭建的服务器关心的事件会被一直遍历查询是否被响应,哪怕1w个关心事件只有一个响应是第一个,剩下的9999个事件我们也得查看其是否被响应。
    我们不应该在响应得到后遍历所有的事件,操作系统也应该轮询的检查所有监控事件被响应,这是低效的2个做法,这就是epoll出现的意义,他的出现解决了这些繁杂的问题,并且在接口使用上做了极大的优化。他利用红黑树来管理需要监视程序员需要关心的事件和利用准备队列构建另一个结构,该结构保存了本次等待得到的所有有响应的事件。

    epoll模型介绍


    创建epoll模型:调用epoll_create,在文件描述符表添加一个描述符,生成对应的文件结构体结构体保存对应生成eventpoll结构体的地址,该结构中有rbr(监视事件红黑树),rdllist(就绪事件队列)等等。        
    添加一个fd到epoll中:调用epoll_ctl,通过epollfd在进程文件描述符表中找到对应的file,然后在对应的文件结构体中的标识符将特定指针强转为eventpoll,访问rbr,增加新结点在树中,并且添加对应的回调函数到对应fd的文件结构体中。
    接收并读取报文:网卡设备得到数据,发送设备中断给cpu,cpu根据接收到的中断号,在中断向量表中查找设备驱动提供的接口回调,将数据从网卡中读取到OS层的file文件结构体中,然后经过部分协议解析到TCP解析后,根据端口找到对应的进程,在进程中依靠五元组和fd的映射关系找到对应的file结构体,然后将网卡file的数据拷贝到对应服务器链接的file下的缓冲区中,并且调用其传入的callback函数传入fd通知epoll模型,有数据来临。这个时候我们的epoll在自己的rb树中依靠fd找到对应结点,并且其是否是自己所关心的事件,找到并且是我们的事件,就会取出其rb中的fd和响应的事件做拼接(一个结点监视一个fd的多个事件,发生响应并不是发生全部响应,一般都是一个响应,这个时候就需要将响应的事件和fd做结合,而不是全部事件和fd做结合)构建ready结点反应给上层。
    诚然在我们放入事件和拿出响应事件的过程中并不是原子的查找,比如访问ready结点操作系统可能在构建,而我们在拿出,这里就会造成执行流混乱的局面,所以这里是需要进程锁的,保证执行流正常。

    庆幸的是,我们的设计者大佬们已将帮我们锁好了,我们用就好了。

    LT和ET的区别


    LT的工作模式:


    • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
    • 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.
    • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
    • 支持阻塞读写和非阻塞读写

    ET的工作模式:


    • 当epoll检测到socket上事件就绪时, 必须立刻处理.
    • 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了. 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会,所以需要一次性读取完毕.
    • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
    • 只支持非阻塞的读写

    二者对比


    • LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.
    • 相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到 每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
    • 另一方面, ET 的代码复杂程度更高了.
    ps:使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践" 上的要求,毕竟我们需要一次性读取全部数据,在最后一次不能读取的时候会阻塞在接口处。

    插件组合

    创建多个类:Epoll类、Sock类、Connection类、Log类

    Epoll类

    用来为我们保存并管理epoll模型。
    1. static const unsigned int epoll_event_size_default = 64;
    2. class Epoll
    3. {
    4. public:
    5.     Epoll(unsigned int epoll_event_size = epoll_event_size_default)
    6.         : _epoll_event_size(epoll_event_size)
    7.     {
    8.         _epoll_fd = epoll_create(254);
    9.         if (_epoll_fd == -1)
    10.         {
    11.             Log()(Fatal, "epoll_create fail:");
    12.             exit(-1);
    13.         }
    14.         _epoll_event = new epoll_event[_epoll_event_size];
    15.     }
    16.     struct epoll_event *bind_ready_ptr()
    17.     {
    18.         return _epoll_event;
    19.     }
    20.     int EpollCtl(int op, int fd, int event)
    21.     {
    22.         struct epoll_event ev;
    23.         ev.data.fd = fd;
    24.         ev.events = event;
    25.         int status = epoll_ctl(_epoll_fd, op, fd, &ev);
    26.         return status == 0;
    27.     }
    28.     int EpollWait(int timeout)
    29.     {
    30.         int n = epoll_wait(_epoll_fd, _epoll_event, _epoll_event_size, timeout);
    31.         return n;
    32.     }
    33.     int fds_numb()
    34.     {
    35.         return _epoll_event_size;
    36.     }
    37. private:
    38.     int _epoll_fd;
    39.     struct epoll_event *_epoll_event;
    40.     unsigned int _epoll_event_size;
    41. };
    复制代码
    该类管理着,epoll模型文件描述符,_epoll_event第一个就绪结点地址、最大可以接收的 _epoll_event_size.
    注意这里的_epoll_event,并不是实际在epoll模型中的自由结点,而是该自由结点将重要信息拷贝到我们传入的这个空间中。

    传入的event_size是告诉epoll模型我最多只能拷贝这么多个结点信息,还有就下次再说了,返回值是本次拷贝数量n。

    Sock类

    替我们来链接新链接的类
    1. class Sock
    2. {
    3. public:
    4.     Sock(int gblock = 5)
    5.         : _listen_socket(socket(AF_INET, SOCK_STREAM, 0)), _gblock(gblock)
    6.     {
    7.         int opt = 1;
    8.         setsockopt(_listen_socket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof opt);
    9.     }
    10.     int get_listen_sock()
    11.     {
    12.         return _listen_socket;
    13.     }
    14.     void Sock_bind(const std::string &ip = "0.0.0.0", uint16_t port = 8080)
    15.     {
    16.         sockaddr_in self;
    17.         bzero(&self, sizeof(self));
    18.         self.sin_family = AF_INET;
    19.         self.sin_addr.s_addr = inet_addr(ip.c_str());
    20.         self.sin_port = htons(port);
    21.         if (0 > bind(_listen_socket, (sockaddr *)&self, sizeof(self)))
    22.         {
    23.             log(Fatal, "bind 致命错误[%d]", __TIME__);
    24.             exit(1);
    25.         }
    26.     }
    27.     void Sock_connect(const char *ip, const char *port)
    28.     {
    29.         struct sigaction s;
    30.         sockaddr_in server;
    31.         bzero(&server, sizeof(server));
    32.         server.sin_family = AF_INET;
    33.         inet_aton(ip, &server.sin_addr);
    34.         server.sin_port = htons(atoi(port));
    35.         connect(_listen_socket, (sockaddr *)&server, sizeof(server));
    36.     }
    37.     void Sock_listen()
    38.     {
    39.         if (listen(_listen_socket, _gblock) > 0)
    40.         {
    41.             log(Fatal, "listen 致命错误[%d]", __TIME__);
    42.             exit(2);
    43.         }
    44.     }
    45.     int Sock_accept(std::string *ip, uint16_t *port)
    46.     {
    47.         sockaddr_in src;
    48.         bzero(&src, sizeof(src));
    49.         socklen_t srclen = sizeof(src);
    50.         int worksocket = accept(_listen_socket, (sockaddr *)&src, &srclen);
    51.         if (worksocket < 0)
    52.         {
    53.             log(Fatal, "link erron 链接失败");
    54.             return -1;
    55.         }
    56.         *ip = inet_ntoa(src.sin_addr);
    57.         *port = ntohs(src.sin_port);
    58.         return worksocket;
    59.     }
    60.     ~Sock()
    61.     {
    62.         if (_listen_socket >= 0)
    63.             close(_listen_socket);
    64.     }
    65. private:
    66.     int _listen_socket;
    67.     const int _gblock;
    68. };
    复制代码
    围绕着_listen_socket来操作的类

    Log类

    就是个日志没啥
    1. class Log
    2. {
    3. public:
    4.     Log()
    5.     {
    6.         std::cout<<"create log...\n"<<std::endl;
    7.         printMethod = Screen;
    8.         path = "./log/";
    9.     }
    10.     void Enable(int method)
    11.     {
    12.         printMethod = method;
    13.     }
    14.     std::string levelToString(int level)
    15.     {
    16.         switch (level)
    17.         {
    18.         case Info:
    19.             return "Info";
    20.         case Debug:
    21.             return "Debug";
    22.         case Warning:
    23.             return "Warning";
    24.         case Error:
    25.             return "Error";
    26.         case Fatal:
    27.             return "Fatal";
    28.         default:
    29.             return "None";
    30.         }
    31.     }
    32.     void printLog(int level, const std::string &logtxt)
    33.     {
    34.         switch (printMethod)
    35.         {
    36.         case Screen:
    37.             std::cout << logtxt << std::endl;
    38.             break;
    39.         case Onefile:
    40.             printOneFile(LogFile, logtxt);
    41.             break;
    42.         case Classfile:
    43.             printClassFile(level, logtxt);
    44.             break;
    45.         default:
    46.             break;
    47.         }
    48.     }
    49.     void printOneFile(const std::string &logname, const std::string &logtxt)
    50.     {
    51.         std::string _logname = path + logname;
    52.         std::cout<<_logname<<std::endl;
    53.         int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
    54.         if (fd < 0)
    55.         {
    56.             perror("fail:");
    57.             return;
    58.         }
    59.         write(fd, logtxt.c_str(), logtxt.size());
    60.         close(fd);
    61.     }
    62.     void printClassFile(int level, const std::string &logtxt)
    63.     {
    64.         std::string filename = LogFile;
    65.         filename += ".";
    66.         filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
    67.         printOneFile(filename, logtxt);
    68.     }
    69.     ~Log()
    70.     {
    71.     }
    72.     void operator()(int level, const char *format, ...)
    73.     {
    74.         time_t t = time(nullptr);
    75.         struct tm *ctime = localtime(&t);
    76.         char leftbuffer[SIZE];
    77.         snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    78.                  ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    79.                  ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
    80.         va_list s;
    81.         va_start(s, format);
    82.         char rightbuffer[SIZE];
    83.         vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    84.         va_end(s);
    85.         // 格式:默认部分+自定义部分
    86.         char logtxt[SIZE * 2];
    87.         snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
    88.         // printf("%s", logtxt); // 暂时打印
    89.         printLog(level, logtxt);
    90.     }
    91. private:
    92.     int printMethod;
    93.     std::string path;
    94. };
    复制代码
    Connection类
    1. using func_t = std::function<void(Connection *)>;
    2. class Connection
    3. {
    4. public:
    5.     Connection(int sock, void *tsvr = nullptr) : _fd(sock), _tsvr(tsvr)
    6.     {
    7.         time_t _lasttime = (time_t)time(0);
    8.     }
    9.     bool SetCallBack(func_t recv_cb, func_t send_cb, func_t except_cb)
    10.     {
    11.         _recv_cb = recv_cb;
    12.         _send_cb = send_cb;
    13.         _except_cb = except_cb;
    14.     }
    15.     int _fd;
    16.     int _events;
    17.     // 三个回调方法,表征的就是对_sock进行特定读写对应的方法
    18.     func_t _recv_cb;
    19.     func_t _send_cb;
    20.     func_t _except_cb;
    21.     // 接收缓冲区&&发送缓冲区
    22.     std::string _inbuffer; // 暂时没有办法处理二进制流,文本是可以的
    23.     std::string _outbuffer;
    24.     int _lasttime = 0;
    25.     std::string _client_ip;
    26.     uint16_t _client_port;
    27.     // 设置对epTcpServer的回值指针
    28.     void *_tsvr;
    29. };
    复制代码
    管理任何链接描述符(包括listen)的链接类,保存某个链接监视的读写异常事件,并且保存这些事件发生后对应的调用方法,并且每个事件设置读写应用层缓冲区,并且采用回值指针(在写入数据后采用该指针通知上层下次该链接修改采用监视事件条件。

    服务器代码
    1. #pragma once
    2. #include "Log.hpp"
    3. #include "sock.hpp"
    4. #include "Epoll.hpp"
    5. #include "Protocol.hpp"
    6. #include <unordered_map>
    7. #include <cassert>
    8. #include <vector>
    9. static const std::uint16_t server_port_defaut = 8080;
    10. static const std::string server_ip_defaut = "0.0.0.0";
    11. static const int READONE = 1024;
    12. #define CLIENTDATA conn->_client_ip.c_str(),conn->_client_port
    13. using callback_t = std::function<void(Connection *, std::string &)>;
    14. class epTcpServer
    15. {
    16.     static const std::uint16_t default_port = 8080;
    17.     static const std::uint16_t default_revs_num = 128;
    18. public:
    19.     epTcpServer(int port = default_port, int revs_num = default_revs_num)
    20.         : _port(port), _epoll(default_revs_num), _revs_num(revs_num)
    21.     {
    22.         _sock.Sock_bind();
    23.         _sock.Sock_listen();
    24.         _listen = _sock.get_listen_sock();
    25.         AddConnection(_listen, std::bind(&epTcpServer::Accept, this, std::placeholders::_1), nullptr, nullptr);
    26.         _revs = _epoll.bind_ready_ptr();
    27.         cout << "debug 1" << endl;
    28.     }
    29.     void Dispather(callback_t cb)
    30.     {
    31.         _cb = cb;
    32.         while (true)
    33.         {
    34.             LoopOnce();
    35.         }
    36.     }
    37.     void EnableReadWrite(Connection *conn, bool readable, bool writeable)
    38.     {
    39.         uint32_t events = ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
    40.         bool res = _epoll.EpollCtl(EPOLL_CTL_MOD, conn->_fd, events);
    41.         assert(res); // 更改成if
    42.     }
    43. private:
    44.     void LoopOnce()
    45.     {
    46.         int n = _epoll.EpollWait(-1);
    47.         log(Info,"The number of links in this response :%d",n);
    48.         for (int i = 0; i < n; i++)
    49.         {
    50.             int sock = _revs[i].data.fd;
    51.             uint32_t revents = _revs[i].events;
    52.             log(Info, "Accessible fd:%d", sock);
    53.             bool status = IsConnectionExists(sock);
    54.             if (!status)
    55.             {
    56.                 log(Error, "There is no such data in the hash sock:%d", sock);
    57.                 continue;
    58.             }
    59.             if (revents & EPOLLIN)
    60.             {
    61.                 if (_Connection_hash[sock]->_recv_cb != nullptr)
    62.                 {
    63.                     _Connection_hash[sock]->_recv_cb(_Connection_hash[sock]);
    64.                 }
    65.             }
    66.             status = IsConnectionExists(sock);
    67.             if (revents & EPOLLOUT)
    68.             {
    69.                 if (!status)
    70.                 {
    71.                     log(Warning, "in read closs sock:%d", sock);
    72.                     continue;
    73.                 }
    74.                 if (_Connection_hash[sock]->_send_cb != nullptr)
    75.                     _Connection_hash[sock]->_send_cb(_Connection_hash[sock]);
    76.             }
    77.         }
    78.     }
    79.     bool IsConnectionExists(int sock)
    80.     {
    81.         auto iter = _Connection_hash.find(sock);
    82.         if (iter == _Connection_hash.end())
    83.             return false;
    84.         else
    85.             return true;
    86.     }
    87.     void Accept(Connection *conn)
    88.     {
    89.         while (1)
    90.         {
    91.             std::string ip;
    92.             uint16_t port;
    93.             int work = _sock.Sock_accept(&ip, &port);
    94.             if (work < 0)
    95.             {
    96.                 if (errno == EAGAIN || errno == EWOULDBLOCK)
    97.                     break;
    98.                 else if (errno == EINTR) // 信号中断
    99.                     continue;            // 概率非常低
    100.                 else
    101.                 {
    102.                     // accept失败
    103.                     log(Warning, "accept error, %d : %s", errno, strerror(errno));
    104.                     break;
    105.                 }
    106.             }
    107.             Connection *ret = AddConnection(work, std::bind(&epTcpServer::Read, this, std::placeholders::_1),
    108.                                             std::bind(&epTcpServer::Write, this, std::placeholders::_1),
    109.                                             std::bind(&epTcpServer::Except, this, std::placeholders::_1));
    110.             ret->_client_ip = ip;
    111.             ret->_client_port = port;
    112.             log(Info, "accept success && TcpServer success clinet[%s|%d]", ret->_client_ip.c_str(), ret->_client_port);
    113.         }
    114.     }
    115.     void Read(Connection *conn)
    116.     {
    117.         int cnt = 0;
    118.         while (1)
    119.         {
    120.             char buffer[READONE] = {0};
    121.             int n = recv(conn->_fd, buffer, sizeof(buffer) - 1, 0);
    122.             if (n < 0)
    123.             {
    124.                 if (errno == EAGAIN || errno == EWOULDBLOCK)
    125.                     break; // 正常的
    126.                 else if (errno == EINTR)
    127.                     continue;
    128.                 else
    129.                 {
    130.                     log(Error, "recv error, %d : %s", errno, strerror(errno));
    131.                     conn->_except_cb(conn);
    132.                     return;
    133.                 }
    134.             }
    135.             else if (n == 0)
    136.             {
    137.                 log(Debug, "client[%s|%d] quit, server close [%d]", CLIENTDATA, conn->_fd);
    138.                 conn->_except_cb(conn);
    139.                 return;
    140.             }
    141.             else
    142.             {
    143.                 buffer[n] = 0;
    144.                 conn->_inbuffer += buffer;
    145.             }
    146.         }
    147.         log(Info,"The data obtained from the client[%s|%d] is:%s",CLIENTDATA,conn->_inbuffer.c_str());
    148.         std::vector<std::string> messages;
    149.         SpliteMessage(conn->_inbuffer, &messages);
    150.         for (auto &msg : messages)
    151.             _cb(conn, msg);
    152.     }
    153.     void Write(Connection *conn)
    154.     {
    155.         printf("write back to client[%s|%d]:%s", conn->_client_ip.c_str(), conn->_client_port, conn->_outbuffer.c_str());
    156.         while (true)
    157.         {
    158.             ssize_t n = send(conn->_fd, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
    159.             if (n > 0)
    160.             {
    161.                 conn->_outbuffer.erase(0, n);
    162.                 if (conn->_outbuffer.empty())
    163.                     break;
    164.             }
    165.             else
    166.             {
    167.                 if (errno == EAGAIN || errno == EWOULDBLOCK)
    168.                     break;
    169.                 else if (errno == EINTR)
    170.                     continue;
    171.                 else
    172.                 {
    173.                     log(Error, "send error, %d : %s", errno, strerror(errno));
    174.                     conn->_except_cb(conn);
    175.                     break;
    176.                 }
    177.             }
    178.         }
    179.         if (conn->_outbuffer.empty())
    180.             EnableReadWrite(conn, true, false);
    181.         else
    182.             EnableReadWrite(conn, true, true);
    183.     }
    184.     void Except(Connection *conn)
    185.     {
    186.         if (!IsConnectionExists(conn->_fd))
    187.             return;
    188.         // 1. 从epoll中移除
    189.         bool res = _epoll.EpollCtl(EPOLL_CTL_DEL, conn->_fd, 0);
    190.         assert(res); // 要判断
    191.         // 2. 从我们的unorder_map中移除
    192.         _Connection_hash.erase(conn->_fd);
    193.         // 3. close(sock);
    194.         close(conn->_fd);
    195.         // 4. delete conn;
    196.         delete conn;
    197.         log(Debug, "Excepter 回收完毕,所有的异常情况");
    198.     }
    199.     Connection *AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb, int sendevent = 0)
    200.     {
    201.         SetNonBlock(sock);
    202.         Connection *conn = new Connection(sock, this);
    203.         conn->SetCallBack(recv_cb, send_cb, except_cb);
    204.         _epoll.EpollCtl(EPOLL_CTL_ADD, sock, EPOLLIN | EPOLLET | sendevent);
    205.         _Connection_hash[sock] = conn;
    206.         return conn;
    207.     }
    208.     bool SetNonBlock(int sock)
    209.     {
    210.         int fl = fcntl(sock, F_GETFL);
    211.         if (fl < 0)
    212.             return false;
    213.         fcntl(sock, F_SETFL, fl | O_NONBLOCK);
    214.         return true;
    215.     }
    216. private:
    217.     int _listen;
    218.     int _port;
    219.     int _revs_num;
    220.     zjy::Sock _sock;
    221.     zjy::Epoll _epoll;
    222.     std::unordered_map<int, Connection *> _Connection_hash;
    223.     callback_t _cb;
    224.     struct epoll_event *_revs;
    225. };
    复制代码
    到此这篇关于基于epoll实现 Reactor服务器的文章就介绍到这了,更多相关epoll Reactor服务器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:互联网
    免责声明:如果侵犯了您的权益,请联系站长(1277306191@qq.com),我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    浏览过的版块

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表