multi-reactor服务器模型的C++封装类(libevent+多线程实现)

最近在看memcached的源码,觉得它那种libevent+多线程的服务器模型(multi-reactor)真的很不错,我将这个模型封装成一个C++类,根据我的简单测试,这个模型的效率真的很不错,欢迎大家试用。

这个类的使用方法很简单(缺点是不太灵活),只要派生一个类,根据需要重写以下这几个虚函数就行了:

  1. //新建连接成功后,会调用该函数  
  2. virtual void ConnectionEvent(Conn *conn) { }  
  3. //读取完数据后,会调用该函数  
  4. virtual void ReadEvent(Conn *conn) { }  
  5. //发送完成功后,会调用该函数(因为串包的问题,所以并不是每次发送完数据都会被调用)  
  6. virtual void WriteEvent(Conn *conn) { }  
  7. //断开连接(客户自动断开或异常断开)后,会调用该函数  
  8. virtual void CloseEvent(Conn *conn, short events) { }  


如果大家有什么建议或意见,欢迎给我发邮件:aa1080711@163.com


上代码:

头文件:MultiServer.h

  1. //MultiServer.h    
  2. #ifndef MULTISERVER_H_    
  3. #define MULTISERVER_H_    
  4.   
  5. #include <stdio.h>    
  6. #include <stdlib.h>    
  7. #include <unistd.h>    
  8. #include <string.h>    
  9. #include <errno.h>    
  10. #include <signal.h>    
  11. #include <time.h>    
  12. #include <pthread.h>    
  13. #include <fcntl.h>    
  14. #include <assert.h>  
  15.   
  16. #include <event.h>    
  17. #include <event2/bufferevent.h>    
  18. #include <event2/buffer.h>    
  19. #include <event2/listener.h>    
  20. #include <event2/util.h>    
  21. #include <event2/event.h>    
  22.   
  23. class MultiServer;    
  24. class Conn;    
  25. class ConnQueue;    
  26. struct LibeventThread;    
  27.   
  28. //这个类一个链表的结点类,结点里存储各个连接的信息,    
  29. //并提供了读写数据的接口    
  30. class Conn    
  31. {    
  32.     //此类只能由TcpBaseServer创建,    
  33.     //并由ConnQueue类管理    
  34.     friend class ConnQueue;    
  35.     friend class MultiServer;    
  36.   
  37. private:    
  38.     const int m_fd;             //socket的ID    
  39.     evbuffer *m_ReadBuf;        //读数据的缓冲区    
  40.     evbuffer *m_WriteBuf;       //写数据的缓冲区    
  41.   
  42.     Conn *m_Prev;               //前一个结点的指针    
  43.     Conn *m_Next;               //后一个结点的指针    
  44.     LibeventThread *m_Thread;    
  45.   
  46.     Conn(int fd=0);    
  47.     ~Conn();    
  48.   
  49. public:    
  50.     LibeventThread *GetThread() { return m_Thread; }    
  51.     int GetFd() { return m_fd; }    
  52.   
  53.     //获取可读数据的长度    
  54.     int GetReadBufferLen()    
  55.     { return evbuffer_get_length(m_ReadBuf); }    
  56.   
  57.     //从读缓冲区中取出len个字节的数据,存入buffer中,若不够,则读出所有数据    
  58.     //返回读出数据的字节数    
  59.     int GetReadBuffer(char *buffer, int len)    
  60.     { return evbuffer_remove(m_ReadBuf, buffer, len); }    
  61.   
  62.     //从读缓冲区中复制出len个字节的数据,存入buffer中,若不够,则复制出所有数据    
  63.     //返回复制出数据的字节数    
  64.     //执行该操作后,数据还会留在缓冲区中,buffer中的数据只是原数据的副本    
  65.     int CopyReadBuffer(char *buffer, int len)    
  66.     { return evbuffer_copyout(m_ReadBuf, buffer, len); }    
  67.   
  68.     //获取可写数据的长度    
  69.     int GetWriteBufferLen()    
  70.     { return evbuffer_get_length(m_WriteBuf); }    
  71.   
  72.     //将数据加入写缓冲区,准备发送    
  73.     int AddToWriteBuffer(char *buffer, int len)    
  74.     { return evbuffer_add(m_WriteBuf, buffer, len); }    
  75.   
  76.     //将读缓冲区中的数据移动到写缓冲区    
  77.     void MoveBufferData()    
  78.     { evbuffer_add_buffer(m_WriteBuf, m_ReadBuf); }    
  79.   
  80. };    
  81.   
  82. //带头尾结点的双链表类,每个结点存储一个连接的数据    
  83. class ConnQueue    
  84. {    
  85. private:    
  86.     Conn *m_head;    
  87.     Conn *m_tail;    
  88. public:    
  89.     ConnQueue();    
  90.     ~ConnQueue();    
  91.     Conn *InsertConn(int fd, LibeventThread *t);    
  92.     void DeleteConn(Conn *c);    
  93.     //void PrintQueue();    
  94. };    
  95.   
  96. //每个子线程的线程信息    
  97. struct LibeventThread    
  98. {    
  99.     pthread_t tid;              //线程的ID    
  100.     struct event_base *base;    //libevent的事件处理机    
  101.     struct event notifyEvent;   //监听管理的事件机    
  102.     int notifyReceiveFd;        //管理的接收端    
  103.     int notifySendFd;           //管道的发送端    
  104.     ConnQueue connectQueue;     //socket连接的链表    
  105.   
  106.     //在libevent的事件处理中要用到很多回调函数,不能使用类隐含的this指针    
  107.     //所以用这样方式将TcpBaseServer的类指针传过去    
  108.     MultiServer *tcpConnect;  //TcpBaseServer类的指针    
  109. };    
  110.   
  111. class MultiServer    
  112. {    
  113. private:  
  114.     static const int EXIT_CODE = -1;    
  115.     static const int MAX_SIGNAL = 256;  
  116.   
  117. private:    
  118.     int m_ThreadCount;                  //子线程数    
  119.     int m_Port;                         //监听的端口    
  120.     LibeventThread *m_MainBase;         //主线程的libevent事件处理机    
  121.     LibeventThread *m_Threads;          //存储各个子线程信息的数组    
  122.     event *m_SignalEvents[MAX_SIGNAL];  //自定义的信号处理    
  123.   
  124. private:    
  125.     //初始化子线程的数据    
  126.     void SetupThread(LibeventThread *thread);    
  127.   
  128.     //子线程的入门函数    
  129.     static void *WorkerLibevent(void *arg);    
  130.     //(主线程收到请求后),对应子线程的处理函数    
  131.     static void ThreadProcess(int fd, short which, void *arg);    
  132.     //被libevent回调的各个静态函数    
  133.     static void ListenerEventCb(evconnlistener *listener, evutil_socket_t fd,    
  134.         sockaddr *sa, int socklen, void *user_data);    
  135.     static void ReadEventCb(struct bufferevent *bev, void *data);    
  136.     static void WriteEventCb(struct bufferevent *bev, void *data);     
  137.     static void CloseEventCb(struct bufferevent *bev, short events, void *data);    
  138.   
  139. protected:    
  140.     //这五个虚函数,一般是要被子类继承,并在其中处理具体业务的    
  141.   
  142.     //新建连接成功后,会调用该函数    
  143.     virtual void ConnectionEvent(Conn *conn) { }    
  144.   
  145.     //读取完数据后,会调用该函数    
  146.     virtual void ReadEvent(Conn *conn) { }    
  147.   
  148.     //发送完成功后,会调用该函数(因为串包的问题,所以并不是每次发送完数据都会被调用)    
  149.     virtual void WriteEvent(Conn *conn) { }    
  150.   
  151.     //断开连接(客户自动断开或异常断开)后,会调用该函数    
  152.     virtual void CloseEvent(Conn *conn, short events) { }    
  153.   
  154. public:    
  155.     MultiServer(int count);    
  156.     ~MultiServer();    
  157.   
  158.     //设置监听的端口号,如果不需要监听,请将其设置为EXIT_CODE    
  159.     void SetPort(int port)    
  160.     { m_Port = port; }    
  161.   
  162.     //开始事件循环    
  163.     bool StartRun();    
  164.     //在tv时间里结束事件循环    
  165.     //否tv为空,则立即停止    
  166.     void StopRun(timeval *tv);    
  167.   
  168.     //添加和删除信号处理事件    
  169.     //sig是信号,ptr为要回调的函数    
  170.     bool AddSignalEvent(int sig, void (*ptr)(intshortvoid*));    
  171.     bool DeleteSignalEvent(int sig);    
  172.   
  173.     //添加和删除定时事件    
  174.     //ptr为要回调的函数,tv是间隔时间,once决定是否只执行一次    
  175.     event *AddTimerEvent(void(*ptr)(intshortvoid*),    
  176.         timeval tv, bool once);    
  177.     bool DeleteTImerEvent(event *ev);    
  178. };    
  179.   
  180. #endif    

 

