服务器预约系统linux小项目-第二节课

服务器预约系统linux小项目-第二节课 一、这节课在整个项目里干什么我们现在做的是预约系统但当前阶段先不做数据库先把客户端和服务器端的通信框架搭起来。上节课我们已经完成了封装监听套接字Lis_Socket让服务器具备socket - bind - listen的能力这节课继续补全LibEvent类回调基类CallBackAccept_CallBackRecv_CallBack事件注册事件循环数据接收与回复本节课最终实现的效果客户端成功连接服务器客户端发送数据服务器接收数据服务器回复ok客户端收到ok也就是说客户端—服务器端最基础的通信主线已经跑通。二、这节课的主线流程复习时先记这 8 步服务器先创建监听套接字服务器初始化LibEvent把监听套接字注册到LibEvent客户端连接服务器服务器执行accept()得到新的连接套接字把新的连接套接字继续注册到LibEvent客户端发送数据服务器recv()收到数据并回复ok一句话记忆监听 socket 负责接人连接 socket 负责聊天LibEvent 负责统一调度。三、通俗理解每个类在干什么1.Lis_Socket服务器门口这个类专门负责服务器监听套接字。它干的事情很简单就是三步socket()开一个 socketbind()绑定地址和端口listen()开始监听你可以这样记Lis_Socket 服务器门口作用就是站在门口等客户端来。最重要的代码模板class Lis_Socket { private: int sockfd; string ip; short port; public: Lis_Socket() { sockfd -1; ip 127.0.0.1; port 6000; } bool Bind(); int Get_Sockfd() { return sockfd; } };Bind()模板代码这是你以后最起码要会默写的服务器监听模板bool Lis_Socket::Bind() { sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd -1) { cout socket err endl; return false; } struct sockaddr_in saddr; memset(saddr, 0, sizeof(saddr)); saddr.sin_family AF_INET; saddr.sin_port htons(port); saddr.sin_addr.s_addr inet_addr(ip.c_str()); int res bind(sockfd, (struct sockaddr*)saddr, sizeof(saddr)); if (res -1) { cout bind err endl; return false; } res listen(sockfd, Max_listen); if (res -1) { cout listen err endl; return false; } return true; }这一段你面试怎么说Lis_Socket类主要用来封装服务器监听套接字完成socket、bind、listen三个基础步骤让服务器具备等待客户端连接的能力。2.LibEvent事件管理员这个类是这节课真正补全的重点。它的作用不是通信而是谁有事了我负责发现并叫对应的人去处理。它主要有三个函数Init()初始化事件系统你可以理解成把事件管理中心搭起来Lib_Add(fd, p)给某个描述符fd注册事件并绑定一个处理对象p你可以理解成把“谁出事了交给谁处理”这个关系记下来Dispatch()启动事件循环你可以理解成事件管理员正式开始上班插图位置说明放在这一小节后面最合适因为这张图讲的就是LibEvent背后的原理。图下配文图 1 展示了 libevent 的基本流程先通过event_init()创建事件管理器base再通过event_add()注册事件接着通过event_base_dispatch(base)进入事件循环最后释放事件和base。本项目中的LibEvent类本质上就是对这套流程的封装。LibEvent类模板代码class LibEvent { public: LibEvent() { base NULL; } bool Init() { base event_init(); if (base NULL) { return false; } return true; } bool Lib_Add(int fd, CallBack *p); bool Dispatch(); private: struct event_base* base; };Lib_Add()模板代码这个函数非常重要建议记住大致样子Dispatch()模板代码bool LibEvent::Dispatch() { if (event_base_dispatch(base) -1) { return false; } return true; }这一段你面试怎么说LibEvent类是对 libevent 的封装主要负责事件系统初始化、事件注册和事件循环。它本身不处理业务而是负责事件监听和分发。3.CallBack统一回调基类这个类本身不干具体工作它只是规定所有处理事件的类都必须有一个CallBack_Fun()所以它像一个统一标准。你可以这样记CallBack 所有事件处理类的共同模板模板代码class CallBack { public: virtual void CallBack_Fun() 0; struct event *ev; };为什么要有这个类因为服务器里不同事件处理逻辑不同新连接来了要accept()数据来了要recv()但是它们又有共同点都是事件来了以后调用一个函数处理。所以就抽象出一个基类。面试说法CallBack是统一回调基类作用是定义统一接口方便后续用继承和多态分别处理不同类型的 socket 事件。4.Accept_CallBack专门接人这个类专门处理监听套接字上的事件也就是有客户端来连接了就执行accept()接收这个新客户端再把这个新客户端交给后续的数据处理类你可以这样记Accept_CallBack 接待员作用是负责把新来的客户端接进来类模板代码class Accept_CallBack : public CallBack { public: Accept_CallBack(int fd, LibEvent *plib) { sockfd fd; m_base plib; } void CallBack_Fun(); private: int sockfd; LibEvent *m_base; };核心处理函数模板void Accept_CallBack::CallBack_Fun() { struct sockaddr_in caddr; socklen_t len sizeof(caddr); int c accept(sockfd, (struct sockaddr*)caddr, len); if (c 0) { return; } Recv_CallBack *r new Recv_CallBack(c); m_base-Lib_Add(c, r); }这一段怎么理解这段代码的核心意思就是有新客户端来了我先接住他再把他交给专门负责通信的人。面试说法Accept_CallBack负责处理监听套接字上的连接事件当有新客户端连接时通过accept()获取新的连接套接字并继续把它注册到事件系统中。5.Recv_CallBack专门收消息这个类专门处理连接套接字上的数据接收也就是客户端发数据来了就执行recv()收到数据后打印出来再回复ok你可以这样记Recv_CallBack 聊天的人作用是负责和已经连上的客户端通信类模板代码class Recv_CallBack : public CallBack { public: Recv_CallBack(int fd) { c fd; } void CallBack_Fun(); private: int c; };核心处理函数模板void Recv_CallBack::CallBack_Fun() { char buff[128] {0}; int n recv(c, buff, 127, 0); if (n 0) { event_free(ev); close(c); delete this; return; } printf(recv(%d):%s\n, c, buff); send(c, ok, 2, 0); }这一段怎么理解这段代码的核心意思就是如果对方发消息了我就接收如果连接断了我就清理资源。面试说法Recv_CallBack负责处理连接套接字上的数据接收事件通过recv()获取客户端数据并通过send()给客户端返回确认信息。四、为什么要这样设计这部分是这节课真正的知识点复习和面试都很重要。1. 为什么要封装LibEvent因为服务器后面要面对多个连接、多种事件。如果不用事件驱动代码会越来越乱。所以这里引入libevent再封装成LibEvent类。一句话记忆socket 负责通信LibEvent 负责监听和调度。2. 为什么要区分监听套接字和连接套接字监听套接字只负责等客户端连接连接套接字只负责和客户端聊天一句话记忆监听套接字接人连接套接字聊天3. 为什么要设计CallBack基类因为不同 socket 的处理逻辑不一样监听 socketaccept()连接 socketrecv()所以不能写一个大函数把所有逻辑都揉在一起。最好的方式就是先定义统一基类再写不同子类分别处理这就是继承 多态这里插入【图 2CallBack / Accept / Recv 关系图】插图位置说明放在这一节后面最合适因为这张图正好解释为什么要设计CallBack基类以及它和两个子类的关系。图下配文图 2 展示了回调类的继承关系。CallBack作为统一基类定义CallBack_Fun()接口Accept_CallBack用来处理监听套接字上的新连接事件Recv_CallBack用来处理连接套接字上的数据接收事件。外部统一按CallBack*管理事件触发时通过多态执行不同子类中的逻辑。五、结合代码理解整条流程这一节建议你重点背因为这是最适合面试复述的部分。第一步服务器先把门口搭起来Lis_Socket ser; ser.Bind();意思是创建监听 socket绑定127.0.0.1:6000开始监听第二步事件管理员开始准备LibEvent *plib new LibEvent(); plib-Init();意思是创建LibEvent初始化event_base第三步给门口配一个接待员Accept_CallBack *pacb new Accept_CallBack(ser.Get_Sockfd(), plib); plib-Lib_Add(ser.Get_Sockfd(), pacb);意思是监听套接字交给LibEvent一旦有新连接就由Accept_CallBack处理第四步启动事件循环plib-Dispatch();意思是服务器正式开始运行持续等待事件发生第五步客户端连接时监听 socket 触发事件调用Accept_CallBack::CallBack_Fun()执行accept()生成新的连接 socket创建Recv_CallBack再把新的连接 socket 注册进事件系统第六步客户端发数据时新连接 socket 触发事件调用Recv_CallBack::CallBack_Fun()执行recv()服务器收到数据再send(ok)回复客户端六、主函数模板代码这部分建议你会写至少会照着写。int main() { Lis_Socket ser; if (!ser.Bind()) { cout bind err endl; exit(1); } LibEvent *plib new LibEvent(); if (plib NULL) { cout new LibEvent err endl; exit(1); } if (!plib-Init()) { cout LibEvent init err endl; exit(1); } Accept_CallBack *pacb new Accept_CallBack(ser.Get_Sockfd(), plib); if (pacb NULL) { cout new accept callback err endl; exit(1); } plib-Lib_Add(ser.Get_Sockfd(), pacb); plib-Dispatch(); return 0; }这一段怎么记主函数就是做三件事开门注册事件开始等事发生七、最应该会写的模板代码清单面试真让你手写你最起码要会下面这些模板。1. 服务器监听 socket 模板socket(); bind(); listen();2.sockaddr_in初始化模板struct sockaddr_in saddr; memset(saddr, 0, sizeof(saddr)); saddr.sin_family AF_INET; saddr.sin_port htons(port); saddr.sin_addr.s_addr inet_addr(ip.c_str());3.LibEvent::Lib_Add()模板event_new(base, fd, EV_READ | EV_PERSIST, G_CallBack_Fun, p); event_add(ev, NULL);4.accept()处理模板int c accept(sockfd, (struct sockaddr*)caddr, len);5.recv()/send()模板int n recv(c, buff, 127, 0); send(c, ok, 2, 0);八、课堂笔记版总结本节主题补全服务端通信框架实现客户端与服务器端最基础的数据收发。本节目标服务器基于libevent启动利用多态处理监听套接字和连接套接字客户端成功连接服务器客户端发送数据服务器接收并回复本节核心类Lis_Socket监听 socket负责等客户端连接LibEvent事件管理类负责初始化、注册事件、事件循环CallBack回调基类统一定义CallBack_Fun()Accept_CallBack处理新连接Recv_CallBack处理接收数据本节最重要思想监听 socket 和连接 socket 职责不同不同事件逻辑不同所以用回调基类 子类实现继承和多态libevent负责监听和调度socket 负责通信本节结果已经实现客户端连接服务器、发送数据服务器接收数据并回复ok说明最基础的通信主线已经跑通。九、面试时怎么说你直接背这一段都可以这个预约系统项目第一阶段我主要负责客户端和服务器端通信框架的搭建。服务端先封装了监听套接字类Lis_Socket完成socket、bind、listen。在此基础上又封装了LibEvent类来管理事件并设计了回调基类CallBack以及两个子类Accept_CallBack用来处理新连接Recv_CallBack用来处理客户端数据接收。这样通过继承和多态把不同类型 socket 的处理逻辑分开。最终实现了客户端连接服务器、发送数据服务器接收并回复ok为后续 JSON 通信和数据库业务处理打下了基础。十、最短背诵版这节课在上节课监听套接字封装的基础上补全了LibEvent类和回调类体系。通过CallBack基类以及Accept_CallBack、Recv_CallBack两个子类分别处理新连接和数据接收最终实现了客户端连接服务器、发送数据服务器接收并回复ok。一、先看这张图在表达什么图里有这几个东西struct eventfdCallBack类accept_CallBackrecv_CallBackCallBack_Fun()它想表达的是每个被监听的描述符fd都会对应一个回调对象而这个回调对象虽然统一看成CallBack类型但实际可能是不同子类比如accept_CallBackrecv_CallBack这样当事件发生时就能根据对象真实类型执行不同处理逻辑。二、这张图背后的问题是什么在服务器里不同的fd干的事不一样。比如1. 监听套接字的fd它的作用是有客户端连接时调用accept()生成新的连接套接字所以它对应的处理逻辑应该是处理新连接2. 已连接套接字的fd它的作用是接收客户端发来的数据调用recv()所以它对应的处理逻辑应该是处理收数据如果不用类来区分你就得在一个大函数里不停判断这个fd是监听套接字吗还是连接套接字如果是监听就accept如果是连接就recv这样代码会越来越乱。三、所以才有了这个设计这张图表达的就是先定义一个统一的基类CallBack比如里面规定一个统一接口virtual void CallBack_Fun() 0;意思是所有回调类都必须提供一个CallBack_Fun()函数这个基类本身不处理具体业务它只是定规则。再定义不同子类accept_CallBack专门处理监听套接字事件它的CallBack_Fun()里做的事一般是accept()得到新的连接套接字再把新套接字注册到事件系统recv_CallBack专门处理连接套接字事件它的CallBack_Fun()里做的事一般是recv()接收客户端发来的数据做后续处理四、这张图中struct event -- fd -- 类是什么意思这里的意思可以理解成一个event本质上是在监听某个fd而这个fd又会绑定一个对应的回调类对象也就是event负责“监听事件”fd表示“监听谁”CallBack对象表示“出了事谁来处理”所以这三者关系可以理解成event 监听 fdfd 对应一个回调对象事件发生时调用这个对象的方法五、这张图真正体现的思想多态这是最重要的一点。外部统一都是CallBack* p也就是说表面上它们都被当成CallBack类型看待。但实际上传进去的可能是accept_CallBackrecv_CallBack当事件发生时统一调用p-CallBack_Fun();虽然写法一样但因为对象实际类型不同所以执行结果不同如果p指向accept_CallBack就执行处理连接的逻辑如果p指向recv_CallBack就执行处理接收的逻辑这就是多态。六、这张图的作用是什么它的作用主要有三个1. 把不同事件的处理逻辑分开监听连接和接收数据不是一回事拆开后更清楚。2. 让事件处理更容易扩展以后如果你还要加别的事件比如发送数据事件定时器事件管理员命令事件你只需要再写新的CallBack子类就行不用大改原有代码。3. 让 libevent 和 C 类结合起来libevent 本身偏 C 风格但你们项目想用面向对象的方式来组织代码。所以这套设计就是在做一件事用 C 的类对象去承接 C 风格事件回调七、你可以把这张图翻译成一句很容易懂的话不同的描述符干的事情不一样所以给它们绑定不同的回调类对象外部统一按CallBack类型管理事件发生时再通过多态调用各自真正的处理函数。