网站设计的优化是一个很大的话题,有一些通用的原则,也有针对不同开发平台的一些建议。这方面的研究一直没有停止过,我在不同的场合也分享过这样的话题。
作为通用的原则,雅虎的工程师团队曾经给出过35个最佳实践。这个列表请参考
同时,他们还发布了一个相应的测试工具Yslow http://developer.yahoo.com/yslow/
我强烈推荐所有的网站开发人员都应该学习这些最佳实践,并结合自己的实际项目情况进行应用。
接下来的一段时间,我将结合ASP.NET这个开发平台,针对这些原则,通过一个系列文章的形式,做些讲解和演绎,以帮助大家更好地理解这些原则,并且更好地使用他们。
为了跟随我进行后续的学习,你需要准备如下的开发环境和工具
这一篇文章讨论的是第一个原则:应该尽可能加减少请求数。这个原则的说明请参考 http://developer.yahoo.com/performance/rules.html#num_http
我们的网页在加载的时候,为了提供更加丰富的内容和效果,除了页面本身这个请求之外,总是需要加载其他一些资源的,例如我们常见的Javascript文件,css文件,图片,甚至还会有一些Flash组件等等。这本无可厚非,但如果过多的外部请求,会很直接地降低页面加载的速度。
我们来看一个例子吧。例如我们经常访问的博客园的首页
这个网页的加载需要多少次请求呢?(如果不考虑缓存的话)
我们看到,请求数为55个。我们进一步通过Yslow来分析,可以得到综合的报表
应该说博客园的设计已经比较注意很多细节了。他们得到了B级的评分。我们再来看看其他一些主要的门户的表现吧(从左至右,依次是新浪,搜狐,腾讯,淘宝),他们都只得到D级的评分。
【备注】这些评分只是作为一个参考,不做任何的结论和推论。
我们可以通过如下的几个方法来减少请求数:
对于这两种文件的合并而言,我们当然可以手工去做(copy,paste),把一个文件的内容粘贴在另外一个文件内容的底部即可。但这种方式有几个缺点:
所以,我并不是很推荐用这种手工合并的方法,事实上,我们有更好的工具来实现, 并且在ASP.NET的一些框架(例如ASP.NET MVC)里面已经有了内置的实现。
我们先来看一个例子,下面是一个典型的ASP.NET MVC项目
我找了其中的一个用户注册页面,在IE中进行查看
我们看到默认情况下,完成这个页面其实要执行8个请求。但经过简单的处理(添加一行代码)之后,我们可以看到如下的效果
而且如果你细心看的话,在这个页面中请求的javascript文件似乎看起来经过了特殊的处理(路径比较特殊)。那么,这是如何实现的呢?
原来,在MVC项目中,默认会有一个所谓的BundleConfig的类,它提供了一个方法RegisterBundles,如下所示
using System.Web; using System.Web.Optimization; namespace MvcApplication1 { public class BundleConfig { // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725 public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include( "~/Scripts/jquery-ui-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.unobtrusive*", "~/Scripts/jquery.validate*")); // Use the development version of Modernizr to develop with and learn from. Then, when you're // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css")); bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/jquery.ui.core.css", "~/Content/themes/base/jquery.ui.resizable.css", "~/Content/themes/base/jquery.ui.selectable.css", "~/Content/themes/base/jquery.ui.accordion.css", "~/Content/themes/base/jquery.ui.autocomplete.css", "~/Content/themes/base/jquery.ui.button.css", "~/Content/themes/base/jquery.ui.dialog.css", "~/Content/themes/base/jquery.ui.slider.css", "~/Content/themes/base/jquery.ui.tabs.css", "~/Content/themes/base/jquery.ui.datepicker.css", "~/Content/themes/base/jquery.ui.progressbar.css", "~/Content/themes/base/jquery.ui.theme.css")); } } }
这个方法会在Global.asax文件中调用
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace MvcApplication1 { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); //BundleTable.EnableOptimizations = false;//启用这一行,则使用Bundle的机制进行文件打包 AuthConfig.RegisterAuth(); } } }
在具体的页面中,如果需要用到上述的脚本组合,则可以使用下面这样的语法来调用
@Scripts.Render("~/bundles/jqueryval")
这就是所有的秘密。
那么,这个技术是不是只能使用在MVC中,在我们另外一种开发框架(ASP.NET Web Forms)中是否也可以使用呢?
答案是:可以的。而且这个技术,确实是最早就是用在ASP.NET Web Forms里面,只不过,因为这方面的文档较少,所以可能大家用的不多而已。下面是我作为演示用的一个简单的ASP.NET Web Forms的项目:
我们看到,在页面中,我们添加了三个脚本引用,这样的话,自然在打开页面的时候,需要单独请求这三个脚本文件。
我们是否可以将这三个文件合并成一个请求呢?请跟随我来进行如下的操作
首先,添加一个组件,这是微软官方发布的System.Web.Optimization,顾名思义,这就是为了优化网络开发用的
按照一般的惯例,我们在项目中添加一个文件,来进行Bundle的注册
using System.Web.Optimization; namespace WebApplication1 { public class BundleConfig { public static void RegisterBundle(BundleCollection config) { config.Add(new ScriptBundle("~/default").Include("~/scripts/jquery-2.0.0.min.js", "~/scripts/knockout-2.2.1.js", "~/default.js")); } } }
然后,我们修改Global.asax文件,添加如下的代码
using System; using System.Web.Optimization; namespace WebApplication1 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { BundleConfig.RegisterBundle(BundleTable.Bundles); BundleTable.EnableOptimizations = true; } } }
最后,我们在页面上也做相应的修改,如下所示(请注意粗体部分)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1.Default" %> <%@ Import Namespace="System.Web.Optimization" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <%= Scripts.Render("~/default") %> </head> <body> <form id="form1" runat="server"> <div> </div> </form> </body> </html>
很不错,我们现在可以查看一下页面运行起来的效果
很明显,原先的三个请求,现在变成了一个请求。需要注意的是,如果我们去计算文件大小,这个合并之后的文件,体积会比之前三个文件体积总和略小一些。所以你可以理解为这里存在一定的压缩,但这个压缩比是不大的(尤其是原有的javascript文件本身就经过了压缩的情况下)。关于javascript文件或者css文件的压缩,后续会有专门的文章介绍。
上面的例子,演示的是javascript文件的合并。其实,css文件的合并也是类似的做法,区别在于要使用StyleBundle : http://msdn.microsoft.com/en-us/library/system.web.optimization.stylebundle.aspx
本文介绍了网站优化的第一个原则(尽量减少请求数),我带领大家分析了为什么需要考虑这个原则,以及具体如何实现(包括在MVC和Web Forms的做法)
本次版本升级要点:
下面请看截图说明:
1:运行的界面,通过点击“单个文件”或“指文件夹”选择相应的日志文件或路径,加载后,系统这里会自动保存路径,方便下次启动时还原路径,然后点击汇总统计,可以看到汇总,如下图:
2:切换到查看明细:点击一个日志文件,可以查看状态码,根据状态码分析是一项比较重要的手段,这里给出状态码的次数和说明,更详细的状态码介绍,可以点帮助菜单下的“状态码帮助”。
3:双击状态码统计的任意一行的单元格,会出来状态码明细,这里可以根据明细再细分各搜索引擎的情况,进一分分析情况。
4:在查看明细的“24小时”里,可以看到24小时段的访问情况,从这里可以看到搜索引擎的频繁,分析搜索引擎在哪个时段来的多一些,做一些优化。
5:同样的,双击“24小时”统计里行里的单元格,也会出来“24小时明细”,可以更精准的看到每个请求的链接。
6:本次版本新增加了“IP”的统计,通过这个统计,你可以看到搜索引擎的请求IP和访问次数和请求头等信息,同时,对于非搜索引擎,如果IP的访问数太高,可以分析是不是网站可能被攻击,或者被采集,应该做点事了。
7:还是一样,双击单元格,会出来明细,从明细时在,可以看到每个IP具体访问的页面和时间,如果一个IP访问同一个页面多次,初步可以分析为被攻击了,如果一个IP每个页面都访问1次,初步可以分析这个IP正在采集你的文章信息。
8:本版本做了Linux下的IIS日志的兼容处理,在默认情况下,会智能的根据某些特征分析对应的字段,如果你发现默认的智能字段对应不上,可以“勾Linux IIS 配置”,复制一行IIS日志到文本框里,然后点格式化,会出来对应的数字和值,然后修正右边的字段和对应数字,点保存即可。
秋式网站日志分析器[IISLogViewer] V3版本的介绍就这里了,若遇到问题或Bug,欢迎留言。
本次版本升级要点:
下面请看截图说明:
1:运行的界面,通过点击“单个文件”或“指文件夹”选择相应的日志文件或路径,加载后,系统这里会自动保存路径,方便下次启动时还原路径,然后点击汇总统计,可以看到汇总,如下图:
2:切换到查看明细:点击一个日志文件,可以查看状态码,根据状态码分析是一项比较重要的手段,这里给出状态码的次数和说明,更详细的状态码介绍,可以点帮助菜单下的“状态码帮助”。
3:双击状态码统计的任意一行的单元格,会出来状态码明细,这里可以根据明细再细分各搜索引擎的情况,进一分分析情况。
4:在查看明细的“24小时”里,可以看到24小时段的访问情况,从这里可以看到搜索引擎的频繁,分析搜索引擎在哪个时段来的多一些,做一些优化。
5:同样的,双击“24小时”统计里行里的单元格,也会出来“24小时明细”,可以更精准的看到每个请求的链接。
6:本次版本新增加了“IP”的统计,通过这个统计,你可以看到搜索引擎的请求IP和访问次数和请求头等信息,同时,对于非搜索引擎,如果IP的访问数太高,可以分析是不是网站可能被攻击,或者被采集,应该做点事了。
7:还是一样,双击单元格,会出来明细,从明细时在,可以看到每个IP具体访问的页面和时间,如果一个IP访问同一个页面多次,初步可以分析为被攻击了,如果一个IP每个页面都访问1次,初步可以分析这个IP正在采集你的文章信息。
8:本版本做了Linux下的IIS日志的兼容处理,在默认情况下,会智能的根据某些特征分析对应的字段,如果你发现默认的智能字段对应不上,可以“勾Linux IIS 配置”,复制一行IIS日志到文本框里,然后点格式化,会出来对应的数字和值,然后修正右边的字段和对应数字,点保存即可。
秋式网站日志分析器[IISLogViewer] V3版本的介绍就这里了,若遇到问题或Bug,欢迎留言。
本次版本升级要点:
下面请看截图说明:
1:运行的界面,通过点击“单个文件”或“指文件夹”选择相应的日志文件或路径,加载后,系统这里会自动保存路径,方便下次启动时还原路径,然后点击汇总统计,可以看到汇总,如下图:
2:切换到查看明细:点击一个日志文件,可以查看状态码,根据状态码分析是一项比较重要的手段,这里给出状态码的次数和说明,更详细的状态码介绍,可以点帮助菜单下的“状态码帮助”。
3:双击状态码统计的任意一行的单元格,会出来状态码明细,这里可以根据明细再细分各搜索引擎的情况,进一分分析情况。
4:在查看明细的“24小时”里,可以看到24小时段的访问情况,从这里可以看到搜索引擎的频繁,分析搜索引擎在哪个时段来的多一些,做一些优化。
5:同样的,双击“24小时”统计里行里的单元格,也会出来“24小时明细”,可以更精准的看到每个请求的链接。
6:本次版本新增加了“IP”的统计,通过这个统计,你可以看到搜索引擎的请求IP和访问次数和请求头等信息,同时,对于非搜索引擎,如果IP的访问数太高,可以分析是不是网站可能被攻击,或者被采集,应该做点事了。
7:还是一样,双击单元格,会出来明细,从明细时在,可以看到每个IP具体访问的页面和时间,如果一个IP访问同一个页面多次,初步可以分析为被攻击了,如果一个IP每个页面都访问1次,初步可以分析这个IP正在采集你的文章信息。
8:本版本做了Linux下的IIS日志的兼容处理,在默认情况下,会智能的根据某些特征分析对应的字段,如果你发现默认的智能字段对应不上,可以“勾Linux IIS 配置”,复制一行IIS日志到文本框里,然后点格式化,会出来对应的数字和值,然后修正右边的字段和对应数字,点保存即可。
秋式网站日志分析器[IISLogViewer] V3版本的介绍就这里了,若遇到问题或Bug,欢迎留言。
近段日子在做一个比较复杂的项目,其中用到了开源软件ZMQ和MessagePack。ZMQ对底层网络通信进行了封装,是一个消息处理队列库,使用起来非常方便。MessagePack是一个基于二进制的对象序列化类库,具有跨语言的特性,同样非常容易使用。在我做的项目中,消息类通过MessagePack进行压包,然后写入ZMQ的消息结构体,通过ZMQ传递,最后接收者利用MessagePack进行解包,从而分析命令。由于我英语水平实在不高,所以我并没有通过阅读它们的说明文档来对它们进行了解,而仅仅是通过它们的示例代码进行探索。虽然因此遇到了一些不解问题,但这种方式却为我节省了很多时间。不过,对于英语好的人,还是应该通过阅读说明文档来去了解它们。
为了说明如何使用它们,在这里构造一个使用场景:有N个Client,一个Server,M个Agent,Client使用ZMQ的请求-响应模式和Server通信,Server收到Client的命令后,通过ZMQ的发布-订阅模式与各个Agent进行通信。下面的代码封装并使用了ZMQ和MessagePack,为了简便,我把类的定义和实现都写在了头文件。
1.对ZMQ的简单封装:
1 #include"Msgpack.h" 2 #include<zmq.h> 3 #include<string> 4 #include<cassert> 5 #include<iostream> 6 7 namespace Tool 8 { 9 //网络工具类 10 class Network 11 { 12 public: 13 14 // 功能 :构造函数。 15 // 参数 :无。 16 // 返回 :无。 17 Network() : m_socket(NULL) { } 18 19 // 功能 :初始化socket。 20 // 参数 :zmqType表示ZMQ的模式,address表示socket绑定或连接地址。 21 // 返回 :true表示初始化成功,false表示失败。 22 bool Init(int zmqType,const std::string& address) 23 { 24 try 25 { 26 m_socket = zmq_socket(Context,zmqType); 27 return SetSocket(zmqType,address); 28 } 29 catch(...) 30 { 31 std::cout << "Network初始化失败。" << std::endl; 32 return false; 33 } 34 } 35 36 // 功能 :发送消息。 37 // 参数 :指向Msgpack的指针,isRelease如果为true表示发送消息后即刻释放资源。 38 // 返回 :true表示发送成功,false表示发送失败。 39 bool SendMessage(Msgpack *msgpack,bool isRelease = true) const 40 { 41 try 42 { 43 zmq_msg_t msg; 44 zmq_msg_init(&msg); 45 if(isRelease) 46 { 47 zmq_msg_init_data(&msg,msgpack->GetSbuf().data(),msgpack->GetSbuf().size(),Tool::Network::Release,msgpack); 48 } 49 else 50 { 51 zmq_msg_init_data(&msg,msgpack->GetSbuf().data(),msgpack->GetSbuf().size(),0,0); 52 } 53 zmq_msg_send(&msg,m_socket,0); 54 return true; 55 } 56 catch(...) 57 { 58 std::cout << "Network发送失败。" << std::endl; 59 return false; 60 } 61 } 62 63 // 功能 :接收消息。 64 // 参数 :无。 65 // 返回 :指向消息的指针。 66 zmq_msg_t* ReceiveMessage() const 67 { 68 zmq_msg_t *reply = NULL; 69 try 70 { 71 reply = new zmq_msg_t(); 72 zmq_msg_init(reply); 73 zmq_msg_recv(reply,m_socket,0); 74 return reply; 75 } 76 catch(...) 77 { 78 if( reply != NULL ) 79 { 80 delete reply; 81 } 82 return NULL; 83 } 84 } 85 86 // 功能 :关闭消息。 87 // 参数 :指向消息的指针。 88 // 返回 :无。 89 void CloseMsg(zmq_msg_t* msg) 90 { 91 try 92 { 93 zmq_msg_close(msg); 94 msg = NULL; 95 } 96 catch(...) 97 { 98 msg = NULL; 99 } 100 } 101 102 // 功能 :析构函数。 103 // 参数 :无。 104 // 返回 :无。 105 ~Network() 106 { 107 if( m_socket != NULL ) 108 { 109 zmq_close(m_socket); 110 m_socket = NULL; 111 } 112 } 113 114 private: 115 116 //通信socket 117 void *m_socket; 118 119 //网络环境 120 static void *Context; 121 122 private: 123 124 // 功能 :设置socket。 125 // 参数 :zmqType表示ZMQ的模式,address表示socket绑定或连接地址。 126 // 返回 :true表示设置成功,false表示设置失败。 127 bool SetSocket(int zmqType,const std::string& address) 128 { 129 int result = -1; 130 switch(zmqType) 131 { 132 case ZMQ_REP: 133 case ZMQ_PUB: 134 result = zmq_bind(m_socket,address.c_str()); 135 break; 136 case ZMQ_REQ: 137 result = zmq_connect(m_socket,address.c_str()); 138 break; 139 case ZMQ_SUB: 140 result = zmq_connect(m_socket,address.c_str()); 141 assert(result == 0); 142 result = zmq_setsockopt(m_socket,ZMQ_SUBSCRIBE,"",0); 143 break; 144 default: 145 return false; 146 } 147 assert( result == 0 ); 148 return true; 149 } 150 151 // 功能 :发送完消息后,释放消息资源。 152 // 参数 :function为函数地址,hint指向要释放资源的对象。 153 // 返回 :无。 154 static void Release(void *function, void *hint) 155 { 156 Msgpack *msgpack = (Msgpack*)hint; 157 if( msgpack != NULL ) 158 { 159 delete msgpack; 160 msgpack = NULL; 161 } 162 } 163 }; 164 165 //整个程序共用一个context 166 void *Tool::Network::Context = zmq_ctx_new(); 167 };
说明:
(1)由zmq_ctx_new创建出来的Context,整个应用程序共用一个就可以了,具体的通信是由zmq_socket创建的socket来完成的。上述代码中没有去释放Context指向的资源。
(2)在zmq_msg_init_data函数的参数中,需要传入一个释放资源的函数地址,在ZMQ发送完消息后就调用这个函数来释放资源。如果没有传入这个参数,而且传入的信息是临时变量,那么接收方很有可能接收不到信息,甚至抛出异常。如果不传入这个参数,那么就要记得由自己去释放资源了。
2.对MessagePack的简单封装:
1 #include"BaseMessage.h" 2 #include"ClientMessage.h" 3 #include"ServerMessage.h" 4 #include<zmq.h> 5 #include<msgpack.hpp> 6 7 namespace Tool 8 { 9 using namespace Message; 10 11 //压包/解包工具类 12 class Msgpack 13 { 14 public: 15 16 // 功能 :构造函数。 17 // 参数 :无。 18 // 返回 :无。 19 Msgpack(void) { } 20 21 // 功能 :析构函数。 22 // 参数 :无。 23 // 返回 :无。 24 ~Msgpack(void) { } 25 26 // 功能 :压包数据。 27 // 参数 :要压包的数据。 28 // 返回 :true表示压包成功。 29 template<typename T> 30 bool Pack(const T& t) 31 { 32 try 33 { 34 Release(); 35 msgpack::pack(m_sbuf,t); 36 return true; 37 } 38 catch(...) 39 { 40 std::cout << "Msgpack压包数据失败。" << std::endl; 41 return false; 42 } 43 } 44 45 // 功能 :解包数据。 46 // 参数 :zmq消息体。 47 // 返回 :返回指向基类消息的指针。 48 BaseMessage* Unpack(zmq_msg_t& msg) 49 { 50 try 51 { 52 int size = zmq_msg_size(&msg); 53 if( size > 0 ) 54 { 55 Release(); 56 m_sbuf.write((char*)zmq_msg_data(&msg),size); 57 size_t offset = 0; 58 msgpack::zone z; 59 msgpack::object obj; 60 msgpack::unpack(m_sbuf.data(),m_sbuf.size(),&offset,&z,&obj); 61 return GetMessage(obj); 62 } 63 } 64 catch(...) 65 { 66 //吃掉异常 67 } 68 return NULL; 69 } 70 71 // 功能 :获取压包/解包工具。 72 // 参数 :无。 73 // 返回 :压包/解包工具。 74 inline msgpack::sbuffer& GetSbuf() 75 { 76 return m_sbuf; 77 } 78 79 private: 80 81 //压包/解包工具 82 msgpack::sbuffer m_sbuf; 83 84 private: 85 86 // 功能 :释放上一次的数据资源。 87 // 参数 :无。 88 // 返回 :无。 89 void Release() 90 { 91 m_sbuf.clear(); 92 m_sbuf.release(); 93 } 94 95 // 功能 :获取消息。 96 // 参数 :用于转换的msgpack::object。 97 // 返回 :指向消息基类的指针。 98 BaseMessage* GetMessage(const msgpack::object& obj) 99 { 100 BaseMessage bmessage; 101 obj.convert(&bmessage); 102 switch(bmessage.Type) 103 { 104 case 1024: 105 return Convert<ClientMessage>(obj); 106 case 2048: 107 return Convert<ServerMessage>(obj); 108 default: 109 return NULL; 110 } 111 } 112 113 // 功能 :将压包后的数据转换为具体的类。 114 // 参数 :用于转换的msgpack::object。 115 // 返回 :指向T的指针。 116 template<typename T> 117 T* Convert(const msgpack::object& obj) 118 { 119 T *t = new T(); 120 obj.convert(t); 121 return t; 122 } 123 }; 124 };
说明:
压包时将zmq_msg_t消息体压包到msgpack::sbuffer,然后就可以关闭这个消息体了。要将解包后的数据转换成具体的某一个类,需要知道这个类是什么类,这里有三种方法:
(1)可以先发送一个消息告知接收者即将收到什么消息,然后接收者将消息解包后转换成对应的类。这种方式需要额外的一次通信,不建议使用。
(2)所有的消息都继承自一个基类,这个基类存储有消息类型的字段。解包后,先将数据转换为基类,然后根据类型再转换为具体的派生类。这种方式需要多转换一次,上面的代码也正是采用这种方式。
(3)压包时先压包一个消息类,然后再压包一个标识这个消息是什么类型的标识类,即压包两次。解包时,先解包标识类,得知消息类的具体类型,然后再解包消息类,即解包两次,转换两次。与(2)相比,除了要做更多的压包、解包工作外,这里还需要对解包的偏移量进行计算,否则容易出错。
3.使用到的消息类:
namespace Message { //消息基类 class BaseMessage { public: MSGPACK_DEFINE(Type); //消息类型 int Type; //默认构造函数 BaseMessage() { Type = 0; } }; //来自客户端的消息 class ClientMessage : public BaseMessage { public: MSGPACK_DEFINE(Type,Information); //信息 std::string Information; //默认构造函数 ClientMessage() { Type = 1024; } }; //来自服务端的消息 class ServerMessage : public BaseMessage { public: MSGPACK_DEFINE(Type,Information); //信息 std::vector<std::string> Information; //默认构造函数 ServerMessage() { Type = 2048; } }; };
说明:
(1)MSPACK_DEFINE标识了一个类的哪些成员可以进行压包/解包。派生类中的MSGPACK_DEFINE还需要写上基类的成员,否则无法使用对MessagePack封装说明的第二个方法。
(2)C++版本的MessagePack压/解包的数据成员,只能是一个类、结构或者联合体,不能使用指针(包括boost库的智能指针)、数组,枚举值也不适用。因此,BaseMessage使用int值来标识派生类属于哪个类型。C#版本的MessagePack可以对枚举值进行压包。
4.Client的示例代码:
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 Network network; 4 bool result = network.Init(ZMQ_REQ,"tcp://192.168.10.179:8888"); 5 if(result) 6 { 7 ClientMessage cmessage; 8 cmessage.Information = "I come form Client."; 9 10 Msgpack msgpack; 11 result = msgpack.Pack<ClientMessage>(cmessage); 12 if(result) 13 { 14 result = network.SendMessageW(&msgpack,false); 15 if(result) 16 { 17 zmq_msg_t *msg = network.ReceiveMessage(); 18 if( msg != NULL ) 19 { 20 BaseMessage *bmessage = msgpack.Unpack(*msg); 21 network.CloseMsg(msg); 22 if( bmessage != NULL && bmessage->Type == 2048 ) 23 { 24 ServerMessage *smessage = static_cast<ServerMessage*>(bmessage); 25 if( smessage != NULL && smessage->Information.size() > 0 ) 26 { 27 std::cout << smessage->Information[0] << std::endl; 28 } 29 delete smessage; 30 smessage = NULL; 31 bmessage = NULL; 32 } 33 } 34 } 35 } 36 } 37 38 system("pause"); 39 return 0; 40 }
5.Server的示例代码:
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 Network responder; 4 bool result = responder.Init(ZMQ_REP,"tcp://192.168.10.179:8888"); 5 if(result) 6 { 7 Network publisher; 8 result = publisher.Init(ZMQ_PUB,"tcp://192.168.10.179:9999"); 9 if(result) 10 { 11 Msgpack msgpack; 12 while(true) 13 { 14 zmq_msg_t *msg = responder.ReceiveMessage(); 15 BaseMessage *bmessage = msgpack.Unpack(*msg); 16 responder.CloseMsg(msg); 17 18 ServerMessage smessage; 19 smessage.Information.push_back("I come from Server."); 20 msgpack.Pack<ServerMessage>(smessage); 21 result = responder.SendMessageW(&msgpack,false); 22 23 if( result ) 24 { 25 if( bmessage != NULL && bmessage->Type == 1024 ) 26 { 27 ClientMessage *cmessage = static_cast<ClientMessage*>(bmessage); 28 if( cmessage != NULL ) 29 { 30 std::cout << cmessage->Information << std::endl; 31 for( int counter = 0 ; counter < 100 ; counter++ ) 32 { 33 publisher.SendMessageW(&msgpack,false); 34 } 35 } 36 delete cmessage; 37 cmessage = NULL; 38 bmessage = NULL; 39 } 40 } 41 } 42 } 43 } 44 45 return 0; 46 }
6.Agent的示例代码:
int _tmain(int argc, _TCHAR* argv[]) { Network network; bool result = network.Init(ZMQ_SUB,"tcp://192.168.10.179:9999"); if(result) { zmq_msg_t *msg = network.ReceiveMessage(); if( msg != NULL ) { Msgpack msgpack; BaseMessage *bmessage = msgpack.Unpack(*msg); network.CloseMsg(msg); if( bmessage->Type == 2048 ) { ServerMessage *smessage = static_cast<ServerMessage*>(bmessage); if( smessage->Information.size() > 0 ) { std::cout << smessage->Information[0] << std::endl; } delete smessage; smessage = NULL; bmessage = NULL; } } } system("pause"); return 0; }
7.启动这三个程序,Client将要发送的消息压包后发给Server,Server接收到消息后反馈一个信息给Client,然后循环发布消息给Agent,Agent不需要回复Server。最后着重说明两点:
(1)ZMQ创建的socket发送数据和接收数据要处在同一条线程。Server接收到Client的数据后,不能通过开一条线程来给Client反馈信息,必须要在接收数据的线程中反馈信息。
(2)ZMQ并不要求发送者和接收者有一定的启动顺序,但在Server中如果只发布一次消息,那么Agent很有可能收不到信息。不管是Agent先启动,还是Server先启动,Agent都有可能收不到信息。在Server的代码中,通过循环发布一百次,来让Agent收到信息。至于实际应用中,可以结合请求-响应模式来保证订阅消息者都收到了发布者的消息。
参考资料:
ZMQ:http://zguide.zeromq.org/page:all
MessagePack:http://wiki.msgpack.org/pages/viewpage.action?pageId=1081387#QuickStartforC%2B%2B-ImplementationStatus