﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-陈硕的Blog-随笔分类-muduo</title><link>http://www.cppblog.com/Solstice/category/15951.html</link><description /><language>zh-cn</language><lastBuildDate>Fri, 01 Nov 2013 03:02:27 GMT</lastBuildDate><pubDate>Fri, 01 Nov 2013 03:02:27 GMT</pubDate><ttl>60</ttl><item><title>《Linux 多线程服务端编程：使用 muduo C++ 网络库》电子版上市</title><link>http://www.cppblog.com/Solstice/archive/2013/11/01/204039.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Fri, 01 Nov 2013 02:35:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2013/11/01/204039.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/204039.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2013/11/01/204039.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/204039.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/204039.html</trackback:ping><description><![CDATA[<p>《Linux 多线程服务端编程：使用 muduo C++ 网络库》 电子版已在京东上市销售。</p>
<p>购买地址：<a href="http://e.jd.com/30149978.html">http://e.jd.com/30149978.html</a></p>
<p>阅读效果：</p>
<h2>PC</h2>
<p><a href="https://chenshuo-public.s3.amazonaws.com/pdf/ebookpc.png"><img title="ebookpc" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="ebookpc" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Linux--muduo-C-_11133/ebookpc_thumb.png" width="707" height="768" /></a></p>
<h2>iPad</h2>
<p><a href="https://chenshuo-public.s3.amazonaws.com/pdf/ebookipad.png"><img title="ebookipad" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="ebookipad" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Linux--muduo-C-_11133/ebookipad_thumb.png" width="576" height="768" /></a></p>
<p>目前京东的阅读器没有切白边功能，值得改进。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/204039.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2013-11-01 10:35 <a href="http://www.cppblog.com/Solstice/archive/2013/11/01/204039.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>用muduo实现memcached协议的例子</title><link>http://www.cppblog.com/Solstice/archive/2013/08/18/202613.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sun, 18 Aug 2013 04:59:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2013/08/18/202613.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/202613.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2013/08/18/202613.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/202613.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/202613.html</trackback:ping><description><![CDATA[<p>最近花了两天时间用 muduo 部分实现了 memcached 服务器协议，代码位于 examples/memcached/server，能通过 memcached 的大部分测试用例（incr/decr 还没有实现）。</p> <p>这不是 memcached 的替代品（它没有实现LRU和超时功能，也没有实现二进制协议，更没有自己管理内存），而是一个网络编程的示例（代码只有 1000 行，比 memcached 小很多），展示 muduo 风格的事件驱动编程，以及将来性能优化的试验品（换句话说，现在这个版本完全没有在性能上做出任何努力）。读过 memcached 代码的人可以对比这两种编程风格的区别，memcached 的 read/write 操作穿插于正常逻辑处理，而 muduo 的网络数据读写是由库完成，应用程序只关心消息收发，目前二者的基本 get/set 操作的性能相当。</p> <p>现在 muduo 的 inspector 内置了 gperftools 的远程 profiling 功能，memcached-debug 展示了其用法。</p> <p>为什么不必优化 set 操作（含 set/add/update/append/prepend/cas 等）的性能？</p> <p>1. 比例。既然是 memcache，那么 get:set 的比例很高，10:1 甚至更高，因此优化的重心应该是 get 而非 set。</p> <p>假设 memcached 能处理 100k QPS，再假设这些操作都是 set（其实应该不到 10% 是 set），再假设所有的 set 都是串行执行的（没有并发），那么每次 set 的 CPU 时间不应该超过 10 us（含服务器本地的网络代码运行时间，但不含网络延迟）。而实际上一次 set 的 CPU 时间最多是 2~3 us （用 memcached-footprint 程序测得），根本不值得优化。</p> <p>2. 网络带宽。假设一次 set 操作的 key + value 的长度是 1k bytes，TCP 的有效载荷带宽按110MB/s估算，那么1kB数据在千兆网上的惯性延迟是 9us（传输延迟是几十上百微秒，与此无关），也就是说服务器的网卡收到这 1kB 数据需要花 9us 时间（从第一个字节到达到服务器到收完最后一个字节），那么在 set 耗时 2~3 us 的情况下再去优化它是做无用功。</p> <p>3. 产生&#8220;需要更新的数据&#8221;的成本远大于 memcached set 的开销。memcached 需要更新，往往是将已写入数据库的新数据放到 memcached 中，那么写数据库的开销远远大于 memcached set 的开销，优化 set 对提升系统整体性能没意义。</p><img src ="http://www.cppblog.com/Solstice/aggbug/202613.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2013-08-18 12:59 <a href="http://www.cppblog.com/Solstice/archive/2013/08/18/202613.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>muduo多机协作网络编程示例一：单词计数及排序</title><link>http://www.cppblog.com/Solstice/archive/2013/01/13/197220.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sat, 12 Jan 2013 20:01:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2013/01/13/197220.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/197220.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2013/01/13/197220.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/197220.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/197220.html</trackback:ping><description><![CDATA[<p>去年我写了《Muduo 网络编程示例》系列文章，这些文章已经收入《<a href="http://blog.csdn.net/solstice/article/details/8493251">Linux 多线程服务端编程：使用 muduo C++ 网络库</a>》一书。这些文章讲的基本都是运行在单机上的网络程序，每个例子都只有一个程序（第7.13节例外）。我接下来打算继续写几篇文章，谈一谈分布在多台机器上、协作发挥作用的网络编程例子。</p> <p>今天先讲第一个，单词计数及排序。单词计数（word count），顾名思义就是统计一个文本文件里边每个词出现了多少次。排序指的是按出现次数从多到少排序，也可以把问题改为&#8220;找出出现次数最多的1000个单词&#8221;。</p> <p>这个问题有三个层次，第一是输入文件比较小，能完全放入内存；第二是输入文件比较大，不能一次性都放入内存；第三是输入文件分布在多台机器上，这需要用到网络编程。</p> <p>第一个层次很好解决，几十行代码就搞定了。<a title="https://gist.github.com/4519962" href="https://gist.github.com/4519962">https://gist.github.com/4519962</a></p> <p>第二个层次不难解决，基本思路是分而治之，先hash分块统计单词出现次数，将每一块按出现次数排序，最后归并。代码见 <a title="https://github.com/chenshuo/recipes/blob/master/puzzle/query_freq.cc" href="https://github.com/chenshuo/recipes/blob/master/puzzle/query_freq.cc">https://github.com/chenshuo/recipes/blob/master/puzzle/query_freq.cc</a> ，分析见 <a title="http://www.cnblogs.com/baiyanhuang/archive/2012/11/11/2764914.html" href="http://www.cnblogs.com/baiyanhuang/archive/2012/11/11/2764914.html">http://www.cnblogs.com/baiyanhuang/archive/2012/11/11/2764914.html</a> 。</p> <p>第三个层次也不难，可以当做网络编程的练习来做。如果有合适的框架，可以轻松解决，因为单词计数是map reduce的经典范例，对出现次数排序也可以再用一步map reduce搞定（估计需要一个好的 shuffle 函数，简单hash是不行的）。</p> <p>如果用普通网络编程，一种设计思路如下图，其中方框代表机器，椭圆代表输入输出文件，圆角矩形代表进程。思路跟第二个层次一样，先hash到多个shard文件（由hasher和receiver负责），再对每个shard文件排序（由sender负责），最后归并（merger）。</p> <p><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="topk" border="0" alt="topk" src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduo_124A1/topk_3.gif" width="733" height="552" /> </p> <p>注意这种思路适合求top K元素，不适合按出现次数排序全部单词，因为最终结果收集在一台机器上。目前这个sender实现的一个限制是，每个shard必须能全部放入内存，因为sender对shard排序是在内存中进行的。如果数据更大，还需要实现单机外部排序。</p> <p>图中hasher和receiver的代码见muduo示例中的 <a href="https://github.com/chenshuo/muduo/tree/master/examples/wordcount">muduo/examples/wordcount</a> ；sender和merger的代码见 <a title="https://github.com/chenshuo/recipes/tree/master/topk" href="https://github.com/chenshuo/recipes/tree/master/topk">https://github.com/chenshuo/recipes/tree/master/topk</a> 。注意merger没有使用muduo，而是采用阻塞网络编程。有兴趣的读者可以思考其背后的原因。要想发挥 merger 正常的性能，需要修改 /usr/include/boost/asio/basic_socket_streambuf.hpp ，增大缓冲区，即 enum { buffer_size = 8192 };</p> <p>这可以看作是map reduce的原始实现，或者说用map reduce的思想手写了一些原始工具。如果把map reduce比作C语言，这里的几个程序相当于汇编写的函数。</p> <p>以后我再写一个按出现次数全排序的例子吧，需要替换这里的sender和merger。</p> <p>(.完.)</p><img src ="http://www.cppblog.com/Solstice/aggbug/197220.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2013-01-13 04:01 <a href="http://www.cppblog.com/Solstice/archive/2013/01/13/197220.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>《Linux 多线程服务端编程：使用 muduo C++ 网络库》网上书店预订</title><link>http://www.cppblog.com/Solstice/archive/2013/01/11/197192.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Fri, 11 Jan 2013 04:43:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2013/01/11/197192.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/197192.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2013/01/11/197192.html#Feedback</comments><slash:comments>6</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/197192.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/197192.html</trackback:ping><description><![CDATA[<h1>内容简介 </h1> <p>本书主要讲述采用现代 C++ 在 x86-64 Linux 上编写多线程 TCP 网络服务程序的主流常规技术，重点讲解一种适应性较强的多线程服务器的编程模型，即 one loop per thread。这是在 Linux 下以 native 语言编写用户态高性能网络程序最成熟的模式，掌握之后可顺利地开发各类常见的服务端网络应用程序。本书以 muduo 网络库为例，讲解这种编程模型的使用方法及注意事项。  </p><p>本书的宗旨是贵精不贵多。掌握两种基本的同步原语就可以满足各种多线程同步的功能需求，还能写出更易用的同步设施。掌握一种进程间通信方式和一种多线程网络编程模型就足以应对日常开发任务，编写运行于公司内网环境的分布式服务统。  </p><h1>基本信息 </h1> <p><a href="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/LinuxmuduoC_12191/cover_2.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="cover" border="0" alt="cover" src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/LinuxmuduoC_12191/cover_thumb.jpg" width="370" height="480" /></a>  </p><p>出版社：电子工业出版社  </p><p>页数：xvi+600  </p><p>定价：人民币89元  </p><p>ISBN：9787121192821  </p><h1>豆瓣及网上书店预订</h1> <p>豆瓣：<a href="http://book.douban.com/subject/20471211/">http://book.douban.com/subject/20471211/</a> <br />互动：<a href="http://product.china-pub.com/3021861">http://product.china-pub.com/3021861</a> <br />亚马逊：<a href="http://www.amazon.cn/dp/B00AYS2KL0">http://www.amazon.cn/dp/B00AYS2KL0</a> <br />当当：<a href="http://product.dangdang.com/product.aspx?product_id=23162953">http://product.dangdang.com/product.aspx?product_id=23162953</a> <br />京东：<a href="http://book.360buy.com/11163782.html">http://book.360buy.com/11163782.html</a>  </p><h1>试读样章</h1> <p>前言与目录：<a href="https://chenshuo-public.s3.amazonaws.com/pdf/preamble.pdf">https://chenshuo-public.s3.amazonaws.com/pdf/preamble.pdf</a><br />第1章：线程安全的对象生命期管理：<a href="https://chenshuo-public.s3.amazonaws.com/pdf/chap1.pdf">https://chenshuo-public.s3.amazonaws.com/pdf/chap1.pdf</a><br />第6章：muduo网络库简介：<a href="https://chenshuo-public.s3.amazonaws.com/pdf/chap6.pdf">https://chenshuo-public.s3.amazonaws.com/pdf/chap6.pdf</a><br />附录：<a href="https://chenshuo-public.s3.amazonaws.com/pdf/appendix.pdf">https://chenshuo-public.s3.amazonaws.com/pdf/appendix.pdf</a> <br />样章合集下载：<a href="http://vdisk.weibo.com/s/mtupb">http://vdisk.weibo.com/s/mtupb</a> 共150页，包括第 11.5 节。  </p><h1>前言（节选）</h1> <p>本书主要讲述采用现代 C++ 在 x86-64 Linux 上编写多线程 TCP 网络服务程序的主流常规技术，这也是我对过去 5 年编写生产环境下的多线程服务端程序的经验总结。本书重点讲解多线程网络服务器的一种 IO 模型，即 one loop per thread。这是一种适应性较强的模型，也是 Linux 下以 native 语言编写用户态高性能网络程序最成熟的模式， 掌握之后可顺利地开发各类常见的服务端网络应用程序。本书以 muduo 网络库为例，讲解这种编程模型的使用方法及注意事项。</p> <p>muduo 是一个基于非阻塞 IO 和事件驱动的现代 C++ 网络库，原生支持 one loop per thread 这种 IO 模型。muduo 适合开发 Linux 下的面向业务的多线程服务端网络应用程序，其中&#8220;面向业务的网络编程&#8221;的定义见附录 A。 &#8220;现代 C++&#8221;指的不是 C++11 新标准，而是 2005 年 TR1 发布之后的 C++ 语言和库。 与传统 C++ 相比，现代 C++ 的变化主要有两方面：资源管理（见第 1 章）与事件回调（见第 449 页）。</p> <p>本书不是多线程编程教程，也不是网络编程教程，更不是 C++ 教程。读者应该已经大致读过《UNIX 环境高级编程》、《UNIX 网络编程》、《C++ Primer》或与之内容相近的书籍。本书不谈 C++11，因为目前（2012 年）主流的 Linux 服务端发行版的 g++ 版本都还停留在 4.4，C++11 进入实用尚需一段时日。 </p> <p>本书适用的硬件环境是主流 x86-64 服务器，多路多核 CPU、几十 GB 内存、千兆以太网互联。除了第 5 章讲诊断日志之外，本书不涉及文件 IO。  </p><p>本书分为四大部分，第 1 部分&#8220;C++ 多线程系统编程&#8221;考察多线程下的对象生命期管理、线程同步方法、多线程与 C++ 的结合、高效的多线程日志等。第 2 部分&#8220;muduo 网络库&#8221;介绍使用现成的非阻塞网络库编写网络应用程序的方法，以及 muduo 的设计与实现。第 3 部分&#8220;工程实践经验谈&#8221;介绍分布式系统的工程化开发方法和 C++ 在工程实践中的功能特性取舍。第 4 部分&#8220;附录&#8221;分享网络编程和 C++ 语言的学习经验。  </p><p>本书的宗旨是贵精不贵多。掌握两种基本的同步原语就可以满足各种多线程同步的功能需求，还能写出更易用的同步设施。掌握一种进程间通信方式和一种多线程网络编程模型就足以应对日常开发任务，编写运行于公司内网环境的分布式服务系统。（本书不涉及分布式存储系统，也不涉及 UDP。）  </p><h2>术语与排版范例</h2> <p><br />本书大量使用英文术语，甚至有少量英文引文。设计模式的名字一律用英文，例如 Observer、Reactor、Singleton。在中文术语不够突出时，也会使用英文，例如 class、heap、event loop、STL algorithm 等。注意几个中文 C++ 术语：对象实体（instance） 、函数重载决议（resolution） 、模板具现化（instantiation） 、覆写（override）虚函数、提领（dereference）指针。本书中的英语可数名词一般不用复数形式，例如两个 class，6 个 syscall；但有时会用 (s) 强调中文名词是复数。fd 是文件描述符（file descriptor）的缩写。&#8220;CPU 数目&#8221;一般指的是核（core）的数目。用诸如&#167;11.5 表示本书第 11.5 节，L42 表示上下文中出现的第 42 行代码。[JCP]、[CC2e] 等是参考文献，见书末清单。  </p><h2>代码 </h2> <p>本书的示例代码以开源项目的形式发布在 GitHub 上，<br />地址是 <a href="http://github.com/chenshuo/recipes/">http://github.com/chenshuo/recipes/</a> 和 <a href="http://github.com/chenshuo/muduo/">http://github.com/chenshuo/muduo/</a> 。本书配套页面提供全部源代码打包下载，正文中出现的类似 recipes/thread 的路径是压缩包内的相对路径，读者不难找到其对应的 GitHub URL。  </p><p>本书假定读者熟悉 diff -u 命令的输出格式，用于表示代码的改动。  </p><p>本书正文中出现的代码有时为了照顾排版而略有改写，例如改变缩进规则，去掉单行条件语句前后的花括号等。就编程风格而论，应以电子版代码为准。  </p><h2>联系方式</h2> <p><br />邮箱：giantchen_at_gmail.com  </p><p>主页：<a href="http://chenshuo.com/book">http://chenshuo.com/book</a> （正文和脚注中出现的 URL 可从这里找到。 ）  </p><p>微博：<a href="http://weibo.com/giantchen">http://weibo.com/giantchen</a>  </p><p>博客：<a href="http://blog.csdn.net/Solstice">http://blog.csdn.net/Solstice</a>  </p><p>代码：<a href="http://github.com/chenshuo">http://github.com/chenshuo</a>  </p><p>陈硕  </p><p>中国&#8226;香港 </p><img src ="http://www.cppblog.com/Solstice/aggbug/197192.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2013-01-11 12:43 <a href="http://www.cppblog.com/Solstice/archive/2013/01/11/197192.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>新书预告：《Linux 多线程服务端编程——使用 muduo C++ 网络库》</title><link>http://www.cppblog.com/Solstice/archive/2012/09/21/191440.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Thu, 20 Sep 2012 23:20:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2012/09/21/191440.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/191440.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2012/09/21/191440.html#Feedback</comments><slash:comments>9</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/191440.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/191440.html</trackback:ping><description><![CDATA[<span style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; ">看完了 W. Richard Stevens 的传世经典《UNIX 网络编程》， 能照着例子用 Sockets API 编写 echo 服务， 却仍然对稍微复杂一点的网络编程任务感到无从下手？ 书中示例代码把业务逻辑和 Sockets 调用混在一起，似乎不利于将来扩展？</span><br style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; " /><ul style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; "><li>程序在本机测试正常，放到网络运行上就经常出现数据收不全的情况？</li><li>TCP 协议真的有所谓的&#8220;粘包问题&#8221;吗？该如何设计打包拆包的协议？又该如何编码实现才不会掉到陷阱里？</li><li>带外数据（OOB）、信号驱动IO这些高级特性到底有没有用？</li><li>网络协议格式该怎么设计？发送 C struct 会有对齐方面的问题吗？对方不用 C/C++ 怎么通信？ 将来服务端软件升级，需要在协议中增加一个字段，现有的客户端就必须强制升级？</li><li>要处理几千上万的并发连接，似乎书上讲的传统 fork() 模型应付不过来，该用哪种并发模型呢？ 试试 select、poll、epoll 这种 IO 复用模型吧，又感觉非阻塞IO陷阱重重，怎么程序的 CPU 使用率一直是100%？</li><li>要不改用现成的 libevent 网络库吧，怎么查询一下数据库就把其他连接上的请求给耽误了？ 再用个线程池吧。万一发回响应的时候对方已经断开连接了怎么办？会不会串话？</li><li>读过《UNIX 环境高级编程》，想用多线程来发挥多核 CPU 的效率， 但对程序该用哪种多线程模型感到一头雾水？ 有没有值得推荐的适用面广的多线程 IO 模型？ 互斥器、条件变量、读写锁、信号量这些底层同步原语哪些该用哪些不该用？ 有没有更高级的同步设施能简化开发？ 《UNIX 网络编程（第二卷）》介绍的那些琳琅满目的IPC机制到底用哪个才能兼顾开发效率与可伸缩性？</li></ul><span style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; ">网络编程和多线程编程的基础打得差不多，开始实际做项目了，更多问题扑面而来：</span><br style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; " /><ul style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; "><li>网上听人说服务端开发要做到 7x24 运行，为了防止内存碎片连动态内存分配都不能用， 那岂不是连 C++ STL 也一并禁用了？硬件的可靠性高到值得去这么做吗？</li><li>传闻服务端开发主要通过日志来查错，那么日志里该写些什么？日志是写给谁看的？怎样写日志才不会影响性能？</li><li>分布式系统跟单机多进程到底有什么本质区别？心跳协议为什么是必须的，该如何实现？</li><li>C++ 的大型工程该如何管理？库的接口如何设计才能保证升级的时候不破坏二进制兼容性？</li></ul><p style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; ">这本《Linux 多线程服务端编程》中，作者凭借多年的工程实践经验试图解答以上疑问。当然，内容还远不止这些&#8230;&#8230;</p><p style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; "><br /></p><p style="color: #333333; font-family: Arial; line-height: 26px; background-color: #ffffff; ">本书配套页面：&nbsp;<a href="http://chenshuo.com/book" style="color: #336699; text-decoration: none; ">http://chenshuo.com/book</a>&nbsp;，将不定期更新。</p><img src ="http://www.cppblog.com/Solstice/aggbug/191440.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2012-09-21 07:20 <a href="http://www.cppblog.com/Solstice/archive/2012/09/21/191440.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>《Muduo 网络库：现代非阻塞C++网络编程》演讲</title><link>http://www.cppblog.com/Solstice/archive/2012/07/01/181058.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sun, 01 Jul 2012 15:55:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2012/07/01/181058.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/181058.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2012/07/01/181058.html#Feedback</comments><slash:comments>29</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/181058.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/181058.html</trackback:ping><description><![CDATA[<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">2012年6月30日下午将在深圳做《Muduo 网络库：现代非阻塞C++网络编程》演讲，</p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">这是PPT：</p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><a href="http://www.slideshare.net/chenshuo/muduo-network-library" style="color: #336699; text-decoration: none; ">http://www.slideshare.net/chenshuo/muduo-network-library</a><br /></p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">演讲视频：</p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><a href="http://v.youku.com/v_show/id_XNDIyNDc5MDMy.html" style="color: #336699; text-decoration: none; ">http://v.youku.com/v_show/id_XNDIyNDc5MDMy.html</a><br /></p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">http://youtu.be/YDnCAs894Bg&nbsp;<br /></p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><img src="http://my.csdn.net/uploads/201206/29/1340983249_5956.png" alt="" style="border: none; " /><br /></p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">活动介绍：</p><p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><a href="http://ouropensource.51qiangzuo.com/" style="color: #336699; text-decoration: none; ">http://ouropensource.51qiangzuo.com/</a></p><img src ="http://www.cppblog.com/Solstice/aggbug/181058.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2012-07-01 23:55 <a href="http://www.cppblog.com/Solstice/archive/2012/07/01/181058.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>发布一个适合服务端C++程序的高效日志库</title><link>http://www.cppblog.com/Solstice/archive/2012/06/06/177828.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 06 Jun 2012 13:27:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2012/06/06/177828.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/177828.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2012/06/06/177828.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/177828.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/177828.html</trackback:ping><description><![CDATA[<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">PPT 见&nbsp;<a href="http://www.slideshare.net/chenshuo/efficient-logging-in-multithreaded-c-server/" style="color: #336699; text-decoration: none; ">http://www.slideshare.net/chenshuo/efficient-logging-in-multithreaded-c-server/<br />
</a></p>
<p style="color: #333333; ">2012年6月30日在深圳的简短演讲：</p><p style="color: #333333; "><a href="http://v.youku.com/v_show/id_XNDIyMjUwMDYw.html" style="color: #336699; text-decoration: none; ">http://v.youku.com/v_show/id_XNDIyMjUwMDYw.html</a><br />
</p>
<p style="color: #333333; ">http://www.youtube.com/watch?v=KM_eQ6uRYdU</p>
<p>&nbsp;</p>
<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><img src="http://my.csdn.net/uploads/201206/06/1338988862_8584.PNG" width="640" height="480" alt="" style="border: none; " /><br />
</p>
<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">代码位于 muduo 网络库中的 muduo/base</p>
<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; "><a href="https://github.com/chenshuo/muduo" style="color: #336699; text-decoration: none; ">https://github.com/chenshuo/muduo</a><br />
</p>
<p style="color: #333333; font-family: Arial; line-height: 26px; text-align: left; background-color: #ffffff; ">muduo 0.5.0 也包含了这个日志库&nbsp;<a href="http://code.google.com/p/muduo/" style="color: #336699; text-decoration: none; ">http://code.google.com/p/muduo/</a></p><img src ="http://www.cppblog.com/Solstice/aggbug/177828.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2012-06-06 21:27 <a href="http://www.cppblog.com/Solstice/archive/2012/06/06/177828.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>关于 TCP 并发连接的几个思考题与试验</title><link>http://www.cppblog.com/Solstice/archive/2011/07/01/149895.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Fri, 01 Jul 2011 04:50:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/07/01/149895.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/149895.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/07/01/149895.html#Feedback</comments><slash:comments>7</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/149895.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/149895.html</trackback:ping><description><![CDATA[<p>陈硕 (giantchen AT gmail)</p> <p>blog.csdn.net/Solstice</p> <p>前几天我在新浪微博上出了两道有关 TCP 的思考题，引发了一场讨论 <a title="http://weibo.com/1701018393/eCuxDrta0Nn" href="http://weibo.com/1701018393/eCuxDrta0Nn">http://weibo.com/1701018393/eCuxDrta0Nn</a> 。</p> <p>第一道初级题目是：</p> <p><font color="#0000ff">有一台机器，它有一个 IP，上面运行了一个 TCP 服务程序，程序只侦听一个端口，问：从理论上讲（只考虑 TCP/IP 这一层面，<font color="#0000ff">不考虑IPv6</font>）这个服务程序可以支持多少并发 TCP 连接？答 65536 上下的直接刷掉。</font></p> <p>具体来说，这个问题等价于：有一个 TCP 服务程序的地址是 1.2.3.4:8765，问它从理论上能接受多少个并发连接？<br></p> <p>第二道进阶题目是：</p> <p><font color="#0000ff">一台被测机器 A，功能同上，同一交换机上还接有一台机器 B，如果允许 B 的程序直接收发以太网 frame，问：让 A 承担 10 万个并发 TCP 连接需要用多少 B 的资源？100万个呢？</font></p> <p>从讨论的结果看，很多人做出了第一道题，而第二道题几乎无人问津。</p> <p>&nbsp;</p> <p>这里先不公布答案（第一题答案见文末），让我们继续思考一个本质的问题：一个 TCP 连接要占用多少系统资源。</p> <p>在现在的 Linux 操作系统上，如果用 socket()/connect() 或 accept() 来创建 TCP 连接，那么每个连接至少要占用一个文件描述符(file descriptor)。为什么说“至少”？因为文件描述符可以复制，比如 dup()；也可以被继承，比如 fork()；这样可能出现系统里边同一个 TCP 连接有多个文件描述符与之对应。据此，很多人给出的第一题答案是：并发连接数受限于系统能同时打开的文件数目的最大值。这个答案在实践中是正确的，却不符合原题意。</p> <p>&nbsp;</p> <p>如果抛开操作系统层面，只考虑 TCP/IP 层面，建立一个 TCP 连接有哪些开销？理论上最小的开销是多少？考虑两个场景：</p> <p>1. 假设有一个 TCP 服务程序，向这个程序成功发起连接需要做哪些事情？换句话说，如何才能让这个 TCP 服务程序认为有客户连接到了它（让它的 accept() 调用正常返回）？</p> <p>2. 假设有一个 TCP 客户端程序，让这个程序成功建立到服务器的连接需要做哪些事情？换句话说，如何才能让这个 TCP 客户端程序认为它自己已经连接到服务器了（让它的 connect() 调用正常返回）？</p> <p>以上这两个问题问的不是如何编程，如何调用 Sockets API，而是问如何让操作系统的 TCP/IP 协议栈认为任务已经成功完成，连接已经成功建立。</p> <p>&nbsp;</p> <p>学过 TCP/IP 协议，理解三路握手的同学明白，TCP 连接是虚拟的连接，不是电路连接，维持 TCP 连接理论上不占用网络资源（会占用两头程序的系统资源）。只要连接的双方认为 TCP 连接存在，并且可以互相发送 IP packet，那么 TCP 连接就一直存在。</p> <p><strong>对于问题 1</strong>，向一个 TCP 服务程序发起一个连接，客户端（为明白起见，以下称为 faketcp 客户端）只需要做三件事情（三路握手）：</p> <p>1a. 向 TCP 服务程序发一个 IP packet，包含 SYN 的 TCP segment </p> <p>1b. 等待对方返回一个包含 SYN 和 ACK 的 TCP segment</p> <p>1c. 向对方发送一个包含 ACK 的 segment</p> <p>在做完这三件事情之后，TCP 服务器程序会认为连接已建立。而做这三件事情并不占用客户端的资源（？），如果faketcp 客户端程序可以绕开操作系统的 TCP/IP 协议栈，自己直接发送并接收 IP packet 或 Ethernet frame 的话。换句话说，faketcp 客户端可以一直重复做这三件事件，每次用一个不同的 IP:PORT，在服务端创建不计其数的 TCP 连接，而 faketcp 客户端自己毫发无损。很快我们将看到如何用程序来实现这一点。</p> <p><strong>对于问题 2</strong>，为了让一个 TCP 客户端程序认为连接已建立，faketcp 服务端只需要做两件事情：</p> <p>2a. 等待客户端发来的 SYN TCP segment</p> <p>2b. 发送一个包含 SYN 和 ACK 的 TCP segment</p> <p>2c. 忽视对方发来的包含 ACK 的 segment</p> <p>在做完这两件事情（收一个 SYN、发一个 SYN+ACK）之后，TCP 客户端程序会认为连接已建立。而做这三件事情并不占用 faketcp 服务端的资源（？）换句话说，faketcp 服务端可以一直重复做这两件事件，接受不计其数的 TCP 连接，而 faketcp 服务端自己毫发无损。很快我们将看到如何用程序来实现这一点。</p> <p>&nbsp;</p> <p>基于对以上两个问题的分析，说明单独谈论“TCP 并发连接数”是没有意义的，因为连接数基本上是要多少有多少。更有意义的性能指标或许是：“每秒钟收发多少条消息”、“每秒钟收发多少字节的数据”、“支持多少个活动的并发客户”等等。</p> <h2>faketcp 的程序实现</h2> <p>代码见： <a href="https://github.com/chenshuo/recipes/tree/master/faketcp">https://github.com/chenshuo/recipes/tree/master/faketcp</a> 可以直接用 make 编译</p> <p>为了验证我上面的说法，我写了几个小程序来实现 faketcp，这几个程序可以发起或接受不计其数的 TCP 并发连接，并且不消耗操作系统资源，连动态内存分配都不会用到。</p> <p>我家里有一台运行 Ubuntu Linux 10.04 的 PC 机，hostname 是 atom，所有的试验都在这上面进行。</p> <p>家里试验环境的网络配置是：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="net" border="0" alt="net" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/-TCP-_7DB4/net_8a152e90-398e-4b5a-82b1-b9719209e096.png" width="453" height="163"></p> <p>陈硕在《<a href="http://blog.csdn.net/Solstice/archive/2011/06/06/6527585.aspx">谈一谈网络编程学习经验</a>》中曾提到“可以用 TUN/TAP 设备在用户态实现一个能与本机点对点通信的 TCP/IP 协议栈”，这次的试验正好可以用上这个办法。</p> <p>试验的网络配置是：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="tun" border="0" alt="tun" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/-TCP-_7DB4/tun_2d64cc6a-acfb-4dd5-80cb-b5e2dce5444b.png" width="539" height="359"></p> <p>具体做法是：在 atom 上通过打开 /dev/net/tun 设备来创建一个 tun0 虚拟网卡，然后把这个网卡的地址设为 192.168.0.1/24，这样 faketcp 程序就扮演了 192.168.0.0/24 这个网段上的所有机器。atom 发给 192.168.0.2~192.168.0.254 的 IP packet 都会发给 faketcp 程序，faketcp 程序可以模拟其中任何一个 IP 给 atom 发 IP packet。</p> <p>程序分成几步来实现。</p> <p><strong>第一步</strong>：实现 icmp echo 协议，这样就能 ping 通 faketcp 了。</p> <p>代码见 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/icmpecho.cc">https://github.com/chenshuo/recipes/blob/master/faketcp/icmpecho.cc</a></p> <p>其中响应 icmp echo request 的函数在 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/faketcp.cc#L57">https://github.com/chenshuo/recipes/blob/master/faketcp/faketcp.cc#L57</a> 这个函数在后面的程序中也会用到。</p> <p>运行方法，打开 3 个命令行窗口：</p> <p>1. 在第 1 个窗口运行 sudo ./icmpecho ，程序显示</p> <p>allocted tunnel interface <font color="#ff0000">tun0</font><br></p> <p>2. 在第 2 个窗口运行 </p> <p>$ sudo ifconfig tun0 192.168.0.1/24</p> <p>$ sudo tcpdump -i tun0</p> <p>3. 在第 3 个窗口运行</p> <p>$ ping 192.168.0.2</p> <p>$ ping 192.168.0.3</p> <p>$ ping 192.168.0.234</p> <p>发现每个 192.168.0.X 的 IP 都能 ping 通。</p> <p>&nbsp;</p> <p><strong>第二步</strong>：实现拒绝 TCP 连接的功能，即在收到 SYN TCP segment 的时候发送 RST segment。</p> <p>代码见 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/rejectall.cc">https://github.com/chenshuo/recipes/blob/master/faketcp/rejectall.cc</a></p> <p>运行方法，打开 3 个命令行窗口，头两个窗口的操作与前面相同，运行的 faketcp 程序是 ./rejectall</p> <p>3. 在第 3 个窗口运行</p> <p>$ nc 192.168.0.2 2000</p> <p>$ nc 192.168.0.2 3333</p> <p>$ nc 192.168.0.7 5555</p> <p>发现向其中任意一个 IP 发起的 TCP 连接都被拒接了。</p> <p>&nbsp;</p> <p><strong>第三步</strong>：实现接受 TCP 连接的功能，即在收到SYN TCP segment 的时候发回 SYN+ACK。这个程序同时处理了连接断开的情况，即在收到 FIN segment 的时候发回 FIN+ACK。</p> <p>代码见 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/acceptall.cc">https://github.com/chenshuo/recipes/blob/master/faketcp/acceptall.cc</a></p> <p>运行方法，打开 3 个命令行窗口，步骤与前面相同，运行的 faketcp 程序是 ./acceptall。这次会发现 nc 能和 192.168.0.X 中的每一个 IP 每一个 PORT 都能连通。还可以在第 4 个窗口中运行 netstat –tpn ，以确认连接确实建立起来了。如果在 nc 中输入数据，数据会堆积在操作系统中，表现为 netstat 显示的发送队列（Send-Q）的长度增加。</p> <p>&nbsp;</p> <p><strong>第四步</strong>：在第三步接受 TCP 连接的基础上，实现接收数据，即在收到包含 payload 数据 的 TCP segment 时发回 ACK。</p> <p>代码见 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/discardall.cc">https://github.com/chenshuo/recipes/blob/master/faketcp/discardall.cc</a></p> <p>运行方法，打开 3 个命令行窗口，步骤与前面相同，运行的 faketcp 程序是 ./acceptall。这次会发现 nc 能和 192.168.0.X 中的每一个 IP 每一个 PORT 都能连通，数据也能发出去。还可以在第 4 个窗口中运行 netstat –tpn ，以确认连接确实建立起来了，并且发送队列的长度为 0。</p> <p>这一步已经解决了前面的问题 2，扮演任意 TCP 服务端。</p> <p><strong></strong>&nbsp;</p> <p><strong>第五步</strong>：解决前面的问题 1，扮演客户端向 atom 发起任意多的连接。</p> <p>代码见 <a href="https://github.com/chenshuo/recipes/blob/master/faketcp/connectmany.cc">https://github.com/chenshuo/recipes/blob/master/faketcp/connectmany.cc</a></p> <p>这一步的运行方法与前面不同，打开 4 个命令行窗口。</p> <p>1. 在第 1 个窗口运行 sudo ./connectmany 192.168.0.1 2007 1000 ，表示将向 192.168.0.1:2007 发起 1000 个并发连接。</p> <p>程序显示</p> <p>allocted tunnel interface tun0<br>press enter key to start connecting 192.168.0.1:2007</p> <p>&nbsp;</p> <p>2. 在第 2 个窗口运行 </p> <p>$ sudo ifconfig tun0 192.168.0.1/24</p> <p>$ sudo tcpdump -i tun0</p> <p>3. 在第 3 个窗口运行一个能接收并发 TCP 连接的服务程序，可以是 httpd，也可以是 muduo 的 echo 或 discard 示例，程序应 listen 2007 端口。</p> <p>4. 回到第 1 个窗口中敲回车，然后在第 4 个窗口中用 netstat -tpn 来观察并发连接。</p> <p>&nbsp;</p> <p>有兴趣的话，还可以继续扩展，做更多的有关 TCP 的试验，以进一步加深理解，验证操作系统 TCP/IP 协议栈面对不同输入的行为。甚至可以按我在《<a href="http://blog.csdn.net/Solstice/archive/2011/06/06/6527585.aspx">谈一谈网络编程学习经验</a>》中提议的那样，实现完整的 TCP 状态机，做出一个简单的 mini tcp stack。</p> <p>&nbsp;</p> <p>第一道题的答案：</p> <p>在只考虑 IPv4 的情况下，并发数的理论上限是 2**48。考虑某些 IP 段被保留了，这个上界可适当缩小，但数量级不变。实际的限制是操作系统全局文件描述符的数量，以及内存大小。</p> <p>一个 TCP 连接有两个 end points，每个 end point 是 {ip, port}，题目说其中一个 end point 已经固定，那么留下一个 end point 的自由度，即 2 ** 48。客户端 IP 的上限是 2**32 个，每个客户端IP发起连接的上限是 2**16，乘到一起得理论上限。</p> <p>即便客户端使用 NAT，也不影响这个理论上限。（为什么？）</p> <p>&nbsp;</p> <p>在真实的 Linux 系统中，可以通过调整内核参数来支持上百万并发连接，具体做法见：</p> <p><a href="http://urbanairship.com/blog/2010/09/29/linux-kernel-tuning-for-c500k/">http://urbanairship.com/blog/2010/09/29/linux-kernel-tuning-for-c500k/</a></p> <p><a href="http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3">http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3</a> </p> <p>&nbsp;</p> <p>(.完.)</p><img src ="http://www.cppblog.com/Solstice/aggbug/149895.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-07-01 12:50 <a href="http://www.cppblog.com/Solstice/archive/2011/07/01/149895.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 多线程模型：一个 Sudoku 服务器演变</title><link>http://www.cppblog.com/Solstice/archive/2011/06/16/148773.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Thu, 16 Jun 2011 04:58:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/06/16/148773.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/148773.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/06/16/148773.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/148773.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/148773.html</trackback:ping><description><![CDATA[<p>陈硕 (giantchen AT gmail)</p> <p>blog.csdn.net/Solstice</p> <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>本文以一个 Sudoku Solver 为例，回顾了并发网络服务程序的多种设计方案，并介绍了使用 muduo 网络库编写多线程服务器的两种最常用手法。以往的例子展现了 Muduo 在编写单线程并发网络服务程序方面的能力与便捷性，今天我们看一看它在多线程方面的表现。</p> <p>本文代码见：<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/">http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/</a></p> <h1>Sudoku Solver</h1> <p>假设有这么一个网络编程任务：写一个求解数独的程序 (Sudoku Solver)，并把它做成一个网络服务。</p> <p>Sudoku Solver 是我喜爱的网络编程例子，它曾经出现在《<a href="http://blog.csdn.net/Solstice/archive/2011/05/09/6406944.aspx">分布式系统部署、监控与进程管理的几重境界</a>》、《<a href="http://blog.csdn.net/Solstice/archive/2011/04/17/6329080.aspx">Muduo 设计与实现之一：Buffer 类的设计</a>》、《<a href="http://blog.csdn.net/Solstice/archive/2010/03/03/5343217.aspx">〈多线程服务器的适用场合〉例释与答疑</a>》等文中，它也可以看成是 echo 服务的一个变种（《<a href="http://blog.csdn.net/Solstice/archive/2011/06/06/6527585.aspx">谈一谈网络编程学习经验</a>》把 echo 列为三大 TCP 网络编程案例之一）。</p> <p>写这么一个程序在网络编程方面的难度不高，跟写 echo 服务差不多（从网络连接读入一个 Sudoku 题目，算出答案，再发回给客户），挑战在于怎样做才能发挥现在多核硬件的能力？在谈这个问题之前，让我们先写一个基本的单线程版。</p> <h3>协议</h3> <p>一个简单的以 \r\n 分隔的文本行协议，使用 TCP 长连接，客户端在不需要服务时主动断开连接。</p> <p>请求：<font color="#0000ff">[id:]〈81digits〉\r\n</font></p> <p>响应：<font color="#0000ff">[id:]〈81digits〉\r\n</font> 或者 <font color="#0000ff">[id:]NoSolution\r\n</font></p> <p>其中[id:]表示可选的 id，用于区分先后的请求，以支持 Parallel Pipelining，响应中会回显请求中的 id。Parallel Pipelining 的意义见赖勇浩的《<a href="http://blog.csdn.net/lanphaday/archive/2011/04/11/6316099.aspx">以小见大&#8212;&#8212;那些基于 protobuf 的五花八门的 RPC（2）</a> 》，或者见我写的《<a href="http://blog.csdn.net/Solstice/archive/2010/10/19/5950190.aspx">分布式系统的工程化开发方法</a>》第 54 页关于 out-of-order RPC 的介绍。</p> <p>〈81digits〉是 Sudoku 的棋盘，9x9 个数字，未知数字以 0 表示。</p> <p>如果 Sudoku 有解，那么响应是填满数字的棋盘；如果无解，则返回 NoSolution。</p> <p>例子1：</p> <p>请求：000000010400000000020000000000050407008000300001090000300400200050100000000806000\r\n</p> <p>响应：693784512487512936125963874932651487568247391741398625319475268856129743274836159\r\n</p> <p>例子2：</p> <p>请求：a:000000010400000000020000000000050407008000300001090000300400200050100000000806000\r\n</p> <p>响应：a:693784512487512936125963874932651487568247391741398625319475268856129743274836159\r\n</p> <p>例子3：</p> <p>请求：b:00000001040000000002000000000005040700800030000109000030040020005010000000080600<font style="background-color: #ffff00" color="#ff0000">5</font>\r\n</p> <p>响应：b:NoSolution\r\n</p> <p>基于这个文本协议，我们可以用 telnet 模拟客户端来测试 sudoku solver，不需要单独编写 sudoku client。SudokuSolver 的默认端口号是 9981，因为它有 9x9=81 个格子。</p> <h3>基本实现</h3> <p>Sudoku 的求解算法见《<a href="http://blog.csdn.net/Solstice/archive/2008/02/15/2096209.aspx">谈谈数独(Sudoku)</a>》一文，这不是本文的重点。假设我们已经有一个函数能求解 Sudoku，它的原型如下</p> <p>string solveSudoku(const string&amp; puzzle);</p> <p>函数的输入是上文的"〈81digits〉"，输出是"〈81digits〉"或"NoSolution"。这个函数是个 pure function，同时也是线程安全的。</p> <p>有了这个函数，我们以《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例之零：前言</a>》中的 EchoServer 为蓝本，稍作修改就能得到 SudokuServer。这里只列出最关键的 onMessage() 函数，完整的代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_basic.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_basic.cc</a> 。onMessage() 的主要功能是处理协议格式，并调用 solveSudoku() 求解问题。</p><pre class="prettyprint"> // muduo/examples/sudoku/server_basic.cc

  const int kCells = 81;

  void onMessage(const TcpConnectionPtr&amp; conn, Buffer* buf, Timestamp)
  {
    LOG_DEBUG &lt;&lt; conn-&gt;name();
    size_t len = buf-&gt;readableBytes();
    while (len &gt;= kCells + 2)
    {
      const char* crlf = buf-&gt;findCRLF();
      if (crlf)
      {
        string request(buf-&gt;peek(), crlf);
        string id;
        buf-&gt;retrieveUntil(crlf + 2);
        string::iterator colon = find(request.begin(), request.end(), ':');
        if (colon != request.end())
        {
          id.assign(request.begin(), colon);
          request.erase(request.begin(), colon+1);
        }
        if (request.size() == implicit_cast&lt;size_t&gt;(kCells))
        {
          string result = solveSudoku(request);
          if (id.empty())
          {
            conn-&gt;send(result+"\r\n");
          }
          else
          {
            conn-&gt;send(id+":"+result+"\r\n");
          }
        }
        else
        {
          conn-&gt;send("Bad Request!\r\n");
          conn-&gt;shutdown();
        }
      }
      else
      {
        break;
      }
    }
  }
