0%

浏览器启动外部软件 - 晓晨Master - 博客园

Excerpt

常可以看见使用浏览器代码启动本地应用的软件.例如qq、迅雷、等等.那么他们是怎么做到的呢? 它的奥秘:Register protocol 前言我们经常看到 tencent://..thunder://这两种开头的网址,往往觉得很奇怪,很想弄懂其中的原理,是如何实现的,我查找了相关的 资料,终于找到了


常可以看见使用浏览器代码启动本地应用的软件.例如qq、迅雷、等等.那么他们是怎么做到的呢?
它的奥秘:Register protocol

前言我们经常看到 tencent://..thunder://这两种开头的网址,往往觉得很奇怪,很想弄懂其中的原理,是如何实现的,我查找了相关的 资料,终于找到了,跟大家分享下。原理篇

这些是腾讯和迅雷的协议,即页面上或地址栏里的链接只要输入带有 tencent://… 的协议,就会自动调用一个已写好的程序执行该协议的操作。 或者是打开页面,迅雷的是下载资源的链接 。通过微软的说明,知道这是Register protocol,对于 Windows、Linux 和 OS X 操作系统都可以注册这样的协议。比如说 Windows,其实只需写入注册表,即可实现协议与执行程序的关联。例如腾讯的协议注册表如下:

1

2

3

4

5

6

7

8

[HKEY_CLASSES_ROOT\TENCENT]

@="TencentProtocol""URL Protocol"="D:\\Program Files\\Tencent\\QQ\\Timwp.exe"

[HKEY_CLASSES_ROOT\TENCENT\DefaultIcon]

@="D:\\Program Files\\Tencent\\QQ\\Timwp.exe,1"

[HKEY_CLASSES_ROOT\TENCENT\shell]

[HKEY_CLASSES_ROOT\TENCENT\shell\open]

[HKEY_CLASSES_ROOT\TENCENT\shell\open\command]

@="\"D:\\Program Files\\Tencent\\QQ\\Timwp.exe\" \"%1\""

 所实现的就是当浏览器(或其它)碰到 tencent://… 时,自动调用Timwp.exe,并把tencent://… 地址作为第一个参数传递给Timwp.exe。实现篇下面我们自己来实现这样的需求:要实现如上功能,最主要的还是对注册表的处理。新建一个注册表文件

1

2

3

4

5

6

7

8

Windows Registry Editor Version 5.00 

[HKEY_CLASSES_ROOT\FinstoneRpt]

@="FRptProtocol" "URL Protocol"="I:\\Release\msdn5.exe"

 [HKEY_CLASSES_ROOT\FinstoneRpt\DefaultIcon] @="I:\\Release\\msdn5.exe,0"

[HKEY_CLASSES_ROOT\FinstoneRpt\shell]

 [HKEY_CLASSES_ROOT\FinstoneRpt\shell\open]

  [HKEY_CLASSES_ROOT\FinstoneRpt\shell\open\command]

@="\"I:\\Release\\msdn5.exe/" \"%1\""

 以上定义了协议名:FinstoneRpt,及相关调用处理程序的位置。各位可自行调整。将上述内容导入到注册表中!

然后在相关网页里加上一个连接:

test

当点击时,则会调用注册表中记录的程序。
当然该程序可以接受参数:就是href里的所有内容。可根据情况自行判断处理。 如需通过vs的安装程序实现安装时自动执行以上操作。在安装程序里,导入上述注册表文件,因为用户安装时的路径不一样,要定位注册表中记录的程序,只需在vs里将相关注册键值修改:将涉及具体地址都一一改成[TARGETDIR]程序名,即可。当你注册协议后,你就可以用指定的程序去“解释”这个协议,在软件里通过取命令行(GetCommandLine)获取传递过来的参数就行了.

本文转自:http://bbs.msdn5.com/forum.php?mod=viewthread&tid=1087

(一)    SDK的引用

由于IPC_SDK没有SDK安装程序,所以需手工把下面图表中的DLL放入Debug或者Release文件夹的根目录下供程序调用,或者加入系统环境变量Path下。

名称

版本号

说明

AudioIntercom.dll

1.1.0.5

AudioRender.dll

1.0.0.2

DsSdk.dll

6.0.10.922

gdiplus.dll

微软库

HCNetSDK.dll

4.3.0.6

网络功能调用,大量功能调用此库

OpenAL32.dll

PlayCtrl.dll

7.2.0.0

播放库,定制版本,增加返角回调及数据结构

QosControl.dll

1.0.0.1

StreamTransClient.dll

1.1.2.12

SuperRender.dll

1.0.1.0

SystemTransform.dll

2.4.0.3

设备信息转发,根据播放库修改过

(二)    C#程序调用DLL中的非托管函数方法

1.        调用外部声明方法

首先在C#语言源程序中声明外部方法,其基本形式是:

[DLLImport(“DLL文件”)]

修饰符 extern 返回变量类型 方法名称 (参数列表)

例如:

1
using System.Runtime.InteropServices;[DllImport("HCNetSDK.dll")]public static extern bool NET_DVR_Init();

注意:

1)        需要在程序声明中使用System.Runtime.InteropServices命名空间。     DllImport只能放置在方法声明上。

2)        DLL文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中Path所设置的路径)。

3)        返回变量类型、方法名称、参数列表一定要与DLL文件中的定义相一致。

4)        若要使用其它函数名,可以使用EntryPoint属性设置,如:[DllImport(“user32.dll”, EntryPoint=”MessageBoxA”)]

static extern int MsgBox(int hWnd, string msg, string caption, int type);

5)        其它可选的 DllImportAttribute 属性:

CharSet 指示用在入口点中的字符集,如:CharSet=CharSet.Ansi;

SetLastError 指示方法是否保留 Win32”上一错误”,如:SetLastError=true;

ExactSpelling 指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配,

如:ExactSpelling=false;

PreserveSig指示方法的签名应当被保留还是被转换, 如:PreserveSig=true;

CallingConvention指示入口点的调用约定, 如:CallingConvention=CallingConvention.Winapi;

2.        参数数据类型转换(详细参考这里

Win32 Types

CLR Type

char,INT8,SBYTE,CHAR

System.SByte

short,short int,INT16,SHORT

System.Int16

int,long,long int,INT32,LONG32,BOOL,INT

System.Int32

_int64,INT64,LONGLONG

System.Int64

unsigned char,UINT8,UCHAR,BYTE

System.Byte

unsigned short,UINT16,USHORT,WORD,ATOM,WCHAR,__wchar_t

System.UInt16

unsigned,unsigned int,UINT32,ULONG32,DWORD32,ULONG,DWORD,UINT

System.UInt32

unsigned __int64,UINT64,DWORDLONG,ULONGLONG

System.UInt64

float,FLOAT

System.Single

double,long double,DOUBLE

System.Double

BSTR

StringBuilder

LPCTSTR

StringBuilder

LPCWSTR

IntPtr

handle

IntPtr

hwnd

IntPtr

char * 

string

int *

ref int

int &

ref int

void *

IntPtrs

unsigned char * 

ref byte

BOOL ——

bool

DWORD

uint或int

注意:

  1. 指针做参数时在C#中一定要使用ref 或out关键字,尤其是结构体指针,要不会报内存读取错误
  2. 首先声明结构体 [StructLayoutAttribute(LayoutKind.Sequential)]
  3. 重写结构体的时候,之前有指明类型长度或数组长度的地方,也要进行相应的标注,要不也会导致内存错误

3.        重写结构体

例如:[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)],具体长度需参看SDK中改结构体的说明文档

或者  [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 64, ArraySubType = UnmanagedType.I1)]

4.        结构体与指针之间的转换

1)        结构体转换为指针

Hik.NET_DVR_IPPARACFG_V40 ipParaCfgV40 = new Hik.NET_DVR_IPPARACFG_V40();//初始化结构体

Int32 size = Marshal.SizeOf(ipParaCfgV40);//返回结构体字节数

IntPtr ptrIpParaCfgV40 = Marshal.AllocHGlobal(size);//定义指针字节数

Marshal.StructureToPtr(ipParaCfgV40, ptrIpParaCfgV40, false);//将结构体封装到内存指针中

//调用需要指针的方法

Marshal.FreeHGlobal(ptrIpParaCfgV40);//释放指针

2)        指针转换为结构体

Hik.NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg = new Hik.NET_DVR_CAMERAPARAMCFG_EX();//实例化结构体

Int32 size = Marshal.SizeOf(cameraParamCfg );//获取结构体字节数

IntPtr ptrCfg = Marshal.AllocHGlobal(size);//为指针分配空间

//调用获取指针的方法

cameraParamCfg = (Hik.NET_DVR_CAMERAPARAMCFG_EX)Marshal.PtrToStructure(ptrCfg, typeof(Hik.NET_DVR_CAMERAPARAMCFG_EX));//把指针转换为结构体

Marshal.FreeHGlobal(ptrCfg);//释放指针

3)        指针转换为结构体精简写法

Int32 size = Marshal.SizeOf(typeof(Hik.NET_DVR_PTZPOS));

IntPtr ptrPTZ = Marshal.AllocHGlobal(size);

//调用获取指针的方法

Hik.NET_DVR_PTZPOS PTZPos = (Hik.NET_DVR_PTZPOS)Marshal.PtrToStructure(ptrPTZ, typeof(Hik.NET_DVR_PTZPOS));//指针转换为结构体

Marshal.FreeHGlobal(ptrPTZ);//释放指针

对第2点和第3点的说明:当一个方法的参数为一个结构体的指针时,并且执行方法后此指针会返回结构体信息时,可以有两种方式来初始化这个指针,第二种更为简洁些。

(三)    SDK的调用

对IPC_SDK的C#封装类见附件Hik.cs(类似我们CHCNetSDK.cs),可参考此基础类文件进行程序功能编写。

(类似我们的HKIPCamera.cs类来调用CHCNetSDK类的win32函数)

1.        获取错误码

Hik.NET_DVR_GetLastError();//获取错误码

使用方式:如果有个非托管函数方法返回结果为false,则调用此方法获取错误码。

例如:

ret = Hik.NET_DVR_Init();

if (ret != true)

    throw new HikException(Hik.NET_DVR_GetLastError());//HikException为自定义异常调用类,用来解析错误码抛出异常

2.        登录

1)        用户注册

Hik.NET_DVR_Init();//初始化SDK,多次初始化会抛出错误

Hik.NET_DVR_SetConnectTime(2000, 1);//设置超时时间

Hik.NET_DVR_SetReconnect(10000, 1);//设置重连时间

Hik.NET_DVR_DEVICEINFO_V30 _deviceInfo = new Hik.NET_DVR_DEVICEINFO_V30();//设备参数结构体,可以返回设备信息

String userId = Hik.NET_DVR_Login_V30(IP,Port,UserName,Password,ref _deviceInfo);//登录后,获取用户ID和设备信息供后续方法调用

2)        用户注销

Hik.NET_DVR_StopRealPlay(_realHandle);//如果有预览视频,则根据其播放句柄关闭视频

Hik.NET_DVR_Logout(_userId);//根据用户ID注销用户登录

Hik.NET_DVR_Cleanup();//必须执行的释放IPC_SDK

3.        获取通道号

由于IPC_SDK同时支持IPC与NVR,所以两者获取通道号的方式有所不同。

1)        获取IPC设备通道号

如果Hik.NET_DVR_DEVICEINFO_V30结构体中byChanNum模拟通道数量属性值大于0,则代表登录设备为IPC设备,需要获取其模拟通道号byStartChan属性的值。每组模拟通道号范围为0-32。

for (Int32 i = 0; i < _deviceInfo.byChanNum; i++) //一个IPC设备可以设置多个模拟通道号,默认为一个模拟通道号

{

   Int channelId = _deviceInfo.byStartChan

}

2)        获取NVR设备通道号

如果Hik.NET_DVR_DEVICEINFO_V30结构体中byIPChanNum数字通道数量属性值大于0,则代表登录设备为NVR设备,需要获取其数字通道号,其获取方法要比获取IPC设备通道号复杂。每组数字通道号范围为33-64。

public void InfoIPChannel()

        {

            Hik.NET_DVR_IPPARACFG_V40 ipParaCfgV40 = new Hik.NET_DVR_IPPARACFG_V40();

            Int32 size = Marshal.SizeOf(ipParaCfgV40);

            IntPtr ptrIpParaCfgV40 = Marshal.AllocHGlobal(size);

            Marshal.StructureToPtr(ipParaCfgV40, ptrIpParaCfgV40, false);

            UInt32 result = 0;

            Int32 groupNo = 0;

            Boolean ret = Hik.NET_DVR_GetDVRConfig(_userId, Hik.NET_DVR_GET_IPPARACFG_V40, groupNo, ptrIpParaCfgV40, (UInt32)size, ref result);//获取配置信息

            if (ret)

            {

                ipParaCfgV40 = (Hik.NET_DVR_IPPARACFG_V40)Marshal.PtrToStructure(ptrIpParaCfgV40, typeof(Hik.NET_DVR_IPPARACFG_V40));

                byte byStreamType;

                for (Int32 i = 0; i < ipParaCfgV40.dwDChanNum; i++)

                {

                    byStreamType = ipParaCfgV40.struStreamMode[i].byGetStreamType;

                    size = Marshal.SizeOf(ipParaCfgV40.struStreamMode[i].uGetStream);

                    switch (byStreamType)

                    {

                        //目前NVR仅支持直接从设备取流 NVR supports only the mode: get stream from device directly

                        case 0:

                            IntPtr ptrChanInfo = Marshal.AllocHGlobal(size);

                            Marshal.StructureToPtr(ipParaCfgV40.struStreamMode[i].uGetStream, ptrChanInfo, false);

                            Hik.NET_DVR_IPCHANINFO _struChanInfo = (Hik.NET_DVR_IPCHANINFO)Marshal.PtrToStructure(ptrChanInfo, typeof(Hik.NET_DVR_IPCHANINFO));

                            Int32 deviceId = _struChanInfo.byIPID + _struChanInfo.byIPIDHigh * 256 - groupNo * 64 - 1;

                            if (deviceId == -1)

                                continue;

                            _channelInfoList.Add(new ChannelInfo() { DeviceID = deviceId, ChannelID = i + (Int32)ipParaCfgV40.dwStartDChan, State = (Int32)_struChanInfo.byEnable, IP = ipParaCfgV40.struIPDevInfo[i].struIP.sIpV4 });//获取数字通道信息封装到自定义通道类中

                            Marshal.FreeHGlobal(ptrChanInfo);

                            break;

                        default:

                            break;

                    }

                }

            }

            Marshal.FreeHGlobal(ptrIpParaCfgV40);

        }

4.        预览

1)        初始化预览结构体

Hik.NET_DVR_PREVIEWINFO previewInfo = new Hik.NET_DVR_PREVIEWINFO();

previewInfo.lChannel = channelId;//预览的设备通道 the device channel number

previewInfo.dwStreamType = 0;//码流类型:0-主码流,1-子码流,2-码流3,3-码流4,以此类推

previewInfo.dwLinkMode = 0;//连接方式:0- TCP方式,1- UDP方式,2- 多播方式,3- RTP方式,4-RTP/RTSP,5-RSTP/HTTP

previewInfo.hPlayWnd = IntPtr.Zero;//预览窗口,如果不使用回调显示视频流,则此处可以设置显示窗口句柄

previewInfo.bBlocked = true; //0- 非阻塞取流,1- 阻塞取流

2)        获取实时视频流和视频句柄

_realHandle = Hik.NET_DVR_RealPlay_V40(UserId, ref previewInfo, realData, IntPtr.Zero);//返回实时流句柄供后续方法调用

参数说明:

UserID为登录后返回的用户句柄,

previewInfo为已实例化的预览结构体

realData为预览实时流回调函数,如果直接在窗口显示实时流而不通过回调方式,则此参数可以设置为null

3)        实时流回调函数的使用

在封装类(CHCNetSDK)中定义回调函数:

///

/// 预览实时流回调函数

///

/// 当前的预览句柄

/// 数据类型:1系统头数据,2流数据(包括复合流或音视频分开的视频流数据),3音频数据

/// 存放数据的缓冲区指针

/// 缓冲区大小

/// 用户数据

public delegate void REALDATACALLBACK(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser);

使用时注意:实例化实时流回调函数时,不能在方法内声明并实例化,否则会被回收机制提前释放实例化对象,导致程序错误。需要在最外部调用类中作为字段预先声明。

例如:(HKIPCamera类中)

private Hik.REALDATACALLBACK _callback;//实时流回调,作为字段声明

public override int IncreaseClient()

{

    if (_callback == null)

         _callback = new Hik.REALDATACALLBACK(RealDataCallBack);//RealDataCallBack是一个方法

         Hik.StartPlay(Hik.ChannelInfoList[0].ChannelID, _callback);//播放实时流视频

}

简易流程描述为:实时流回调函数返回实时视频流,然后把实时视频流传入到播放库解码(PlayCtrl.dll),通过播放库的两个回调函数来返回解析后的视频流和角度。角度回调信息的格式为我公司定制协议,具体内容参看角度信息私有数据格式,59个字节长度的16进制字符串。

private Hik.DECCBFUN _displayCallback;//解码回调

private Hik.ADDITIONDATACBFUN _additionDataDisplayCallback;//角度回调

获取播放库错误码

Int port;

PlayM4_GetPort(ref port);//首先需获取播放句柄

PlayM4_GetLastError(port);//获取播放库错误码

具体回调如下:

int port;播放库端口号

switch (dwDataType)

{

        case 1:     // sys head 系统头数据

        if (dwBufSize > 0)

        {

           //获取播放句柄 Get the port to play

           PlayM4_GetPort(ref _port);

           //设置流播放模式

           PlayM4_SetStreamOpenMode(_port, 0);

           //打开码流,送入头数据 Open stream

          PlayM4_OpenStream(_port, pBuffer, dwBufSize, 2 * 1024 * 1024);

           //设置显示缓冲区个数

           PlayM4_SetDisplayBuf(_port, 15);

           //设置解码回调函数,获取解码后音视频原始数据 Set callback function of decoded data

           PlayM4_SetDecCallBackEx(_port, displayFun, IntPtr.Zero, 0);

           //设置角度回调函数,获取解码前角度信息

          PlayM4_SetAdditionDataCallBack(_port, 0x1004, additionDataDisplayFun, IntPtr.Zero);

          //开始解码 Start to play 

          PlayM4_Play(_port, IntPtr.Zero);//最后一个参数表示播放窗体的指针

          }

          break;

          case 2:     // video stream data 视频流数据(包括复合流和音视频分开的视频流数据)

          if (dwBufSize > 0 && _port != -1)

          {

            for (Int32 i = 0; i < 999; i++)

            {

              //送入码流数据进行解码 Input the stream data to decode

             if (!PlayM4_InputData(_port, pBuffer, dwBufSize))

               Thread.Sleep(2);

            else

                 break; 

                      }

            }

           break;

          default:

           if (dwBufSize > 0 && _port != -1)

           {

              //送入其他数据 Input the other data

             for (Int32 i = 0; i < 999; i++)

             {

              if (!PlayM4_InputData(_port, pBuffer, dwBufSize))

                  Thread.Sleep(2);

              else

                  break;

              }

            }

              break;

            }

