socket网络库设计——muduo观后感

文章目录

  • 对《muduo》封面提出疑问的一些解答
  • 网络库设计思路
    • 代码复用
      • 事件处理模型
      • Reator vs. Proactor
      • 连接模型
      • 并发模型
    • 多线程安全
  • 主流语言网络库实现思路
    • C/C++
    • C#/Java
    • Golang/JavaScript/Lua/python...

对《muduo》封面提出疑问的一些解答

  • TCP协议真的有所谓的“粘包问题”吗?该如何设计消息帧的协议?又该如何编码实现分包才不会掉到陷阱里?

    TCP本身不存在“粘包问题”,因为TCP是基于字节流的协议,没有所谓的“包”。实际上“粘包问题”是上层协议消息转成字节流发送之后,接收方如何正确处理字节流到上层协议消息的unmashal问题。

    设计(上层协议)消息帧最基础的原则就是正确能从字节流中unmashal出消息数据包,例如在上层消息数据包加上帧头、消息协议编号,帧尾(或者帧头,数据包长度,消息协议编号)。另外优秀的设计也要兼顾mashal和unmashal的效率和可扩展性,例如Protobuf

    分包要解决的问题:生命周期管理;出错如何处理;如何处理一次收到半条消息、一条消息、一条半消息、两条消息。

  • 要不改成用现成的libevent网络库吧,怎么查询一下数据库就把其他连接上的请求给耽误了?

    这里指MySQL等只提供同步操作API的数据库,实际上API操作的表现往往同步且阻塞。解决的办法就是多开一条线程专门处理数据库的I/O。

  • 再用一个线程池吧。万一发回响应的时候对方已经断开连接了怎么办?会不会串话?

    网络编程所谓“串话”实际是指程序尝试使用已释放的描述符进行通信。于是乎问题就来到如何维护释放描述符的问题,遇到资源释放问题,那么解决办法就是使用RAII,用shared_ptr维护一个socket对象。这样一旦断开连接,描述符也会被自动回收。

  • 分布式系统跟单机多进程到底有什么本质区别?心跳协议为什么是必需的,该如何实现?

    分布式系统的出错状况比单机要复杂:例如远程机子断联,我们无法知道是进程宕机还是网络中断。

    心跳协议是必需的。心跳协议用于判断对方进程是否能正常工作。(心跳间隔往往大于网络时延,以便区别到底是网络问题还是进程问题)

网络库设计思路

网络库是介于socket层和应用层之间一层抽象,实际上它也应属于应用层的一部分,它为应用层解决网络的三个半事件:建立连接、断开连接、消息到达、消息发送完毕(半个)。 对于网络服务器来说,因为它需要处理多个连接的收发,而且需要高效地处理多个连接收发。优秀的网络库必须要解决以下问题:

  • 高性能
    • 多线程 多线程安全
    • 网络I/O尽量不阻塞于某个线程
    • 尽可能少的内存切换
  • 代码复用
    • 高内聚 低耦合

代码复用

Douglas C. Schmidt给我们总结了一个网络库设计可以解耦成几个部分:事件处理模型、并发模型和连接模型。 二十多年来模型也依旧如此,仍能解决现今绝大部分网络性能问题,说明网络库设计已成定式,实际运用少有突破。

事件处理模型

从UNP v.1我们可以学习到单线程非阻塞select编程实例多线程阻塞accept编程实例,以及对应线程池版阻塞accept编程实例。根据UNP v.1统计,单线程非阻塞select编程实例的I/O执行效率最高(足以解决c10k问题)。而它唯一缺点就是代码量大,因此我们对其进行代码封住复用(归功D. C. Schmidt),而单线程非阻塞select编程实例封装之后就是Reactor模型了。
事件处理模型主要有Reactor(反应式)和Proactor(前摄式)两种模式。

  • Reactor
    Reactor是基于I/O multiplexting的一个同步事件循环模型,关键结构包括:

    • Demultiplexer:解复用,一般和select(poll, epoll)内聚,用于处理select(poll, epoll)状态。

    • Event Handler:事件句柄调度,根据Demultiplexer返回handle调用对应event。

    • Dispatcher:事件循环主体,用于初始化,注册Event Handler。
      一个Reactor事件循环:

      handle
      finished and cotinue loop
      send
      sync
      sync
      client
      I/O multipexting-Demultiplexer
      Event Handler
      Event/Functor: Accept/Read/Write
      Server
    • select(poll, epoll)有响应之后,将有状态的event handle传给Handler,交由Handler指派执行已经注册的回调函数Event/Functor。

    • 因为是同步I/O,所以实际上Server要等待操作系统I/O完成后才能进行数据处理。

    • 多个client实际都要阻塞在I/O multiplexting上,然而由于select/poll/epoll机制优势,使得阻塞时间可以忽略。

      muduo库并没有严格按以上模型实现,不过也大同小异:muduo把Demultiplexer再解耦,一部分和Event Handler内聚成Channel类。
      libevent则时遵循以上模型实现:Demultiplexer和I/O multiplexting内聚,Event Handler为evmap类和event类。
      其实muduo库只考虑linux下poll/epoll的socket编程,所以显得Event Handler模块太轻,同时Demultiplexer和Event Handler都是属于根据handle做“派生”动作,逻辑上可以整合一起。
      muduo的eventLoop比libevent多做了Demultiplexer的一部分工作。

      Reactor实现重点是epoll状态管理和非阻塞I/O的buffer处理。

  • Proactor
    Proactor是基于AIO的异步事件模型,目前主流OS只有Windows实现了类POSIX的AIO(linux出了io_uring) ,故Proactor没有Reactor模型流行。而C++的asio库将Reactor整合成了Proactor模型,实现跨平台异步网络库。Proactor关键结构包括:

    • Asynchronous Operation:异步操作
    • Completion Handler:functor
    • Completion Dispatcher:句柄复用+消息队列
    • Initiator:注册异步操作,Dispatcher,Handler
      一个Proactor事件循环:
    async
    complete
    notice
    async write
    send
    clinet
    Asynchronous Operation: async-accept/read
    Asynchronous Process
    Initiator
    Completion Dispatcher
    Completion Handler
    • 发起一个异步操作后 ,等待Process处理完成将通知Completion Dispatcher分派执行已经注册的回调函数Completion Handler处理读写后操作。
    • Asynchronous Operation每一次异步请求发起后,可以立即处理下一个client请求。
    • Handler每一次异步请求发起后,可以立即处理下一个Dispatcher发来的notice请求。

      asio库将Completion Dispatcher解耦成Event queue和Demultiplexer两个部分(也是所谓的Proactor部分)。
      IOCP天然适合Proactor模型,因为内核实现了最复杂的异步处理部分。

