48.muduo学习笔记之example_聊天服务器例子

1. 说明

  1. 本例子是与 Boost.Asio 的示例代码中的聊天服务器功能类似的网络服务程序,主要目的是介绍如何处理分包,并初步涉及 Muduo 的多线程功能。代码位于 examples/asio/chat/

  2. 一共有四个版本的服务器

    • server.cc 单线程
    • server_threaded.cc 多线程TcpServer,并用mutex来保护共享数据mutex
    • server_threaded_efficient.cc 借shared_ptr实现copy-on-write的手法来降低锁竞争
    • server_threaded_highperformance.cc 采用thread local变量实现多线程高效转发

2. 分包问题

  1. 分包指的是在发生一个消息(message) 或一帧 (frame) 数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个消息。“粘包问题”是个伪问题。

短连接分包

  1. 对于短连接的 TCP 服务,分包不是一个问题,只要发送方主动关闭连接,就表示一条消息发送完毕,接收方 read() 返回 0,从而知道消息的结尾。

长连接分包

  • 对于长连接的 TCP 服务,分包有四种方法:
  1. 消息长度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字节消息;

  2. 使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以”\r\n” 为字段的分隔符;

  3. 在每条消息的头部加一个长度字段,这恐怕是最常见的做法,本例的聊天协议也采用这一办法;

  4. 利用消息本身的格式来分包,例如 XML 格式的消息中 … 的配对,或者 JSON 格式中的 { … } 的配对。解析这种消息格式通常会用到状态机。

3. 聊天服务

  • 协议

    1. 服务端程序中某个端口侦听 (listen) 新的连接;

    2. 客户端向服务端发起连接;

    3. 连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来;

    4. 客户端接受键盘输入,以回车为界,把消息发送给服务端;

    5. 服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息;

    6. 一个服务端进程可以同时服务多个客户端进程。当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意的,不一定哪个客户端会先收到这条消息。

    7. (可选)如果消息 A 先于消息 B 到达服务端,那么每个客户端都会先收到 A 再收到 B

  • 这实际上是一个简单的基于 TCP 的应用层广播协议,由服务端负责把消息发送给每个连接到它的客户端。参与“聊天”的既可以是人,也可以是程序。

4. 消息格式

  • “消息”本身是一个字符串,每条消息的有一个 4 字节的头部,以网络序存放字符串的长度。消息之间没有间隙,字符串也不一定以 ’\0’ 结尾。比方说有两条消息”hello” 和”chenshuo”,那么打包后的字节流是:0x00, 0x00, 0x00, 0x05, ’h’, ’e’, ’l’, ’l’, ’o’, 0x00, 0x00, 0x00, 0x08, ’c’, ’h’, ’e’, ’n’, ’s’, ’h’, ’u’, ’o’共 21 字节

  • “打包"和"分包”

    • "打包"就是把消息打包发送给对方,主要是send()函数
    • “分包"就是把收到的数据解析成"消息”,再做处理,重要是onMessage()函数
  • 编解码器 LengthHeaderCodec

    • 让用户代码只关心“消息到达”而不是“数据到达”,负责消息的打包和分包工作

5. 服务端的实现

1. server.cc

  1. onConnection()中把连接对象加入队列

  2. 消息回调绑定LengthHeaderCodec的onMessage,这个函数里在解析玩网络消息后会回调server类中的onStringMessage(),这里有会调用LengthHeaderCodec的send(),把主机信息打包发送,就是用一个中间层负责打包和分包

2. server_threaded.cc

  1. 多线程,代码变得不多,只是多了句server_.setThreadNum(numThreads);设置线程数量,这就是封装好的代码的好处吧…还有在需要的地方加锁…

3. server_threaded_efficient.cc

  1. 借shared_ptr实现copy-on-write的手法来降低锁竞争,比如在服务器给所有连接发消息的同时,又有一个连接连上了,那要不要给这个连接发消息?不要,但是这个连接可能加入到了发送连接列表的最后.在上一个版本中是采用发消息的时候加锁.这里使用share_ptr实现COW

  2. copy-on-write:对一个东西修改时候,不是在原来的容器中修改,而是通过复制一个新的容器,把内容复制到新的容器中,并加以修改,保存后再替换到原来的容器中

  3. 这里的逻辑就是,发消息的时候,用一个新的连接列表指针(在onStringMessage()函数中),在新连接到来的回调函数中,判断连接列表指针是否只有一个,如果不止一个,说明有其他线程正在onStringMessage()函数中发送消息,这时候,就把原来的share_ptr重置成一个新的指向所有连接的指针,这时候计数又变成了1,然后再把新连接加进队列.

4. server_threaded_highperformance.cc

  1. 采用thread local变量实现多线程高效转发,每个线程都有一个ConnectionList连接列表,多了一个EventLoop列表

  2. 首先线程启动的时候调用threadInit()把当前线程所属的EventLoop加入EventLoop列表

  3. 每个线程都有connection实例,所以在onConnection()连接到来时不用锁保护,前两个都需要锁

  4. 在onStringMessage()消息到来处理函数中,并没有转发消息,而是把消息转发的任务distributeMessage()加到事件循环列表,让对应的IO线程来执行转发任务,从而达到以下效果:

    • 前面的例子是,Client1给Server发了消息,让这个线程给其他所有Client转发
    • 现在实现的效果是Client1个Server发了消息,然后Thread1给Client1发,Thread2给Client2发…这样降低了一条消息发给所有客户端的延迟
  5. 之前我对onStringMessage()中的循环代码不理解,不明白为什么每个EventLoop都要加入这个分发消息的任务,后来才想明白:因为每个线程的connection列表是局部的,都是不一样的,所以每个线程的EventLoop都处理分发消息任务(distributeMessage()),给每个连接都发送了消息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值