wenmo8 发布的文章

Winform混合式开发框架,是一种支持分布式部署的应用模式,支持直接连接数据库,访问远程WCF服务,访问远程Web API服务等服务的综合性框架,根据不同的需求采用不同的数据接口,是一个适应性很广的应用框架。  混合式开发框架它本身是一个完整的业务系统外,它外围的所有辅助性模块(如通用权限、通用字典、通用附件管理、通用人员管理、通讯录管理......)均都实现了这种混合型的框架,因此使用非常方便。

1、多种数据接入方式

《混合式开发框架》混合了传统《Winform开发框架》、《WCF开发框架》和Web API接口框架的特点,可以在直接访问数据库、利用WCF服务获取数据、利用Web API服务获取数据三者之间自由切换,统一了系统界面层对业务服务的调用模式,所有组件模块均实现三种方式的调用,是一种弹性化非常好的框架应用,既可用于单机版软件或者基于局域网内的应用软件,也可以用于分布式技术的互联网环境应用,是一种成熟稳定、安全高效的技术框架。

由于混合型框架,既可以用于传统Winform系统开发,也可以用于WCF分布式系统开发,还可以用于轻型高效的Web API的分布式系统开发,因此环境适应性强;而且由于模块具有这些特点,可重用性更高,特别对于通用性的模块,更是具有无可替代的优越性。

2、
独立配置,更少的代码修改

混合框架所有通用模块,如果是通过WCF或者Web API服务接入的,那么客户端模块都需要配置好对应的访问地址。

WCF服务连接,通过独立配置文件进行配置WCF的连接,减少主配置文件的复杂性;

Web API每个服务的连接地址也是可以通过配置文件进行指定,根据需要采用HTTP或者HTTPS协议进行数据传输。

这些接入的配置,都是独立放在在不同的文件里面,这样可以方便集中修改,也方便通过程序界面进行配置调整。如在混合框架里面,提供了一个管理界面,用来管理这些接口的切换和配置参数的。

其中相关的接口地址参数,可以在管理界面里面统一进行维护修改。

3、提供多种主体界面布局方式

混合框架提供多种模块调用方式,可以通过预先配置好的菜单,以插件方式动态构建对应的菜单按钮,并触发调用对应的插件模块进行展示;也可以按常规的添加菜单按钮方式进行功能按钮布局。

混合框架主体界面同时也提供多种界面布局方式,可以按标准的顶栏菜单模式展示,如下所示。

也可以在左边放置类似OutLookBar的功能条,在放置对应的菜单功能树,这样可以展示更多的功能模块,适用于系统功能较为复杂的情况使用。

4、代码生成工具的集成

整个框架的应用开发,代码生成工具Database2Sharp是灵魂,它围绕着不同的框架,根据设计好的数据库信息,生成主体框架信息,把不同类型的类文件放在不同的项目中,实现快速的框架增量开发;另外整个框架的Winform界面和Web界面,都可以快速生成,稍微调整下即可实现专业性界面的设计开发工作,并能够迅速编译运行起来,从而实现快速、高效、统一的框架应用开发。

1)主体框架代码的生成

EnterpriseLibrary代码生成时一个整体性项目代码的生成操作,它能根据设计好数据库信息以及模板文件,生成一个完整性非常高的项目。一般结合我们的
Winform开发框架

WCF开发框架、混合型开发框架
或者Web开发框架,进行增量式的项目开发,效率更高,而且可以可以利用更多已经开发好的、现成的组件模块的集成,完美的整合,以及模块化的封装,能带给你无穷的开发乐趣同时,使得项目无论从代码风格、用户界面、设计理念,都能保持很好的统一,快速优雅的完成碰到的项目。

代码生成工具,生成整体性的混合型框架项目如下所示,只是没有下图的界面部分,这部分在实际开发过程中,结合我的混合型框架案例进行整合即可,另外也可以界使用Database2Sharp进行Winform界面的开发,这样整体性就非常方便操作了:

2)界面代码的生成

界面开发,无论对于Web开发,还是Winform开发,都需要耗费一定的时间,特别对于一个数据库字段比较多的界面,一般就需要在编辑界面上摆的更多的控件来做数据显示,每次碰到这个,都有点头痛,反复的机械操作让人挺累,也很烦,但是又必须这样做。

由于数据库字段和界面的排版都有一定的关联关系,因此可以通过代码生成工具Database2Sharp的数据库元数据,包含表名称、备注信息、字段列表,以及每个字段的名称、备注、类型等信息,构造一个基础的界面,把重复机械的部分给快速完成,这就是我所说的界面快速生成。当然,对于精致的界面,机械的生成肯定不能满足我们的需要,因此真正的界面需要在这个基础上修改完善一下,但是由于重复劳动部分,已经给工具处理掉了,因此,界面开发效率会大大提高。

Winform界面可以生成标准的列表展示界面,也可以生成主从表界面,基本上适应了大多数的情况。