4)        停止预览

Hik.NET_DVR_StopRealPlay(_realHandle);//_realHandle为播放句柄

5.        云台控制

1)        云台操作命令

            SET_PRESET = 8,// 设置预置点

            CLE_PRESET = 9,// 清除预置点

            Up = 21,/* 云台以SS的速度上仰 */

            Down = 22,/* 云台以SS的速度下俯 */

            Left = 23,/* 云台以SS的速度左转 */

            Right = 24,/* 云台以SS的速度右转 */

            UpLeft = 25,/* 云台以SS的速度上仰和左转 */

            UpRight = 26,/* 云台以SS的速度上仰和右转 */

            DownLeft = 27,/* 云台以SS的速度下俯和左转 */

            DownRight = 28,/* 云台以SS的速度下俯和右转 */

            Auto = 29,/* 云台以SS的速度左右自动扫描 */

            ZOOM_IN = 11,/* 焦距以速度SS变大(倍率变大) */

            ZOOM_OUT = 12,/* 焦距以速度SS变小(倍率变小) */

            FOCUS_NEAR = 13,  /* 焦点以速度SS前调 */

            FOCUS_FAR = 14,  /* 焦点以速度SS后调 */

            IRIS_OPEN = 15,  /* 光圈以速度SS扩大 */

            IRIS_CLOSE = 16,  /* 光圈以速度SS缩小 */

            GOTO_PRESET = 39/* 快球转到预置点 */

2)        云台操作方法

根据定制协议,云台控制速度为1-160。但是需保证球机的升级包版本为IPD_R3_STD_5.2.0_141111。

///

/// 带速度的云台控制操作(需先启动图像预览)

///

/// NET_DVR_RealPlay_V40的返回值

/// 云台控制命令

/// 云台停止动作或开始动作:0- 开始;1- 停止

/// 云台控制的速度,用户按不同解码器的速度控制值设置。取值范围[1,160]

/// TRUE表示成功,FALSE表示失败

bool NET_DVR_PTZControlWithSpeed(int lRealHandle, uint dwPTZCommand, uint dwStop, uint dwSpeed);

6.        云台预置位

1)        预置位命令

     SET_PRESET = 8,// 设置预置点

            CLE_PRESET = 9,// 清除预置点

            GOTO_PRESET = 39// 转到预置点

2)        预置位操作

///

/// 设置预置位

///

/// NET_DVR_RealPlay_V40的返回值

/// 预置位控制命令

/// 预置位编号最多支持255个预置点

/// TRUE表示成功,FALSE表示失败

bool NET_DVR_PTZPreset(int lRealHandle, uint dwPTZPresetCmd, uint dwPresetIndex);

3)        反向操作

Hik.NET_DVR_PTZPreset(_realHandle, (uint)8, 33);

7.        获取设备的配置信息

在设定某些配置时,需预先执行此方法来获取相关配置信息的结构体。

///

/// 获取设备的配置信息

///

/// NET_DVR_Login_V30的返回值

/// 设备配置命令

/// 通道号或者组号,不同的命令对应不同的取值

/// 接收数据的缓冲指针

/// 接收数据的缓冲长度(以字节为单位),不能为0

/// 实际收到的数据长度指针,不能为NULL

///

bool NET_DVR_GetDVRConfig(int lUserID, uint dwCommand, int lChannel, IntPtr lpOutBuffer, uint dwOutBufferSize, ref uint lpBytesReturned);

具体调用见下面操作示例

8.        设置设备的配置信息

///

/// 设置设备的配置信息

///

/// NET_DVR_Login_V30的返回值

/// 设备配置命令

/// 通道号或者组号,不同的命令对应不同的取值

/// 输入数据的缓冲指针

/// 输入数据的缓冲长度(以字节为单位)

///

bool NET_DVR_SetDVRConfig(int lUserID, uint dwCommand, int lChannel, IntPtr lpInBuffer, uint dwInBufferSize);

具体调用见下面操作示例

9.        日夜切换 (前端参数配置结构体中)

1)        设备配置命令

3369; //IPC设置CCD参数配置

3368; //IPC获取CCD参数配置

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg = new Hik.NET_DVR_CAMERAPARAMCFG_EX();//实例化前端参数结构体

Int32 nSize = Marshal.SizeOf(cameraParamCfg );//获取结构体空间大小

IntPtr ptrCfg = Marshal.AllocHGlobal(nSize);//设置指针大小

Hik.NET_DVR_GetDVRConfig(_userId,3368, _channelId, ptrCfg, (UInt32)nSize, ref dwReturn);//获取配置信息的指针

cameraParamCfg = (Hik.NET_DVR_CAMERAPARAMCFG_EX)Marshal.PtrToStructure(ptrCfg, typeof(Hik.NET_DVR_CAMERAPARAMCFG_EX));//指针转换为数据结构

Marshal.FreeHGlobal(ptrCfg);//释放指针

4)        日夜切换命令

5)        日夜切换结构体

NET_DVR_DAYNIGHT struDayNight;/*日夜转换*/

6)        设置配置信息

Hik.NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg=获取配置信息();

Int32 size = Marshal.SizeOf(cameraParamCfg);//获取结构体大小

IntPtr ptrCfg = Marshal.AllocHGlobal(size);//设置指针空间大小

cameraParamCfg.struDayNight.byDayNightFilterType = (byte)日夜切换命令;

Marshal.StructureToPtr(cameraParamCfg, ptrCfg, false);//结构体转换为指针

Hik.NET_DVR_SetDVRConfig(_userId, 3369, _channelId, ptrCfg, (uint)size);//设置参数

Marshal.FreeHGlobal(ptrCfg);//释放指针

10.    透雾切换(前端参数配置结构体中)

1)        设备配置命令

3369; //IPC设置CCD参数配置

3368; //IPC获取CCD参数配置

2)        通道号或者组号

        ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg = new Hik.NET_DVR_CAMERAPARAMCFG_EX();//实例化前端参数结构体

Int32 nSize = Marshal.SizeOf(cameraParamCfg );//获取结构体空间大小

IntPtr ptrCfg = Marshal.AllocHGlobal(nSize);//设置指针大小

Hik.NET_DVR_GetDVRConfig(_userId,3368, _channelId, ptrCfg, (UInt32)nSize, ref dwReturn);//获取配置信息的指针

cameraParamCfg = (Hik.NET_DVR_CAMERAPARAMCFG_EX)Marshal.PtrToStructure(ptrCfg, typeof(Hik.NET_DVR_CAMERAPARAMCFG_EX));//指针转换为数据结构

Marshal.FreeHGlobal(ptrCfg);//释放指针

4)        透雾切换命令

5)        透雾切换结构体

NET_DVR_DEFOGCFG struDefogCfg;//透雾参数

6)        设置配置信息

Hik.NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg=获取配置信息();

Int32 size = Marshal.SizeOf(cameraParamCfg);//获取结构体大小

IntPtr ptrCfg = Marshal.AllocHGlobal(size);//设置指针空间大小

cameraParamCfg.struDefogCfg.byMode = (byte)透雾切换命令;

cameraParamCfg.struDefogCfg.byLevel = (byte)100;//透雾级别

Marshal.StructureToPtr(cameraParamCfg, ptrCfg, false);//结构体转换为指针

Hik.NET_DVR_SetDVRConfig(_userId, 3369, _channelId, ptrCfg, (uint)size);//设置参数

Marshal.FreeHGlobal(ptrCfg);//释放指针

11.    聚焦模式切换

1)        设备配置命令

        3306; //设置快球聚焦模式参数

        3305; //获取快球聚焦模式参数

2)        通道号或者组号

        ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_FOCUSMODE_CFG focusModeCfg = new Hik.NET_DVR_FOCUSMODE_CFG();//实例化聚焦模式结构体

Int32 size = Marshal.SizeOf(focusModeCfg);//获取结构体大小

IntPtr ptrCfg = Marshal.AllocHGlobal(size);//设置指针空间大小

Hik.NET_DVR_GetDVRConfig(_userId, 3305, _channelId, ptrCfg, (UInt32)size, ref dwReturn);//获取聚焦模式信息的指针

focusModeCfg = (Hik.NET_DVR_FOCUSMODE_CFG)Marshal.PtrToStructure(ptrCfg, typeof(Hik.NET_DVR_FOCUSMODE_CFG));//指针转换为聚焦模式结构体

Marshal.FreeHGlobal(ptrCfg);//释放指针

4)        聚焦模式切换命令

5)        聚焦模式切换结构体

6)        设置配置信息

Hik.NET_DVR_FOCUSMODE_CFG focusModeCfg = 获取配置信息();

Int32 size = Marshal.SizeOf(focusModeCfg);//获取结构体大小

IntPtr ptrCfg = Marshal.AllocHGlobal(size);//设置指针空间大小

focusModeCfg.byFocusMode = (byte)聚焦模式命令;

Marshal.StructureToPtr(focusModeCfg, ptrCfg, false);//结构体转换为指针

Hik.NET_DVR_SetDVRConfig(_userId, Hik.NET_DVR_SET_FOCUSMODECFG, _channelId, ptrCfg, (uint)size);//设置聚焦模式

Marshal.FreeHGlobal(ptrCfg);//释放指针

12.    OSD字符设置

1)        设备配置命令

1030;//获取叠加字符操作命令

1031;//设置叠加字符操作命令

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_SHOWSTRING_V30 struShowStrCfg = new Hik.NET_DVR_SHOWSTRING_V30();//初始化叠加字符结构体

Int32 size = Marshal.SizeOf(struShowStrCfg);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针大小

Hik.NET_DVR_GetDVRConfig(_userId, 1030, _channelId, ptr, (UInt32)size, ref dwReturn);//获取配置信息

struShowStrCfg = (Hik.NET_DVR_SHOWSTRING_V30)Marshal.PtrToStructure(ptr, typeof(Hik.NET_DVR_SHOWSTRING_V30));//指针转换为结构体

Marshal.FreeHGlobal(ptr);//释放指针

4)        叠加字符结构体

NET_DVR_SHOWSTRINGINFO struStringInfo;/* 要显示的字符内容 */

说明:设置叠加字符需要为其属性赋值。

5)        设置配置信息

Hik.NET_DVR_SHOWSTRING_V30 struShowStrCfg =获取叠加字符配置信息();

Int32 size = Marshal.SizeOf(struShowStrCfg);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针空间大小

String osd = “海康智能监控视频一”;

struShowStrCfg.struStringInfo[0].wShowString = 1;//1为显示,0为不显示

struShowStrCfg.struStringInfo[0].sString = osd;//叠加的字符串

struShowStrCfg.struStringInfo[0].wStringSize = (ushort)(osd.Length * 2);//字符串大小

struShowStrCfg.struStringInfo[0].wShowStringTopLeftX = 0;//坐标

struShowStrCfg.struStringInfo[0].wShowStringTopLeftY = 0;//坐标

Marshal.StructureToPtr(struShowStrCfg, ptr, false);//街头体转换为指针

Hik.NET_DVR_SetDVRConfig(_userId, 1031, _channelId, ptr, (UInt32)size);//设置配置信息

Marshal.FreeHGlobal(ptr);//释放指针

说明:可以叠加显示多个字符串。

13.    球机定位速度设置

1)        设备配置命令

3270;//获取PTZ基本参数信息

3271;//设置PTZ基本参数信息

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_PTZ_BASICPARAMCFG basicParamCfg = new Hik.NET_DVR_PTZ_BASICPARAMCFG();//初始化PTZ基本参数结构体

Int32 size = Marshal.SizeOf(basicParamCfg);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针大小

Hik.NET_DVR_GetDVRConfig(userId, 3270, _channelId, ptr, (UInt32)size, ref bytesReturned);//获取配置参数指针

basicParamCfg = (Hik.NET_DVR_PTZ_BASICPARAMCFG)Marshal.PtrToStructure(ptr, typeof(Hik.NET_DVR_PTZ_BASICPARAMCFG));//指针转换为结构体

Marshal.FreeHGlobal(ptr);//释放指针

4)        球机定位速度命令

1-8

5)        球机定位速度结构体

Hik.NET_DVR_PTZ_BASICPARAMCFG basicParamCfg

6)        设置配置信息

Hik.NET_DVR_PTZ_BASICPARAMCFG basicParamCfg = 获取配置信息();

Int32 size = Marshal.SizeOf(basicParamCfg);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针空间大小

basicParamCfg.byPresetSpeed = (byte)速度值;

Marshal.StructureToPtr(basicParamCfg, ptr, false);//结构体转换为指针

Hik.NET_DVR_SetDVRConfig(userId, 3271, _channelId, ptr, size);//设置配置信息

Marshal.FreeHGlobal(ptr);//释放指针

14.    获取球机转动范围

1)        设备配置命令

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

Hik.NET_DVR_PTZSCOPE PTZScope = new Hik.NET_DVR_PTZSCOPE();//初始化球机范围信息结构体

Int32 size = Marshal.SizeOf(PTZScope);//获取结构体空间大小

IntPtr ptrScope = Marshal.AllocHGlobal(size);//设置指针空间大小

Hik.NET_DVR_GetDVRConfig(_userId, 294, _channelId, ptrScope, (UInt32)size, ref result);//获取配置信息

PTZScope = (Hik.NET_DVR_PTZSCOPE)Marshal.PtrToStructure(ptrScope, typeof(Hik.NET_DVR_PTZSCOPE));//指针转换为结构体

Marshal.FreeHGlobal(ptrScope);//释放指针

4)        球机转动范围结构体

Hik.NET_DVR_PTZSCOPE PTZScope

15.    球机定位

使用球机定位功能前,需先设置球机定位速度和获取球机转动范围,再根据球机转动范围信息来操作球机定位功能。

1)        设备配置命令

292;//云台设置PTZ位置

293;//云台获取PTZ位置

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        获取配置信息

//用精简方式实现

Int32 size = Marshal.SizeOf(typeof(Hik.NET_DVR_PTZPOS));//获取球机位置信息结构体大小

IntPtr ptrPTZ = Marshal.AllocHGlobal(size);//设置指针空间大小

Hik.NET_DVR_GetDVRConfig(_userId, 293, _channelId, ptrPTZ, (UInt32)size, ref result);//获取球机位置配置信息

Hik.NET_DVR_PTZPOS PTZPos = (Hik.NET_DVR_PTZPOS)Marshal.PtrToStructure(ptrPTZ, typeof(Hik.NET_DVR_PTZPOS));//指针转换为结构体

Marshal.FreeHGlobal(ptrPTZ);//释放指针

4)        透雾切换结构体

Hik.NET_DVR_PTZPOS PTZPos

5)        设置配置信息

Hik.NET_DVR_PTZPOS PTZPos = 获取球机位置信息();

Int32 size = Marshal.SizeOf(PTZPos);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针空间大小

PTZPos.wAction = 1;//-表示定位PTZ参数

//本结构体中的wAction参数是设置时的操作类型,因此获取时该参数无效。实际显示的PTZ值是获取到的十六进制值的十分之一,如获取的水平参数P的值是0x1750,实际显示的P值为175度;获取到的垂直参数T的值是0x0789,实际显示的T值为78.9度;获取到的变倍参数Z的值是0x1100,实际显示的Z值为110度。

String temp = “0x” + Convert.ToString(horAngle * 10);//实际显示的PTZ值是获取到的十六进制值的十分之一,所以需要把输入的数值乘以10再拼成十六进制字符串

PTZPos.wPanPos = Convert.ToUInt16(temp, 16);//转换为16进制水平角度

if (pitAngle >= 0)//判断俯仰角度的正负。由于零方位角的设置不同,会导致出现负的俯仰角度,所以处理方式不同

   PTZPos.wTiltPos = Convert.ToUInt16(“0x” + Convert.ToString(pitAngle * 10), 16);

else

   PTZPos.wTiltPos = Convert.ToUInt16(“0x” + Convert.ToString((pitAngle + 360) * 10), 16);

PTZPos.wZoomPos = Convert.ToUInt16(“0x” + Convert.ToString(viewAngle * 10), 16);

Marshal.StructureToPtr(PTZPos, ptr, false);//结构体转换为指针

Hik.NET_DVR_SetDVRConfig(_userId, 292, _channelId, ptr, (uint)size);//设置配置

Marshal.FreeHGlobal(ptr);//释放指针

16.    零方位角控制

1)        设备配置命令

2)        通道号或者组号

ChannelId//登录IPC设备时获取的通道号

3)        零方位角控制命令

SET = 0,//设置

GOTO = 1,//调用

CLE = 2//清除

4)        零方位角控制结构体

Hik.NET_DVR_INITIALPOSITIONCTRL initialPositionCtrl

5)        设置控制信息

Hik.NET_DVR_INITIALPOSITIONCTRL initialPositionCtrl = new Hik.NET_DVR_INITIALPOSITIONCTRL();//初始化零方位角控制结构体

Int32 size = Marshal.SizeOf(initialPositionCtrl);//获取结构体空间大小

IntPtr ptr = Marshal.AllocHGlobal(size);//设置指针大小

initialPositionCtrl.dwSize = (uint)size;//结构体大小

initialPositionCtrl.dwChan = (uint)_channelId;//播放通道号

initialPositionCtrl.byWorkMode = (byte)command;//零方位角控制命令

Marshal.StructureToPtr(initialPositionCtrl, ptr, false);//结构体转换为指针

Hik.NET_DVR_RemoteControl(_userId, (uint)3283, ptr, (uint)size);//零方位角控制

Marshal.FreeHGlobal(ptr);//释放指针

17.    录像回放

此功能需要硬盘录像机支持。需要先登录NVR设备。在登录和获取数字通道号后,根据设备所对应的通道号操作相关IPC设备的一些基础功能。

1)        设备配置命令

1;//开始播放

2;//停止播放

3;//暂停播放

4;//恢复播放

5;//快放

6;//慢放

7;//正常速度

8;//单帧放

9;//打开声音

10;//关闭声音

11;//调节音量

12;//改变文件回放的进度

13;//获取文件回放的进度

14;//获取当前已经播放的时间(按文件回放的时候有效)

15;//获取当前已经播放的帧数(按文件回放的时候有效)

16;//获取当前播放文件总的帧数(按文件回放的时候有效)

17;//获取当前播放文件总的时间(按文件回放的时候有效)

20;//丢B帧

24;//设置码流速度

25;//保持与设备的心跳(如果回调阻塞,建议2秒发送一次)

26;//按绝对时间定位

27;//获取按时间回放对应时间段内的所有文件的总长度

29;//倒放切换为正放

30;//正放切换为倒放

32;//设置转封装类型

33;//正放切换为倒放

2)        初始化录像回放结构体

Hik.NET_DVR_VOD_PARA struVodPara = new Hik.NET_DVR_VOD_PARA();//初始化录像回放结构体

struVodPara.dwSize = (uint)Marshal.SizeOf(struVodPara);//获取结构体空间大小

struVodPara.struIDInfo.dwChannel = (uint)_ipChannelId; //数字通道号 Channel number,海康设备数字通道号从33开始 

//struVodPara.hWnd = this.pictureBoxVideo.Handle;//不使用回调获取回放视频时,使用此属性设置回放窗口句柄