</pre>
<p>server_basic.cc 是一个并发服务器，可以同时服务多个客户连接。但是它是单线程的，无法发挥多核硬件的能力。</p>
<p>Sudoku 是一个计算密集型的任务（见《<a href="http://blog.csdn.net/Solstice/archive/2011/04/17/6329080.aspx">Muduo 设计与实现之一：Buffer 类的设计</a>》中关于其性能的分析），其瓶颈在 CPU。为了让这个单线程 server_basic 程序充分利用 CPU 资源，一个简单的办法是在同一台机器上部署多个 server_basic 进程，让每个进程占用不同的端口，比如在一台 8 核机器上部署 8 个 server_basic 进程，分别占用 9981、9982、&#8230;&#8230;、9988 端口。这样做其实是把难题推给了客户端，因为客户端(s)要自己做负载均衡。再想得远一点，在 8 个 server_basic 前面部署一个 load balancer？似乎小题大做了。</p>
<p>能不能在一个端口上提供服务，并且又能发挥多核处理器的计算能力呢？当然可以，办法不止一种。</p>
<h1>常见的并发网络服务程序设计方案</h1>
<p>W. Richard Stevens 的 UNP2e 第 27 章 Client-Server Design Alternatives 介绍了十来种当时（90 年代末）流行的编写并发网络程序的方案。UNP3e 第 30 章，内容未变，还是这几种。以下简称 UNP CSDA 方案。UNP 这本书主要讲解阻塞式网络编程，在非阻塞方面着墨不多，仅有一章。正确使用 non-blocking IO 需要考虑的问题很多，不适宜直接调用 Sockets API，而需要一个功能完善的网络库支撑。</p>
<p>随着 2000 年前后第一次互联网浪潮的兴起，业界对高并发 http 服务器的强烈需求大大推动了这一领域的研究，目前高性能 httpd 普遍采用的是单线程 reactor 方式。另外一个说法是 IBM Lotus 使用 TCP 长连接协议，而把 Lotus 服务端移植到 Linux 的过程中 IBM 的工程师们大大提高了 Linux 内核在处理并发连接方面的可伸缩性，因为一个公司可能有上万人同时上线，连接到同一台跑着 Lotus server 的 Linux 服务器。</p>
<p>可伸缩网络编程这个领域其实近十年来没什么新东西，POSA2 已经作了相当全面的总结，另外以下几篇文章也值得参考。</p>
<p><a title="http://bulk.fefe.de/scalable-networking.pdf" href="http://bulk.fefe.de/scalable-networking.pdf">http://bulk.fefe.de/scalable-networking.pdf</a></p>
<p><a title="http://www.kegel.com/c10k.html" href="http://www.kegel.com/c10k.html">http://www.kegel.com/c10k.html</a></p>
<p><a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf">http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf</a></p>
<p>下表是陈硕总结的 10 种常见方案。其中&#8220;多连接互通&#8221;指的是如果开发 chat 服务，多个客户连接之间是否能方便地交换数据（chat 也是《<a href="http://blog.csdn.net/Solstice/archive/2011/06/06/6527585.aspx">谈一谈网络编程学习经验</a>》中举的三大 TCP 网络编程案例之一）。对于 echo/http/sudoku 这类&#8220;连接相互独立&#8221;的服务程序，这个功能无足轻重，但是对于 chat 类服务至关重要。&#8220;顺序性&#8221;指的是在 http/sudoku 这类请求-响应服务中，如果客户连接顺序发送多个请求，那么计算得到的多个响应是否按相同的顺序发还给客户（这里指的是在自然条件下，不含刻意同步）。<img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="reactor_comparison" border="0" alt="reactor_comparison" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/3a541217cdb6_121C3/reactor_comparison_14c3857a-1b9e-40cd-ad3c-3b92c518e0d9.png" width="929" height="266" /></p>
<p>UNP CSDA 方案归入 0~5。5 也是目前用得很多的单线程 reactor 方案，muduo 对此提供了很好的支持。6 和 7 其实不是实用的方案，只是作为过渡品。8 和 9 是本文重点介绍的方案，其实这两个方案已经在《<a href="http://blog.csdn.net/Solstice/archive/2010/02/12/5307710.aspx">多线程服务器的常用编程模型</a>》一文中提到过，只不过当时我还没有写 muduo，无法用具体的代码示例来说明。</p>
<p>在对比各方案之前，我们先看看基本的 micro benchmark 数据（前三项由 lmbench 测得）：</p>
<ul>
<li>fork()+exit(): 160us</li>
<li>pthread_create()+pthread_join(): 12us</li>
<li>context switch : 1.5us</li>
<li>sudoku resolve: 100us (根据题目难度不同，浮动范围 20~200us)</li></ul>
<p><strong>方案 0</strong>：这其实不是并发服务器，而是 iterative 服务器，因为它一次只能服务一个客户。代码见 UNP figure 1.9，UNP 以此为对比其他方案的基准点。这个方案不适合长连接，到是很适合 daytime 这种 write-only 服务。</p>
<p><strong>方案 1</strong>：这是传统的 Unix 并发网络编程方案，UNP 称之为 child-per-client 或 fork()-per-client，另外也俗称 process-per-connection。这种方案适合并发连接数不大的情况。至今仍有一些网络服务程序用这种方式实现，比如 PostgreSQL 和 Perforce 的服务端。这种方案适合&#8220;计算响应的工作量远大于 fork() 的开销&#8221;这种情况，比如数据库服务器。这种方案适合长连接，但不太适合短连接，因为 fork() 开销大于求解 sudoku 的用时。</p>
<p><strong>方案 2</strong>：这是传统的 Java 网络编程方案 thread-per-connection，在 Java 1.4 引入 NIO 之前，Java 网络服务程序多采用这种方案。它的初始化开销比方案 1 要小很多。这种方案的伸缩性受到线程数的限制，一两百个还行，几千个的话对操作系统的 scheduler 恐怕是个不小的负担。</p>
<p><strong>方案 3</strong>：这是针对方案 1 的优化，UNP 详细分析了几种变化，包括对 accept 惊群问题的考虑。</p>
<p><strong>方案 4</strong>：这是对方案 2 的优化，UNP 详细分析了它的几种变化。</p>
<p>以上几种方案都是阻塞式网络编程，程序（thread-of-control）通常阻塞在 read() 上，等待数据到达。但是 TCP 是个全双工协议，同时支持 read() 和 write() 操作，当一个线程/进程阻塞在 read() 上，但程序又想给这个 TCP 连接发数据，那该怎么办？比如说 echo client，既要从 stdin 读，又要从网络读，当程序正在阻塞地读网络的时候，如何处理键盘输入？又比如 proxy，既要把连接 a 收到的数据发给连接 b，又要把从连接 b 收到的数据发给连接 a，那么到底读哪个？（proxy 是《<a href="http://blog.csdn.net/Solstice/archive/2011/06/06/6527585.aspx">谈一谈网络编程学习经验</a>》中举的三大 TCP 网络编程案例之一。）</p>
<p>一种方法是用两个线程/进程，一个负责读，一个负责写。UNP 也在实现 echo client 时介绍了这种方案。另外见 Python Pinhole 的代码：<a href="http://code.activestate.com/recipes/114642/">http://code.activestate.com/recipes/114642/</a></p>
<p>另一种方法是使用 IO multiplexing，也就是 select/poll/epoll/kqueue 这一系列的&#8220;多路选择器&#8221;，让一个 thread-of-control 能处理多个连接。&#8220;IO 复用&#8221;其实复用的不是 IO 连接，而是复用线程。使用 select/poll 几乎肯定要配合 non-blocking IO，而使用 non-blocking IO 肯定要使用应用层 buffer，原因见《<a href="http://blog.csdn.net/Solstice/archive/2011/04/17/6329080.aspx">Muduo 设计与实现之一：Buffer 类的设计</a>》。这就不是一件轻松的事儿了，如果每个程序都去搞一套自己的 IO multiplexing 机制（本质是 event-driven 事件驱动），这是一种很大的浪费。感谢 Doug Schmidt 为我们总结出了 Reactor 模式，让 event-driven 网络编程有章可循。继而出现了一些通用的 reactor 框架/库，比如 libevent、muduo、Netty、twisted、POE 等等，有了这些库，我想基本不用去编写阻塞式的网络程序了（特殊情况除外，比如 proxy 流量限制）。</p>
<p>单线程 reactor 的程序结构是（图片取自 Doug Lea 的演讲）：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="reactor_basic" border="0" alt="reactor_basic" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/3a541217cdb6_121C3/reactor_basic_55bfac4c-901d-430f-8f5a-9f7b7201a1a2.png" width="633" height="480" /></p>
<p><strong>方案 5</strong>：基本的单线程 reactor 方案，即前面的 server_basic.cc 程序。本文以它作为对比其他方案的基准点。这种方案的优点是由网络库搞定数据收发，程序只关心业务逻辑；缺点在前面已经谈了：适合 IO 密集的应用，不太适合 CPU 密集的应用，因为较难发挥多核的威力。</p>
<p><strong>方案 6</strong>：这是一个过渡方案，收到 Sudoku 请求之后，不在 reactor 线程计算，而是创建一个新线程去计算，以充分利用多核 CPU。这是非常初级的多线程应用，因为它为每个请求（而不是每个连接）创建了一个新线程。这个开销可以用线程池来避免，即方案 8。这个方案还有一个特点是 out-of-order，即同时创建多个线程去计算同一个连接上收到的多个请求，那么算出结果的次序是不确定的，可能第 2 个 Sudoku 比较简单，比第 1 个先算出结果。这也是为什么我们在一开始设计协议的时候使用了 id，以便客户端区分 response 对应的是哪个 request。</p>
<p><strong>方案 7</strong>：为了让返回结果的顺序确定，我们可以为每个连接创建一个计算线程，每个连接上的请求固定发给同一个线程去算，先到先得。这也是一个过渡方案，因为并发连接数受限于线程数目，这个方案或许还不如直接使用阻塞 IO 的 thread-per-connection 方案2。方案 7 与方案 6 的另外一个区别是一个 client 的最大 CPU 占用率，在方案 6 中，一个 connection 上发来的一长串突发请求(burst requests) 可以占满全部 8 个 core；而在方案 7 中，由于每个连接上的请求固定由同一个线程处理，那么它最多占用 12.5% 的 CPU 资源。这两种方案各有优劣，取决于应用场景的需要，到底是公平性重要还是突发性能重要。这个区别在方案 8 和方案 9 中同样存在，需要根据应用来取舍。</p>
<p><strong>方案 8</strong>：为了弥补方案 6 中为每个请求创建线程的缺陷，我们使用固定大小线程池，程序结构如下图。全部的 IO 工作都在一个 reactor 线程完成，而计算任务交给 thread pool。如果计算任务彼此独立，而且 IO 的压力不大，那么这种方案是非常适用的。Sudoku Solver 正好符合。代码见：<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_threadpool.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_threadpool.cc</a> 后文给出了它与方案 9 的区别。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="reactor_threadpool" border="0" alt="reactor_threadpool" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/3a541217cdb6_121C3/reactor_threadpool_d9d919ff-6e4c-495d-beec-3cc15a9067cc.png" width="633" height="480" /></p>
<p>如果 IO 的压力比较大，一个 reactor 忙不过来，可以试试 multiple reactors 的方案 9。</p>
<p><strong>方案 9</strong>：这是 muduo 内置的多线程方案，也是 Netty 内置的多线程方案。这种方案的特点是 one loop per thread，有一个 main reactor 负责 accept 连接，然后把连接挂在某个 sub reactor 中（muduo 采用 round-robin 的方式来选择 sub reactor），这样该连接的所有操作都在那个 sub reactor 所处的线程中完成。多个连接可能被分派到多个线程中，以充分利用 CPU。Muduo 采用的是固定大小的 reactor pool，池子的大小通常根据 CPU 核数确定，也就是说线程数是固定的，这样程序的总体处理能力不会随连接数增加而下降。另外，由于一个连接完全由一个线程管理，那么请求的顺序性有保证，突发请求也不会占满全部 8 个核（如果需要优化突发请求，可以考虑方案 10）。这种方案把 IO 分派给多个线程，防止出现一个 reactor 的处理能力饱和。与方案 8 的线程池相比，方案 9 减少了进出 thread pool 的两次上下文切换。我认为这是一个适应性很强的多线程 IO 模型，因此把它作为 muduo 的默认线程模型。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="reactor_multiple" border="0" alt="reactor_multiple" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/3a541217cdb6_121C3/reactor_multiple_8ca778e1-717c-42db-8e63-11ddcf1c55ae.png" width="634" height="480" /></p>
<p>代码见：<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_multiloop.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_multiloop.cc</a></p>
<p>server_multiloop.cc 与 server_basic.cc 的区别很小，关键只有一行代码：server_.setThreadNum(numThreads); </p><pre class="prettyprint">$ diff server_basic.cc server_multiloop.cc -up
--- server_basic.cc     2011-06-15 13:40:59.000000000 +0800
+++ server_multiloop.cc 2011-06-15 13:39:53.000000000 +0800
@@ -21,19 +21,22 @@ using namespace muduo::net;
 class SudokuServer
 {
  public:
-  SudokuServer(EventLoop* loop, const InetAddress&amp; listenAddr)
+  SudokuServer(EventLoop* loop, const InetAddress&amp; listenAddr, int numThreads)
     : loop_(loop),
       server_(loop, listenAddr, "SudokuServer"),
+      numThreads_(numThreads),
       startTime_(Timestamp::now())
   {
     server_.setConnectionCallback(
         boost::bind(&amp;SudokuServer::onConnection, this, _1));
     server_.setMessageCallback(
         boost::bind(&amp;SudokuServer::onMessage, this, _1, _2, _3));
+    server_.setThreadNum(numThreads);
   }

</pre>

<p>方案 8 使用 thread pool 的代码与使用多 reactors 的方案 9 相比变化不大，只是把原来 onMessage() 中涉及计算和发回响应的部分抽出来做成一个函数，然后交给 ThreadPool 去计算。记住方案 8 有 out-of-order 的可能，客户端要根据 id 来匹配响应。</p><pre class="prettyprint">$ diff server_multiloop.cc server_threadpool.cc -up
--- server_multiloop.cc 2011-06-15 13:39:53.000000000 +0800
+++ server_threadpool.cc        2011-06-15 14:07:52.000000000 +0800
@@ -31,12 +32,12 @@ class SudokuServer
         boost::bind(&amp;SudokuServer::onConnection, this, _1));
     server_.setMessageCallback(
         boost::bind(&amp;SudokuServer::onMessage, this, _1, _2, _3));
-    server_.setThreadNum(numThreads);
   }

   void start()
   {
     LOG_INFO &lt;&lt; "starting " &lt;&lt; numThreads_ &lt;&lt; " threads.";
+    threadPool_.start(numThreads_);
     server_.start();
   }

@@ -68,15 +69,7 @@ class SudokuServer
         }
         if (request.size() == implicit_cast&lt;size_t&gt;(kCells))
         {
-          string result = solveSudoku(request);
-          if (id.empty())
-          {
-            conn-&gt;send(result+"\r\n");
-          }
-          else
-          {
-            conn-&gt;send(id+":"+result+"\r\n");
-          }
+          threadPool_.run(boost::bind(solve, conn, request, id));
         }
         else
         {
@@ -91,8 +84,23 @@ class SudokuServer
     }
   }

+  static void solve(const TcpConnectionPtr&amp; conn, const string&amp; request, const string&amp; id)
+  {
+    LOG_DEBUG &lt;&lt; conn-&gt;name();
+    string result = solveSudoku(request);
+    if (id.empty())
+    {
+      conn-&gt;send(result+"\r\n");
+    }
+    else
+    {
+      conn-&gt;send(id+":"+result+"\r\n");
+    }
+  }
+
   EventLoop* loop_;
   TcpServer server_;
+  ThreadPool threadPool_;
   int numThreads_;
   Timestamp startTime_;
 };
</pre>