在我前面很多关于Visio的开发过程中,介绍了各种Visio的C#开发应用场景,包括对Visio的文档、模具文档、形状、属性数据、各种事件等相关的基础处理,以及Visio本身的整体项目应用,虽然时间过去很久,不过这些技术依旧还在使用中,最近应客户培训的需要,我对所有的内容进行了重新整理,把一些没有介绍的很详细或者很少的内容进行了丰富,因此本文介绍的主题-Visio二次开发之文件导出及另存Web页面,介绍一下Visio文件另存为其他几种格式的处理,以及另存为Web文件等相关操作。

1、Visio导出为PDF格式

在一般情况下,PDF格式是较为常用的内容格式,因此Visio文档(Vsd格式)导出为PDF也是很常见的一件事情,Office文档本身很好支持PDF格式的输出,因此对于Visio来说,也不是什么难事,基本上利用它现有的API就可以导出为PDF格式了。

在Visio的Document文档对象中,就有ExportAsFixedFormat这个方法,可以导出为PDF或者XPS的格式的,这个格式有很多参数,用来确定导出那页,以及格式等设置。

expression
.ExportAsFixedFormat(
FixedFormat
,
OutputFileName
,
Intent
,
PrintRange
,
FromPage
,
ToPage
,
ColorAsBlack
,
IncludeBackground
,
IncludeDocumentProperties
,
IncludeStructureTags
,
UseISO19005_1
,
FixedFormatExtClass
)

同时,这些参数的相关说明如下所示。

Name Required/Optional Data Type Description
FixedFormat Required VisFixedFormatTypes The format type in which to export the document. See Remarks for possible values.
OutputFileName Optional String The name and path of the file to which to output, enclosed in quotation marks.
Intent Required VisDocExIntent The output quality. See Remarks for possible values.
PrintRange Required VisPrintOutRange The range of document pages to be exported. See Remarks for possible values.
FromPage Optional Long If
PrintRange
is
visPrintFromTo
, the first page in the range to be exported. The default is 1, which indicates the first page of the drawing.
ToPage Optional Long If
PrintRange
is
visPrintFromTo
, the last page in the range to be exported. The default is -1, which indicates the last page of the drawing.
ColorAsBlack Optional Boolean True
to render all colors as black to ensure that all shapes are visible in the exported drawing.
False
to render colors normally. The default is
False
.
IncludeBackground Optional Boolean Whether to include background pages in the exported file. The default is
True
.
IncludeDocumentProperties Optional Boolean Whether to include document properties in the exported file. The default is
True
.
IncludeStructureTags Optional Boolean Whether to include document structure tags to improve document accessibility. The default is
True
.
UseISO19005_1 Optional Boolean Whether the resulting document is compliant with ISO 19005-1 (PDF/A). The default is
False
.
FixedFormatExtClass Optional [UNKNOWN] A pointer to a class that implements the
IMsoDocExporter
interface for purposes of creating custom fixed output. The default is a null pointer.

我们在代码里面导出PDF如下所示。

            SaveFileDialog dlg = newSaveFileDialog();
dlg.FileName
= "";
dlg.Filter
= "Pdf文件 (*.pdf)|*.pdf|AutoCAD 绘图 (*.dwg)|*.dwg|所有文件(*.*)|*.*";
dlg.FilterIndex
= 1;if (dlg.ShowDialog() ==DialogResult.OK)
{
if (dlg.FileName.Trim() != string.Empty)
{
VisDocument.ExportAsFixedFormat(Visio.VisFixedFormatTypes.visFixedFormatPDF,
dlg.FileName,
Visio.VisDocExIntent.visDocExIntentScreen,
Visio.VisPrintOutRange.visPrintAll,
1, VisDocument.Pages.Count, false, true, true, true, true,
System.Reflection.Missing.Value);
}
}

这样,我们通过指定PDF格式,以及导出文件名,以及起止页码等信息后,就可以顺利导出对应的Visio文档了,这种方式导出的Visio文档,效果非常好,可以放大到最大清晰都很好的。

2、Visio另存为CAD格式

Visio和CAD之间是比较好的兼容模式的,Visio和CAD本身都是基于矢量图形的绘制,因此转换为CAD在继续进行编辑也是很常见的事情,因此在较早时期,Visio本身就对CAD格式(dwg格式)就提供了很好的支持,它可以通过下面代码进行CAD格式的导出。

            SaveFileDialog dlg = newSaveFileDialog();
dlg.FileName
= "";
dlg.Filter
= "AutoCAD 绘图 (*.dwg)|*.dwg|所有文件(*.*)|*.*";
dlg.FilterIndex
= 1;if (dlg.ShowDialog() ==DialogResult.OK)
{
if (dlg.FileName.Trim() != string.Empty)
{
VisApplication.ActivePage.Export(dlg.FileName);
}
}

如果CAD文件顺利导出,那么会有一个日志文件提示用户操作的结果的,如下所示。

Visio还可以导出为JPG格式,这个和CAD操作类似,都是通过Page对象的Export方法进行导出,操作代码如下所示。

            SaveFileDialog dlg = newSaveFileDialog();