struVodPara.hWnd = IntPtr.Zero;//使用回调获取回放视频时,需如此设置回放窗口句柄

//设置回放的开始时间 Set the starting time to search video files

struVodPara.struBeginTime.dwYear = (uint)dateTimeStart.Value.Year;

struVodPara.struBeginTime.dwMonth = (uint)dateTimeStart.Value.Month;

struVodPara.struBeginTime.dwDay = (uint)dateTimeStart.Value.Day;

struVodPara.struBeginTime.dwHour = (uint)dateTimeStart.Value.Hour;

struVodPara.struBeginTime.dwMinute = (uint)dateTimeStart.Value.Minute;

struVodPara.struBeginTime.dwSecond = (uint)dateTimeStart.Value.Second;

//设置回放的结束时间 Set the stopping time to search video files

struVodPara.struEndTime.dwYear = (uint)dateTimeEnd.Value.Year;

struVodPara.struEndTime.dwMonth = (uint)dateTimeEnd.Value.Month;

struVodPara.struEndTime.dwDay = (uint)dateTimeEnd.Value.Day;

struVodPara.struEndTime.dwHour = (uint)dateTimeEnd.Value.Hour;

struVodPara.struEndTime.dwMinute = (uint)dateTimeEnd.Value.Minute;

struVodPara.struEndTime.dwSecond = (uint)dateTimeEnd.Value.Second;

3)        获取录像回放播放句柄

目前根据需求只实现根据录像起止时间范围播放录像视频,其他功能请参考SDK功能说明文档。

//按时间回放 Playback by time

_realHandle = Hik.NET_DVR_PlayBackByTime_V40(_NVRUserId, ref struVodPara);// struVodPara为录像回放结构体

4)        录像数据回调函数的使用

///

/// 录像数据回调函数

///

/// 当前的录像播放句柄

/// 数据类型

/// 存放数据的缓冲区指针

/// 缓冲区大小

/// 用户数据

public delegate void PlayDataCallBack_V40(Int32 lPlayHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser);

此回调函数的使用方式与实时流回调函数的操作方式类似,都是获取视频流后,用播放库解码,返回角度信息和解码后的视频流。

Hik.NET_DVR_PlayBackControl_V40(_realHandle, 1, IntPtr.Zero, 0, IntPtr.Zero, ref iOutValue);//播放录像回放视频

5)        停止录像回放

Hik.NET_DVR_StopPlayBack(_realHandle)

18.    录像下载

1)        初始化录像下载结构体

Hik.NET_DVR_PLAYCOND struDownPara = new Hik.NET_DVR_PLAYCOND();//初始化录像下载结构体

struDownPara.dwChannel = (uint)_ipChannelId; //数字通道号 Channel number 

//设置下载的开始时间 Set the starting time

struDownPara.struStartTime.dwYear = (uint)dateTimeStart.Value.Year;

struDownPara.struStartTime.dwMonth = (uint)dateTimeStart.Value.Month;

struDownPara.struStartTime.dwDay = (uint)dateTimeStart.Value.Day;

struDownPara.struStartTime.dwHour = (uint)dateTimeStart.Value.Hour;

struDownPara.struStartTime.dwMinute = (uint)dateTimeStart.Value.Minute;

struDownPara.struStartTime.dwSecond = (uint)dateTimeStart.Value.Second;

//设置下载的结束时间 Set the stopping time

struDownPara.struStopTime.dwYear = (uint)dateTimeEnd.Value.Year;

struDownPara.struStopTime.dwMonth = (uint)dateTimeEnd.Value.Month;

struDownPara.struStopTime.dwDay = (uint)dateTimeEnd.Value.Day;

struDownPara.struStopTime.dwHour = (uint)dateTimeEnd.Value.Hour;

struDownPara.struStopTime.dwMinute = (uint)dateTimeEnd.Value.Minute;

struDownPara.struStopTime.dwSecond = (uint)dateTimeEnd.Value.Second;

2)        录像下载结构体

Hik.NET_DVR_PLAYCOND  struDownPara

3)        录像下载

录像下载后默认保存格式为mp4。

_realHandle = Hik.NET_DVR_GetFileByTime_V40(_NVRUserId, sVideoFileName, ref struDownPara);

海康平台可以挂载多个NVR设备,调用NVR下挂载的IPC设备时,只需要在海康平台的设备设置中找到其摄像头ID即可。

(一)    SDK的引用

具体引用方式,参见IPC_SDK的引用。由于海康平台技术人员提供的DLL文件过于繁杂,就不一一列出DLL的说明,详情参见IVMS_SDK文件夹目录。

(二)    C#程序调用DLL中的非托管函数方法

详情参见C#程序调用DLL中的非托管函数方法

(三)    SDK的调用

根据海康视频设备及平台使用的方案设计,只应用海康平台SDK的视频回调显示功能,其它功能暂不使用。对Plat_SDK的C#封装类见附件HikIVMS.cs,可参考此基础类文件进行程序功能编写。

1.        获取错误码

Plat_GetLastError(_userId);//获取错误码,userId为登录后获得的用户ID

使用方式:如果每个非托管函数方法返回结果不为0(正确),则调用此方法获取错误码。

例如:

Int32 ret = Plat_Init();

if (ret != 0)

   throw new HikIVMSException(Plat_GetLastError(_userId));// HikIVMSException为自定义异常调用类,用来解析错误码抛出异常

2.        登录

1)        用户注册

Int32 ret;

ret = Plat_Init();//platformSDK初始化

if (ret != 0)

   throw new HikIVMSException(Plat_GetLastError(_userId));

IntPtr ptrIP = Marshal.StringToHGlobalAnsi(uri.Host);//IP

IntPtr ptrUserName = Marshal.StringToHGlobalAnsi(a[0]);//用户名

IntPtr ptrPassword = Marshal.StringToHGlobalAnsi(a[1]);//密码

IntPtr ptrPort = Marshal.StringToHGlobalAnsi(uri.Port.ToString());//端口号

_userId = Plat_LoginCMS(ptrIP, ptrUserName, ptrPassword, ptrPort, 0);//登录

//释放指针

Marshal.FreeHGlobal(ptrIP);

Marshal.FreeHGlobal(ptrUserName);

Marshal.FreeHGlobal(ptrPassword);

Marshal.FreeHGlobal(ptrPort);

2)        用户注销

//如果存在播放的视频,也就是_streamHandle播放句柄>0,则先停止播放视频

Plat_StopVideo(_streamHandle);

Plat_LogoutCMS(_userId);//注销用户

Plat_Free();//释放SDK,必须执行

3.        预览

1)        播放预览

///

/// 播放实时视频(开始预览/回放)

///

/// 预览或回放的播放路径,由Plat_QueryRealStreamURL或Plat_QueryRecordFile

/// 播放窗口句柄,如果为空,则不播放

/// 用户ID

/// 视频码流接收回调函数指针,如果回调函数为NULL则不给码流

/// >=0成功,返回实时视频句柄,错误时返回-1

Int32 Plat_PlayVideo(String url, IntPtr hWnd, Int32 userHandle, fStreamCallback streamCallback, IntPtr user);

例如:

_streamHandle = Plat_PlayVideo(cameraId, IntPtr.Zero, _userId, callback, IntPtr.Zero);

注意:此函数的第一个参数,在应用中只需要传入摄像机ID即可。

2)        实时流回调函数的使用

///

/// 实时流回调

///

/// Plat_PlayVideo返回的句柄

/// 接收视频码流数据缓冲区指针

/// 接收视频码流数据字节数

/// 用户数据

public delegate void fStreamCallback(Int32 handle, IntPtr data, UInt32 size, IntPtr user, UInt32 dataType);

其调用方式与IPC_SDK中回调实时视频流的方式相同,都是调用播放库解码,然后返回角度信息和解码后的视频。详情参见IPC_SDK预览功能说明

示例如下:

private HikIVMS.fStreamCallback _callback;

private HikIVMS.DECCBFUN _displayCallback;//解码回调

private HikIVMS.ADDITIONDATACBFUN _additionDataDisplayCallback;//角度回调

if (_callback == null)

_callback = new HikIVMS.fStreamCallback(StreamCallback);

if (_displayCallback == null)

_displayCallback = new HikIVMS.DECCBFUN(DecCallbackFUN);

if (_additionDataDisplayCallback == null)

_additionDataDisplayCallback = new HikIVMS.ADDITIONDATACBFUN(AdditionDataCallBackFUN);

Hik.Play(cameraCode, _callback);

注意:Plat的回调函数与IPC回调函数中唯一的区别在于数据类型的定义不同,IPC的系统头数据类型为1,视频流数据类型为2;Plat系统头数据类型为0,视频流数据类型为1,仅此区别,其它相同。

这章会介绍GMAP.NET的核心功能之一:离线地图。这个功能可以满足很多政府项目、保密项目、或者由于种种原因不能上网的项目的需求。

本章主要分成三个方面介绍:演示、生成离线地图、Demo代码。

地图显示

地图缩放后还可以显示

网络是断了的

前面已经演示了Demo,这个部分说如何生成离线地图,也就是Data.gmdb。GMAP.NET提供了集中缓存方式,MySQL,SQLLite,MSSQL,Postgre等等,

默认是使用SQLLite的,这部分以后再分析。

1. 启动Demo.WindowsPresentation项目作为生成工具,这个项目在svn或者Download中都有。

2. 选择一个地图厂商(例如百度),并定位你的位置(例如重庆)

3. 按中Alt键并使用鼠标框选,会用蓝色区域表示,点击prefetch

4. 然后开始生成了,是否生成下一层会提示你。

5. 最后生成的文件放在C:\Users\用户名<你的计算机用户名>\AppData\Local\GMap.NET\TileDBv5\en,那个Data.gmdb就是你的地图文件

OK,整个离线地图就生成好了。

代码已经上传至http://code.google.com/p/ypmap/downloads/list:这里有点抱歉的地方,因为地图包实在太大了,上传又是龟速,因此没有上传地图包。

在运行Demo的时候,如果没有离线地图包会Crash掉,需要先生成离线地图包,并在App.Config中配置好,或者注释MapManagerLoader加载的代码。

先看下目录结构,主要包括下面几个文件:

引用GMAP.NET.Core.dll和GMAP.NET.WindowsPresentaion

**离线地图包maps\Data.gmdb:**比较大,大概有140MB的样子,还只是重庆渝中区 18~22层的地图。

配置文件App.Config: 主要是配置了地图启动的中心点,地图文件的路径

封装的地图显示控件代码GMapTrack.xaml和GMapTrack.xaml.cs:主要逻辑在里面

地图包加载代码MapManagerLoader:做了下简单封装

GMapTrack.xaml

主要包括了GMapControl这个控件,并且放了个进度条ProgressBar,是为了在加载图片的时候显示。

复制代码

<UserControl x:Class=”Demo.Offline.GMapTrack” xmlns=“http://schemas.microsoft.com/winfx/2006/xaml/presentation“ xmlns:x=“http://schemas.microsoft.com/winfx/2006/xaml“ xmlns:gmap=“clr-namespace:GMap.NET.WindowsPresentation;assembly=GMap.NET.WindowsPresentation”>

    <GroupBox Name="mapgroup" Margin="0,0,50,0"  VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"\>
        <gmap:GMapControl x:Name="MainMap"  MaxZoom="24" MinZoom="1"\>

        </gmap:GMapControl>
    </GroupBox>
    <GroupBox x:Name="progressBar" Header="正在载入" Height="50" HorizontalAlignment="Right" Margin="0,0,12,12"  VerticalAlignment="Bottom" Width="169"\>
        <Grid>
            <ProgressBar Margin="2"  IsIndeterminate="True" />
        </Grid>
    </GroupBox>
</Grid>

复制代码

GMapTrack.cs

这里是关键, 仔细看:

1.首先从配置文件中读出地图中心点

this.MainMap.Position = new PointLatLng(lat,lng);

2.设置地图的显示区域,因为离线地图肯定是某个区域的,其他地方也显示不出来。

this.MainMap.BoundsOfMap = new RectLatLng(lat+2, lng+2, 2.765142, 4.120995);

3.设置地图的加载模式:缓存,并且设置地图为百度地图

this.MainMap.Manager.Mode = AccessMode.CacheOnly;

this.MainMap.MapProvider = GMapProviders.BaiduMap;

4.从配置文件中读取地图

MapManagerLoader.Instance.Load(ConfigurationManager.AppSettings[“mapName”]);

复制代码

private void MainMap_Loaded(object sender, RoutedEventArgs e)
{ double lat = double.Parse(ConfigurationManager.AppSettings[“defaultLat”]); double lng = double.Parse(ConfigurationManager.AppSettings[“defaultLng”]); this.MainMap.Position = new PointLatLng(lat,lng); // this.MainMap.MapProvider.Area = new RectLatLng(30.981178, 105.351914, 2.765142, 4.120995);
this.MainMap.BoundsOfMap = new RectLatLng(lat+2, lng+2, 2.765142, 4.120995); this.MainMap.Manager.Mode = AccessMode.CacheOnly; this.MainMap.MapProvider = GMapProviders.BaiduMap; this.MainMap.DragButton = MouseButton.Left; this.MainMap.Zoom = 13; this.MainMap.MinZoom = 8; this.MainMap.MaxZoom = 24;

        MapManagerLoader.Instance.Load(ConfigurationManager.AppSettings\["mapName"\]);
    }

复制代码

 下面是GMapTrack.cs的完整代码:

View Code

using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Text; using System.Windows; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; using GMap.NET; using GMap.NET.MapProviders; using MouseEventArgs = System.Windows.Input.MouseEventArgs; using UserControl = System.Windows.Controls.UserControl; namespace Demo.Offline
{ ///


/// UserControl.xaml 的交互逻辑 ///

public partial class GMapTrack : UserControl
{ public GMapTrack()
{
InitializeComponent(); this.MainMap.MouseEnter += MainMap_MouseEnter; this.MainMap.OnTileLoadStart += MainMap_OnTileLoadStart; this.MainMap.OnTileLoadComplete += MainMap_OnTileLoadComplete; this.MainMap.Loaded += MainMap_Loaded;
} private void MainMap_Loaded(object sender, RoutedEventArgs e)
{ double lat = double.Parse(ConfigurationManager.AppSettings[“defaultLat”]); double lng = double.Parse(ConfigurationManager.AppSettings[“defaultLng”]); this.MainMap.Position = new PointLatLng(lat,lng); // this.MainMap.MapProvider.Area = new RectLatLng(30.981178, 105.351914, 2.765142, 4.120995);
this.MainMap.BoundsOfMap = new RectLatLng(lat+2, lng+2, 2.765142, 4.120995); this.MainMap.Manager.Mode = AccessMode.CacheOnly; this.MainMap.MapProvider = GMapProviders.BaiduMap; this.MainMap.DragButton = MouseButton.Left; this.MainMap.Zoom = 13; this.MainMap.MinZoom = 8; this.MainMap.MaxZoom = 24;

        MapManagerLoader.Instance.Load(ConfigurationManager.AppSettings\["mapName"\]);
    } private void MainMap\_OnTileLoadComplete(long elapsedmilliseconds)
    {
        MethodInvoker m \= delegate()
        {
            progressBar.Visibility \= Visibility.Hidden;
        }; try {
            Dispatcher.BeginInvoke(DispatcherPriority.Loaded, m);
        } catch (Exception ex)
        { //\_logger.Info(ex.Message);

}
} private void MainMap_OnTileLoadStart()
{
MethodInvoker m = delegate()
{
progressBar.Visibility = Visibility.Visible;
}; try {
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, m);
} catch (Exception ex)
{ //_logger.Info(ex.Message);
}
} private void MainMap_MouseEnter(object sender, MouseEventArgs e)
{ this.MainMap.Focus();
}
}
}

MapManagerLoader.cs

一个单例模式的地图加载器,主要看这个代码,启动了个线程,并且调用了GMaps.Instance.ImportFromGMDB函数

复制代码

public bool Load(string fileName)
{ if (!_isLoaded)
{ new Thread(() => GMaps.Instance.ImportFromGMDB(fileName)).Start();
_isLoaded = true;
} return _isLoaded;
}

复制代码

下面是MapManagerLoader.cs的完整代码:

View Code

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using GMap.NET; namespace Demo.Offline
{ public class MapManagerLoader
{ private static readonly MapManagerLoader _instance = new MapManagerLoader(); public static MapManagerLoader Instance
{ get { return _instance; }
} private MapManagerLoader()
{
} private bool _isLoaded; public bool Load(string fileName)
{ if (!_isLoaded)
{ new Thread(() => GMaps.Instance.ImportFromGMDB(fileName)).Start();
_isLoaded = true;
} return _isLoaded;
}
}
}

其他代码没什么好讲的了,有WPF或C#基础的都应该很快能理解

原文链接:http://www.cnblogs.com/enjoyeclipse/archive/2013/01/29/2882254.html

作为点石成金的开门篇-开源GIS二维研发

第一章:简介

开源GIS桌面应用程序uDig

**它是什么
**

1、uDig是一个开源桌面应用程序框架,构建在Eclipse RCP和GeoTools(一个开源的Java GIS工具包)上的桌面GIS(地理信息系统) ;

2、是一款开源桌面GIS软件,基于Java和Eclipse平台,可以进行shp格式地图文件的编辑和查看;

3、是一个开源空间数据查看器/编辑器,对OpenGIS标准,关于互联网GIS、网络地图服务器和网络功能服务器有特别的加强。uDig提供一个一般的java平台来用开源组件建设空间应用。

它可以做什么

1、可以编辑数据和提交数据。例如纠正一条道路的轨迹

2、可以创建、编辑地图,例如创建一个地图,自制道路、河流、湖泊

3、可进行SLD样式文件编辑,可设置是否显示标注信息

4、图层最大比例尺、最小比例尺设置

开源GIS服务器GeoServer

它是什么

1、GeoServer 是 OpenGIS Web 服务器规范的 J2EE 实现,利用 GeoServer 可以方便的发布地图数据,允许用户对特征数据进行更新、删除、插入操作,通过 GeoServer 可以比较容易的在用户之间迅速共享空间地理信息。

2、兼容 WMS 和 WFS 特性;支持 PostgreSQL、 Shapefile 、 ArcSDE 、 Oracle 、 VPF 、 MySQL 、 MapInfo ;支持上百种投影;

3、能够将网络地图输出为 jpeg 、 gif 、 png 、 SVG 、 KML 等格式;能够运行在任何基于 J2EE/Servlet 容器之上;

4、嵌入 MapBuilder 支持 AJAX 的地图客户端OpenLayers;除此之外还包括许多其他的特性。

它可以做什么

1、连接数据库,发布数据库中的数据作为图层,详见文章GeoServer系列之三.将postGis数据表发布成WMS/WFS服务

2、导入shp,发布shp文件为地图,详见文章GeoServer系列之基于Geoserver发布shp文件为地图

3、SQLViews查库发布(参数设置、调用存储过程函数、防止注入),详见文章GeoServer系列之SqlView

4、切片GeoServer系列之四.基于GeoServer切片地图服务的发布

5、Geoserver可以发布两类数据,分别为shp文件类型和postgis数据表类数据

开源GIS前端类库openlayers

它是什么

1、OpenLayers 是一个专为Web GIS 客户端开发提供的JavaScript 类库包,用于实现标准格式发布的地图数据访问

