http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx
陈硕 (giantchen_AT_gmail)
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
我将会写一系列文章,介绍用 muduo 网络库完成常见的 TCP 网络编程任务。目前计划如下:
1. UNP 中的简单协议,包括 echo、daytime、time、discard 等。
2. Boost.Asio 中的示例,包括 timer2~6、chat 等。
3. Java Netty 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 带流量统计
功能。
4. Python twisted 中的示例,包括 finger01~07
5. 用于测试两台机器的往返延迟的 roundtrip
6. 用于测试两台机器的带宽的 pingpong
7. 云风的串并转换连接服务器 multiplexer,包括单线程和多线程两个版本。
8. 文件传输
9. 一个基于 TCP 的应用层广播 hub
10. socks4a 代理服务器,包括简单的 TCP 中继(relay)。
11. 一个 Sudoku 服务器的演变,从单线程到多线程,从阻塞到 event-based。
12. 一个提供短址服务的 httpd 服务器
其中前面 7 个已经放到了 muduo 代码的 examples 目录中,下载地址是:
http://muduo.googlecode.com/files/muduo-0.1.6-alpha.tar.gz
在线阅读:
http://code.google.com/p/muduo/source/browse/#svn%2Ftrunk
我认为,TCP 网络编程最本质的是处理三个半事件:
1. 连接的建立,包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接。
2. 连接的断开,包括主动断开 (close 或 shutdown) 和被动断开 (read 返回 0)。
3. 消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如
何设计等等)。
4. 消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里"发送完毕"是指将数据写入操作系统的缓冲区,将由
TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。
这其中有很多难点,也有很多细节需要注意,比方说:
1. 如果要主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必须的,见下文),那么如何保证先发送完缓
冲区中的数据,然后再断开连接。直接调用 close(2) 恐怕是不行的。
2. 如果主动发起连接,但是对方主动拒绝,如何定期 (带 back-off) 重试?
3. 非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)?(这两个中文术语有其他译法,我选
择了一个电子工程师熟悉的说法。)如果是电平触发,那么什么时候关注 EPOLLOUT 事件?会不会造成 busy-loop?如果是边沿触发,如何
防止漏读造成的饥饿?epoll 一定比 poll 快吗?
4. 在非阻塞网络编程中,为什么要使用应用层缓冲区?假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个
地方,等剩余的数据收到之后再一并处理?见 lighttpd 关于 \r\n\r\n 分包的 bug。假如数据是一个字节一个字节地到达,间隔
10ms,每个字节触发一次文件描述符可读 (readable) 事件,程序是否还能正常工作?lighttpd 在这个问题上出过安全漏洞。
5. 在非阻塞网络编程中,如何设计并使用缓冲区?一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。另一
方面,我们系统减少内存占用。如果有 10k 个连接,每个连接一建立就分配 64k 的读缓冲的话,将占用 640M 内存,而大多数时候这些缓冲区
的使用率很低。muduo 用 readv 结合栈上空间巧妙地解决了这个问题。
6. 如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成内存暴涨?如何做应用层的流量控制?
7. 如何设计并实现定时器?并使之与网络 IO 共用一个线程,以避免锁。
这些问题在 muduo 的代码中可以找到答案。
On Feb 3, 11:45 am, Shuo Chen <giantc...@gmail.com> wrote:
> 前言
>
我编写 Muduo 网络库的目的之一就是简化日常的 TCP 网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和 Sockets
API 较劲。借用 Brooks 的话说,我希望 Muduo 能减少网络编程中的偶发复杂性 (accidental complexity)。
Muduo 只支持 Linux 2.6.x 下的并发非阻塞 TCP 网络编程,它的安装方法见陈硕的 blog 文章。
http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx
Muduo 的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了。
以经典的 echo 回显服务为例:
http://code.google.com/p/muduo/source/browse/#svn%2Ftrunk%2Fexamples%2Fsimple%2Fecho
"业务多迭代几次"是什么意思?有什么问题?
On Feb 3, 1:41 pm, yuan zhu <zy498...@gmail.com> wrote:
> 一直在关注你博客的人飘过.....
>
> 陈大家的这个玩意好像已有小成了?libevent2在1月也终于出stable版了,陈大家好像有比一比的想法?
>
> io并发俺觉得还是蛮重要的,reactor终究只能适合业务瓶颈非io的时候。可是业务多迭代几次的话.......
>
> 当然原生的proator在unix及其后代中的实现好像都比较难,是因为没有一个比较好的内核线程与用户线程通信的方法?
>
> 鄙人陋见,还请诸位大家多多指点。本命年寡居深圳的我,恭祝大家,新春快乐。
>
> 2011/2/3 Shuo Chen <giantc...@gmail.com>
>
> > Muduo 简介
>
> > 我编写 Muduo 网络库的目的之一就是简化日常的 TCP 网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和 Sockets
> > API 较劲。借用 Brooks 的话说,我希望 Muduo 能减少网络编程中的偶发复杂性 (accidental complexity)。
>
> > Muduo 只支持 Linux 2.6.x 下的并发非阻塞 TCP 网络编程,它的安装方法见陈硕的 blog 文章。
> >http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx
>
> > Muduo 的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了。
>
> > 以经典的 echo 回显服务为例:
>
> >http://code.google.com/p/muduo/source/browse/#svn%2Ftrunk%2Fexamples%...
On Feb 3, 10:20 pm, yuan zhu <zy498...@gmail.com> wrote:
> 有时候业务的瓶颈在带宽上,后来加了几个应用,结果cpu又成瓶颈了,很难把2者吞吐量都在保证服务质量的前提下提升到极限。
> 总体来说,用reactor模式始终感觉有些浪费多任务操作系统的并发能力,就像你说的:"...由于 libevent2 每次最多从网络读取 4096
> 字节,大大限制了它的吞吐量。"
>
> 2011/2/3 Shuo Chen <giantc...@gmail.com>
On Feb 3, 10:39 pm, yuan zhu <zy498...@gmail.com> wrote:
> 并发策略如果和线程策略耦合总是感觉不太好。
>
> 关键是想利用好内核的io并发能力,而不是利用2.6内核的内核可抢占特性来在用户进程中模拟并发。
>
> 2011/2/3 Shuo Chen <giantc...@gmail.com>
这是《Muduo 网络编程示例》系列的第二篇文章。
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
本文讲介绍一个与 Boost.Asio 的示例代码中的聊天服务器功能类似的网络服务程序,
包括客户端与服务端的 muduo 实现。这个例子的主要目的是介绍如何处理 TCP 分包,并
初步涉及 Muduo 的多线程功能。Muduo 的下载地址:
http://muduo.googlecode.com/files/muduo-0.1.7-alpha.tar.gz ,本文的完整代码可
在线阅读 http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/
。
*** TCP 分包 ***
前面一篇《五个简单 TCP 协议》中处理的协议没有涉及分包,在 TCP 这种字节流协议
上做应用层分包是网络编程的基本需求。分包指的是在发生一个消息(message)或一
帧(frame)数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个
消息。"粘包问题"是个伪问题。
对于短连接的 TCP 服务,分包不是一个问题,只要发送方主动关闭连接,就表示一条
消息发送完毕,接收方 read() 返回 0,从而知道消息的结尾。例如前一篇文章里的 daytime
和 time 协议。
对于长连接的 TCP 服务,分包有四种方法:
1. 消息长度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字节消息;
2. 使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以 "\r\n" 为字段的分隔符;
3. 在每条消息的头部加一个长度字段,这恐怕是最常见的做法,本文的聊天协议也采用这一办法;
4. 利用消息本身的格式来分包,例如 XML 格式的消息中 <root>...</root> 的配对,或者 JSON 格式中的
{ ... } 的配对。解析这种消息格式通常会用到状态机。
在后文的代码讲解中还会仔细讨论用长度字段分包的常见陷阱。
*** 聊天服务 ***
本文实现的聊天服务非常简单,由服务端程序和客户端程序组成,协议如下:
* 服务端程序中某个端口侦听 (listen) 新的连接;
* 客户端向服务端发起连接;
* 连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来;
* 客户端接受键盘输入,以回车为界,把消息发送给服务端;
* 服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息;
* 一个服务端进程可以同时服务多个客户端进程,当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意
的,不一定哪个客户端会先收到这条消息。
* (可选)如果消息 A 先于消息 B 到达服务端,那么每个客户端都会先收到 A 再收到 B。
这实际上是一个简单的基于 TCP 的应用层广播协议,由服务端负责把消息
发送给每个连接到它的客户端。参与"聊天"的既可以是人,也可以是程序。
在以后的文章中,我将介绍一个稍微复杂的一点的例子 hub,它有"聊天室"
的功能,客户端可以注册特定的 topic(s),并往某个 topic 发送消息,这样
代码更有意思。
消息格式
本聊天服务的消息格式非常简单,"消息"本身是一个字符串,每条消息有
一个 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 字节。
*** 打包的代码 ***
略,请阅读原文,地址见后。
*** 分包的代码 ***
解析数据往往比生成数据复杂,分包打包也不例外。
1: void onMessage(const muduo::net::TcpConnectionPtr& conn,
2: muduo::net::Buffer* buf,
3: muduo::Timestamp receiveTime)
4: {
5: while (buf->readableBytes() >= kHeaderLen)
6: {
7: const void* data = buf->peek();
8: int32_t tmp = *static_cast<const int32_t*>(data);
9: int32_t len = muduo::net::sockets::networkToHost32(tmp);
10: if (len > 65536 || len < 0)
11: {
12: LOG_ERROR << "Invalid length " << len;
13: conn->shutdown();
14: }
15: else if (buf->readableBytes() >= len + kHeaderLen)
16: {
17: buf->retrieve(kHeaderLen);
18: muduo::string message(buf->peek(), len);
19: buf->retrieve(len);
20: messageCallback_(conn, message, receiveTime); // 收到完整的消息,通
知用户
21: }
22: else
23: {
24: break;
25: }
26: }
27: }
上面这段代码第 7 行用了 while 循环来反复读取数据,直到 Buffer 中
的数据不够一条完整的消息。请读者思考,如果换成
if (buf->readableBytes() >= kHeaderLen) 会有什么后果。
以前面提到的两条消息的字节流为例:
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00,
0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'
假设数据最终都全部到达,onMessage() 至少要能正确处理以下各种
数据到达的次序,每种情况下 messageCallback_ 都应该被调用两次:
1. 每次收到一个字节的数据,onMessage() 被调用 21 次;
2. 数据分两次到达,第一次收到 2 个字节,不足消息的长度字段;
3. 数据分两次到达,第一次收到 4 个字节,刚好够长度字段,但是没有 body;
4. 数据分两次到达,第一次收到 8 个字节,长度完整,但 body 不完整;
5. 数据分两次到达,第一次收到 9 个字节,长度完整,body 也完整;
6. 数据分两次到达,第一次收到 10 个字节,第一条消息的长度完整、body 也完整,第二条消息长度不完整;
7. 请自行移动分割点,验证各种情况;
8. 数据一次就全部到达,这时必须用 while 循环来读出两条消息,否则消息会堆积。
请读者验证 onMessage() 是否做到了以上几点。这个例子充分说明了
non-blocking read 必须和 input buffer 一起使用。
继续阅读全文: http://blog.csdn.net/Solstice/archive/2011/02/04/6172391.aspx
On Feb 3, 11:48 am, Shuo Chen <giantc...@gmail.com> wrote:
> Muduo 简介
>
> 我编写 Muduo 网络库的目的之一就是简化日常的 TCP 网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和 Sockets
> API 较劲。借用 Brooks 的话说,我希望 Muduo 能减少网络编程中的偶发复杂性 (accidental complexity)。
>
> Muduo 只支持 Linux 2.6.x 下的并发非阻塞 TCP 网络编程,它的安装方法见陈硕的 blog 文章。http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx
>
> Muduo 的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了。
>
> 以经典的 echo 回显服务为例:http://code.google.com/p/muduo/source/browse/#svn%2Ftrunk%2Fexamples%...
>
陈硕 (giantchen_AT_gmail)
这是《Muduo 网络编程示例》系列的第三篇文章。
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
*** 程序中的时间 ***
程序中对时间的处理是个大问题,我打算单独写一篇文章来全面地讨论这个问题。文章暂定名《〈程序中的日期与时间〉第二章 计时与定时》,跟《〈程序中的
日期与时间〉第一章 日期计算》放到一个系列,这个系列预计会有四篇文章。
在这篇博客里里我先简要谈谈与编程直接相关的内容,把更深入的内容留给上面提到的日期与时间专题文章。
在一般的服务端程序设计中,与时间有关的常见任务有:
1. 获取当前时间,计算时间间隔;
2. 时区转换与日期计算;把纽约当地时间转换为上海当地时间;2011-02-05 之后第 100 天是几月几号星期几?等等
3. 定时操作,比如在预定的时间执行一项任务,或者在一段延时之后执行一项任务。
其中第 2 项看起来复杂,其实最简单。日期计算用 Julian Day Number,时区转换用 tz database;惟一麻烦一点的是夏令
时,但也可以用 tz database 解决。这些操作都是纯函数,很容易用一套单元测试来验证代码的正确性。需要特别注意的是,用 tzset/
localtime_r 来做时区转换在多线程环境下可能会有问题;对此我的解决办法是写一个 TimeZone class,以避免影响全局,将来在
日期与时间专题中会讲到。以下本文不考虑时区,均为 UTC 时间。
真正麻烦的是第 1 项和第 3 项。一方面,Linux 有一大把令人眼花缭乱的与时间相关的函数和结构体,在程序中该如何选用?另一方面,计算机中
的时钟不是理想的计时器,它可能会漂移或跳变;最后,民用的 UTC 时间与闰秒的关系也让定时任务变得复杂和微妙。当然,与系统当前时间有关的操作也
让单元测试变得困难。
** Linux 时间函数 **
Linux 的计时函数,用于获得当前时间:
* time(2) / time_t (秒)
* ftime(3) / struct timeb (毫秒)
* gettimeofday(2) / struct timeval (微秒)
* clock_gettime(2) / struct timespec (纳秒)
* gmtime / localtime / timegm / mktime / strftime / struct tm (这些与当
前时间无关)
定时函数,用于让程序等待一段时间或安排计划任务:
* sleep
* alarm
* usleep
* nanosleep
* clock_nanosleep
* getitimer / setitimer
* timer_create / timer_settime / timer_gettime / timer_delete
* timerfd_create / timerfd_gettime / timerfd_settime
我的取舍如下:
* (计时)只使用 gettimeofday 来获取当前时间。
* (定时)只使用 timerfd_* 系列函数来处理定时。
gettimeofday 入选原因:(这也是 muduo::Timestamp class 的主要设计考虑)
1. time 的精度太低,ftime 已被废弃,clock_gettime 精度最高,但是它系统调用的开销比
gettimeofday 大。
2. 在 x86-64 平台上,gettimeofday 不是系统调用,而是在用户态实现的(搜 vsyscall),没有上下文切换和陷入
内核的开销。
3. gettimeofday 的分辨率 (resolution) 是 1 微秒,足以满足日常计时的需要。
muduo::Timestamp 用一个 int64_t 来表示从 Epoch 到现在的微秒数,其范围可达上下 30 万年。
timerfd_* 入选的原因:
1. sleep / alarm / usleep 在实现时有可能用了信号 SIGALRM,在多线程程序中处理信号是个相当麻烦的事情,应
当尽量避免。(近期我会写一篇博客仔细讲讲"多线程、RAII、fork() 与信号")
2. nanosleep 和 clock_nanosleep 是线程安全的,但是在非阻塞网络编程中,绝对不能用让线程挂起的方式来等待一段
时间,程序会失去响应。正确的做法是注册一个时间回调函数。
3. getitimer 和 timer_create 也是用信号来 deliver 超时,在多线程程序中也会有麻烦。
timer_create 可以指定信号的接收方是进程还是线程,算是一个进步,不过在信号处理函数(signal handler)能做的事情实在很
受限。
4. timerfd_create 把时间变成了一个文件描述符,该"文件"在定时器超时的那一刻变得可读,这样就能很方便地融入到
select/poll 框架中,用统一的方式来处理 IO 事件和超时事件,这也正是 Reactor 模式的长处。我在一年前发表的《Linux
新增系统调用的启示》中也谈到这个想法,现在我把这个想法在 muduo 网络库中实现了。
5. 传统的 Reactor 利用 select/poll/epoll 的 timeout 来实现定时功能,但 poll 和
epoll 的定时精度只有毫秒,远低于 timerfd_settime 的定时精度。
必须要说明,在 Linux 这种非实时多任务操作系统中,在用户态实现完全精确可控的计时和定时是做不到的,因为当前任务可能会被随时切换出去,这
在 CPU 负载大的时候尤为明显。但是,我们的程序可以尽量提高时间精度,必要的时候通过控制 CPU 负载来提高时间操作的可靠性,在程序在
99.99% 的时候都是按预期执行的。这或许比换用实时操作系统并重新编写并测试代码要经济一些。
关于时间的精度(accuracy)问题我留到专题博客文章中讨论,它与分辨率(resolution)不完全是一回事儿。时间跳变和闰秒的影响与应对
也不在此处展开讨论了。
*** Muduo 的定时器接口 ***
Muduo EventLoop 有三个定时器函数:
* runAt 在指定的时间调用 TimerCallback
* runAfter 等一段时间调用 TimerCallback
* runEvery 以固定的间隔反复调用 TimerCallback
* cancel 取消 timer,目前未实现
回调函数在 EventLoop 对象所在的线程发生,与 onMessage() onConnection() 等网络事件函数在同一个线程。
Muduo 的 TimerQueue 采用了最简单的数据结构(链表)来管理定时器,它的效率比不上常见的 binary heap 的做法,如果程
序中大量(10 个以上)使用重复触发的定时器,或许值得考虑改用更高级的实现。我目前还没有在一个程序里用过这么多定时器,暂时也不打算优化
TimerQueue。
*** Boost.Asio Timer 示例 ***
Boost.Asio 教程里以 Timer 和 Daytime 为例介绍 asio 的基本使用,daytime 已经在前文"示例一"中介绍过,
这里着重谈谈 Timer。Asio 有 5 个 Timer 示例,muduo 把其中四个重新实现了一遍,并扩充了第 5 个示例。
1. 阻塞式的定时,muduo 不支持这种用法,无代码。
2. 非阻塞定时,见 examples/asio/tutorial/timer2
3. 在 TimerCallback 里传递参数,见 examples/asio/tutorial/timer3
4. 以成员函数为 TimerCallback,见 examples/asio/tutorial/timer4
5. 在多线程中回调,用 mutex 保护共享变量,见 examples/asio/tutorial/timer5
6. 在多线程中回调,缩小临界区,把不需要互斥执行的代码移出来,见 examples/asio/tutorial/timer6
继续阅读全文: http://blog.csdn.net/Solstice/archive/2011/02/06/6173563.aspx
--
Sent from my mobile device
*Regards,
Linker Lin
linker...@gmail.com*
2011/2/8 Linker <linker...@gmail.com>:
--
welcom to gtalk me
http://hi.baidu.com/jyf1987