dlg.FileName
= "";
dlg.Filter
= "JPEG文件 (*.jpg)|*.jpg|所有文件(*.*)|*.*";
dlg.FilterIndex
= 1;if (dlg.ShowDialog() ==DialogResult.OK)
{
if (dlg.FileName.Trim() != string.Empty)
{
VisApplication.ActivePage.Export(dlg.FileName);
}
}

虽然这个导出的JPG格式,也是比较不错的,不过相对PDF的矢量效果来说,JPG放大的话,一般来说没有PDF格式那么清晰,但总体效果也还是可以。

3、Visio文档另存Web页面

对于Visio文档的另存为Web页面的操作,就没有上述几个方法那么简单了,一般需要更加复杂一点的处理方式。

虽然对于Visio文档来说,在IE上可以通过ActiveX的Visio Viewer来进行查看,不过其他浏览器都不支持,因此对于另存为Web页面的文件,这种方式显得比较通用一些,可以在各个浏览器上查看HTML页面,里面就包含了对Visio文件的显示了。

Visio的文档另存为Web页面的操作,主要思路是利用Application对象的SaveAsWebObject属性,并通过VisWebPageSettings对象进行一些导出属性的设置,如页面范围,文档分辨率等属性设置,以及是否在完成后使用浏览器打开文件等设置。

如获得对象的操作如下所示。

                //获取文档的Application对象
                targetApplication =targetDocument.Application;//获取并转换SaveAsWebObject对象
                saveAsWebAddon =(VisSaveAsWeb)targetApplication.SaveAsWebObject;//获取保存Web页面的参数设置对象
                saveAsWebSetting = (VisWebPageSettings)saveAsWebAddon.WebPageSettings;

通过获得页面参数对象,我们可以设定导出的起始页面,如下所示。

                    saveAsWebSetting.StartPage =startPage;
saveAsWebSetting.EndPage
= endPage;

然后在绑定到具体导出的文档里面就确定对应
导出

文档了。

                //使用AttachToVisioDoc指定那个文档作为保存页面的对象
                saveAsWebAddon.AttachToVisioDoc(targetDocument);    

为了提高导出Web页面的Visio清晰度,我们需要设置文档的显示比例,如下所示为使用源格式大小。

                //设置其中的相关参数
                saveAsWebSetting.DispScreenRes = VISWEB_DISP_RES.resSource;//显示比例

这个VISWEB_DISP_RES里面有很多参数可以设置的。

Constant Value Description

resSource

0

Use resolution of the source image for output.

res180x260

1

180 x 260 pixels

res544x376

2

544 x 376 pixels

res640x480

3

640 x 480 pixels

res720x512

4

720 x 512 pixels

res768x1024

5

768 x 1024 pixels

res800x600

6

800 x 600 pixels

res1024x768

7

1024 x 768 pixels

res1152x882

8

1152 x 882 pixels

res1152x900

9

1152 x 900 pixels

res1280x1024

10

1280 x 1024 pixels

res1600x1200

11

1600 x 1200 pixels

res1800x1440

12

1800 x 1440 pixels

res1920x1200

13

1920 x 1200 pixels

resINVALID

14

Reserved.

另外还有一个参数确定是批处理方式(静默方式)还是完成后通过浏览器打开文件的方式,如下所示。

                //判断是否为批处理模式
                if ((flags & RunInBatchMode) != 0)
{
//如果为批处理模式,那么浏览器窗口不会自动打开 saveAsWebSetting.OpenBrowser = 0;
saveAsWebSetting.SilentMode
= 1;
}
else{//否则保存完毕后打开对应给的浏览器显示文件 saveAsWebSetting.OpenBrowser = 1;
saveAsWebSetting.QuietMode
= 1;
}

如果一切顺利,那么通过方法直接创建页面就可以了,如下所示。

saveAsWebAddon.CreatePages();//创建页面

以上的方法处理,我们一般封装在一个类里面,方便调用处理,那么在界面上,我们处理的方法就可以简单化一些。

            var fileName = System.IO.Path.Combine(System.Environment.CurrentDirectory, "test.html");var success = SaveAsWebApi.SaveDocAsWebPage(this.axDrawingControl1.Document, -1, -1, fileName,
SaveAsWebApi.ShowPropertiesWindow
| SaveAsWebApi.ShowNavigationBar |SaveAsWebApi.ShowSearchTool|SaveAsWebApi.ShowPanAndZoom);

MessageBox.Show(success
? "成功生成Web文件" : "生成Web文件操作失败");

最后,我们就可以在各个浏览器里面查看相关的Visio文件了,这种方式比Visio Viewer的处理更通用,效果也很不错哦。

虽然在APP应用、Web应用、Winform应用等大趋势下,越来越多的企业趋向于这些应用系统开发,但是Socket的应用在某些场合是很必要的,如一些停车场终端设备的接入,农业或者水利、压力监测方面的设备数据采集等,以及常见的IM(即时通讯,如腾讯QQ、阿里旺旺等)的客户端,都可以采用Socket框架进行相关的数据采集和信息通讯用途的,Socket应用可以做为APP应用、Web应用和Winform应用的补充。

