逃离 IE 浏览器与 ActiveX 控件

前言

因为公司产品是 B/S 架构,所以很多与硬件交互资源的功能,不可避免的要使用 ActiveX 控件。

但是因为安全性问题,现代浏览器例如 Chrome、Firefox、Edge 等都已经放弃了对 ActiveX 的支持,所以这个方案已经不再是合适的选择。

当然现阶段还是有很多解决方案,例如不使用新特性与新功能,坚定不移的使用支持 ActiveX 的浏览器版本;使用开源的一些浏览器组件例如 MiniBlink,开发自己的“浏览器”等等。

但是对于开发来说,面对前端涌现的很多新技术,因为浏览器版本太低,无法支持一些新特性而无法使用畏手畏脚,是一种很痛苦的开发体验。

解决打印问题

因为最初产品设计的问题,没有使用水晶报表、FastReport 等报表控件,所以打印现在还依托于 HTML 打印。

用过的其实都知道,浏览器本身以及市面上一些 ActiveX 打印控件,其实对打印功能支持都有限,一些复杂的功能无法实现,而且经常要被一些内容溢出、所见非所得等问题折磨。

而产品升级到用 Chrome 就更麻烦了,本身自带的打印功能,产品发布以后基本没有使用价值,和 C/S 架构的打印体验更是差了十万八千里。

因为之前发过一篇将打印迁移到使用 LODOP 打印控件,所以这里不再赘述:Web下打印体验最好的打印控件LODOP

现在已经将所有的打印例如条码、报表、报告单迁移到 LODOP 上。而且在不支持 ActiveX 的浏览器版本,可以使用其提供的 CLodop 控件,其会使用 HTTP 与 WebSocket 进行通信,控制打印,目前上线的项目实际体验比过去使用 ActiveX 也要好很多。

解决读卡问题

如果仅仅是磁条卡、二维码等,刷卡和扫码设备会直接将文本输出,可以使用输入框接收。

但如果是芯片卡,例如 IC 卡、身份证、社保卡、银行卡等,读卡器厂商会提供动态链接库给第三方开发使用。

一般如果是 C/S 架构,自然不会存在读卡问题,但是 B/S 架构,网上提供的资料一般都是让封装成 ocx 使用。

这里建议是参考 CLodop 的解决方案,开发一个小工具,使用 HTTP 监听处理读卡请求:

2019122516011220191225160112

监听工具这里依然是 C# 的例子,可以托管到 WinForm、WPF、控制台应用程序,但是需要注意 Win7 以上 Windows 版本必须 以管理员身份运行。建议是托管于 Windows 服务。

首先我们基于 HttpListener 封装一个简单的 HttpServer

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
/// <summary>
/// 使用HttpListener实现的HttpServer
/// </summary>
public class HttpServer
{
/// <summary>
/// 监听
/// </summary>
public HttpListener Listener { get; }

/// <summary>
/// 构造函数
/// </summary>
/// <param name="prefixes">定义url</param>
/// <param name="auth">指定身份验证 默认匿名</param>
public HttpServer(IEnumerable<string> prefixes, AuthenticationSchemes auth = AuthenticationSchemes.Anonymous)
{
if (!HttpListener.IsSupported)
{
Console.WriteLine("Windows XP SP2 or Server 2003 is required to use the HttpListener class.");
}
else if (prefixes == null || prefixes.Count() == 0)
{
Console.WriteLine("初始化服务监听失败,服务监听链接不能为空!");
}
else
{
try
{
Listener = new HttpListener
{
AuthenticationSchemes = auth
};
foreach (string prefix in prefixes)
{
Listener.Prefixes.Add(prefix);
}
}
catch (Exception exc)
{
Console.WriteLine($"开启监听服务出现未经处理的异常:{exc.Message}");
}
}
}

/// <summary>
/// Http监听程序响应事件委托
/// </summary>
/// <param name="ctx"></param>
public delegate void ResponseEventArges(HttpListenerContext ctx);

/// <summary>
/// Http监听程序响应事件
/// </summary>
public event ResponseEventArges ResponseEvent;

/// <summary>
/// 请求的回调
/// </summary>
private AsyncCallback _asyncCallback;

/// <summary>
/// 开启监听服务
/// </summary>
public void Start()
{
if (Listener == null)
{
Console.WriteLine("监听服务初始化失败,无法开启监听。");
}
else if (!Listener.IsListening)
{
try
{
Listener.Start();
_asyncCallback = new AsyncCallback(GetContextAsyncCallback);
Listener.BeginGetContext(_asyncCallback, null);
Console.WriteLine($"开启监听:{string.Join("、", Listener.Prefixes)}。");
}
catch (Exception exc)
{
Console.WriteLine($"开启监听服务出现未经处理的异常:{exc.Message}");
}
}
else
{
Console.WriteLine($"监听服务正在运行,无需重复开启。");
}
}

/// <summary>
/// 关闭服务监听
/// </summary>
public void Stop()
{
try
{
Listener?.Stop();
Console.WriteLine($"已关闭监听服务。");
}
catch (Exception exc)
{
Console.WriteLine($"关闭监听服务出现未经处理的异常:{exc.Message}");
}
}

/// <summary>
/// 异步检查传入的请求
/// </summary>
/// <param name="result">结果</param>
public void GetContextAsyncCallback(IAsyncResult result)
{
if (result.IsCompleted)
{
HttpListenerContext ctx = Listener.EndGetContext(result);
if (ResponseEvent != null)
{
ResponseEvent.Invoke(ctx);
}
else
{
dynamic data = new ExpandoObject();
data.success = false;
data.msg = $"未注册http请求处理事件。";
ctx.Response.StatusCode = 200;
// 允许跨域访问
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
ctx.Response.ContentType = "application/json";
ctx.Response.ContentEncoding = Encoding.UTF8;
JavaScriptSerializer serializer = new JavaScriptSerializer();
byte[] binaryData = Encoding.UTF8.GetBytes(serializer.Serialize(data));
ctx.Response.OutputStream.Write(binaryData, 0, binaryData.Length);
}
ctx.Response.Close();
}
Listener.BeginGetContext(_asyncCallback, null);
}

/// <summary>
/// 获取请求上下文中传递的参数,注意参数名已经默认转换为小写
/// </summary>
/// <param name="ctx">HttpListener请求上下文</param>
/// <returns>参数数据</returns>
public static Dictionary<string, string> GetData(HttpListenerContext ctx)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
if (ctx.Request.HttpMethod == "GET")
{
for (int i = 0; i < ctx.Request.QueryString.AllKeys.Length; i++)
{
data[ctx.Request.QueryString.AllKeys[i].ToLower()] = ctx.Request.QueryString[ctx.Request.QueryString.AllKeys[i]];
}
}
else
{
string rawData;
using (StreamReader reader = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding))
{
rawData = reader.ReadToEnd();
}
if (!string.IsNullOrEmpty(rawData))
{
string[] parameters = rawData.Split('&');
for (int i = 0; i < parameters.Length; i++)
{
string[] kvPair = parameters[i].Split('=');
if (kvPair.Length > 1)
{
string key = HttpUtility.UrlDecode(kvPair[0], ctx.Request.ContentEncoding);
string value = HttpUtility.UrlDecode(kvPair[1], ctx.Request.ContentEncoding);
data[key.ToLower()] = value;
}
}
}
}
return data;
}
}

然后我们可以借助这个封装的类型,简单的托管一个 HTTP 服务:

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
class Program
{
static void Main(string[] args)
{
string[] prefexes = new string[] { "http://*/read_card/" };
HttpServer server = new HttpServer(prefexes);
server.ResponseEvent += (ctx) =>
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
bool success = false;
string msg = string.Empty;
string data = null;
Dictionary<string, string> parameters = HttpServer.GetData(ctx);
try
{
if (parameters.Count == 0)
{
msg = "入参不能为空!";
}
else if (!parameters.ContainsKey("token") || parameters["token"] != "sMO9sIU5OiNyIWDtKXSneWuW7TO7Kuev")
{
msg = "token 无效";
}
else if (!parameters.ContainsKey("key"))
{
msg = "无法获取必要的[key]信息!";
}
else
{
if (parameters["key"] == "USER_NAME")
{
success = true;
data = Environment.UserName;
}
else if (parameters["key"] == "USER_DOMAIN_NAME")
{
success = true;
data = Environment.UserDomainName;
}
else
{
msg = "未知的请求!";
}
}

Console.WriteLine($"响应数据:{msg}");
}
catch (Exception exc)
{
msg = $"错误:{exc.Message}";
}
finally
{
var result = new { success, msg, data };
ctx.Response.StatusCode = 200;
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
ctx.Response.ContentType = "application/json";
ctx.Response.ContentEncoding = Encoding.UTF8;
byte[] binaryData = Encoding.UTF8.GetBytes(serializer.Serialize(result));
ctx.Response.OutputStream.Write(binaryData, 0, binaryData.Length);
}
};
server.Start();
Console.WriteLine("监听程序启动,回车键终止程序运行!");
Console.ReadLine();
}
}

如果我们调试程序,Visual Studio 又没有使用管理员身份证运行,很容易出现以下问题:

2019122517572920191225175729

如果直接双击打开应该也会有类似问题,需要右键该程序以管理员身份运行才可以,后面会另外写一篇文章,说明如何处理这个问题。

正常启动程序,并使用 Postman 访问效果如下:

2019122518091620191225180916

这里给出的例子没有给出具体使用动态链接库读卡的例子,这样更方便测试,一般读卡器厂商会提供动态链接库各种编程语言的例子,抄过来就能使用。

同样的,更多需要使用 ActiveX 的功能,例如 CA 签名、U盾 等也可以使用这个方案实现。

参考:

多线程操作 Windows Form 或 WPF 的控件

前言

写代码就是这样,不是天天写的代码,很长一段时间不用,下次再用大概率是已经忘记了,不知道怎么处理。

虽然可以从搜索引擎中找到答案,但是肯定没有自己整理的看着舒服,所以后面会把 C/S 开发时遇到的一些小坑整理以下。

多线程操作 UI 组件

今天写一个 HTTP 监听的小工具,其中需要输出一个日志窗,其中会由于日志类型的不同,调整 RichTextBoxSelectionColorSelectionBackColor 以更新日志窗体的前景色或背景色。

当然可能涉及多线程中输出日志,所以更新组件内容自然也用了 InvokeBeginInvoke 方法。

因为写日志时会将之前设置的前景色以及背景色取出来,等输出完成后将原本的前景色或背景色设置回去,所以调用方法以后就在想,这个过程需不需要加锁。

因为万一在输出 Error 日志时,将颜色设置成红色了,那另外一个线程需要写 Info 类型的日志,那前景色与背景色岂不是就乱掉了。

然后就简单写了个段,测试了一下:

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
for (int i = 0; i < 20; i++)
{
int num = i;

Task.Factory.StartNew(() =>
{
richTextBox1.BeginInvoke(new Action(() =>
{
richTextBox1.SelectionColor = Color.Red;
richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} 任务一开始执行 {num}\r\n");
Thread.Sleep(10);
richTextBox1.SelectionColor = Color.Red;
richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} 任务一执行结束 {num}\r\n");
}));
});
Task.Factory.StartNew(() =>
{
richTextBox1.BeginInvoke(new Action(() =>
{
richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} Task two begains {num}\r\n");
Thread.Sleep(1);
richTextBox1.AppendText($"{DateTime.Now:HH:mm:ss.fff} {Thread.CurrentThread.ManagedThreadId:00000} Task two is over {num}\r\n");
}));
});
}

执行效果:

2019122420154520191224201545

可以看到输出的线程 ID 全部是主线程的 ID:00001,所以这时候才想起来,无论使用同步方法 Invoke,还是异步方法 BeginInvoke,都仅仅知识针对 UI 主线程外的其他线程,实际上调用以后的委托只有一个 UI 线程来负责执行。

否则怎么可能避免 线程间操作无效: 从不是创建控件的线程访问它。,所以就是杞人忧天了。

Windows Form

WinForm 的控件基类型 Control 提供了 Invoke 方法与 BeginInvoke,多线程中如果需要操作 UI 组件(赋值操作),可以使用这两个方法。

区别是 Invoke 是同步方法,当前线程会等待 UI 主线程将该委托执行完成,而 BeginInvoke 是异步的则不会等待 UI 主线程的操作。

传递的委托我常常使用以下几种写法,都没有问题:

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
Task.Factory.StartNew(() => 
{
// 多线程中可以从控件中取值
Color color = richTextBox1.SelectionColor;
// 但是不可以在多线程中为控件赋值
// richTextBox1.SelectionColor = color;

// BeginInvoke 相对于当前线程异步执行,不会等待 UI 主线程的更新
this.BeginInvoke((Action)delegate
{
richTextBox1.SelectionColor = color;
});

// Invoke 相对于当前线程同步执行,会等待 UI 主线程将委托执行完成
this.Invoke(new EventHandler(delegate
{
richTextBox1.SelectionColor = color;
}));

// 传递一个委托的方式多种多样,这时我常用的几种写法
richTextBox1.BeginInvoke(new Action(() =>
{
richTextBox1.SelectionColor = color;
}));
});

注意:我们使用任何组件来执行 InvokeBeginInvoke 都是一样的,例如上面这个例子,无论是使用 this 指代的当前窗体,还是这个窗体的富文本框 richTextBox1,最终目的和效果都是在 UI 主线程中执行代码。

WPF