2、OpenLayers 支持Open GIS 协会制定的WMS(Web Mapping Service)和WFS(Web Feature Service)等网络服务规范,可以通过远程服务的方式,将以OGC 服务形式发布的地图数据加载到基于浏览器的OpenLayers 客户端中进行显示

3、OpenLayers采用面向对象方式开发,并使用来自Prototype.js和Rico中的一些组件。

可以做什么

  1、可以在浏览器中帮助开发者实现地图浏览的基本效果,比如放大(Zoom In)、缩小(Zoom Out)、平移(Pan)等常用操作之外,还可以进行选取面、选取线、要素选择、图层叠加等不同的操作

  2、可以对已有的OpenLayers 操作和数据支持类型进行扩充,为其赋予更多的功能。例如,它可以为OpenLayers 添加网络处理服务WPS 的操作接口,从而利用已有的空间分析处理服务来对加载的地理空间数据进行计算。

    openlayers实现wfs属性查询和空间查询

  3、同时,在OpenLayers提供的类库当中,它还使用了类库 Prototype.js 和Rico 中的部分组件,为地图浏览操作客户端增加Ajax 效果。

  4、WMTS的逐级无缝缩放

5、前端支持返回WKT、GeoJson等格式,WKT直接存储几何对象,GeoJson可用于存储带属性的对象;详见:OpenLayers系列之-Geometry格式转换

openlayers api http://openlayers.org/en/latest/apidoc/index.html

6、openlayers系列之基于openlayers实现聚类统计展示

Openlayers2中聚类的动态实现

7、卷帘Openlayers2卷帘功能的实现

    OL3实现多图联动

8、统计图Openlayers3中统计图的实现:结合highcharts实现统计图

    Openlayers2中统计图的实现

  9、热力图效果结合heatmap.js,在Openlayers中如何实现热力图

  10、A、B、C、D效果OL2中实现百度地图ABCD marker的效果

GeoWebCache

它是什么,它可以做什么

  geowebcache就相当于是openlayer和geoserver之间的中介,可提升地图访问效率

  GeoWebCache的配置与使用

GeoJson

它是什么

  1、GeoJSON是基于JavaScript 对象表示法的地理空间信息数据交换格式

  2、GeoJSON对象可以表示几何、特征或者特征集合。

  3、GeoJSON支持下面几何类型:点、线、面、多点、多线、多面和几何集合。

  4、GeoJSON里的特征包含一个几何对象和其他属性,特征集合表示一系列特征。

  5、GeoJSON 可以被 JavaScript 简单、快速的解析,得到了包括 FireEagleOpenLayers在内的一些流行的 API 的支持

  6、地图聚合的数据格式前三名:KML、GeoRSS 和 GeoJSON

  7、GeoJSON就是JSON格式,只不过是针对Geometry的JSON,遵循JSON的语法和格式,其解析方式和JSON完全相同。

可以做什么

  1、通过Openlayers展现后台服务提供的一个点的元素信息

  2、通过Openlayers展现后台服务提供的多个点的元素信息

  3、通过Openlayers展现后台服务提供的多个点的元素信息,并且根据状态改变成不同的图片信息

  资料地址:

  http://www.oschina.net/translate/geojson-spec#id2

  http://www.oschina.net/translate/geojson-spec#id3

  GeoJson在线工具地址:http://geojson.io/#map=3/44.53/111.80

  官网:http://geojson.org/

空间数据库

常见开源空间数据库:postgresql、postgis、mysql

  mysql:http://doc.mysql.cn/mysql5/refman-5.1-zh.html-chapter/spatial-extensions-in-mysql.html

性能篇

   (一)海量POI如何进行快速搜索,并进行排序包括点查询、多边形查询、中英文分词查询?

   场景:比如查找当前所在位置附近1000米的酒店,如下图

      

  方案一:基于Solr的空间搜索

  方案二:使用Lucene索引和检索POI数据

  (二)海量POI,ol3加载缓慢

  方案一:抽稀。每放大一级加一部分点 抽稀+图片生成

  方案二:后台绘制,前台显示 大量POI点展示的一种解决方案

  方案三:取当前区域,绘制当前区域的  其他地方显示的时候再绘制  大量POI点展示的一种解决方案

第二章:思路整理

1、传统线路

空间表和shp数据–》打开udig–》导入空间表数据或shp数据–》编辑数据和样式–》打开geoserver–》geoserver发布wms–》openlayers访问

2、制图线路

打开udig–》制图–》编辑数据和样式–》打开geoserver–》geoserver发布wms–》openlayers访问

3、geoserver发布postgis数据表

创建链接–》设置样式–》发布

4、geoserver只支持发布arcgis格式的地图数据

5、多图层叠加

    geoserver发布图层组、openlayers加载的时候叠加

6、比例尺与图层样式的关系

udig设置不同图层比例尺与样式关系–》geoserver发布wms–》标注名显示+不同比例尺下样式的使用

第三章:可以做的事

一、GIS桌面应用程序

  可以基于uDig开发自己的GIS桌面应用程序,类似Supermap的Idesktop产品****

二、可对Geoserver进行二次开发,调整为类似Supermap的Iserver产品

三、可将Geoserver作为war包或直接集成进项目进行开发,达到初步让地图使用者自己制作地图的目的,例如可以让使用者在面板进行坐标点绘制然后生成shp文件通过Geoserver发布为服务,然后采用openlayers进行调用,如下图

四、结合Sqlview+ssm框架进行空间分析

人在山中,才知道,白云也可以抓上一把,苍翠竟有清甜的味道。 人在山中,才知道,高度永远是一个变量,而快乐则是附于中跋涉过程的函数。 人在山中,才知道,庄严是望远时的一种心境,高处才能指点江山。

构建属于自己的ORM框架之二–IQueryable的奥秘 - Poiuyt_cyc - 博客园

Excerpt

上篇文章标题乱起,被吐槽了,这次学乖了。上篇文章中介绍了如何解析Expression生成对应的SQL语句,以及IQueryable的一些概念,以及我们所搭建的框架的思想等。但还没把它们结合并应用起来。这一篇文章将更黄更暴力,揭露IQueryable在实际使用中延迟加载的实现原理,结合上篇对Expre


上篇文章标题乱起,被吐槽了,这次学乖了。

上篇文章中介绍了如何解析Expression生成对应的SQL语句,以及IQueryable的一些概念,以及我们所搭建的框架的思想等。但还没把它们结合并应用起来。这一篇文章将更黄更暴力,揭露IQueryable在实际使用中延迟加载的实现原理,结合上篇对Expression的解析,我们来实现一个自己的“延迟加载”

如果还不太了解如何解析Expression和IQueryable的一些基本概念,可以先看看我的上篇文章

我们先来做些基本工作,定义一个IDataBase接口,里面可以定义些查询,删除,修改,新增等方法,为了节约时间,我们就定义一个查询和删除的方法,再定义一个获取IQueryable实例的方法

复制代码

1
2
3
4
5
6
   <span>public</span> <span>interface</span><span> IDataBase
{
List</span>&lt;T&gt; FindAs&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere);
</span><span>int</span> Remove&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere);
IQueryable</span>&lt;T&gt; Source&lt;T&gt;<span>();
}</span>

复制代码

再添加一个类DBSql,实现我们上面的IDataBase接口,这个类是负责提供对sql数据库的操作

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<span>public</span> <span>class</span><span> DBSql : IDataBase
{
</span><span>public</span> List&lt;T&gt; FindAs&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> <span>int</span> Remove&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> IQueryable&lt;T&gt; Source&lt;T&gt;<span>()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}</span>

复制代码

IQueryable

上篇文章中有个朋友的回复对IQueryable的解释十分到位,“IQueryable只存贮条件,不立即运行,从而可以实现延迟加载。”那它是如何存贮条件,如何延迟加载的?

这时我们为了提供 public IQueryable Source() 所需的对象。我们再来建一个SqlQuery类,实现IQueryable

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   <span>public</span> <span>class</span> SqlQuery&lt;T&gt; : IQueryable&lt;T&gt;<span>
{
</span><span>public</span> IEnumerator&lt;T&gt;<span> GetEnumerator()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span><span> Type ElementType
{
</span><span>get</span> { <span>throw</span> <span>new</span><span> NotImplementedException(); }
}

</span><span>public</span><span> Expression Expression
{
</span><span>get</span> { <span>throw</span> <span>new</span><span> NotImplementedException(); }
}

</span><span>public</span><span> IQueryProvider Provider
{
</span><span>get</span> { <span>throw</span> <span>new</span><span> NotImplementedException(); }
}
}</span>

复制代码

看到这里大家都不陌生吧?

GetEnumerator()是IEnumerable里的。有了它我们就能foreach了。有泛型和非泛型版本,所以有2个

Type提供访问当前对象的类型(反正由你定义。。。)

Expression是贮存查询条件的