1、Socket应用场景

一般情况下,客户端和服务端进行Socket连接,需要进行数据的交换,也就是后台提供数据查询或者写入的相关操作,它们的应用场景也是在后台有一个应用数据库支持的,如下所示。

Socket服务器和客户端的通讯原理如下所示,客户端通过服务器地址和端口发起Socket连接,服务器在接收到Socket客户端的请求后,开辟一个新的Socket连接进行通讯管理,两方基于Socket协议进行数据的交互处理。

2、Socket框架设计思路

Socket开发是属于通信底层的开发,.NET本身也提供了非常丰富的类来实现Socket的开发工作,Socket框架应针对这些基础功能进行了很好的封装处理,已达到统一、高效的使用。

要掌握或者了解Socket开发,必须了解下面所述的场景及知识。

  • TCP客户端,连接服务器端,进行数据通信
  • TCP服务器端,负责侦听客户端连接
  • 连接客户端的管理,如登陆,注销等,使用独立线程处理
  • 数据接收管理,负责数据的接受,并处理队列的分发,使用独立线程处理,简单处理后叫给
    “数据处理线程
  • 数据处理线程,对特定的数据,采用独立的线程进行数据处理
  • 数据的封包和解包,按照一定的协议进行数据的封装和解包

针对以上内容,可以封装以下功能的操作类作为共用基类:

  • BaseSocketClient,客户端基类,负责客户端的链接、断开、发送、接收等操作。
  • BaseSocketServer,
    TCP服务器管理基类,负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理。
  • BaseClientManager,连接客户端管理类,该类主要负责客户端登录超时处理,连接上来的客户端维护,经过登陆验证的客户端维护,客户端登陆验证接口,客户端发送数据处理等功能。
  • BaseReceiver,数据接收处理类,该基类是所有接受数据的处理类,负责维护数据的队列关系,并进一步进行处理。
  • ThreadHandler,数据独立线程处理类,对每个不同类型的数据(不同的协议类型),可以用独立的线程进行处理,这里封装了一个基类,用于进行数据独立线程的处理。

1)Socket客户端基类

我们知道Socket通讯,分为了客户端和服务端,它们各自处理的事情是有所不同的,因此为了实现更好的代码重用,我们在这个基础上进行了不同的封装。针对Socket客户端类,我们主要需要提供基础的Socket连接及断开、接收及发送、封包拆包等常规操作过程,因此我们封装了一个客户端基类 BaseSocketClient。

但是为了基于不同的应用客户端,实现不同的业务沟通,我们可以在服务端接收处理不同的客户端,因此也就是需要对Socket客户端进行派生扩展,例如本框架增加了一个中心的Socket客户端、分店的Socket客户端、还有一个桥接的连接客户端(可实现转发数据功能)。

2)Socket服务端基类

相对于Socket客户端基类,同样我们也创建一个Socket服务端基类,通过继承的方式,我们可以用于简化代码的重复性。该服务端基类称为TCP服务器管理基类 BaseSocketServer,负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理。

同样我们也派生了两个服务端的基类,方便对不同的Socket客户端进行差异性处理,如对应上面的中心客户端类ClientOfCall,我们增加一个对应的服务端类ServerForCall,其他的也类似,它们的继承关系如下所示。

另外,由于我们允许不同的Socket客户端类(如ClientOfCall、ClientOfShop)的接入,那么在服务器端也会有对应Socket服务端类(ServerForCall、ServerForShop)进行不同端口的侦听,一旦在自己所属端口有Socket接入,那么服务端类会分派给不同Socket客户端管理类来处理他们的关系和数据,这样也就进一步引入一个客户端管理类的概念,它对应不同的Socket客户端。

这里也根据需要定义了一个Socket客户端管理基类BaseClientManager<T>,这个T代表对应不同的客户端,这样我们就可以派生出CallClientManager和ShopClientManager两个不同的客户端管理类了,它们的继承关系如下所示。

3)数据接收处理基类

在不同的Socket客户端连接到服务端后,服务端开辟一个新的线程进行对应的Socket数据通讯,那么数据通讯这里面的管理,我们可以为不同的Socket客户端订做一个对应的数据接收处理类,专门针对特定的Socket客户端连接的数据进行处理。

这里也根据需要定义了一个数据接收的基类BaseReceiver,同样我们派生对应不同客户端的数据接收类ReceivedForCall、ReceivedForShop和ReceivedForBridge等几个具体的数据处理类,它们的继承关系如下所示。

3、框架界面设计

1)参数配置

Socket服务器需要一些参数来确定侦听的IP地址、端口,以及数据库的连接信息,各种数据的处理时间间隔等参数,因此需要提供一个较好的管理界面来进行管理,本框架使用基于本地配置文件的参数管理方式进行管理,参数界面如下所示。

客户端也同样需要配置一些参数,用来确定连接的服务器IP及端口信息,如下配置界面所示。