WinForm 一样,WPF 中主线程维护的 UI 子线程也不能直接更新,但是不同的是 WPF 是通过 Dispatcher 处理,由 Dispatcher 来管理线程工作项队列。

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
Task.Factory.StartNew(() =>
{
// 多线程中可以从控件中取值
Brush brush = richTextBox1.Background;
// 但是不可以在多线程中为控件赋值
// richTextBox1.Background = brush;

// BeginInvoke 相对于当前线程异步执行,不会等待 UI 主线程的更新
this.Dispatcher.BeginInvoke((Action)delegate
{
richTextBox1.Background = brush;
});

// Invoke 相对于当前线程同步执行,会等待 UI 主线程将委托执行完成
this.Dispatcher.Invoke(new EventHandler(delegate
{
richTextBox1.Background = brush;
}));

// 传递一个委托的方式多种多样,这时我常用的几种写法
richTextBox1.Dispatcher.BeginInvoke(new Action(() =>
{
richTextBox1.Background = brush;
}));
});

其继承关系可以参考我从网上找到的一幅图:

DispatcherObject继承关系DispatcherObject继承关系

注:因为 Windows XP 支持 .NET Framewrok 的最后一个版本是 .NET Framewrok 4.0,所以没有特别说明,我习惯上创建的 Windows FormWPF 等客户端程序选择的框架都是 .NET Framewrok 4.0

参考:

贝克曼 DxH800 血球仪图片绘制问题

过去开发 LIS 接口对接仪器,大部分血细胞分析仪厂商是希森美康、迈瑞等。

与这些仪器通讯时有个好处,仪器会将图片内容直接发送给我们,或使用第三方软件提前绘制,我们直接读取或对 Base64 数据转码即可,对接都很简单。

但是有些厂商,例如贝克曼、雅培提供的并不是绘制好的图片数据,而是需要我们使用他们提供的数据信息,自行绘制,相对来说开发投入的精力就比希森美康和迈瑞大很多。

以下就是我在绘制血液分析散点图与直方图的一些经验总结。

雅培 Ruby 系列

第一次试水是同事负责的一台雅培 Ruby 系列的全自动血细胞分析仪,图形不是常见的可以读文件或者直接从 Base64 转码。

因为当时在出差,而且不是我负责的,所以没有仔细研究,也仅仅只绘制出了直方图供同事参考。

这里也将这部分代码存在这里,如果以后有用的话备查:

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
public static Bitmap GetHistgrame(string type, string lines, string base64)
{
#region 画直线的X坐标
List<int> listLines = new List<int>();
string[] arrLines = lines.Split(new string[] { "\\" }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < arrLines.Length; i++)
{
int temp = 0;
if (int.TryParse(arrLines[i], out temp))
{
listLines.Add(temp);
}
}
#endregion

//直方图的X坐标
List<byte> list = Convert.FromBase64String(base64).ToList();

Bitmap bitmap = new Bitmap(280, 300);
using (Graphics graphics = Graphics.FromImage(bitmap))
using (graphics.FillRectangle(Brushes.White, new Rectangle(0, 0, bitmap.Width, bitmap.Height)))
using (Pen pen = new Pen(Brushes.Black, 1))
{
//画坐标系
{
//Y轴
graphics.DrawLine(pen, 10, 5, 10, 260);// (10,5) (10,260)

//X轴
graphics.DrawLine(pen, 10, 260, 265, 260);// (10, 260)(265, 260)

//X轴上坐标点
for (int i = 0; i < 6; i++)
{
graphics.DrawLine(pen, 10 + i * 50, 260, 10 + i * 50, 262);
graphics.DrawString((i * 50).ToString(), new Font("宋体", 10), Brushes.Black, i * 50 - 2, 262);
}
//描述信息
graphics.DrawString(type, new Font("宋体", 15), Brushes.Black, 130, 275);
}

//画直方图
{
pen.Brush = Brushes.Red;
for (int i = 0; i < list.Count / 2; i++)
{
int height = (((int)list[i * 2]) >> 8) + (int)list[i * 2];
if (height > 0)
{
height = height > 255 ? 255 : height;
graphics.DrawLine(pen, 10 + i + 1, 260 - 1, 10 + i + 1, 260 - 1 - height);
}
}
}

//画线
{
pen.Brush = Brushes.Blue;
for (int i = 0; i < listLines.Count; i++)
{
graphics.DrawLine(pen, 10 + listLines[i], 260, 10 + listLines[i], 5);
}
}
}

return bitmap;
}

调用测试:

1
2
3
4
5
6
string text = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAgADAAMABQAGAAkADQAQABYAGwAcACcALwA3AD8AUQBjAHAAfgCWAKgAswDFAN4A4QDcAOEA7ADsAOoA9AD2AO0A7QD4AP8A+QDwAOwA4ADTAMcAwQCzAJ8AlgCOAIAAcQBmAGIAUQBIAEAANgAtACoAJgAhABoAGgAZABkAGAAYABQAFwAYABoAGgAYABQAEQAVABUAFQAVABQAFAAXABoAHQAbACAAHgAhACgALQAvAC8ANQA7AEcATABSAFcAXgBhAG4AdgB3AHEAeAB/AIEAgACCAIIAfQB/AH8AfAB4AHYAcgBnAGAAWgBWAFEASwA/ADoAMQAxADEALAAmACAAHAAbABUAEwASAAkACQAHAAcABgAFAAMAAwACAAMAAQACAAIAAgACAAIAAgACAAEAAQAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";

using (Bitmap bitmap = GetHistgrame("WB1", "33\\82", text))
{
bitmap.Save("WBC.bmp");
}

11

贝克曼 DxH800

开发贝克曼的这台接口时,第一时间想到的就是雅培这个型号的接口,虽然数据结构不太一样,但是直方图绘制思路是类似的。

直方图

仪器那边提供的数据可以解析成 128 个数据点,然后这 128 个数据点对应在 Y轴 的位置,绘制直方图或折线图。

因为雅培的绘制的为直方图,其实也可以绘制成折线图,这里提供一个绘制成折线图的例子:

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
public static Bitmap GetHistgrame(string type, string hex)
{
Bitmap result = new Bitmap(720, 320);
using (Bitmap bitmap = new Bitmap(hex.Length, 256))
using (Pen pen = new Pen(Brushes.Black, 3))
{

using (Graphics graphics = Graphics.FromImage(bitmap))
{
for (int i = 0; i < hex.Length / 2; i++)
{
int num1 = i == 0 ? 0 : int.Parse(hex.Substring((i - 1) * 2, 2), NumberStyles.HexNumber);
int num2 = int.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber);
graphics.DrawLine(pen, i * 2 - 1, bitmap.Height - num1, i * 2 + 1, bitmap.Height - num2);
}
}

int left = 20;
int top = 20;

using (Font font = new Font(new FontFamily("宋体"), 25, FontStyle.Bold))
using (Graphics graphics1 = Graphics.FromImage(result))
{
//画Y轴
graphics1.DrawLine(pen, left, top, left, top + 256);

//画X轴
graphics1.DrawLine(pen, left, top + 256, left + 640, top + 256);

//将图形填充
graphics1.DrawImage(bitmap, new RectangleF(left, top, 640, 256));

//画刻度
switch (type.ToUpper())
{
case "WBC":
{
int length = 140;
for (int i = 0; i <= 640 / length; i++)
{
graphics1.DrawLine(pen, left + i * length, top + 256, left + i * length, top + 256 + 5);
{
graphics1.DrawString((i * 100).ToString(), font, Brushes.Black, left + i * length - 20, top + 256 + 5);
}
}
graphics1.DrawString("IL", font, Brushes.Black, left + 640 - 20, top + 256 + 5);
}
break;
case "RBC":
{
int length = 180;
for (int i = 0; i <= 640 / length; i++)
{
graphics1.DrawLine(pen, left + i * length, top + 256, left + i * length, top + 256 + 5);
{
graphics1.DrawString((i * 100).ToString(), font, Brushes.Black, left + i * length - 20, top + 256 + 5);
}
}
graphics1.DrawString("IL", font, Brushes.Black, left + 640 - 20, top + 256 + 5);
}
break;
case "PLT":
{
int length = 180;
for (int i = 0; i <= 640 / length; i++)
{
graphics1.DrawLine(pen, left + i * length, top + 256, left + i * length, top + 256 + 5);
{
graphics1.DrawString((i * 10).ToString(), font, Brushes.Black, left + i * length - 20, top + 256 + 5);
}
}
graphics1.DrawString("IL", font, Brushes.Black, left + 640 - 20, top + 256 + 5);
}
break;
default:
break;
}
}
}
return result;
}

调用测试:

1
2
3
4
5
6
string text = "0102020406090F16202D3C4D5E7081909BA4A8AAA9A59E958A7D7064584D443C35302B2824211F1C1A19171616151514141313121212111111111111111111111111111111121212131414151515161617171818191919191919191A1A1A1A1B1C1D1E1E1F2021212222232324242526272828292A2A2B2B2B2C2C2C2C2D2D2D2E2E2E2E2E2E2D2D2D2C2C2B2B2A29282726252423222121201F1E1D1C1B1B1A1A191918181717161514131211100F0E0D0C0C0B0A090908080707070606060505050505040404040303030202020202020101010101010101010100000000000000000000000000000000000000000000000000000000000000000000000000";

using (Bitmap bitmap = GetHistgrame("WBC", text))
{
bitmap.Save("WBC.bmp");
}

WBCWBC

散点图

其实两次绘图都是卡在散点图的绘制上,不过 DxH800 基本上可以确定是怎么画。

其图片信息简单解析如下:

  • 散点图数据头
    • 传输数据总长度 16 bit MSB+LSB
    • 传输数据块总数 8 bit
  • 散点图数据块
    • 散点图数据块头
      • 散点图类型代码 8 bit

        0x01: 五分类 0x02: 网织红 0x04: 未成熟红细胞

      • 散点图选项代码 8 bit

        0x00: 无渲染信息 0x01: 散点图被压缩 0x02: 有一个有效的渲染信息块

      • 散点图宽度 16 bit MSB+LSB
      • 散点图高度 16 bit MSB+LSB
    • 渲染信息块
      • 渲染信息块头
        • 渲染信息块选项 8 bit

          0x00: 任意选项

        • 调色板表条目数 8 bit
        • 位图图库模式 8 bit
        • 位图像素宽度 8 bit
        • 位图像素高度 8 bit
      • 调色板颜色值 8 bit
      • 抖动显示位图库 8 bit * n

        n = 位图像素宽度 * 位图像素高度

    • 散点图头
      • 散点图数据选项 8 bit

        0x00: 任意选项

      • 散点图基点尺寸 8 bit

        散点图中每个点的大小,通常是 8 表示一个字节

      • 未定义 8 bit
      • 未定义 8 bit
    • 散点图数据
      • 数据点块的数量
      • 数据

按照以上结构解析串口接收到的数据,可以确认散点图是 64 × 64 的图片,问题是由于公司运维提供的数据,其 散点图选项代码0x00 也就是 无渲染信息,所以无法确认通信文档中提到的 调色板 以及 抖动算法

从维基百科中可以了解到,调色板可以使用 8bit 绘制丰富的颜色,而抖动算法可以将图片颜色过度更平滑。

但是这些都涉及到了我的知识盲区,所以我们在不考虑调色板,以及抖动算法,使用红色来绘制这张图看一下效果:

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
public static Bitmap GetScatter(string type, string hex)
{
// 计算图片宽高
int height = int.Parse(hex.Substring(10, 2), NumberStyles.HexNumber)
+ (int.Parse(hex.Substring(12, 2), NumberStyles.HexNumber) << 8);
int width = int.Parse(hex.Substring(14, 2), NumberStyles.HexNumber)
+ (int.Parse(hex.Substring(16, 2), NumberStyles.HexNumber) << 8);

// 初始化位图
Bitmap bitmap = new Bitmap(width, height);

// 从字符串后端截取存储 8bit 调色板颜色的数据
hex = hex.Substring(hex.Length - height * width * 2);

// 通过设置位图像素点颜色的方式绘制散点图
for (int i = 0; i < bitmap.Width; i++)
{
for (int j = 0; j < bitmap.Height; j++)
{
byte color = byte.Parse(hex.Substring((i * bitmap.Width + j) * 2, 2), NumberStyles.HexNumber);
bitmap.SetPixel(i, j, Color.FromArgb(color, Color.Red));
}
}
return bitmap;
}

调用一下测试绘制效果:

1
2
3
4
5
string text = "0E1001010040004000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F000000000000001500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00001F00150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F001F00000015000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F1F0000001F00000000001F20001F00000C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F211F1F1500000B000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020211F1F00000B0B00150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F20001F000B170B160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F2220001F0000161717150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F21200000001517180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022201F000000191B1A1600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021231F0000001A1B190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F1F1F200000151A0B0B0B000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B0000001F1B1B1A160000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000200000161B0B1A160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00200000161B1B1A1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015001F0000181B1B1A160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029002000191B1B18150000000000000000000000000000000000000000000000000000000000000015000029000000000015000000000000000000000000002A00000000181B1B191600000000000000000000000000000000000000000000000000000000000000000000290000000000000000000000000000000000002929290000151A1B1B170000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002A292A150000171B1B1A001500150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929290000000001161A1A17000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929292A0000290100191700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292C002A2901151617001515000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029292E2E2E1500000016150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002D2E2E2D2D00010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292E2D2E2F2A000000150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002900292C2D2E2F2F2A2A0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292C2C2F2E2E29000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292D2E2E2E2E2B2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002C2D2C2E2D2D2A2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000290000000000000000000000002A2E2E2F2C2E2A0000000000000000000000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000002A2D2E2D2E2E292900000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000002900002C2C2D2B2F2D292A00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029002D2D2B2B2E292B0000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929292A2B2929000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029292A00290000002A171500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029000000000000000029352900000000000000000000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000293329353334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333534003300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333335003300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003400353434000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033383839393837383400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033000000003637393A3B3B3A3A39363533000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000330000003300373A3A3B3B3B3B3A3A39373300340000000000000000000033000000000000000000000000000000000000000000000000000000000000000000000000003438393A3A3A3A3A3A3937353433000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003300330000330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
using (Bitmap bitmap = GetScatter("5PD1", text))
{
bitmap.Save(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "5PD1.bmp"));
}