Reator vs. Proactor

Reator或Proactor都只是代码复用模型,没有性能上的优劣对比:Reactor可以是Proactor的同步版本,Proactor也可以看作Reactor的异步版本。结构上Reactor比Proactor简单,原因是同步模型可以把几个同步操作合并看作一个组件。
如果要把异步I/O封装成Reactor模型,只要把异步完成后loop(Process-Dispatcher-Completion Handle)封装成一个同步接口即可。(libevent)

loop
send
client
Async Process+Completion
Server

对于异步I/O来说,Process就是提供异步操作的操作系统;所以要把同步I/O封装成Proactor模型,那就要提供一个用户态的async Process-Completion handle组件用于异步操作。(asio)

send
clinet
queue
Reactor
queue
Completion Handler

Reactor/Proactor一般用于长连接网络问题;短连接网络直接使用套接字编程即可。

连接模型

  • Acceptor
    把listen和accept操作封装成为Acceptor。另外Acceptor语义不同于R/W网络I/O的语义,Acceptor多了一个register Handle的操作。
  • Connector
    非阻塞connect的封装,用于client高效连接多个服务器。

并发模型

单线程的事件模型网络库已足解决c10k问题。如果还要再高效的实现,那就要引入多线程并发。

  • thread pool
    当我们遇到计算事务比较多的时候,我们不想让计算阻塞了主线程(I/O)执行,我们可以另开线程专门做计算,再者,我们可以建立线程池,提供给计算线程。
  • one loop per thread
    thread pool只能解决计算量大但网络I/O并不大的网络问题,如果网络I/O非常大的情况,我们可以利用连接模型,把连接和事件分离,主线程只处理连接,Handle事件交由新线程执行。比起只开计算线程而言,事件线程保留了读写I/O的上下文,减少内存切换开销。
  • one loop per process
    同上,多进程
  • one loop per thread + work thread pool
    结合体,分离连接-Handle,同时再把计算事务分离。
  • 异步
    同上节Reactor转Proactor原理一样:利用queue分离Event和Server之间的同步通信。一般用于多server的并发问题。

多线程安全

正如陈硕大大所言,线程同步原语只要用到mutex互斥和condition条件变量即可。而恰好C++11/14/17已经提供完善的线程库:

  • mutex
    只用非递归mutex。配合std::lock_guard使用(std::unique_lock也可)。
  • condition
    std::condition_variable 只可与 std::unique_lock配合使用.std::condition_variable_any可搭配std::shared_lock使用。
  • 读写锁std::shared_mutex
    配合std::shared_lock/std::unique_lock。只要记得std::shared_lock锁读,std::unique_lock锁写即可。

条件变量使用正确方式:std::condition_variable::wait已经考虑了虚假唤醒的情况,故直接调用即可。

#include 
std::condition_variable cv;
std::mutex cv_m;
void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);//可用 std::lock_guard替代
    cv.wait(lk);
}
 
void signals()
{
    std::lock_guard<std::mutex> lk(cv_m);
    cv.notify_one(); //cv.notify_all();
}

std::unique_lock和std::lock_guard的区别是std::unique_lock可以手动加锁解锁,而std::lock_guard自动加锁,粒度较大。

主流语言网络库实现思路

C/C++

可以直接调用操作系统接口。目前已经成熟的网络库都是基于linux的同步I/O来开发,特别是epoll出现使得Reactor模型更加流行。随着Windows的IOCP机制成熟,故也可以借此封装Proactor模型。近几年linux出现io_uring机制,Proactor估计也应该可以大热一下。另外low-level的异步语义std::execution准备进入标准库,异步估计继续火下去。

C#/Java

依赖虚拟机提供的接口。JVM的nio只封装了Reactor;.Net则有着Windows 优良传统,提供完善的IOCP机制。

Golang/JavaScript/Lua/python…

主要是借由c语言库进行api交互。

你可能感兴趣的:(编程,网络,架构)