实现文件:MulitServer.cpp

  1. //MultiServer.cpp    
  2. #include "MultiServer.h"    
  3.   
  4. Conn::Conn(int fd) : m_fd(fd)    
  5. {    
  6.     m_Prev = NULL;    
  7.     m_Next = NULL;    
  8. }    
  9.   
  10. Conn::~Conn()    
  11. {    
  12.   
  13. }    
  14.   
  15. ConnQueue::ConnQueue()    
  16. {    
  17.     //建立头尾结点,并调整其指针    
  18.     m_head = new Conn(0);    
  19.     m_tail = new Conn(0);    
  20.     m_head->m_Prev = m_tail->m_Next = NULL;    
  21.     m_head->m_Next = m_tail;    
  22.     m_tail->m_Prev = m_head;    
  23. }    
  24.   
  25. ConnQueue::~ConnQueue()    
  26. {    
  27.     Conn *tcur, *tnext;    
  28.     tcur = m_head;    
  29.     //循环删除链表中的各个结点    
  30.     while( tcur != NULL )    
  31.     {    
  32.         tnext = tcur->m_Next;    
  33.         delete tcur;    
  34.         tcur = tnext;    
  35.     }    
  36. }    
  37.   
  38. Conn *ConnQueue::InsertConn(int fd, LibeventThread *t)    
  39. {    
  40.     Conn *c = new Conn(fd);    
  41.     c->m_Thread = t;    
  42.     Conn *next = m_head->m_Next;    
  43.   
  44.     c->m_Prev = m_head;    
  45.     c->m_Next = m_head->m_Next;    
  46.     m_head->m_Next = c;    
  47.     next->m_Prev = c;    
  48.     return c;    
  49. }    
  50.   
  51. void ConnQueue::DeleteConn(Conn *c)    
  52. {    
  53.     c->m_Prev->m_Next = c->m_Next;    
  54.     c->m_Next->m_Prev = c->m_Prev;    
  55.     delete c;    
  56. }    
  57.   
  58. /*  
  59. void ConnQueue::PrintQueue()  
  60.  
  61. Conn *cur = m_head->m_Next;  
  62. while( cur->m_Next != NULL )  
  63.  
  64. printf("%d ", cur->m_fd);  
  65. cur = cur->m_Next;  
  66.  
  67. printf("\n");  
  68.  
  69. */    
  70.   
  71. MultiServer::MultiServer(int count)    
  72. {    
  73.     //初始化各项数据    
  74.     m_ThreadCount = count;    
  75.     m_Port = -1;    
  76.     m_MainBase = new LibeventThread;    
  77.     m_Threads = new LibeventThread[m_ThreadCount];    
  78.     m_MainBase->tid = pthread_self();    
  79.     m_MainBase->base = event_base_new();   
  80.     memset(m_SignalEvents, 0, sizeof(m_SignalEvents));  
  81.   
  82.     //初始化各个子线程的结构体    
  83.     for(int i=0; i<m_ThreadCount; i++)    
  84.     {    
  85.         SetupThread(&m_Threads[i]);    
  86.     }    
  87.   
  88. }    
  89.   
  90. MultiServer::~MultiServer()    
  91. {    
  92.     //停止事件循环(如果事件循环没开始,则没效果)    
  93.     StopRun(NULL);    
  94.   
  95.     //释放内存    
  96.     event_base_free(m_MainBase->base);    
  97.     for(int i=0; i<m_ThreadCount; i++)    
  98.         event_base_free(m_Threads[i].base);    
  99.   
  100.     delete m_MainBase;    
  101.     delete [] m_Threads;    
  102. }    
  103.   
  104. void MultiServer::SetupThread(LibeventThread *me)    
  105. {    
  106.     int res;   
  107.   
  108.     //建立libevent事件处理机制    
  109.     me->tcpConnect = this;    
  110.     me->base = event_base_new();    
  111.     assert( me->base != NULL );  
  112.   
  113.     //在主线程和子线程之间建立管道    
  114.     int fds[2];    
  115.     res = pipe(fds);    
  116.     assert( res == 0 );  
  117.     me->notifyReceiveFd = fds[0];    
  118.     me->notifySendFd = fds[1];    
  119.   
  120.     //让子线程的状态机监听管道    
  121.     event_set( &me->notifyEvent, me->notifyReceiveFd,    
  122.         EV_READ | EV_PERSIST, ThreadProcess, me );    
  123.     event_base_set(me->base, &me->notifyEvent);    
  124.     res = event_add(&me->notifyEvent, 0);  
  125.     assert( res == 0 );  
  126. }    
  127.   
  128. void *MultiServer::WorkerLibevent(void *arg)    
  129. {    
  130.     //开启libevent的事件循环,准备处理业务    
  131.     LibeventThread *me = (LibeventThread*)arg;    
  132.     //printf("thread %u started\n", (unsigned int)me->tid);    
  133.     event_base_dispatch(me->base);    
  134.     //printf("subthread done\n");    
  135. }    
  136.   
  137. bool MultiServer::StartRun()    
  138. {    
  139.     evconnlistener *listener;    
  140.   
  141.     //如果端口号不是EXIT_CODE,就监听该端口号    
  142.     if( m_Port != EXIT_CODE )    
  143.     {    
  144.         sockaddr_in sin;    
  145.         memset(&sin, 0, sizeof(sin));    
  146.         sin.sin_family = AF_INET;    
  147.         sin.sin_port = htons(m_Port);    
  148.         listener = evconnlistener_new_bind(m_MainBase->base,     
  149.             ListenerEventCb, (void*)this,    
  150.             LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1,    
  151.             (sockaddr*)&sin, sizeof(sockaddr_in));    
  152.         if( NULL == listener )  
  153.         {  
  154.             fprintf(stderr, "listen error: %s\n", strerror(errno));  
  155.             exit(1);  
  156.         }  
  157.     }    
  158.   
  159.     //开启各个子线程    
  160.     for(int i=0; i<m_ThreadCount; i++)    
  161.     {    
  162.         pthread_create(&m_Threads[i].tid, NULL,      
  163.             WorkerLibevent, (void*)&m_Threads[i]);    
  164.     }    
  165.   
  166.     //开启主线程的事件循环    
  167.     event_base_dispatch(m_MainBase->base);    
  168.   
  169.     //事件循环结果,释放监听者的内存    
  170.     if( m_Port != EXIT_CODE )    
  171.     {    
  172.         //printf("free listen\n");    
  173.         evconnlistener_free(listener);    
  174.     }    
  175. }    
  176.   
  177. void MultiServer::StopRun(timeval *tv)    
  178. {    
  179.     int contant = EXIT_CODE;    
  180.     //向各个子线程的管理中写入EXIT_CODE,通知它们退出    
  181.     for(int i=0; i<m_ThreadCount; i++)    
  182.     {    
  183.         write(m_Threads[i].notifySendFd, &contant, sizeof(int));    
  184.     }    
  185.     //结果主线程的事件循环    
  186.     event_base_loopexit(m_MainBase->base, tv);    
  187. }    
  188.   
  189. void MultiServer::ListenerEventCb(struct evconnlistener *listener,     
  190.                                      evutil_socket_t fd,    
  191. struct sockaddr *sa,     
  192.     int socklen,     
  193.     void *user_data)    
  194. {    
  195.     MultiServer *server = (MultiServer*)user_data;    
  196.   
  197.     //随机选择一个子线程,通过管道向其传递socket描述符    
  198.     int num = rand() % server->m_ThreadCount;    
  199.     int sendfd = server->m_Threads[num].notifySendFd;    
  200.     write(sendfd, &fd, sizeof(evutil_socket_t));    
  201. }    
  202.   
  203. void MultiServer::ThreadProcess(int fd, short which, void *arg)    
  204. {    
  205.     LibeventThread *me = (LibeventThread*)arg;    
  206.   
  207.     //从管道中读取数据(socket的描述符或操作码)    
  208.     int pipefd = me->notifyReceiveFd;    
  209.     evutil_socket_t confd;    
  210.     read(pipefd, &confd, sizeof(evutil_socket_t));    
  211.   
  212.     //如果操作码是EXIT_CODE,则终于事件循环    
  213.     if( EXIT_CODE == confd )    
  214.     {    
  215.         event_base_loopbreak(me->base);    
  216.         return;    
  217.     }    
  218.   
  219.     //新建连接    
  220.     struct bufferevent *bev;    
  221.     bev = bufferevent_socket_new(me->base, confd, BEV_OPT_CLOSE_ON_FREE);    
  222.     if (!bev)    
  223.     {    
  224.         fprintf(stderr, "Error constructing bufferevent!");    
  225.         event_base_loopbreak(me->base);    
  226.         return;    
  227.     }    
  228.   
  229.     //将该链接放入队列    
  230.     Conn *conn = me->connectQueue.InsertConn(confd, me);    
  231.   
  232.     //准备从socket中读写数据    
  233.     bufferevent_setcb(bev, ReadEventCb, WriteEventCb, CloseEventCb, conn);    
  234.     bufferevent_enable(bev, EV_WRITE);    
  235.     bufferevent_enable(bev, EV_READ);    
  236.   
  237.     //调用用户自定义的连接事件处理函数    
  238.     me->tcpConnect->ConnectionEvent(conn);    
  239. }    
  240.   
  241. void MultiServer::ReadEventCb(struct bufferevent *bev, void *data)    
  242. {    
  243.     Conn *conn = (Conn*)data;    
  244.     conn->m_ReadBuf = bufferevent_get_input(bev);    
  245.     conn->m_WriteBuf = bufferevent_get_output(bev);    
  246.   
  247.     //调用用户自定义的读取事件处理函数    
  248.     conn->m_Thread->tcpConnect->ReadEvent(conn);    
  249. }     
  250.   
  251. void MultiServer::WriteEventCb(struct bufferevent *bev, void *data)    
  252. {    
  253.     Conn *conn = (Conn*)data;    
  254.     conn->m_ReadBuf = bufferevent_get_input(bev);    
  255.     conn->m_WriteBuf = bufferevent_get_output(bev);    
  256.   
  257.     //调用用户自定义的写入事件处理函数    
  258.     conn->m_Thread->tcpConnect->WriteEvent(conn);    
  259.   
  260. }    
  261.   
  262. void MultiServer::CloseEventCb(struct bufferevent *bev, short events, void *data)    
  263. {    
  264.     Conn *conn = (Conn*)data;    
  265.     //调用用户自定义的断开事件处理函数    
  266.     conn->m_Thread->tcpConnect->CloseEvent(conn, events);    
  267.     conn->GetThread()->connectQueue.DeleteConn(conn);    
  268.     bufferevent_free(bev);    
  269. }    
  270.   
  271. bool MultiServer::AddSignalEvent(int sig, void (*ptr)(intshortvoid*))    
  272. {    
  273.     if( sig >= MAX_SIGNAL )  
  274.         return false;  
  275.   
  276.     //新建一个信号事件    
  277.     event *ev = evsignal_new(m_MainBase->base, sig, ptr, (void*)this);    
  278.     if ( !ev ||     
  279.         event_add(ev, NULL) < 0 )    
  280.     {    
  281.         event_del(ev);    
  282.         return false;    
  283.     }    
  284.   
  285.     //删除旧的信号事件(同一个信号只能有一个信号事件)   
  286.     if( NULL != m_SignalEvents[sig] )  
  287.         DeleteSignalEvent(sig);    
  288.     m_SignalEvents[sig] = ev;    
  289.   
  290.     return true;    
  291. }    
  292.   
  293. bool MultiServer::DeleteSignalEvent(int sig)    
  294. {    
  295.     event *ev = m_SignalEvents[sig];  
  296.     if( sig >= MAX_SIGNAL || NULL == ev )  
  297.         return false;  
  298.   
  299.     event_del(ev);    
  300.     ev = NULL;  
  301.     return true;    
  302. }    
  303.   
  304. event *MultiServer::AddTimerEvent(void (*ptr)(intshortvoid *),     
  305.                                      timeval tv, bool once)    
  306. {    
  307.     int flag = 0;    
  308.     if( !once )    
  309.         flag = EV_PERSIST;    
  310.   
  311.     //新建定时器信号事件    
  312.     event *ev = new event;    
  313.     event_assign(ev, m_MainBase->base, -1, flag, ptr, (void*)this);    
  314.     if( event_add(ev, &tv) < 0 )    
  315.     {    
  316.         event_del(ev);    
  317.         return NULL;    
  318.     }    
  319.     return ev;    
  320. }    
  321.   
  322. bool MultiServer::DeleteTImerEvent(event *ev)    
  323. {    
  324.     int res = event_del(ev);    
  325.     return (0 == res);    
  326. }    

 