IQueryProvider简单的翻译过来就是查询提供者,它是负责创建查询条件和执行查询的。我们写一个SqlProvider类来实现它

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<span>public</span> <span>class</span> SqlProvider&lt;T&gt;<span> : IQueryProvider
{

</span><span>public</span> IQueryable&lt;TElement&gt; CreateQuery&lt;TElement&gt;<span>(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span><span> IQueryable CreateQuery(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> TResult Execute&lt;TResult&gt;<span>(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> <span>object</span><span> Execute(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}</span>

复制代码

CreateQuery是创建查询条件。。

平时我们

IQueryable query=xxx源;

query=query.Where(x=>x.Name==”123”);

这时Where方法里做的其实就是将前面query的Expression属性和Where里的(x=>x.Name==”123”)相并,并且调用Provider属性里的CreateQuery方法。我们可以把我们的代码改成这样,来看看到底是不是这么回事。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<span>public</span> <span>class</span><span> DBSql : IDataBase
{
</span><span>public</span> IQueryable&lt;T&gt; Source&lt;T&gt;<span>()
{
</span><span>return</span> <span>new</span> SqlQuery&lt;T&gt;<span>();
}

</span><span>public</span> List&lt;T&gt; FindAs&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> <span>int</span> Remove&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}

</span><span>public</span> <span>class</span> SqlQuery&lt;T&gt; : IQueryable&lt;T&gt;<span>
{

</span><span>private</span><span> Expression _expression;
</span><span>private</span><span> IQueryProvider _provider;

</span><span>public</span><span> SqlQuery()
{
_provider </span>= <span>new</span> SqlProvider&lt;T&gt;<span>();
_expression </span>= Expression.Constant(<span>this</span><span>);
}

</span><span>public</span><span> SqlQuery(Expression expression, IQueryProvider provider)
{
_expression </span>=<span> expression;
_provider </span>=<span> provider;
}

</span><span>public</span> IEnumerator&lt;T&gt;<span> GetEnumerator()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span><span> Type ElementType
{
</span><span>get</span> { <span>return</span> <span>typeof</span>(SqlQuery&lt;T&gt;<span>); }
}

</span><span>public</span><span> Expression Expression
{
</span><span>get</span> { <span>return</span><span> _expression; }
}

</span><span>public</span><span> IQueryProvider Provider
{
</span><span>get</span> { <span>return</span><span> _provider; }
}
}

</span><span>public</span> <span>class</span> SqlProvider&lt;T&gt;<span> : IQueryProvider
{
</span><span>public</span> IQueryable&lt;TElement&gt; CreateQuery&lt;TElement&gt;<span>(Expression expression)
{
IQueryable</span>&lt;TElement&gt; query = <span>new</span> SqlQuery&lt;TElement&gt;(expression, <span>this</span><span>);
</span><span>return</span><span> query;
}

</span><span>public</span><span> IQueryable CreateQuery(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> TResult Execute&lt;TResult&gt;<span>(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> <span>object</span><span> Execute(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}</span>

复制代码

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     <span>public</span> <span>class</span><span> Staff
{
</span><span>public</span> <span>int</span> ID { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>string</span> Code { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>string</span> Name { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DateTime? Birthday { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>bool</span> Deletion { <span>get</span>; <span>set</span><span>; }
}

</span><span>static</span> <span>void</span> Main(<span>string</span><span>[] args)
{
IDataBase db </span>= <span>new</span><span> DBSql();
IQueryable</span>&lt;Staff&gt; query = db.Source&lt;Staff&gt;<span>();
</span><span>string</span> name = <span>"</span><span>张三</span><span>"</span><span>;
Expression express </span>= <span>null</span><span>;
query </span>= query.Where(x =&gt; x.Name == <span>"</span><span>赵建华</span><span>"</span><span>);
express </span>=<span> query.Expression;
query </span>= query.Where(x =&gt; x.Name ==<span> name);
express </span>=<span> query.Expression;
}</span>

复制代码

段点打在 

public IQueryable CreateQuery(Expression expression)

每次query.Where都会跑这里来。并且Expression都是前后相并的结果。

到了这一步,相信大家都明白了IQueryable只存贮条件这个概念了吧。

那延迟加载呢?什么时候加载啊!当我们foreach或者ToList/ToArray时啊。这时你想到了什么?GetEnumerator()。在调用GetEnumerator()时。我们再调用Provider里的Execute(Expression)。里面解析Expression,生成SQL语句,通过反射的方式生成实例,再一个个返回回去。完成!下面我直接给代码了。解析Expression的类我也改了,这个更黄更暴力。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
  <span>public</span> <span>class</span><span> ResolveExpression
{
</span><span>public</span> Dictionary&lt;<span>string</span>, <span>object</span>&gt;<span> Argument;
</span><span>public</span> <span>string</span><span> SqlWhere;
</span><span>public</span><span> SqlParameter[] Paras;
</span><span>private</span> <span>int</span> index = <span>0</span><span>;
</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 解析lamdba,生成Sql查询条件
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>///</span> <span>&lt;param name="expression"&gt;&lt;/param&gt;</span>
<span>///</span> <span>&lt;returns&gt;&lt;/returns&gt;</span>
<span>public</span> <span>void</span><span> ResolveToSql(Expression expression)
{
</span><span>this</span>.index = <span>0</span><span>;
</span><span>this</span>.Argument = <span>new</span> Dictionary&lt;<span>string</span>, <span>object</span>&gt;<span>();
</span><span>this</span>.SqlWhere =<span> Resolve(expression);
</span><span>this</span>.Paras = Argument.Select(x =&gt; <span>new</span><span> SqlParameter(x.Key, x.Value)).ToArray();
}

</span><span>private</span> <span>object</span><span> GetValue(Expression expression)
{
</span><span>if</span> (expression <span>is</span><span> ConstantExpression)
</span><span>return</span> (expression <span>as</span><span> ConstantExpression).Value;
</span><span>if</span> (expression <span>is</span><span> UnaryExpression)
{
UnaryExpression unary </span>= expression <span>as</span><span> UnaryExpression;
LambdaExpression lambda </span>=<span> Expression.Lambda(unary.Operand);
Delegate fn </span>=<span> lambda.Compile();
</span><span>return</span> fn.DynamicInvoke(<span>null</span><span>);
}
</span><span>if</span> (expression <span>is</span><span> MemberExpression)
{
MemberExpression member </span>= expression <span>as</span><span> MemberExpression;
</span><span>string</span> name =<span> member.Member.Name;
</span><span>var</span> constant = member.Expression <span>as</span><span> ConstantExpression;
</span><span>if</span> (constant == <span>null</span><span>)
</span><span>throw</span> <span>new</span> Exception(<span>"</span><span>取值时发生异常</span><span>"</span> +<span> member);
</span><span>return</span> constant.Value.GetType().GetFields().First(x =&gt; x.Name ==<span> name).GetValue(constant.Value);
}
</span><span>throw</span> <span>new</span> Exception(<span>"</span><span>无法获取值</span><span>"</span> +<span> expression);
}

</span><span>private</span> <span>string</span><span> Resolve(Expression expression)
{
</span><span>if</span> (expression <span>is</span><span> LambdaExpression)
{
LambdaExpression lambda </span>= expression <span>as</span><span> LambdaExpression;
expression </span>=<span> lambda.Body;
</span><span>return</span><span> Resolve(expression);
}
</span><span>if</span> (expression <span>is</span> BinaryExpression)<span>//</span><span>解析二元运算符</span>
<span> {
BinaryExpression binary </span>= expression <span>as</span><span> BinaryExpression;
</span><span>if</span> (binary.Left <span>is</span><span> MemberExpression)
{
</span><span>object</span> value =<span> GetValue(binary.Right);
</span><span>return</span><span> ResolveFunc(binary.Left, value, binary.NodeType);
}
</span><span>if</span> (binary.Left <span>is</span> MethodCallExpression &amp;&amp; (binary.Right <span>is</span> UnaryExpression || binary.Right <span>is</span><span> MemberExpression))
{
</span><span>object</span> value =<span> GetValue(binary.Right);
</span><span>return</span><span> ResolveLinqToObject(binary.Left, value, binary.NodeType);
}
}
</span><span>if</span> (expression <span>is</span> UnaryExpression)<span>//</span><span>解析一元运算符</span>
<span> {
UnaryExpression unary </span>= expression <span>as</span><span> UnaryExpression;
</span><span>if</span> (unary.Operand <span>is</span><span> MethodCallExpression)
{
</span><span>return</span> ResolveLinqToObject(unary.Operand, <span>false</span><span>);
}
</span><span>if</span> (unary.Operand <span>is</span><span> MemberExpression)
{
</span><span>return</span> ResolveFunc(unary.Operand, <span>false</span><span>, ExpressionType.Equal);
}
}
</span><span>if</span> (expression <span>is</span> MethodCallExpression)<span>//</span><span>解析扩展方法</span>
<span> {
</span><span>return</span> ResolveLinqToObject(expression, <span>true</span><span>);
}
</span><span>if</span> (expression <span>is</span> MemberExpression)<span>//</span><span>解析属性。。如x.Deletion</span>
<span> {
</span><span>return</span> ResolveFunc(expression, <span>true</span><span>, ExpressionType.Equal);
}
</span><span>var</span> body = expression <span>as</span><span> BinaryExpression;
</span><span>if</span> (body == <span>null</span><span>)
</span><span>throw</span> <span>new</span> Exception(<span>"</span><span>无法解析</span><span>"</span> +<span> expression);
</span><span>var</span> Operator =<span> GetOperator(body.NodeType);
</span><span>var</span> Left =<span> Resolve(body.Left);
</span><span>var</span> Right =<span> Resolve(body.Right);
</span><span>string</span> Result = <span>string</span>.Format(<span>"</span><span>({0} {1} {2})</span><span>"</span><span>, Left, Operator, Right);
</span><span>return</span><span> Result;
}

</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 根据条件生成对应的sql查询操作符
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>///</span> <span>&lt;param name="expressiontype"&gt;&lt;/param&gt;</span>
<span>///</span> <span>&lt;returns&gt;&lt;/returns&gt;</span>
<span>private</span> <span>string</span><span> GetOperator(ExpressionType expressiontype)
{
</span><span>switch</span><span> (expressiontype)
{
</span><span>case</span><span> ExpressionType.And:
</span><span>return</span> <span>"</span><span>and</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.AndAlso:
</span><span>return</span> <span>"</span><span>and</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.Or:
</span><span>return</span> <span>"</span><span>or</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.OrElse:
</span><span>return</span> <span>"</span><span>or</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.Equal:
</span><span>return</span> <span>"</span><span>=</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.NotEqual:
</span><span>return</span> <span>"</span><span>&lt;&gt;</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.LessThan:
</span><span>return</span> <span>"</span><span>&lt;</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.LessThanOrEqual:
</span><span>return</span> <span>"</span><span>&lt;=</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.GreaterThan:
</span><span>return</span> <span>"</span><span>&gt;</span><span>"</span><span>;
</span><span>case</span><span> ExpressionType.GreaterThanOrEqual:
</span><span>return</span> <span>"</span><span>&gt;=</span><span>"</span><span>;
</span><span>default</span><span>:
</span><span>throw</span> <span>new</span> Exception(<span>string</span>.Format(<span>"</span><span>不支持{0}此种运算符查找!</span><span>"</span> +<span> expressiontype));
}
}


</span><span>private</span> <span>string</span> ResolveFunc(Expression left, <span>object</span><span> value, ExpressionType expressiontype)
{
</span><span>string</span> Name = (left <span>as</span><span> MemberExpression).Member.Name;
</span><span>string</span> Operator =<span> GetOperator(expressiontype);
</span><span>string</span> Value =<span> value.ToString();
</span><span>string</span> CompName =<span> SetArgument(Name, Value);
</span><span>string</span> Result = <span>string</span>.Format(<span>"</span><span>({0} {1} {2})</span><span>"</span><span>, Name, Operator, CompName);
</span><span>return</span><span> Result;
}

</span><span>private</span> <span>string</span> ResolveLinqToObject(Expression expression, <span>object</span> value, ExpressionType? expressiontype = <span>null</span><span>)
{
</span><span>var</span> MethodCall = expression <span>as</span><span> MethodCallExpression;
</span><span>var</span> MethodName =<span> MethodCall.Method.Name;
</span><span>switch</span> (MethodName)<span>//</span><span>这里其实还可以改成反射调用,不用写switch</span>
<span> {
</span><span>case</span> <span>"</span><span>Contains</span><span>"</span><span>:
</span><span>if</span> (MethodCall.Object != <span>null</span><span>)
</span><span>return</span><span> Like(MethodCall);
</span><span>return</span><span> In(MethodCall, value);
</span><span>case</span> <span>"</span><span>Count</span><span>"</span><span>:
</span><span>return</span><span> Len(MethodCall, value, expressiontype.Value);
</span><span>case</span> <span>"</span><span>LongCount</span><span>"</span><span>:
</span><span>return</span><span> Len(MethodCall, value, expressiontype.Value);
</span><span>default</span><span>:
</span><span>throw</span> <span>new</span> Exception(<span>string</span>.Format(<span>"</span><span>不支持{0}方法的查找!</span><span>"</span><span>, MethodName));
}
}

</span><span>private</span> <span>string</span> SetArgument(<span>string</span> name, <span>string</span><span> value)
{
name </span>= <span>"</span><span>@</span><span>"</span> +<span> name;
</span><span>string</span> temp =<span> name;
</span><span>while</span><span> (Argument.ContainsKey(temp))
{
temp </span>= name +<span> index;
index </span>= index + <span>1</span><span>;
}
Argument[temp] </span>=<span> value;
</span><span>return</span><span> temp;
}

</span><span>private</span> <span>string</span> In(MethodCallExpression expression, <span>object</span><span> isTrue)
{
</span><span>var</span> Argument1 = expression.Arguments[<span>0</span><span>];
</span><span>var</span> Argument2 = expression.Arguments[<span>1</span>] <span>as</span><span> MemberExpression;
</span><span>var</span> fieldValue =<span> GetValue(Argument1);
</span><span>object</span>[] array = fieldValue <span>as</span> <span>object</span><span>[];
List</span>&lt;<span>string</span>&gt; SetInPara = <span>new</span> List&lt;<span>string</span>&gt;<span>();
</span><span>for</span> (<span>int</span> i = <span>0</span>; i &lt; array.Length; i++<span>)
{
</span><span>string</span> Name_para = <span>"</span><span>InParameter</span><span>"</span> +<span> i;
</span><span>string</span> Value =<span> array[i].ToString();
</span><span>string</span> Key =<span> SetArgument(Name_para, Value);
SetInPara.Add(Key);
}
</span><span>string</span> Name =<span> Argument2.Member.Name;
</span><span>string</span> Operator = Convert.ToBoolean(isTrue) ? <span>"</span><span>in</span><span>"</span> : <span>"</span><span> not in</span><span>"</span><span>;
</span><span>string</span> CompName = <span>string</span>.Join(<span>"</span><span>,</span><span>"</span><span>, SetInPara);
</span><span>string</span> Result = <span>string</span>.Format(<span>"</span><span>{0} {1} ({2})</span><span>"</span><span>, Name, Operator, CompName);
</span><span>return</span><span> Result;
}

</span><span>private</span> <span>string</span><span> Like(MethodCallExpression expression)
{
Expression argument </span>= expression.Arguments[<span>0</span><span>];
</span><span>object</span> Temp_Vale =<span> GetValue(argument);
</span><span>string</span> Value = <span>string</span>.Format(<span>"</span><span>%{0}%</span><span>"</span><span>, Temp_Vale);
</span><span>string</span> Name = (expression.Object <span>as</span><span> MemberExpression).Member.Name;
</span><span>string</span> CompName =<span> SetArgument(Name, Value);
</span><span>string</span> Result = <span>string</span>.Format(<span>"</span><span>{0} like {1}</span><span>"</span><span>, Name, CompName);
</span><span>return</span><span> Result;
}

</span><span>private</span> <span>string</span> Len(MethodCallExpression expression, <span>object</span><span> value, ExpressionType expressiontype)
{
</span><span>object</span> Name = (expression.Arguments[<span>0</span>] <span>as</span><span> MemberExpression).Member.Name;
</span><span>string</span> Operator =<span> GetOperator(expressiontype);
</span><span>string</span> CompName =<span> SetArgument(Name.ToString(), value.ToString());
</span><span>string</span> Result = <span>string</span>.Format(<span>"</span><span>len({0}){1}{2}</span><span>"</span><span>, Name, Operator, CompName);
</span><span>return</span><span> Result;
}

}</span>

复制代码

复制代码

1
2
3
4
5
6
<span>  public</span> <span>interface</span><span> IDataBase
{
List</span>&lt;T&gt; FindAs&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere);
</span><span>int</span> Remove&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere);
IQueryable</span>&lt;T&gt; Source&lt;T&gt;<span>();
}</span>

复制代码

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<span>namespace</span><span> Data.DataBase
{
</span><span>public</span> <span>class</span><span> DBSql : IDataBase
{
</span><span>private</span> <span>readonly</span> <span>static</span> <span>string</span> ConnectionString = <span>@"</span><span>Data Source=.;Initial Catalog=btmmcms-Standard;Persist Security Info=True;User ID=sa;Password=sa;</span><span>"</span><span>;

</span><span>public</span> IQueryable&lt;T&gt; Source&lt;T&gt;<span>()
{
</span><span>return</span> <span>new</span> SqlQuery&lt;T&gt;<span>();
}

</span><span>public</span> List&lt;T&gt; FindAs&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>using</span> (SqlConnection Conn = <span>new</span><span> SqlConnection(ConnectionString))
{
</span><span>using</span> (SqlCommand Command = <span>new</span><span> SqlCommand())
{
</span><span>try</span><span>
{
Command.Connection </span>=<span> Conn;
Conn.Open();
</span><span>string</span> sql = <span>string</span>.Format(<span>"</span><span>select * from {0}</span><span>"</span>, <span>typeof</span><span>(T).Name);
</span><span>if</span> (lambdawhere != <span>null</span><span>)
{
ResolveExpression resolve </span>= <span>new</span><span> ResolveExpression();
resolve.ResolveToSql(lambdawhere);
sql </span>= <span>string</span>.Format(<span>"</span><span>{0} where {1}</span><span>"</span><span>, sql, resolve.SqlWhere);
Command.Parameters.AddRange(resolve.Paras);
}
</span><span>//</span><span>为了测试,就在这里打印出sql语句了</span>
<span> Console.WriteLine(sql);
Command.CommandText </span>=<span> sql;
SqlDataReader dataReader </span>=<span> Command.ExecuteReader();
List</span>&lt;T&gt; ListEntity = <span>new</span> List&lt;T&gt;<span>();
</span><span>while</span><span> (dataReader.Read())
{
</span><span>var</span> constructor = <span>typeof</span>(T).GetConstructor(<span>new</span><span> Type[] { });
T Entity </span>= (T)constructor.Invoke(<span>null</span><span>);
</span><span>foreach</span> (<span>var</span> item <span>in</span><span> Entity.GetType().GetProperties())
{
</span><span>var</span> value =<span> dataReader[item.Name];
</span><span>if</span> (value == <span>null</span><span>)
</span><span>continue</span><span>;
</span><span>if</span> (value <span>is</span><span> DBNull)
value </span>= <span>null</span><span>;
item.SetValue(Entity, value, </span><span>null</span><span>);
}
ListEntity.Add(Entity);
}
</span><span>if</span> (ListEntity.Count == <span>0</span><span>)
</span><span>return</span> <span>null</span><span>;
</span><span>return</span><span> ListEntity;
}
</span><span>catch</span><span> (Exception ex)
{
</span><span>throw</span><span> ex;
}
</span><span>finally</span><span>
{
Conn.Close();
}
}
}
}

</span><span>public</span> <span>int</span> Remove&lt;T&gt;(Expression&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span> lambdawhere)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}

</span><span>public</span> <span>class</span> SqlQuery&lt;T&gt; : IQueryable&lt;T&gt;<span>
{

</span><span>private</span><span> Expression _expression;
</span><span>private</span><span> IQueryProvider _provider;

</span><span>public</span><span> SqlQuery()
{
_provider </span>= <span>new</span> SqlProvider&lt;T&gt;<span>();
_expression </span>= Expression.Constant(<span>this</span><span>);
}

</span><span>public</span><span> SqlQuery(Expression expression, IQueryProvider provider)
{
_expression </span>=<span> expression;
_provider </span>=<span> provider;
}

</span><span>public</span> IEnumerator&lt;T&gt;<span> GetEnumerator()
{
</span><span>var</span> result = _provider.Execute&lt;List&lt;T&gt;&gt;<span>(_expression);
</span><span>if</span> (result == <span>null</span><span>)
</span><span>yield</span> <span>break</span><span>;
</span><span>foreach</span> (<span>var</span> item <span>in</span><span> result)
{
</span><span>yield</span> <span>return</span><span> item;
}
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span><span> Type ElementType
{
</span><span>get</span> { <span>return</span> <span>typeof</span>(SqlQuery&lt;T&gt;<span>); }
}

</span><span>public</span><span> Expression Expression
{
</span><span>get</span> { <span>return</span><span> _expression; }
}

</span><span>public</span><span> IQueryProvider Provider
{
</span><span>get</span> { <span>return</span><span> _provider; }
}
}

</span><span>public</span> <span>class</span> SqlProvider&lt;T&gt;<span> : IQueryProvider
{

</span><span>public</span> IQueryable&lt;TElement&gt; CreateQuery&lt;TElement&gt;<span>(Expression expression)
{
IQueryable</span>&lt;TElement&gt; query = <span>new</span> SqlQuery&lt;TElement&gt;(expression, <span>this</span><span>);
</span><span>return</span><span> query;
}

</span><span>public</span><span> IQueryable CreateQuery(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}

</span><span>public</span> TResult Execute&lt;TResult&gt;<span>(Expression expression)
{
MethodCallExpression methodCall </span>= expression <span>as</span><span> MethodCallExpression;
Expression</span>&lt;Func&lt;T, <span>bool</span>&gt;&gt; result = <span>null</span><span>;
</span><span>while</span> (methodCall != <span>null</span><span>)
{
Expression method </span>= methodCall.Arguments[<span>0</span><span>];
Expression lambda </span>= methodCall.Arguments[<span>1</span><span>];
LambdaExpression right </span>= (lambda <span>as</span> UnaryExpression).Operand <span>as</span><span> LambdaExpression;
</span><span>if</span> (result == <span>null</span><span>)
{
result </span>= Expression.Lambda&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span>(right.Body, right.Parameters);
}
</span><span>else</span><span>
{
Expression left </span>= (result <span>as</span><span> LambdaExpression).Body;
Expression temp </span>=<span> Expression.And(right.Body, left);
result </span>= Expression.Lambda&lt;Func&lt;T, <span>bool</span>&gt;&gt;<span>(temp, result.Parameters);
}
methodCall </span>= method <span>as</span><span> MethodCallExpression;
}
</span><span>var</span> source = <span>new</span> DBSql().FindAs&lt;T&gt;<span>(result);
dynamic _temp </span>=<span> source;
TResult t </span>=<span> (TResult)_temp;
</span><span>return</span><span> t;
}

</span><span>public</span> <span>object</span><span> Execute(Expression expression)
{
</span><span>throw</span> <span>new</span><span> NotImplementedException();
}
}
}</span>

复制代码

搞定,这时可以改下数据库连接,连到自己的数据库,然后像下面这样,添加一个实体类(要与数据库表对应),就可以使用了

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<span>class</span><span> Program
{
</span><span>public</span> <span>class</span><span> Staff
{
</span><span>public</span> <span>int</span> ID { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>string</span> Code { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>string</span> Name { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> DateTime? Birthday { <span>get</span>; <span>set</span><span>; }
</span><span>public</span> <span>bool</span> Deletion { <span>get</span>; <span>set</span><span>; }
}

</span><span>static</span> <span>void</span> Main(<span>string</span><span>[] args)
{
IDataBase db </span>= <span>new</span><span> DBSql();
IQueryable</span>&lt;Staff&gt; query = db.Source&lt;Staff&gt;<span>();
query </span>= query.Where(x =&gt; x.Name == <span>"</span><span>张三</span><span>"</span><span>);
</span><span>foreach</span> (<span>var</span> item <span>in</span><span> query)
{

}
}
}</span>

复制代码

是不是很简单?

虽然信息量有点大,但慢慢理清并消化,我相信会对你又很大帮助!

根据twitter的snowflake算法生成唯一ID - 梁照彬 - 博客园

Excerpt

C#版本 JAVA版本


C#版本

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 根据twitter的snowflake算法生成唯一ID
</span><span>///</span><span> snowflake算法 64 位
</span><span>///</span><span> 0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
</span><span>///</span><span> 第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
</span><span>///</span><span> 其中datacenter标识位起始是机器位,机器ID其实是线程标识,可以同一一个10位来表示不同机器
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>public</span> <span>class</span><span> IdWorker
{
</span><span>//</span><span>机器ID</span>
<span>private</span> <span>static</span> <span>long</span> workerId = <span>1</span><span>;
</span><span>private</span> <span>static</span> <span>long</span> twepoch = <span>687888001020L</span>; <span>//</span><span>唯一时间,这是一个避免重复的随机量,自行设定不要大于当前时间戳</span>
<span>private</span> <span>static</span> <span>long</span> sequence = <span>0L</span><span>;
</span><span>private</span> <span>static</span> <span>int</span> workerIdBits = <span>4</span>; <span>//</span><span>机器码字节数。4个字节用来保存机器码</span>
<span>public</span> <span>static</span> <span>long</span> maxWorkerId = -<span>1L</span> ^ -<span>1L</span> &lt;&lt; workerIdBits; <span>//</span><span>最大机器ID</span>
<span>private</span> <span>static</span> <span>int</span> sequenceBits = <span>10</span>; <span>//</span><span>计数器字节数,10个字节用来保存计数码</span>
<span>private</span> <span>static</span> <span>int</span> workerIdShift = sequenceBits; <span>//</span><span>机器码数据左移位数,就是后面计数器占用的位数</span>
<span>private</span> <span>static</span> <span>int</span> timestampLeftShift = sequenceBits + workerIdBits; <span>//</span><span>时间戳左移动位数就是机器码和计数器总字节数</span>
<span>public</span> <span>static</span> <span>long</span> sequenceMask = -<span>1L</span> ^ -<span>1L</span> &lt;&lt; sequenceBits; <span>//</span><span>一微秒内可以产生计数,如果达到该值则等到下一微妙在进行生成</span>
<span>private</span> <span>long</span> lastTimestamp = -<span>1L</span><span>;

</span><span>public</span> <span>long</span><span> nextId()
{
</span><span>lock</span> (<span>this</span><span>)
{
</span><span>long</span> timestamp =<span> timeGen();
</span><span>if</span> (<span>this</span>.lastTimestamp ==<span> timestamp)
{ </span><span>//</span><span>同一微妙中生成ID</span>
IdWorker.sequence = (IdWorker.sequence + <span>1</span>) &amp; IdWorker.sequenceMask; <span>//</span><span>用&amp;运算计算该微秒内产生的计数是否已经到达上限</span>
<span>if</span> (IdWorker.sequence == <span>0</span><span>)
{
</span><span>//</span><span>一微妙内产生的ID计数已达上限,等待下一微妙</span>
timestamp = tillNextMillis(<span>this</span><span>.lastTimestamp);
}
}
</span><span>else</span><span>
{ </span><span>//</span><span>不同微秒生成ID</span>
IdWorker.sequence = <span>0</span>; <span>//</span><span>计数清0</span>
<span> }
</span><span>if</span> (timestamp &lt;<span> lastTimestamp)
{ </span><span>//</span><span>如果当前时间戳比上一次生成ID时时间戳还小,抛出异常,因为不能保证现在生成的ID之前没有生成过</span>
<span>throw</span> <span>new</span> Exception(<span>string</span>.Format(<span>"</span><span>Clock moved backwards. Refusing to generate id for {0} milliseconds</span><span>"</span><span>,
</span><span>this</span>.lastTimestamp -<span> timestamp));
}
</span><span>this</span>.lastTimestamp = timestamp; <span>//</span><span>把当前时间戳保存为最后生成ID的时间戳</span>
<span>long</span> nextId = (timestamp - twepoch &lt;&lt; timestampLeftShift) | IdWorker.workerId &lt;&lt; IdWorker.workerIdShift |<span> IdWorker.sequence;
</span><span>return</span><span> nextId;
}
}

</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 获取下一微秒时间戳
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>///</span> <span>&lt;param name="lastTimestamp"&gt;&lt;/param&gt;</span>
<span>///</span> <span>&lt;returns&gt;&lt;/returns&gt;</span>
<span>private</span> <span>long</span> tillNextMillis(<span>long</span><span> lastTimestamp)
{
</span><span>long</span> timestamp =<span> timeGen();
</span><span>while</span> (timestamp &lt;=<span> lastTimestamp)
{
timestamp </span>=<span> timeGen();
}
</span><span>return</span><span> timestamp;
}

</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 生成当前时间戳
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>///</span> <span>&lt;returns&gt;&lt;/returns&gt;</span>
<span>private</span> <span>long</span><span> timeGen()
{
</span><span>return</span> (<span>long</span>)(DateTime.UtcNow - <span>new</span> DateTime(<span>1970</span>, <span>1</span>, <span>1</span>, <span>0</span>, <span>0</span>, <span>0</span><span>, DateTimeKind.Utc)).TotalMilliseconds;
}
}</span>

复制代码

JAVA版本

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<span>import</span><span> org.slf4j.Logger;
</span><span>import</span><span> org.slf4j.LoggerFactory;
</span><span>import</span><span> org.springframework.stereotype.Component;
</span>
<span>public</span> <span>class</span><span> IdWorker {

</span><span>protected</span> <span>static</span> <span>final</span> Logger LOG = LoggerFactory.getLogger(IdWorker.<span>class</span><span>);

</span><span>private</span> <span>long</span><span> workerId;
</span><span>private</span> <span>long</span><span> datacenterId;
</span><span>private</span> <span>long</span> sequence = 0L<span>;

</span><span>private</span> <span>long</span> twepoch = 1288834974657L<span>;

</span><span>private</span> <span>long</span> workerIdBits = 5L<span>;
</span><span>private</span> <span>long</span> datacenterIdBits = 5L<span>;
</span><span>private</span> <span>long</span> maxWorkerId = -1L ^ (-1L &lt;&lt;<span> workerIdBits);
</span><span>private</span> <span>long</span> maxDatacenterId = -1L ^ (-1L &lt;&lt;<span> datacenterIdBits);
</span><span>private</span> <span>long</span> sequenceBits = 12L<span>;

</span><span>private</span> <span>long</span> workerIdShift =<span> sequenceBits;
</span><span>private</span> <span>long</span> datacenterIdShift = sequenceBits +<span> workerIdBits;
</span><span>private</span> <span>long</span> timestampLeftShift = sequenceBits + workerIdBits +<span> datacenterIdBits;
</span><span>private</span> <span>long</span> sequenceMask = -1L ^ (-1L &lt;&lt;<span> sequenceBits);

</span><span>private</span> <span>long</span> lastTimestamp = -1L<span>;

</span><span>public</span> IdWorker(<span>long</span> workerId, <span>long</span><span> datacenterId) {
</span><span>//</span><span> sanity check for workerId</span>
<span>if</span> (workerId &gt; maxWorkerId || workerId &lt; 0<span>) {
</span><span>throw</span> <span>new</span> IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0"<span>, maxWorkerId));
}
</span><span>if</span> (datacenterId &gt; maxDatacenterId || datacenterId &lt; 0<span>) {
</span><span>throw</span> <span>new</span> IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0"<span>, maxDatacenterId));
}
</span><span>this</span>.workerId =<span> workerId;
</span><span>this</span>.datacenterId =<span> datacenterId;
LOG.info(String.format(</span>"worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d"<span>, timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId));
}

</span><span>public</span> <span>synchronized</span> <span>long</span><span> nextId() {
</span><span>long</span> timestamp =<span> timeGen();

</span><span>if</span> (timestamp &lt;<span> lastTimestamp) {
LOG.error(String.format(</span>"clock is moving backwards. Rejecting requests until %d."<span>, lastTimestamp));
</span><span>throw</span> <span>new</span> RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp -<span> timestamp));
}

</span><span>if</span> (lastTimestamp ==<span> timestamp) {
sequence </span>= (sequence + 1) &amp;<span> sequenceMask;
</span><span>if</span> (sequence == 0<span>) {
timestamp </span>=<span> tilNextMillis(lastTimestamp);
}
} </span><span>else</span><span> {
sequence </span>= 0L<span>;
}

lastTimestamp </span>=<span> timestamp;

</span><span>return</span> ((timestamp - twepoch) &lt;&lt; timestampLeftShift) | (datacenterId &lt;&lt; datacenterIdShift) | (workerId &lt;&lt; workerIdShift) |<span> sequence;
}

</span><span>protected</span> <span>long</span> tilNextMillis(<span>long</span><span> lastTimestamp) {
</span><span>long</span> timestamp =<span> timeGen();
</span><span>while</span> (timestamp &lt;=<span> lastTimestamp) {
timestamp </span>=<span> timeGen();
}
</span><span>return</span><span> timestamp;
}

</span><span>protected</span> <span>long</span><span> timeGen() {
</span><span>return</span><span> System.currentTimeMillis();
}


}</span>

复制代码

流媒体服务器

流媒体解决方案 Live555(C++)

流媒体平台框架 EasyDarwin(GO,国产精品)

实时流媒体播放服务器程序DarwinStreamingSrvr(C++)

流媒体实时传输开发包 jrtplib

多媒体处理工具 ffmpeg

多媒体编码工具包Libav

Flash流媒体服务器 Red5(Java)

流媒体服务器 Open Streaming Server(Java)

FMS流媒体服务器(Adobe,收费的)

Wowza流媒体服务器(Java)

开源流媒体平台FreeCast(Java)

在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体。在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能问题。虽然在DB上可以做一些读写分离的设计,但在业务上如果在读写方面混合在一起的话,仍然会出现一些问题。

本文介绍了命令查询职责分离模式(Command Query Responsibility Segregation,CQRS),该模式从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为。从而使得逻辑更加清晰,便于对不同部分进行针对性的优化。文章首先简要介绍了传统的CRUD方式存在的问题,接着介绍了CQRS模式,最后以一个简单的在线日记系统演示了如何实现CQRS模式。要谈到读写操作,首先我们来看传统的CRUD的问题。

一 CRUD方式的问题

在以前的管理系统中,命令(Command,通常用来更新数据,操作DB)和查询(Query)通常使用的是在数据访问层中Repository中的实体对象(这些对象是对DB中表的映射),这些实体有可能是SQLServer中的一行数据或者多个表。

通常对DB执行的增,删,改,查(CRUD)都是针对的系统的实体对象。如通过数据访问层获取数据,然后通过数据传输对象DTO传给表现层。或者,用户需要更新数据,通过DTO对象将数据传给Model,然后通过数据访问层写回数据库,系统中的所有交互都是和数据查询和存储有关,可以认为是数据驱动(Data-Driven)的,如下图:

 Traditional CRUD Architecture

对于一些比较简单的系统,使用这种CRUD的设计方式能够满足要求。特别是通过一些代码生成工具及ORM等能够非常方便快速的实现功能。

但是传统的CRUD方法有一些问题

  • 使用同一个对象实体来进行数据库读写可能会太粗糙,大多数情况下,比如编辑的时候可能只需要更新个别字段,但是却需要将整个对象都穿进去,有些字段其实是不需要更新的。在查询的时候在表现层可能只需要个别字段,但是需要查询和返回整个实体对象。
  • 使用同一实体对象对同一数据进行读写操作的时候,可能会遇到资源竞争的情况,经常要处理的锁的问题,在写入数据的时候,需要加锁。读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并且会对系统吞吐量的增长会产生影响。
  • 同步的,直接与数据库进行交互在大数据量同时访问的情况下可能会影响性能和响应性,并且可能会产生性能瓶颈。
  • 由于同一实体对象都会在读写操作中用到,所以对于安全和权限的管理会变得比较复杂。

这里面很重要的一个问题是,系统中的读写频率比,是偏向读,还是偏向写,就如同一般的数据结构在查找和修改上时间复杂度不一样,在设计系统的结构时也需要考虑这样的问题。解决方法就是我们经常用到的对数据库进行读写分离。 让主数据库处理事务性的增,删,改操作(Insert,Update,Delete)操作,让从数据库处理查询操作(Select操作),数据库复制被用来将事务性操作导致的变更同步到集群中的从数据库。这只是从DB角度处理了读写分离,但是从业务或者系统上面读和写仍然是存放在一起的。他们都是用的同一个实体对象。

要从业务上将读和写分离,就是接下来要介绍的命令查询职责分离模式。

二 什么是CQRS

CQRS最早来自于Betrand Meyer(Eiffel语言之父,开-闭原则OCP提出者)在 Object-Oriented Software Construction 这本书中提到的一种 命令查询分离 (Command Query Separation,CQS) 的概念。其基本思想在于,任何一个对象的方法可以分为两大类:

  • 命令(Command):不返回任何结果(void),但会改变对象的状态。
  • 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

根据CQS的思想,任何一个方法都可以拆分为命令和查询两部分,比如:

1
private int i = 0;
1
2
3
4
5
private int Increase(int value)
{
i += value;
return i;
}

这个方法,我们执行了一个命令即对变量i进行相加,同时又执行了一个Query,即查询返回了i的值,如果按照CQS的思想,该方法可以拆成Command和Query两个方法,如下:

1
2
3
4
5
6
7
8
private void IncreaseCommand(int value)
{
i += value;
}
private int QueryValue()
{
return i;
}

操作和查询分离使得我们能够更好的把握对象的细节,能够更好的理解哪些操作会改变系统的状态。当然CQS也有一些缺点,比如代码需要处理多线程的情况。

CQRS是对CQS模式的进一步改进成的一种简单模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。“CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)”。

CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的。这样读和写逻辑就隔离开来了。

A basic CQRS architecture

使用CQRS分离了读写职责之后,可以对数据进行读写分离操作来改进性能,可扩展性和安全。如下图:

A CQRS architecture with separate read and write stores

主数据库处理CUD,从库处理R,从库的的结构可以和主库的结构完全一样,也可以不一样,从库主要用来进行只读的查询操作。在数量上从库的个数也可以根据查询的规模进行扩展,在业务逻辑上,也可以根据专题从主库中划分出不同的从库。从库也可以实现成ReportingDatabase,根据查询的业务需求,从主库中抽取一些必要的数据生成一系列查询报表来存储。

reportingDatabase

使用ReportingDatabase的一些优点通常可以使得查询变得更加简单高效:

  • ReportingDatabase的结构和数据表会针对常用的查询请求进行设计。
  • ReportingDatabase数据库通常会去正规化,存储一些冗余而减少必要的Join等联合查询操作,使得查询简化和高效,一些在主数据库中用不到的数据信息,在ReportingDatabase可以不用存储。
  • 可以对ReportingDatabase重构优化,而不用去改变操作数据库。
  • 对ReportingDatabase数据库的查询不会给操作数据库带来任何压力。
  • 可以针对不同的查询请求建立不同的ReportingDatabase库。

当然这也有一些缺点,比如从库数据的更新。如果使用SQLServer,本身也提供了一些如故障转移和复制机制来方便部署。

三 什么时候可以考虑CQRS

CQRS模式有一些优点:

  1. 分工明确,可以负责不同的部分
  2. 将业务上的命令和查询的职责分离能够提高系统的性能、可扩展性和安全性。并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对查询或者修改中的某一方进行改动,导致另一方出现问题的情况。
  3. 逻辑清晰,能够看到系统中的那些行为或者操作导致了系统的状态变化。
  4. 可以从数据驱动(Data-Driven) 转到任务驱动(Task-Driven)以及事件驱动(Event-Driven).

在下场景中,可以考虑使用CQRS模式:

  1. 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突
  2. 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致。
  3. 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况
  4. 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。
  5. 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统
  6. 需要和其他系统整合,特别是需要和事件溯源Event Sourcing进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

但是在以下场景中,可能不适宜使用CQRS:

  1. 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。
  2. 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。
  3. 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。

四 CQRS与Event Sourcing的关系

在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回。在操作(Command)方面,是通过发送Command实现,由CommandBus处理特定的Command,然后由Command将特定的Event发布到EventBus上,然后EventBus使用特定的Handler来处理事件,执行一些诸如,修改,删除,更新等操作。这里,所有与Command相关的操作都通过Event实现。这样我们可以通过记录Event来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing就是用来进行存储和管理事件的。这里不展开介绍。

五 CQRS的简单实现

CQRS模式在思想上比较简单,但是实现上还是有些复杂。它涉及到DDD,以及Event Sourcing,这里使用codeproject上的 Introduction to CQRS 这篇文章的例子来说明CQRS模式。这个例子是一个简单的在线记日志(Diary)系统,实现了日志的增删改查功能。整体结构如下:

CQRS

上图很清晰的说明了CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。

例子代码可以到codeproject上下载,整体结构如下:

DiaryCQRS project

由三个项目构成,Diary.CQRS包含了所有的Domain和消息对象。Configuration通过使用一个名为StructMap的IOC来初始化一些变量方便Web调用,Web是一个简单的MVC3项目,在Controller中有与CQRS交互的代码。

下面分别看Query和Command方面的实现:

Query方向的实现

查询方面很简单,日志列表和明细获取就是简单的查询。下面先看列表查询部分的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public ActionResult Index()
{
ViewBag.Model = ServiceLocator.ReportDatabase.GetItems();
return View();
}

public ActionResult Edit(Guid id)
{
var item = ServiceLocator.ReportDatabase.GetById(id);
var model = new DiaryItemDto()
{
Description = item.Description,
From = item.From,
Id = item.Id,
Title = item.Title,
To = item.To,
Version = item.Version
};
return View(model);
}

ReportDatabase的GetItems和GetById(id)方法就是简单的查询,从命名可以看出他是ReportDatabase。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReportDatabase : IReportDatabase
{
static List<DiaryItemDto> items = new List<DiaryItemDto>();

public DiaryItemDto GetById(Guid id)
{
return items.Where(a => a.Id == id).FirstOrDefault();
}

public void Add(DiaryItemDto item)
{
items.Add(item);
}

public void Delete(Guid id)
{
items.RemoveAll(i => i.Id == id);
}

public List<DiaryItemDto> GetItems()
{
return items;
}
}

ReportDataBase只是在内部维护了一个List的DiaryItemDto列表。在使用的时候,是通过IRepositoryDatabase对其进行操作的,这样便于mock代码。

Query方面的代码很简单。在实际的应用中,这一块就是直接对DB进行查询,然后通过DTO对象返回,这个DB可能是应对特定场景的报表数据库,这样可以提升查询性能。

下面来看Command方向的实现:

Command方向的实现

Command的实现比较复杂,下面以简单的创建一个新的日志来说明。

在MVC的Control中,可以看到Add的Controller中只调用了一句话:

1
2
3
4
5
6
7
[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));

return RedirectToAction("Index");
}

首先声明了一个CreateItemCommand,这个Command只是保存了一些必要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CreateItemCommand:Command
{
public string Title { get; internal set; }
public string Description { get;internal set; }
public DateTime From { get; internal set; }
public DateTime To { get; internal set; }

public CreateItemCommand(Guid aggregateId, string title,
string description,int version,DateTime from, DateTime to)
: base(aggregateId,version)
{
Title = title;
Description = description;
From = from;
To = to;
}
}

然后将Command发送到了CommandBus上,其实就是让CommandBus来选择合适的CommandHandler来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CommandBus:ICommandBus
{
private readonly ICommandHandlerFactory _commandHandlerFactory;

public CommandBus(ICommandHandlerFactory commandHandlerFactory)
{
_commandHandlerFactory = commandHandlerFactory;
}

public void Send<T>(T command) where T : Command
{
var handler = _commandHandlerFactory.GetHandler<T>();
if (handler != null)
{
handler.Execute(command);
}
else
{
throw new UnregisteredDomainCommandException("no handler registered");
}
}
}

这个里面需要值得注意的是CommandHandlerFactory这个类型的GetHandler方法,他接受一个类型为T的泛型,这里就是我们之前传入的CreateItemCommand。来看他的GetHandler方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StructureMapCommandHandlerFactory : ICommandHandlerFactory
{
public ICommandHandler<T> GetHandler<T>() where T : Command
{
var handlers = GetHandlerTypes<T>().ToList();

var cmdHandler = handlers.Select(handler =>
(ICommandHandler<T>)ObjectFactory.GetInstance(handler)).FirstOrDefault();

return cmdHandler;
}

private IEnumerable<Type> GetHandlerTypes<T>() where T : Command
{
var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes()
.Where(x => x.GetInterfaces()
.Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>) ))
.Where(h=>h.GetInterfaces()
.Any(ii=>ii.GetGenericArguments()
.Any(aa=>aa==typeof(T)))).ToList();


return handlers;
}

}

这里可以看到,他首先查找当前的程序集中(ICommandHandler)所在的程序集中的所有的实现了ICommandHandler的接口的类型,然后在所有的类型找查找实现了该泛型接口并且泛型的类型参数类型为T类型的所有类型。以上面的代码为例,就是要找出实现了ICommandHandler接口的类型。可以看到就是CreateItemCommandHandler类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
{
private IRepository<DiaryItem> _repository;

public CreateItemCommandHandler(IRepository<DiaryItem> repository)
{
_repository = repository;
}

public void Execute(CreateItemCommand command)
{
if (command == null)
{
throw new ArgumentNullException("command");
}
if (_repository == null)
{
throw new InvalidOperationException("Repository is not initialized.");
}
var aggregate = new DiaryItem(command.Id, command.Title, command.Description, command.From, command.To);
aggregate.Version = -1;
_repository.Save(aggregate, aggregate.Version);
}
}

找到之后然后使用IOC实例化了该对象返回。

现在CommandBus中,找到了处理特定Command的Handler。然后执行该类型的Execute方法。

可以看到在该类型中实例化了一个名为aggregate的DiaryItem对象。这个和我们之前查询所用到的DiaryItemDto有所不同,这个一个领域对象,里面包含了一系列事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class DiaryItem : AggregateRoot, 
IHandle<ItemCreatedEvent>,
IHandle<ItemRenamedEvent>,
IHandle<ItemFromChangedEvent>,
IHandle<ItemToChangedEvent>,
IHandle<ItemDescriptionChangedEvent>,
IOriginator
{
public string Title { get; set; }

public DateTime From { get; set; }
public DateTime To { get; set; }
public string Description { get; set; }

public DiaryItem()
{

}

public DiaryItem(Guid id,string title, string description, DateTime from, DateTime to)
{
ApplyChange(new ItemCreatedEvent(id, title,description, from, to));
}

public void ChangeTitle(string title)
{
ApplyChange(new ItemRenamedEvent(Id, title));
}

public void Handle(ItemCreatedEvent e)
{
Title = e.Title;
From = e.From;
To = e.To;
Id = e.AggregateId;
Description = e.Description;
Version = e.Version;
}

public void Handle(ItemRenamedEvent e)
{
Title = e.Title;
}
...
}

ItemCreatedEvent 事件的定义如下,其实就是用来存储传输过程中需要用到的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ItemCreatedEvent:Event
{
public string Title { get; internal set; }
public DateTime From { get; internal set; }
public DateTime To { get; internal set; }
public string Description { get;internal set; }

public ItemCreatedEvent(Guid aggregateId, string title ,
string description, DateTime from, DateTime to)
{
AggregateId = aggregateId;
Title = title;
From = from;
To = to;
Description = description;
}
}

可以看到在Domain对象中,除了定义基本的字段外,还定义了一些相应的事件,比如在构造函数中,实际上是发起了一个名为ItemCreateEvent的事件,同时还定义了处理时间的逻辑,这些逻辑都放在名为Handle的接口方法发,例如ItemCerateEvent的处理方法为Handle(ItemCreateEvent)方法。

ApplyChange方法在AggregateRoot对象中,他是聚集根,这是DDD中的概念。通过这个根可以串起所有对象。 该类实现了IEventProvider接口,他保存了所有在_changes中的所有没有提交的变更,其中的ApplyChange的用来为特定的Event查找Eventhandler的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public abstract class AggregateRoot : IEventProvider
{
private readonly List<Event> _changes;

public Guid Id { get; internal set; }
public int Version { get; internal set; }
public int EventVersion { get; protected set; }

protected AggregateRoot()
{
_changes = new List<Event>();
}

public IEnumerable<Event> GetUncommittedChanges()
{
return _changes;
}

public void MarkChangesAsCommitted()
{
_changes.Clear();
}

public void LoadsFromHistory(IEnumerable<Event> history)
{
foreach (var e in history) ApplyChange(e, false);
Version = history.Last().Version;
EventVersion = Version;
}

protected void ApplyChange(Event @event)
{
ApplyChange(@event, true);
}

private void ApplyChange(Event @event, bool isNew)
{
dynamic d = this;

d.Handle(Converter.ChangeTo(@event, @event.GetType()));
if (isNew)
{
_changes.Add(@event);
}
}
}

在ApplyChange的实现中,this其实就是对应的实现了AggregateRoot的DiaryItem的Domain对象,调用的Handle方法就是我们之前在DiaryItem中定义的行为。然后将该event保存在内部的未提交的事件列表中。相关的信息及事件都保存在了定义的aggregate对象中并返回。

然后Command继续执行,然后调用了_repository.Save(aggregate, aggregate.Version);这个方法。先看这个Repository对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Repository<T> : IRepository<T> where T : AggregateRoot, new()
{
private readonly IEventStorage _storage;
private static object _lockStorage = new object();

public Repository(IEventStorage storage)
{
_storage = storage;
}

public void Save(AggregateRoot aggregate, int expectedVersion)
{
if (aggregate.GetUncommittedChanges().Any())
{
lock (_lockStorage)
{
var item = new T();

if (expectedVersion != -1)
{
item = GetById(aggregate.Id);
if (item.Version != expectedVersion)
{
throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified",
item.Id));
}
}

_storage.Save(aggregate);
}
}
}

public T GetById(Guid id)
{
IEnumerable<Event> events;
var memento = _storage.GetMemento<BaseMemento>(id);
if (memento != null)
{
events = _storage.GetEvents(id).Where(e=>e.Version>=memento.Version);
}
else
{
events = _storage.GetEvents(id);
}
var obj = new T();
if(memento!=null)
((IOriginator)obj).SetMemento(memento);

obj.LoadsFromHistory(events);
return obj;
}
}

这个方法主要是用来对事件进行持久化的。 所有的聚合的变动都会存在该Repository中,首先,检查当前的聚合是否和之前存储在storage中的聚合一致,如果不一致,则表示对象在其他地方被更改过,抛出ConcurrencyException,否则将该变动保存在Event Storage中。

IEventStorage用来存储所有的事件,其实现类型为InMemoryEventStorage。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class InMemoryEventStorage:IEventStorage
{
private List<Event> _events;
private List<BaseMemento> _mementos;

private readonly IEventBus _eventBus;

public InMemoryEventStorage(IEventBus eventBus)
{
_events = new List<Event>();
_mementos = new List<BaseMemento>();
_eventBus = eventBus;
}

public IEnumerable<Event> GetEvents(Guid aggregateId)
{
var events = _events.Where(p => p.AggregateId == aggregateId).Select(p => p);
if (events.Count() == 0)
{
throw new AggregateNotFoundException(string.Format("Aggregate with Id: {0} was not found", aggregateId));
}
return events;
}

public void Save(AggregateRoot aggregate)
{
var uncommittedChanges = aggregate.GetUncommittedChanges();
var version = aggregate.Version;

foreach (var @event in uncommittedChanges)
{
version++;
if (version > 2)
{
if (version % 3 == 0)
{
var originator = (IOriginator)aggregate;
var memento = originator.GetMemento();
memento.Version = version;
SaveMemento(memento);
}
}
@event.Version=version;
_events.Add(@event);
}
foreach (var @event in uncommittedChanges)
{
var desEvent = Converter.ChangeTo(@event, @event.GetType());
_eventBus.Publish(desEvent);
}
}

public T GetMemento<T>(Guid aggregateId) where T : BaseMemento
{
var memento = _mementos.Where(m => m.Id == aggregateId).Select(m=>m).LastOrDefault();
if (memento != null)
return (T) memento;
return null;
}

public void SaveMemento(BaseMemento memento)
{
_mementos.Add(memento);
}
}

在GetEvent方法中,会找到所有的聚合根Id相关的事件。在Save方法中,将所有的事件保存在内存中,然后每隔三个事件建立一个快照。可以看到这里面使用了备忘录模式。

然后在foreach循环中,对于所有的没有提交的变更,EventBus将该事件发布出去。

现在,所有的发生变更的事件已经记录下来了。事件已经被发布到EventBus上,然后对应的EventHandler再处理对应的事件,然后与DB交互。现在来看EventBus的Publish方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EventBus:IEventBus
{
private IEventHandlerFactory _eventHandlerFactory;

public EventBus(IEventHandlerFactory eventHandlerFactory)
{
_eventHandlerFactory = eventHandlerFactory;
}

public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();
foreach (var eventHandler in handlers)
{
eventHandler.Handle(@event);
}
}
}

可以看到EventBus的Publish和CommandBus中的Send方法很相似,都是首先通过EventHandlerFactory查找对应Event的Handler,然后调用其Handler方法。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StructureMapEventHandlerFactory : IEventHandlerFactory
{
public IEnumerable<IEventHandler<T>> GetHandlers<T>() where T : Event
{
var handlers = GetHandlerType<T>();

var lstHandlers = handlers.Select(handler => (IEventHandler<T>) ObjectFactory.GetInstance(handler)).ToList();
return lstHandlers;
}

private static IEnumerable<Type> GetHandlerType<T>() where T : Event
{

var handlers = typeof(IEventHandler<>).Assembly.GetExportedTypes()
.Where(x => x.GetInterfaces()
.Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(IEventHandler<>)))
.Where(h => h.GetInterfaces()
.Any(ii => ii.GetGenericArguments()
.Any(aa => aa == typeof(T))))
.ToList();
return handlers;
}
}

然后返回并实例化了ItemCreatedEventHandler 对象,该对象的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ItemCreatedEventHandler : IEventHandler<ItemCreatedEvent>
{
private readonly IReportDatabase _reportDatabase;
public ItemCreatedEventHandler(IReportDatabase reportDatabase)
{
_reportDatabase = reportDatabase;
}
public void Handle(ItemCreatedEvent handle)
{
DiaryItemDto item = new DiaryItemDto()
{
Id = handle.AggregateId,
Description = handle.Description,
From = handle.From,
Title = handle.Title,
To=handle.To,
Version = handle.Version
};

_reportDatabase.Add(item);
}
}

可以看到在Handler方法中,从事件中获取参数,然后新建DTO对象,然后将该对象更新到DB中。

到此,整个Command执行完成。

六 结语

CQRS是一种思想很简单清晰的设计模式,他通过在业务上分离操作和查询来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在CQRS中,所有的涉及到对DB的操作都是通过发送Command,然后特定的Command触发对应事件来完成操作,这个过程是异步的,并且所有涉及到对系统的变更行为都包含在具体的事件中,结合Eventing Source模式,可以记录下所有的事件,而不是以往的某一点的数据信息,这些信息可以作为系统的操作日志,可以来对系统进行回退或者重放。

CQRS 模式在实现上有些复杂,很多地方比如AggregationRoot、Domain Object都涉及到DDD中的相关概念,本人对DDD不太懂。这里仅为了演示CQRS模式,所以使用的例子是codeproject上的,末尾列出了一些参考文章,如果您想了解更多,可以有针对性的阅读。

最后,希望CQRS模式能让您在设计高性能,可扩展性的程序时能够多一种选择和考虑。

七 参考文献

  1. Introduction to CQRS http://www.codeproject.com/Articles/555855/Introduction-to-CQRS
  2. CQRS http://martinfowler.com/bliki/CQRS.html
  3. CQRS Journey http://msdn.microsoft.com/en-us/library/jj554200.aspx
  4. Command and Query Responsibility Segregation (CQRS) Pattern http://msdn.microsoft.com/en-us/library/dn568103.aspx
  5. EntityFramework之领域驱动设计实践:CQRS体系结构模式 http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html
  6. Event Sourcing Pattern http://msdn.microsoft.com/en-us/library/dn589792.aspx

引用:http://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html

浅谈命令查询职责分离(CQRS)模式_command query responsibility segregation-CSDN博客

Excerpt

文章浏览阅读469次。在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体。在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能问题。虽然在DB上可以做一些读写分离的设计,但在业务上如果在读写方面混合在一起的话,仍然会出现一些问题。本文介绍了命令查询职责分离模式(Command Query Responsibility S…_command query responsibility segregation


在常用的三层架构中,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体。在一些业务逻辑简单的系统中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能问题。虽然在DB上可以做一些读写分离的设计,但在业务上如果在读写方面混合在一起的话,仍然会出现一些问题。

本文介绍了命令查询职责分离模式(Command Query Responsibility Segregation,CQRS),该模式从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为。从而使得逻辑更加清晰,便于对不同部分进行针对性的优化。文章首先简要介绍了传统的CRUD方式存在的问题,接着介绍了CQRS模式,最后以一个简单的在线日记系统演示了如何实现CQRS模式。要谈到读写操作,首先我们来看传统的CRUD的问题。

一 CRUD方式的问题

在以前的管理系统中,命令(Command,通常用来更新数据,操作DB)和查询(Query)通常使用的是在数据访问层中Repository中的实体对象(这些对象是对DB中表的映射),这些实体有可能是SQLServer中的一行数据或者多个表。

通常对DB执行的增,删,改,查(CRUD)都是针对的系统的实体对象。如通过数据访问层获取数据,然后通过数据传输对象DTO传给表现层。或者,用户需要更新数据,通过DTO对象将数据传给Model,然后通过数据访问层写回数据库,系统中的所有交互都是和数据查询和存储有关,可以认为是数据驱动(Data-Driven)的,如下图:

 

对于一些比较简单的系统,使用这种CRUD的设计方式能够满足要求。特别是通过一些代码生成工具及ORM等能够非常方便快速的实现功能。

但是传统的CRUD方法有一些问题

  • 使用同一个对象实体来进行数据库读写可能会太粗糙,大多数情况下,比如编辑的时候可能只需要更新个别字段,但是却需要将整个对象都穿进去,有些字段其实是不需要更新的。在查询的时候在表现层可能只需要个别字段,但是需要查询和返回整个实体对象。
  • 使用同一实体对象对同一数据进行读写操作的时候,可能会遇到资源竞争的情况,经常要处理的锁的问题,在写入数据的时候,需要加锁。读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并且会对系统吞吐量的增长会产生影响。
  • 同步的,直接与数据库进行交互在大数据量同时访问的情况下可能会影响性能和响应性,并且可能会产生性能瓶颈。
  • 由于同一实体对象都会在读写操作中用到,所以对于安全和权限的管理会变得比较复杂。

这里面很重要的一个问题是,系统中的读写频率比,是偏向读,还是偏向写,就如同一般的数据结构在查找和修改上时间复杂度不一样,在设计系统的结构时也需要考虑这样的问题。解决方法就是我们经常用到的对数据库进行读写分离。 让主数据库处理事务性的增,删,改操作(Insert,Update,Delete)操作,让从数据库处理查询操作(Select操作),数据库复制被用来将事务性操作导致的变更同步到集群中的从数据库。这只是从DB角度处理了读写分离,但是从业务或者系统上面读和写仍然是存放在一起的。他们都是用的同一个实体对象。

CQRS与读写分离有什么区别

  • 读写分离,是从DB角度,但业务逻辑上读和写依然混在一起,他们操作的是同一个实体对象
  • CQRS是把CRUD系统拆分为2部分:Command and Query,这2部分分别采用不同的架构设计
  • CQRS可以让我们分别针对读、写场景各自建模

要从业务上将读和写分离,就是接下来要介绍的命令查询职责分离模式。

二 什么是CQRS

CQRS最早来自于Betrand Meyer(Eiffel语言之父,开-闭原则OCP提出者)在 Object-Oriented Software Construction 这本书中提到的一种 命令查询分离 (Command Query Separation,CQS) 的概念。其基本思想在于,任何一个对象的方法可以分为两大类:

  • 命令(Command):不返回任何结果(void),但会改变对象的状态。
  • 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

根据CQS的思想,任何一个方法都可以拆分为命令和查询两部分,比如:

1
private int i = 0;
1
2
3
4
5
6
7
8
9
private int Increase(int value)

{

i += value;

return i;

}

这个方法,我们执行了一个命令即对变量i进行相加,同时又执行了一个Query,即查询返回了i的值,如果按照CQS的思想,该方法可以拆成Command和Query两个方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void IncreaseCommand(int value)

{

i += value;

}

private int QueryValue()

{

return i;

}

操作和查询分离使得我们能够更好的把握对象的细节,能够更好的理解哪些操作会改变系统的状态。当然CQS也有一些缺点,比如代码需要处理多线程的情况。

CQRS是对CQS模式的进一步改进成的一种简单模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。“CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)”。

CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的。这样读和写逻辑就隔离开来了。

使用CQRS分离了读写职责之后,可以对数据进行读写分离操作来改进性能,可扩展性和安全。如下图:

主数据库处理CUD,从库处理R,从库的的结构可以和主库的结构完全一样,也可以不一样,从库主要用来进行只读的查询操作。在数量上从库的个数也可以根据查询的规模进行扩展,在业务逻辑上,也可以根据专题从主库中划分出不同的从库。从库也可以实现成ReportingDatabase,根据查询的业务需求,从主库中抽取一些必要的数据生成一系列查询报表来存储。

使用ReportingDatabase的一些优点通常可以使得查询变得更加简单高效:

  • ReportingDatabase的结构和数据表会针对常用的查询请求进行设计。
  • ReportingDatabase数据库通常会去正规化,存储一些冗余而减少必要的Join等联合查询操作,使得查询简化和高效,一些在主数据库中用不到的数据信息,在ReportingDatabase可以不用存储。
  • 可以对ReportingDatabase重构优化,而不用去改变操作数据库。
  • 对ReportingDatabase数据库的查询不会给操作数据库带来任何压力。
  • 可以针对不同的查询请求建立不同的ReportingDatabase库。

当然这也有一些缺点,比如从库数据的更新。如果使用SQLServer,本身也提供了一些如故障转移和复制机制来方便部署。

三 什么时候可以考虑CQRS

CQRS模式有一些优点:

  1. 分工明确,可以负责不同的部分
  2. 将业务上的命令和查询的职责分离能够提高系统的性能、可扩展性和安全性。并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对查询或者修改中的某一方进行改动,导致另一方出现问题的情况。
  3. 逻辑清晰,能够看到系统中的那些行为或者操作导致了系统的状态变化。
  4. 可以从数据驱动(Data-Driven) 转到任务驱动(Task-Driven)以及事件驱动(Event-Driven).

在下场景中,可以考虑使用CQRS模式:

  1. 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突
  2. 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致。
  3. 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况
  4. 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。
  5. 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统
  6. 需要和其他系统整合,特别是需要和事件溯源Event Sourcing进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。

但是在以下场景中,可能不适宜使用CQRS:

  1. 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。
  2. 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。
  3. 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。

四 CQRS与Event Sourcing的关系

在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回。在操作(Command)方面,是通过发送Command实现,由CommandBus处理特定的Command,然后由Command将特定的Event发布到EventBus上,然后EventBus使用特定的Handler来处理事件,执行一些诸如,修改,删除,更新等操作。这里,所有与Command相关的操作都通过Event实现。这样我们可以通过记录Event来记录系统的运行历史记录,并且能够方便的回滚到某一历史状态。Event Sourcing就是用来进行存储和管理事件的。这里不展开介绍。

五 CQRS的简单实现

CQRS模式在思想上比较简单,但是实现上还是有些复杂。它涉及到DDD,以及Event Sourcing,这里使用codeproject上的 Introduction to CQRS 这篇文章的例子来说明CQRS模式。这个例子是一个简单的在线记日志(Diary)系统,实现了日志的增删改查功能。整体结构如下:

上图很清晰的说明了CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。

例子代码可以到codeproject上下载,整体结构如下:

DiaryCQRS project

由三个项目构成,Diary.CQRS包含了所有的Domain和消息对象。Configuration通过使用一个名为StructMap的IOC来初始化一些变量方便Web调用,Web是一个简单的MVC3项目,在Controller中有与CQRS交互的代码。

下面分别看Query和Command方面的实现:

Query方向的实现

查询方面很简单,日志列表和明细获取就是简单的查询。下面先看列表查询部分的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public ActionResult Index()

{

ViewBag.Model = ServiceLocator.ReportDatabase.GetItems();

return View();

}

public ActionResult Edit(Guid id)

{

var item = ServiceLocator.ReportDatabase.GetById(id);

var model = new DiaryItemDto()

{

Description = item.Description,

From = item.From,

Id = item.Id,

Title = item.Title,

To = item.To,

Version = item.Version

};

return View(model);

}

ReportDatabase的GetItems和GetById(id)方法就是简单的查询,从命名可以看出他是ReportDatabase。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ReportDatabase : IReportDatabase

{

static List<DiaryItemDto> items = new List<DiaryItemDto>();

public DiaryItemDto GetById(Guid id)

{

return items.Where(a => a.Id == id).FirstOrDefault();

}

public void Add(DiaryItemDto item)

{

items.Add(item);

}

public void Delete(Guid id)

{

items.RemoveAll(i => i.Id == id);

}

public List<DiaryItemDto> GetItems()

{

return items;

}

}

ReportDataBase只是在内部维护了一个List的DiaryItemDto列表。在使用的时候,是通过IRepositoryDatabase对其进行操作的,这样便于mock代码。

Query方面的代码很简单。在实际的应用中,这一块就是直接对DB进行查询,然后通过DTO对象返回,这个DB可能是应对特定场景的报表数据库,这样可以提升查询性能。

下面来看Command方向的实现:

Command方向的实现

Command的实现比较复杂,下面以简单的创建一个新的日志来说明。

在MVC的Control中,可以看到Add的Controller中只调用了一句话:

1
2
3
4
5
6
7
8
9
10
11
[HttpPost]

public ActionResult Add(DiaryItemDto item)

{

ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));

