1. 说明
-
本例子是与 Boost.Asio 的示例代码中的聊天服务器功能类似的网络服务程序,主要目的是介绍如何处理分包,并初步涉及 Muduo 的多线程功能。代码位于 examples/asio/chat/
-
一共有四个版本的服务器
- server.cc 单线程
- server_threaded.cc 多线程TcpServer,并用mutex来保护共享数据mutex
- server_threaded_efficient.cc 借shared_ptr实现copy-on-write的手法来降低锁竞争
- server_threaded_highperformance.cc 采用thread local变量实现多线程高效转发
2. 分包问题
- 分包指的是在发生一个消息(message) 或一帧 (frame) 数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个消息。“粘包问题”是个伪问题。
短连接分包
- 对于短连接的 TCP 服务,分包不是一个问题,只要发送方主动关闭连接,就表示一条消息发送完毕,接收方 read() 返回 0,从而知道消息的结尾。
长连接分包
- 对于长连接的 TCP 服务,分包有四种方法:
-
消息长度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字节消息;
-
使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以”\r\n” 为字段的分隔符;
-
在每条消息的头部加一个长度字段,这恐怕是最常见的做法,本例的聊天协议也采用这一办法;
-
利用消息本身的格式来分包,例如 XML 格式的消息中 … 的配对,或者 JSON 格式中的 { … } 的配对。解析这种消息格式通常会用到状态机。
3. 聊天服务
-
协议
-
服务端程序中某个端口侦听 (listen) 新的连接;
-
客户端向服务端发起连接;
-
连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来;
-
客户端接受键盘输入,以回车为界,把消息发送给服务端;
-
服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息;
-
一个服务端进程可以同时服务多个客户端进程。当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意的,不一定哪个客户端会先收到这条消息。
-
(可选)如果消息 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
-
onConnection()中把连接对象加入队列
-
消息回调绑定LengthHeaderCodec的onMessage,这个函数里在解析玩网络消息后会回调server类中的onStringMessage(),这里有会调用LengthHeaderCodec的send(),把主机信息打包发送,就是用一个中间层负责打包和分包
2. server_threaded.cc
- 多线程,代码变得不多,只是多了句server_.setThreadNum(numThreads);设置线程数量,这就是封装好的代码的好处吧…还有在需要的地方加锁…
3. server_threaded_efficient.cc
-
借shared_ptr实现copy-on-write的手法来降低锁竞争,比如在服务器给所有连接发消息的同时,又有一个连接连上了,那要不要给这个连接发消息?不要,但是这个连接可能加入到了发送连接列表的最后.在上一个版本中是采用发消息的时候加锁.这里使用share_ptr实现COW
-
copy-on-write:对一个东西修改时候,不是在原来的容器中修改,而是通过复制一个新的容器,把内容复制到新的容器中,并加以修改,保存后再替换到原来的容器中
-
这里的逻辑就是,发消息的时候,用一个新的连接列表指针(在onStringMessage()函数中),在新连接到来的回调函数中,判断连接列表指针是否只有一个,如果不止一个,说明有其他线程正在onStringMessage()函数中发送消息,这时候,就把原来的share_ptr重置成一个新的指向所有连接的指针,这时候计数又变成了1,然后再把新连接加进队列.
4. server_threaded_highperformance.cc
-
采用thread local变量实现多线程高效转发,每个线程都有一个ConnectionList连接列表,多了一个EventLoop列表
-
首先线程启动的时候调用threadInit()把当前线程所属的EventLoop加入EventLoop列表
-
每个线程都有connection实例,所以在onConnection()连接到来时不用锁保护,前两个都需要锁
-
在onStringMessage()消息到来处理函数中,并没有转发消息,而是把消息转发的任务distributeMessage()加到事件循环列表,让对应的IO线程来执行转发任务,从而达到以下效果:
- 前面的例子是,Client1给Server发了消息,让这个线程给其他所有Client转发
- 现在实现的效果是Client1个Server发了消息,然后Thread1给Client1发,Thread2给Client2发…这样降低了一条消息发给所有客户端的延迟
-
之前我对onStringMessage()中的循环代码不理解,不明白为什么每个EventLoop都要加入这个分发消息的任务,后来才想明白:因为每个线程的connection列表是局部的,都是不一样的,所以每个线程的EventLoop都处理分发消息任务(distributeMessage()),给每个连接都发送了消息