Socket服务器监控界面,需要显示一些基础的状态和Socket连接等基础信息,作为我们对整体状态的了解,同时这些信息可以记录到日志里面供我们进行查阅和分析。

除了上面总体的设计外,其中还有一个地方需要细致的展开来介绍,就是对Socket传输消息的封装和拆包,一般的Socket应用,多数采用基于顺序位置和字节长度的方式来确定相关的内容,这些处理对我们分析复杂的协议内容,简直是一场灾难,协议位置一旦变化或者需要特殊的处理,就是很容易出错的,而且大多数代码充斥着很多位置的数值变量,分析和理解都是非常不便的。

如果对于整体的内容,使用一种比较灵活的消息格式,如JSON格式,那么我们可以很好的把消息封装和消息拆包解析两个部分,交给第三方的JSON解析器来进行,我们只需要关注具体的消息处理逻辑就可以了,而且对于协议的扩展,就如JSON一样,可以自由灵活,这样瞬间,整个世界都会很清静了。由于篇幅的原因,我将在下一个随笔在进行介绍JSON格式的消息处理过程。

除了上面的场景外,我们还需要考虑用户消息的加密和校验等内容处理,这样才能达到安全、完整的消息处理,我们可以采用 RSA公钥密码系统。平台通过发送平台RSA公钥消息向终端告知自己的RSA公钥,终端回复终端RSA公钥消息,反之亦然。这样平台和终端的消息,就可以通过自身的私钥加密,让对方公钥解密就可以了。

我在前面一篇随笔《
Socket开发框架之框架设计及分析
》中,介绍了整个Socket开发框架的总体思路,对各个层次的基类进行了一些总结和抽象,已达到重用、简化代码的目的。本篇继续分析其中重要的协议设计部分,对其中消息协议的设计,以及数据的拆包和封包进行了相关的介绍,使得我们在更高级别上更好利用Socket的特性。

1、协议设计思路

对Socket传输消息的封装和拆包,一般的Socket应用,多数采用基于顺序位置和字节长度的方式来确定相关的内容,这样的处理方式可以很好减少数据大小,但是这些处理对我们分析复杂的协议内容,简直是一场灾难。对跟踪解决过这样协议的开发人员来说会很好理解其中的难处,协议位置一旦变化或者需要特殊的处理,就是很容易出错的,而且大多数代码充斥着很多位置的数值变量,分析和理解都是非常不便的。随着网络技术的发展,有时候传输的数据稍大一点,损失一些带宽来传输数据,但是能成倍提高开发程序的效率,是我们值得追求的目标。例如,目前Web API在各种设备大行其道,相对Socket消息来说,它本身在数据大小上不占优势,但是开发的便利性和高效性,是众所周知的。

借鉴了Web API的特点来考虑Socket消息的传输,如果对于整体的内容,Socket应用也使用一种比较灵活的消息格式,如JSON格式来传输数据,那么我们可以很好的把消息封装和消息拆包解析两个部分,交给第三方的JSON解析器来进行,我们只需要关注具体的消息处理逻辑就可以了,而且对于协议的扩展,就如JSON一样,可以自由灵活,这样瞬间,整个世界都会很清静了。

对于Socket消息的安全性和完整性,加密处理方面我们可以采用 RSA公钥密码系统。平台通过发送平台RSA公钥消息向终端告知自己的RSA公钥,终端回复终端RSA公钥消息,这样平台和终端的消息,就可以通过自身的私钥加密,让对方根据接收到的公钥解密就可以了,虽然加密的数据长度会增加不少,但是对于安全性要求高的,采用这种方式也是很有必要的。

对于数据的完整性,传统意义的CRC校验码其实没有太多的用处了,因为我们的数据不会发生部分的丢失,而我们更应该关注的是数据是否被篡改过,这点我想到了微信公众号API接口的设计,它们带有一个安全签名的加密字符串,也就是对其中内容进行同样规则的加密处理,然后对比两个签名内容是否一致即可。不过对于非对称的加密传输,这种数据完整性的校验也可以不必要。

前面介绍了,我们可以参照Web API的方式,以JSON格式作为我们传输的内容,方便序列号和反序列化,这样我们可以大大降低Socket协议的分析难度和出错几率,降低Socket开发难度并提高开发应用的速度。那么我们应该如何设计这个格式呢?

首先我们需要为Socket消息,定义好开始标识和结束标识,中间部分就是整个通用消息的JSON内容。这样,一条完整的Socket消息内容,除了开始和结束标识位外,剩余部分是一个JSON格式的字符串数据。

我们准备根据需要,设计好整个JSON字符串的内容,而且最好设计的较为通用一些,这样便于我们承载更多的数据信息。

2、协议设计分析和演化

参考微信的API传递消息的定义,我设计了下面的消息格式,包括了送达用户ID,发送用户ID、消息类型、创建时间,以及一个通用的内容字段,这个通用的字段应该是另外一个消息实体的JSON字符串,这样我们整个消息格式不用变化,但是具体的内容不同,我们把这个对象类称之BaseMessage,常用字段如下所示。

