仿muduo库实现高并发服务器-口试常见问题

打印 上一主题 下一主题

主题 1378|帖子 1378|积分 4134

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

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

x
通用问题

为什么要做这个项目

自己的某个业务类项目需要自己搭建网络通信框架, 为了锻炼并提升自己的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服务器的搭建.


项目重要流程:
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客户端环境: 同一个虚拟机
客户端数量QPS/min吞吐/sec5003910612620396字节50003845772589020字节100003571572423604字节 以上测试中,使⽤浏览器访问服务器,均能流畅获取哀求的⻚⾯。但是根据测试结果能够看出,固然并发量⼀直在提⾼,但是总的哀求服务器的数量并没有增加,反⽽有所低落,侧⾯反馈了处置惩罚所耗时间更多了,基本上可以根据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线程上运行。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

李优秀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表