5PD15PD1

使用瑞美动态链接库解决绘图问题

如果不需要出具彩色报告单,以上绘制方法已经满足需求。

以下方案仅供研究学习,如果实际需要应用,建议研究如何开启仪器的渲染信息块传输,自行绘制。

因为瑞美连接的仪器型号比较全面,如果有其他方面的通信问题,也可以参考本方案,从瑞美的通信中学习经验。

显然单调的颜色不符合散点图的绘制需求,网上又检索不到关于血球仪图片绘制的解决方案,那么只有从其他的 LIS 厂商那里学习。

当然第一个能想到,也是最优方案肯定是瑞美,因为只有瑞美可以直接从官网下载到软件的安装包。

我们将瑞美的安装包解压,找到 DxH800 血球仪的接口,反编译解析找到瑞美的解决方案,可以发现,其提供了一个名为 richpic.dll 的动态链接库来绘制图片。

那么接下来就很简单了,我们只需要将文件反解析成瑞美动态链接库需要的格式,同样调用动态链接库解析生成图片。

直方图

首先是直方图,虽然我们成功绘制了图片,但是其提供的解决方案也具有一定参考意义:

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
/// <summary>
/// 添加直方图
/// </summary>
/// <param name="as_pic">图片数据</param>
/// <param name="as_file">存储文件路径</param>
/// <param name="as_picname">项目名称</param>
/// <param name="xxs">直方图相对 X轴 缩放</param>
/// <param name="yxs">直方图相对 Y轴 缩放</param>
/// <param name="pwidht">图片宽度</param>
/// <param name="pheight">图片高度</param>
/// <param name="xlen">X轴 长度</param>
/// <param name="ylen">Y轴 长度</param>
/// <param name="xinc">X轴 坐标点间距</param>
/// <param name="linew">画图线宽</param>
/// <param name="dotlen">未知 一般设置为3</param>
/// <returns></returns>
[DllImport("richpic.dll")]
public static extern int func_creathistogram(string as_pic, string as_file, string as_picname, double xxs, double yxs, int pwidht, int pheight, int xlen, int ylen, int xinc, int linew, int dotlen);

private static int AddGraphHistogram(DateTime sampleDate, string sampleNo, string graphItem, string graphData, int seq, string fileExt, ref string graphFile)
{
try
{
graphFile = $@"{AppDomain.CurrentDomain.BaseDirectory}\{sampleDate:yyyyMM}\{sampleDate:yyyyMMdd}_{sampleNo}_{graphItem}.{fileExt}";

if (!Directory.Exists(Path.GetDirectoryName(graphFile)))
Directory.CreateDirectory(Path.GetDirectoryName(graphFile));

double xxs = 2d;
double yxs = 1d;
int picw = 300;
int pich = 180;
int xlen = 150;
int ylen = 200;
int xinc = 50;
int linew = 1;
int dotlen = 3;
return func_creathistogram(graphData, graphFile, graphItem, xxs, yxs, picw, pich, xlen, ylen, xinc, linew, dotlen);
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);
return -1;
}
}

调用需要传输一些图片设置的信息,包括图片大小、X轴、Y轴、缩放、线宽、图片后缀名等。

测试一下瑞美画图调用效果,动态链接库会将图片写入到我们指定的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
string text = "0102020406090F16202D3C4D5E7081909BA4A8AAA9A59E958A7D7064584D443C35302B2824211F1C1A19171616151514141313121212111111111111111111111111111111121212131414151515161617171818191919191919191A1A1A1A1B1C1D1E1E1F2021212222232324242526272828292A2A2B2B2B2C2C2C2C2D2D2D2E2E2E2E2E2E2D2D2D2C2C2B2B2A29282726252423222121201F1E1D1C1B1B1A1A191918181717161514131211100F0E0D0C0C0B0A090908080707070606060505050505040404040303030202020202020101010101010101010100000000000000000000000000000000000000000000000000000000000000000000000000";

// 瑞美动态链接库解析需要将数据由十六进制转换成十进制
StringBuilder strGragh = new StringBuilder();
for (int i = 0; i < text.Length; i += 2)
{
strGragh.Append(int.Parse(text.Substring(i, 2), NumberStyles.HexNumber).ToString("000"));
}

// 封装了一个方法 可以传入标本日期标本号等 解析完成后返回该图片所在路径
string path = "";
int result = AddGraphHistogram(DateTime.Now, "1", "WBC", strGragh.ToString(), 1, "gif", ref path);


if (result != -1)
{
Console.WriteLine($"转换成功:{path}");
}
else
{
Console.WriteLine("转换失败");
}

20191219_1_WBC20191219_1_WBC

注意:瑞美图片的绘制信息是可以通过配置文件设置的,如果需要调整坐标轴的信息达到和仪器中图片效果一致,我们也需要设置这些参数,该测试没有根据实际需要的图片坐标信息对入参调整,而是将入参固定成一个值测试绘制效果。

散点图

同样的散点图也调用动态链接库进行绘制:

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
/// <summary>
/// 添加散点图
/// </summary>
/// <param name="as_picstr">图片数据</param>
/// <param name="as_line">未知</param>
/// <param name="colortype">颜色类型,可能和调色板有关</param>
/// <param name="xlen">X轴 长度</param>
/// <param name="ylen">Y轴 长度</param>
/// <param name="filename">存储文件路径</param>
/// <returns></returns>
[DllImport("richpic.dll")]
public static extern int func_creatscatter(string as_picstr, string as_line, int colortype, int xlen, int ylen, string filename);

private static int AddGraphScatter(DateTime sampleDate, string sampleNo, string graphItem, string graphData, string graphLine, int seq, string fileExt, ref string graphFile)
{
graphFile = $@"{AppDomain.CurrentDomain.BaseDirectory}\{sampleDate:yyyyMM}\{sampleDate:yyyyMMdd}_{sampleNo}_{graphItem}.{fileExt}";

if (!Directory.Exists(Path.GetDirectoryName(graphFile)))
Directory.CreateDirectory(Path.GetDirectoryName(graphFile));

int xlen = 64; // 实际是图片宽高
int ylen = 64;
int color = 3;
return func_creatscatter(graphData, graphLine, color, xlen, ylen, graphFile);
}

调用这个方法可以将图片绘制到指定目录:

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
string text = "0E1001010040004000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F000000000000001500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00001F00150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F001F00000015000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F1F0000001F00000000001F20001F00000C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F211F1F1500000B000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020211F1F00000B0B00150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F20001F000B170B160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F2220001F0000161717150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F21200000001517180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022201F000000191B1A1600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021231F0000001A1B190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F1F1F200000151A0B0B0B000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B0000001F1B1B1A160000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000200000161B0B1A160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00200000161B1B1A1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015001F0000181B1B1A160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029002000191B1B18150000000000000000000000000000000000000000000000000000000000000015000029000000000015000000000000000000000000002A00000000181B1B191600000000000000000000000000000000000000000000000000000000000000000000290000000000000000000000000000000000002929290000151A1B1B170000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002A292A150000171B1B1A001500150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929290000000001161A1A17000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929292A0000290100191700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292C002A2901151617001515000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029292E2E2E1500000016150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002D2E2E2D2D00010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292E2D2E2F2A000000150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002900292C2D2E2F2F2A2A0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292C2C2F2E2E29000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000292D2E2E2E2E2B2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002C2D2C2E2D2D2A2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000290000000000000000000000002A2E2E2F2C2E2A0000000000000000000000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000002A2D2E2D2E2E292900000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000002900002C2C2D2B2F2D292A00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029002D2D2B2B2E292B0000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002929292A2B2929000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029292A00290000002A171500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000029000000000000000029352900000000000000000000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000293329353334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333534003300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333335003300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003400353434000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033383839393837383400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033000000003637393A3B3B3A3A39363533000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000330000003300373A3A3B3B3B3B3A3A39373300340000000000000000000033000000000000000000000000000000000000000000000000000000000000000000000000003438393A3A3A3A3A3A3937353433000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003300330000330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";

// 通过图片宽高计算用于绘制图形内容的数据
int height = int.Parse(text.Substring(10, 2), NumberStyles.HexNumber)
+ (int.Parse(text.Substring(12, 2), NumberStyles.HexNumber) << 8);
int width = int.Parse(text.Substring(14, 2), NumberStyles.HexNumber)
+ (int.Parse(text.Substring(16, 2), NumberStyles.HexNumber) << 8);
string resultText = text.Substring(text.Length - height * width * 2);

// 瑞美动态链接库解析需要提供坐标点 以及对应坐标点的十进制调色板颜色值
StringBuilder strDec = new StringBuilder();
for (int i = 0; i < resultText.Length; i += 2)
{
strDec.Append($"{((i + 1) / 128)},{((i / 2) % 64)},{int.Parse(resultText.Substring(i, 2), System.Globalization.NumberStyles.HexNumber).ToString("000")}|");
}

// 封装了一个方法 可以传入标本日期标本号等 解析完成后返回该图片所在路径
string path = "";
int result = AddGraphScatter(DateTime.Now, "1", "5PD1", strDec.ToString(), "", 4, "gif", ref path);

if (result != -1)
{
Console.WriteLine($"转换成功:{path}");
}
else
{
Console.WriteLine("转换失败");
}

20191219_1_5PD120191219_1_5PD1

这时可以对比瑞美和我们前面绘制的图片,除了因为调色板的问题颜色不对外,其他的信息基本一致。当然瑞美动态链接库绘制的图片也没有使用所谓的抖动算法对图形进行优化就是了。

20191219_1_5PD120191219_1_5PD1

注:文中部分图片绘制较小,可以缩放查看。

参考:

SOA —— ASP.NET Web API 依赖注入

依赖注入(Dependency Injection,缩写DI)是将系统中各层对象解耦的一种方式,是实现控制反转(Inversion of Control,缩写IoC)的一种常用方式。

搭建框架

首先我们搭建一个用于测试的基本框架,因为最近在了解 FreeSql,所以数据访问就使用 FreeSql,在这里推荐一下这个项目,叶老板真的很🐂🍺。

下图可以看出项目的简单架构:

2019113010402820191130104028

实体

实体层定义了实体的接口 IBaseEntity

1
2
3
4
5
6
7
public class IBaseEntity
{
[Column(IsPrimary = true, IsIdentity = true)]
public long Id { get; set; }
public DateTime CreateTime { get; set; } = DateTime.Now;
public DateTime UpdateTime { get; set; } = DateTime.Now;
}

标本类 Specimen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Specimen : IBaseEntity
{
public DateTime InspectionDate { get; set; }
public int Type { get; set; }
public string SpeicimenNo { get; set; }
public int State { get; set; }
public string Barcode { get; set; }
public DateTime? ResultTime { get; set; }
public int? ResultUserId { get; set; }
public DateTime? CheckTime { get; set; }
public int? CheckUserId { get; set; }
public DateTime? ReportTime { get; set; }
public int? ReportUserId { get; set; }
}

服务接口

服务接口首先定义了服务常用的一些操作 IBaseService

1
2
3
4
5
6
7
public interface IBaseService<T> where T : IBaseEntity
{
T Insert(T t);
T Get(int id);
T Update(T t);
bool Delete(int id);
}

然后不同的业务实体,也有自己的业务逻辑 ISpecimenService

1
2
3
4
public interface ISpecimenService : IBaseService<Specimen>
{
Specimen CheckSpecimen(int specimenId, int userId);
}

服务实现

服务接口的实现,首先是 IBaseService 的实现 BaseService

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
public class BaseService<T> : IBaseService<T> where T : IBaseEntity
{
protected virtual IFreeSql FreeSql { get; set; }

public BaseService(IFreeSql freeSql)
{
FreeSql = freeSql;
}

public virtual bool Delete(int id)
{
return FreeSql.Delete<T>(id).ExecuteAffrows() > 0;
}

public virtual T Get(int id)
{
return FreeSql.Select<T>(id).First();
}

public virtual T Insert(T t)
{
int id = (int)FreeSql.Insert<T>().AppendData(t).ExecuteIdentity();
return FreeSql.Select<T>(id).First();
}

public virtual T Update(T t)
{
t.UpdateTime = DateTime.Now;
int rows = FreeSql.Update<T>(t).ExecuteAffrows();
if (rows == 1)
return FreeSql.Select<T>(t.Id).First();
else
return null;
}
}

ISpecimenService 的实现 SpecimenService

1
2
3
4
5
6
7
8
9
public class SpecimenService : BaseService<Specimen>, ISpecimenService
{
public SpecimenService(IFreeSql freeSql) : base(freeSql) { }

public Specimen CheckSpecimen(int specimenId, int userId)
{
return FreeSql.Update<Specimen>().Set(s => new { CheckUserId = userId, CheckTime = DateTime.Now }).ExecuteUpdated().First();
}
}

Web 站点

首先是需要创建用于初始化 FreeSql 的工厂 FreeSqlFactory

1
2
3
4
5
6
7
8
9
10
11
public class FreeSqlFactory
{
public static IFreeSql FreeSql { get; private set; }
static FreeSqlFactory()
{
FreeSql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(global::FreeSql.DataType.Sqlite, "Data Source=|DataDirectory|\\data.db;Pooling=true;Max Pool Size=10")
.UseAutoSyncStructure(true)
.Build();
}
}

这里主要是想让该项目简单一些,所以只考虑服务层 JohnSun.SOA.WebAPI.Service 的注入,所以直接将数据库访问层依赖细节,使用 SQLite 数据库,并使用 FreeSqlCodeFirst 模式。