return RedirectToAction("Index");

}

首先声明了一个CreateItemCommand,这个Command只是保存了一些必要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CreateItemCommand:Command

{

public string Title { get; internal set; }

public string Description { get;internal set; }

public DateTime From { get; internal set; }

public DateTime To { get; internal set; }

public CreateItemCommand(Guid aggregateId, string title,

string description,int version,DateTime from, DateTime to)

: base(aggregateId,version)

{

Title = title;

Description = description;

From = from;

To = to;

}

}

然后将Command发送到了CommandBus上,其实就是让CommandBus来选择合适的CommandHandler来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CommandBus:ICommandBus

{

private readonly ICommandHandlerFactory _commandHandlerFactory;

public CommandBus(ICommandHandlerFactory commandHandlerFactory)

{

_commandHandlerFactory = commandHandlerFactory;

}

public void Send<T>(T command) where T : Command

{

var handler = _commandHandlerFactory.GetHandler<T>();

if (handler != null)

{

handler.Execute(command);

}

else

{

throw new UnregisteredDomainCommandException("no handler registered");

}

}

}

这个里面需要值得注意的是CommandHandlerFactory这个类型的GetHandler方法,他接受一个类型为T的泛型,这里就是我们之前传入的CreateItemCommand。来看他的GetHandler方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class StructureMapCommandHandlerFactory : ICommandHandlerFactory

