目录
- 通用问题
-
- 为什么要做这个项目
- 这个项目为什么和上一个同学的项目一样
- 介绍一下这个项目流程和架构设计
- 项目最终的成果如何? 你从这个项目中学到了什么?
- 你在这个项目中使用了哪些技术? 这些技术是怎么选型的, 对比过那些其他的相关技术吗?
- 你在项目中扮演了什么角色? 你负责了哪些模块或功能?
- 在项目中, 你是如何平衡技术实现和项目时间线的?
- 你在项目中遇到了哪些挑战? 你是如何解决这些问题的?
- 你是如何测试和部署这个项目的?
- 项目性能如何, 有没有尝试优化过性能呢?
- 你是如何设计项目以支持未来的扩展?你们是如何确保项目代码的可维护性的?
- 专属问题
-
- 你怎么理解Reactor模型? 相比较传统的多线程I/O模型有什么优势
- eventfd的作用是什么?为什么要用eventfd而不用普通的fd?eventfd的原理?
- runInLoop的作用及实现? 为什么执行_tasks任务的时候需要交换
- 定时器的设计以及为什么要用shared_ptr和weak_ptr?
- 怎么理解 One Loop Per Thread
通用问题
为什么要做这个项目
自己的某个业务类项目需要自己搭建网络通信框架, 为了锻炼并提升自己的C++多线程编程和工程化的能力. 参考muduo库这个开源项目做了一些简化, 自己实现了一个支持高并发的网络通信组件
这个项目为什么和上一个同学的项目一样
是参考github上的一个开源项目muduo库, 网上一搜有很多这个项目的博客分析, 写得优质的主要就是那几篇, 可能我们参考了一样的博客讲解梳理框架吧.
介绍一下这个项目流程和架构设计
整个项目的实现划分为两个的模块:
- SERVER模块: 实现Reactor模型的TCP服务器.
- Buffer模块:是一个缓冲区模块, 用于实现通信中用户态的接收缓冲区和发送缓冲区功能
- Socket模块: Socket模块是对套接字操作封装的一个模块, 主要实现的socket的各项操作
- Channel模块: Channel模块是对一个描述符需要进行的IO事件管理的模块, 实现对描述符可读, 可写, 错误…事件的管理操作, 以及Poller模块对描述符进行IO事件监控就绪后, 根据不同的事件, 回调不同的处理函数功能
- Connection模块: Connection模块是对Buffer模块, Socket模块, Channel模块的一个整体封装, 实现了对一个通信套接字的整体的管理, 每一个进行数据通信的套接字(也就是accept获取到的新连接) 都会使用Connection进行管理.
- Acceptor模块: Acceptor模块是对Socket模块, Channel模块的一个整体封装, 实现了对一个监听套接字的整体的管理
- TimerQueue模块: 是实现固定时间定时任务的模块, 可以理解就是要给定时任务管理器, 想定时任务管理器中添加一个任务, 任务将在固定时间后被执行, 同时也可以通过刷新定时任务来延迟任务的执行. 这个模块主要是对Connection对象的生命周期管理, 对非活跃连接进行超时后的释放功能
- Poller模块: 是对epoll进行封装的一个模块, 主要实现epoll的IO事件添加, 修改, 移除, 获取活跃连接的功能
- EventLoop模块: EVentLoop模块可以理解就是我们上边所说的Reactor模块, 它是对Poller模块, TimerQueue模块, Socket模块的一个整体封装, 进行所有描述符的事件监控
- TcpServer模块: 这个模块是一个整体Tcp服务器模块的封装, 内部封装了Acceptor模块, EventLoop模块作为主, EventLoopThreadPool模块作为从
- 协议模块: 对当前的Reactor模型服务器提供应用层协议支持
- http协议模块
- Util模块: 这个模块是一个工具模块, 主要提供HTTP协议模块所用到的一些工具函数, 比如url编解码, 文件读写…等
- HttpRequest模块: 这个模块是HTTP请求数据模块, 用于保存HTTP请求数据被解析后的各项请求元素信息
- HttpResponse模块: 这个模块是HTTP响应数据模块, 用于业务处理后设置并保存HTTP响应数据的各项元素信息, 最终会被按照HTTP协议响应格式组织成为响应信息发送给客户端
- HttpContext模块: 这个模块是一个HTTP请求接收的上下文模块
- HttpServer模块: 这个模块是最终给组件使用者提供的HTTP服务器模块了, 用于以简单的接口实现HTTP服务器的搭建.
- http协议模块
项目主要流程: 1.在实例化TcpServer对象过程中, 完成Baseloop的设置, Acceptor对象的实例化, 以及EventLoop线程池的实例化, 以及std::shared_ptr< Connection >的hash表的实例化 2.为Acceptor对象设置回调函数: 获取到新连接后, 为新连接构建Connection对象, 设置Connection的各项回调, 并使用shared_ptr进行管理, 并添加到hash表中进行管理, 并未Connection选择一个EventLoop线程, 为Connection添加一个定时销毁任务, 为Connection添加事件监控 3.启动BaseLoop 4.通过Poller模块对当前模块管理内的所有描述符进行IO事件监控, 有描述符事件就绪后, 通过描述符对应的Channel进行事件处理 5.所有就绪的描述符IO事件处理完毕后, 对任务队列中的所有操作顺序进行执行 eventfd的作用: 6.由于epoll的事件监控, 有可能会因为没有事件到来而持续阻塞, 导致任务队列中的任务不能及时得到执行, 因此创建了eventfd, 添加到Poller的事件监控中, 用于实现每次向任务队列添加任务的时候, 通过向eventfd写入数据来唤醒epoll的阻塞. 7.实现向Channel提供可读, 可写, 错误等不同事件的IO事件回调函数, 然后将Channel和对应的描述符添加到Poller事件监控中. 再具体一点说: 8.当描述符在Poller模块中就绪了IO可读事件, 则调用描述符对应Channel中保存的读事件处理函数, 进行数据读取, 将socket就收缓冲区全部读取到Connection管理的用户态接收缓冲区中. 然后调用由组件使用者传入的新数据到来回调函数进行处理. 9.组件使用者进行数据的业务处理完毕后, 通过Connection向使用者提供的数据发送接口, 将数据写入Connection的发送缓冲区中. 10.启动描述符在Poller模块中的IO写事件监控, 就绪后, 调用Channel中保存的写事件处理函数, 将发送缓冲区中的数据通过Socket进行面向系统的实际数据发送.
项目最终的成果如何? 你从这个项目中学到了什么?
成果:
- 通过实现的高并发服务器组件, 可以简洁快速的完成一个高性能的服务器搭建
- 通过组件内提供的不同应用层协议支持, 也可以快速完成一个高性能应用服务器的搭建(当前项目中提供HTTP协议组件的支持) 学到了什么:
- 学到了以项目为主导的开发应该如何完成, 比如从项目确定到功能拆解, 再到开发测试部署
- 学到了当项目中遇到问题, 迫使我思考和寻找解决方案, 这增强了我的问题解决能力
- 学到了如何进行有效的测试, 以确保项目不出现bug. 例如功能测试, 安全性测试等.
你在这个项目中使用了哪些技术? 这些技术是怎么选型的, 对比过那些其他的相关技术吗?
这个项目作为一个基础的网络通信组件, 使用纯C++语言编写的, 没有使用任何的第三方库. 只是用到了C++11的线程库和多路复用的系统接口.
- epoll: 对比select, poll效率更高, 但是只有unix平台支持
- std::thread: 方便易用, 可移植
你在项目中扮演了什么角色? 你负责了哪些模块或功能?
(比较简单的项目不应该是和别人合作完成的, 一定是独立完成)
- 独立完成项目的设计, 实现, 测试, 部署.
- 社招的同学可以说明基础通信模块是自己实现的, 协议支持模块是同事实现的
在项目中, 你是如何平衡技术实现和项目时间线的?
这个项目我花了2个月来完成, 从项目目标的确定, 到项目原型图的敲定, 到各个模块的拆分, 再到最后的开发测试部署. 关于时间, 其实我没有制定详细的规划, 是因为我也是抱着学习的心态来完成这个项目. 在开发中可能会遇到一些问题, 解决问题也是我学习的过程, 因此时间就不能确定下来.
你在项目中遇到了哪些挑战? 你是如何解决这些问题的?
在参考muduo源码的时候,发现有很多回调函数的设置,导致在项目的流程梳理上特别困难,我花了好长的时间才把这些回调函数的设置梳理清楚。
- Acceptor模块启动的时候需要一个newConnection的回调, 这个是因为当主Reactor接受到新连接的时候, 即监听套接字产生可读事件, 这个时候通过EventLoop分发到newConnection回调的调用创建一个新连接并分配给从Reactor
- Connection对象中需要用户设置3个回调函数进去, 分别是连接建立完成回调, 新数据回调, 关闭回调, 当从Reactor监听到IO事件的时候, 通过EventLoop分发到对应的回调函数进行调用
你是如何测试和部署这个项目的?
测试思路: 1.编写一个简单的html欢迎页面, 然后使用浏览器进行访问 2.长连接连续请求测试: 一个连接中每隔3s向服务器发送一个请求, 查看是否会收到响应. 3.超时连接释放测试: 创建一个客户端, 连接上服务器后, 不进行消息发送, 等待看超时后, 连接是否会自动释放(当前默认设置超时时间为10s). 预期结果: 10s后连接被释放. 4.数据中多条请求处理测试: 给服务器发送的一条数据中包含有多个HTTP请求, 观察服务器的处理. 预期结果: 每一条请求都有其对应的响应 5.PUT大文件上传测试: 使用put请求上传一个大文件进行保存, 大文件的接收会被分在多次请求中接收, 然后计算源文件和上传后的文件的MD5值, 判断请求的接收处理是否存在问题. 预期结果: 两个文件的MD5是一样的
项目性能如何, 有没有尝试优化过性能呢?
性能测试采用Webbench进行服务器性能测试. Webbench是知名的网站压力测试工具, Webbench的标准测试可以向我们展示服务器的两项内容: 每秒钟相应请求数 和每秒钟传输数据量, 即QPS和吞吐
服务器环境: 4核4G虚拟机ubuntu-22.04LTS, 服务器程序采用1主3从Reactor模式 Webbench客户端环境: 同一个虚拟机
500 | 391061 | 2620396字节 |
5000 | 384577 | 2589020字节 |
10000 | 357157 | 2423604字节 |
以上测试中,使⽤浏览器访问服务器,均能流畅获取请求的⻚⾯。但是根据测试结果能够看出,虽然并发量⼀直在提⾼,但是总的请求服务器的数量并没有增加,反⽽有所降低,侧⾯反馈了处理所耗时间更多了,基本上可以根据35w/min左右的请求量计算出10000并发量时服务器的极限了。 但是这个测试其实意义不⼤,因为测试客⼾端和服务器都在同⼀台机器上,传输的速度更快,但同时抢占cpu也影响了处理,最好的⽅式就是在两台不同的机器上进⾏测试,这⾥只是通过这个⽅法告诉⼤家该如何对服务器进⾏性能测试。
你是如何设计项目以支持未来的扩展?你们是如何确保项目代码的可维护性的?
项目扩展:
- 支持其他的应用层协议, 比如webosocket, rpc, amqp等等
- 确保代码的可维护性: 1.遵循编码规范: 定并遵守一致的编码标准和命名约定,使代码易于理解和维护 2.模块化设计: 系统分解为独立的模块,每个模块负责一个特定的功能,这样可以降低复杂性,提高可维护性。 3.文档化: 写清晰的文档,包括代码注释、API文档和用户手册,以帮助其他开发者理解代码的工作原理。
专属问题
你怎么理解Reactor模型? 相比较传统的多线程I/O模型有什么优势
传统的IO模型是指当接收到一个新连接之后, 为这个新连接开辟一个进程/线程负责处理IO事件和业务. 特点:
- 采用阻塞式IO模型获取输入数据
- 每个连接都需要独立的线程完成数据输入, 业务处理, 数据返回的完整操作
缺点:
- 当并发数较大时, 需要创建大量线程来处理连接, 系统资源占用较大
- 连接建立后, 如果当前线程暂时没有数据可读, 则线程就阻塞在Read操作上, 造成线程资源浪费
解决方案:
- 基于I/O多路复用模型: 多个连接共用一个阻塞对象, 应用程序只需要在一个阻塞对象上等待, 无需阻塞等待所有连接. 当某条连接有新的数据可以处理时, 操作系统通知应用程序, 线程从阻塞态返回, 开始进行业务处理
- 基于线程池复用线程资源: 不必再为每个连接创建线程, 将连接完成后的业务处理任务分配给线程进行处理, 一个线程可以处理多个连接的业务
Reactor模式, 是指通过一个或多个输入同事传递给服务器进行请求处理时的事件驱动处理模式. 服务端程序处理传入多路请求, 并将它们同步分派给请求对应的处理线程, Reactor模式也叫Dispatcher模式 简单理解就是使用 IO多路复用 统一监听事件, 收到事件后分发给处理进程或线程, 是编写高性能网络服务器的必备技术之一.
eventfd的作用是什么?为什么要用eventfd而不用普通的fd?eventfd的原理?
- 作用: eventfd是用来传递事件的fd, 在本项目的作用是为了唤醒有可能epoll因为没有事件就绪而导致的阻塞, 因为要执行后续的任务, 所以通过eventfd来唤醒epoll_wait
- 为什么: 不是所有的fd类型都可用epoll池来监听事件的, 只有实现了file_operation->poll的调用的 “文件” fd 才能被epoll管理. eventfd刚好就实现了这个接口.
- 原理: eventfd实现的是一个计数功能, 写进去一个8字节的整数, eventfd实现的逻辑是累计计数, 读的时候, 读到总计数, 并且会清零. 使用epoll监听事件, 那么都是监听读事件, 因为监听写事件无意义, read eventfd的时候, 如果计数器的值为0, 就会阻塞, 一旦向该eventfd写入数据, 则唤醒epoll
runInLoop的作用及实现? 为什么执行_tasks任务的时候需要交换
- 作用: 任何一个线程, 只要创建并运行了EventLoop, 都称之为IO线程. runInLoop()使得IO线程能够执行某个用户任务回调. 如果用户在当前IO线程调用这个函数, 回调会同步进行; 如果用户在其他线程调用runInLoop(), 回调函数会加入到队列中, IO线程会被唤醒来调用这个函数. 这样就能够轻易地在线程间调配任务, 比如将回调函数都移到IO线程中执行, 就可以在不使用锁的情况下保证线程安全.
- 实现: eventfd实现, 判断回调函数是否是当前IO线程调用, 如果是, 则直接执行, 如果不是, 则push到EventLoop中的_tasks任务池中, 然后通过向eventfd中write一个数据唤醒epoll_wait而执行对应的回调任务
- 为什么: 这样做可以减小锁的力度, 相当于一次批处理
定时器的设计以及为什么要用shared_ptr和weak_ptr?
定时器的设计是一个二维数组,第一维是一个循环数组,数组中每个元素也是一个数组,其他存储具体的定时任务。 使用shared_ptr的引l用计数来解决定时刷新问题,当需要刷新定时任务的时候,重新构造一个shared_ptr对象插入时间轮,第一个shared_ptr到期会销毁,引用计数-1,但此时引用计数还不是0,表示当前这个shared_ptr指向的任务失效。但是要注意,需要保存之前定时任务的weak_ptr,通过weak_ptr来构造新的shared_ptr才会共享引用计数。
怎么理解 One Loop Per Thread
OneLoop PerThread的含义就是,一个EventLoop和一个线程唯一绑定,和这个EventLoop有关的,被这个EventLoop管辖的一切操作都必须在这个EventLoop绑定线程中执行,比如在MainEventLoop中,负责新连接建立的操作都要在MainEventLoop线程中运行。已建立的连接分发到某个SubEventLoop上,这个已建立连接的任何操作,比如接收数据发送数据,连接断开等事件处理都必须在这个SubEventLoop线程上运行。
评论前必须登录!
注册