然后就创建一个 Web API 控制器 SpecimensController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SpecimensController : ApiController
{
public Specimen Get(int id)
{
SpecimenService service = new SpecimenService(FreeSqlFactory.FreeSql);
return service.Get(id);
}

public Specimen Post(DateTime inspectionDate, string speicimenNo, int type, string barcode = null)
{
SpecimenService service = new SpecimenService(FreeSqlFactory.FreeSql);
return service.Insert(new Specimen { InspectionDate = inspectionDate, SpeicimenNo = speicimenNo, Type = type, Barcode = barcode });
}
}

这里只提供两个简单的方法,一个新增方法,一个通过Id查询数据。

访问测试

截止到目前,一个简单的测试项目创建完成,我们可以使用 postman 对项目进行简单的测试。

首先是调用 POST 方法,新增标本 http://localhost:58683/api/specimens?inspectionDate=2019-11-30&speicimenNo=1001&type=0

2019113010162120191130101621

返回的是一个创建成功后的数据库实体,我们可以通过这个对象的 id 再使用 GET 方法进行查询 http://localhost:58683/api/specimens/1

2019113010164320191130101643

Unity 实现依赖注入

为了方便理解,我们将移除依赖具体实现的步骤罗列出来,方便对控制反转的实现有一个简单的了解。

使用 Unity 创建对象

首先我们将对象的创建移交给 Unity,首先我们使用 nugetWeb 项目添加 Unity 的包引用:

1
Install-Package Unity -Version 5.11.1

然后我们添加一个统一的依赖注入容器,将需要用到的类型注册到容器中:

1
2
3
4
5
IUnityContainer container = new UnityContainer();
// 注册单例 因为 ISpecimenService 的构造函数需要提供一个 IFreeSql 的参数
container.RegisterInstance(FreeSqlFactory.FreeSql);
// 注册 SpecimenService 到 ISpecimenService
container.RegisterType<ISpecimenService, SpecimenService>();

这样我们创建 ISpecimenService 的实例对象就不需要再依赖细节 SpecimenService

1
ISpecimenService service = container.Resolve<ISpecimenService>();

调整为工厂

我们并不需要每次初始化 ISpecimenService 都创建一个依赖注入的容器,实际上这个容器只需要创建一次,所以我们增加一个 ContainerFactory 的类型:

1
2
3
4
5
6
7
8
9
10
public class ContainerFactory
{
public static IUnityContainer Container { get; private set; }
static ContainerFactory()
{
Container = new UnityContainer();
Container.RegisterInstance(FreeSqlFactory.FreeSql);
Container.RegisterType<ISpecimenService, SpecimenService>();
}
}

然后调整我们的控制器 SpecimensController

1
2
3
4
5
6
7
8
9
10
11
public Specimen Get(int id)
{
ISpecimenService service = ContainerFactory.Container.Resolve<ISpecimenService>();
return service.Get(id);
}

public Specimen Post(DateTime inspectionDate, string speicimenNo, int type, string barcode = null)
{
ISpecimenService service = ContainerFactory.Container.Resolve<ISpecimenService>();
return service.Insert(new Specimen { InspectionDate = inspectionDate, SpeicimenNo = speicimenNo, Type = type, Barcode = barcode });
}

调整为使用配置文件

因为 ContainerFactory 工厂类注册时仍然存在 SpecimenService,所以我们要将改类型从上层中移除,否则我们仍然无法指定 ISpecimenService

Unity 为我们提供了使用配置文件初始化依赖注入容器的方案,所以首先我们移除 Web 对于 JohnSun.SOA.WebAPI.Service 项目的引用,使服务不再强依赖于该项目,方便后期对产品的升级改造。

但是测试项目仍然需要一个实现,所以我们需要在移除依赖后,手动的将 JohnSun.SOA.WebAPI.Service/bin 目录下的文件拷贝到 JohnSun.SOA.WebAPI.Server/bin 下。

添加 Unity.Configuration 的引用:

1
Install-Package Unity.Configuration -Version 5.11.1

Web 项目中添加配置文件 ConfigFiles/Unity.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration" />
</configSections>

<unity>
<aliases>
<add alias="ISpecimenService" type="JohnSun.SOA.WebAPI.Interface.ISpecimenService, JohnSun.SOA.WebAPI.Interface" />
<add alias="SpecimenService" type="JohnSun.SOA.WebAPI.Service.SpecimenService, JohnSun.SOA.WebAPI.Service" />
</aliases>
<container>
<register type="ISpecimenService" mapTo="SpecimenService" />
</container>
</unity>
</configuration>

调整 ContainerFactory 的静态构造函数代码:

1
2
3
4
5
6
7
8
9
Container = new UnityContainer();
Container.RegisterInstance(FreeSqlFactory.FreeSql);

string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"ConfigFiles\Unity.config");
ExeConfigurationFileMap map = new ExeConfigurationFileMap { ExeConfigFilename = configPath };
Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);

section.Configure(Container);

这时在 Web 项目中已经不需要依赖 JohnSun.SOA.WebAPI.Service 的任何细节,均可以通过配置文件实现。

注意:

FreeSql 仍然依赖细节,前文已经提到为了测试项目的简单,所以就 不要在意细节 了。

后面写到这段代码的时候有考虑将 FreeSqlFactory.FreeSql 移交给配置文件,但是配置文件 instance 节点在 Unity 项目中给出的测试用例,无法指定 value 为一个程序中的字段或属性,value 只能设置一些常量值例如字符串或数字。

后面仔细想了一下,如果真的想移除依赖,可能需要增加一个类似 IFreeSqlInfo 的对象,将 IFreeSql 作为该类型的一个属性,这样我们还可以提供一个多个参数的构造函数,用来指定连接的数据库、数据库类型、连接字符串、是否启用数据库迁移等等。

当然,最终也要将 SpecimenService 构造函数中的 IFreeSql 调整成 IFreeSqlInfo 的实现才行。

控制器的注入

虽然已经成功添加容器,移除了细节的依赖,但是控制器中我们仍然需要使用 Resolve 构造对象,所以下一步我们需要将 Service 对象的构造也注入进来。

首先我们需要修改控制器的结构,将 SpecimensController 控制器调整为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SpecimensController : ApiController
{
private readonly ISpecimenService _specimenService;
public SpecimensController(ISpecimenService specimenService)
{
_specimenService = specimenService;
}

public Specimen Get(int id)
{
return _specimenService.Get(id);
}

public Specimen Post(DateTime inspectionDate, string speicimenNo, int type, string barcode = null)
{
return _specimenService.Insert(new Specimen { InspectionDate = inspectionDate, SpeicimenNo = speicimenNo, Type = type, Barcode = barcode });
}
}

这个时候如果直接运行项目肯定是不行的,因为控制器默认无参数的构造函数已经不存在了,会报错。

传统方式注入

根据 MSDN 上的文档,我们可以借助 Web API 定义用于解析依赖项的 IDependencyResolver 接口来实现控制器的注入。

1
2
3
4
5
6
7
8
9
10
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}

public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}

IDependencyScope 接口有两种方法:

  • GetService 创建一个类型的实例。
  • GetServices 创建指定类型的对象的集合。

IDependencyResolver 方法继承 IDependencyScope 并添加 BeginScope 方法。

当 Web API 创建控制器实例时,它将首先调用 IDependencyResolverGetService 方法,并传入控制器类型。我们可以借助该特性,来构造任何控制器,如果 GetService 返回 Null,Web API 将在控制器类上查找无参数的构造函数。

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
public class UnityResolver : IDependencyResolver
{
protected IUnityContainer container;

public UnityResolver(IUnityContainer container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
this.container = container;
}

public IDependencyScope BeginScope()
{
return new UnityResolver(container.CreateChildContainer());
}

public void Dispose()
{
Dispose(true);
}

protected virtual void Dispose(bool disposing)
{
container.Dispose();
}

public object GetService(Type serviceType)
{
try
{
return ContainerFactory.Container.Resolve(serviceType);
}
catch (ResolutionFailedException)
{
return null;
}
}

public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return ContainerFactory.Container.ResolveAll(serviceType);
}
catch (ResolutionFailedException)
{
return new List<object>();
}
}
}

最后我们需要将 HttpConfiguration 中的 DependencyResolver 属性替换为我们实现的 UnityResolver

修改 WebApiConfig.Register 方法,增加以下代码:

1
config.DependencyResolver = new UnityResolver(ContainerFactory.Container);

使用 Unity.WebAPI

Unity 提供了一个包,方便我们控制器的注入,首先要通过 nuget 安装包:

1
Install-Package Unity.WebAPI -Version 5.4.0

同之前一样,修改 WebApiConfig.Register 方法,增加以下代码:

1
config.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(ContainerFactory.Container);

UnityDependencyResolver 类型是帮我们实现的 IDependencyResolver 接口,通过反编译代码可以了解和我们从 MSDN 的常规实现基本一致:

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
// Unity.WebApi.UnityDependencyScope
using System;
using System.Collections.Generic;
using System.Web.Http.Controllers;
using System.Web.Http.Dependencies;
using Unity;
using Unity.Resolution;

public class UnityDependencyScope : IDependencyScope, IDisposable
{
protected IUnityContainer Container
{
get;
private set;
}

public UnityDependencyScope(IUnityContainer container)
{
Container = container;
}

public object GetService(Type serviceType)
{
if (typeof(IHttpController).IsAssignableFrom(serviceType))
{
return UnityContainerExtensions.Resolve(Container, serviceType, (ResolverOverride[])(object)new ResolverOverride[0]);
}
try
{
return UnityContainerExtensions.Resolve(Container, serviceType, (ResolverOverride[])(object)new ResolverOverride[0]);
}
catch
{
return null;
}
}

public IEnumerable<object> GetServices(Type serviceType)
{
return UnityContainerExtensions.ResolveAll(Container, serviceType, (ResolverOverride[])(object)new ResolverOverride[0]);
}

public void Dispose()
{
((IDisposable)Container).Dispose();
}
}

// Unity.WebApi.UnityDependencyResolver
using System;
using System.Web.Http.Dependencies;
using Unity;
using Unity.WebApi;

public class UnityDependencyResolver : UnityDependencyScope, IDependencyResolver, IDependencyScope, IDisposable
{
public UnityDependencyResolver(IUnityContainer container)
: base(container)
{
}

public IDependencyScope BeginScope()
{
return (IDependencyScope)(object)new UnityDependencyScope(base.Container.CreateChildContainer());
}
}

Autofac 实现依赖注入

相较于 Unity,其实 Autofac 使用更广泛,普遍认为后者的性能表现要优于前者,并且后者拥有中文文档,社区更活跃。

代码中为容器注册类型

本质上 Autofac 与前者差异不大,所以这里只是简单的介绍一下使用,更多的知识可以了解官方的文档。

这里添加一个 AutofacContainerFactory 的类型:

1
2
3
4
5
6
7
8
9
10
11
12
public class AutofacContainerFactory
{
public static IContainer Container { get; private set; }
static AutofacContainerFactory()
{
// Autofac 在程序中注册
var builder = new ContainerBuilder();
builder.RegisterInstance(FreeSqlFactory.FreeSql);
builder.RegisterType<SpecimenService>().As<ISpecimenService>();
Container = builder.Build();
}
}

这时无法运行程序,甚至会报错,因为基于 Unity 的那套代码,Service 的项目依赖已经移除,这里会报错。所以这里知识代码演示。

使用配置文件

由于 Autofac 4.0+ 配置依赖于 Microsoft.Extensions.Configuration.XmlMicrosoft.Extensions.Configuration.Json,并且这两个项目依赖于 .NET Standard 2.0,所以需要将项目都升级到 .NET Framework 4.6.1+

另外为了能让项目成功运行起来,并且使用的是 Autofac 实现依赖注入,我们还需要引用几个 nuget 程序包:

  • Autofac.Configuration:为 autofac 提供配置功能。
  • Autofac.WebApi2:ASP.NET Web API 控制器的依赖注入。
  • Microsoft.Extensions.Configuration.Json:如果使用 json 文件配置 autofac 添加该引用。
  • Microsoft.Extensions.Configuration.Xml:如果使用 xml 文件配置 autofac 添加该引用。

调整 AutofacContainerFactory 静态构造函数中的代码:

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
public class AutofacContainerFactory
{
public static IContainer Container { get; private set; }
static AutofacContainerFactory()
{
// Autofac 在程序中注册
//var builder = new ContainerBuilder();
//builder.RegisterInstance(FreeSqlFactory.FreeSql);
//builder.RegisterType<Service.SpecimenService>().As<ISpecimenService>();
//builder.RegisterApiControllers(AppDomain.CurrentDomain.GetAssemblies());
//Container = builder.Build();

// Autofac 使用配置文件注册
var builder = new ContainerBuilder();
builder.RegisterInstance(FreeSqlFactory.FreeSql);

var config = new ConfigurationBuilder();

// Json 文件
//string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"ConfigFiles\Autofac.json");
//config.AddJsonFile(configPath);

// Xml 文件
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"ConfigFiles\Autofac.config");
config.AddXmlFile(configPath);

var module = new ConfigurationModule(config.Build());
builder.RegisterModule(module);

builder.RegisterApiControllers(AppDomain.CurrentDomain.GetAssemblies());

Container = builder.Build();
}
}

添加对应的配置文件到 ConfigFiles 文件夹,Autofac.config 文件:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8" ?>
<autofac>
<components name="0">
<type>JohnSun.SOA.WebAPI.Service.SpecimenService, JohnSun.SOA.WebAPI.Service</type>
<services name="0" type="JohnSun.SOA.WebAPI.Interface.ISpecimenService, JohnSun.SOA.WebAPI.Interface" />
</components>
</autofac>