上面的Content字段就是用来承载具体的消息数据的,它会根据不同的消息类型,传送不同的内容的,而这些内容也是具体的实体类序列化为JSON字符串的,我们为了方便,也设计了这些类的基类,也就是Socket传递数据的实体类基类BaseEntity。

我们在不同的请求和应答消息,都继承于它即可。我们为了方便让它转换为我们所需要的BaseMessage消息,为它增加一个MsgType协议类型的标识,同时增加PackData的方法,让它把实体类转换为JSON字符串。

例如我们一般情况下的请求Request和应答Response的消息对象,都是继承自BaseEntity的,我们可以把这两类消息对象放在不同的目录下方便管理。

继承关系示例如下所示。

其中子类都可以使用基类的PackData方法,直接序列号为JSON字符串即可,那个PacketData的函数主要就是用来组装好待发送的对象BaseMessage的,函数代码如下所示:

        /// <summary>
        ///封装数据进行发送/// </summary>
        /// <returns></returns>
        publicBaseMessage PackData()
{
BaseMessage info
= newBaseMessage()
{
MsgType
= this.MsgType,
Content
= this.SerializeObject()
};
returninfo;
}

有时候我们需要根据请求的信息,用来构造返回的应答消息,因为需要把发送者ID和送达者ID逆反过来。

        /// <summary>
        ///封装数据进行发送(复制请求部分数据)/// </summary>
        /// <returns></returns>
        publicBaseMessage PackData(BaseMessage request)
{
BaseMessage info
= newBaseMessage()
{
MsgType
= this.MsgType,
Content
= this.SerializeObject(),
CallbackID
=request.CallbackID
};
if(!string.IsNullOrEmpty(request.ToUserId))
{
info.ToUserId
=request.FromUserId;
info.FromUserId
=request.ToUserId;
}
returninfo;
}

以登陆请求的数据实体对象介绍,它继承自BaseEntity,同时指定好对应的消息类型即可。

    /// <summary>
    ///登陆请求消息实体/// </summary>
    public classAuthRequest : BaseEntity
{
#region 字段信息 /// <summary> ///用户帐号/// </summary> public string UserId { get; set; }/// <summary> ///用户密码/// </summary> public string Password { get; set; }#endregion /// <summary> ///默认构造函数/// </summary> publicAuthRequest()
{
this.MsgType =DataTypeKey.AuthRequest;
}
/// <summary> ///参数化构造函数/// </summary> /// <param name="userid">用户帐号</param> /// <param name="password">用户密码</param> public AuthRequest(string userid, string password) : this()
{
this.UserId =userid;this.Password =password;
}
}

这样我们的消息内容就很简单,方便我们传递及处理了。

3、消息的接收和发送

前面我们介绍过了一些基类,包括Socket客户端基类,和数据接收的基类设计,这些封装能够给我提供很好的便利性。

在上面的BaseSocketClient里面,我们为了能够解析不同协议的Socket消息,把它转换为我们所需要的基类对象,那么我们这里引入一个解析器MessageSplitter,这个类主要的职责就是用来分析字节数据,并进行整条消息的提取的。

因此我们把BaseSocketClient的类定义的代码设计如下所示。

    /// <summary>
    ///基础的Socket操作类,提供连接、断开、接收和发送等相关操作。/// </summary>
    /// <typeparam name="TSplitter">对应的消息解析类,继承自MessageSplitter</typeparam>
    public class BaseSocketClient<TSplitter>  where TSplitter : MessageSplitter, new()

MessageSplitter对象,给我们处理低层次的协议解析,前面介绍了我们除了协议头和协议尾标识外,其余部分就是一个JSON的,那么它就需要根据这个规则来实现字节数据到对象级别的转换。

首先需要把字节数据进行拆分,把它完整的一条数据加到列表里面后续进行处理。

其中结尾部分,我们就是需要提取缓存的直接数据到一个具体的对象上了。

RawMessage msg = this.ConvertMessage(MsgBufferCache, from);

这个转换的大概规则如下所示。

这样我们在收到消息后,利用TSplitter对象来进行解析就可以了,如下所示就是对Socket消息的处理。

                    TSplitter splitter = newTSplitter();
splitter.InitParam(
this.Socket, this.StartByte, this.EndByte);//指定分隔符,用来拆包 splitter.DataReceived += splitter_DataReceived;//如果有完整的包处理,那么通过事件通知