<p>完整代码见：<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_threadpool.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/sudoku/server_threadpool.cc</a></p>
<p><strong>方案 10</strong>：把方案 8 和方案 9 混合，既使用多个 reactors 来处理 IO，又使用线程池来处理计算。这种方案适合既有突发 IO （利用多线程处理多个连接上的 IO），又有突发计算的应用（利用线程池把一个连接上的计算任务分配给多个线程去做）。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="reactor_hybrid" border="0" alt="reactor_hybrid" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/3a541217cdb6_121C3/reactor_hybrid_86ee4e66-a8a7-4689-a131-05d96fe4feb7.png" width="630" height="480" /></p>
<p>这种其实方案看起来复杂，其实写起来很简单，只要把方案 8 的代码加一行 server_.setThreadNum(numThreads); 就行，这里就不举例了。</p>
<h1>结语</h1>
<p>我在《<a href="http://blog.csdn.net/Solstice/archive/2010/02/12/5307710.aspx">多线程服务器的常用编程模型</a>》一文中说</p>
<p><font color="#0000ff">总结起来，我推荐的多线程服务端编程模式为：event loop per thread + thread pool。</font></p>
<ul>
<li><font color="#0000ff">event loop 用作 non-blocking IO 和定时器。</font> 
</li><li><font color="#0000ff">thread pool 用来做计算，具体可以是任务队列或消费者-生产者队列。</font></li></ul>
<p>当时（2010年2月）我还说&#8220;<font color="#0000ff">以这种方式写服务器程序，需要一个优质的基于 Reactor 模式的网络库来支撑，我只用过in-house的产品，无从比较并推荐市面上常见的 C++ 网络库，抱歉。</font>&#8221;</p>
<p>现在有了 muduo 网络库，我终于能够用具体的代码示例把思想完整地表达出来。</p><img src ="http://www.cppblog.com/Solstice/aggbug/148773.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-06-16 12:58 <a href="http://www.cppblog.com/Solstice/archive/2011/06/16/148773.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>谈一谈网络编程学习经验(06-08更新)</title><link>http://www.cppblog.com/Solstice/archive/2011/06/06/148129.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Mon, 06 Jun 2011 00:44:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/06/06/148129.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/148129.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/06/06/148129.html#Feedback</comments><slash:comments>14</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/148129.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/148129.html</trackback:ping><description><![CDATA[<p>谈一谈网络编程学习经验 </p>
<p>陈硕 </p>
<p>giantchen@gmail.com </p>
<p>blog.csdn.net/Solstice </p>
<p>2011-06-08 </p>
<p>PDF 版下载：<a title="https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf" href="https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf">https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf</a></p>
<p>本文谈一谈我在学习网络编程方面的一些个人经验。&#8220;网络编程&#8221;这个术语的范围很广，本文指用Sockets API开发基于TCP/IP的网络应用程序，具体定义见&#8220;网络编程的各种任务角色&#8221;一节。 </p>
<p>受限于本人的经历和经验，这篇文章的适应范围是： </p>
<p>&#183; x86-64 Linux服务端网络编程，直接或间接使用 Sockets API </p>
<p>&#183; 公司内网。不一定是局域网，但总体位于公司防火墙之内，环境可控 </p>
<p>本文可能不适合： </p>
<p>&#183; PC客户端网络编程，程序运行在客户的PC上，环境多变且不可控 </p>
<p>&#183; Windows网络编程 </p>
<p>&#183; 面向公网的服务程序 </p>
<p>&#183; 高性能网络服务器 </p>
<p>本文分两个部分： </p>
<p>1. 网络编程的一些胡思乱想，谈谈我对这一领域的认识 </p>
<p>2. 几本必看的书，基本上还是W. Richard Stevents那几本 </p>
<p>另外，本文没有特别说明时均暗指TCP协议，&#8220;连接&#8221;是&#8220;TCP连接&#8221;，&#8220;服务端&#8221;是&#8220;TCP服务端&#8221;。 </p>
<h3>网络编程的一些胡思乱想</h3>
<p>以下胡乱列出我对网络编程的一些想法，前后无关联。 </p>
<h4>网络编程是什么？</h4>
<p>网络编程是什么？是熟练使用Sockets API吗？说实话，在实际项目里我只用过两次Sockets API，其他时候都是使用封装好的网络库。 </p>
<p>第一次是2005年在学校做一个羽毛球赛场计分系统：我用C# 编写运行在PC机上的软件，负责比分的显示；再用C# 写了运行在PDA上的计分界面，记分员拿着PDA记录比分；这两部分程序通过 TCP协议相互通信。这其实是个简单的分布式系统，体育馆有不止一片场地，每个场地都有一名拿PDA的记分员，每个场地都有两台显示比分的PC机（显示器是42吋平板电视，放在场地的对角，这样两边看台的观众都能看到比分）。这两台PC机功能不完全一样，一台只负责显示当前比分，另一台还要负责与PDA通信，并更新数据库里的比分信息。此外，还有一台PC机负责周期性地从数据库读出全部7片场地的比分，显示在体育馆墙上的大屏幕上。这台PC上还运行着一个程序，负责生成比分数据的静态页面，通过FTP上传发布到某门户网站的体育频道。系统中还有一个录入赛程（参赛队，运动员，出场顺序等）数据库的程序，运行在数据库服务器上。算下来整个系统有十来个程序，运行在二十多台设备（PC和PDA）上，还要考虑可靠性。将来有机会把这个小系统仔细讲一讲，挺有意思的。 </p>
<p>这是我第一次写实际项目中的网络程序，当时写下来的感觉是像写命令行与用户交互的程序：程序在命令行输出一句提示语，等待客户输入一句话，然后处理客户输入，再输出下一句提示语，如此循环。只不过这里的&#8220;客户&#8221;不是人，而是另一个程序。在建立好TCP连接之后，双方的程序都是read/write循环（为求简单，我用的是blocking读写），直到有一方断开连接。 </p>
<p>第二次是2010年编写muduo网络库，我再次拿起了Sockets API，写了一个基于Reactor模式的C++ 网络库。写这个库的目的之一就是想让日常的网络编程从Sockets API的琐碎细节中解脱出来，让程序员专注于业务逻辑，把时间用在刀刃上。Muduo 网络库的示例代码包含了几十个网络程序，这些示例程序都没有直接使用Sockets API。 </p>
<p>在此之外，无论是实习还是工作，虽然我写的程序都会通过TCP协议与其他程序打交道，但我没有直接使用过Sockets API。对于TCP网络编程，我认为核心是处理&#8220;三个半事件&#8221;，见《Muduo 网络编程示例之零：前言》中的&#8220;TCP 网络编程本质论&#8221;。程序员的主要工作是在事件处理函数中实现业务逻辑，而不是和Sockets API较劲。 </p>
<p>这里还是没有说清楚&#8220;网络编程&#8221;是什么，请继续阅读后文&#8220;网络编程的各种任务角色&#8221;。 </p>
<h4>学习网络编程有用吗？</h4>
<p>以上说的是比较底层的网络编程，程序代码直接面对从TCP或UDP收到的数据以及构造数据包发出去。在实际工作中，另一种常见 的情况是通过各种 client library 来与服务端打交道，或者在现成的框架中填空来实现server，或者采用更上层的通信方式。比如用libmemcached与memcached打交道，使用libpq来与PostgreSQL 打交道，编写Servlet来响应http请求，使用某种RPC与其他进程通信，等等。这些情况都会发生网络通信，但不一定算作&#8220;网络编程&#8221;。如果你的工作是前面列举的这些，学习TCP/IP网络编程还有用吗？ </p>
<p>我认为还是有必要学一学，至少在troubleshooting 的时候有用。无论如何，这些library或framework都会调用底层的Sockets API来实现网络功能。当你的程序遇到一个线上问题，如果你熟悉Sockets API，那么从strace不难发现程序卡在哪里，尽管可能你没有直接调用这些Sockets API。另外，熟悉TCP/IP协议、会用tcpdump也大大有助于分析解决线上网络服务问题。 </p>
<h4>在什么平台上学习网络编程？</h4>
<p>对于服务端网络编程，我建议在Linux上学习。 </p>
<p>如果在10年前，这个问题的答案或许是FreeBSD，因为FreeBSD根正苗红，在2000年那一次互联网浪潮中扮演了重要角色，是很多公司首选的免费服务器操作系统。2000年那会儿Linux还远未成熟，连epoll都还没有实现。（FreeBSD在2001年发布4.1版，加入了kqueue，从此C10k不是问题。） </p>
<p>10年后的今天，事情起了变化，Linux成为了市场份额最大的服务器操作系统(http://en.wikipedia.org/wiki/Usage_share_of_operating_systems)。在Linux这种大众系统上学网络编程，遇到什么问题会比较容易解决。因为用的人多，你遇到的问题别人多半也遇到过；同样因为用的人多，如果真的有什么内核bug，很快就会得到修复，至少有work around的办法。如果用别的系统，可能一个问题发到论坛上半个月都不会有人理。从内核源码的风格看，FreeBSD更干净整洁，注释到位，但是无奈它的市场份额远不如Linux，学习Linux是更好的技术投资。 </p>
<h4>可移植性重要吗？</h4>
<p>写网络程序要不要考虑移植性？这取决于项目需要，如果贵公司做的程序要卖给其他公司，而对方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX等等操作系统，这时候考虑移植性。如果编写公司内部的服务器上用的网络程序，那么大可只关注一个平台，比如Linux。因为编写和维护可移植的网络程序的代价相当高，平台间的差异可能远比想象中大，即便是POSIX系统之间也有不小的差异（比如Linux没有SO_NOSIGPIPE选项），错误的返回码也大不一样。 </p>
<p>我就不打算把muduo往Windows或其他操作系统移植。如果需要编写可移植的网络程序，我宁愿用libevent或者Java Netty这样现成的库，把脏活累活留给别人。 </p>
<h4>网络编程的各种任务角色</h4>
<p>计算机网络是个 big topic，涉及很多人物和角色，既有开发人员，也有运维人员。比方说：公司内部两台机器之间 ping 不通，通常由网络运维人员解决，看看是布线有问题还是路由器设置不对；两台机器能ping通，但是程序连不上，经检查是本机防火墙设置有问题，通常由系统管理员解决；两台机器能连上，但是丢包很严重，发现是网卡或者交换机的网口故障，由硬件维修人员解决；两台机器的程序能连上，但是偶尔发过去的请求得不到响应，通常是程序bug，应该由开发人员解决。 </p>
<p>本文主要关心开发人员这一角色。下面简单列出一些我能想到的跟网络打交道的编程任务，其中前三项是面向网络本身，后面几项是在计算机网络之上构建信息系统。 </p>
<p>1. 开发网络设备，编写防火墙、交换机、路由器的固件 firmware </p>
<p>2. 开发或移植网卡的驱动 </p>
<p>3. 移植或维护TCP/IP协议栈（特别是在嵌入式系统上） </p>
<p>4. 开发或维护标准的网络协议程序，HTTP、FTP、DNS、SMTP、POP3、NFS </p>
<p>5. 开发标准网络协议的&#8220;附加品&#8221;，比如HAProxy、squid、varnish等web load balancer </p>
<p>6. 开发标准或非标准网络服务的客户端库，比如ZooKeeper客户端库，memcached客户端库 </p>
<p>7. 开发与公司业务直接相关的网络服务程序，比如即时聊天软件的后台服务器，网游服务器，金融交易系统，互联网企业用的分布式海量存储，微博发帖的内部广播通知，等等 </p>
<p>8. 客户端程序中涉及网络的部分，比如邮件客户端中与 POP3、SMTP通信的部分，以及网游的客户端程序中与服务器通信的部分 </p>
<p>本文所指的&#8220;网络编程&#8221;专指第7项，即在TCP/IP协议之上开发业务软件。 </p>
<h4>面向业务的网络编程的特点</h4>
<p>跟开发通用的网络程序不同，开发面向公司业务的专用网络程序有其特点： </p>
<p>&#183; 业务逻辑比较复杂，而且时常变化 </p>
<p>如果写一个HTTP服务器，在大致实现HTTP /1.1标准之后，程序的主体功能一般不会有太大的变化，程序员会把时间放在性能调优和bug修复上。而开发针对公司业务的专用程序时，功能说明书（spec）很可能不如HTTP/1.1标准那么细致明确。更重要的是，程序是快速演化的。以即时聊天工具的后台服务器为例，可能第一版只支持在线聊天；几个月之后发布第二版，支持离线消息；又过了几个月，第三版支持隐身聊天；随后，第四版支持上传头像；如此等等。这要求程序员能快速响应新的业务需求，公司才能保持竞争力。 </p>
<p>&#183; 不一定需要遵循公认的通信协议标准 </p>
<p>比方说网游服务器就没什么协议标准，反正客户端和服务端都是本公司开发，如果发现目前的协议设计有问题，两边一起改了就是了。 </p>
<p>&#183; 程序结构没有定论 </p>
<p>对于高并发大吞吐的标准网络服务，一般采用单线程事件驱动的方式开发，比如HAProxy、lighttpd等都是这个模式。但是对于专用的业务系统，其业务逻辑比较复杂，占用较多的CPU资源，这种单线程事件驱动方式不见得能发挥现在多核处理器的优势。这留给程序员比较大的自由发挥空间，做好了横扫千军，做烂了一败涂地。 </p>
<p>&#183; 性能评判的标准不同 </p>
<p>如果开发httpd这样的通用服务，必然会和开源的Nginx、lighttpd等高性能服务器比较，程序员要投入相当的精力去优化程序，才能在市场上占有一席之地。而面向业务的专用网络程序不一定有开源的实现以供对比性能，程序员通常更加注重功能的稳定性与开发的便捷性。性能只要一代比一代强即可。 </p>
<p>&#183; 网络编程起到支撑作用，但不处于主导地位 </p>
<p>程序员的主要工作是实现业务逻辑，而不只是实现网络通信协议。这要求程序员深入理解业务。程序的性能瓶颈不一定在网络上，瓶颈有可能是CPU、Disk IO、数据库等等，这时优化网络方面的代码并不能提高整体性能。只有对所在的领域有深入的了解，明白各种因素的权衡(trade-off)，才能做出一些有针对性的优化。 </p>
<h4>几个术语</h4>
<p>互联网上的很多口水战是由对同一术语的不同理解引起的，比我写的《多线程服务器的适用场合》就曾经人被说是&#8220;挂羊头卖狗肉&#8221;，因为这篇文章中举的 master例子&#8220;根本就算不上是个网络服务器。因为它的瓶颈根本就跟网络无关。&#8221; </p>
<p>&#183; 网络服务器 </p>
<p>&#8220;网络服务器&#8221;这个术语确实含义模糊，到底指硬件还是软件？到底是服务于网络本身的机器（交换机、路由器、防火墙、NAT），还是利用网络为其他人或程序提供服务的机器（打印服务器、文件服务器、邮件服务器）。每个人根据自己熟悉的领域，可能会有不同的解读。比方说或许有人认为只有支持高并发高吞吐的才算是网络服务器。 </p>
<p>为了避免无谓的争执，我只用&#8220;网络服务程序&#8221;或者&#8220;网络应用程序&#8221;这种含义明确的术语。&#8220;开发网络服务程序&#8221;通常不会造成误解。 </p>
<p>&#183; 客户端？服务端？ </p>
<p>在TCP网络编程里边，客户端和服务端很容易区分，主动发起连接的是客户端，被动接受连接的是服务端。当然，这个&#8220;客户端&#8221;本身也可能是个后台服务程序，HTTP Proxy对HTTP Server来说就是个客户端。 </p>
<p>&#183; 客户端编程？服务端编程？ </p>
<p>但是&#8220;服务端编程&#8221;和&#8220;客户端编程&#8221;就不那么好区分。比如 Web crawler，它会主动发起大量连接，扮演的是HTTP客户端的角色，但似乎应该归入&#8220;服务端编程&#8221;。又比如写一个 HTTP proxy，它既会扮演服务端&#8212;&#8212;被动接受 web browser 发起的连接，也会扮演客户端&#8212;&#8212;主动向 HTTP server 发起连接，它究竟算服务端还是客户端？我猜大多数人会把它归入服务端编程。 </p>
<p>那么究竟如何定义&#8220;服务端编程&#8221;？ </p>
<p>服务端编程需要处理大量并发连接？也许是，也许不是。比如云风在一篇介绍网游服务器的博客<a href="http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html">http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html</a>中就谈到，网游中用到的&#8220;连接服务器&#8221;需要处理大量连接，而&#8220;逻辑服务器&#8221;只有一个外部连接。那么开发这种网游&#8220;逻辑服务器&#8221;算服务端编程还是客户端编程呢？ </p>
<p>我认为，&#8220;服务端网络编程&#8221;指的是编写没有用户界面的长期运行的网络程序，程序默默地运行在一台服务器上，通过网络与其他程序打交道，而不必和人打交道。与之对应的是客户端网络程序，要么是短时间运行，比如wget；要么是有用户界面（无论是字符界面还是图形界面）。本文主要谈服务端网络编程。 </p>
<h4>7x24重要吗？内存碎片可怕吗？</h4>
<p>一谈到服务端网络编程，有人立刻会提出7x24运行的要求。对于某些网络设备而言，这是合理的需求，比如交换机、路由器。对于开发商业系统，我认为要求程序7x24运行通常是系统设计上考虑不周。具体见《分布式系统的工程化开发方法》第20页起。重要的不是7x24，而是在程序不必做到7x24的情况下也能达到足够高的可用性。一个考虑周到的系统应该允许每个进程都能随时重启，这样才能在廉价的服务器硬件上做到高可用性。 </p>
<p>既然不要求7x24，那么也不必害怕内存碎片，理由如下： </p>
<p>&#183; 64-bit系统的地址空间足够大，不会出现没有足够的连续空间这种情况。 </p>
<p>&#183; 现在的内存分配器（malloc及其第三方实现）今非昔比，除了memcached这种纯以内存为卖点的程序需要自己设计分配器之外，其他网络程序大可使用系统自带的malloc或者某个第三方实现。 </p>
<p>&#183; Linux Kernel也大量用到了动态内存分配。既然操作系统内核都不怕动态分配内存造成碎片，应用程序为什么要害怕？ </p>
<p>&#183; 内存碎片如何度量？有没有什么工具能为当前进程的内存碎片状况评个分？如果不能比较两种方案的内存碎片程度，谈何优化？ </p>
<p>有人为了避免内存碎片，不使用STL容器，也不敢new/delete，这算是premature optimization还是因噎废食呢？ </p>
<h4>协议设计是网络编程的核心</h4>
<p>对于专用的业务系统，协议设计是核心任务，决定了系统的开发难度与可靠性，但是这个领域还没有形成大家公认的设计流程。 </p>
<p>系统中哪个程序发起连接，哪个程序接受连接？如果写标准的网络服务，那么这不是问题，按RFC来就行了。自己设计业务系统，有没有章法可循？以网游为例，到底是连接服务器主动连接逻辑服务器，还是逻辑服务器主动连接&#8220;连接服务器&#8221;？似乎没有定论，两种做法都行。一般可以按照&#8220;依赖-&gt;被依赖&#8221;的关系来设计发起连接的方向。 </p>
<p>比新建连接难的是关闭连接。在传统的网络服务中（特别是短连接服务），不少是服务端主动关闭连接，比如daytime、HTTP/1.0。也有少部分是客户端主动关闭连接，通常是些长连接服务，比如 echo、chargen等。我们自己的业务系统该如何设计连接关闭协议呢？ </p>
<p>服务端主动关闭连接的缺点之一是会多占用服务器资源。服务端主动关闭连接之后会进入TIME_WAIT状态，在一段时间之内hold住一些内核资源。如果并发访问量很高，这会影响服务端的处理能力。这似乎暗示我们应该把协议设计为客户端主动关闭，让TIME_WAIT状态分散到多台客户机器上，化整为零。 </p>
<p>这又有另外的问题：客户端赖着不走怎么办？会不会造成拒绝服务攻击？或许有一个二者结合的方案：客户端在收到响应之后就应该主动关闭，这样把 TIME_WAIT 留在客户端。服务端有一个定时器，如果客户端若干秒钟之内没有主动断开，就踢掉它。这样善意的客户端会把TIME_WAIT留给自己，buggy的客户端会把 TIME_WAIT留给服务端。或者干脆使用长连接协议，这样避免频繁创建销毁连接。 </p>
<p>比连接的建立与断开更重要的是设计消息协议。消息格式很好办，XML、JSON、Protobuf都是很好的选择；难的是消息内容。一个消息应该包含哪些内容？多个程序相互通信如何避免race condition（见《分布式系统的工程化开发方法》p.16的例子）？系统的全局状态该如何跃迁？可惜这方面可供参考的例子不多，也没有太多通用的指导原则，我知道的只有30年前提出的end-to-end principle和happens-before relationship。只能从实践中慢慢积累了。 </p>
<h4>网络编程的三个层次</h4>
<p>侯捷先生在《漫談程序員與編程》中讲到 STL 运用的三个档次：&#8220;會用STL，是一種檔次。對STL原理有所了解，又是一個檔次。追蹤過STL源碼，又是一個檔次。第三種檔次的人用起 STL 來，虎虎生風之勢絕非第一檔次的人能夠望其項背。&#8221; </p>
<p>我认为网络编程也可以分为三个层次： </p>
<p>1. 读过教程和文档 </p>
<p>2. 熟悉本系统TCP/IP协议栈的脾气 </p>
<p>3. 自己写过一个简单的TCP/IP stack </p>
<p>第一个层次是基本要求，读过《Unix网络编程》这样的编程教材，读过《TCP/IP详解》基本理解TCP/IP协议，读过本系统的manpage。这个层次可以编写一些基本的网络程序，完成常见的任务。但网络编程不是照猫画虎这么简单，若是按照manpage的功能描述就能编写产品级的网络程序，那人生就太幸福了。 </p>
<p>第二个层次，熟悉本系统的TCP/IP协议栈参数设置与优化是开发高性能网络程序的必备条件。摸透协议栈的脾气还能解决工作中遇到的比较复杂的网络问题。拿Linux的TCP/IP协议栈来说： </p>
<p>&#183; 有可能出现自连接（见《学之者生，用之者死&#8212;&#8212;ACE历史与简评》举的三个硬伤），程序应该有所准备。 </p>
<p>&#183; Linux的内核会有bug，比如某种TCP拥塞控制算法曾经出现TCP window clamping（窗口箝位）bug，导致吞吐量暴跌，可以选用其他拥塞控制算法来绕开(work around)这个问题。 </p>
<p>这些阴暗角落在manpage里没有描述，要通过其他渠道了解。 </p>
<p>编写可靠的网络程序的关键是熟悉各种场景下的error code（文件描述符用完了如何？本地ephemeral port暂时用完，不能发起新连接怎么办？服务端新建并发连接太快，backlog用完了，客户端connect会返回什么错误？），有的在manpage里有描述，有的要通过实践或阅读源码获得。 </p>
<p>第三个层次，通过自己写一个简单的TCP/IP协议栈，能大大加深对TCP/IP的理解，更能明白TCP为什么要这么设计，有哪些因素制约，每一步操作的代价是什么，写起网络程序来更是成竹在胸。 </p>
<p>其实实现TCP/IP只需要操作系统提供三个接口函数：一个函数，两个回调函数。分别是：send_packet()、on_receive_packet()、on_timer()。多年前有一篇文章《使用libnet与libpcap构造TCP/IP协议软件》介绍了在用户态实现TCP/IP的方法。lwIP也是很好的借鉴对象。 </p>
<p>如果有时间，我打算自己写一个Mini/Tiny/Toy/Trivial/Yet-Another TCP/IP。我准备换一个思路，用TUN/TAP设备在用户态实现一个能与本机点对点通信的TCP/IP协议栈，这样那三个接口函数就表现为我最熟悉的文件读写。在用户态实现的好处是便于调试，协议栈做成静态库，与应用程序链接到一起（库的接口不必是标准的Sockets API）。做完这一版，还可以继续发挥，用FTDI的USB-SPI接口芯片连接ENC28J60适配器，做一个真正独立于操作系统的TCP/IP stack。如果只实现最基本的IP、ICMP Echo、TCP的话，代码应能控制在3000行以内；也可以实现UDP，如果应用程序需要用到DNS的话。 </p>
<h4>最主要的三个例子</h4>
<p>我认为TCP网络编程有三个例子最值得学习研究，分别是echo、chat、proxy，都是长连接协议。 </p>
<p>Echo的作用：熟悉服务端被动接受新连接、收发数据、被动处理连接断开。每个连接是独立服务的，连接之间没有关联。在消息内容方面Echo有一些变种：比如做成一问一答的方式，收到的请求和发送响应的内容不一样，这时候要考虑打包与拆包格式的设计，进一步还可以写简单的HTTP服务。 </p>
<p>Chat的作用：连接之间的数据有交流，从a收到的数据要发给b。这样对连接管理提出的更高的要求：如何用一个程序同时处理多个连接？fork() per connection似乎是不行的。如何防止串话？b有可能随时断开连接，而新建立的连接c可能恰好复用了b的文件描述符，那么a会不会错误地把消息发给c？ </p>
<p>Proxy的作用：连接的管理更加复杂：既要被动接受连接，也要主动发起连接，既要主动关闭连接，也要被动关闭连接。还要考虑两边速度不匹配，见《Muduo 网络编程示例之十：socks4a 代理服务器》。 </p>
<p>这三个例子功能简单，突出了TCP网络编程中的重点问题，挨着做一遍基本就能达到层次一的要求。 </p>
<h4>TCP的可靠性有多高？</h4>
<p>TCP是&#8220;面向连接的、可靠的、字节流传输协议&#8221;，这里的&#8220;可靠&#8221;究竟是什么意思？《Effective TCP/IP Programming》第9条说：Realize That TCP Is a Reliable Protocol, Not an Infallible Protocol，那么TCP在哪种情况下会出错？这里说的&#8220;出错&#8221;指的是收到的数据与发送的数据不一致，而不是数据不可达。 </p>
<p>我在《一种自动反射消息类型的 Google Protobuf 网络传输方案》中设计了带check sum的消息格式，很多人表示不理解，认为是多余的。IP header里边有check sum，TCP header也有check sum，链路层以太网还有CRC32校验，那么为什么还需要在应用层做校验？什么情况下TCP传送的数据会出错？ </p>
<p>IP header和TCP header的check sum是一种非常弱的16-bit check sum算法，把数据当成反码表示的16-bit integers，再加到一起。这种checksum算法能检出一些简单的错误，而对某些错误无能为力，由于是简单的加法，遇到&#8220;和&#8221;不变的情况就无法检查出错误（比如交换两个16-bit整数，加法满足交换律，结果不变）。以太网的CRC32只能保证同一个网段上的通信不会出错（两台机器的网线插到同一个交换机上，这时候以太网的CRC是有用的）。但是，如果两台机器之间经过了多级路由器呢？ </p>
<p><img style="background-image: none; border-right-width: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="router" border="0" alt="router" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/958c781e7d58_E312/router_3.png" width="471" height="305" /></p>
<p>上图中Client向Server发了一个TCP segment，这个segment先被封装成一个IP packet，再被封装成ethernet frame，发送到路由器（图中消息a）。Router收到ethernet frame (b)，转发到另一个网段(c)，最后Server收到d，通知应用程序。Ethernet CRC能保证a和b相同，c和d相同；TCP header check sum的强度不足以保证收发payload的内容一样。另外，如果把Router换成NAT，那么NAT自己会构造c（替换掉源地址），这时候a和d的payload不能用tcp header checksum校验。 </p>
<p>路由器可能出现硬件故障，比方说它的内存故障（或偶然错误）导致收发IP报文出现多bit的反转或双字节交换，这个反转如果发生在payload区，那么无法用链路层、网络层、传输层的check sum查出来，只能通过应用层的check sum来检测。这个现象在开发的时候不会遇到，因为开发用的几台机器很可能都连到同一个交换机，ethernet CRC能防止错误。开发和测试的时候数据量不大，错误很难发生。之后大规模部署到生产环境，网络环境复杂，这时候出个错就让人措手不及。有一篇论文《When the CRC and TCP checksum disagree》分析了这个问题。另外《The Limitations of the Ethernet CRC and TCP/IP checksums for error detection》(<a href="http://noahdavids.org/self_published/CRC_and_checksum.html">http://noahdavids.org/self_published/CRC_and_checksum.html</a>)也值得一读。</p>
<p>这个情况真的会发生吗？会的，Amazon S3 在2008年7月就遇到过，单bit反转导致了一次严重线上事故，所以他们吸取教训加了 check sum。见<a href="http://status.aws.amazon.com/s3-20080720.html">http://status.aws.amazon.com/s3-20080720.html</a> </p>
<p>另外一个例证：下载大文件的时候一般都会附上MD5，这除了有安全方面的考虑（防止篡改），也说明应用层应该自己设法校验数据的正确性。这是end-to-end principle的一个例证。 </p>
<h3>三本必看的书</h3>
<p>谈到Unix编程和网络编程，W. Richard Stevens 是个绕不开的人物，他生前写了6本书，APUE、两卷UNP、三卷TCP/IP。有四本与网络编程直接相关。UNP第二卷其实跟网络编程关系不大，是APUE在多线程和进程间通信(IPC)方面的补充。很多人把TCP/IP一二三卷作为整体推荐，其实这三本书用处不同，应该区别对待。 </p>
<p>这里谈到的几本书都没有超出孟岩在《TCP/IP 网络编程之四书五经》中的推荐，说明网络编程这一领域已经相对成熟稳定。 </p>
<p>&#183; 《<em>TCP/IP Illustrated</em>, <em>Vol. 1: The Protocols</em>》中文名《TCP/IP 详解》，以下简称 TCPv1。 </p>
<p>TCPv1 是一本奇书。 </p>
<p>这本书迄今至少被三百多篇学术论文引用过<a href="http://portal.acm.org/citation.cfm?id=161724">http://portal.acm.org/citation.cfm?id=161724</a>。一本学术专著被论文引用算不上出奇，难得的是一本写给程序员看的技术书能被学术论文引用几百次，我不知道还有哪本技术书能做到这一点。 </p>
<p>TCPv1 堪称 TCP/IP领域的圣经。作者 W. Richard Stevens 不是 TCP/IP 协议的发明人，他从使用者（程序员）的角度，以 tcpdump 为工具，对 TCP 协议抽丝剥茧娓娓道来（第17~24章），让人叹服。恐怕 TCP 协议的设计者也难以讲解得如此出色，至少不会像他这么耐心细致地画几百幅收发 package 的时序图。 </p>
<p>TCP作为一个可靠的传输层协议，其核心有三点： </p>
<p>1. Positive acknowledgement with retransmission </p>
<p>2. Flow control using sliding window（包括Nagle 算法等） </p>
<p>3. Congestion control（包括slow start、congestion avoidance、fast retransmit等） </p>
<p>第一点已经足以满足&#8220;可靠性&#8221;要求（为什么？）；第二点是为了提高吞吐量，充分利用链路层带宽；第三点是防止过载造成丢包。换言之，第二点是避免发得太慢，第三点是避免发得太快，二者相互制约。从反馈控制的角度看，TCP像是一个自适应的节流阀，根据管道的拥堵情况自动调整阀门的流量。 </p>
<p>TCP的 flow control 有一个问题，每个TCP connection是彼此独立的，保存有自己的状态变量；一个程序如果同时开启多个连接，或者操作系统中运行多个网络程序，这些连接似乎不知道他人的存在，缺少对网卡带宽的统筹安排。（或许现代的操作系统已经解决了这个问题？） </p>
<p>TCPv1 唯一的不足是它出版太早了，1993 年至今网络技术发展了几代。链路层方面，当年主流的 10Mbit 网卡和集线器早已经被淘汰；100Mbit 以太网也没什么企业在用了，交换机(switch)也已经全面取代了集线器(hub)；服务器机房以 1Gbit 网络为主，有些场合甚至用上了 10Gbit 以太网。另外，无线网的普及也让TCP flow control面临新挑战；原来设计TCP的时候，人们认为丢包通常是拥塞造成的，这时应该放慢发送速度，减轻拥塞；而在无线网中，丢包可能是信号太弱造成的，这时反而应该快速重试，以保证性能。网络层方面变化不大，IPv6 雷声大雨点小。传输层方面，由于链路层带宽大增，TCP window scale option 被普遍使用，另外 TCP timestamps option 和 TCP selective ack option 也很常用。由于这些因素，在现在的 Linux 机器上运行 tcpdump 观察 TCP 协议，程序输出会与原书有些不同。 </p>
<p>一个好消息：TCPv1将于今年10月（2011年）推出第二版，Amazon 的预定页面是：<a href="http://www.amazon.com/gp/product/0321336313">http://www.amazon.com/gp/product/0321336313</a>，让我们拭目以待。 </p>
<p>&#183; 《Unix Network Programming, Vol. 1: Networking API》第二版或第三版（这两版的副标题稍有不同，第三版去掉了 XTI），以下统称 UNP，如果需要会以 UNP2e、UNP3e 细分。 </p>
<p>UNP是Sockets API的权威指南，但是网络编程远不是使用那十几个Sockets API那么简单，作者 W. Richard Stevens深刻地认识到这一点，他在UNP2e的前言中写到：<a href="http://www.kohala.com/start/preface.unpv12e.html">http://www.kohala.com/start/preface.unpv12e.html</a> </p>
<p>I have found when teaching network programming that about 80% of all network programming problems have nothing to do with network programming, per se. That is, the problems are not with the API functions such as accept and select, but the problems arise from a lack of understanding of the underlying network protocols. For example, I have found that once a student understands TCP's three-way handshake and four-packet connection termination, many network programming problems are immediately understood. </p>
<p>搞网络编程，一定要熟悉TCP/IP协议及其外在表现（比如打开和关闭Nagle算法对收发包的影响），不然出点意料之外的情况就摸不着头脑了。我不知道为什么UNP3e在前言中去掉了这段至关重要的话。 </p>
<p>另外值得一提的是，UNP中文版翻译得相当好，译者杨继张先生是真懂网络编程的。 </p>
<p>UNP很详细，面面俱到，UDP、TCP、IPv4、IPv6都讲到了。要说有什么缺点的话，就是太详细了，重点不够突出。我十分赞同孟岩说的 </p>
<p>&#8220;（孟岩）我主张，在具备基础之后，学习任何新东西，都要抓住主线，突出重点。对于关键理论的学习，要集中精力，速战速决。而旁枝末节和非本质性的知识内容，完全可以留给实践去零敲碎打。 </p>
<p>&#8220;原因是这样的，任何一个高级的知识内容，其中都只有一小部分是有思想创新、有重大影响的，而其它很多东西都是琐碎的、非本质的。因此，集中学习时必须把握住真正重要那部分，把其它东西留给实践。对于重点知识，只有集中学习其理论，才能确保体系性、连贯性、正确性，而对于那些旁枝末节，只有边干边学能够让你了解它们的真实价值是大是小，才能让你留下更生动的印象。如果你把精力用错了地方，比如用集中大块的时间来学习那些本来只需要查查手册就可以明白的小技巧，而对于真正重要的、思想性东西放在平时零敲碎打，那么肯定是事倍功半，甚至适得其反。 </p>
<p>&#8220;因此我对于市面上绝大部分开发类图书都不满&#8212;&#8212;它们基本上都是面向知识体系本身的，而不是面向读者的。总是把相关的所有知识细节都放在一堆，然后一堆一堆攒起来变成一本书。反映在内容上，就是毫无重点地平铺直叙，不分轻重地陈述细节，往往在第三章以前就用无聊的细节谋杀了读者的热情。为什么当年侯捷先生的《深入浅出MFC》和 Scott Meyers 的 <em>Effective C++</em> 能够成为经典？就在于这两本书抓住了各自领域中的主干，提纲挈领，纲举目张，一下子打通读者的任督二脉。可惜这样的书太少，就算是已故 Richard Stevens 和当今 Jeffrey Richter 的书，也只是在体系性和深入性上高人一头，并不是面向读者的书。&#8221; </p>
<p>什么是旁枝末节呢？拿以太网来说，CRC32如何计算就是&#8220;旁枝末节&#8221;。网络程序员要明白check sum的作用，知道为什么需要check sum，至于具体怎么算CRC就不需要程序员操心。这部分通常是由网卡硬件完成的，在发包的时候由硬件填充CRC，在收包的时候网卡自动丢弃CRC不合格的包。如果代码里边确实要用到CRC计算，调用通用的zlib就行，也不用自己实现。 </p>
<p>UNP就像给了你一堆做菜的原料（各种Sockets 函数的用法），常用和不常用的都给了（Out-of-Band Data、Signal-Driven IO 等等），要靠读者自己设法取舍组合，做出一盘大菜来。在第一遍读的时候，我建议只读那些基本且重要的章节；另外那些次要的内容可略作了解，即便跳过不读也无妨。UNP是一本操作性很强的书，读这本这本书一定要上机练习。 </p>
<p>另外，UNP举的两个例子（菜谱）太简单，daytime和echo一个是短连接协议，一个是长连接无格式协议，不足以覆盖基本的网络开发场景（比如 TCP封包与拆包、多连接之间交换数据）。我估计 W. Richard Stevens 原打算在 UNP第三卷中讲解一些实际的例子，只可惜他英年早逝，我等无福阅读。 </p>
<p>UNP是一本偏重Unix传统的书，这本书写作的时候服务端还不需要处理成千上万的连接，也没有现在那么多网络攻击。书中重点介绍的以accept()+fork()来处理并发连接的方式在现在看来已经有点吃力，这本书的代码也没有特别防范恶意攻击。如果工作涉及这些方面，需要再进一步学习专门的知识（C10k问题，安全编程）。 </p>
<p>TCPv1和UNP应该先看哪本？我不知道。我自己是先看的TCPv1，花了大约半学期时间，然后再读UNP2e和APUE。 </p>
<p>&#183; 《<em>Effective TCP/IP Programming</em>》 </p>
<p>第三本书我犹豫了很久，不知道该推荐哪本，还有哪本书能与 W. Richard Stevens 的这两本比肩吗？W. Richard Stevens 为技术书籍的写作树立了难以逾越的标杆，他是一位伟大的技术作家。没能看到他写完 UNP 第三卷实在是人生的遗憾。 </p>
<p>《<em>Effective TCP/IP Programming</em>》这本书属于专家经验总结类，初看时觉得收获很大，工作一段时间再看也能有新的发现。比如第6 条&#8220;TCP是一个字节流协议&#8221;，看过这一条就不会去研究所谓的&#8220;TCP粘包问题&#8221;。我手头这本电力社2001年的中文版翻译尚可，但是很狗血的是把参考文献去掉了，正文中引用的文章资料根本查不到名字。人邮2011年重新翻译出版的版本有参考文献。 </p>
<h4>其他值得一看的书</h4>
<p>以下两本都不易读，需要相当的基础。 </p>
<p>&#183; 《<em>TCP/IP </em>Illustrated, <em>Vol. 2: The Implementation</em>》以下简称 TCPv2 </p>
<p>1200页的大部头，详细讲解了4.4BSD的完整TCP/IP协议栈，注释了15,000行C源码。这本书啃下来不容易，如果时间不充裕，我认为没必要啃完，应用层的网络程序员选其中与工作相关的部分来阅读即可。 </p>
<p>这本书第一作者是Gary Wright，从叙述风格和内容组织上是典型的&#8220;面向知识体系本身&#8221;，先讲mbuf，再从链路层一路往上、以太网、IP网络层、ICMP、IP多播、IGMP、IP路由、多播路由、Sockets系统调用、ARP等等。到了正文内容3/4的地方才开始讲TCP。面面俱到、主次不明。 </p>
<p>对于主要使用TCP的程序员，我认为TCPv2一大半内容可以跳过不看，比如路由表、IGMP等等（开发网络设备的人可能更关心这些内容）。在工作中大可以把IP视为host-to-host的协议，把&#8220;IP packet如何送达对方机器&#8221;的细节视为黑盒子，这不会影响对TCP的理解和运用，因为网络协议是分层的。这样精简下来，需要看的只有三四百页，四五千行代码，大大减轻了负担。 </p>
<p>这本书直接呈现高质量的工业级操作系统源码，读起来有难度，读懂它甚至要有&#8220;不求甚解的能力&#8221;。其一，代码只能看，不能上机运行，也不能改动试验。其二，与操作系统其他部分紧密关联。比如TCP/IP stack下接网卡驱动、软中断；上承inode转发来的系统调用操作；中间还要与平级的进程文件描述符管理子系统打交道；如果要把每一部分都弄清楚，把持不住就迷失主题了。其三，一些历史包袱让代码变复杂晦涩。比如BSD在80年代初需要在只有4M内存的VAX上实现TCP/IP，内存方面捉襟见肘，这才发明了mbuf结构，代码也增加了不少偶发复杂度（buffer不连续的处理）。 </p>
<p>读这套TCP/IP书切忌胶柱鼓瑟，这套书以4.4BSD为底，其描述的行为（特别是与timer相关的行为）与现在的Linux TCP/IP有不小的出入，用书本上的知识直接套用到生产环境的Linux系统可能会造成不小的误解和困扰。（TCPv3不重要，可以成套买来收藏，不读亦可。） </p>
<p>&#183; 《<em>Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects</em>》以下简称POSA2 </p>
<p>这本书总结了开发并发网络服务程序的模式，是对UNP很好的补充。UNP中的代码往往把业务逻辑和Sockets API调用混在一起，代码固然短小精悍，但是这种编码风格恐怕不适合开发大型的网络程序。POSA2强调模块化，网络通信交给library/framework去做，程序员写代码只关注业务逻辑，这是非常重要的思想。阅读这本书对于深入理解常用的event-driven网络库（libevent、Java Netty、Java Mina、Perl POE、Python Twisted等等）也很有帮助，因为这些库都是依照这本书的思想编写的。 </p>
<p>POSA2的代码是示意性的，思想很好，细节不佳。其C++ 代码没有充分考虑资源的自动化管理(RAII)，如果直接按照书中介绍的方式去实现网络库，那么会给使用者造成不小的负担与陷阱。换言之，照他说的做，而不是照他做的学。 </p>
<h4>不值一看的书</h4>
<p>Douglas Comer 教授名气很大，著作等身，但是他写的网络方面的书不值一读，味同嚼蜡。网络编程与 TCP/IP 方面，有W. Richard Stevens 的书扛鼎；计算机网络原理方面，有Kurose的&#8220;自顶向下&#8221;和Peterson的&#8220;系统&#8221;打旗，没其他人什么事儿。顺便一提，Tanenbaum的操作系统教材是最好的之一（嗯，之二，因为他写了两本：&#8220;现代&#8221;和&#8220;设计与实现&#8221;），不过他的计算机网络和体系结构教材的地位比不上他的操作系统书的地位。体系结构方面，Patterson 和 Hennessy二人合作的两本书是最好的，近年来崭露头角的《深入理解计算机系统》也非常好；当然，侧重点不同。 </p>
<p>(完)</p><img src ="http://www.cppblog.com/Solstice/aggbug/148129.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-06-06 08:44 <a href="http://www.cppblog.com/Solstice/archive/2011/06/06/148129.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之十：socks4a 代理服务器</title><link>http://www.cppblog.com/Solstice/archive/2011/06/02/147985.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Thu, 02 Jun 2011 15:02:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/06/02/147985.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/147985.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/06/02/147985.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/147985.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/147985.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之十：socks4a 代理服务器</p> <p>陈硕 (giantchen_AT_gmail)</p> <p>Blog.csdn.net/Solstice&nbsp; t.sina.com.cn/giantchen</p> <p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第十篇文章，本系列暂告一段落。</p> <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>本文介绍用 muduo 实现一个简单的 socks4a 代理服务器，代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/">http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/</a> 。</p> <h1>TCP 中继器</h1> <p>在实现 socks4a proxy 之前，我们先写一个功能更简单的网络程序—— TCP 中继器 (TCP relay)，或者叫做穷人的 tcpdump (poor man's tcpdump)。</p> <p>一般情况下，客户端程序直接连接服务端，如下图。有时候，我们想在 client 和 server 之间放一个中继器 (relay)，把 client 与 server 之间的通信内容记录下来。这时用 tcpdump 是最方便省事的，但是 tcpdump 需要 root 权限，万一没有 root 密码呢？穷人有穷人的办法，自己写一个 relay，让 client 连接 relay，再让 relay 连接 server，如下图中的 T 型结构，relay 扮演了类似 proxy 的角色。</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="relay" border="0" alt="relay" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/4b8b6e4110d7_12319/relay_725a0bef-f65a-4b76-beb9-96b4c964ec64.png" width="471" height="361"></p> <p>TcpRelay 是我们自己写的，可以动动手脚。除了记录通信内容，还可以制造延时，或者故意翻转 1 bit 数据以模拟 router 硬件故障。</p> <p>TcpRelay 的功能（业务逻辑）看上去很简单，无非是把连接 C 上收到的数据发给连接 S，同时把连接 S 上收到的数据发给连接 C。但仔细考虑起来，细节其实不那么简单：</p> <ul> <li>建立连接。为了真实模拟 client，TcpRelay 在 accept 连接 C 之后才向 server 发起连接 S，那么在 S 建立起来之前，从 C 收到数据怎么办？要不要暂存起来？</li> <li>并发连接的管理。上图中只画出了一个 client，实际上 TcpRelay 可以服务多个 clients，左右两边这些并发连接如何管理，如何防止串话(cross talk)？</li> <li>连接断开。Client 和 Server 都可能主动断开连接。当 Client 主动断开连接 C 时，TcpRelay 应该立刻断开 S。当 Server 主动断开连接 S 时，TcpRelay 应立刻断开 C。这样才能比较精确地模拟 Client 和 Server 的行为。在关闭连接的刹那，又有新的 client 连接进来，复用了刚刚 close 的 fd 号码，会不会造成串话？ 万一 Client 和 Server 几乎同时主动断开连接，TcpRelay 如何应对？</li> <li>速度不匹配。如果连接 C 的带宽是 100KB/s，而连接 S 的带宽是 10MB/s，不巧 Server 是个 chargen 服务，会全速发送数据，那么会不会撑爆 TcpRelay 的 buffer？如何限速？特别是在使用 non-blocking IO 和 level-trigger polling 的时候如何限制读取数据的速度？</li></ul> <p>在看 muduo 的实现之前，请读者思考：如果用 Sockets API 来实现 TcpRelay，如何解决以上这些问题。</p> <p>TcpRelay 的实现很简单，只有几十行代码 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/tcprelay.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/tcprelay.cc</a>，主要逻辑都在 Tunnel class 里</p> <p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/tunnel.h">http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/tunnel.h</a> 。这个实现解决了前三个问题，第四个留给将来吧。</p> <h1>Socks4a 代理服务器</h1> <p>Socks4a 的功能与 TcpRelay 非常相似，也是把连接 C 上收到的数据发给连接 S，同时把连接 S 上收到的数据发给连接 C。它与 TcpRelay 的区别在于，TcpRelay 固定连到某个 server 地址，而 socks4a 允许 client 指定要连哪个 server。在 accept 连接 C 之后，Socks4a server 会读几个字节，以了解 server 的地址，再发起连接 S。</p> <p>Socks4a 的协议非常简单，请参考维基百科 <a href="http://en.wikipedia.org/wiki/SOCKS#SOCKS_4a">http://en.wikipedia.org/wiki/SOCKS#SOCKS_4a</a> 。</p> <p>muduo 的 socks4a 代理服务器的实现在 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/socks4a.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/socks4a/socks4a.cc</a>，它也使用了 Tunnel class。与 TcpRelay 相比，只多了解析 server 地址这一步骤。</p> <p>muduo 这个 socks4a 是个标准的网络服务，可以供 Web 浏览器使用（我正是这么测试它的）。</p> <h1>n:1 与 1:n 连接转发</h1> <p>云风在《<a href="http://blog.codingnow.com/2011/05/xtunnel.html">写了一个 proxy 用途你懂的</a>》中写了一个 TCP 隧道 tunnel，程序由三部分组成：n:1 连接转发服务，1:n 连接转发服务，socks 代理服务。</p> <p>我仿照他的思路，用 muduo 实现了这三个程序。不同的是，我没有做数据混淆，所以不能用来翻传说中的墙。</p> <ul> <li>n:1 连接转发服务就是《<a href="http://blog.csdn.net/Solstice/archive/2011/05/02/6384968.aspx">Muduo 网络编程示例之七：“串并转换”连接服务器及其自动化测试</a>》中的 multiplexer (数据选择器)。</li> <li>1:n 连接转发服务是该文提到的 backend，一个数据分配器(demultiplexer)，代码在 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/demux.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/demux.cc</a></li> <li>socks 代理服务正是本文实现的 socks4a。</li></ul> <p>有兴趣的读者可以把这三个程序级联起来试一试。</p> <h1>Muduo 编程示例系列告一段落</h1> <p>《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》从今年2月初开始写，到今天正好是四个月，我写了十一篇博客，基本按计划完成了任务。这个系列暂告一段落。</p> <p>这个系列基本涵盖了 muduo 为编写单线程服务端和客户端 TCP 网络程序提供的功能，muduo 的能力不止于此：</p> <ul> <li>多线程，muduo::net::TcpServer 内置了一个简单但适应性很强的线程模型。目前博客上的例子涉及的业务逻辑很简单，没有复杂的运算，瓶颈通常在 IO 上，多线程的优势发挥不出来。</li> <li>高级应用。比方说用 muduo::net::Channel 配合 signalfd 来处理信号；其他非阻塞网络客户端库（例如 ZooKeeper 的 C 客户端，PostgreSQL 的客户端 libpq）与 muduo EventLoop 的集成。</li></ul> <p>以上两点在以后的文章里会提及，不会明珠暗藏。</p> <p>Muduo 在 2010 年 8 月底发布 0.1.0 版，随着这个编程示例系列文章的发表，迄今已发布了 14 次小升级，下载地址： <a href="http://code.google.com/p/muduo/downloads/list">http://code.google.com/p/muduo/downloads/list</a></p> <h1>接下来的计划</h1> <p>接下来，我还会写一系列博客，目前想到的有：</p> <ol> <li>谈一谈我的网络编程学习经验。文章已经完成大半，端午节之后可以发布。</li> <li>muduo 设计与实现系列，介绍如何一步步实现一个非阻塞网络库。代码已经准备得差不多了，在 <a href="https://github.com/chenshuo/recipes/tree/master/reactor">https://github.com/chenshuo/recipes/tree/master/reactor</a></li> <li>用 muduo 实现一些稍微复杂一些的网络程序，比如小规模的分布式系统。计划有：利用 Paxos 算法实现一个高可用的 in-memory key value 存储，在此基础上实现 naming service，然后实现我以前多次提到的简单机群管理系统等等。目前 muduo 的示例程序都是简单独立的网络程序，下半年我想多写一写由多个程序组成的系统，具体谈一谈分布式系统细节设计。</li></ol> <p>另外，我会逐步把已有的博客文章整理成 PDF 合集，方便下载保存，地址是： <a href="http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx">http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx</a></p><img src ="http://www.cppblog.com/Solstice/aggbug/147985.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-06-02 23:02 <a href="http://www.cppblog.com/Solstice/archive/2011/06/02/147985.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之九：简单的消息广播服务</title><link>http://www.cppblog.com/Solstice/archive/2011/05/25/147131.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 25 May 2011 15:21:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/05/25/147131.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/147131.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/05/25/147131.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/147131.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/147131.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之九：简单的消息广播服务</p>
<p>陈硕 (giantchen_AT_gmail)</p>
<p>Blog.csdn.net/Solstice&nbsp; t.sina.com.cn/giantchen</p>
<p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第九篇文章，讲用 muduo 实现一个简单的 <a href="http://en.wikipedia.org/wiki/Publish/subscribe">pub/sub 服务</a>。</p>
<p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p>
<p>本文介绍用 muduo 实现一个简单的 topic-based 消息广播服务，这其实是&#8220;聊天室&#8221;的一个简单扩展，不过聊天的不是人，而是分布式系统中的程序。</p>
<p>本文的代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub">http://code.google.com/p/muduo/source/browse/trunk/examples/hub</a></p>
<p>&nbsp;</p>
<p>在分布式系统中，除了常用的 end-to-end 通信，还有一对多的广播通信。一提到&#8220;广播&#8221;，或许会让人联想到 IP 多播或 IP 组播，这不是本文的主题。本文将要谈的是基于 TCP 协议的应用层广播。示意图如下：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="hub" border="0" alt="hub" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/d59aa822e826_12D5E/hub_030fc751-30fe-4b70-a7ef-e1b2cd874029.png" width="885" height="443" /></p>
<p>上图中圆角矩形代表程序，"Hub"是一个服务程序，不是网络集线器，它起到类似集线器的作用，故而得名。Publisher 和 Subscriper 通过 TCP 协议与 Hub 程序通信。Publisher 把消息发到某个 topic 上，Subscribers 订阅该 topic，然后就能收到消息。即 publisher 借助 hub 把消息广播给了多个 subscribers。这种 pub/sub 结构的好处在于可以增加多个 Subscriber 而不用修改 Publisher，一定程度上实现了&#8220;解耦&#8221;（也可以看成分布式的 observer pattern）。 由于走的是 TCP 协议，广播是基本可靠的，这里的&#8220;可靠&#8221;指的是&#8220;比 UDP 可靠&#8221;，不是&#8220;完全可靠&#8221;。（思考：如何避免 Hub 成为 single point of failure？）</p>
<p>为了避免串扰（cross-talk），每个 topic 在同一时间只应该有一个 publisher，hub 不提供 compare-and-swap 操作。</p>
<p>（&#8220;可靠广播、原子广播&#8221;在分布式系统中有重大意义，是以 replicated state machine 方式实现可靠的分布式服务的基础，&#8220;可靠广播&#8221;涉及 consensus 算法，超出了本文的范围。）</p>
<p>&nbsp;</p>
<p>应用层广播在分布式系统中用处很大，这里略举几例：</p>
<p>1. 体育比分转播。有 8 片比赛场地正在进行羽毛球比赛，每个场地的计分程序把当前比分发送到各自的 topic 上（第 1 号场地发送到 court1，第 2 号发送到 court2，以此类推）。需要用到比分的程序（赛场的大屏幕显示，网上比分转播等等）自己订阅感兴趣的 topic ，就能及时收到最新比分数据。由于本文实现的不是 100% 可靠广播，那么<strong>消息应该是 snapshot，而不是 incremental</strong>。（换句话说，消息的内容是&#8220;现在是几比几&#8221;，而不是&#8220;刚才谁得分&#8221;。）</p>
<p>2. 负载监控。每台机器上运行一个监控程序，周期性地把本机当前负载（CPU、网络、磁盘、温度）publish 到以 hostname 命名的 topic 上，这样需要用到这些数据的程序只要在 hub 订阅相应的 topic 就能获得数据，无需与多台机器直接打交道。（为了可靠起见，监控程序发送的消息里边应该包含时间戳，这样能防止 stale 数据，甚至一定程度上起到心跳的作用。）沿着这个思路，分布式系统中的服务程序也可以把自己的当前负载发布到 hub 上，供 load balancer 和 monitor 取用。</p>
<h1>协议</h1>
<p>为了简单起见，muduo 的 hub 示例采用以 '\r\n' 分界的文本协议，这样用 telnet 就能测试 hub。协议只有三个命令：</p>
<ul><li>sub &lt;topic&gt;\r\n 
<ul><li>该命令表示订阅 &lt;topic&gt;，以后该 topic 有任何跟新都会发给这个 tcp 连接。在 sub 的时候，hub 会把该 &lt;topic&gt; 上最近的消息发给此 subscriber。</li></ul></li><li>unsub &lt;topic&gt;\r\n 
<ul><li>该命令表示退订 &lt;topic&gt;</li></ul></li><li>pub &lt;topic&gt;\r\n&lt;content&gt;\r\n 
<ul><li>往 &lt;topic&gt; 发送消息，内容为 &lt;content&gt;。所有订阅了此 &lt;topic&gt; 的 subscribers 会收到同样的消息&#8220;pub &lt;topic&gt;\r\n&lt;content&gt;\r\n&#8221;</li></ul></li></ul>
<h1>代码</h1>
<p>muduo 示例中的 hub 分为几个部分：</p>
<ul><li>hub 服务程序，负责一对多的消息分发。它会记住每个 client 订阅了哪些 topic，只把消息发给特定的订阅者。代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub/hub.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/hub/hub.cc</a></li><li>pubsub 库，为了方便编写使用 hub 服务的应用程序，我写了一个简单的 client library，用来和 hub 打交道。这个 library 可以订阅 topic、退订 topic、往指定 topic 发布消息。代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pubsub.h">http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pubsub.h</a> 和 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pubsub.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pubsub.cc</a></li><li>sub 示例程序，这个命令行程序订阅一个或多个 topic，然后等待 hub 的数据。代码 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub/sub.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/hub/sub.cc</a></li><li>pub 示例程序，这个命令行程序往某个 topic 发布一条消息，消息内容由命令行参数指定。代码 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pub.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/hub/pub.cc</a></li></ul>
<p>一个程序可以既是 publisher 又是 subscriber，而且 pubsub 库只用一个 tcp 连接（这样 failover 比较简便）。</p>
<p>使用范例：</p>
<ol><li>开启 4 个命令行窗口</li><li>在第一个窗口运行 $ hub 9999</li><li>在第二个窗口运行 $ sub 127.0.0.1:9999 mytopic</li><li>在第三个窗口运行 $ sub 127.0.0.1:9999 mytopic court</li><li>在第四个窗口运行 $ pub 127.0.0.1:9999 mytopic "Hello world."&nbsp; ，这时第二三号窗口都会打印 &#8220;mytopic: Hello world.&#8221;，表明收到了 mytopic 这个主题上的消息。</li><li>在第四个窗口运行 $ pub 127.0.0.1:9999 court "13:11"&nbsp; ，这时第三号窗口会打印 &#8220;court: 13:11&#8221;，表明收到了 court 这个主题上的消息。第二号窗口没有订阅此消息，故无输出。</li></ol>
<p>借助这个简单的 pub/sub 机制，还可以做很多有意思的事情。比如把分布式系统中的程序的一部分 end-to-end 通信改为通过 pub/sub 来做（例如，原来是 A 向 B 发一个 SOAP request，B 通过同一个 tcp 连接发回 response (分析二者的通信只能通过查看 log 或用 tcpdump 截获）；现在是 A 往 topic_a_to_b 上发布 request，B 在 topic_b_to_a 上发 response），这样多挂一个 monitoring subscriber 就能轻易地查看通信双方的沟通情况，很容易做状态监控与 trouble shooting。</p><img src ="http://www.cppblog.com/Solstice/aggbug/147131.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-05-25 23:21 <a href="http://www.cppblog.com/Solstice/archive/2011/05/25/147131.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之八：用 Timing wheel 踢掉空闲连接</title><link>http://www.cppblog.com/Solstice/archive/2011/05/04/145691.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 04 May 2011 13:19:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/05/04/145691.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/145691.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/05/04/145691.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/145691.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/145691.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之八：Timing wheel 踢掉空闲连接</p>
<p>陈硕 (giantchen_AT_gmail)</p>
<p>Blog.csdn.net/Solstice&nbsp; <a href="http://t.sina.com.cn/giantchen">t.sina.com.cn/giantchen</a></p>
<p>这是《Muduo 网络编程示例》系列的第八篇文章，原计划讲文件传输，这里插入一点计划之外的内容。</p>
<p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p>
<p>本文介绍如何使用 timing wheel 来踢掉空闲的连接，一个连接如果若干秒没有收到数据，就认为是空闲连接。</p>
<p>本文的代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/idleconnection">http://code.google.com/p/muduo/source/browse/trunk/examples/idleconnection</a></p>
<p>&nbsp;</p>
<p>在严肃的网络程序中，应用层的心跳协议是必不可少的。应该用心跳消息来判断对方进程是否能正常工作，&#8220;踢掉空闲连接&#8221;只是一时权宜之计。我这里想顺便讲讲 shared_ptr 和 weak_ptr 的用法。</p>
<p>如果一个连接连续几秒钟（后文以 8s 为例）内没有收到数据，就把它断开，为此有两种简单粗暴的做法：</p>
<ul>
    <li>每个连接保存&#8220;最后收到数据的时间 lastReceiveTime&#8221;，然后用一个定时器，每秒钟遍历一遍所有连接，断开那些 (now - connection.lastReceiveTime) &gt; 8s 的 connection。这种做法全局只有一个 repeated timer，不过每次 timeout 都要检查全部连接，如果连接数目比较大（几千上万），这一步可能会比较费时。
    <li>每个连接设置一个 one-shot timer，超时定为 8s，在超时的时候就断开本连接。当然，每次收到数据要去更新 timer。这种做法需要很多个 one-shot timer，会频繁地更新 timers。如果连接数目比较大，可能对 reactor 的 timer queue 造成压力。</li>
</ul>
<p>使用 timing wheel 能避免上述两种做法的缺点。timing wheel 可以翻译为&#8220;时间轮盘&#8221;或&#8220;刻度盘&#8221;，本文保留英文。</p>
<p>连接超时不需要精确定时，只要大致 8 秒钟超时断开就行，多一秒少一秒关系不大。处理连接超时可以用一个简单的数据结构：8 个桶组成的循环队列。第一个桶放下一秒将要超时的连接，第二个放下 2 秒将要超时的连接。每个连接一收到数据就把自己放到第 8 个桶，然后在每秒钟的 callback 里把第一个桶里的连接断开，把这个空桶挪到队尾。这样大致可以做到 8 秒钟没有数据就超时断开连接。更重要的是，每次不用检查全部的 connection，只要检查第一个桶里的 connections，相当于把任务分散了。</p>
<h1>Timing wheel 原理</h1>
<p>《Hashed and hierarchical timing wheels: efficient data structures for implementing a timer facility》这篇论文详细比较了实现定时器的各种数据结构，并提出了层次化的 timing wheel 与 hash timing wheel 等新结构。针对本文要解决的问题的特点，我们不需要实现一个通用的定时器，只用实现 simple timing wheel 即可。</p>
<p>Simple timing wheel 的基本结构是一个循环队列，还有一个指向队尾的指针 (tail)，这个指针每秒钟移动一格，就像钟表上的时针，timing wheel 由此得名。</p>
<p>以下是某一时刻 timing wheel 的状态，格子里的数字是倒计时（与通常的 timing wheel 相反），表示这个格子（桶子）中的连接的剩余寿命。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel1 border=0 alt=wheel1 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel1_26649bd2-811d-4c55-ab01-a7d1d69cf4d2.png" width=297 height=297></p>
<p>一秒钟以后，tail 指针移动一格，原来四点钟方向的格子被清空，其中的连接已被断开。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel2 border=0 alt=wheel2 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel2_237b5de9-94e0-4d02-8af6-6c11ade2abf1.png" width=297 height=297></p>
<h3>连接超时被踢掉的过程</h3>
<p>假设在某个时刻，conn 1 到达，把它放到当前格子中，它的剩余寿命是 7 秒。此后 conn 1 上没有收到数据。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel3 border=0 alt=wheel3 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel3_beea0fd8-1268-49e6-9c21-ad59795144ae.png" width=395 height=297></p>
<p>1 秒钟之后，tail 指向下一个格子，conn 1 的剩余寿命是 6 秒。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel4 border=0 alt=wheel4 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel4_ad067035-8462-42c5-b33c-6d7b40faf5ec.png" width=395 height=297></p>
<p>又过了几秒钟，tail 指向 conn 1 之前的那个格子，conn 1 即将被断开。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel5 border=0 alt=wheel5 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel5_7d0e810c-87f1-4e49-ba0b-535d39aab3c8.png" width=395 height=297></p>
<p>下一秒，tail 重新指向 conn 1 原来所在的格子，清空其中的数据，断开 conn 1 连接。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel6 border=0 alt=wheel6 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel6_54f35ddd-452d-4ff8-aeed-fdaa2aa35287.png" width=411 height=297></p>
<h3>连接刷新</h3>
<p>如果在断开 conn 1 之前收到数据，就把它移到当前的格子里。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel4 border=0 alt=wheel4 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel4_da2f6a05-b4c1-4e23-bfb0-705f474ce47a.png" width=395 height=297></p>
<p>收到数据，conn 1 的寿命延长为 7 秒。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel7 border=0 alt=wheel7 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel7_999067a1-cb64-47df-b2c3-113b84ac30ed.png" width=395 height=297></p>
<p>时间继续前进，conn 1 寿命递减，不过它已经比第一种情况长寿了。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel8 border=0 alt=wheel8 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel8_fa1932b2-56b0-4528-a57f-4625ae38542d.png" width=395 height=297></p>
<h3>多个连接</h3>
<p>timing wheel 中的每个格子是个 hash set，可以容纳不止一个连接。</p>
<p>比如一开始，conn 1 到达。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel3 border=0 alt=wheel3 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel3_31111b7b-9e9d-4ea0-83fe-832f9b091e69.png" width=395 height=297></p>
<p>随后，conn 2 到达，这时候 tail 还没有移动，两个连接位于同一个格子中，具有相同的剩余寿命。（下图中画成链表，代码中是哈希表。）</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel9 border=0 alt=wheel9 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel9_05feeadf-8733-4577-94be-27462fd5762b.png" width=539 height=297></p>
<p>几秒钟之后，conn 1 收到数据，而 conn 2 一直没有收到数据，那么 conn 1 被移到当前的格子中。这时 conn 1 的寿命比 conn 2 长。</p>
<p><img style="BACKGROUND-IMAGE: none; BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px; PADDING-TOP: 0px" title=wheel10 border=0 alt=wheel10 src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_7386/wheel10_8a944ded-d9f6-48a6-9917-45579d672e14.png" width=495 height=297></p>
<h1>代码实现与改进</h1>
<p>我们用以前多次出现的 EchoServer 来说明具体如何实现 timing wheel。代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/idleconnection">http://code.google.com/p/muduo/source/browse/trunk/examples/idleconnection</a></p>
<p>在具体实现中，格子里放的不是连接，而是一个特制的 Entry struct，每个 Entry 包含 TcpConnection 的 weak_ptr。Entry 的析构函数会判断连接是否还存在（用 weak_ptr），如果还存在则断开连接。</p>
<p>数据结构：</p>
<pre class=prettyprint>  typedef boost::weak_ptr&lt;muduo::net::TcpConnection&gt; WeakTcpConnectionPtr;
struct Entry : public muduo::copyable
{
Entry(const WeakTcpConnectionPtr&amp; weakConn)
: weakConn_(weakConn)
{
}
~Entry()
{
muduo::net::TcpConnectionPtr conn = weakConn_.lock();
if (conn)
{
conn-&gt;shutdown();
}
}
WeakTcpConnectionPtr weakConn_;
};
typedef boost::shared_ptr&lt;Entry&gt; EntryPtr;
typedef boost::weak_ptr&lt;Entry&gt; WeakEntryPtr;
typedef boost::unordered_set&lt;EntryPtr&gt; Bucket;
typedef boost::circular_buffer&lt;Bucket&gt; WeakConnectionList;
</pre>
<p>在实现中，为了简单起见，我们不会真的把一个连接从一个格子移到另一个格子，而是采用引用计数的办法，用 shared_ptr 来管理 Entry。如果从连接收到数据，就把对应的 EntryPtr 放到这个格子里，这样它的引用计数就递增了。当 Entry 的引用计数递减到零，说明它没有在任何一个格子里出现，那么连接超时，Entry 的析构函数会断开连接。</p>
<p>Timing wheel 用 boost::circular_buffer 实现，其中每个 Bucket 元素是个 hash set of EntryPtr。</p>
<p>&nbsp;</p>
<p>在构造函数中，注册每秒钟的回调（EventLoop::runEvery() 注册 EchoServer::onTimer() ），然后把 timing wheel 设为适当的大小。</p>
<pre class=prettyprint>EchoServer::EchoServer(EventLoop* loop,
const InetAddress&amp; listenAddr,
int idleSeconds)
: loop_(loop),
server_(loop, listenAddr, "EchoServer"),
connectionBuckets_(idleSeconds)
{
server_.setConnectionCallback(
boost::bind(&amp;EchoServer::onConnection, this, _1));
server_.setMessageCallback(
boost::bind(&amp;EchoServer::onMessage, this, _1, _2, _3));
loop-&gt;runEvery(1.0, boost::bind(&amp;EchoServer::onTimer, this));
connectionBuckets_.resize(idleSeconds);
}</pre>
<p>其中 EchoServer::onTimer() 的实现只有一行：往队尾添加一个空的 Bucket，这样 circular_buffer 会自动弹出队首的 Bucket，并析构之。在析构 Bucket 的时候，会依次析构其中的 EntryPtr 对象，这样 Entry 的引用计数就不用我们去操心，C++ 的值语意会帮我们搞定一切。</p>
<pre class=prettyprint>void EchoServer::onTimer()
{
connectionBuckets_.push_back(Bucket());
}</pre>
<p>在连接建立时，创建一个 Entry 对象，把它放到 timing wheel 的队尾。另外，我们还需要把 Entry 的弱引用保存到 TcpConnection 的 context 里，因为在收到数据的时候还要用到 Entry。（<strong>思考题</strong>：如果 TcpConnection::setContext 保存的是强引用 EntryPtr，会出现什么情况？）</p>
<pre class=prettyprint>void EchoServer::onConnection(const TcpConnectionPtr&amp; conn)
{
LOG_INFO &lt;&lt; "EchoServer - " &lt;&lt; conn-&gt;peerAddress().toHostPort() &lt;&lt; " -&gt; "
&lt;&lt; conn-&gt;localAddress().toHostPort() &lt;&lt; " is "
&lt;&lt; (conn-&gt;connected() ? "UP" : "DOWN");
if (conn-&gt;connected())
{
EntryPtr entry(new Entry(conn));
connectionBuckets_.back().insert(entry);
WeakEntryPtr weakEntry(entry);
conn-&gt;setContext(weakEntry);
}
else
{
assert(!conn-&gt;getContext().empty());
WeakEntryPtr weakEntry(boost::any_cast&lt;WeakEntryPtr&gt;(conn-&gt;getContext()));
LOG_DEBUG &lt;&lt; "Entry use_count = " &lt;&lt; weakEntry.use_count();
}
}</pre>
<p>在收到消息时，从 TcpConnection 的 context 中取出 Entry 的弱引用，把它提升为强引用 EntryPtr，然后放到当前的 timing wheel 队尾。（<strong>思考题</strong>，为什么要把 Entry 作为 TcpConnection 的 context 保存，如果这里再创建一个新的 Entry 会有什么后果？）</p>
<pre class=prettyprint>void EchoServer::onMessage(const TcpConnectionPtr&amp; conn,
Buffer* buf,
Timestamp time)
{
string msg(buf-&gt;retrieveAsString());
LOG_INFO &lt;&lt; conn-&gt;name() &lt;&lt; " echo " &lt;&lt; msg.size() &lt;&lt; " bytes at " &lt;&lt; time.toString();
conn-&gt;send(msg);
assert(!conn-&gt;getContext().empty());
WeakEntryPtr weakEntry(boost::any_cast&lt;WeakEntryPtr&gt;(conn-&gt;getContext()));
EntryPtr entry(weakEntry.lock());
if (entry)
{
connectionBuckets_.back().insert(entry);
}
}
</pre>
<p>然后呢？没有然后了，程序已经完成了我们想要的功能。（完整的代码会打印 circular_buffer 变化的情况，运行一下即可理解。）</p>
<p>希望本文有助于您理解 shared_ptr 和 weak_ptr。</p>
<h3>改进</h3>
<p>在现在的实现中，每次收到消息都会往队尾添加 EntryPtr （当然，hash set 会帮我们去重。）一个简单的改进措施是，在 TcpConnection 里保存&#8220;最后一次往队尾添加引用时的 tail 位置&#8221;，然后先检查 tail 是否变化，若无变化则不重复添加 EntryPtr。这样或许能提高效率。</p>
<p>以上改进留作练习。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/145691.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-05-04 21:19 <a href="http://www.cppblog.com/Solstice/archive/2011/05/04/145691.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之七：&amp;ldquo;串并转换&amp;rdquo;连接服务器及其自动化测试</title><link>http://www.cppblog.com/Solstice/archive/2011/05/02/145506.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Mon, 02 May 2011 11:47:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/05/02/145506.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/145506.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/05/02/145506.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/145506.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/145506.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之七：连接服务器及其自动化测试 </p> <p>陈硕 (giantchen_AT_gmail)  <p>Blog.csdn.net/Solstice&nbsp; <a href="http://t.sina.com.cn/giantchen">t.sina.com.cn/giantchen</a>  <p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第七篇文章。  <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a>  <p>本文介绍如何使用 test harness 来测试一个具有内部逻辑的网络服务程序。</p> <p>本文的代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer">http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer</a></p> <p>下载地址：<a title="http://muduo.googlecode.com/files/muduo-0.2.0-alpha.tar.gz" href="http://muduo.googlecode.com/files/muduo-0.2.0-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.2.0-alpha.tar.gz</a> SHA1 checksum: 75a09a82f96b583004876e95105c679e64c95715</p> <p>&nbsp;</p> <p><a href="http://blog.codingnow.com/">云风</a>在他的博客中提到了<a href="http://blog.codingnow.com/2010/11/go_prime.html">网游连接服务器的功能需求</a>（搜“练手项目”），我用 C++ 初步实现了这些需求，并为之编写了配套的自动化 test harness，作为 muduo 网络库的示例。</p> <p>注意：本文呈现的代码仅仅实现了基本的功能需求，没有考虑安全性，也没有特别优化性能，不适合用作真正的放在公网上运行的网游连接服务器。</p> <h1>功能需求</h1> <p>这个连接服务器把多个客户连接汇聚为一个内部 TCP 连接，起到“数据串并转换”的作用，让 backend 的逻辑服务器专心处理业务，而无需顾及多连接的并发性。以下是系统的框图：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="multiplexer" border="0" alt="multiplexer" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_D7EC/multiplexer_671bb44b-d8ff-4b62-8bf6-0b585f6ac739.png" width="729" height="305"></p> <p>这个连接服务器的作用与数字电路中的数据选择器 (multiplexer) 类似，所以我把它命名为 multiplexer。（其实 IO-Multiplexing 也是取的这个意思，让一个 thread-of-control 能有选择地处理多个 IO 文件描述符。）</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="mux" border="0" alt="mux" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_D7EC/mux_8609f0e5-cfd0-4ecf-a8e9-b1afe1348cfd.png" width="657" height="261"></p> <p>(上图取自 wikipedia，是 public domain 版权)</p> <h1>实现</h1> <p>Multiplexer 的功能需求不复杂，无非是在 backend connection 和 client connections 之间倒腾数据。具体来说，主要是处理四种事件：</p> <ul> <li>对每个新 client connection 分配一个新的整数 id，如果 id 用完了，则断开新连接（这样通过控制 id 的数目就能控制最大连接数）。另外，为了避免 id 过快地被复用（有可能造成 backend 串话），multiplexer 采用 queue 来管理 free id，每次从队列的头部取 id，用完之后放回 queue 的尾部。</li> <li>当 client connection 到达或断开时，向 backend 发出通知。onClientConnection() <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#54">http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#54</a></li> <li>当从 client connection 收到数据时，把数据连同 connection id 一同发给 backend。onClientMessage() <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#117">http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#117</a></li> <li>当从 backend connection 收到数据时，辨别数据是发给哪个 client connection，并执行相应的转发操作。onBackendMessage() <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#194">http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#194</a></li> <li>如果 backend connection 断开连接，则断开所有 client connections（假设 client 会自动重试）。 onBackendConnection() <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#162">http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#162</a></li></ul> <p>由上可见，multiplexer 的功能与 proxy 颇为类似。<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/multiplexer_simple.cc">multiplexer_simple.cc</a> 是一个线程版的实现，借助 muduo 的 io-multiplexing 特性，可以方便地处理多个并发连接。</p> <p>在实现的时候有两点值得注意：</p> <ul> <li><strong>TcpConnection 的 id 如何存放</strong>？当从 backend 收到数据，如何根据 id 找到对应的 client connection？当从 client connection 收到数据，如何得知其 id ？</li></ul> <p>第一个问题比较好解决，用 std::map〈int, TcpConnectionPtr〉 clientConns_; 保存从 id 到 client connection 的映射就行。</p> <p>第二个问题固然可以用类似的办法解决，但是我想借此介绍一下 <strong>muduo::net::TcpConnection 的 context 功能</strong>。每个 TcpConnection 都有一个 boost::any 成员，可由客户代码自由支配(get/set)，代码如下。这个 boost::any 是 TcpConnection 的 context，可以用于保存与 connection 绑定的任意数据（比方说 connection id、connection 的最后数据到达时间、connection 所代表的用户的名字等等）。这样客户代码不必继承 TcpConnection 就能 attach 自己的状态，而且也用不着 TcpConnectionFactory 了（如果允许继承，那么必然要向 TcpServer 注入此 factory）。</p><pre class="prettyprint">class TcpConnection : public boost::enable_shared_from_this&lt;TcpConnection&gt;,
                      boost::noncopyable
{
 public:

  void <font color="#0000ff">setContext</font>(const boost::any&amp; context)
  { context_ = context; }

  boost::any&amp; <font color="#0000ff">getContext</font>()
  { return context_; }

  const boost::any&amp; getContext() const
  { return context_; }

  // ...

 private:
  // ...
  <font color="#0000ff">boost::any context_</font>;
};

typedef boost::shared_ptr&lt;TcpConnection&gt; TcpConnectionPtr;</pre>

<p>对于 Multiplexer，在 <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#54">onClientConnection()</a> 里调用 <a href="http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#76">conn-&gt;setContext(id)</a>，把 id 存到 TcpConnection 对象中。onClientMessage() 从 TcpConnection 对象中取得 id，连同数据一起发送给 backend，完整实现如下：</p><pre class="prettyprint">  void onClientMessage(const TcpConnectionPtr&amp; conn, Buffer* buf, Timestamp)
  {
    if (!conn-&gt;getContext().empty())
    {
      <font color="#0000ff">int id = boost::any_cast&lt;int&gt;(conn-&gt;getContext());</font>
      sendBackendBuffer(id, buf);
    }
    else
    {
      buf-&gt;retrieveAll();
    }
  }</pre>

<ul>
<li><strong>TcpConnection 的生命期如何管理</strong>？由于 Client Connection 是动态创建并销毁，其生与灭完全由客户决定，如何保证 backend 想向它发送数据的时候，这个 TcpConnection 对象还活着？解决思路是用 reference counting，当然，不用自己写，用 boost::shared_ptr 即可。TcpConnection 是 muduo 中唯一默认采用 shared_ptr 来管理生命期的对象，盖由其动态生命期的本质决定。更多内容请参考陈硕《<a href="http://blog.csdn.net/Solstice/archive/2010/01/22/5238671.aspx">当析构函数遇到多线程──C++ 中线程安全的对象回调</a>》</li></ul>
<p>multiplexer 是二进制协议，如何测试呢？</p>
<h1>自动化测试</h1>
<p>Multiplexer 是 <a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">muduo 网络编程示例</a>中第一个具有 non-trivial 业务逻辑的网络程序，根据陈硕《<a href="http://blog.csdn.net/Solstice/archive/2011/04/25/6359748.aspx">分布式程序的自动化回归测试</a>》一文的思想，我为它编写了 test harness。代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/harness/src/com/chenshuo/muduo/example/multiplexer">http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/harness/src/com/chenshuo/muduo/example/multiplexer</a></p>
<p>这个 Test harness 采用 Java 编写，用的是 Netty 库。这个 test harness 要扮演 clients 和 backend，也就是既要主动发起连接，也要被动接受连接。结构如下：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="harness" border="0" alt="harness" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_D7EC/harness_ce7924b7-3e1d-4035-9bf1-dfa0d6839b8b.png" width="405" height="333"></p>
<p>Test harness 会把各种 event 汇聚到一个 blocking queue 里边，方便编写 test case。Test case 则操纵 test harness，发起连接、发送数据、检查收到的数据，例如以下是其中一个 test case</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/harness/src/com/chenshuo/muduo/example/multiplexer/testcase/TestOneClientSend.java">http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/harness/src/com/chenshuo/muduo/example/multiplexer/testcase/TestOneClientSend.java</a></p>
<p>这里的几个 test cases 都以用 java 直接写的，如果有必要，也可以采用 Groovy 来编写，这样可以在不重启 test harness 的情况下随时修改添加 test cases。具体做法见陈硕《<a href="http://www.cnblogs.com/Solstice/archive/2011/04/22/2024791.html">“过家家”版的移动离线计费系统实现</a>》。</p>
<h3>将来的改进</h3>有了这个自动化的 test harness，我们可以比较方便且安全地修改（甚至重新设计）multiplexer。例如<br>
<ul>
<li>增加“backend 发送指令断开 client connection”的功能。有了自动化测试，这个新功能可以被单独测试（指开发者测试），而不需要真正的 backend 参与进来。</li>
<li>将 Multiplexer 改用多线程重写。有了自动化回归测试，我们不用担心破坏原有的功能，可以放心大胆地重写。而且由于 test harness 是从外部测试，不是单元测试，重写 multiplexer 的时候不用动 test cases，这样保证了测试的稳定性。另外，这个 test harness 稍作改进还可以进行 stress testing，既可用于验证多线程 multiplexer 的正确性，亦可对比其相对单线程版的效率提升。</li></ul><img src ="http://www.cppblog.com/Solstice/aggbug/145506.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-05-02 19:47 <a href="http://www.cppblog.com/Solstice/archive/2011/05/02/145506.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之六：限制服务器的最大并发连接数</title><link>http://www.cppblog.com/Solstice/archive/2011/04/27/145102.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Tue, 26 Apr 2011 16:03:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/04/27/145102.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/145102.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/04/27/145102.html#Feedback</comments><slash:comments>9</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/145102.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/145102.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之六：限制服务器的最大并发连接数</p> <p>陈硕 (giantchen_AT_gmail)</p> <p>Blog.csdn.net/Solstice&nbsp; <a href="http://t.sina.com.cn/giantchen">t.sina.com.cn/giantchen</a></p> <p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第六篇文章。</p> <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>&nbsp;</p> <p>本文已以大家都熟悉的 EchoServer 介绍如何限制服务器的并发连接数。</p> <p>本文的代码见 <a title="http://code.google.com/p/muduo/source/browse/trunk/examples/maxconnection/
" href="http://code.google.com/p/muduo/source/browse/trunk/examples/maxconnection/">http://code.google.com/p/muduo/source/browse/trunk/examples/maxconnection/<br></a></p> <p>《Muduo 网络编程示例 系列》计划中的第六篇文章原本是“用于测试两台机器的带宽的 pingpong 程序”，pingpong 协议的程序已经在《<a href="http://blog.csdn.net/Solstice/archive/2010/09/04/5863411.aspx">muduo 与 boost asio 吞吐量对比</a>》和《<a href="http://blog.csdn.net/Solstice/archive/2010/09/05/5864889.aspx">muduo 与 libevent2 吞吐量对比</a>》两篇文章中介绍过了，所以我改为写另外一个有点意思的主题。</p> <p>这篇文章中的“并发连接数”是指一个 server program 能同时支持的客户端连接数，连接系由客户端主动发起，服务端被动接受(accept)连接。（如果要限制应用程序主动发起的连接，则问题要简单得多，毕竟主动权和决定权都在程序本身。）</p> <h1>为什么要限制并发连接数？</h1> <p>一方面，我们不希望服务程序超载，另一方面，更因为 file descriptor 是稀缺资源，如果出现 file descriptor 耗尽，很棘手（跟 “malloc 失败/new() 抛出 std::bad_alloc”差不多同样棘手）。</p> <p>我在《<a href="http://blog.csdn.net/Solstice/archive/2010/10/19/5950190.aspx">分布式系统的工程化开发方法</a>》一文中曾谈到 libev 作者建议的一种<a href="http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#The_special_problem_of_accept_ing_wh">应对“accept()ing 时 file descriptor 耗尽”</a>的办法。</p> <p>&nbsp;</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="幻灯片35" border="0" alt="幻灯片35" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_13DA6/%E5%B9%BB%E7%81%AF%E7%89%8735.png" width="640" height="480"></p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="幻灯片36" border="0" alt="幻灯片36" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo-_13DA6/%E5%B9%BB%E7%81%AF%E7%89%8736.png" width="640" height="480"></p> <p>Muduo 的 acceptor 正是这么实现的，但是，这个做法在多线程下不能保证正确，会有 race condition。（思考题：是什么 race condition？）</p> <p>其实有另外一种比较简单的办法：file descriptor 是 hard limit，我们可以自己设一个稍低一点的 soft limit，如果超过 soft limit 就主动关闭新连接，这样就避免触及“file descriptor 耗尽”这种边界条件。比方说当前进程的 max file descriptor 是 1024，那么我们可以在连接数达到 1000 的时候进入“拒绝新连接”状态，这样留给我们足够的腾挪空间。</p> <p>&nbsp;</p> <h1>Muduo 中限制并发连接数</h1> <p><br>Muduo 中限制并发连接数的做法简单得出奇。以在《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例之零：前言</a>》中出场过的 EchoServer 为例，只需要为它增加一个 int 成员，表示当前的活动连接数。（如果是多线程程序，应该用 muduo::AtomicInt32。）</p><pre class="prettyprint">class EchoServer
{
 public:
  EchoServer(muduo::net::EventLoop* loop,
             const muduo::net::InetAddress&amp; listenAddr,
             int maxConnections);

  void start();

 private:
  void onConnection(const muduo::net::TcpConnectionPtr&amp; conn);

  void onMessage(const muduo::net::TcpConnectionPtr&amp; conn,
                 muduo::net::Buffer* buf,
                 muduo::Timestamp time);

  muduo::net::EventLoop* loop_;
  muduo::net::TcpServer server_;
<font color="#0000ff">  int numConnected_; // should be atomic_int</font>
  const int kMaxConnections;
};</pre>

<p>然后，在 EchoServer::onConnection() 中判断当前活动连接数，如果超过最大允许数，则踢掉连接。</p><pre class="prettyprint">void EchoServer::onConnection(const TcpConnectionPtr&amp; conn)
{
  LOG_INFO &lt;&lt; "EchoServer - " &lt;&lt; conn-&gt;peerAddress().toHostPort() &lt;&lt; " -&gt; "
    &lt;&lt; conn-&gt;localAddress().toHostPort() &lt;&lt; " is "
    &lt;&lt; (conn-&gt;connected() ? "UP" : "DOWN");

  if (conn-&gt;connected())
  {
<font color="#0000ff">    ++numConnected_;
    if (numConnected_ &gt; kMaxConnections)
    {
      conn-&gt;shutdown();
    }</font>
  }
  else
  {
    --numConnected_;
  }
  LOG_INFO &lt;&lt; "numConnected = " &lt;&lt; numConnected_;
}
</pre>

<p>这种做法可以积极地防止耗尽 file descriptor。</p>
<p>另外，如果是有业务逻辑的服务，可以在 shutdown() 之前发送一个简单的响应，表明本服务程序的负载能力已经饱和，提示客户端尝试下一个可用的 server（当然，下一个可用的 server 地址不一定要在这个响应里给出，客户端可以自己去 name service 查询），这样方便客户端快速 failover。</p>
<p>&nbsp;</p>
<p>后文将介绍如何处理空闲连接的超时：如果一个连接长时间（若干秒）没有输入数据，则踢掉此连接。办法有很多种，我用 Time Wheel 解决。</p><img src ="http://www.cppblog.com/Solstice/aggbug/145102.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-04-27 00:03 <a href="http://www.cppblog.com/Solstice/archive/2011/04/27/145102.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之五： 测量两台机器的网络延迟</title><link>http://www.cppblog.com/Solstice/archive/2011/04/20/144622.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 20 Apr 2011 01:26:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/04/20/144622.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/144622.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/04/20/144622.html#Feedback</comments><slash:comments>7</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/144622.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/144622.html</trackback:ping><description><![CDATA[<p>Muduo 网络编程示例之五： 测量两台机器的网络延迟</p> <p>陈硕 (giantchen_AT_gmail)</p> <p>Blog.csdn.net/Solstice&nbsp; <a href="http://t.sina.com.cn/giantchen">t.sina.com.cn/giantchen</a></p> <p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第五篇文章。</p> <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>&nbsp;</p> <p>本文介绍一个简单的网络程序 roundtrip，用于测量两台机器之间的网络延迟，即“往返时间 / round trip time / RTT”。这篇文章主要考察定长 TCP 消息的分包，TCP_NODELAY 的作用。</p> <p>本文的代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/roundtrip/roundtrip.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/roundtrip/roundtrip.cc</a></p> <p>测量 RTT 的办法很简单：</p> <ul> <li>host A 发一条消息给 host B，其中包含 host A 发送消息的本地时间</li> <li>host B 收到之后立刻把消息 echo 回 host A</li> <li>host A 收到消息之后，用当前时间减去消息中的时间就得到了 RTT。</li></ul> <p>NTP 协议的工作原理与之类似，不过，除了测量 RTT，NTP 还需要知道两台机器之间的时间差 (clock offset)，这样才能校准时间。</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="roundtrip_ntp" border="0" alt="roundtrip_ntp" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo--_7341/roundtrip_ntp_a456d95d-00d0-43ea-8267-0c9e4bbbd955.png" width="206" height="244"></p> <p>以上是 NTP 协议收发消息的协议，RTT = (T4-T1) – (T3-T2)，时间差 = ((T4+T1)-(T2+T3))/2。NTP 的要求是往返路径上的单程延迟要尽量相等，这样才能减少系统误差。偶然误差由单程延迟的不确定性决定。</p> <p>在我设计的 roundtrip 示例程序中，协议有所简化：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="roundtrip_simple" border="0" alt="roundtrip_simple" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo--_7341/roundtrip_simple_afb9e3d3-7faa-4803-9e8a-97aa3d74dd2a.png" width="266" height="347"></p> <p>简化之后的协议少取一次时间，因为 server 收到消息之后立刻发送回 client，耗时很少（若干微秒），基本不影响最终结果。</p> <p>我设计的消息格式是 16 字节定长消息：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="roundtrip_msg" border="0" alt="roundtrip_msg" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo--_7341/roundtrip_msg_c15a36c0-b07b-422c-89f8-f510506b1652.png" width="397" height="136"></p> <p>T1 和 T2 都是 muduo::Timestamp，一个 int64_t，表示从 Epoch 到现在的微秒数。</p> <p>为了让消息的单程往返时间接近，server 和 client 发送的消息都是 16 bytes，这样做到对称。</p> <p>由于是定长消息，可以不必使用 codec，在 message callback 中直接用 </p> <p>while (buffer-&gt;readableBytes() &gt;= frameLen) { ... } 就能 decode。</p> <p>请读者思考，如果把 while 换成 if 会有什么后果？</p> <p>&nbsp;</p> <p>client 程序以 200ms 为间隔发送消息，在收到消息之后打印 RTT 和 clock offset。一次运作实例如下：</p> <p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="roundtrip_example" border="0" alt="roundtrip_example" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/Muduo--_7341/roundtrip_example_154a6dc4-37e4-4152-9bd6-0871672fb940.png" width="623" height="387"></p> <p>这个例子中，client 和 server 的时钟不是完全对准的，server 的时间快了 850 us，用 roundtrip 程序能测量出这个时间差。有了这个时间差就能校正分布式系统中测量得到的消息延迟。</p> <p>比方说以上图为例，server 在它本地 1.235000 时刻发送了一条消息，client 在它本地 1.234300 收到这条消息，直接计算的话延迟是 –700us。这个结果肯定是错的，因为 server 和 client 不在一个时钟域（这是数字电路中的概念），它们的时间直接相减无意义。如果我们已经测量得到 server 比 client 快 850us，那么做用这个数据一次校正： -700+850 = 150us，这个结果就比较符合实际了。当然，在实际应用中，clock offset 要经过一个低通滤波才能使用，不然偶然性太大。</p> <p>请读者思考，为什么不能直接以 RTT/2 作为两天机器之间收发消息的单程延迟？</p> <p>这个程序在局域网中使用没有问题，如果在广域网上使用，而且 RTT 大于 200ms，那么受 Nagle 算法影响，测量结果是错误的（具体分析留作练习，这能测试对 Nagle 的理解），这时候我们需要设置 TCP_NODELAY 参数，让程序在广域网上也能正常工作。</p><img src ="http://www.cppblog.com/Solstice/aggbug/144622.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-04-20 09:26 <a href="http://www.cppblog.com/Solstice/archive/2011/04/20/144622.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>在 muduo 中实现 protobuf 编解码器与消息分发器</title><link>http://www.cppblog.com/Solstice/archive/2011/04/13/144085.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Tue, 12 Apr 2011 23:47:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/04/13/144085.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/144085.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/04/13/144085.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/144085.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/144085.html</trackback:ping><description><![CDATA[<p>陈硕 (giantchen_AT_gmail)</p> <p>Blog.csdn.net/Solstice&nbsp; <a href="http://t.sina.com.cn/giantchen">t.sina.com.cn/giantchen</a></p> <p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>本文是《<a href="http://blog.csdn.net/Solstice/archive/2011/04/03/6300108.aspx">一种自动反射消息类型的 Google Protobuf 网络传输方案</a>》的延续，介绍如何将前文介绍的打包方案与 muduo::net::Buffer 结合，实现了 protobuf codec 和 dispatcher。</p> <p>Muduo 的下载地址： <a href="http://muduo.googlecode.com/files/muduo-0.1.9-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.9-alpha.tar.gz</a> ，SHA1 dc0bb5f7becdfc0277fb35f6dfaafee8209213bc ，本文的完整代码可在线阅读 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/">http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/</a> 。</p> <p>考虑到不是每个人都安装了 Google Protobuf，muduo 中的 protobuf 相关示例默认是不 build 的，如果你的机器上安装了 protobuf 2.3.0 或 2.4.0a，那么可以用 ./build.sh protobuf_all 来构建 protobuf 相关的 examples。</p> <p>&nbsp;</p> <p>在介绍 codec 和 dispatcher 之前，先讲讲前文的一个未决问题。</p> <h1>为什么 Protobuf 的默认序列化格式没有包含消息的长度与类型？</h1> <p>Protobuf 是经过深思熟虑的消息打包方案，它的默认序列化格式没有包含消息的长度与类型，自然有其道理。哪些情况下不需要在 protobuf 序列化得到的字节流中包含消息的长度和（或）类型？我能想到的答案有：</p> <ul> <li>如果把消息写入文件，一个文件存一个消息，那么序列化结果中不需要包含长度和类型，因为从文件名和文件长度中可以得知消息的类型与长度。  <li>如果把消息写入文件，一个文件存多个消息，那么序列化结果中不需要包含类型，因为文件名就代表了消息的类型。  <li>如果把消息存入数据库（或者 NoSQL），以 VARBINARY 字段保存，那么序列化结果中不需要包含长度和类型，因为从字段名和字段长度中可以得知消息的类型与长度。  <li>如果把消息以 UDP 方式发生给对方，而且对方一个 UDP port 只接收一种消息类型，那么序列化结果中不需要包含长度和类型，因为从 port 和 UDP packet 长度中可以得知消息的类型与长度。  <li>如果把消息以 TCP 短连接方式发给对方，而且对方一个 TCP port 只接收一种消息类型，那么序列化结果中不需要包含长度和类型，因为从 port 和 TCP 字节流长度中可以得知消息的类型与长度。  <li>如果把消息以 TCP 长连接方式发给对方，但是对方一个 TCP port 只接收一种消息类型，那么序列化结果中不需要包含类型，因为 port 代表了消息的类型。  <li>如果采用 RPC 方式通信，那么只需要告诉对方 method name，对方自然能推断出 Request 和 Response 的消息类型，这些可以由 protoc 生成的 RPC stubs 自动搞定。</li></ul> <p>对于最后一点，比方说 sudoku.proto 定义为：</p><pre class="prettyprint">service SudokuService {
  rpc Solve (SudokuRequest) returns (SudokuResponse);
}</pre>
<p>那么 RPC method <font color="#0000ff">Sudoku.Solve</font> 对应的请求和响应分别是 SudokuRequest 和 SudokuResponse。在发送 RPC 请求的时候，不需要包含 SudokuRequest 的类型，只需要发送 method name <font color="#0000ff">Sudoku.Solve</font>，对方自知道应该按照 SudokuRequest 来解析(parse)请求。这个例子来自我的半成品项目 evproto，见 <a href="http://blog.csdn.net/Solstice/archive/2010/04/17/5497699.aspx">http://blog.csdn.net/Solstice/archive/2010/04/17/5497699.aspx</a> 。</p>
<p>对于上述这些情况，如果 protobuf 无条件地把长度和类型放到序列化的字节串中，只会浪费网络带宽和存储。可见 protobuf 默认不发送长度和类型是正确的决定。Protobuf 为消息格式的设计树立了典范，哪些该自己搞定，哪些留给外部系统去解决，这些都考虑得很清楚。</p>
<p>只有在使用 TCP 长连接，且在一个连接上传递不止一种消息的情况下（比方同时发 Heartbeat 和 Request/Response），才需要我前文提到的那种打包方案。（为什么要在一个连接上同时发 Heartbeat 和业务消息？请见陈硕《<a href="http://blog.csdn.net/Solstice/archive/2010/10/19/5950190.aspx">分布式系统的工程化开发方法</a>》 p.51 心跳协议的设计。）这时候我们需要一个分发器 dispatcher，把不同类型的消息分给各个消息处理函数，这正是本文的主题之一。</p>
<p>以下均只考虑 TCP 长连接这一应用场景。</p>
<p>先谈谈编解码器。</p>
<h1>什么是编解码器 codec?</h1>
<p><a href="http://en.wikipedia.org/wiki/Codec">Codec</a> 是 en<font color="#ff0000">co</font>der 和 <font color="#ff0000">dec</font>oder 的缩写，这是一个到软硬件都在使用的术语，这里我借指“把网络数据和业务消息之间互相转换”的代码。</p>
<p>在最简单的网络编程中，没有消息 message 只有字节流数据，这时候是用不到 codec 的。比如我们前面讲过的 <a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171905.aspx">echo server</a>，它只需要把收到的数据原封不动地发送回去，它不必关心消息的边界（也没有“消息”的概念），收多少就发多少，这种情况下它干脆直接使用 muduo::net::Buffer，取到数据再交给 TcpConnection 发送回去，见下图。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_echo" border="0" alt="codec_echo" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_echo_d96dadaf-7a7f-43b9-be0d-ab2b79f4b8d5.png" width="731" height="450"></p>
<p>non-trivial 的网络服务程序通常会以消息为单位来通信，每条消息有明确的长度与界限。程序每次收到一个完整的消息的时候才开始处理，发送的时候也是把一个完整的消息交给网络库。比如我们前面讲过的 <a href="http://blog.csdn.net/Solstice/archive/2011/02/04/6172391.aspx">asio chat 服务</a>，它的一条聊天记录就是一条消息，我们设计了一个简单的消息格式，即在聊天记录前面加上 4 字节的 length header，<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/codec.h">LengthHeaderCodec 代码</a>及解说见《<a href="http://blog.csdn.net/Solstice/archive/2011/02/04/6172391.aspx">Muduo 网络编程示例之二：Boost.Asio 的聊天服务器</a>》一文。</p>
<p>codec 的基本功能之一是做 <a href="http://blog.csdn.net/Solstice/archive/2011/02/04/6172391.aspx">TCP 分包</a>：确定每条消息的长度，为消息划分界限。在 non-blocking 网络编程中，codec 几乎是必不可少的。如果只收到了半条消息，那么不会触发消息回调，数据会停留在 Buffer 里（数据已经读到 Buffer 中了），等待收到一个完整的消息再通知处理函数。既然这个任务太常见，我们干脆做一个 utility class，避免服务端和客户端程序都要自己处理分包，这就有了 LengthHeaderCodec。这个 codec 的使用有点奇怪，不需要继承，它也没有基类，只要把它当成普通 data member 来用，把 TcpConnection 的数据喂给它，然后向它注册 onXXXMessage() 回调，代码见 <a href="http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/client.cc">asio chat 示例</a>。muduo 里的 codec 都是这样的风格，通过 boost::function 粘合到一起。</p>
<p>codec 是一层间接性，它位于 TcpConnection 和 ChatServer 之间，拦截处理收到的数据，在收到完整的消息之后再调用 CharServer 对应的处理函数，注意 CharServer::onStringMessage() 的参数是 std::string，不再是 muduo::net::Buffer，也就是说 LengthHeaderCodec 把 Buffer <strong>解码</strong>成了 string。另外，在发送消息的时候，ChatServer 通过 LengthHeaderCodec::send() 来发送 string，LengthHeaderCodec 负责把它<strong>编码</strong>成 Buffer。这正是“编解码器”名字的由来。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_chat" border="0" alt="codec_chat" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_chat_720c58f4-c2bd-4ee5-99f8-7908a2b9ac83.png" width="731" height="450"></p>
<p>Protobuf codec 与此非常类似，只不过消息类型从 std::string 变成了 protobuf::Message。对于只接收处理 Query 消息的 QueryServer 来说，用 ProtobufCodec 非常方便，收到 protobuf::Message 之后 down cast 成 Query 来用就行。如果要接收处理不止一种消息，ProtobufCodec 恐怕还不能单独完成工作，请继续阅读下文。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_protobuf" border="0" alt="codec_protobuf" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_protobuf_a3ac3ed2-b874-4729-9dbb-d0c21ac6e8a7.png" width="853" height="449"></p>
<p>&nbsp;</p>
<h1>实现 ProtobufCodec </h1>
<p>Protobuf 的打包方案我已经在《<a href="http://blog.csdn.net/Solstice/archive/2011/04/03/6300108.aspx">一种自动反射消息类型的 Google Protobuf 网络传输方案</a>》中讲过，并以 string 为载体演示了 encode 和 decode 操作。在 muduo 里，我们有专门的 Buffer class，编码更轻松。</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/codec.cc#101">编码算法</a>很直截了当，按照前文定义的消息格式一路打包下来，最后更新一下首部的长度即可。</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/codec.cc#190">解码算法</a>有几个要点：</p>
<ul>
<li>protobuf::Message 是 new 出来的对象，它的生命期如何管理？muduo 采用 shared_ptr&lt;Message&gt; 来自动管理对象生命期，这与其他地方的做法是一致的。 
<li>出错如何处理？比方说长度超出范围、check sum 不正确、message type name 不能识别、message parse 出错等等。ProtobufCodec 定义了 ErrorCallback，用户代码可以注册这个回调。如果不注册，默认的处理是断开连接，让客户重连重试。<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/codec_test.cc">codec 的单元测试</a>里模拟了各种出错情况。 
<li>如何处理一次收到半条消息、一条消息、一条半消息、两条消息等等情况？这是每个 non-blocking 网络程序中的 codec 都要面对的问题。</li></ul>
<p>ProtobufCodec 在实际使用中有明显的不足：它只负责把 muduo::net::Buffer 转换为具体类型的 protobuf::Message，应用程序拿到 Message 之后还有再根据其具体类型做一次分发。我们可以考虑做一个简单通用的分发器 dispatcher，以简化客户代码。</p>
<p>此外，目前 ProtobufCodec 的实现非常初级，它没有充分利用 ZeroCopyInputStream 和 ZeroCopyOutputStream，而是把收到的数据作为 byte array 交给 protobuf Message 去解析，这给性能优化留下了空间。protobuf Message 不要求数据连续(像 vector 那样)，只要求数据分段连续(像 deque 那样)，这给 buffer 管理带来性能上的好处（避免重新分配内存，减少内存碎片），当然也使得代码变复杂。muduo::net::Buffer 非常简单，它内部是 vector&lt;char&gt;，我目前不想让 protobuf 影响 muduo 本身的设计，毕竟 muduo 是个通用的网络库，不是为实现 protobuf RPC 而特制的。</p>
<h1>消息分发器 dispatcher 有什么用？</h1>
<p>前面提到，在使用 TCP 长连接，且在一个连接上传递不止一种 protobuf 消息的情况下，客户代码需要对收到的消息按类型做分发。比方说，收到 Logon 消息就交给 QueryServer::onLogon() 去处理，收到 Query 消息就交给 QueryServer::onQuery() 去处理。这个消息分派机制可以做得稍微有点通用性，让所有 muduo+protobuf 程序收益，而且不增加复杂性。</p>
<p>换句话说，又是一层间接性，ProtobufCodec 拦截了 TcpConnection 的数据，把它转换为 Message，ProtobufDispatcher 拦截了 ProtobufCodec 的 callback，按消息具体类型把它分派给多个 callbacks。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_dispatcher" border="0" alt="codec_dispatcher" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_dispatcher_31ad08f3-494c-44ec-9183-14d1ec998f53.png" width="973" height="548"></p>
<h1>ProtobufCodec 与 ProtobufDispatcher 的综合运用</h1>
<p>我写了两个示例代码，client 和 server，把 ProtobufCodec 和 ProtobufDispatcher 串联起来使用。server 响应 Query 消息，发生回 Answer 消息，如果收到未知消息类型，则断开连接。client 可以选择发送 Query 或 Empty 消息，由命令行控制。这样可以测试 unknown message callback。</p>
<p>为节省篇幅，这里就不列出代码了，请移步阅读</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/client.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/client.cc</a>&nbsp;</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/server.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/server.cc</a></p>
<p>在构造函数中，通过注册回调函数把四方 (TcpConnection、codec、dispatcher、QueryServer) 结合起来。</p>
<h1>ProtobufDispatcher 的两种实现</h1>
<p>要完成消息分发，那么就是对消息做 type-switch，这似乎是一个 bad smell，但是 protobuf Message 的 Descriptor 没有留下定制点（比如暴露一个 boost::any 成员），我们只好硬来了。</p>
<p>先定义 </p>
<p>typedef boost::function&lt;void (Message*)&gt; <font color="#0000ff">ProtobufMessageCallback</font>; </p>
<p>注意，本节出现的不是 muduo dispatcher 真实的代码，仅为示意，突出重点，便于画图。</p>
<p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/dispatcher_lite.h">ProtobufDispatcherLite</a> 的结构非常简单，它有一个 map&lt;Descriptor*, ProtobufMessageCallback&gt; 成员，客户代码可以以 Descriptor* 为 key 注册回调（recall: 每个具体消息类型都有一个全局的 Descriptor 对象，其地址是不变的，可以用来当 key）。在收到 protobuf Message 之后，在 map 中找到对应的 ProtobufMessageCallback，然后调用之。如果找不到，就调用 defaultCallback。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_dispatcher_lite" border="0" alt="codec_dispatcher_lite" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_dispatcher_lite_e903b2a2-31e6-4d5c-87c8-9ebae6adb952.png" width="584" height="237"></p>
<p>当然，它的设计也有小小的缺陷，那就是 ProtobufMessageCallback 限制了客户代码只能接受基类 Message，客户代码需要自己做向下转型，比如：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_query_server1" border="0" alt="codec_query_server1" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_query_server1_b78ab392-e5ba-4acc-8591-f9529fb4a55a.png" width="589" height="126"></p>
<p>&nbsp;</p>
<p>如果我希望 QueryServer 这么设计：不想每个消息处理函数自己做 down casting，而是交给 dispatcher 去处理，客户代码拿到的就已经是想要的具体类型。如下：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_query_server2" border="0" alt="codec_query_server2" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_query_server2_1d2b5a53-e5f6-4b9d-b570-04d4242df529.png" width="223" height="122"></p>
<p>那么该该如何实现 ProtobufDispatcher 呢？它如何与<strong>多个未知的消息类型</strong>合作？做 down cast 需要知道目标类型，难道我们要用一长串模板类型参数吗？</p>
<p>有一个办法，把多态与模板结合，利用 templated derived class 来提供类型上的灵活性。设计如下。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_dispatcher_class" border="0" alt="codec_dispatcher_class" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_dispatcher_class_47920806-2a6f-4ecc-bde0-a4cb4a352ed6.png" width="960" height="336"></p>
<p>ProtobufDispatcher 有一个模板成员函数，可以接受注册任意消息类型 T 的回调，然后它创建一个模板化的派生类 CallbackT&lt;T&gt;，这样消息的类新信息就保存在了 CallbackT&lt;T&gt; 中，做 down casting 就简单了。</p>
<p>&nbsp;</p>
<p>比方说，我们有两个具体消息类型 Query 和 Answer。</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_query" border="0" alt="codec_query" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_query_7d03105f-1bfa-49a5-add2-6cb11da1afc4.png" width="429" height="248"></p>
<p>然后我们这样注册回调：</p><pre class="prettyprint">dispatcher_.registerMessageCallback<font color="#0000ff">&lt;muduo::Query&gt;</font>(
    boost::bind(&amp;QueryServer::<font color="#0000ff">onQuery</font>, this, _1, _2, _3));
dispatcher_.registerMessageCallback<font color="#0000ff">&lt;muduo::Answer&gt;</font>(
    boost::bind(&amp;QueryServer::<font color="#0000ff">onAnswer</font>, this, _1, _2, _3));</pre>
<p>这样会具现化 (instantiation) 出两个 CallbackT 实体，如下：</p>
<p><img style="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="codec_query_callback" border="0" alt="codec_query_callback" src="http://www.cppblog.com/images/cppblog_com/Solstice/Windows-Live-Writer/e1dc22d846cf_131A6/codec_query_callback_ce86102a-0362-47e8-9a4e-3449e8d723a4.png" width="730" height="336"></p>
<p>以上设计参考了 shared_ptr 的 deleter，<a href="http://www.artima.com/cppsource/top_cpp_aha_moments.html">Scott Meyers 也谈到过</a>。</p>
<h1>ProtobufCodec 和 ProtobufDispatcher 有何意义？</h1>
<p>ProtobufCodec 和 ProtobufDispatcher 把每个直接收发 protobuf Message 的网络程序都会用到的功能提炼出来做成了公用的 utility，这样以后新写 protobuf 网络程序就不必为打包分包和消息分发劳神了。它俩以库的形式存在，是两个可以拿来就当 data member 用的 class，它们没有基类，也没有用到虚函数或者别的什么面向对象特征，不侵入 muduo::net 或者你的代码。</p>
<p>&nbsp;</p>
<p>下一篇文章讲《分布式程序的自动回归测试》会介绍利用 protobuf 的跨语言特性，采用 Java 为 C++ 服务程序编写 test harness。</p><img src ="http://www.cppblog.com/Solstice/aggbug/144085.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-04-13 07:47 <a href="http://www.cppblog.com/Solstice/archive/2011/04/13/144085.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>为什么 muduo 的 shutdown() 没有直接关闭 TCP 连接？</title><link>http://www.cppblog.com/Solstice/archive/2011/02/25/140669.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Fri, 25 Feb 2011 13:30:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/25/140669.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/140669.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/25/140669.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/140669.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/140669.html</trackback:ping><description><![CDATA[<p>陈硕 (giantchen_AT_gmail)</p>
<p>Blog.csdn.net/Solstice</p>
<p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p>
<p>&nbsp;</p>
<p>今天收到一位网友来信：</p>
<blockquote>
<p><font color=#0000ff>在 simple 中的 daytime 示例中，服务端主动关闭时调用的是如下函数序列，这不是只是关闭了连接上的写操作吗，怎么是关闭了整个连接？</font></p>
</blockquote>
<div style="BORDER-BOTTOM: silver 1px solid; TEXT-ALIGN: left; BORDER-LEFT: silver 1px solid; PADDING-BOTTOM: 4px; LINE-HEIGHT: 12pt; BACKGROUND-COLOR: #f4f4f4; MARGIN: 20px 0px 10px; PADDING-LEFT: 4px; WIDTH: 97.5%; PADDING-RIGHT: 4px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; MAX-HEIGHT: 200px; FONT-SIZE: 8pt; OVERFLOW: auto; BORDER-TOP: silver 1px solid; CURSOR: text; BORDER-RIGHT: silver 1px solid; PADDING-TOP: 4px" id=codeSnippetWrapper>
<div style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px" id=codeSnippet>
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum1>   1:</span> <span style="COLOR: #0000ff">void</span> DaytimeServer::onConnection(<span style="COLOR: #0000ff">const</span> muduo::net::TcpConnectionPtr&amp; conn)</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum2>   2:</span> {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum3>   3:</span>   <span style="COLOR: #0000ff">if</span> (conn-&gt;connected())</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum4>   4:</span>   {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum5>   5:</span>     conn-&gt;send(Timestamp::now().toFormattedString() + <span style="COLOR: #006080">"\n"</span>);</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum6>   6:</span>     conn-&gt;shutdown();</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum7>   7:</span>   }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum8>   8:</span> }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum9>   9:</span>&nbsp; </pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum10>  10:</span> <span style="COLOR: #0000ff">void</span> TcpConnection::shutdown()</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum11>  11:</span> {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum12>  12:</span>   <span style="COLOR: #0000ff">if</span> (state_ == kConnected)</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum13>  13:</span>   {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum14>  14:</span>     setState(kDisconnecting);</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum15>  15:</span>     loop_-&gt;runInLoop(boost::bind(&amp;TcpConnection::shutdownInLoop, <span style="COLOR: #0000ff">this</span>));</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum16>  16:</span>   }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum17>  17:</span> }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum18>  18:</span>&nbsp; </pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum19>  19:</span> <span style="COLOR: #0000ff">void</span> TcpConnection::shutdownInLoop()</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum20>  20:</span> {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum21>  21:</span>   loop_-&gt;assertInLoopThread();</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum22>  22:</span>   <span style="COLOR: #0000ff">if</span> (!channel_-&gt;isWriting())</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum23>  23:</span>   {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum24>  24:</span>     <span style="COLOR: #008000">// we are not writing</span></pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum25>  25:</span>     socket_-&gt;shutdownWrite();</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum26>  26:</span>   }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum27>  27:</span> }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum28>  28:</span>  </pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum29>  29:</span> <span style="COLOR: #0000ff">void</span> Socket::shutdownWrite()</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum30>  30:</span> {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum31>  31:</span>   sockets::shutdownWrite(sockfd_);</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum32>  32:</span> }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum33>  33:</span>  </pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum34>  34:</span> <span style="COLOR: #0000ff">void</span> sockets::shutdownWrite(<span style="COLOR: #0000ff">int</span> sockfd)</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum35>  35:</span> {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum36>  36:</span>   <span style="COLOR: #0000ff">if</span> (::shutdown(sockfd, SHUT_WR) &lt; 0)</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum37>  37:</span>   {</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum38>  38:</span>     LOG_SYSERR &lt;&lt; <span style="COLOR: #006080">"sockets::shutdownWrite"</span>;</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum39>  39:</span>   }</pre>
<!--crlf-->
<pre style="BORDER-BOTTOM-STYLE: none; TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BORDER-RIGHT-STYLE: none; BACKGROUND-COLOR: #f4f4f4; MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New', courier, monospace; DIRECTION: ltr; BORDER-TOP-STYLE: none; COLOR: black; FONT-SIZE: 8pt; BORDER-LEFT-STYLE: none; OVERFLOW: visible; PADDING-TOP: 0px"><span style="COLOR: #606060" id=lnum40>  40:</span> }</pre>
<!--crlf--></div>
</div>
<p>陈硕答复如下：</p>
<p>Muduo TcpConnection 没有提供 close，而只提供 shutdown ，这么做是为了收发数据的完整性。</p>
<p>TCP 是一个全双工协议，同一个文件描述符既可读又可写， shutdownWrite() 关闭了&#8220;写&#8221;方向的连接，保留了&#8220;读&#8221;方向，这称为 TCP half-close。如果直接 close(socket_fd)，那么 socket_fd 就不能读或写了。</p>
<p>用 shutdown 而不用 close 的效果是，如果对方已经发送了数据，这些数据还&#8220;在路上&#8221;，那么 muduo 不会漏收这些数据。换句话说，muduo 在 TCP 这一层面解决了&#8220;当你打算关闭网络连接的时候，如何得知对方有没有发了一些数据而你还没有收到？&#8221;这一问题。当然，这个问题也可以在上面的协议层解决，双方商量好不再互发数据，就可以直接断开连接。</p>
<p>等于说 muduo 把&#8220;主动关闭连接&#8221;这件事情分成两步来做，如果要主动关闭连接，它会先关本地&#8220;写&#8221;端，等对方关闭之后，再关本地&#8220;读&#8221;端。练习：阅读代码，回答&#8220;如果被动关闭连接，muduo 的行为如何？&#8221; 提示：muduo 在 read() 返回 0 的时候会回调 connection callback，这样客户代码就知道对方断开连接了。</p>
<p>Muduo 这种关闭连接的方式对对方也有要求，那就是对方 read() 到 0 字节之后会主动关闭连接（无论 shutdownWrite() 还是 close()），一般的网络程序都会这样，不是什么问题。当然，这么做有一个潜在的安全漏洞，万一对方故意不不关，那么 muduo 的连接就一直半开着，消耗系统资源。<br><br>完整的流程是：我们发完了数据，于是 shutdownWrite，发送 TCP FIN 分节，对方会读到 0 字节，然后对方通常会关闭连接，这样 muduo 会读到 0 字节，然后 muduo 关闭连接。<span style="WIDOWS: 2; TEXT-TRANSFORM: none; TEXT-INDENT: 0px; BORDER-COLLAPSE: separate; FONT: medium 'Times New Roman'; WHITE-SPACE: normal; ORPHANS: 2; LETTER-SPACING: normal; COLOR: rgb(0,0,0); WORD-SPACING: 0px; -webkit-border-horizontal-spacing: 0px; -webkit-border-vertical-spacing: 0px; -webkit-text-decorations-in-effect: none; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px" class=Apple-style-span><span style="TEXT-ALIGN: left; LINE-HEIGHT: 21px; FONT-FAMILY: verdana, sans-serif; FONT-SIZE: 14px" class=Apple-style-span>（思考题：在 shutdown() 之后，muduo 回调 connection callback 的时间间隔大约是一个 round-trip time，为什么？）</span></span><br><br>另外，如果有必要，对方可以在 read() 返回 0 之后继续发送数据，这是直接利用了 half-close TCP 连接。muduo 会收到这些数据，通过 message callback 通知客户代码。 </p>
<p>那么 muduo 什么时候真正 close socket 呢？在 TcpConnection 对象析构的时候。TcpConnection 持有一个 Socket 对象，Socket 是一个 RAII handler，它的析构函数会 close(sockfd_)。这样，如果发生 TcpConnection 对象泄漏，那么我们从 /proc/pid/fd/ 就能找到没有关闭的文件描述符，便于查错。
<p>muduo 在 read() 返回 0 的时候会回调 connection callback，然后把 TcpConnection 的引用计数减一，如果 TcpConnection 的引用计数降到零，它就会析构了。
<p>参考：</p>
<p>《TCP/IP 详解》第一卷第 18.5 节，TCP Half-Close。</p>
<p>《UNIX 网络编程》第一卷第三版第 6.6 节， shutdown() 函数。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/140669.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-25 21:30 <a href="http://www.cppblog.com/Solstice/archive/2011/02/25/140669.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之四：Twisted Finger</title><link>http://www.cppblog.com/Solstice/archive/2011/02/23/140548.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 23 Feb 2011 13:33:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/23/140548.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/140548.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/23/140548.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/140548.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/140548.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 这是《Muduo 网络编程示例》系列的第四篇文章。 Muduo 全系列文章列表： http://blog.csdn.net/Solstice/category/779646.aspx  Python Twisted 是一款非常好的网络库，它也采用 Reactor 作为网络编程的基本模型，所以从使用上与 m...&nbsp;&nbsp;<a href='http://www.cppblog.com/Solstice/archive/2011/02/23/140548.html'>阅读全文</a><img src ="http://www.cppblog.com/Solstice/aggbug/140548.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-23 21:33 <a href="http://www.cppblog.com/Solstice/archive/2011/02/23/140548.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之三：定时器</title><link>http://www.cppblog.com/Solstice/archive/2011/02/06/139769.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sun, 06 Feb 2011 14:56:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/06/139769.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/139769.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/06/139769.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/139769.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/139769.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 陈硕 (giantchen_AT_gmail)  Blog.csdn.net/Solstice  这是《Muduo 网络编程示例》系列的第三篇文章。  Muduo 全系列文章列表： http://blog.csdn.net/Solstice/category/779646.aspx &nbsp; 程序中的时间 程序中对时间的处理是个大问题，我打算单独写一篇文章来全面地讨论这个问题。文章暂定名《〈程...&nbsp;&nbsp;<a href='http://www.cppblog.com/Solstice/archive/2011/02/06/139769.html'>阅读全文</a><img src ="http://www.cppblog.com/Solstice/aggbug/139769.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-06 22:56 <a href="http://www.cppblog.com/Solstice/archive/2011/02/06/139769.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之二：Boost.Asio 的聊天服务器</title><link>http://www.cppblog.com/Solstice/archive/2011/02/04/139718.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Fri, 04 Feb 2011 00:57:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/04/139718.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/139718.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/04/139718.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/139718.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/139718.html</trackback:ping><description><![CDATA[
<p>陈硕 (giantchen_AT_gmail)  </p><p>Blog.csdn.net/Solstice  </p><p>这是《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171831.aspx">Muduo 网络编程示例</a>》系列的第二篇文章。  </p><p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p> <p>本文讲介绍一个与 Boost.Asio 的<a href="http://www.boost.org/doc/libs/release/doc/html/boost_asio/examples.html#boost_asio.examples.chat">示例代码中的聊天服务器</a>功能类似的网络服务程序，包括客户端与服务端的 muduo 实现。这个例子的主要目的是介绍如何处理分包，并初步涉及 Muduo 的多线程功能。Muduo 的下载地址： <a title="http://muduo.googlecode.com/files/muduo-0.1.6-alpha.tar.gz" href="http://muduo.googlecode.com/files/muduo-0.1.7-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.7-alpha.tar.gz</a> ，SHA1 873567e43b3c2cae592101ea809b30ba730f2ee6，本文的完整代码可在线阅读 </p> <p><a href="http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/">http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/</a> 。</p> <h1>TCP 分包</h1> <p>前面一篇《<a href="http://blog.csdn.net/Solstice/archive/2011/02/02/6171905.aspx">五个简单 TCP 协议</a>》中处理的协议没有涉及分包，在 TCP 这种字节流协议上做应用层分包是网络编程的基本需求。分包指的是在发生一个消息(message)或一帧(frame)数据时，通过一定的处理，让接收方能从字节流中识别并截取（还原）出一个个消息。&#8220;粘包问题&#8221;是个伪问题。</p> <p>对于短连接的 TCP 服务，分包不是一个问题，只要发送方主动关闭连接，就表示一条消息发送完毕，接收方 read() 返回 0，从而知道消息的结尾。例如前一篇文章里的 daytime 和 time 协议。</p> <p>对于长连接的 TCP 服务，分包有四种方法：</p> <ol> <li>消息长度固定，比如 muduo 的 roundtrip 示例就采用了固定的 16 字节消息；  </li><li>使用特殊的字符或字符串作为消息的边界，例如 HTTP 协议的 headers 以 "\r\n" 为字段的分隔符；  </li><li>在每条消息的头部加一个长度字段，这恐怕是最常见的做法，本文的聊天协议也采用这一办法；  </li><li>利用消息本身的格式来分包，例如 XML 格式的消息中 &lt;root&gt;...&lt;/root&gt; 的配对，或者 JSON 格式中的 { ... } 的配对。解析这种消息格式通常会用到状态机。</li></ol> <p>在后文的代码讲解中还会仔细讨论用长度字段分包的常见陷阱。</p> <h1>聊天服务</h1> <p>本文实现的聊天服务非常简单，由服务端程序和客户端程序组成，协议如下：</p> <ul> <li>服务端程序中某个端口侦听 (listen) 新的连接；  </li><li>客户端向服务端发起连接；  </li><li>连接建立之后，客户端随时准备接收服务端的消息并在屏幕上显示出来；  </li><li>客户端接受键盘输入，以回车为界，把消息发送给服务端；  </li><li>服务端接收到消息之后，依次发送给每个连接到它的客户端；原来发送消息的客户端进程也会收到这条消息；  </li><li>一个服务端进程可以同时服务多个客户端进程，当有消息到达服务端后，每个客户端进程都会收到同一条消息，服务端广播发送消息的顺序是任意的，不一定哪个客户端会先收到这条消息。  </li><li>（可选）如果消息 A 先于消息 B 到达服务端，那么每个客户端都会先收到 A 再收到 B。</li></ul> <p>这实际上是一个简单的基于 TCP 的应用层广播协议，由服务端负责把消息发送给每个连接到它的客户端。参与&#8220;聊天&#8221;的既可以是人，也可以是程序。在以后的文章中，我将介绍一个稍微复杂的一点的例子 hub，它有&#8220;聊天室&#8221;的功能，客户端可以注册特定的 topic(s)，并往某个 topic 发送消息，这样代码更有意思。</p> <h1>消息格式</h1> <p>本聊天服务的消息格式非常简单，&#8220;消息&#8221;本身是一个字符串，每条消息的有一个 4 字节的头部，以网络序存放字符串的长度。消息之间没有间隙，字符串也不一定以 '\0' 结尾。比方说有两条消息 "hello" 和 "chenshuo"，那么打包后的字节流是：</p> <p>0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'</p> <p>共 21 字节。</p> <h1>打包的代码</h1> <p>这段代码把 const string&amp; message 打包为 muduo::net::Buffer，并通过 conn 发送。</p> <div id="codeSnippetWrapper" class="csharpcode-wrapper"> <div id="codeSnippet" class="csharpcode"><pre class="alteven"><span id="lnum1" class="lnum">   1:</span> <span class="kwrd">void</span> send(muduo::net::TcpConnection* conn, <span class="kwrd">const</span> string&amp; message)</pre><!--CRLF--><pre class="alteven"><span id="lnum2" class="lnum">   2:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum3" class="lnum">   3:</span>   muduo::net::Buffer buf;</pre><!--CRLF--><pre class="alteven"><span id="lnum4" class="lnum">   4:</span>   buf.append(message.data(), message.size());</pre><!--CRLF--><pre class="alteven"><span id="lnum5" class="lnum">   5:</span>   int32_t len = muduo::net::sockets::hostToNetwork32(<span class="kwrd">static_cast</span>&lt;int32_t&gt;(message.size()));</pre><!--CRLF--><pre class="alteven"><span id="lnum6" class="lnum">   6:</span>   buf.prepend(&amp;len, <span class="kwrd">sizeof</span> len);</pre><!--CRLF--><pre class="alteven"><span id="lnum7" class="lnum">   7:</span>   conn-&gt;send(&amp;buf);</pre><!--CRLF--><pre class="alteven"><span id="lnum8" class="lnum">   8:</span> }</pre><!--CRLF--></div></div>
<p>muduo::Buffer 有一个很好的功能，它在头部预留了 8 个字节的空间，这样第 6 行的 prepend() 操作就不需要移动已有的数据，效率较高。</p>
<h1>分包的代码</h1>
<p>解析数据往往比生成数据复杂，分包打包也不例外。</p>
<div id="codeSnippetWrapper" class="csharpcode-wrapper">
<div id="codeSnippet" class="csharpcode"><pre class="alteven"><span id="lnum1" class="lnum">   1:</span> <span class="kwrd">void</span> onMessage(<span class="kwrd">const</span> muduo::net::TcpConnectionPtr&amp; conn,</pre><!--CRLF--><pre class="alteven"><span id="lnum2" class="lnum">   2:</span>                muduo::net::Buffer* buf,</pre><!--CRLF--><pre class="alteven"><span id="lnum3" class="lnum">   3:</span>                muduo::Timestamp receiveTime)</pre><!--CRLF--><pre class="alteven"><span id="lnum4" class="lnum">   4:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum5" class="lnum">   5:</span>   <span class="kwrd"><font color="#0000ff"><strong>while</strong></font></span> (buf-&gt;readableBytes() &gt;= kHeaderLen)</pre><!--CRLF--><pre class="alteven"><span id="lnum6" class="lnum">   6:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum7" class="lnum">   7:</span>     <span class="kwrd">const</span> <span class="kwrd">void</span>* data = buf-&gt;peek();</pre><!--CRLF--><pre class="alteven"><span id="lnum8" class="lnum">   8:</span>     int32_t tmp = *<span class="kwrd">static_cast</span>&lt;<span class="kwrd">const</span> int32_t*&gt;(data);</pre><!--CRLF--><pre class="alteven"><span id="lnum9" class="lnum">   9:</span>     int32_t len = muduo::net::sockets::networkToHost32(tmp);</pre><!--CRLF--><pre class="alteven"><span id="lnum10" class="lnum">  10:</span>     <span class="kwrd">if</span> (len &gt; 65536 || len &lt; 0)</pre><!--CRLF--><pre class="alteven"><span id="lnum11" class="lnum">  11:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum12" class="lnum">  12:</span>       LOG_ERROR &lt;&lt; <span class="str">"Invalid length "</span> &lt;&lt; len;</pre><!--CRLF--><pre class="alteven"><span id="lnum13" class="lnum">  13:</span>       conn-&gt;shutdown();</pre><!--CRLF--><pre class="alteven"><span id="lnum14" class="lnum">  14:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum15" class="lnum">  15:</span>     <span class="kwrd">else</span> <span class="kwrd">if</span> (buf-&gt;readableBytes() &gt;= len + kHeaderLen)</pre><!--CRLF--><pre class="alteven"><span id="lnum16" class="lnum">  16:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum17" class="lnum">  17:</span>       buf-&gt;retrieve(kHeaderLen);</pre><!--CRLF--><pre class="alteven"><span id="lnum18" class="lnum">  18:</span>       muduo::string message(buf-&gt;peek(), len);</pre><!--CRLF--><pre class="alteven"><span id="lnum19" class="lnum">  19:</span>       buf-&gt;retrieve(len);</pre><!--CRLF--><pre class="alteven"><span id="lnum20" class="lnum">  20:</span>       <font color="#0000ff">messageCallback_</font>(conn, message, receiveTime);  // 收到完整的消息，通知用户</pre><!--CRLF--><pre class="alteven"><span id="lnum21" class="lnum">  21:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum22" class="lnum">  22:</span>     <span class="kwrd">else</span></pre><!--CRLF--><pre class="alteven"><span id="lnum23" class="lnum">  23:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum24" class="lnum">  24:</span>       <span class="kwrd">break</span>;</pre><!--CRLF--><pre class="alteven"><span id="lnum25" class="lnum">  25:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum26" class="lnum">  26:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum27" class="lnum">  27:</span> }</pre><!--CRLF--></div></div>
<p>上面这段代码第 7 行用了 while 循环来反复读取数据，直到 Buffer 中的数据不够一条完整的消息。请读者思考，如果换成 if (buf-&gt;readableBytes() &gt;= kHeaderLen) 会有什么后果。</p>
<p>以前面提到的两条消息的字节流为例：</p>
<p>0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'</p>
<p>假设数据最终都全部到达，onMessage() 至少要能正确处理以下各种数据到达的次序，每种情况下 messageCallback_ 都应该被调用两次：</p>
<ol>
<li>每次收到一个字节的数据，onMessage() 被调用 21 次； 
</li><li>数据分两次到达，第一次收到 2 个字节，不足消息的长度字段； 
</li><li>数据分两次到达，第一次收到 4 个字节，刚好够长度字段，但是没有 body； 
</li><li>数据分两次到达，第一次收到 8 个字节，长度完整，但 body 不完整； 
</li><li>数据分两次到达，第一次收到 9 个字节，长度完整，body 也完整； 
</li><li>数据分两次到达，第一次收到 10 个字节，第一条消息的长度完整、body 也完整，第二条消息长度不完整； 
</li><li>请自行移动分割点，验证各种情况； 
</li><li>数据一次就全部到达，这时必须用 while 循环来读出两条消息，否则消息会堆积。</li></ol>
<p>请读者验证 onMessage() 是否做到了以上几点。这个例子充分说明了 non-blocking read 必须和 input buffer 一起使用。</p>
<h1>编解码器 LengthHeaderCodec</h1>
<p>有人<a href="http://www.cnblogs.com/Solstice/archive/2011/02/02/1948839.html#2022206">评论 Muduo 的接收缓冲区不能设置回调函数的触发条件</a>，确实如此。每当 socket 可读，Muduo 的 TcpConnection 会读取数据并存入 Input Buffer，然后回调用户的函数。不过，一个简单的间接层就能解决问题，让用户代码只关心&#8220;消息到达&#8221;而不是&#8220;数据到达&#8221;，如本例中的 LengthHeaderCodec 所展示的那一样。</p>
<div id="codeSnippetWrapper" class="csharpcode-wrapper">
<div id="codeSnippet" class="csharpcode"><pre class="alteven"><span id="lnum1" class="lnum">   1:</span> <span class="preproc">#ifndef</span> MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H</pre><!--CRLF--><pre class="alteven"><span id="lnum2" class="lnum">   2:</span> <span class="preproc">#define</span> MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H</pre><!--CRLF--><pre class="alteven"><span id="lnum3" class="lnum">   3:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum4" class="lnum">   4:</span> <span class="preproc">#include</span> &lt;muduo/base/Logging.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum5" class="lnum">   5:</span> <span class="preproc">#include</span> &lt;muduo/net/Buffer.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum6" class="lnum">   6:</span> <span class="preproc">#include</span> &lt;muduo/net/SocketsOps.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum7" class="lnum">   7:</span> <span class="preproc">#include</span> &lt;muduo/net/TcpConnection.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum8" class="lnum">   8:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum9" class="lnum">   9:</span> <span class="preproc">#include</span> &lt;boost/function.hpp&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum10" class="lnum">  10:</span> <span class="preproc">#include</span> &lt;boost/noncopyable.hpp&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum11" class="lnum">  11:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum12" class="lnum">  12:</span> <span class="kwrd">using</span> muduo::Logger;</pre><!--CRLF--><pre class="alteven"><span id="lnum13" class="lnum">  13:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum14" class="lnum">  14:</span> <span class="kwrd">class</span> LengthHeaderCodec : boost::noncopyable</pre><!--CRLF--><pre class="alteven"><span id="lnum15" class="lnum">  15:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum16" class="lnum">  16:</span>  <span class="kwrd">public</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum17" class="lnum">  17:</span>   <span class="kwrd">typedef</span> boost::function&lt;<span class="kwrd">void</span> (<span class="kwrd">const</span> muduo::net::TcpConnectionPtr&amp;,</pre><!--CRLF--><pre class="alteven"><span id="lnum18" class="lnum">  18:</span>                                 <span class="kwrd">const</span> muduo::string&amp; message,</pre><!--CRLF--><pre class="alteven"><span id="lnum19" class="lnum">  19:</span>                                 muduo::Timestamp)&gt; StringMessageCallback;</pre><!--CRLF--><pre class="alteven"><span id="lnum20" class="lnum">  20:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum21" class="lnum">  21:</span>   <span class="kwrd">explicit</span> LengthHeaderCodec(<span class="kwrd">const</span> StringMessageCallback&amp; cb)</pre><!--CRLF--><pre class="alteven"><span id="lnum22" class="lnum">  22:</span>     : messageCallback_(cb)</pre><!--CRLF--><pre class="alteven"><span id="lnum23" class="lnum">  23:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum24" class="lnum">  24:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum25" class="lnum">  25:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum26" class="lnum">  26:</span>   <span class="kwrd">void</span> onMessage(<span class="kwrd">const</span> muduo::net::TcpConnectionPtr&amp; conn,</pre><!--CRLF--><pre class="alteven"><span id="lnum27" class="lnum">  27:</span>                  muduo::net::Buffer* buf,</pre><!--CRLF--><pre class="alteven"><span id="lnum28" class="lnum">  28:</span>                  muduo::Timestamp receiveTime)</pre><!--CRLF--><pre class="alteven"><span id="lnum29" class="lnum">  29:</span>   { 同上 }</pre><!--CRLF--><pre class="alteven"><span id="lnum30" class="lnum">  30:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum31" class="lnum">  31:</span>   <span class="kwrd">void</span> send(muduo::net::TcpConnection* conn, <span class="kwrd">const</span> muduo::string&amp; message)</pre><!--CRLF--><pre class="alteven"><span id="lnum32" class="lnum">  32:</span>   { 同上 }</pre><!--CRLF--><pre class="alteven"><span id="lnum33" class="lnum">  33:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum34" class="lnum">  34:</span>  <span class="kwrd">private</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum35" class="lnum">  35:</span>   StringMessageCallback messageCallback_;</pre><!--CRLF--><pre class="alteven"><span id="lnum36" class="lnum">  36:</span>   <span class="kwrd">const</span> <span class="kwrd">static</span> size_t kHeaderLen = <span class="kwrd">sizeof</span>(int32_t);</pre><!--CRLF--><pre class="alteven"><span id="lnum37" class="lnum">  37:</span> };</pre><!--CRLF--><pre class="alteven"><span id="lnum38" class="lnum">  38:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum39" class="lnum">  39:</span> <span class="preproc">#endif</span>  <span class="rem">// MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H</span></pre><!--CRLF--></div></div>
<p>这段代码把以 Buffer* 为参数的 MessageCallback 转换成了以 const string&amp; 为参数的 StringMessageCallback，让用户代码不必关心分包操作。客户端和服务端都能从中受益。</p>
<h1>服务端的实现</h1>
<p>聊天服务器的服务端代码小于 100 行，不到 asio 的一半。</p>
<p>请先阅读第 68 行起的数据成员的定义。除了经常见到的 EventLoop 和 TcpServer，ChatServer 还定义了 codec_ 和 std::set&lt;TcpConnectionPtr&gt; connections_ 作为成员，connections_ 是目前已建立的客户连接，在收到消息之后，服务器会遍历整个容器，把消息广播给其中每一个 TCP 连接。</p>
<p>&nbsp;</p>
<p>首先，在构造函数里注册回调：</p>
<div id="codeSnippetWrapper" class="csharpcode-wrapper">
<div id="codeSnippet" class="csharpcode"><pre class="alteven"><span id="lnum1" class="lnum">   1:</span> <span class="preproc">#include</span> <span class="str">"codec.h"</span></pre><!--CRLF--><pre class="alteven"><span id="lnum2" class="lnum">   2:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum3" class="lnum">   3:</span> <span class="preproc">#include</span> &lt;muduo/base/Logging.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum4" class="lnum">   4:</span> <span class="preproc">#include</span> &lt;muduo/base/Mutex.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum5" class="lnum">   5:</span> <span class="preproc">#include</span> &lt;muduo/net/EventLoop.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum6" class="lnum">   6:</span> <span class="preproc">#include</span> &lt;muduo/net/SocketsOps.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum7" class="lnum">   7:</span> <span class="preproc">#include</span> &lt;muduo/net/TcpServer.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum8" class="lnum">   8:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum9" class="lnum">   9:</span> <span class="preproc">#include</span> &lt;boost/bind.hpp&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum10" class="lnum">  10:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum11" class="lnum">  11:</span> <span class="preproc">#include</span> &lt;set&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum12" class="lnum">  12:</span> <span class="preproc">#include</span> &lt;stdio.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum13" class="lnum">  13:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum14" class="lnum">  14:</span> <span class="kwrd">using</span> <span class="kwrd">namespace</span> muduo;</pre><!--CRLF--><pre class="alteven"><span id="lnum15" class="lnum">  15:</span> <span class="kwrd">using</span> <span class="kwrd">namespace</span> muduo::net;</pre><!--CRLF--><pre class="alteven"><span id="lnum16" class="lnum">  16:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum17" class="lnum">  17:</span> <span class="kwrd">class</span> ChatServer : boost::noncopyable</pre><!--CRLF--><pre class="alteven"><span id="lnum18" class="lnum">  18:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum19" class="lnum">  19:</span>  <span class="kwrd">public</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum20" class="lnum">  20:</span>   ChatServer(EventLoop* loop,</pre><!--CRLF--><pre class="alteven"><span id="lnum21" class="lnum">  21:</span>              <span class="kwrd">const</span> InetAddress&amp; listenAddr)</pre><!--CRLF--><pre class="alteven"><span id="lnum22" class="lnum">  22:</span>   : loop_(loop),</pre><!--CRLF--><pre class="alteven"><span id="lnum23" class="lnum">  23:</span>     server_(loop, listenAddr, <span class="str">"ChatServer"</span>),</pre><!--CRLF--><pre class="alteven"><span id="lnum24" class="lnum">  24:</span>     codec_(boost::bind(&amp;ChatServer::onStringMessage, <span class="kwrd">this</span>, _1, _2, _3))</pre><!--CRLF--><pre class="alteven"><span id="lnum25" class="lnum">  25:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum26" class="lnum">  26:</span>     server_.setConnectionCallback(</pre><!--CRLF--><pre class="alteven"><span id="lnum27" class="lnum">  27:</span>         boost::bind(&amp;ChatServer::onConnection, <span class="kwrd">this</span>, _1));</pre><!--CRLF--><pre class="alteven"><span id="lnum28" class="lnum">  28:</span>     server_.setMessageCallback(</pre><!--CRLF--><pre class="alteven"><span id="lnum29" class="lnum">  29:</span>         boost::bind(&amp;LengthHeaderCodec::onMessage, &amp;codec_, _1, _2, _3));</pre><!--CRLF--><pre class="alteven"><span id="lnum30" class="lnum">  30:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum31" class="lnum">  31:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum32" class="lnum">  32:</span>   <span class="kwrd">void</span> start()</pre><!--CRLF--><pre class="alteven"><span id="lnum33" class="lnum">  33:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum34" class="lnum">  34:</span>     server_.start();</pre><!--CRLF--><pre class="alteven"><span id="lnum35" class="lnum">  35:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum36" class="lnum">  36:</span>&nbsp; </pre><pre class="alteven">这里有几点值得注意，在以往的代码里是直接把本 class 的 onMessage() 注册给 server_；这里我们把 LengthHeaderCodec::onMessage() 注册给 server_，然后向 codec_ 注册了 ChatServer::onStringMessage()，等于说让 codec_ 负责解析消息，然后把完整的消息回调给 ChatServer。这正是我前面提到的&#8220;一个简单的间接层&#8221;，在不增加 Muduo 库的复杂度的前提下，提供了足够的灵活性让我们在用户代码里完成需要的工作。</pre><pre class="alteven">另外，server_.start() 绝对不能在构造函数里调用，这么做将来会有线程安全的问题，见我在《<a href="http://www.cnblogs.com/Solstice/archive/2010/02/10/dtor_meets_threads.html">当析构函数遇到多线程 ── C++ 中线程安全的对象回调</a>》一文中的<a href="http://www.cnblogs.com/Solstice/archive/2010/02/10/dtor_meets_threads.html#_Toc22002">论述</a>。</pre><pre class="alteven">以下是处理连接的建立和断开的代码，注意它把新建的连接加入到 connections_ 容器中，把已断开的连接从容器中删除。这么做是为了避免内存和资源泄漏，TcpConnectionPtr 是 boost::shared_ptr&lt;TcpConnection&gt;，是 muduo 里唯一一个默认采用 shared_ptr 来管理生命期的对象。以后我们会谈到这么做的原因。</pre><!--CRLF--><pre class="alteven"><span id="lnum37" class="lnum">  37:</span>  <span class="kwrd">private</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum38" class="lnum">  38:</span>   <span class="kwrd">void</span> onConnection(<span class="kwrd">const</span> TcpConnectionPtr&amp; conn)</pre><!--CRLF--><pre class="alteven"><span id="lnum39" class="lnum">  39:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum40" class="lnum">  40:</span>     LOG_INFO &lt;&lt; conn-&gt;localAddress().toHostPort() &lt;&lt; <span class="str">" -&gt; "</span></pre><!--CRLF--><pre class="alteven"><span id="lnum41" class="lnum">  41:</span>         &lt;&lt; conn-&gt;peerAddress().toHostPort() &lt;&lt; <span class="str">" is "</span></pre><!--CRLF--><pre class="alteven"><span id="lnum42" class="lnum">  42:</span>         &lt;&lt; (conn-&gt;connected() ? <span class="str">"UP"</span> : <span class="str">"DOWN"</span>);</pre><!--CRLF--><pre class="alteven"><span id="lnum43" class="lnum">  43:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum44" class="lnum">  44:</span>     MutexLockGuard lock(mutex_);</pre><!--CRLF--><pre class="alteven"><span id="lnum45" class="lnum">  45:</span>     <span class="kwrd">if</span> (conn-&gt;connected())</pre><!--CRLF--><pre class="alteven"><span id="lnum46" class="lnum">  46:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum47" class="lnum">  47:</span>       connections_.insert(conn);</pre><!--CRLF--><pre class="alteven"><span id="lnum48" class="lnum">  48:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum49" class="lnum">  49:</span>     <span class="kwrd">else</span></pre><!--CRLF--><pre class="alteven"><span id="lnum50" class="lnum">  50:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum51" class="lnum">  51:</span>       connections_.erase(conn);</pre><!--CRLF--><pre class="alteven"><span id="lnum52" class="lnum">  52:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum53" class="lnum">  53:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum54" class="lnum">  54:</span>&nbsp; </pre><pre class="alteven">以下是服务端处理消息的代码，它遍历整个 connections_ 容器，把消息打包发送给各个客户连接。</pre><!--CRLF--><pre class="alteven"><span id="lnum55" class="lnum">  55:</span>   <span class="kwrd">void</span> onStringMessage(<span class="kwrd">const</span> TcpConnectionPtr&amp;,</pre><!--CRLF--><pre class="alteven"><span id="lnum56" class="lnum">  56:</span>                        <span class="kwrd">const</span> string&amp; message,</pre><!--CRLF--><pre class="alteven"><span id="lnum57" class="lnum">  57:</span>                        Timestamp)</pre><!--CRLF--><pre class="alteven"><span id="lnum58" class="lnum">  58:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum59" class="lnum">  59:</span>     MutexLockGuard lock(mutex_);</pre><!--CRLF--><pre class="alteven"><span id="lnum60" class="lnum">  60:</span>     <span class="kwrd">for</span> (ConnectionList::iterator it = connections_.begin();</pre><!--CRLF--><pre class="alteven"><span id="lnum61" class="lnum">  61:</span>         it != connections_.end();</pre><!--CRLF--><pre class="alteven"><span id="lnum62" class="lnum">  62:</span>         ++it)</pre><!--CRLF--><pre class="alteven"><span id="lnum63" class="lnum">  63:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum64" class="lnum">  64:</span>       codec_.send(get_pointer(*it), message);</pre><!--CRLF--><pre class="alteven"><span id="lnum65" class="lnum">  65:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum66" class="lnum">  66:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum67" class="lnum">  67:</span>&nbsp; </pre><pre class="alteven">数据成员：</pre><!--CRLF--><pre class="alteven"><span id="lnum68" class="lnum">  68:</span>   <span class="kwrd">typedef</span> std::set&lt;TcpConnectionPtr&gt; ConnectionList;</pre><!--CRLF--><pre class="alteven"><span id="lnum69" class="lnum">  69:</span>   EventLoop* loop_;</pre><!--CRLF--><pre class="alteven"><span id="lnum70" class="lnum">  70:</span>   TcpServer server_;</pre><!--CRLF--><pre class="alteven"><span id="lnum71" class="lnum">  71:</span>   LengthHeaderCodec codec_;</pre><!--CRLF--><pre class="alteven"><span id="lnum72" class="lnum">  72:</span>   MutexLock mutex_;</pre><!--CRLF--><pre class="alteven"><span id="lnum73" class="lnum">  73:</span>   ConnectionList connections_;</pre><!--CRLF--><pre class="alteven"><span id="lnum74" class="lnum">  74:</span> };</pre><!--CRLF--><pre class="alteven"><span id="lnum75" class="lnum">  75:</span>&nbsp; </pre><pre class="alteven">main() 函数里边是例行公事的代码：</pre><!--CRLF--><pre class="alteven"><span id="lnum76" class="lnum">  76:</span> <span class="kwrd">int</span> main(<span class="kwrd">int</span> argc, <span class="kwrd">char</span>* argv[])</pre><!--CRLF--><pre class="alteven"><span id="lnum77" class="lnum">  77:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum78" class="lnum">  78:</span>   LOG_INFO &lt;&lt; <span class="str">"pid = "</span> &lt;&lt; getpid();</pre><!--CRLF--><pre class="alteven"><span id="lnum79" class="lnum">  79:</span>   <span class="kwrd">if</span> (argc &gt; 1)</pre><!--CRLF--><pre class="alteven"><span id="lnum80" class="lnum">  80:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum81" class="lnum">  81:</span>     EventLoop loop;</pre><!--CRLF--><pre class="alteven"><span id="lnum82" class="lnum">  82:</span>     uint16_t port = <span class="kwrd">static_cast</span>&lt;uint16_t&gt;(atoi(argv[1]));</pre><!--CRLF--><pre class="alteven"><span id="lnum83" class="lnum">  83:</span>     InetAddress serverAddr(port);</pre><!--CRLF--><pre class="alteven"><span id="lnum84" class="lnum">  84:</span>     ChatServer server(&amp;loop, serverAddr);</pre><!--CRLF--><pre class="alteven"><span id="lnum85" class="lnum">  85:</span>     server.start();</pre><!--CRLF--><pre class="alteven"><span id="lnum86" class="lnum">  86:</span>     loop.loop();</pre><!--CRLF--><pre class="alteven"><span id="lnum87" class="lnum">  87:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum88" class="lnum">  88:</span>   <span class="kwrd">else</span></pre><!--CRLF--><pre class="alteven"><span id="lnum89" class="lnum">  89:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum90" class="lnum">  90:</span>     printf(<span class="str">"Usage: %s port\n"</span>, argv[0]);</pre><!--CRLF--><pre class="alteven"><span id="lnum91" class="lnum">  91:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum92" class="lnum">  92:</span> }</pre><!--CRLF--></div></div>
<p>如果你读过 asio 的对应代码，会不会觉得 Reactor 往往比 Proactor 容易使用？</p>
<p>&nbsp;</p>
<h1>客户端的实现</h1>
<p>我有时觉得服务端的程序常常比客户端的更容易写，聊天服务器再次验证了我的看法。客户端的复杂性来自于它要读取键盘输入，而 EventLoop 是独占线程的，所以我用了两个线程，main() 函数所在的线程负责读键盘，另外用一个 EventLoopThread 来处理网络 IO。我暂时没有把标准输入输出融入 Reactor 的想法，因为服务器程序的 stdin 和 stdout 往往是重定向了的。</p>
<p>来看代码，首先，在构造函数里注册回调，并使用了跟前面一样的 LengthHeaderCodec 作为中间层，负责打包分包。</p>
<div id="codeSnippetWrapper" class="csharpcode-wrapper">
<div id="codeSnippet" class="csharpcode"><pre class="alteven"><span id="lnum1" class="lnum">   1:</span> <span class="preproc">#include</span> <span class="str">"codec.h"</span></pre><!--CRLF--><pre class="alteven"><span id="lnum2" class="lnum">   2:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum3" class="lnum">   3:</span> <span class="preproc">#include</span> &lt;muduo/base/Logging.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum4" class="lnum">   4:</span> <span class="preproc">#include</span> &lt;muduo/base/Mutex.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum5" class="lnum">   5:</span> <span class="preproc">#include</span> &lt;muduo/net/EventLoopThread.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum6" class="lnum">   6:</span> <span class="preproc">#include</span> &lt;muduo/net/TcpClient.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum7" class="lnum">   7:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum8" class="lnum">   8:</span> <span class="preproc">#include</span> &lt;boost/bind.hpp&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum9" class="lnum">   9:</span> <span class="preproc">#include</span> &lt;boost/noncopyable.hpp&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum10" class="lnum">  10:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum11" class="lnum">  11:</span> <span class="preproc">#include</span> &lt;iostream&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum12" class="lnum">  12:</span> <span class="preproc">#include</span> &lt;stdio.h&gt;</pre><!--CRLF--><pre class="alteven"><span id="lnum13" class="lnum">  13:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum14" class="lnum">  14:</span> <span class="kwrd">using</span> <span class="kwrd">namespace</span> muduo;</pre><!--CRLF--><pre class="alteven"><span id="lnum15" class="lnum">  15:</span> <span class="kwrd">using</span> <span class="kwrd">namespace</span> muduo::net;</pre><!--CRLF--><pre class="alteven"><span id="lnum16" class="lnum">  16:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum17" class="lnum">  17:</span> <span class="kwrd">class</span> ChatClient : boost::noncopyable</pre><!--CRLF--><pre class="alteven"><span id="lnum18" class="lnum">  18:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum19" class="lnum">  19:</span>  <span class="kwrd">public</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum20" class="lnum">  20:</span>   ChatClient(EventLoop* loop, <span class="kwrd">const</span> InetAddress&amp; listenAddr)</pre><!--CRLF--><pre class="alteven"><span id="lnum21" class="lnum">  21:</span>     : loop_(loop),</pre><!--CRLF--><pre class="alteven"><span id="lnum22" class="lnum">  22:</span>       client_(loop, listenAddr, <span class="str">"ChatClient"</span>),</pre><!--CRLF--><pre class="alteven"><span id="lnum23" class="lnum">  23:</span>       codec_(boost::bind(&amp;ChatClient::onStringMessage, <span class="kwrd">this</span>, _1, _2, _3))</pre><!--CRLF--><pre class="alteven"><span id="lnum24" class="lnum">  24:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum25" class="lnum">  25:</span>     client_.setConnectionCallback(</pre><!--CRLF--><pre class="alteven"><span id="lnum26" class="lnum">  26:</span>         boost::bind(&amp;ChatClient::onConnection, <span class="kwrd">this</span>, _1));</pre><!--CRLF--><pre class="alteven"><span id="lnum27" class="lnum">  27:</span>     client_.setMessageCallback(</pre><!--CRLF--><pre class="alteven"><span id="lnum28" class="lnum">  28:</span>         boost::bind(&amp;LengthHeaderCodec::onMessage, &amp;codec_, _1, _2, _3));</pre><!--CRLF--><pre class="alteven"><span id="lnum29" class="lnum">  29:</span>     client_.enableRetry();</pre><!--CRLF--><pre class="alteven"><span id="lnum30" class="lnum">  30:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum31" class="lnum">  31:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum32" class="lnum">  32:</span>   <span class="kwrd">void</span> connect()</pre><!--CRLF--><pre class="alteven"><span id="lnum33" class="lnum">  33:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum34" class="lnum">  34:</span>     client_.connect();</pre><!--CRLF--><pre class="alteven"><span id="lnum35" class="lnum">  35:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum36" class="lnum">  36:</span>&nbsp; </pre><pre class="alteven">disconnect() 目前为空，客户端的连接由操作系统在进程终止时关闭。</pre><!--CRLF--><pre class="alteven"><span id="lnum37" class="lnum">  37:</span>   <span class="kwrd">void</span> disconnect()</pre><!--CRLF--><pre class="alteven"><span id="lnum38" class="lnum">  38:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum39" class="lnum">  39:</span>     <span class="rem">// client_.disconnect();</span></pre><!--CRLF--><pre class="alteven"><span id="lnum40" class="lnum">  40:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum41" class="lnum">  41:</span>&nbsp; </pre><pre class="alteven">write() 会由 main 线程调用，所以要加锁，这个锁不是为了保护 TcpConnection，而是保护 shared_ptr。</pre><!--CRLF--><pre class="alteven"><span id="lnum42" class="lnum">  42:</span>   <span class="kwrd">void</span> write(<span class="kwrd">const</span> string&amp; message)</pre><!--CRLF--><pre class="alteven"><span id="lnum43" class="lnum">  43:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum44" class="lnum">  44:</span>     MutexLockGuard lock(mutex_);</pre><!--CRLF--><pre class="alteven"><span id="lnum45" class="lnum">  45:</span>     <span class="kwrd">if</span> (connection_)</pre><!--CRLF--><pre class="alteven"><span id="lnum46" class="lnum">  46:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum47" class="lnum">  47:</span>       codec_.send(get_pointer(connection_), message);</pre><!--CRLF--><pre class="alteven"><span id="lnum48" class="lnum">  48:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum49" class="lnum">  49:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum50" class="lnum">  50:</span>&nbsp; </pre><pre class="alteven">onConnection() 会由 EventLoop 线程调用，所以要加锁以保护 shared_ptr。</pre><!--CRLF--><pre class="alteven"><span id="lnum51" class="lnum">  51:</span>  <span class="kwrd">private</span>:</pre><!--CRLF--><pre class="alteven"><span id="lnum52" class="lnum">  52:</span>   <span class="kwrd">void</span> onConnection(<span class="kwrd">const</span> TcpConnectionPtr&amp; conn)</pre><!--CRLF--><pre class="alteven"><span id="lnum53" class="lnum">  53:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum54" class="lnum">  54:</span>     LOG_INFO &lt;&lt; conn-&gt;localAddress().toHostPort() &lt;&lt; <span class="str">" -&gt; "</span></pre><!--CRLF--><pre class="alteven"><span id="lnum55" class="lnum">  55:</span>         &lt;&lt; conn-&gt;peerAddress().toHostPort() &lt;&lt; <span class="str">" is "</span></pre><!--CRLF--><pre class="alteven"><span id="lnum56" class="lnum">  56:</span>         &lt;&lt; (conn-&gt;connected() ? <span class="str">"UP"</span> : <span class="str">"DOWN"</span>);</pre><!--CRLF--><pre class="alteven"><span id="lnum57" class="lnum">  57:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum58" class="lnum">  58:</span>     MutexLockGuard lock(mutex_);</pre><!--CRLF--><pre class="alteven"><span id="lnum59" class="lnum">  59:</span>     <span class="kwrd">if</span> (conn-&gt;connected())</pre><!--CRLF--><pre class="alteven"><span id="lnum60" class="lnum">  60:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum61" class="lnum">  61:</span>       connection_ = conn;</pre><!--CRLF--><pre class="alteven"><span id="lnum62" class="lnum">  62:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum63" class="lnum">  63:</span>     <span class="kwrd">else</span></pre><!--CRLF--><pre class="alteven"><span id="lnum64" class="lnum">  64:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum65" class="lnum">  65:</span>       connection_.reset();</pre><!--CRLF--><pre class="alteven"><span id="lnum66" class="lnum">  66:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum67" class="lnum">  67:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum68" class="lnum">  68:</span>&nbsp; </pre><pre class="alteven">把收到的消息打印到屏幕，这个函数由 EventLoop 线程调用，但是不用加锁，因为 printf() 是线程安全的。</pre><pre class="alteven">注意这里不能用 cout，它不是线程安全的。</pre><!--CRLF--><pre class="alteven"><span id="lnum69" class="lnum">  69:</span>   <span class="kwrd">void</span> onStringMessage(<span class="kwrd">const</span> TcpConnectionPtr&amp;,</pre><!--CRLF--><pre class="alteven"><span id="lnum70" class="lnum">  70:</span>                        <span class="kwrd">const</span> string&amp; message,</pre><!--CRLF--><pre class="alteven"><span id="lnum71" class="lnum">  71:</span>                        Timestamp)</pre><!--CRLF--><pre class="alteven"><span id="lnum72" class="lnum">  72:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum73" class="lnum">  73:</span>     printf(<span class="str">"&lt;&lt;&lt; %s\n"</span>, message.c_str());</pre><!--CRLF--><pre class="alteven"><span id="lnum74" class="lnum">  74:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum75" class="lnum">  75:</span>&nbsp; </pre><pre class="alteven">&nbsp;</pre><pre class="alteven">数据成员：</pre><!--CRLF--><pre class="alteven"><span id="lnum76" class="lnum">  76:</span>   EventLoop* loop_;</pre><!--CRLF--><pre class="alteven"><span id="lnum77" class="lnum">  77:</span>   TcpClient client_;</pre><!--CRLF--><pre class="alteven"><span id="lnum78" class="lnum">  78:</span>   LengthHeaderCodec codec_;</pre><!--CRLF--><pre class="alteven"><span id="lnum79" class="lnum">  79:</span>   MutexLock mutex_;</pre><!--CRLF--><pre class="alteven"><span id="lnum80" class="lnum">  80:</span>   TcpConnectionPtr connection_;</pre><!--CRLF--><pre class="alteven"><span id="lnum81" class="lnum">  81:</span> };</pre><!--CRLF--><pre class="alteven"><span id="lnum82" class="lnum">  82:</span>&nbsp; </pre><pre class="alteven">main() 函数里除了例行公事，还要启动 EventLoop 线程和读取键盘输入。</pre><!--CRLF--><pre class="alteven"><span id="lnum83" class="lnum">  83:</span> <span class="kwrd">int</span> main(<span class="kwrd">int</span> argc, <span class="kwrd">char</span>* argv[])</pre><!--CRLF--><pre class="alteven"><span id="lnum84" class="lnum">  84:</span> {</pre><!--CRLF--><pre class="alteven"><span id="lnum85" class="lnum">  85:</span>   LOG_INFO &lt;&lt; <span class="str">"pid = "</span> &lt;&lt; getpid();</pre><!--CRLF--><pre class="alteven"><span id="lnum86" class="lnum">  86:</span>   <span class="kwrd">if</span> (argc &gt; 2)</pre><!--CRLF--><pre class="alteven"><span id="lnum87" class="lnum">  87:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum88" class="lnum">  88:</span>     EventLoopThread loopThread;</pre><!--CRLF--><pre class="alteven"><span id="lnum89" class="lnum">  89:</span>     uint16_t port = <span class="kwrd">static_cast</span>&lt;uint16_t&gt;(atoi(argv[2]));</pre><!--CRLF--><pre class="alteven"><span id="lnum90" class="lnum">  90:</span>     InetAddress serverAddr(argv[1], port);</pre><!--CRLF--><pre class="alteven"><span id="lnum91" class="lnum">  91:</span>&nbsp; </pre><!--CRLF--><pre class="alteven"><span id="lnum92" class="lnum">  92:</span>     ChatClient client(loopThread.startLoop(), serverAddr); // 注册到 EventLoopThread 的 EventLoop 上。</pre><!--CRLF--><pre class="alteven"><span id="lnum93" class="lnum">  93:</span>     client.connect();</pre><!--CRLF--><pre class="alteven"><span id="lnum94" class="lnum">  94:</span>     std::string line;</pre><!--CRLF--><pre class="alteven"><span id="lnum95" class="lnum">  95:</span>     <span class="kwrd">while</span> (std::getline(std::cin, line))</pre><!--CRLF--><pre class="alteven"><span id="lnum96" class="lnum">  96:</span>     {</pre><!--CRLF--><pre class="alteven"><span id="lnum97" class="lnum">  97:</span>       string message(line.c_str()); // 这里似乎多此一举，可直接发送 line。这里是</pre><!--CRLF--><pre class="alteven"><span id="lnum98" class="lnum">  98:</span>       client.write(message);</pre><!--CRLF--><pre class="alteven"><span id="lnum99" class="lnum">  99:</span>     }</pre><!--CRLF--><pre class="alteven"><span id="lnum100" class="lnum"> 100:</span>     client.disconnect();</pre><!--CRLF--><pre class="alteven"><span id="lnum101" class="lnum"> 101:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum102" class="lnum"> 102:</span>   <span class="kwrd">else</span></pre><!--CRLF--><pre class="alteven"><span id="lnum103" class="lnum"> 103:</span>   {</pre><!--CRLF--><pre class="alteven"><span id="lnum104" class="lnum"> 104:</span>     printf(<span class="str">"Usage: %s host_ip port\n"</span>, argv[0]);</pre><!--CRLF--><pre class="alteven"><span id="lnum105" class="lnum"> 105:</span>   }</pre><!--CRLF--><pre class="alteven"><span id="lnum106" class="lnum"> 106:</span> }</pre><!--CRLF--><pre class="alteven"><span id="lnum107" class="lnum"> 107:</span>&nbsp; </pre><!--CRLF--></div></div>
<p>&nbsp;</p>
<h1>简单测试</h1>
<p>开三个命令行窗口，在第一个运行</p>
<p>$ ./asio_chat_server 3000</p>
<p>&nbsp;</p>
<p>第二个运行</p>
<p>$ ./asio_chat_client 127.0.0.1 3000</p>
<p>&nbsp;</p>
<p>第三个运行同样的命令</p>
<p>$ ./asio_chat_client 127.0.0.1 3000</p>
<p>&nbsp;</p>
<p>这样就有两个客户端进程参与聊天。在第二个窗口里输入一些字符并回车，字符会出现在本窗口和第三个窗口中。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>下一篇文章我会介绍 Muduo 中的定时器，并实现 Boost.Asio 教程中的 timer2~5 示例，以及带流量统计功能的 discard 和 echo 服务器（来自 Java Netty）。流量等于单位时间内发送或接受的字节数，这要用到定时器功能。</p>
<p>(待续)</p><img src ="http://www.cppblog.com/Solstice/aggbug/139718.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-04 08:57 <a href="http://www.cppblog.com/Solstice/archive/2011/02/04/139718.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之一：五个简单 TCP 协议</title><link>http://www.cppblog.com/Solstice/archive/2011/02/02/139688.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Wed, 02 Feb 2011 04:33:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/02/139688.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/139688.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/02/139688.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/139688.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/139688.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 这是《Muduo 网络编程示例》系列的第一篇文章。本文将介绍五个简单 TCP 网络服务协议的 muduo 实现，包括 echo、discard、chargen、daytime、time，以及 time 协议的客户端。以上五个协议使用不同的端口，可以放到同一个进程中实现，且不必使用多线程。&nbsp;&nbsp;<a href='http://www.cppblog.com/Solstice/archive/2011/02/02/139688.html'>阅读全文</a><img src ="http://www.cppblog.com/Solstice/aggbug/139688.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-02 12:33 <a href="http://www.cppblog.com/Solstice/archive/2011/02/02/139688.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Muduo 网络编程示例之零：前言</title><link>http://www.cppblog.com/Solstice/archive/2011/02/02/139673.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Tue, 01 Feb 2011 17:07:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2011/02/02/139673.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/139673.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2011/02/02/139673.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/139673.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/139673.html</trackback:ping><description><![CDATA[<p>陈硕 (giantchen_AT_gmail)</p>
<p>Blog.csdn.net/Solstice</p>
<p>Muduo 全系列文章列表： <a href="http://blog.csdn.net/Solstice/category/779646.aspx">http://blog.csdn.net/Solstice/category/779646.aspx</a></p>
<p>我将会写一系列文章，介绍用 <a href="http://code.google.com/p/muduo/">muduo 网络库</a>完成常见的 TCP 网络编程任务。目前计划如下：</p>
<ol>
    <li><a href="http://www.unpbook.com/">UNP</a> 中的简单协议，包括 echo、daytime、time、discard 等。&nbsp;
    <li><a href="http://www.boost.org/doc/libs/release/doc/html/boost_asio.html">Boost.Asio</a> 中的示例，包括 timer2~6、chat 等。
    <li><a href="http://www.jboss.org/netty">Java Netty</a> 中的示例，包括 discard、echo、uptime 等，其中的 discard 和 echo 带流量统计功能。
    <li><a href="http://www.twistedmatrix.com/">Python twisted</a> 中的示例，包括 finger01~07
    <li><a href="http://blog.codingnow.com/2010/11/go_prime.html">云风的串并转换连接服务器</a> multiplexer，包括单线程和多线程两个版本。
    <li>用于测试两台机器的往返延迟的 roundtrip
    <li>用于测试两台机器的带宽的 pingpong
    <li>文件传输
    <li>一个基于 TCP 的应用层广播 hub
    <li>socks4a 代理服务器，包括简单的 TCP 中继(relay)。
    <li>一个 Sudoku 服务器的演变，从单线程到多线程，从阻塞到 event-based。
    <li>一个提供短址服务的 httpd 服务器 </li>
</ol>
<p>其中前面 7 个已经放到了 muduo 代码的 examples 目录中，下载地址是： <a title=http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz href="http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz</a>&nbsp;</p>
<p>这些例子都比较简单，逻辑不复杂，代码也很短，适合摘取关键部分放到博客上。其中一些有一定的代表性与针对性，比如&#8220;如何传输完整的文件&#8221;估计是网络编程的初学者经常遇到的问题。请注意，muduo 是设计来开发内网的网络程序，它没有做任何安全方面的加强措施，如果用在公网上可能会受到攻击，在后面的例子中我会谈到这一点。</p>
<p>本系列文章适用于 Linux 2.6.x (x &gt; 25)，主要测试发行版为 <a href="http://releases.ubuntu.com/lucid/">Ubuntu 10.04 LTS</a> 和 <a href="http://www.debian.org/releases/squeeze/">Debian 6.0 Squeeze</a>，64-bit x86 硬件。</p>
<h1>TCP 网络编程本质论</h1>
<p>我认为，TCP 网络编程最本质的是处理三个半事件：</p>
<ol>
    <li>连接的建立，包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接。
    <li>连接的断开，包括主动断开 (close 或 shutdown) 和被动断开 (read 返回 0)。
    <li>消息到达，文件描述符可读。这是最为重要的一个事件，对它的处理方式决定了网络编程的风格（阻塞还是非阻塞，如何处理分包，应用层的缓冲如何设计等等）。
    <li>消息发送完毕，这算半个。对于低流量的服务，可以不必关心这个事件；另外，这里&#8220;发送完毕&#8221;是指将数据写入操作系统的缓冲区，将由 TCP 协议栈负责数据的发送与重传，不代表对方已经收到数据。 </li>
</ol>
<p>这其中有很多难点，也有很多细节需要注意，比方说：</p>
<ol>
    <li>如果要主动关闭连接，如何保证对方已经收到全部数据？如果应用层有缓冲（这在非阻塞网络编程中是必须的，见下文），那么如何保证<span style="WIDOWS: 2; TEXT-TRANSFORM: none; TEXT-INDENT: 0px; BORDER-COLLAPSE: separate; FONT: medium 'Times New Roman'; WHITE-SPACE: normal; ORPHANS: 2; LETTER-SPACING: normal; COLOR: rgb(0,0,0); WORD-SPACING: 0px; -webkit-border-horizontal-spacing: 0px; -webkit-border-vertical-spacing: 0px; -webkit-text-decorations-in-effect: none; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px" class=Apple-style-span><span style="TEXT-ALIGN: left; LINE-HEIGHT: 21px; FONT-FAMILY: verdana, sans-serif; FONT-SIZE: 14px" class=Apple-style-span>先发送完缓冲区中的数据，然后再断开连接</span></span>。直接调用 close(2) 恐怕是不行的。
    <li>如果主动发起连接，但是对方主动拒绝，如何定期 (带 back-off) 重试？
    <li>非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)？（这两个中文术语有其他译法，我选择了一个电子工程师熟悉的说法。）如果是电平触发，那么什么时候关注 EPOLLOUT 事件？会不会造成 busy-loop？如果是边沿触发，如何防止漏读造成的饥饿？epoll 一定比 poll 快吗？
    <li>在非阻塞网络编程中，为什么要使用应用层缓冲区？假如一次读到的数据不够一个完整的数据包，那么这些已经读到的数据是不是应该先暂存在某个地方，等剩余的数据收到之后再一并处理？见 lighttpd 关于 <a href="http://redmine.lighttpd.net/issues/show/2105">\r\n\r\n 分包的 bug</a>。假如数据是一个字节一个字节地到达，间隔 10ms，每个字节触发一次文件描述符可读 (readable) 事件，程序是否还能正常工作？lighttpd 在这个问题上出过<a href="http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2010_01.txt">安全漏洞</a>。
    <li>在非阻塞网络编程中，如何设计并使用缓冲区？一方面我们希望减少系统调用，一次读的数据越多越划算，那么似乎应该准备一个大的缓冲区。另一方面，我们系统减少内存占用。如果有 10k 个连接，每个连接一建立就分配 64k 的读缓冲的话，将占用 640M 内存，而大多数时候这些缓冲区的使用率很低。muduo 用 readv 结合栈上空间巧妙地解决了这个问题。
    <li>如果使用发送缓冲区，万一接收方处理缓慢，数据会不会一直堆积在发送方，造成内存暴涨？如何做应用层的流量控制？
    <li>如何设计并实现定时器？并使之与网络 IO 共用一个线程，以避免锁。 </li>
</ol>
<p>这些问题在 muduo 的代码中可以找到答案。</p>
<h1>Muduo 简介</h1>
<p>我编写 Muduo 网络库的目的之一就是简化日常的 TCP 网络编程，让程序员能把精力集中在业务逻辑的实现上，而不要天天和 Sockets API 较劲。借用 Brooks 的话说，我希望 Muduo 能减少网络编程中的偶发复杂性 (accidental complexity)。</p>
<p>Muduo 只支持 Linux 2.6.x 下的并发非阻塞 TCP 网络编程，它的安装方法见<a href="http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx">陈硕的 blog 文章</a>。</p>
<p>Muduo 的使用非常简单，不需要从指定的类派生，也不用覆写虚函数，只需要注册几个回调函数去处理前面提到的三个半事件就行了。</p>
<p>以经典的 echo 回显服务为例：</p>
<h2>1. 定义 EchoServer class，不需要派生自任何基类：</h2>
<p>&nbsp;</p>
<div style="BORDER-BOTTOM: #cccccc 1px solid; BORDER-LEFT: #cccccc 1px solid; PADDING-BOTTOM: 4px; BACKGROUND-COLOR: #eeeeee; PADDING-LEFT: 4px; WIDTH: 98%; PADDING-RIGHT: 5px; FONT-SIZE: 13px; WORD-BREAK: break-all; BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; PADDING-TOP: 4px"><span style="COLOR: #008080">&nbsp;1</span>&nbsp;<span style="COLOR: #000000">#ifndef&nbsp;MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H&nbsp;<br></span><span style="COLOR: #008080">&nbsp;2</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">#define</span><span style="COLOR: #000000">&nbsp;MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H</span><span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;3</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;4</span>&nbsp;<span style="COLOR: #000000">#include&nbsp;</span><span style="COLOR: #000000">&lt;</span><span style="COLOR: #000000">muduo</span><span style="COLOR: #000000">/</span><span style="COLOR: #000000">net</span><span style="COLOR: #000000">/</span><span style="COLOR: #000000">TcpServer.h</span><span style="COLOR: #000000">&gt;</span><span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;5</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;6</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;RFC&nbsp;862&nbsp;</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">&nbsp;7</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #0000ff">class</span><span style="COLOR: #000000">&nbsp;EchoServer&nbsp;<br></span><span style="COLOR: #008080">&nbsp;8</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">&nbsp;9</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">public</span><span style="COLOR: #000000">:&nbsp;<br></span><span style="COLOR: #008080">10</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;EchoServer(muduo::net::EventLoop</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;loop,&nbsp;<br></span><span style="COLOR: #008080">11</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;muduo::net::InetAddress</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;listenAddr);<br></span><span style="COLOR: #008080">12</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">13</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;</span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;start();<br></span><span style="COLOR: #008080">14</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">15</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">private</span><span style="COLOR: #000000">:&nbsp;<br></span><span style="COLOR: #008080">16</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;</span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;onConnection(</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;muduo::net::TcpConnectionPtr</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;conn);<br></span><span style="COLOR: #008080">17</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">18</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;</span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;onMessage(</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;muduo::net::TcpConnectionPtr</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;conn,&nbsp;<br></span><span style="COLOR: #008080">19</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;muduo::net::Buffer</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;buf,&nbsp;<br></span><span style="COLOR: #008080">20</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;muduo::Timestamp&nbsp;time);<br></span><span style="COLOR: #008080">21</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">22</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;muduo::net::EventLoop</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;loop_;&nbsp;<br></span><span style="COLOR: #008080">23</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;muduo::net::TcpServer&nbsp;server_;&nbsp;<br></span><span style="COLOR: #008080">24</span>&nbsp;<span style="COLOR: #000000">};<br></span><span style="COLOR: #008080">25</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">26</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">#endif</span><span style="COLOR: #000000">&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H</span><span style="COLOR: #000000"><br></span><span style="COLOR: #008080">27</span>&nbsp;<span style="COLOR: #000000"></span></div>
<p>&nbsp;</p>
<h2>在构造函数里注册回调函数：</h2>
<p>&nbsp;</p>
<div style="BORDER-BOTTOM: #cccccc 1px solid; BORDER-LEFT: #cccccc 1px solid; PADDING-BOTTOM: 4px; BACKGROUND-COLOR: #eeeeee; PADDING-LEFT: 4px; WIDTH: 98%; PADDING-RIGHT: 5px; FONT-SIZE: 13px; WORD-BREAK: break-all; BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; PADDING-TOP: 4px"><span style="COLOR: #008080">&nbsp;1</span>&nbsp;<span style="COLOR: #000000">EchoServer::EchoServer(EventLoop</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;loop,&nbsp;<br></span><span style="COLOR: #008080">&nbsp;2</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;InetAddress</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;listenAddr)&nbsp;<br></span><span style="COLOR: #008080">&nbsp;3</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;:&nbsp;loop_(loop),&nbsp;<br></span><span style="COLOR: #008080">&nbsp;4</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;server_(loop,&nbsp;listenAddr,&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">EchoServer</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">)&nbsp;<br></span><span style="COLOR: #008080">&nbsp;5</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">&nbsp;6</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;server_.setConnectionCallback(&nbsp;<br></span><span style="COLOR: #008080">&nbsp;7</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;boost::bind(</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">EchoServer::onConnection,&nbsp;</span><span style="COLOR: #0000ff">this</span><span style="COLOR: #000000">,&nbsp;_1));&nbsp;<br></span><span style="COLOR: #008080">&nbsp;8</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;server_.setMessageCallback(&nbsp;<br></span><span style="COLOR: #008080">&nbsp;9</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;boost::bind(</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">EchoServer::onMessage,&nbsp;</span><span style="COLOR: #0000ff">this</span><span style="COLOR: #000000">,&nbsp;_1,&nbsp;_2,&nbsp;_3));&nbsp;<br></span><span style="COLOR: #008080">10</span>&nbsp;<span style="COLOR: #000000">}<br></span><span style="COLOR: #008080">11</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">12</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;EchoServer::start()&nbsp;<br></span><span style="COLOR: #008080">13</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">14</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;server_.start();&nbsp;<br></span><span style="COLOR: #008080">15</span>&nbsp;<span style="COLOR: #000000">}&nbsp;<br></span><span style="COLOR: #008080">16</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">17</span>&nbsp;<span style="COLOR: #000000"></span></div>
<p>&nbsp;</p>
<h2>2. 实现 EchoServer::onConnection() 和 EchoServer::onMessage()：</h2>
<p>&nbsp;</p>
<div style="BORDER-BOTTOM: #cccccc 1px solid; BORDER-LEFT: #cccccc 1px solid; PADDING-BOTTOM: 4px; BACKGROUND-COLOR: #eeeeee; PADDING-LEFT: 4px; WIDTH: 98%; PADDING-RIGHT: 5px; FONT-SIZE: 13px; WORD-BREAK: break-all; BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; PADDING-TOP: 4px"><span style="COLOR: #008080">&nbsp;1</span>&nbsp;<span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;EchoServer::onConnection(</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;TcpConnectionPtr</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;conn)&nbsp;<br></span><span style="COLOR: #008080">&nbsp;2</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">&nbsp;3</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;LOG_INFO&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">EchoServer&nbsp;-&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;conn</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">peerAddress().toHostPort()&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;-&gt;&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">&nbsp;4</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;conn</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">localAddress().toHostPort()&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;is&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">&nbsp;5</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;(conn</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">connected()&nbsp;</span><span style="COLOR: #000000">?</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">UP</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;:&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">DOWN</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">);&nbsp;<br></span><span style="COLOR: #008080">&nbsp;6</span>&nbsp;<span style="COLOR: #000000">}<br></span><span style="COLOR: #008080">&nbsp;7</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;8</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;EchoServer::onMessage(</span><span style="COLOR: #0000ff">const</span><span style="COLOR: #000000">&nbsp;TcpConnectionPtr</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;conn,&nbsp;<br></span><span style="COLOR: #008080">&nbsp;9</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Buffer</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;buf,&nbsp;<br></span><span style="COLOR: #008080">10</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Timestamp&nbsp;time)&nbsp;<br></span><span style="COLOR: #008080">11</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">12</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;</span><span style="COLOR: #0000ff">string</span><span style="COLOR: #000000">&nbsp;msg(buf</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">retrieveAsString());&nbsp;<br></span><span style="COLOR: #008080">13</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;LOG_INFO&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;conn</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">name()&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;echo&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;msg.size()&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;bytes&nbsp;at&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;time.toString();&nbsp;<br></span><span style="COLOR: #008080">14</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;conn</span><span style="COLOR: #000000">-&gt;</span><span style="COLOR: #000000">send(msg);&nbsp;<br></span><span style="COLOR: #008080">15</span>&nbsp;<span style="COLOR: #000000">}<br></span><span style="COLOR: #008080">16</span>&nbsp;<span style="COLOR: #000000"></span></div>
<p>&nbsp;<br></p>
<h2>3. 在 main() 里用 EventLoop 让整个程序跑起来：</h2>
<p>&nbsp;</p>
<div style="BORDER-BOTTOM: #cccccc 1px solid; BORDER-LEFT: #cccccc 1px solid; PADDING-BOTTOM: 4px; BACKGROUND-COLOR: #eeeeee; PADDING-LEFT: 4px; WIDTH: 98%; PADDING-RIGHT: 5px; FONT-SIZE: 13px; WORD-BREAK: break-all; BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; PADDING-TOP: 4px"><span style="COLOR: #008080">&nbsp;1</span>&nbsp;<span style="COLOR: #000000">#include&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">echo.h</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;2</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;3</span>&nbsp;<span style="COLOR: #000000">#include&nbsp;</span><span style="COLOR: #000000">&lt;</span><span style="COLOR: #000000">muduo</span><span style="COLOR: #000000">/</span><span style="COLOR: #0000ff">base</span><span style="COLOR: #000000">/</span><span style="COLOR: #000000">Logging.h</span><span style="COLOR: #000000">&gt;</span><span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">&nbsp;4</span>&nbsp;<span style="COLOR: #000000">#include&nbsp;</span><span style="COLOR: #000000">&lt;</span><span style="COLOR: #000000">muduo</span><span style="COLOR: #000000">/</span><span style="COLOR: #000000">net</span><span style="COLOR: #000000">/</span><span style="COLOR: #000000">EventLoop.h</span><span style="COLOR: #000000">&gt;</span><span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;5</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;6</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">using</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">namespace</span><span style="COLOR: #000000">&nbsp;muduo;&nbsp;<br></span><span style="COLOR: #008080">&nbsp;7</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">using</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">namespace</span><span style="COLOR: #000000">&nbsp;muduo::net;<br></span><span style="COLOR: #008080">&nbsp;8</span>&nbsp;<span style="COLOR: #000000"><br></span><span style="COLOR: #008080">&nbsp;9</span>&nbsp;<span style="COLOR: #000000"></span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;main()&nbsp;<br></span><span style="COLOR: #008080">10</span>&nbsp;<span style="COLOR: #000000">{&nbsp;<br></span><span style="COLOR: #008080">11</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;LOG_INFO&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">pid&nbsp;=&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">&lt;&lt;</span><span style="COLOR: #000000">&nbsp;getpid();&nbsp;<br></span><span style="COLOR: #008080">12</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;EventLoop&nbsp;loop;&nbsp;<br></span><span style="COLOR: #008080">13</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;InetAddress&nbsp;listenAddr(</span><span style="COLOR: #000000">2007</span><span style="COLOR: #000000">);&nbsp;<br></span><span style="COLOR: #008080">14</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;EchoServer&nbsp;server(</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">loop,&nbsp;listenAddr);&nbsp;<br></span><span style="COLOR: #008080">15</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;server.start();&nbsp;<br></span><span style="COLOR: #008080">16</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;loop.loop();&nbsp;<br></span><span style="COLOR: #008080">17</span>&nbsp;<span style="COLOR: #000000">}<br></span><span style="COLOR: #008080">18</span>&nbsp;<span style="COLOR: #000000"></span></div>
<p>&nbsp;<br></p>
<p>完整的代码见 muduo/examples/simple/echo。<br>这个几十行的小程序实现了一个并发的 echo 服务程序，可以同时处理多个连接。<br>对这个程序的详细分析见下一篇博客《Muduo 网络编程示例之一：五个简单 TCP 协议》</p>
<p>(待续)</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/139673.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2011-02-02 01:07 <a href="http://www.cppblog.com/Solstice/archive/2011/02/02/139673.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>击鼓传花：对比 muduo 与 libevent2 的事件处理效率</title><link>http://www.cppblog.com/Solstice/archive/2010/09/08/muduo_vs_libevent_bench.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Tue, 07 Sep 2010 17:15:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2010/09/08/muduo_vs_libevent_bench.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/126130.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2010/09/08/muduo_vs_libevent_bench.html#Feedback</comments><slash:comments>4</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/126130.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/126130.html</trackback:ping><description><![CDATA[<p>前面我们<a href="http://blog.csdn.net/Solstice/archive/2010/09/05/5864889.aspx">比较了 muduo 和 libevent2 的吞吐量</a>，得到的结论是 muduo 比 libevent2 快 18%。有人会说，libevent2 并不是为高吞吐的应用场景而设计的，这样的比较不公平，胜之不武。为了公平起见，这回我们用 libevent2 自带的性能测试程序（击鼓传花）来对比 muduo 和 libevent2 在高并发情况下的 IO 事件处理效率。</p>
<h1>测试对象</h1>
<ul>
    <li>libevent 2.0.6-rc， 源代码包 <a href="http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz">http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz</a>&nbsp;
    <li>muduo 0.1.2-alpha，源码 <a href="http://muduo.googlecode.com/files/muduo-0.1.2-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.2-alpha.tar.gz</a> SHA1 Checksum: 9e7da4b46ad87602dd206eaedf54e67c17dfe4e1 。须编译为 release 版。 </li>
</ul>
<h1>测试环境</h1>
<p>测试用的软硬件环境与《<a href="http://blog.csdn.net/Solstice/archive/2010/09/04/5863411.aspx">muduo 与 boost asio 吞吐量对比</a>》和《<a href="http://blog.csdn.net/Solstice/archive/2010/09/05/5864889.aspx">muduo 与 libevent2 吞吐量对比</a>》相同，另外我还在自己的笔记本上运行了测试，结果也附在后面。</p>
<h1>测试内容</h1>
<p>测试的场景是：有 1000 个人围成一圈，玩击鼓传花的游戏，一开始第 1 个人手里有花，他把花传给右手边的人，那个人再继续把花传给右手边的人，当花转手 100 次之后游戏停止，记录从开始到结束的时间。</p>
<p>用程序表达是，有 1000 个网络连接 (socketpairs 或 pipes)，数据在这些连接中顺次传递，一开始往第 1 个连接里写 1 个字节，然后从这个连接的另一头读出这 1 个字节，再写入第 2 个连接，然后读出来继续写到第 3 个连接，直到一共写了 100 次之后程序停止，记录所用的时间。</p>
<p>以上是只有一个活动连接的场景，我们实际测试的是 100 个或 1000 个活动连接（即 100 朵花或 1000 朵花，均匀分散在人群手中），而连接总数（即并发数）从 100 到 100,000 (十万)。注意每个连接是两个文件描述符，为了运行测试，需要调高每个进程能打开的文件数，比如设为 256000。</p>
<p>libevent2 的测试代码位于 test/bench.c，我修复了 2.0.6-rc 版里的一个小 bug，修正后的代码见 <a href="http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/bench.c">http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/bench.c</a></p>
<p>muduo 的测试代码位于 examples/pingpong/bench.cc，见 <a href="http://gist.github.com/564985#file_pingpong_bench.cc">http://gist.github.com/564985#file_pingpong_bench.cc</a></p>
<h1>测试结果与讨论</h1>
<p>第一轮，分别用 100 个活动连接和 1000 个活动连接，无超时，读写 100 次，测试一次游戏的总时间（包含初始化）和事件处理的时间（不包含注册 event watcher）随连接数（并发数）变化的情况。具体解释见 libev 的性能测试文档 <a href="http://libev.schmorp.de/bench.html">http://libev.schmorp.de/bench.html</a> ，不同之处在于我们不比较 timer event 的性能，只比较 IO event 的性能。对每个并发数，程序循环 25 次，刨去第一次的热身数据，后 24 次算平均值。测试用的脚本在 <a href="http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh">http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh</a> 。这个脚本是 libev 的作者 Marc Lehmann 写的，我略作改用，用于测试 muduo 和 libevent2。</p>
<p>第一轮的结果，请先只看红线和绿线。红线是 libevent2 用的时间，绿线是 muduo 用的时间。数字越小越好。注意这个图的横坐标是对数的，每一个数量级的取值点为 1, 2, 3, 4, 5, 6, 7.5, 10。</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=muduo_libevent_bench_490 border=0 alt=muduo_libevent_bench_490 src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduolibevent2_4F9/muduo_libevent_bench_490_3.png" width=1200 height=800> </p>
<p>从红绿线对比可以看出：</p>
<p>1. libevent2 在初始化 event watcher 上面比 muduo 快 20% （左边的两个图）</p>
<p>2. 在事件处理方面（右边的两个图）：a) 在 100 个活动连接的情况下，libevent2 和 muduo 分段领先。当总连接数（并发数）小于 1000 时，二者性能差不多；当总连接数大于 30000 时，muduo 略占优；当总连接数大于 1000 小于 30000 时，libevent2 明显领先。b) 在 1000 个活动连接的情况下，当并发数小于 10000 时，libevent2 和 muduo 得分接近；当并发数大于 10000 时，muduo 明显占优。</p>
<p>这里我们有两个问题：1. 为什么 muduo 花在初始化上的时间比较多？ 2. 为什么在一些情况下它比 libevent2 慢很多。</p>
<p>我仔细分析了其中的原因，并参考了 libev 的作者 Marc Lehmann 的观点 ( <a href="http://lists.schmorp.de/pipermail/libev/2010q2/001041.html">http://lists.schmorp.de/pipermail/libev/2010q2/001041.html</a> )，结论是：在第一轮初始化时，libevent2 和 muduo 都是用 epoll_ctl(fd, EPOLL_CTL_ADD, &#8230;) 来添加 fd event watcher。不同之处在于，在后面 24 轮中，muduo 使用了 epoll_ctl(fd, EPOLL_CTL_MOD, &#8230;) 来更新已有的 event watcher；然而 libevent2 继续调用 epoll_ctl(fd, EPOLL_CTL_ADD, &#8230;) 来重复添加 fd，并忽略返回的错误码 EEXIST (File exists)。在这种重复添加的情况下，EPOLL_CTL_ADD 将会快速地返回错误，而 EPOLL_CTL_MOD 会做更多的工作，花的时间也更长。于是 libevent2 捡了个便宜。</p>
<p>为了验证这个结论，我改动了 muduo，让它每次都用 EPOLL_CTL_ADD 方式初始化和更新 event watcher，并忽略返回的错误。</p>
<p>第二轮测试结果见上图的蓝线，可见改动之后的 muduo 的初始化性能比 libevent2 更好，事件处理的耗时也有所降低（我推测是 kernel 内部的原因）。</p>
<p>这个改动只是为了验证想法，我并没有把它放到 muduo 最终的代码中去，这或许可以留作日后优化的余地。（具体的改动是 muduo/net/poller/EPollPoller.cc 第 115 行和 144 行，读者可自行验证。）</p>
<p>同样的测试在双核笔记本电脑上运行了一次，结果如下：（我的笔记本的 CPU 主频是 2.4GHz，高于台式机的 1.86GHz，所以用时较少。）</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=muduo_libevent_bench_6400 border=0 alt=muduo_libevent_bench_6400 src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduolibevent2_4F9/muduo_libevent_bench_6400_3.png" width=1200 height=800> </p>
<p>结论：在事件处理效率方面，muduo 与 libevent2 总体比较接近，各擅胜场。在并发量特别大的情况下（大于 10k），muduo 略微占优。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>关于 muduo 的更多介绍请见《<a href="http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx">发布一个基于 Reactor 模式的 C++ 网络库</a>》。muduo 的项目网站是 <a href="http://code.google.com/p/muduo">http://code.google.com/p/muduo</a> ，上面有个 class diagram 可供参考。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/126130.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2010-09-08 01:15 <a href="http://www.cppblog.com/Solstice/archive/2010/09/08/muduo_vs_libevent_bench.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>muduo 与 libevent2 吞吐量对比</title><link>http://www.cppblog.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sun, 05 Sep 2010 11:14:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/125944.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/125944.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/125944.html</trackback:ping><description><![CDATA[<p>libevent 是一款非常好用的 C 语言网络库，它也采用 Reactor 模型，正好可以与 muduo 做一对比。</p>
<p>本文用 ping pong 测试来对比 muduo 和 libevent2 的吞吐量，测试结果表明 muduo 吞吐量平均比 libevent2 高 18% 以上，个别情况达到 70%。</p>
<h1>测试对象</h1>
<ul>
    <li>libevent 2.0.6-rc (<a title=http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz href="http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz">http://monkey.org/~provos/libevent-2.0.6-rc.tar.gz</a>)
    <li>muduo 0.1.1 (<a href="http://muduo.googlecode.com/files/muduo-0.1.1-alpha.tar.gz)">http://muduo.googlecode.com/files/muduo-0.1.1-alpha.tar.gz)</a> SHA1 Checksum: a446ea8a22915f439063d2bc52eb2dc4b9caf92d </li>
</ul>
<h1>测试环境与测试方法</h1>
<p>测试环境与前文《<a href="http://blog.csdn.net/Solstice/archive/2010/09/04/5863411.aspx">muduo 与 boost asio 吞吐量对比</a>》相同。</p>
<p>我自己编写了 libevent2 的 ping pong 测试代码，地址在 <a href="http://github.com/chenshuo/recipes/tree/master/pingpong/libevent/">http://github.com/chenshuo/recipes/tree/master/pingpong/libevent/</a> 。由于这个测试代码没有使用多线程，所以本次测试只对比单线程下的性能。</p>
<p>测试内容为：客户端与服务器运行在同一台机器，均为单线程，测试并发连接数为 1/10/100/1000/10000 时的吞吐量。</p>
<p>在同一台机器测试吞吐量的原因：</p>
<ul>
    <li>现在的 CPU 很快，即便是单线程单 TCP 连接也能把 Gigabit 以太网的带宽跑满。如果用两台机器，所有的吞吐量测试结果都将是 100 MiB/s，失去了对比的意义。（或许可以对比哪个库占的 CPU 少。）
    <li>在同一台机器上测试，可以在 CPU 资源相同的情况下，单纯对比网络库的效率。也就是说单线程下，服务端和客户端各占满 1 个 CPU，比较哪个库的吞吐量高。 </li>
</ul>
<h1>测试结果</h1>
<p>单线程吞吐量测试，数字越大越好：</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=muduo_libevent_16k border=0 alt=muduo_libevent_16k src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduolibevent2_10220/muduo_libevent_16k_3.png" width=679 height=515> </p>
<p>以上结果让人大跌眼镜，muduo 居然比 libevent 快 70%！跟踪 libevent2 的源代码发现，它每次最多从 socket 读取 4096 字节的数据 (证据在 buffer.c 的 evbuffer_read() 函数)，怪不得吞吐量比 muduo 小很多。因为在这一测试中，muduo 每次读取 16384 字节，系统调用的性价比较高。</p>
<p>buffer.c:#define EVBUFFER_MAX_READ&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4096</p>
<p>为了公平起见，我再测了一次，这回两个库都发送 4096 字节的消息。</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=muduo_libevent_4k border=0 alt=muduo_libevent_4k src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduolibevent2_10220/muduo_libevent_4k_3.png" width=679 height=512> </p>
<p>测试结果表明 muduo 吞吐量平均比 libevent2 高 18% 以上。</p>
<h1>讨论</h1>
<p>由于 libevent2 每次最多从网络读取 4096 字节，大大限制了它的吞吐量。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/125944.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2010-09-05 19:14 <a href="http://www.cppblog.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>muduo 与 boost asio 吞吐量对比</title><link>http://www.cppblog.com/Solstice/archive/2010/09/04/muduo_vs_asio.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sat, 04 Sep 2010 08:30:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2010/09/04/muduo_vs_asio.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/125879.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2010/09/04/muduo_vs_asio.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/125879.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/125879.html</trackback:ping><description><![CDATA[<p>muduo (<a href="http://code.google.com/p/muduo">http://code.google.com/p/muduo</a>) 是一个基于 Reactor 模式的 C++ 网络库，我在编写它的时候并没有以高并发高吞吐为主要目标，但出乎我的意料，ping pong 测试表明，muduo 吞吐量比 boost.asio 高 15% 以上。</p>
<h1>测试对象</h1>
<ul>
    <li>boost 1.40 中的 asio 1.4.3
    <li>asio 1.4.5 (<a href="http://think-async.com/Asio/Download">http://think-async.com/Asio/Download</a>)
    <li>muduo 0.1.1 (<a title=http://muduo.googlecode.com/files/muduo-0.1.0-alpha.tar.gz href="http://muduo.googlecode.com/files/muduo-0.1.1-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.1-alpha.tar.gz</a>) SHA1 Checksum: a446ea8a22915f439063d2bc52eb2dc4b9caf92d </li>
</ul>
<h1>测试环境</h1>
<p>硬件：DELL 490 工作站，双路 Intel quad core Xeon E5320 CPU，16G 内存</p>
<p>操作系统：Ubuntu Linux Server 10.04.1 LTS x86_64</p>
<p>编译器：g++ 4.4.3</p>
<h1>测试方法</h1>
<p>依据 asio 性能测试 <a href="http://think-async.com/Asio/LinuxPerformanceImprovements">http://think-async.com/Asio/LinuxPerformanceImprovements</a> 的办法，用 ping pong 协议来测试吞吐量。</p>
<p>简单地说，ping pong 协议是客户端和服务器都实现 echo 协议。当 TCP 连接建立时，客户端向服务器发送一些数据，服务器会 echo 回这些数据，然后客户端再 echo 回服务器。这些数据就会像乒乓球一样在客户端和服务器之间来回传送，直到有一方断开连接为止。这是用来测试吞吐量的常用办法。</p>
<p>asio 的测试代码取自 <a href="http://asio.cvs.sourceforge.net/viewvc/asio/asio/src/tests/performance/"><font color=#0066cc>http://asio.cvs.sourceforge.net/viewvc/asio/asio/src/tests/performance/</font></a> ，未作更改。</p>
<p>muduo 的测试代码在 0.1.1 软件包内，路径为 examples/pingpong/，代码如 <a href="http://gist.github.com/564985">http://gist.github.com/564985</a> 所示。</p>
<p>muduo 和 asio 的优化编译参数均为 -O2 -finline-limit=1000</p>
<p>$ BUILD_TYPE=release ./build.sh&nbsp; # 编译 muduo 的优化版本</p>
<p>我主要做了两项测试：</p>
<ul>
    <li>单线程测试，测试并发连接数为 1/10/100/1000/10000 时的吞吐量。
    <li>多线程测试，并发连接数为 100 或 1000，服务器和客户端的线程数同时设为 1/2/3/4。（由于我家里只有一台 8 核机器，而且服务器和客户端运行在同一台机器上，线程数大于 4 没有意义。） </li>
</ul>
<p>所有测试中，ping pong 消息的大小均为 16k bytes。测试用的 shell 脚本可从 <a title=http://gist.github.com/564985 href="http://gist.github.com/564985">http://gist.github.com/564985</a> 下载。</p>
<h1>测试结果</h1>
<p>单线程测试的结果，数字越大越好：</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=single_thread border=0 alt=single_thread src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduoboostasio_E6B8/image002_3.png" width=679 height=540> </p>
<p>多线程测试的结果，数字越大越好：</p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=multiple_thread_100conn border=0 alt=multiple_thread_100conn src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduoboostasio_E6B8/image005_3.png" width=678 height=565> </p>
<p><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=image007 border=0 alt=image007 src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/muduoboostasio_E6B8/image007_3.png" width=679 height=565> </p>
<p>测试结果表明 muduo 吞吐量平均比 asio 高 15% 以上。</p>
<h1>讨论</h1>
<p>muduo 出乎意料地比 asio 性能优越，我想主要得益于其简单的设计和简洁的代码。</p>
<p>asio 在多线程测试中表现不佳，我猜测其主要原因是测试代码只使用了一个 io_service，如果改用&#8220;io_service per CPU&#8221;的话，性能应该有所提高。我对 asio 的了解程度仅限于能读懂其代码，希望能有 asio 高手编写&#8220;io_service per CPU&#8221;的 ping pong 测试，以便与 muduo 做一个公平的比较。</p>
<p>ping pong 测试很容易实现，欢迎其他网络库（ACE、POCO、libevent 等）也能加入到对比中来，期待这些库的高手出马。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/125879.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2010-09-04 16:30 <a href="http://www.cppblog.com/Solstice/archive/2010/09/04/muduo_vs_asio.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>发布一个基于 Reactor 模式的 C++ 网络库</title><link>http://www.cppblog.com/Solstice/archive/2010/08/29/muduo_net_lib.html</link><dc:creator>陈硕</dc:creator><author>陈硕</author><pubDate>Sun, 29 Aug 2010 15:42:00 GMT</pubDate><guid>http://www.cppblog.com/Solstice/archive/2010/08/29/muduo_net_lib.html</guid><wfw:comment>http://www.cppblog.com/Solstice/comments/125234.html</wfw:comment><comments>http://www.cppblog.com/Solstice/archive/2010/08/29/muduo_net_lib.html#Feedback</comments><slash:comments>19</slash:comments><wfw:commentRss>http://www.cppblog.com/Solstice/comments/commentRss/125234.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/Solstice/services/trackbacks/125234.html</trackback:ping><description><![CDATA[<p>发布一个基于 Reactor 模式的 C++ 网络库</p>
<p>陈硕 (giantchen_AT_gmail)</p>
<p>Blog.csdn.net/Solstice</p>
<p>2010 Aug 30</p>
<p>本文主要介绍 muduo 网络库的使用。其设计与实现将有另文讲解。</p>
<p>目录</p>
<p><a href="#_Toc17667">由来 1</a></p>
<p><a href="#_Toc20754">下载与编译 2</a></p>
<p><a href="#_Toc2416">例子 2</a></p>
<p><a href="#_Toc32039">基本结构 3</a></p>
<p><a href="#_Toc29754">公开接口 4</a></p>
<p><a href="#_Toc24136">内部实现 4</a></p>
<p><a href="#_Toc8317">线程模型 5</a></p>
<p><a href="#_Toc20019">结语 5</a></p>
<h3><a name=_Toc17667></a>由来</h3>
<p>半年前我写了一篇《<a href="http://blog.csdn.net/Solstice/archive/2010/03/10/5364096.aspx">学之者生，用之者死——ACE历史与简评</a>》，其中提到&#8220;我心目中理想的网络库&#8221;的样子：</p>
<ul>
    <li>线程安全，支持多核多线程
    <li>不考虑可移植性，不跨平台，只支持 Linux，不支持 Windows。
    <li>在不增加复杂度的前提下可以支持 FreeBSD/Darwin，方便将来用 Mac 作为开发用机，但不为它做性能优化。也就是说 IO multiplexing 使用 poll 和 epoll。
    <li>主要支持 x86-64，兼顾 IA32
    <li>不支持 UDP，只支持 TCP
    <li>不支持 IPv6，只支持 IPv4
    <li>不考虑广域网应用，只考虑局域网
    <li>只支持一种使用模式：non-blocking IO + one event loop per thread，不考虑阻塞 IO
    <li>API 简单易用，只暴露具体类和标准库里的类，不使用 non-trivial templates，也不使用虚函数
    <li>只满足常用需求的 90%，不面面俱到，必要的时候以 app 来适应 lib
    <li>只做 library，不做成 framework
    <li>争取全部代码在 5000 行以内（不含测试）
    <li>以上条件都满足时，可以考虑搭配 Google Protocol Buffers RPC </li>
</ul>
<p>在想清楚这些目标之后，我开始第三次尝试编写自己的 C++ 网络库。与前两次不同，这次我一开始就想好了库的名字，叫 muduo （木铎），并在 Google code 上创建了项目： <a href="http://code.google.com/p/muduo/">http://code.google.com/p/muduo/</a> 。muduo 的主体内容在 5 月底已经基本完成，现在我把它开源。</p>
<p>本文主要介绍 muduo 网络库的使用，其设计与实现将有另文讲解。</p>
<h3><a name=_Toc20754></a>下载与编译</h3>
<p>下载地址： <a href="http://muduo.googlecode.com/files/muduo-0.1.0-alpha.tar.gz">http://muduo.googlecode.com/files/muduo-0.1.0-alpha.tar.gz</a></p>
<p>SHA1 Checksum: 5d3642e311177ded89ed0d15c10921738f8c984c</p>
<p>Muduo 使用了 <a href="http://blog.csdn.net/Solstice/archive/2010/02/26/5327881.aspx">Linux 较新的系统调用</a>，要求 Linux 的内核版本大于 2.6.28 （我自己用的是 2.6.32 ）。在 Debian Squeeze / Ubuntu 10.04 LTS 上编译测试通过，32 位和 64 位系统都能使用。</p>
<p>Muduo 采用 CMake 为 build system，安装方法：</p>
<p>$ sudo apt-get install cmake</p>
<p>Muduo 依赖 Boost，很容易安装：</p>
<p>$ sudo apt-get install libboost1.40-dev # 或 libboost1.42-dev</p>
<p>编译方法很简单：</p>
<p>$ tar zxf muduo-0.1.0-alpha.tar.gz</p>
<p>$ cd muduo/</p>
<p>$ ./build.sh</p>
<p># 编译生成的可执行文件和静态库文件分别位于 ../build/debug/{bin,lib}</p>
<p>如果要编译 release 版，可执行 </p>
<p>$ BUILD_TYPE=release ./build.sh</p>
<p># 编译生成的可执行文件和静态库文件分别位于 ../build/release/{bin,lib}</p>
<p>编译完成之后请试运行其中的例子。比如 bin/inspector_test ，然后通过浏览器访问 http://10.0.0.10:12345/ 或 http://10.0.0.10:12345/proc/status，其中 10.0.0.10 替换为你的 Linux box 的 IP。</p>
<h3><a name=_Toc2416></a>例子</h3>
<p>Muduo 附带了几十个小例子，位于 examples 目录。其中包括从 Boost.Asio、JBoss Netty、Python Twisted 等处移植过来的例子。</p>
<p>examples</p>
<p>|-- simple # 简单网络协议的实现</p>
<p>|&nbsp;&nbsp; |-- allinone&nbsp; # 在一个程序里同时实现下面 5 个协议</p>
<p>|&nbsp;&nbsp; |-- chargen&nbsp;&nbsp; # RFC 864，可测试带宽</p>
<p>|&nbsp;&nbsp; |-- daytime # RFC 867</p>
<p>|&nbsp;&nbsp; |-- discard # RFC 863</p>
<p>|&nbsp;&nbsp; |-- echo # RFC 862</p>
<p>|&nbsp;&nbsp; |-- time # RFC 868</p>
<p>|&nbsp;&nbsp; `-- timeclient # time 协议的客户端</p>
<p>|-- hub # 一个简单的 pub/sub/hub 服务，演示应用级的广播</p>
<p>|-- roundtrip # 测试两台机器的网络延时与时间差</p>
<p>|-- asio # 从 Boost.Asio 移植的例子</p>
<p>|&nbsp;&nbsp; |-- chat # 聊天服务</p>
<p>|&nbsp;&nbsp; `-- tutorial # 一系列 timers</p>
<p>|-- netty # 从 JBoss Netty 移植的例子</p>
<p>|&nbsp;&nbsp; |-- discard # 可用于测试带宽，服务器可多线程运行</p>
<p>|&nbsp;&nbsp; |-- echo # 可用于测试带宽，服务器可多线程运行</p>
<p>|&nbsp;&nbsp; `-- uptime # TCP 长连接</p>
<p>`-- twisted # 从 Python Twisted 移植的例子</p>
<p>&nbsp;&nbsp;&nbsp; `-- finger # finger01 ~ 07</p>
<h3><a name=_Toc32039></a>基本结构</h3>
<p>Muduo 的目录结构如下。</p>
<p>muduo</p>
<p>|-- base # 与网络无关的基础代码，已<a href="http://blog.csdn.net/Solstice/archive/2010/08/21/5829421.aspx">提前发布</a></p>
<p>`-- net # 网络库</p>
<p>&nbsp;&nbsp;&nbsp; |-- http # 一个简单的可嵌入的 web 服务器</p>
<p>&nbsp;&nbsp;&nbsp; |-- inspect # 基于以上 web 服务器的&#8220;窥探器&#8221;，用于报告进程的状态</p>
<p>&nbsp;&nbsp;&nbsp; `-- poller # poll(2) 和 epoll(4) 两种 IO multiplexing 后端</p>
<p>Muduo 是基于 Reactor 模式的网络库，其核心是个事件循环 EventLoop，用于响应计时器和 IO 事件。Muduo 采用基于对象（object based）而非面向对象（object oriented）的设计风格，其<a href="http://blog.csdn.net/Solstice/archive/2008/10/13/3066268.aspx">接口多以 boost::function + boost::bind 表达</a>。</p>
<p>Muduo 的头文件明确分为客户可见和客户不可见两类。客户可见的为白底，客户不可见的为灰底。</p>
<p><a href="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/ReactorC_14C67/inc_2.png"><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=inc border=0 alt=inc src="http://www.cppblog.com/images/cppblog_com/Solstice/WindowsLiveWriter/ReactorC_14C67/inc_thumb.png" width=909 height=613></a> </p>
<p>这里简单介绍各个头文件及 class 的作用，详细的介绍留给以后的博客。</p>
<h5><a name=_Toc29754></a>公开接口</h5>
<ul>
    <li>Buffer 仿 Netty ChannelBuffer 的 buffer class，数据的读写透过 buffer 进行
    <li>InetAddress 封装 IPv4 地址 (end point)，注意，muduo 目前不能解析域名，只认 IP
    <li>EventLoop 反应器 Reactor，用户可以注册计时器回调
    <li>EventLoopThread 启动一个线程，在其中运行 EventLoop::loop()
    <li>TcpConnection 整个网络库的核心，封装一次 TCP 连接
    <li>TcpClient 用于编写网络客户端，能发起连接，并且有重试功能
    <li>TcpServer 用于编写网络服务器，接受客户的连接
    <li>在这些类中，TcpConnection 的生命期依靠 shared_ptr 控制（即用户和库共同控制）。Buffer 的生命期由 TcpConnection 控制。其余类的生命期由用户控制。
    <li>HttpServer 和 Inspector，暴露出一个 http 界面，用于监控进程的状态，类似于 Java JMX。这么做的原因是，《程序员修炼之道》第 6 章第 34 条提到&#8220;对于更大、更复杂的服务器代码，提供其操作的内部试图的一种漂亮技术是使用内建的 Web 服务器&#8221;，Jeff Dean 也说&#8220;（每个 Google 的服务器进程）Export HTML-based status pages for easy diagnosis&#8221;。 </li>
</ul>
<h5><a name=_Toc24136></a>内部实现</h5>
<ul>
    <li>Channel 是 selectable IO channel，负责注册与响应 IO 事件，它不拥有 file descriptor。它是 Acceptor、Connector、EventLoop、TimerQueue、TcpConnection 的成员，生命期由后者控制。
    <li>Socket 封装一个 file descriptor，并在析构时关闭 fd。它是 Acceptor、TcpConnection 的成员，生命期由后者控制。EventLoop、TimerQueue 也拥有 fd，但是不封装为 Socket。
    <li>SocketsOps 封装各种 sockets 系统调用。
    <li>EventLoop 封装事件循环，也是事件分派的中心。它用 eventfd(2) 来异步唤醒，这有别于传统的用一对 pipe(2) 的办法。它用 TimerQueue 作为计时器管理，用 Poller 作为 IO Multiplexing。
    <li>Poller 是 PollPoller 和 EPollPoller 的基类，采用&#8220;电平触发&#8221;的语意。它是 EventLoop 的成员，生命期由后者控制。
    <li>PollPoller 和 EPollPoller 封装 poll(2) 和 epoll(4) 两种 IO Multiplexing 后端。Poll 的存在价值是便于调试，因为 poll(2) 调用是上下文无关的，用 strace 很容易知道库的行为是否正确。
    <li>Connector 用于发起 TCP 连接，它是 TcpClient 的成员，生命期由后者控制。
    <li>Acceptor 用于接受 TCP 连接，它是 TcpServer 的成员，生命期由后者控制。
    <li>TimerQueue 用 timerfd 实现定时，这有别于传统的设置 poll/epoll_wait 的等待时长的办法。为了简单起见，目前用链表来管理 Timer，如果有必要可改为优先队列，这样复杂度可从 O(n) 降为 O(ln n) （某些操作甚至是 O(1)）。它是 EventLoop 的成员，生命期由后者控制。
    <li>EventLoopThreadPool 用于创建 IO 线程池，也就是说把 TcpConnection 分派到一组运行 EventLoop 的线程上。它是 TcpServer 的成员，生命期由后者控制。 </li>
</ul>
<h3><a name=_Toc8317></a>线程模型</h3>
<p>Muduo 的线程模型符合我主张的 <a href="http://blog.csdn.net/Solstice/archive/2010/02/12/5307710.aspx">one loop per thread + thread pool</a> 模型。每个线程最多有一个 EventLoop。每个 TcpConnection 必须归某个 EventLoop 管理，所有的 IO 会转移到这个线程，换句话说一个 file descriptor 只能由一个线程读写。TcpConnection 所在的线程由其所属的 EventLoop 决定，这样我们可以很方便地把不同的 TCP 连接放到不同的线程去，也可以把一些 TCP 连接放到一个线程里。TcpConnection 和 EventLoop 是线程安全的，可以跨线程调用。TcpServer 直接支持多线程，它有两种模式：</p>
<p>1. 单线程，accept 与 TcpConnection 用同一个线程做 IO。</p>
<p>2. 多线程，accept 与 EventLoop 在同一个线程，另外创建一个 EventLoopThreadPool，新到的连接会按 round-robin 方式分配到线程池中。</p>
<h3><a name=_Toc20019></a>结语</h3>
<p>Muduo 是我对常见网络编程任务的总结，用它我能很容易地编写多线程的 TCP 服务器和客户端。Muduo 是我业余时间的作品，代码估计还有很多 bug，功能也不完善（例如不支持 signal 处理），待日后慢慢改进吧。</p>
<img src ="http://www.cppblog.com/Solstice/aggbug/125234.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/Solstice/" target="_blank">陈硕</a> 2010-08-29 23:42 <a href="http://www.cppblog.com/Solstice/archive/2010/08/29/muduo_net_lib.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>