{

public ICommandHandler<T> GetHandler<T>() where T : Command

{

var handlers = GetHandlerTypes<T>().ToList();

var cmdHandler = handlers.Select(handler =>

(ICommandHandler<T>)ObjectFactory.GetInstance(handler)).FirstOrDefault();

return cmdHandler;

}

private IEnumerable<Type> GetHandlerTypes<T>() where T : Command

{

var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes()

.Where(x => x.GetInterfaces()

.Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>) ))

.Where(h=>h.GetInterfaces()

.Any(ii=>ii.GetGenericArguments()

.Any(aa=>aa==typeof(T)))).ToList();

return handlers;

}

}

这里可以看到,他首先查找当前的程序集中(ICommandHandler)所在的程序集中的所有的实现了ICommandHandler的接口的类型,然后在所有的类型找查找实现了该泛型接口并且泛型的类型参数类型为T类型的所有类型。以上面的代码为例,就是要找出实现了ICommandHandler接口的类型。可以看到就是CreateItemCommandHandler类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>

{

private IRepository<DiaryItem> _repository;

public CreateItemCommandHandler(IRepository<DiaryItem> repository)

{

_repository = repository;

}

public void Execute(CreateItemCommand command)

{

if (command == null)

{

throw new ArgumentNullException("command");

}

if (_repository == null)

{

throw new InvalidOperationException("Repository is not initialized.");

}

var aggregate = new DiaryItem(command.Id, command.Title, command.Description, command.From, command.To);

aggregate.Version = -1;

_repository.Save(aggregate, aggregate.Version);

}

}