Autofac.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
{
"components": [
{
"type": "JohnSun.SOA.WebAPI.Service.SpecimenService, JohnSun.SOA.WebAPI.Service",
"services": [
{
"type": "JohnSun.SOA.WebAPI.Interface.ISpecimenService, JohnSun.SOA.WebAPI.Interface"
}
]
}
]
}

最终修改我们的 WebApiConfig.Register 方法,将 config.DependencyResolver 修改为AutofacWebApiDependencyResolver,注意这个类型是在 Autofac.WebApi2 中的类型:

1
config.DependencyResolver = new Autofac.Integration.WebApi.AutofacWebApiDependencyResolver(AutofacContainerFactory.Container);

这时,如果没有配置错误,就可以正常访问我们的控制器获取数据,需要注意的是:

  • ConfigFiles 文件夹下的配置文件属性“复制到输出目录”应该修改为:“如果较新则复制”。
  • 工厂类 AutofacContainerFactory 构建容器时一定要调用 builder.RegisterApiControllers() 方法,不同于 Unity 的原因可以参看后文 AutofacWebApiDependencyResolver 类型的反编译代码。

AutofacWebApiDependencyResolver 类型

反编译 nugetAutofac.WebApi2.nupkg 中的 Autofac.Integration.WebApi.dll 文件可以找到该类型:

Autofac.Integration.WebApi.AutofacWebApiDependencyScope

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
using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Dependencies;

/// <summary>
/// Autofac implementation of the <see cref="T:System.Web.Http.Dependencies.IDependencyScope" /> interface.
/// </summary>
public class AutofacWebApiDependencyScope : IDependencyScope, IDisposable
{
private bool _disposed;

private readonly ILifetimeScope _lifetimeScope;

/// <summary>
/// Gets the lifetime scope for the current dependency scope.
/// </summary>
public ILifetimeScope LifetimeScope => _lifetimeScope;

/// <summary>
/// Initializes a new instance of the <see cref="T:Autofac.Integration.WebApi.AutofacWebApiDependencyScope" /> class.
/// </summary>
/// <param name="lifetimeScope">The lifetime scope to resolve services from.</param>
public AutofacWebApiDependencyScope(ILifetimeScope lifetimeScope)
{
if (lifetimeScope == null)
{
throw new ArgumentNullException("lifetimeScope");
}
_lifetimeScope = lifetimeScope;
}

/// <summary>
/// Finalizes an instance of the <see cref="T:Autofac.Integration.WebApi.AutofacWebApiDependencyScope" /> class.
/// </summary>
~AutofacWebApiDependencyScope()
{
Dispose(disposing: false);
}

/// <summary>
/// Try to get a service of the given type.
/// </summary>
/// <param name="serviceType">ControllerType of service to request.</param>
/// <returns>An instance of the service, or null if the service is not found.</returns>
public object GetService(Type serviceType)
{
return ResolutionExtensions.ResolveOptional((IComponentContext)(object)_lifetimeScope, serviceType);
}

/// <summary>
/// Try to get a list of services of the given type.
/// </summary>
/// <param name="serviceType">ControllerType of services to request.</param>
/// <returns>An enumeration (possibly empty) of the service.</returns>
public IEnumerable<object> GetServices(Type serviceType)
{
if (!ResolutionExtensions.IsRegistered((IComponentContext)(object)_lifetimeScope, serviceType))
{
return Enumerable.Empty<object>();
}
Type enumerableServiceType = typeof(IEnumerable<>).MakeGenericType(serviceType);
return (IEnumerable<object>)ResolutionExtensions.Resolve((IComponentContext)(object)_lifetimeScope, enumerableServiceType);
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && _lifetimeScope != null)
{
((IDisposable)_lifetimeScope).Dispose();
}
_disposed = true;
}
}
}

Autofac.Integration.WebApi.AutofacWebApiDependencyResolver

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
using Autofac;
using Autofac.Core.Lifetime;
using Autofac.Integration.WebApi;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

/// <summary>
/// Autofac implementation of the <see cref="T:System.Web.Http.Dependencies.IDependencyResolver" /> interface.
/// </summary>
public class AutofacWebApiDependencyResolver : IDependencyResolver, IDependencyScope, IDisposable
{
private bool _disposed;

private readonly ILifetimeScope _container;

private readonly IDependencyScope _rootDependencyScope;

private readonly Action<ContainerBuilder> _configurationAction;

/// <summary>
/// Gets the root container provided to the dependency resolver.
/// </summary>
public ILifetimeScope Container => _container;

/// <summary>
/// Initializes a new instance of the <see cref="T:Autofac.Integration.WebApi.AutofacWebApiDependencyResolver" /> class.
/// </summary>
/// <param name="container">The container that nested lifetime scopes will be create from.</param>
/// <param name="configurationAction">A configuration action that will execute during lifetime scope creation.</param>
public AutofacWebApiDependencyResolver(ILifetimeScope container, Action<ContainerBuilder> configurationAction)
: this(container)
{
if (configurationAction == null)
{
throw new ArgumentNullException("configurationAction");
}
_configurationAction = configurationAction;
}

/// <summary>
/// Initializes a new instance of the <see cref="T:Autofac.Integration.WebApi.AutofacWebApiDependencyResolver" /> class.
/// </summary>
/// <param name="container">The container that nested lifetime scopes will be create from.</param>
public AutofacWebApiDependencyResolver(ILifetimeScope container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
_container = container;
_rootDependencyScope = (IDependencyScope)(object)new AutofacWebApiDependencyScope(container);
}

/// <summary>
/// Finalizes an instance of the <see cref="T:Autofac.Integration.WebApi.AutofacWebApiDependencyResolver" /> class.
/// </summary>
~AutofacWebApiDependencyResolver()
{
Dispose(disposing: false);
}

/// <summary>
/// Try to get a service of the given type.
/// </summary>
/// <param name="serviceType">Type of service to request.</param>
/// <returns>An instance of the service, or null if the service is not found.</returns>
public virtual object GetService(Type serviceType)
{
return _rootDependencyScope.GetService(serviceType);
}

/// <summary>
/// Try to get a list of services of the given type.
/// </summary>
/// <param name="serviceType">ControllerType of services to request.</param>
/// <returns>An enumeration (possibly empty) of the service.</returns>
public virtual IEnumerable<object> GetServices(Type serviceType)
{
return _rootDependencyScope.GetServices(serviceType);
}

/// <summary>
/// Starts a resolution scope. Objects which are resolved in the given scope will belong to
/// that scope, and when the scope is disposed, those objects are returned to the container.
/// </summary>
/// <returns>
/// The dependency scope.
/// </returns>
public IDependencyScope BeginScope()
{
return (IDependencyScope)(object)new AutofacWebApiDependencyScope((_configurationAction == null) ? _container.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag) : _container.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag, _configurationAction));
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && _rootDependencyScope != null)
{
((IDisposable)_rootDependencyScope).Dispose();
}
_disposed = true;
}
}
}

参考:

源码下载:

MacOS 使用 DBeaver 连接 Oracle 数据库

作为“专业”的商业数据库,Oracle 不允许第三方公司私自集成 Oracle 数据库的 jdbc 连接驱动,所以 DBeaver 不能像其他数据库那样,自动的为我们下载驱动文件。

DBeaver 添加连接

添加 Oracle 连接会出现如下配置:

2019120210091220191202100912

如上图,点击“测试连接”会提示让我们配置连接驱动:

2019120210094020191202100940

下载 jar 包

如果数据库服务器是 11g12c,根据数据库版本选择框下的连接,到 Oracle 官网登录下载即可。

这里我安装的是 19c,直接到根据指引下载最新的 jar 包:18.3 JDBC and UCP Downloads page

其他的一些指引信息可以参考:Quick Start with JDBC

配置驱动

点击弹出缺少驱动文件窗体中的 添加 jar 包 或点击 Oracle 连接设置界面的 编辑驱动设置 可以进入以下页面:

2019120210124420191202101244

按如图所示,将我们下载的文件解压到一个合适的位置,点击 添加文件 将如图所示的三个文件添加进列表即可。

完成后点击确定可能会报错,可以忽略报错信息,直接从连接配置界面,重新点击 测试连接 即可确认驱动是否正常。

当然还可能会仍然无法连接,但是需要确认的是驱动是否正常,例如提示网络错误或登录用户密码错误等信息,就是另外一个问题了,解决即可。

491E0BFC-E982-46AF-9E64-3857658E55E2491E0BFC-E982-46AF-9E64-3857658E55E2

SOA —— ASP.NET Web API 知识点小结

ASP.NET Web API 是一个框架,可以轻松地构建包含浏览器和移动设备在内的广泛客户端的 HTTP 服务。

ASP.NET Web API 是在 .NET Framework 框架上构建 REST 风格应用程序的理想平台。

什么是 RESTful

表现层状态转换(英语: Representational State Transfer ,缩写: REST )是一种架构风格,方便不同软件或程序在网络中互相传递消息。表现层状态转换是基于 HTTP 之上而确定的一组约定和属性,是一种设计提供全球资讯网络服务的软件构建风格。

符合或兼容这种架构风格(简称为 REST 或 RESTful )的网络服务,目前主流的 Web 服务实现方案中,因为 REST 模式与复杂的 SOAP 和 XML-RPC 相比更加简洁,所以越来越多的服务开始采用 REST 风格设计和实现。

REST 架构的要点

需要注意的是, REST 是设计风格而不是标准。 REST 通常基于使用 HTTP , URI ,和 XML 以及 HTML 这些现有的广泛流行的协议和标准。

  • 资源是由URI来指定。
  • 对资源的操作包括获取、创建、修改和删除资源,这些操作正好对应 HTTP 协议提供的 GET 、 POST 、 PUT 和 DELETE 方法。
  • 通过操作资源的表现形式来操作资源。
  • 资源的表现形式则是 XML 或者 HTML ,取决于读者是机器还是人,是消费 web 服务的客户软件还是 web 浏览器。当然也可以是任何其他的格式,例如 JSON 。

REST 架构的约束条件

REST 架构风格最重要的架构约束有 6 个:

  • 客户端-服务器(Client-Server)
  • 无状态(Stateless)
  • 缓存(Cacheability)
  • 统一接口(Uniform Interface)
  • 分层系统(Layered System)
  • 按需代码(Code-On-Demand,可选)

应用于 Web 服务

符合 REST 设计风格的 Web API 称为 RESTful API 。它从以下三个方面资源进行定义:

  • 直观简短的资源地址:URI,比如:http://example.com/resources
  • 传输的资源:Web 服务接受与返回的互联网媒体类型,比如:JSON,XML,YAML等。
  • 对资源的操作:Web服务在该资源上所支持的一系列请求方法(比如:POST,GET,PUT或DELETE)。

下表列出了实现 RESTful API 时 HTTP 请求方法的典型用途。

资源 一组资源的URI,比如https://example.com/resources 单个资源的URI,比如https://example.com/resources/142
GET 列出URI,以及该资源组中每个资源的详细信息(后者可选)。 获取指定的资源的详细信息,格式可以自选一个合适的网络媒体类型(比如:XML、JSON等)
PUT 使用给定的一组资源替换当前整组资源。 替换/创建指定的资源。并将其追加到相应的资源组中。
POST 在本组资源中创建/追加一个新的资源。该操作往往返回新资源的URL。 把指定的资源当做一个资源组,并在其下创建/追加一个新的元素,使其隶属于当前资源。
DELETE 删除整组资源。 删除指定的元素。

创建 ASP.NET Web API

使用 Visual Studio 可以直接创建 ASP.NET Web API,不过需要注意的是框架需要选择 .NET Framework 4.5+

2019112618230420191126182304

创建成功以后,可以直接找到视图中的 Home/Index.cshtml 将项目运行起来:

2019112618590620191126185906

可以注意到 Controllers 文件夹中,创建了一个默认的 ValuesController,其继承自 ApiController

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace JohnSun.SOA.WebAPI.Server.Controllers
{
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/values/5
public string Get(int id)
{
return "value";
}

// POST api/values
public void Post([FromBody]string value)
{
}

// PUT api/values/5
public void Put(int id, [FromBody]string value)
{
}

// DELETE api/values/5
public void Delete(int id)
{
}
}
}

我们可以通过浏览器来直接访问 http://localhost:58683/api/values

2019112619025720191126190257

浏览器是 GET 请求,所以返回的是如上内容,如果需要使用其他请求,可以使用 postman 进行调试。

2019112619063320191126190633

路由

首先我们自定义一个控制器用于测试:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace JohnSun.SOA.WebAPI.Server.Controllers
{
public class UsersController : ApiController
{
public static List<User> Users = new List<User>
{
new User{ Id = 1, Type = UserType.Admin, Name = "Admin" },
new User{ Id = 2, Type = UserType.Admin, Name = "John", Sex = UserSex.Male },
new User{ Id = 3, Name = "Kangkang", Sex = UserSex.Male },
new User{ Id = 4, Name = "Micheal", Sex = UserSex.Male },
new User{ Id = 5, Name = "Jane", Sex = UserSex.Female },
new User{ Id = 6, Name = "Meimei", Sex = UserSex.Female },
new User{ Id = 7, Name = "Xiaoli", Sex = UserSex.Female },
};

public List<User> GetAll()
{
return Users;
}

public User GetUser(int id)
{
return Users.Find(u => u.Id == id);
}
}
public class User
{
public int Id { get; set; }
public UserType Type { get; set; }
public string Name { get; set; }
public UserSex Sex { get; set; }
}

public enum UserType
{
Normal,
Admin,
}

public enum UserSex
{
Unknown,
Male,
Female,
}
}