测试文件:test.cpp

  1. /*  
  2. 这是一个测试用的服务器,只有两个功能:  
  3. 1:对于每个已连接客户端,每10秒向其发送一句hello, world  
  4. 2:若客户端向服务器发送数据,服务器收到后,再将数据回发给客户端  
  5. */    
  6. //test.cpp    
  7. #include "MultiServer.h"  
  8. #include <set>    
  9. #include <vector>    
  10. using namespace std;    
  11.   
  12. //测试示例    
  13. class TestServer : public MultiServer    
  14. {    
  15. private:    
  16.     vector<Conn*> vec;    
  17. protected:    
  18.     //重载各个处理业务的虚函数    
  19.     void ReadEvent(Conn *conn);    
  20.     void WriteEvent(Conn *conn);    
  21.     void ConnectionEvent(Conn *conn);    
  22.     void CloseEvent(Conn *conn, short events);    
  23. public:    
  24.     TestServer(int count) : MultiServer(count) { }    
  25.     ~TestServer() { }     
  26.   
  27.     //退出事件,响应Ctrl+C    
  28.     static void QuitCb(int sig, short events, void *data);    
  29.     //定时器事件,每10秒向所有客户端发一句hello, world    
  30.     static void TimeOutCb(int id, int short events, void *data);    
  31. };    
  32.   
  33. void TestServer::ReadEvent(Conn *conn)    
  34. {    
  35.     conn->MoveBufferData();    
  36. }    
  37.   
  38. void TestServer::WriteEvent(Conn *conn)    
  39. {    
  40.   
  41. }    
  42.   
  43. void TestServer::ConnectionEvent(Conn *conn)    
  44. {    
  45.     TestServer *me = (TestServer*)conn->GetThread()->tcpConnect;    
  46.     printf("new connection: %d\n", conn->GetFd());    
  47.     me->vec.push_back(conn);    
  48. }    
  49.   
  50. void TestServer::CloseEvent(Conn *conn, short events)    
  51. {    
  52.     printf("connection closed: %d\n", conn->GetFd());    
  53. }    
  54.   
  55. void TestServer::QuitCb(int sig, short events, void *data)    
  56. {     
  57.     printf("Catch the SIGINT signal, quit in one second\n");    
  58.     TestServer *me = (TestServer*)data;    
  59.     timeval tv = {1, 0};    
  60.     me->StopRun(&tv);    
  61. }    
  62.   
  63. void TestServer::TimeOutCb(int id, short events, void *data)    
  64. {    
  65.     TestServer *me = (TestServer*)data;    
  66.     char temp[33] = "hello, world\n";    
  67.     for(int i=0; i<me->vec.size(); i++)    
  68.         me->vec[i]->AddToWriteBuffer(temp, strlen(temp));    
  69. }    
  70.   
  71. int main()    
  72. {    
  73.     printf("pid: %d\n", getpid());    
  74.     TestServer server(3);    
  75.     server.AddSignalEvent(SIGINT, TestServer::QuitCb);    
  76.     timeval tv = {10, 0};    
  77.     server.AddTimerEvent(TestServer::TimeOutCb, tv, false);    
  78.     server.SetPort(2111);    
  79.     server.StartRun();    
  80.     printf("done\n");    
  81.   
  82.     return 0;    
  83. }    

 


编译与运行命令:

    1. qch@LinuxMint ~/program/ztemp $ g++ TcpEventServer.cpp test.cpp -o test -levent  
    2. qch@LinuxMint ~/program/ztemp $ ./test  
    3. pid: 20264  
    4. new connection: 22  
    5. connection closed: 22  
    6. ^CCatch the SIGINT signal, quit in one second  
    7. done  
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值