数据接收并获取一条消息的直接数据对象后,我们就进一步把直接对象转换为具体的消息对象了

        /// <summary>
        ///消息分拆类收到消息事件/// </summary>
        /// <param name="data">原始消息对象</param>
        voidsplitter_DataReceived(RawMessage data)
{
ReceivePackCount
+= 1;//增加收到的包数量 OnReadRaw(data);
}
/// <summary> ///接收数据后的处理,可供子类重载/// </summary> /// <param name="data">原始消息对象(包含原始的字节数据)</param> protected virtual voidOnReadRaw(RawMessage data)
{
//提供默认的包体处理:假设整个内容为Json的方式;//如果需要处理自定义的消息体,那么需要在子类重写OnReadMessage方法。 if (data != null && data.Buffer != null)
{
var json =EncodingGB2312.GetString(data.Buffer);var msg = JsonTools.DeserializeObject<BaseMessage>(json); OnReadMessage(msg);//给子类重载 }
}

在更高一层的数据解析上面,我们就可以对对象级别的消息进行处理了

例如我们收到消息后,它本身解析为一个实体类BaseMessage的,那么我们就可以利用BaseMessage的消息内容,也可以把它的Content内容转换为对应的实体类进行处理,如下代码所示是接收对象后的处理。

        voidTextMsgAnswer(BaseMessage message)
{
var msg = string.Format("来自【{0}】的消息:", message.FromUserId);var request = JsonTools.DeserializeObject<TextMsgRequest>(message.Content);if (request != null)
{
msg
+= string.Format("{0} {1}", request.Message, message.CreateTime.IntToDateTime());
}
//MessageUtil.ShowTips(msg); Portal.gc.MainDialog.AppendMessage(msg);
}

对于消息的发送处理,我们可以举一个例子,如果客户端登陆后,需要获取在线用户列表,那么可以发送一个请求命令,那么服务器需要根据这个命令返回列表信息给终端,如下代码所示。

        /// <summary>
        ///处理客户端请求用户列表的应答/// </summary>
        /// <param name="data">具体的消息对象</param>
        private voidUserListProcess(BaseMessage data)
{
CommonRequest request
= JsonTools.DeserializeObject<CommonRequest>(data.Content);if (request != null)
{
Log.WriteInfo(
string.Format("############\r\n{0}", data.SerializeObject()));

List
<CListItem> list = new List<CListItem>();foreach(ClientOfShop client in Singleton<ShopClientManager>.Instance.LoginClientList.Values)
{
list.Add(
newCListItem(client.Id, client.Id));
}

UserListResponse response
= newUserListResponse(list);
Singleton
<ShopClientManager>.Instance.AddSend(data.FromUserId, response.PackData(data), true);
}
}

在前面两篇介绍了Socket框架的设计思路以及数据传输方面的内容,整个框架的设计指导原则就是易于使用及安全性较好,可以用来从客户端到服务端的数据安全传输,那么实现这个目标就需要设计好消息的传输和数据加密的处理。本篇主要介绍如何利用Socket传输协议来实现数据加密和数据完整性校验的处理,数据加密我们可以采用基于RSA非对称加密的方式来实现,数据的完整性,我们可以对传输的内容进行MD5数据的校验对比。

1、Socket框架传输内容分析

前面介绍过Socket的协议,除了起止标识符外,整个内容是一个JSON的字符串内容,这种格式如下所示。

上述消息内容,我们可以通过开始标识位和结束标识位,抽取出一个完整的Socket消息,这样我们对其中的JSON内容进行序列号就可以得到对应的实体类,我们定义实体类的内容如下所示。

我们把消息对象分为请求消息对象和应答消息对象,他们对应的是Request和Response的消息,也就是一个是发起的消息,一个是应答的消息。其中上图的“承载的JSON内容就是我们另一个传输对象的JSON字符串,这样我们通过这种字符串来传输不同对象的信息,就构造出了一个通用的消息实体对象。

另外这些传输的消息对象,它本身可以继承于一个实体类的基类,这样方便我们对它们的统一处理,如下图所示,就是一个通用的消息对象BaseMessage和其中JSON内容的对象关系图,如AuthRequest是登陆验证请求,AuthorRepsonse是登陆验证的应答。

当然,我们整个Socket应用,可以派生出很多类似的Request和Response的消息对象,如下所示是部分消息的定义。

对于非对称加密的处理,一般来说会有一些性能上的损失,不过我们考虑到如果是安全环境的数据传输处理的话,我们使用非对称加密还是比较好的。

当然也有人建议采用非对称加密部分内容,如双方采用约定的对称加密键,通过非对称加密的方式来传输这个加密键,然后两边采用对称加密算法来处理也是可以的。不过本框架主要介绍采用非对称加密的方式来加密其中的JSON内容,其他部分常规的信息不进行加密。

2、非对称加密的公钥传递

消息加密数据的传输前,我们需要交换算法的公钥,也就是服务器把自己公钥给客户端,客户端收到服务器的公钥请求后,返回客户端的公钥给服务器,实现两者的交换,以后双方的消息都通过对方公钥加密,把加密内容通过标准的Socket消息对象传递,这样对方收到的加密内容,就可以通过自身的私钥进行解密了。

那么要在传递消息前处理这个公钥交换的话,我们可以设计在服务器接入一个新的客户端连接后(在登录处理前),向客户端发送服务器的公钥,客户端受到服务器的公钥后,回应自己的公钥信息,并存储服务器的公钥。这样我们就可以在登陆的时候以及后面的消息传递过程中,使用对方公钥进行加密数据,实现较好的安全性。

公钥传递的过程如下图所示,也就是客户端发起连接服务器请求后,由服务器主动发送一个公钥请求命令,客户端收到后进行响应,发送自身的公钥给服务器,服务器把客户端的公钥信息存储在对应的Socket对象上,以后所有消息都通过客户端公钥加密,然后发送给客户端。

前面我们介绍过,我们所有的自定义Socket对象,都是继承于一个BaseSocketClient这样的基类对象,那么我们只需要在它的对象里面增加几个属性几个,一个是自己的公钥、私钥,一个是对方的公钥信息,如下所示。

在程序的启动后,包括客户端启动,服务器启动,我们都需要构建好自己的公钥私钥信息,如下代码是产生对应的公钥私钥信息,并存储在属性里面。

            using (RSACryptoServiceProvider rsa = newRSACryptoServiceProvider())
{
this.RSAPublicKey = rsa.ToXmlString(false);//公钥 this.RSAPrivateKey = rsa.ToXmlString(true);//私钥 }

例如在服务器端,在客户端Socket成功接入后,我们就给对应的客户端发送公钥请求消息,如下代码所示。

        /// <summary>
        ///客户端连接后的处理(如发送公钥秘钥)/// </summary>
        /// <param name="client">连接客户端</param>
        protected override voidOnAfterClientConnected(ClientOfShop client)
{
//先记录服务端的公钥,私钥 client.RSAPrivateKey =Portal.gc.RSAPrivateKey;
client.RSAPublicKey
=Portal.gc.RSAPublicKey;//发送一个公钥交换命令 var request = newRsaKeyRequest(Portal.gc.RSAPublicKey);var data =request.PackData();
client.SendData(data);
Thread.Sleep(
100);
}

那么在客户端,接收到服务端的消息后,对消息类型判断,如果是公钥请求,那么我们需要进行回应,把自己的公钥发给服务器,否则就进行其他的业务处理了。

        /// <summary>
        ///重写读取消息的处理/// </summary>
        /// <param name="message">获取到的完整Socket消息对象</param>
        protected override voidOnMessageReceived(BaseMessage message)
{
if (message.MsgType ==DataTypeKey.RSARequest)
{
var info = JsonTools.DeserializeObject<RsaKeyRequest>(message.Content);if (info != null)
{
//记录对方的公钥到Socket对象里面 this.PeerRSAPublicKey = Portal.gc.UseRSAEncrypt ? info.RSAPublicKey : "";
Console.WriteLine(
"使用RAS加密:{0},获取到加密公钥为:{1}", Portal.gc.UseRSAEncrypt, info.RSAPublicKey);//公钥请求应答 var publicKey = Portal.gc.UseRSAEncrypt ? Portal.gc.RSAPublicKey : "";var data = new RsaKeyResponse(publicKey);//返回客户端的公钥 var msg =data.PackData(message);

SendData(msg);
Thread.Sleep(
100);//暂停下 }
}
else{//交给业务消息处理过程 this.MessageReceiver.AppendMessage(message);this.MessageReceiver.Check();
}
}

如果我们交换成功后,我们后续的消息,就可以通过RSA非对称加密进行处理了,如下代码所示。

data.Content = RSASecurityHelper.RSAEncrypt(this.PeerRSAPublicKey, data.Content);

而解密消息,则是上面代码的逆过程,如下所示。

message.Content = RSASecurityHelper.RSADecrypt(this.RSAPrivateKey, message.Content);

最后我们把加密后的内容组成一个待发送的Socket消息,包含起止标识符,如下所示。

                //转为JSON,并组装为发送协议格式
                var json =JsonTools.ObjectToJson(data);
toSendData
= string.Format("{0}{1}{2}", (char)this.StartByte, json, (char)this.EndByte);

这样就是我们需要发送的消息内容了,我们拦截内容,可以看到大概的内容如下所示。

上面红色框的内容,必须使用原有的私钥才能进行解密,也就是在网络上,被谁拦截了,也无法进行解开,保证了数据的安全性。

3、数据完整性检查

数据的完整性,我们可以通过消息内容的MD5值进行比对,实现检查是否内容被篡改过,不过如果是采用了非对称加密,这种 完整性检查也可以忽略,不过我们可以保留它作为一个检查处理。

因此在封装数据的时候,就把内容部分MD5值计算出来,如下所示。

data.MD5 = MD5Util.GetMD5_32(data.Content);//获取内容的MD5值

然后在获得消息,并进行解密后(如果有),那么在服务器端计算一下MD5值,并和传递过来的MD5值进行比对,如果一致则说明没有被篡改过,如下代码所示。

                var md5 =MD5Util.GetMD5_32(message.Content);if (md5 ==message.MD5)
{
OnMessageReceived(message);
//给子类重载 }else{
Log.WriteInfo(
string.Format("收到一个被修改过的消息:\r\n{0}", message.Content));
}

以上就是我在Socket开发框架里面,实现传输数据的非对称加密,以及数据完整性校验的
处理过程。