同默认创建的控制器啊一样,可以直接通过 postman 或浏览器访问:

2019112710354920191127103549

模板路由

MVC 项目一样,如果需要了解项目的配置,可以找到程序的入口进行了解,打开 Global.asax.cs 文件,查看 Application_Start() 方法,其中有这么一段:

1
GlobalConfiguration.Configure(WebApiConfig.Register);

转到 WebApiConfig.Register 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务

// Web API 路由
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

由上可以看到注册了路由规则,api 代表在资源前面要带上 api 目录,{controller} 为请求资源的控制器名称,{id} 为代表一个资源的 ID ,而下面指定的 RouteParameter.Optional 则表示这个参数是可选的。

默认的路由就是模板路由,路由规则是遵循 REST 风格的,可以理解为一个控制器代表一类资源。如果没有特殊的需求,不建议修改该路由规则。

特殊占位符除了默认的 {controller} ,还支持 {action} ,如果模板中配置,则该位置标志对应控制器中的方法。

例如我们将模板路由修改为 routeTemplate: api/{controller}/{action}/{id} ,如果我们仍然需要调用 UsersController 中的方法就需要指定调用的方法名,如果我们不调整将出现报错 http://localhost:58683/api/users

2019112712034120191127120341

这时就需要我们指定需要调用的控制器的方法 http://localhost:58683/api/users/getall

2019112712051020191127120510

默认控制器中占位符 {id} 为自定义占位符,defaults: new { id = RouteParameter.Optional } 指定了其默认值,表示其为一个可选参数。

除可以指定默认值以外,还可以通过正则指定录入值的规则,可以通过入参 constraints 指定,如下:

1
2
3
4
5
6
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d{1,2}" }
);

需要注意的是, constraints 优先级要比 RouteParameter.Optional 高,例如以上设定的路由模板,如果不传输ID,将返回 404

但是如果指定是默认值,例如 defaults: new { id = 1 },未录入也可以访问到资源。

特性路由

除路由模板外,还可以在控制器与控制器方法上标记 RouteAttribute 来定义特性路由,例如我们在 UsersController 增加两个带有特性路由的方法:

1
2
3
4
5
6
7
8
9
10
11
[Route("api/admins")]
public List<User> GetAllAdmin()
{
return Users.FindAll(u => u.Type == UserType.Admin);
}

[Route("api/admins/{id}")]
public User GetAdmin(int id)
{
return Users.Find(u => u.Type == UserType.Admin && u.Id == id);
}

这样我们就可以通过 http://localhost:58683/api/admins 来请求管理员身份的用户信息。

特性路由除了上文中演示的重载我们的 URI 外,还可以实现 API 的版本控制,例如定义为: api/v1/usersapi/v2/users

另外还可以用于传输多个参数,例如:

1
2
3
4
5
[Route("api/users/type/{type}/sex/{sex}")]
public User GetUser(UserType type, UserSex sex)
{
return Users.Find(u => u.Type == type && u.Sex == sex);
}

我们可以使用 http://localhost:58683/api/users/type/admin/sex/male 来访问:

2019112719471320191127194713

HTTP 方法

Web API 除匹配路由规则外,还将选择操作请求 ( GETPOST 等) 的 HTTP 方法。 默认情况下,Web API 将查找与控制器方法名称的开头的不区分大小写匹配。 例如,名为的控制器方法 PutCustomers 匹配 HTTP PUT 请求。

以下属性来修饰的方法与任何替代此约定:

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

以下代码测试了标记了 HTTP 方法特性的 GetTest 方法,如果没有标记 [HttpPost] ,该方法只能使用 GET 请求,但是标记之后将只能使用 POST 请求。

1
2
3
4
5
6
[HttpPost]
[Route("api/users/test")]
public string GetTest()
{
return "test";
}

GET 请求:

2019112719591120191127195911

POST 请求:

2019112719592220191127195922

如果两种都需要,可以标记多个,在添加一个 [HttpGet] 特性即可。

对于其他的 HTTP 方法,包括非标准的方法,使用 AcceptVerbs 特性:

1
2
3
4
5
6
[AcceptVerbs("View")]
[Route("api/users/test")]
public string GetTest()
{
return "test";
}

2019112720061620191127200616

路由前缀

通常情况下,所有以相同前缀开头的控制器中的路由,可以通过使用针对整个控制器设置公共前缀 [RoutePrefix] 特性:

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
public class BooksController : ApiController
{
[Route("api/books")]
public IEnumerable<Book> GetBooks() { ... }

[Route("api/books/{id:int}")]
public Book GetBook(int id) { ... }

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
}

// 资源请求链接与上一段代码一致
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// GET api/books
[Route("")]
public IEnumerable<Book> Get() { ... }

// GET api/books/5
[Route("{id:int}")]
public Book Get(int id) { ... }

// POST api/books
[Route("")]
public HttpResponseMessage Post(Book book) { ... }
}

如果不想使用路由前缀,可以使用 ~ 符号:

1
2
3
4
5
6
7
8
9
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// GET /api/authors/1/books
[Route("~/api/authors/{authorId:int}/books")]
public IEnumerable<Book> GetByAuthor(int authorId) { ... }

// ...
}

路由前缀可以包含参数:

1
2
3
4
5
6
7
[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
// GET customers/1/orders
[Route("orders")]
public IEnumerable<Order> Get(int customerId) { ... }
}

路由约束

路由约束让你限制如何匹配路由模板中的参数。 常规语法 {参数名:约束} 。 例如:

1
2
3
4
5
[Route("api/users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("api/users/{name}")]
public User GetUserByName(string name) { ... }

在这里,第一个路由将仅选择 URI 段中 id 是一个整数,例如 http://localhost:58683/api/users/1001 。否则,将选择第二个路由,例如 http://localhost:58683/api/users/John

下表列出了支持的约束:

约束 描述 示例
alpha 匹配大写或小写字符 (a-z、 A-Z) {x: alpha}
bool 匹配一个布尔值。 {x: bool}
datetime 匹配项DateTime值。 {x: datetime}
decimal 匹配十进制值。 {x:decimal}
double 64 位浮点值匹配。 {x:double}
float 32 位浮点值匹配。 {x: float}
guid 匹配的 GUID 值。 {x: guid}
int 32 位整数值匹配。 {x: int}
length 与具有指定长度或长度的指定范围内的字符串匹配。 {x:length(6)} {x:length(1,20)}
long 64 位整数值匹配。 {x: long}
max 匹配一个整数,其最大值。 {x:max(10)}
maxlength 与最大长度的字符串匹配。 {x:maxlength(10)}
min 匹配一个整数,其最小值。 {x: min(10)}
minlength 与最小长度的字符串相匹配。 {x: minlength(10)}
range 一个整数值的范围内的匹配项。 {x: range(10,50)}
正则表达式 与正则表达式匹配。 {x:regex(^\d{3}-\d{3}-\d{4}$)}

注意:某些约束,如 min ,在括号中采用自变量。可以将多个约束应用于参数,用冒号分隔。

1
2
[Route("api/users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

除通过以上约束限制入参外,还可以通过实现 IHttpRouteConstraint 接口来创建自定义路由约束。例如,以下将限制参数为非零的整数值:

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
public class NonZeroConstraint : IHttpRouteConstraint
{
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
long longValue;
if (value is long)
{
longValue = (long)value;
return longValue != 0;
}

string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
if (Int64.TryParse(valueString, NumberStyles.Integer,
CultureInfo.InvariantCulture, out longValue))
{
return longValue != 0;
}
}
return false;
}
}

下面的代码演示如何注册该约束:

1
2
3
4
5
6
7
8
9
10
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

config.MapHttpAttributeRoutes(constraintResolver);
}
}

现在可以将约束应用在路由中:

1
2
[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

注:也可以替换整个 DefaultInlineConstraintResolver 类的实现 IInlineConstraintResolver 接口。执行此操作将替换的所有内置的约束,除非你实现 IInlineConstraintResolver 接口再将它们添加。

路由顺序

当框架尝试匹配路由的 URI 时,它会按特定的顺序匹配路由。若要指定顺序,可以设置 Route 特性的 RouteOrder 的值,默认顺序值为零。

下面是确认最终排序的方式:

  • 比较 Route 特性的 RouteOrder 属性。
  • 查看路由模板中的每个 URI 段。为每个段进行排序,如下所示:
    1. 文本内容。
    2. 受路由约束限制的路由参数。
    3. 不受限制的路由参数。
    4. 受路由约束的通配符路由参数。
    5. 不受限制的通配符路由参数。
  • 一个对路由排序的路由模板,使用不区分大小写的字符串序列进行比较 (OrdinalIgnoreCase) 。

假设定义以下控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[RoutePrefix("orders")]
public class OrdersController : ApiController
{
[Route("{id:int}")] // constrained parameter
public HttpResponseMessage Get(int id) { ... }

[Route("details")] // literal
public HttpResponseMessage GetDetails() { ... }

[Route("pending", RouteOrder = 1)]
public HttpResponseMessage GetPending() { ... }

[Route("{customerName}")] // unconstrained parameter
public HttpResponseMessage GetByCustomer(string customerName) { ... }

[Route("{*date:datetime}")] // wildcard
public HttpResponseMessage Get(DateTime date) { ... }
}

这些路由匹配顺序如下所示:

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

注:details 是一个文本段所以会出现在 {id} 之前,但 pending 设置了 RouteOrder1 ,所以会显示在其他未设置该属性,默认 RouteOrder0 的路由之后。 (本示例假定没有客户名为 detailspending 的客户。一般情况下,应该避免不明确的路由。在此示例中,更适合 GetByCustomer 的路由模板是 customers/{customerName}

参考:

SOA —— WCF 知识点小结

WCF 相较于 WebService 更加灵活,支持多种宿主、多种协议,并且支持双工。

印象中读书时也开过 WCF 的课程,但是那本讲 WCF 的书实在是太厚了,所以其实并没有学进去,混个学分后来不了了之。

工作中,使用更多的是 WebService 或者 Socket,所以仅凭偶尔接触的一些小例子,来总结一些简单的使用场景。

创建服务端

不同于 WebService 只能托管在 IIS 上,WCF 可以托管在任意的程序上,可以是网站,也可以是控制台、WinFormWPFWindows Service 等。

注意:Visual Studio 2019 安装时默认没有勾选 WPF 的模板,可以通过 Visual Studio Installer 安装,点击 修改 -> 单个组件 -> 开发活动 -> Windows Communication Foundation

2019112118365420191121183654

定义并实现服务协定

WCF 创建服务端第一步是创建一个服务协定,这样我们就可以很方便的将服务与宿主分离。

后期创建服务端时,可以指定服务运行在不同的宿主上,而不需要调整服务本身的代码,这里我们创建一个 WCF 服务库

2019112118455520191121184555

创建完成以后完善一下服务协定中的代码,需要在接口中指定方法,然后再实现。

IMyService.cs 文件:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace JohnSun.SOA.WCF.Service
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IService1”。
[ServiceContract]
public interface IMyService
{
[OperationContract]
string GetData(int value);

[OperationContract]
CompositeType GetDataUsingDataContract(CompositeType composite);

// TODO: 在此添加您的服务操作

[OperationContract]
decimal Sum(decimal x, decimal y);

[OperationContract]
UserInfo GetUser(int id);

[OperationContract]
UserInfo[] GetUsers(string country);

[OperationContract]
List<UserInfo> GetAllUsers();
}
}

MyService.cs 文件:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace JohnSun.SOA.WCF.Service
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的类名“Service1”。
public class MyService : IMyService
{
private static List<UserInfo> _users = new List<UserInfo>()
{
new UserInfo(){ Id = 1, Name = "Kangkang", Country = "China" },
new UserInfo(){ Id = 2, Name = "John", Country = "America" },
new UserInfo(){ Id = 3, Name = "Jane", Country = "France" },
new UserInfo(){ Id = 4, Name = "Han Meimei", Country = "China" },
};

public List<UserInfo> GetAllUsers()
{
return _users;
}

public string GetData(int value)
{
return string.Format("You entered: {0}", value);
}

public CompositeType GetDataUsingDataContract(CompositeType composite)
{
if (composite == null)
{
throw new ArgumentNullException("composite");
}
if (composite.BoolValue)
{
composite.StringValue += "Suffix";
}
return composite;
}

public UserInfo GetUser(int id)
{
return _users.Find(u => u.Id == id);
}

public UserInfo[] GetUsers(string country)
{
return _users.FindAll(u => u.Country == country).ToArray();
}

public decimal Sum(decimal x, decimal y)
{
return x + y;
}
}
}

使用宿主运行服务端

WCF 的服务协定创建好以后,就可以使用控制台或窗体程序等将该服务托管并运行起来。

这里以控制台为例,创建一个控制台应用程序,并添加以下代码:

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
static void Main(string[] args)
{
ServiceHost selfHost = null;

try
{
// 定义服务运行的 URL 并实例化服务主机
Uri baseAddress = new Uri("http://localhost:7895/MyService/");
selfHost = new ServiceHost(typeof(MyService), baseAddress);

// 指定服务协定
selfHost.AddServiceEndpoint(typeof(IMyService), new WSHttpBinding(), "MyService");

// 指定服务可以使用 HTTP 访问
ServiceMetadataBehavior smb = new ServiceMetadataBehavior
{
HttpGetEnabled = true,
};
selfHost.Description.Behaviors.Add(smb);

// 开启服务
selfHost.Open();
Log(LogLevel.Info, "服务开启成功,你可以点击 <Enter> 终止服务。");

Console.ReadLine();
selfHost.Close();
}
catch (CommunicationException ce)
{
Log(LogLevel.Error, $"开启服务失败 {ce.Message}");
if (selfHost != null)
selfHost.Abort();
Console.ReadKey();
}
}