找到之后然后使用IOC实例化了该对象返回。

现在CommandBus中,找到了处理特定Command的Handler。然后执行该类型的Execute方法。

可以看到在该类型中实例化了一个名为aggregate的DiaryItem对象。这个和我们之前查询所用到的DiaryItemDto有所不同,这个一个领域对象,里面包含了一系列事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class DiaryItem : AggregateRoot,

IHandle<ItemCreatedEvent>,

IHandle<ItemRenamedEvent>,

IHandle<ItemFromChangedEvent>,

IHandle<ItemToChangedEvent>,

IHandle<ItemDescriptionChangedEvent>,

IOriginator

{

public string Title { get; set; }

public DateTime From { get; set; }

public DateTime To { get; set; }

public string Description { get; set; }

public DiaryItem()

{

}

public DiaryItem(Guid id,string title, string description, DateTime from, DateTime to)

{

ApplyChange(new ItemCreatedEvent(id, title,description, from, to));

}

public void ChangeTitle(string title)

{

ApplyChange(new ItemRenamedEvent(Id, title));

}

public void Handle(ItemCreatedEvent e)

{

Title = e.Title;

From = e.From;

To = e.To;

Id = e.AggregateId;

Description = e.Description;

Version = e.Version;

}

public void Handle(ItemRenamedEvent e)

{

Title = e.Title;

}

...

}

ItemCreatedEvent 事件的定义如下,其实就是用来存储传输过程中需要用到的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ItemCreatedEvent:Event

{

public string Title { get; internal set; }

public DateTime From { get; internal set; }

public DateTime To { get; internal set; }

public string Description { get;internal set; }

public ItemCreatedEvent(Guid aggregateId, string title ,

string description, DateTime from, DateTime to)

{

AggregateId = aggregateId;

Title = title;

From = from;

To = to;

Description = description;

}

}

可以看到在Domain对象中,除了定义基本的字段外,还定义了一些相应的事件,比如在构造函数中,实际上是发起了一个名为ItemCreateEvent的事件,同时还定义了处理时间的逻辑,这些逻辑都放在名为Handle的接口方法发,例如ItemCerateEvent的处理方法为Handle(ItemCreateEvent)方法。

ApplyChange方法在AggregateRoot对象中,他是聚集根,这是DDD中的概念。通过这个根可以串起所有对象。 该类实现了IEventProvider接口,他保存了所有在_changes中的所有没有提交的变更,其中的ApplyChange的用来为特定的Event查找Eventhandler的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public abstract class AggregateRoot : IEventProvider

{

private readonly List<Event> _changes;

public Guid Id { get; internal set; }

public int Version { get; internal set; }

public int EventVersion { get; protected set; }

protected AggregateRoot()

{

_changes = new List<Event>();

}

public IEnumerable<Event> GetUncommittedChanges()

{

return _changes;

}

public void MarkChangesAsCommitted()

{

_changes.Clear();

}

public void LoadsFromHistory(IEnumerable<Event> history)

{

foreach (var e in history) ApplyChange(e, false);

Version = history.Last().Version;

EventVersion = Version;

}

protected void ApplyChange(Event @event)

{

ApplyChange(@event, true);

}

private void ApplyChange(Event @event, bool isNew)

{

dynamic d = this;

d.Handle(Converter.ChangeTo(@event, @event.GetType()));

if (isNew)

{

_changes.Add(@event);

}

}

}

在ApplyChange的实现中,this其实就是对应的实现了AggregateRoot的DiaryItem的Domain对象,调用的Handle方法就是我们之前在DiaryItem中定义的行为。然后将该event保存在内部的未提交的事件列表中。相关的信息及事件都保存在了定义的aggregate对象中并返回。

然后Command继续执行,然后调用了_repository.Save(aggregate, aggregate.Version);这个方法。先看这个Repository对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class Repository<T> : IRepository<T> where T : AggregateRoot, new()

{

private readonly IEventStorage _storage;

private static object _lockStorage = new object();

public Repository(IEventStorage storage)

{

_storage = storage;

}

public void Save(AggregateRoot aggregate, int expectedVersion)

{

if (aggregate.GetUncommittedChanges().Any())

{

lock (_lockStorage)

{

var item = new T();

if (expectedVersion != -1)

{

item = GetById(aggregate.Id);

if (item.Version != expectedVersion)

{

throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified",

item.Id));

}

}

_storage.Save(aggregate);

}

}

}

public T GetById(Guid id)

{

IEnumerable<Event> events;

var memento = _storage.GetMemento<BaseMemento>(id);

if (memento != null)

{

events = _storage.GetEvents(id).Where(e=>e.Version>=memento.Version);

}

else

{

events = _storage.GetEvents(id);

}

var obj = new T();

if(memento!=null)

((IOriginator)obj).SetMemento(memento);

obj.LoadsFromHistory(events);

return obj;

}

}

这个方法主要是用来对事件进行持久化的。 所有的聚合的变动都会存在该Repository中,首先,检查当前的聚合是否和之前存储在storage中的聚合一致,如果不一致,则表示对象在其他地方被更改过,抛出ConcurrencyException,否则将该变动保存在Event Storage中。

IEventStorage用来存储所有的事件,其实现类型为InMemoryEventStorage。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class InMemoryEventStorage:IEventStorage

{

private List<Event> _events;

private List<BaseMemento> _mementos;

private readonly IEventBus _eventBus;

public InMemoryEventStorage(IEventBus eventBus)

{

_events = new List<Event>();

_mementos = new List<BaseMemento>();

_eventBus = eventBus;

}

public IEnumerable<Event> GetEvents(Guid aggregateId)

{

var events = _events.Where(p => p.AggregateId == aggregateId).Select(p => p);

if (events.Count() == 0)

{

throw new AggregateNotFoundException(string.Format("Aggregate with Id: {0} was not found", aggregateId));

}

return events;

}

public void Save(AggregateRoot aggregate)

{

var uncommittedChanges = aggregate.GetUncommittedChanges();

var version = aggregate.Version;

foreach (var @event in uncommittedChanges)

{

version++;

if (version > 2)

{

if (version % 3 == 0)

{

var originator = (IOriginator)aggregate;

var memento = originator.GetMemento();

memento.Version = version;

SaveMemento(memento);

}

}

@event.Version=version;

_events.Add(@event);

}

foreach (var @event in uncommittedChanges)

{

var desEvent = Converter.ChangeTo(@event, @event.GetType());

_eventBus.Publish(desEvent);

}

}

public T GetMemento<T>(Guid aggregateId) where T : BaseMemento

{

var memento = _mementos.Where(m => m.Id == aggregateId).Select(m=>m).LastOrDefault();

if (memento != null)

return (T) memento;

return null;

}

public void SaveMemento(BaseMemento memento)

{

_mementos.Add(memento);

}

}

在GetEvent方法中,会找到所有的聚合根Id相关的事件。在Save方法中,将所有的事件保存在内存中,然后每隔三个事件建立一个快照。可以看到这里面使用了备忘录模式。

然后在foreach循环中,对于所有的没有提交的变更,EventBus将该事件发布出去。

现在,所有的发生变更的事件已经记录下来了。事件已经被发布到EventBus上,然后对应的EventHandler再处理对应的事件,然后与DB交互。现在来看EventBus的Publish方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EventBus:IEventBus

{

private IEventHandlerFactory _eventHandlerFactory;

public EventBus(IEventHandlerFactory eventHandlerFactory)

{

_eventHandlerFactory = eventHandlerFactory;

}

public void Publish<T>(T @event) where T : Event

{

var handlers = _eventHandlerFactory.GetHandlers<T>();

foreach (var eventHandler in handlers)

{

eventHandler.Handle(@event);

}

}

}

可以看到EventBus的Publish和CommandBus中的Send方法很相似,都是首先通过EventHandlerFactory查找对应Event的Handler,然后调用其Handler方法。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class StructureMapEventHandlerFactory : IEventHandlerFactory

{

public IEnumerable<IEventHandler<T>> GetHandlers<T>() where T : Event

{

var handlers = GetHandlerType<T>();

var lstHandlers = handlers.Select(handler => (IEventHandler<T>) ObjectFactory.GetInstance(handler)).ToList();

return lstHandlers;

}

private static IEnumerable<Type> GetHandlerType<T>() where T : Event

{

var handlers = typeof(IEventHandler<>).Assembly.GetExportedTypes()

.Where(x => x.GetInterfaces()

.Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(IEventHandler<>)))

.Where(h => h.GetInterfaces()

.Any(ii => ii.GetGenericArguments()

.Any(aa => aa == typeof(T))))

.ToList();

return handlers;

}

}

然后返回并实例化了ItemCreatedEventHandler 对象,该对象的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ItemCreatedEventHandler : IEventHandler<ItemCreatedEvent>

{

private readonly IReportDatabase _reportDatabase;

public ItemCreatedEventHandler(IReportDatabase reportDatabase)

{

_reportDatabase = reportDatabase;

}

public void Handle(ItemCreatedEvent handle)

{

DiaryItemDto item = new DiaryItemDto()

{

Id = handle.AggregateId,

Description = handle.Description,

From = handle.From,

Title = handle.Title,

To=handle.To,

Version = handle.Version

};

_reportDatabase.Add(item);

}

}

可以看到在Handler方法中,从事件中获取参数,然后新建DTO对象,然后将该对象更新到DB中。

到此,整个Command执行完成。

六 结语

CQRS是一种思想很简单清晰的设计模式,他通过在业务上分离操作和查询来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在CQRS中,所有的涉及到对DB的操作都是通过发送Command,然后特定的Command触发对应事件来完成操作,这个过程是异步的,并且所有涉及到对系统的变更行为都包含在具体的事件中,结合Eventing Source模式,可以记录下所有的事件,而不是以往的某一点的数据信息,这些信息可以作为系统的操作日志,可以来对系统进行回退或者重放。

CQRS 模式在实现上有些复杂,很多地方比如AggregationRoot、Domain Object都涉及到DDD中的相关概念,本人对DDD不太懂。这里仅为了演示CQRS模式,所以使用的例子是codeproject上的,末尾列出了一些参考文章,如果您想了解更多,可以有针对性的阅读。

最后,希望CQRS模式能让您在设计高性能,可扩展性的程序时能够多一种选择和考虑。

七 参考文献

  1. Introduction to CQRS http://www.codeproject.com/Articles/555855/Introduction-to-CQRS
  2. CQRS http://martinfowler.com/bliki/CQRS.html
  3. CQRS Journey http://msdn.microsoft.com/en-us/library/jj554200.aspx
  4. Command and Query Responsibility Segregation (CQRS) Pattern http://msdn.microsoft.com/en-us/library/dn568103.aspx
  5. EntityFramework之领域驱动设计实践:CQRS体系结构模式 http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html
  6. Event Sourcing Pattern http://msdn.microsoft.com/en-us/library/dn589792.aspx

文章来源:https://www.cnblogs.com/yangecnu/p/introduction-cqrs.html

http://edisonxu.com/2017/03/23/hello-cqrs.html

chromium是google chrome浏览器所采用的内核,最开始由苹果的webkit发展而出,由于webkit在发展上存在分歧,而google希望在开发上有更大的自由度,2013年google决定自己开发webcore的分支,叫做Blink引擎,而后google以BSD伯克利许可开源,BSD许可限制较为宽松,很多浏览器都是基于chromium开发的,比如,此后省略100字。google在原有基础上做了进一步的精简优化,并开发出v8 javascript引擎,2010年google收购了webrtc技术随后开放了源代码,webrtc采用vp编码,兼容html5标准,同年google推出了chrome os云操作系统,浏览器的衍生产品。

废话不多说,检索一下Chromium Embedded Framework,简称cef,你可以在cefbuilds上看到当前chromium最新放出的版本,也可以在google code上下载到,里面包括浏览器的核心库和底层api,支持c和c++的编程语言,另外也有第三方的包括.net/mono、java、python、delphi等开源项目。

我们从google code下载win32的c++库,打开release文件夹。

1

这里面作个介绍:

libcef.dll:cef核心库。

icudt.dll:编码格式库。

ffmpegsumo.dll:视频解码器,包含vp8 vp9编码库。

d3dcompiler_43.dll、d3dcompiler_46.dll、libEGL.dll、libGLESv2.dll

这几个是3d图形的库,d3dcompiler_43.dll适用于xp,d3dcompiler_46适用于xp以上版本。

include文件夹里面是cef c++的头文件,可以去github下载.net调用的project,叫做cefsharp,提供了winform和wpf的完整demo。

2

CefSharp:封装是C#调用api的入口和数据接受类。

CefSharp.BrowserSubprocess:是.net写的一个伴随进程,主要负责处理javascript和后台线程。

CefSharp.BrowserSubprocess.Core:是一个c++的工程,需要引用到cef的c++头文件,主要是javascript相关操作。

CefSharp.Core:也是一个c++工程,包括cef的初始化配置、接受事件等。

CefSharp.Example:c#调用cef的初始化配置。

CefSharp.WinForm.Example:写了一个自定义控件,作为cef浏览器的窗口。

ChromimumWebBrowser.cs所有接口的实现可以放在这里面,详见cefsharp demo。

1 public class ChromiumWebBrowser : Control, IWebBrowserInternal, IWinFormsWebBrowser 2 { 3 … 4 }

再加一个自定义控件BrowserUserControl。自定义一个构造函数。

复制代码

1 public BrowserUserControl(string url) 2 {
3 InitializeComponent();
4
5 var browser = new ChromiumWebBrowser(url) 6 {
7 Dock = DockStyle.Fill 8 };
9 this.Controls.Add(browser); 10 }

复制代码

 建立一个Form1启动窗口,添加创建的用户控件。

复制代码

1 public Form1() 2 {
3 InitializeComponent();
4
5 var browser = new BrowserUserControl(CefExample.DefaultUrl) 6 {
7 Dock = DockStyle.Fill, 8 };
9 browser.CreateControl(); 10 this.Controls.Add(browser); 11 }

复制代码

再来看一下Program.cs中的main方法入口,CefExample调用了一个Init初始化方法。

复制代码

1 ///


2 /// The main entry point for the application. 3 ///

4 [STAThread]
5 static void Main() 6 {
7 CefExample.Init();
8
9 Application.EnableVisualStyles(); 10 Application.SetCompatibleTextRenderingDefault(false); 11 Application.Run(new Form1()); 12 }

复制代码

在CefSharp.Example工程中,defaulturl就是默认首页url地址。

复制代码

1 public static class CefExample 2 {
3 public const string DefaultUrl = “http://www.google.com/“;
4 private static readonly bool DebuggingSubProcess = Debugger.IsAttached; 5
6 public static void Init() 7 {
8 var settings = new CefSettings(); 9 settings.RemoteDebuggingPort = 8088; 10 settings.CefCommandLineArgs.Add(“enable-media-stream”, “enable-media-stream”); 11 settings.IgnoreCertificateErrors = true; 12 settings.LogSeverity = LogSeverity.Verbose; 13
14 if(DebuggingSubProcess) 15 { 16 //var architecture = Environment.Is64BitProcess ? “x64” : “x86”; 17 //settings.BrowserSubprocessPath = “..\\..\\..\\..\\CefSharp.BrowserSubprocess\\bin\\“ + architecture + “\\Debug\\CefSharp.BrowserSubprocess.exe”;
18 } 19
20 settings.RegisterScheme(new CefCustomScheme 21 { 22 SchemeName = CefSharpSchemeHandlerFactory.SchemeName, 23 SchemeHandlerFactory = new CefSharpSchemeHandlerFactory() 24 }); 25
26 if (!Cef.Initialize(settings)) 27 { 28 if (Environment.GetCommandLineArgs().Contains(“--type=renderer”)) 29 { 30 Environment.Exit(0); 31 } 32 else
33 { 34 return; 35 } 36 } 37 } 38 }

复制代码

我们放一个release版本,里面大概有这些文件。locales里面放的本地化资源包,包括cef_100_percent/cef_200_percent,如果删掉,会出现诸如窗口滚动条外观异常等,degug.log会记录操作记录。

11 

运行一下打开一个网页。

5

我们再写一个html页面。

复制代码

1
2
3 <html>
4 <head>
5 <meta http-equiv=“Content-Type” content=“text/html; charset=utf-8”>
6 <meta id=“viewport” name=“viewport” content=“width=device-width, initial-scale=1”>
7 <title></title>
8 </head>
9 <body>
10 <video autoplay></video>
11 <script>
12 ‘use strict’; 13
14 var video = document.querySelector(‘video’); 15 var constraints = { 16 audio: false, 17 video: true
18 }; 19
20 navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
21 navigator.mozGetUserMedia; 22
23 function successCallback(stream) { 24 window.stream = stream; 25 if (window.URL) { 26 video.src = window.URL.createObjectURL(stream); 27 } else { 28 video.src = stream; 29 } 30 } 31
32 function errorCallback(error) { 33 console.log(‘navigator.getUserMedia error: ‘, error); 34 } 35
36 navigator.getUserMedia(constraints, successCallback, errorCallback); 37 </script>
38 </body>
39 </html>

复制代码

修改CefExample的defaulturl指向这个页面。

有一点要注意,在CefCommandLineArgs添加了enable-media-stream参数,意思是开启chrome的媒体流。看下效果。

这里我们基于chromium内核使用到了html5 webrtc技术,页面开启了摄像头。

这里只是粗略的列了个小demo,还有比如一些基本的鼠标事件、页面重定向等功能,除此之外chromium很有功能api值得学习和挖掘。