注意:服务端如果需要运行,需要使用管理员身份,否则可能出现下图错误,可以使用管理员身份运行 Visual Studio,或者到 bin 目录下右键可执行程序,使用右键菜单中的 以管理员身份运行

2019112120081620191121200816

成功运行,控制台输出如下:

2019112120084020191121200840

我们可以在浏览器中访问设置的链接:

2019112214373420191122143734

新建客户端调用服务

服务端创建完成以后,就可以创建一个客户端连接服务,执行简单的调用。

首先创建一个控制台应用程序用于演示,和 WebService 一样,直接在 引用 上右键添加 服务引用 即可。

引用完成以后,会生成一个配置文件 app.config ,里面记录着连接服务端的配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IMyService" />
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:7895/MyService/" binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IMyService" contract="MyWcfService.IMyService"
name="WSHttpBinding_IMyService">
<identity>
<userPrincipalName value="DESKTOP-V2RPVP4\hd2y" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>

修改控制台的 Program.cs 文件进行调用:

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
MyServiceClient client = null;
try
{
client = new MyServiceClient();
decimal x = 1m;
decimal y = 2m;
decimal d = client.Sum(x, y);
Log(LogLevel.Info, $"调用 Sum 方法成功:Sum({x}, {y}) = {d}");
int id = 1;
UserInfo info = client.GetUser(id);
JavaScriptSerializer serializer = new JavaScriptSerializer();
Log(LogLevel.Info, $"调用 GetUserInfo 方法成功:GetUserInfo({id}) = {serializer.Serialize(info)}");
string country = "China";
UserInfo[] users = client.GetUsers(country);
Log(LogLevel.Info, $"调用 GetUsers 方法成功:GetUsers({country}) 获取到用户数量: {users.Length}");
UserInfo[] allUsers = client.GetAllUsers();
Log(LogLevel.Info, $"调用 GetAllUsers 方法成功,获取到用户数量: {allUsers.Length}");
client.Close();
}
catch (Exception exc)
{
if (client != null)
client.Abort();
Log(LogLevel.Error, "测试服务失败:" + exc.Message);
}
Console.ReadKey();

注意,同 WebService 一样,连接的释放不能依托于 using 块。

自定义连接

如果不想通过自动生成的服务配置连接服务,可以调整客户端初始化为以下代码:

1
client = new MyServiceClient(new WSHttpBinding(), new EndpointAddress("http://localhost:7895/MyService/"));

运行的结果和配置文件式初始化的执行结果一致:

2019112514135520191125141355

WCF 进阶

以上只是类似 WebService 服务的创建与调用,还不能体现 WCF 的优越性,下面一些简单的例子将展示 WCF 的配置,将 WCF 服务承载于 TCP 上。并且演示 WCF 的双工如何使用。

通过配置文件设定服务

除了直接在代码中配置托管服务外,我们也可以通过配置文件来进行设定,配置信息可以参考 WCF 配置架构

我们可以通过一个 简化配置 ,来配置我们的客户端,以下是服务端控制台 App.config 的文件内容。

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
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="MyWcfServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" httpHelpPageEnabled="false" />
<serviceTimeouts transactionTimeout="00:10:00" />
<serviceThrottling maxConcurrentCalls="1000" maxConcurrentInstances="1000" maxConcurrentSessions="1000" />
</behavior>
</serviceBehaviors>
</behaviors>

<bindings>
<basicHttpBinding>
<binding name="MyBindingConfig" maxBufferSize="1024" maxReceivedMessageSize="1024" closeTimeout="00:01:00" />
<!-- Default binding for basicHttpBinding -->
<binding closeTimeout="00:03:00" />
</basicHttpBinding>
</bindings>

<services>
<service behaviorConfiguration="MyWcfServiceBehavior" name="JohnSun.SOA.WCF.Service.MyService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:7895/MyService/" />
</baseAddresses>
</host>
<endpoint address="" binding="basicHttpBinding" contract="JohnSun.SOA.WCF.Service.IMyService" bindingConfiguration="MyBindingConfig" />
</service>
</services>
</system.serviceModel>
</configuration>

配置完成后就可以通过重新生成并由客户端调用,注意之前绑定的协议为 wsHttpBinding,配置文件配置的为 basicHttpBinding,所以客户端需要 更新服务引用

需要调整为 wsHttpBinding 也很简单,只需要 bindings 节点下的 basicHttpBinding 节点以及 endpoint 中的 binding 属性修改为 wsHttpBinding 即可。

更新后执行效果如下图:

2019112514592720191125145927

使用 TCP 协议

WCF 除支持 HTTP 协议外还支持多种协议,例如:TCP、MSMQ、命名管道等。

这里仅演示 TCP 协议,首先需要修改配置文件,调整 serviceBehaviors 中的 serviceMetadata 指定 httpGetEnabled 必须为 false

然后我们修改配置文件中的 bindings 节点,将之前的 basicHttpBindingwsHttpBinding 修改为:

1
2
3
4
5
6
7
<netTcpBinding>
<binding name="MyBindingConfig" closeTimeout="00:01:00" >
<security mode="None">
<transport clientCredentialType="None" protectionLevel="None" />
</security>
</binding>
</netTcpBinding>

调整 baseAddresses 节点下绑定的链接,将 http 协议修改为 net.tcp 协议:

1
<add baseAddress="net.tcp://localhost:7895/MyService/" />

最后调整 endpoint 中的 binding 属性即可:

1
<endpoint address="" binding="netTcpBinding" contract="JohnSun.SOA.WCF.Service.IMyService" bindingConfiguration="MyBindingConfig" />

调整完成后重新生成即可运行服务,我们可以调整以下客户端的服务引用,可以右键对应的服务,选择 配置服务引用,修改服务地址,修改为 net.tcp 协议。

当然之前我们已经删除了配置文件,所以这里直接修改服务端的初始化代码:

1
client = new MyServiceClient(new NetTcpBinding(securityMode: SecurityMode.None), new EndpointAddress("net.tcp://localhost:7895/MyService/"));

如果调用服务报错,可以添加服务引用,这时可能出现如下图的错误:

2019112515483620191125154836

无法识别该 URI 前缀。

网上搜索了很多解决方案,包括 启用或关闭 Windows 功能 中有关 WCF 的选项,以及 服务Net.Tcp Listener Adapter 是否启动,均正常。

2019112515452220191125154522

然后调整增加 mexTcpBinding 节点,仍然无法调用(此处必须配置):

1
<endpoint address="mex" binding="mexTcpBinding" bindingConfiguration="" contract="IMetadataExchange" />

后看到一篇文章发现可能是序列化问题,之前我们增加的类型 UserInfo ,均未配置 DataContractDataMember 特性,增加对应特性后,重新生成并运行服务端,客户端可以正常添加服务引用。

1
2
3
4
5
6
7
8
9
10
[DataContract]
public class UserInfo
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public string Country { get; set; }
}

但是,在可以正常添加以后,移除对应特性,重新编译,无法再重现这个问题,所以建议是添加该特性,并按照以上内容检查所有信息,避免出现这个问题。

2019112516564420191125165644

使用双工

因为 WebService 是基于 HTTP协议,其是无状态的,所以无法使用双工,但是 WCF 还支持有状态的 TCP 协议,所以可以实现双工通讯。

WCF 中双工的设计是通过回调来实现:

  • 定义一个用于回调的接口 ICallback,供客户端实现;
  • 添加服务协定 ICallbackService ,指定用于回调的协定 ICallback
  • 实现服务协定,可以通过服务上下文获取回调的通道并调用;
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
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading;

namespace JohnSun.SOA.WCF.Service
{
[ServiceContract(CallbackContract = typeof(ICallback))]
public interface ICallbackService
{
[OperationContract(IsOneWay = true)]
void TestConnect(string message);
}

/// <summary>
/// 不需要协议,是一个约束由客户端实现
/// </summary>
public interface ICallback
{
[OperationContract(IsOneWay = true)]
void Reply(string message);
}

public class CallbackService : ICallbackService
{
public void TestConnect(string message)
{
Logger.Log(LogLevel.Info, message);
Thread.Sleep(2000);
ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>();
callback.Reply(message);
}
}

public class Logger
{
public static void Log(LogLevel level, string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($"{DateTime.Now:HH:mm:ss} ");
Console.ForegroundColor = level == LogLevel.Info ? ConsoleColor.White : ConsoleColor.Red;
Console.Write($"{level,5}: ");
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write($"{message}\r\n");
}
}

[Flags]
public enum LogLevel
{
Info = 1,
Error = 2,
}
}

这样服务协定我们就创建好了,然后就是修改服务托管的代码:

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
ServiceHost host = null;
try
{
// 定义服务运行的 URL 并实例化服务主机
Uri baseAddress = new Uri("net.tcp://localhost:7895/MyDuplexService/");
host = new ServiceHost(typeof(CallbackService), baseAddress);

// 指定服务协定
host.AddServiceEndpoint(typeof(ICallbackService), new NetTcpBinding(securityMode: SecurityMode.None), "CallbackService");
// 不指定客户端将无法添加服务引用
host.Description.Behaviors.Add(new ServiceMetadataBehavior());
host.AddServiceEndpoint(typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexTcpBinding(), "mex");

// 开启服务
host.Open();
Log(LogLevel.Info, "服务开启成功,你可以点击 <Enter> 终止服务。");

Console.ReadLine();
host.Close();
}
catch (Exception exc)
{
Log(LogLevel.Error, $"开启服务失败 {exc.Message} {exc.StackTrace}");
if (host != null)
host.Abort();
Console.ReadKey();
}

之后我们就可以在客户端添加一个新的服务引用 MyDuplexWcfService,初始化服务连接并调用服务中定义的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CallbackServiceClient client = null;
try
{
InstanceContext callbackInstance = new InstanceContext(new Callback());
client = new CallbackServiceClient(callbackInstance);

for (int i = 0; i < 10; i++)
{
string message = $"{i:00} {DateTime.Now:HH:mm:ss.fff}";
client.TestConnect(message);
Log(LogLevel.Info, $"调用 TestConnect 方法成功:{message}");
Thread.Sleep(50);
}
Console.WriteLine("服务调用成功,你可以点击 <Enter> 关闭客户端。");
Console.ReadLine();
client.Close();
}
catch (Exception exc)
{
if (client != null)
client.Abort();
Log(LogLevel.Error, "测试服务失败:" + exc.Message);
Console.ReadLine();
}

需要注意的是,带有回调的服务,需要初始化一个InstanceContext,构造函数传递的是一个回调类型 ICallback 的实现类型的实例化对象。

我们引用的服务有一个 ICallbackServiceCallback 类型,这个其实就是 ICallback,需要添加实现:

1
2
3
4
5
6
7
public class Callback : ICallbackServiceCallback
{
public void Reply(string message)
{
Program.Log(LogLevel.Info, $"回调:{message}");
}
}

实现以后的客户端与服务端执行效果如下:

2019112519272820191125192728

同样的针对服务端与客户端调整实现方式,以上服务端展示的是使用代码实现的托管,下面增加一个使用配置文件来进行托管的方案,首先在配置文件的 services 节点内增加以下内容:

1
2
3
4
5
6
7
8
9
<service behaviorConfiguration="MyWcfServiceBehavior" name="JohnSun.SOA.WCF.Service.CallbackService">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:7895/MyDuplexService/" />
</baseAddresses>
</host>
<endpoint address="" binding="netTcpBinding" contract="JohnSun.SOA.WCF.Service.ICallbackService" bindingConfiguration="MyBindingConfig" />
<endpoint address="mex" binding="mexTcpBinding" bindingConfiguration="" contract="IMetadataExchange" />
</service>

然后服务开启的代码可以调整为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ServiceHost host = null;
try
{
host = new ServiceHost(typeof(CallbackService));

// 开启服务
host.Open();
Log(LogLevel.Info, "服务开启成功,你可以点击 <Enter> 终止服务。");

Console.ReadLine();
host.Close();
}
catch (Exception exc)
{
Log(LogLevel.Error, $"开启服务失败 {exc.Message} {exc.StackTrace}");
if (host != null)
host.Abort();
Console.ReadKey();
}

服务调整好以后,我们可能需要更新一下客户端的服务引用才能使用,因为以上配置文件指定与代码实现的并不完全一致。

客户端目前使用的是配置文件初始化,同样的我们可以删除客户端下的 App.config 文件,客户端实例化修改为以下代码:

1
client = new CallbackServiceClient(callbackInstance, new NetTcpBinding(securityMode: SecurityMode.None), new EndpointAddress("net.tcp://localhost:7895/MyDuplexService/"));

后记

WCF 的知识点比较多,这里仅仅是简单的实现与使用,更多的内容建议还是查看 MSDN 文档。

基本以上内容学习以后,工作中一些常见的场景都可以实现,身份认证建议使用 Token 会简单一些,如果有兴趣可以研究一下 x509证书

参考:

源码下载:

SOA —— WebService 知识点总结

虽然 WebService 已经很 Low 了,但是胜在简单。所以很多小公司或者公司内部仍然会使用这个做一些接口。

这里总结一下 WebService 的一些使用技巧,以及经验总结。

创建服务端

WebService 宿主是 IIS,所以我们需要先创建一个 ASP.Net Web 的空项目,当然如果选择 MVCWebForm 也没有影响。

2019112011094420191120110944

创建以后我们就可以添加对应的服务文件,如下图:

2019112011153120191120111531

会生成一个 *.asmx 文件与 一个 *.asmx.cs 文件,结构与 WebForm 的窗体页面或一般处理程序等一致。

如果我们没有将后台代码 cs 文件另外单独存放的需求,那么就不需要调整,直接修改展开对 cs 文件进行修改即可。

这里我们添加一些方法用于测试:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;

namespace JohnSun.SOA.WebService.Server
{
/// <summary>
/// MyWebService 的摘要说明
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消注释以下行。
// [System.Web.Script.Services.ScriptService]
public class MyWebService : System.Web.Services.WebService
{
private static List<UserInfo> _users = new List<UserInfo>()
{
new UserInfo(){ Id = 1, Name = "Kangkang", Country = "China" },
new UserInfo(){ Id = 2, Name = "John", Country = "America" },
new UserInfo(){ Id = 3, Name = "Jane", Country = "France" },
new UserInfo(){ Id = 4, Name = "Han Meimei", Country = "China" },
};
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}

[WebMethod]
public decimal Sum(decimal x, decimal y)
{
return x + y;
}

[WebMethod]
public UserInfo GetUserInfo(int id)
{
return _users.Find(u => u.Id == id);
}

[WebMethod]
public List<UserInfo> GetUsers(string country)
{
return _users.FindAll(u => u.Country == country);
}

[WebMethod]
public UserInfo[] GetAllUsers()
{
return _users.ToArray();
}
}

public class UserInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Country { get; set; }
}
}

需要注意的是:

  • 如果我们需要调用一个方法,则需要将方法标记为 WebMethod 特性,才能调用。
  • 这里不遵循重载,所以方法名不能重复,即便入参不同也不行。
  • 入参和返回值都可以是数组或集合,但是调用服务时,可以配置,这里再调用时会说明。

完成后,我们就可以右键该文件,在浏览器打开访问该服务。

2019112011430720191120114307

另外,我们也可以在浏览器里直接调用服务。

2019112011440520191120114405

连接服务端

服务端托管完成以后,我们就可以使用客户端完成对服务端的调用了,客户端的项目没有什么限制,但是建议是使用 .NET Framework 4.0 或以上版本,否则添加服务的界面可能会与截图演示的有些许区别。

这里为了方便演示,直接添加一个 WinForm 的项目,然后可以在引用中,选择添加服务引用:

2019112011542020191120115420

在弹出的添加页面,录入我们的服务地址,点击发现,添加我们需要的服务,另外我们需要调整这个服务的命名控件,需要注意的是不要与其他命名控件或类型重名,否则会比较麻烦:

2019112011572620191120115726

服务创建后,在窗体中简单写一些调用服务的代码进行测试:

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
private void button1_Click(object sender, EventArgs e)
{
MyWebServiceSoapClient client = null;
try
{
client = new MyWebServiceSoapClient();
client.Open();
string h = client.HelloWorld();
Log(LogLevel.Info, $"调用 HelloWorld 方法成功:{h}");
decimal x = 1m;
decimal y = 2m;
decimal d = client.Sum(x, y);
Log(LogLevel.Info, $"调用 Sum 方法成功:Sum({x}, {y}) = {d}");
int id = 1;
UserInfo info = client.GetUserInfo(id);
JavaScriptSerializer serializer = new JavaScriptSerializer();
Log(LogLevel.Info, $"调用 GetUserInfo 方法成功:GetUserInfo({id}) = {serializer.Serialize(info)}");
string country = "China";
UserInfo[] users = client.GetUsers(country);
Log(LogLevel.Info, $"调用 GetUsers 方法成功:GetUsers({country}) 获取到用户数量: {users.Length}");
UserInfo[] allUsers = client.GetAllUsers();
Log(LogLevel.Info, $"调用 GetAllUsers 方法成功,获取到用户数量: {allUsers.Length}");
client.Close();
}
catch (Exception exc)
{
if (client != null)
client.Abort();
Log(LogLevel.Error, "测试服务失败:" + exc.Message);
}
}

测试一下调用:

2019112016230920191120162309

这里主要需要注意两点:

  1. MyWebServiceSoapClient 并未继承 IDisposable 接口,所以不能使用 using 来关闭连接,使用上文的写法即可,否则资源释放存在问题。
  2. 无论返回值是集合还是数组,我们只能使用一种类型,服务创建以后我们也可以指定,如上虽然我们 GetUsers 在服务中定义返回集合,但是调用时返回的仍然是数组。

如果我们想让服务默认返回的数据类型调整为集合,可以在引用的服务上右键,选择“配置服务引用”,将集合类型调整为 System.Collections.Generic.List,同样的字典类型也可以调整。

2019112019450220191120194502

添加身份认证

以上服务方法都没有进行身份认证,如果一些私有的方法,就会有安全问题,我们也可以在服务中配置简单身份认证。

比较常用的是使用 SoapHeader 为服务方法,添加 SOAP 标头,这样我们在调用服务时就需要传入一个标头信息,我们可以使用这个信息来传递用于用户验证的信息。

首先需要调整 WebService 服务端的代码,定义一个继承自 SoapHeader 的类型 AuthenticationHeader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AuthenticationHeader : SoapHeader
{
public string UserName { get; set; }
public string Password { get; set; }
public string Token { get; set; }
public void Validate()
{
if (UserName == "admin" && Password == "admin")
{
MD5 md5 = MD5.Create();
Token = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(Password)));
}
else
{
throw new SoapException("Failed to verify user login information.", SoapException.ServerFaultCode);
}
}
}

修改服务后台类,增加一个用于接收 SoapHeader 的字段,为需要附加标头的方法标记 SoapHeader 特性,并指定 SOAP 标头 的数据赋值给服务后台类的哪个字段:

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
public AuthenticationHeader authenticationHeader;

[WebMethod]
[SoapHeader("authenticationHeader")]
public string Validate()
{
if (authenticationHeader != null)
{
authenticationHeader.Validate();
return authenticationHeader.Token;
}
else
return null;
}

[WebMethod]
[SoapHeader("authenticationHeader", Direction = SoapHeaderDirection.InOut)]
public UserInfo[] GetAllUsers()
{
if (authenticationHeader != null)
authenticationHeader.Validate();
else
return null;
return _users.ToArray();
}

修改完成以后需要重新编译这个服务,并且在客户端更新服务引用,更新完成以后 GetAllUsers 方法应该会报错,因为需要我们传递一个 AuthenticationHeader,可以将客户端调用服务方法的代码略作调整:

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
AuthenticationHeader header = new AuthenticationHeader() { UserName = "admin", Password = "admin" };
UserInfo[] allUsers = null;
try
{
allUsers = client.GetAllUsers(ref header);
Log(LogLevel.Info, $"调用 GetAllUsers 方法成功,获取到用户数量: {allUsers?.Length},Token:{header.Token}");
}
catch (Exception exc)
{
Log(LogLevel.Error, $"调用 GetAllUsers 方法失败:{exc.Message}");
}

try
{
header = new AuthenticationHeader() { UserName = "test", Password = "test" };
string token = client.Validate(header);
if (token != null)
{
Log(LogLevel.Info, $"调用 Validate 方法成功:{token}");
}
else
{
Log(LogLevel.Error, $"调用 Validate 方法失败,请验证用户名和密码。");
}
}
catch (Exception exc)
{
Log(LogLevel.Error, $"调用 Validate 方法失败:{exc.Message}");
}

修改以后可以执行测试,运行客户端查看一下效果:

2019112115111020191121151110

当然除以上方法外,我们还可以提供一个登录的服务方法,如果验证成功返回一个 token,然后私有的方法增加一个 token 的入参,每次调用方法前进行验证,因为比较简单这里不再过多演示。

动态调整服务

目前来说服务是固定指向了一个我们生成服务时的地址,当然如果我们想要修改服务地址也是很简单的,只需要打开 app.config 文件,修改默认生成的配置信息:

2019112115561220191121155612

这样我们要严格的依赖这个配置文件,而且如果我们想要配置多个服务地址,在一个地址无法连接时自动切换到其他服务,好像是不可实现的,所以第一步就是移除掉对配置文件的依赖,我们删除 app.config 文件,重新生成项目并运行:

1
17:19:31 Error: 测试服务失败:在 ServiceModel 客户端配置部分中,找不到引用协定“MyServiceTest.MyWebServiceSoap”的默认终结点元素。这可能是因为未找到应用程序的配置文件,或者是因为客户端元素中找不到与此协定匹配的终结点元素。

这个其实很简单,因为没有了配置文件读取不到服务的链接地址,我们可以在初始化客户端的时候,指定服务端连接:

1
client = new MyWebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress("http://localhost:15178/MyWebService.asmx"));

因为已经不再依赖配置文件,这样我们初始化客户端时就可以更自由,我们可以创建一个工厂来初始化服务:

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
using JohnSun.SOA.WebService.Client.MyServiceTest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;

namespace JohnSun.SOA.WebService.Client
{
public class SoapClientFactory
{
public static bool TryGetSoapClient(out MyWebServiceSoapClient soapClient, params string[] urls)
{
soapClient = null;
if (urls == null || urls.Length == 0)
return false;
foreach (string url in urls)
{
try
{
soapClient = new MyWebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress(url));
soapClient.HelloWorld();
break;
}
catch
{
if (soapClient != null)
soapClient.Abort();
soapClient = null;
}
}
return soapClient != null;
}
}
}

然后初始化服务可以调整为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
if (!SoapClientFactory.TryGetSoapClient(out client
, "http://localhost:80/MyWebService.asmx"
, "http://localhost:1008/MyWebService.asmx"
, "http://localhost:15178/MyWebService.asmx"))
{
Log(LogLevel.Error, "初始化服务失败!");
return;
}
else
{
Log(LogLevel.Info, $"初始化服务成功:{client.Endpoint.ListenUri}");
}

测试效果:

2019112116272720191121162727

我们已经解决了必须通过 app.config 来配置服务地址的问题,但是,如果我们只有 wsdl 文件,无法连接服务添加服务应该怎么处理呢?

首先我们下载服务的 wsdl 文件用于演示,在服务后面添加 wsdl 参数即可下载:http://localhost:15178/MyWebService.asmx?wsdl

然后和通过链接添加服务一样,不过我们输入的是下载下来的 wsdl 文件的文件路径:

2019112116334820191121163348

其实如果我们做出来的服务要提供给第三方调用,也是通过这种方式,将 wsdl 文件下载下来,发送给第三方即可。

参考:

源码下载:

代码仓管理系统 Gitea 搭建

开始用二进制文件搭建,结果一直启动不起来,后来换了一个版本可以启动起来,但是设置自启动总是提示找不到 [Unit],但是配置文件是配置了的。

很无奈,太菜了只能用 Docker 来安装了,还简单一些。

升级 Git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 安装依赖
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel asciidoc
yum install gcc perl-ExtUtils-MakeMaker

# 卸载 git
git --version
yum remove git

# 安装 git
cd /usr/local/src/
wget https://www.kernel.org/pub/software/scm/git/git-2.24.0.tar.xz
tar -vxf git-2.24.0.tar.xz
cd git-2.24.0
make prefix=/usr/local/git all
make prefix=/usr/local/git install
echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/profile
source /etc/profile
git --version

安装 Docker

1
2
3
4
5
6
7
8
9
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum install docker-ce

# 开机自启
sudo systemctl enable docker

# 启动服务
sudo systemctl start docker

安装 Gitea

1
2
3
docker pull gitea/gitea:latest
sudo mkdir -p /var/lib/gitea
docker run -d --name=gitea -p 10022:22 -p 10080:3000 -v /var/lib/gitea:/data gitea/gitea:latest

到这里就可以使用端口 10080 访问了,可以登录进网站执行一些初始化的操作,例如设置数据库信息,网站根目录等。

配置 Gitea

安装时我们将 docker 的数据持久化到 /var/lib/gitea 文件夹下,所以可以到该文件夹下找 app.ini 文件。

我这里使用的是 mysql 数据库,性能会比 SQLite3 好很多,如果需要的话自行安装。

1
vim /var/lib/gitea/gitea/conf/app.ini
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
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
RUN_USER = git

[repository]
ROOT = /data/git/repositories

[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo

[repository.upload]
TEMP_PATH = /data/gitea/uploads

[server]
APP_DATA_PATH = /data/gitea
SSH_DOMAIN = git.hd2y.net
HTTP_PORT = 3000
ROOT_URL = https://git.hd2y.net/
DISABLE_SSH = true
SSH_PORT = 10022
SSH_LISTEN_PORT = 10022
LFS_START_SERVER = true
LFS_CONTENT_PATH = /data/git/lfs
DOMAIN = git.hd2y.net
LFS_JWT_SECRET = ****
OFFLINE_MODE = false

[database]
PATH = /data/gitea/gitea.db
DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = gitea
USER = root
PASSWD = ******
SSL_MODE = disable
CHARSET = utf8

配置 Caddy

Caddy 类似 Nginx 的反向代理软件,但是配置会简单很多,并且可以自动帮我们申请 SSL 证书。

1
2
3
4
5
# 安装 Caddy 软件包
yum install caddy -y

# 使用 vim 编辑 Caddyfile
vim /etc/caddy/conf.d/Caddyfile.conf

对该配置文件进行修改:

1
2
3
4
5
6
7
8
9
10
11
https://git.hd2y.net {
gzip
tls hd2y@outlook.com
proxy / localhost:10080 {
transparent
}
}

http://git.hd2y.net {
redir https://git.hd2y.net{url}
}

修改完成之后启动 Caddy 服务即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开启自启 Caddy 服务
systemctl enable caddy

# 启动 Caddy
service caddy start

# 停止运行 Caddy
service caddy stop

# 重启 Caddy
service caddy restart

# 查看 Caddy 运行状态
service caddy status

参考文档

Gitea 官方文档:https://docs.gitea.io/zh-cn/

Caddy 官方文档:https://github.com/caddyserver/caddy/wiki/v2:-Documentation