項目筆記---C#異步Socket示例,
概要
在C#領域或者說.net通信領域中有著眾多的解決方案,WCF,HttpRequest,WebAPI,Remoting,socket等技術。這些技術都有著自己擅長的領域,或者被合並或者仍然應用於某些場合。本文主要介紹Socket通訊,因其有著跨平台、跨語言、高性能等優勢,適合某些情況的應用以及性能優越的解決方案。
本文是基於一個小項目中的應用,使用了異步方式的Socket通訊,性能上達到多客戶端多點通訊,大文件(M-G級別)的文件傳輸,異步長連接上的性能優勢,但此項目也有些不足:未進行大量的外網長時間傳輸測試,對不穩定的網絡狀況未做很好的處理,例如加入斷點續傳功能,對Socket傳輸錯誤(其中微軟提供了大量的Socket通訊錯誤代碼指示錯誤類型,請參考http://www.cnblogs.com/Aricc/archive/2010/01/29/1659134.html)未做一個很好的分類處理,只簡單的統一為一個類型的錯誤。所以,此項目介紹只是想拋磚引玉介紹異步Socket通訊,如果有不足或改進之處,還請各位不吝指出。
同步和異步區別
這裡的同步和異步指的是服務端Accept接受客戶端連接請求的方式。在同步模式下,服務端要開啟一個Thread線程循環監聽可能來自客戶端的服務,如果沒有則阻塞,如果有連接則接受連接並存入Connection Pool連接池,這樣一個連接(或者說一個連接線程,一般占用系統內存約為2M,具體原因請參考《CLR via C#》中線程章節),這樣32位系統下單一應用程序最大內存不超過2G,也就是說,單一連接服務端所能接受客戶端最大的請求數為1000(實測下為700+);而異步連接則應用非阻塞式異步連接機制(http://www.cnblogs.com/2018/archive/2011/05/10/2040333.html)BeginAccept異步接受客戶端請求並執行相應請求邏輯,在執行完畢後系統能自動優化,並當再次應用時喚醒,從而達到可接受大量的客戶端請求,但也限於“同時”執行的客戶端數量,對於某些長連接客戶端巨大,但並發性小的情景適用。
自定義協議
眾所周知,在Socket通訊中傳輸的普通的字符串或者二進制數據流,不適用於一些復雜的情況,例如約定一個可擴展的協議,可變長協議等,所以本項目采用自定義協議類型來滿足可擴展需求:
Header
協議頭
命令
流1長度
流2長度
2字節
4字節
4字節
4字節
Body
流1
流2
N字節
N字節
說明:
協議頭為:FF 7E ,2字節固定值
命令為:命令編號,如1001
流1長度:Int32型指示Body中的流1的長度
流2長度:Int32型指示Body中的流2的長度
流1:字節流
流2:字節流
這樣,基於上述協議可自定義流1、流2的長度分別存放不同數據,基於協議還可以對數據協議進行封裝,提供公共的解析方式。

![]()
1 /// <summary>
2 /// 通訊二進制協議,此協議基於變長的流傳輸。
3 /// 注:擴展此方法成員時,請重寫相關方法。
4 /// </summary>
5 /// <remarks>
6 /// Create By CYS
7 /// </remarks>
8 public class CommunicateProtocol : IDisposable
9 {
10 #region Public Properties
11 /// <summary>
12 /// Byte array length of flag
13 /// </summary>
14 public const int ByteLength_HeaderFlag = 2;
15 /// <summary>
16 /// Byte array length of command
17 /// </summary>
18 public const int ByteLength_HeaderCmd = 4;
19 /// <summary>
20 /// Byte array length of header stream1
21 /// </summary>
22 public const int ByteLength_HeaderStream1Len = 4;
23 /// <summary>
24 /// Byte array length of header stream2
25 /// </summary>
26 public const int ByteLength_HeaderStream2Len = 4;
27 /// <summary>
28 /// 協議頭長度
29 /// </summary>
30 public static int FlagLen
31 {
32 get { return ByteLength_HeaderFlag; }
33 }
34 /// <summary>
35 /// 命令(Int32)
36 /// </summary>
37 public int Command
38 {
39 get
40 {
41 return BitConverter.ToInt32(header_Cmd, 0);
42 }
43 set
44 {
45 BitConverter.GetBytes(value).CopyTo(header_Cmd, 0);
46 }
47 }
48 /// <summary>
49 /// 流1長度
50 /// </summary>
51 /// <returns></returns>
52 public int Stream1Len
53 {
54 get
55 {
56 return BitConverter.ToInt32(header_Stream1Len, 0);
57 }
58 set
59 {
60 BitConverter.GetBytes(value).CopyTo(header_Stream1Len, 0);
61 }
62 }
63 /// <summary>
64 /// 流2長度
65 /// </summary>
66 /// <returns></returns>
67 public int Stream2Len
68 {
69 get
70 {
71 return BitConverter.ToInt32(header_Stream2Len, 0);
72 }
73 set
74 {
75 BitConverter.GetBytes(value).CopyTo(header_Stream2Len, 0);
76 }
77 }
78 #endregion Public Properties
79
80 #region Private Properties
81 private static byte[] header_Flag = new byte[ByteLength_HeaderFlag];
82 private byte[] header_Cmd = new byte[ByteLength_HeaderCmd];
83 private byte[] header_Stream1Len = new byte[ByteLength_HeaderStream1Len];
84 private byte[] header_Stream2Len = new byte[ByteLength_HeaderStream1Len];
85 private byte[] body_Stream1 = new byte[0];
86 private Stream body_Stream2;
87 #endregion Private Properties
88
89 #region Constructor
90 /// <summary>
91 /// Static constructor
92 /// </summary>
93 static CommunicateProtocol()
94 {
95 header_Flag = new byte[ByteLength_HeaderFlag] { 0xFF, 0x7E };
96 }
97
98 #endregion Constructor
99
100 #region Public Method
101 /// <summary>
102 /// 判斷是否是協議頭標志
103 /// </summary>
104 /// <param name="bytes"></param>
105 /// <returns></returns>
106 public static bool CheckFlag(byte[] bytes)
107 {
108 if (bytes.Length != header_Flag.Length)
109 return false;
110 if (bytes.Length != 2)
111 return false;
112 if (!bytes[0].Equals(header_Flag[0]) || !bytes[1].Equals(header_Flag[1]))
113 return false;
114 return true;
115 }
116 /// <summary>
117 /// SetStream1
118 /// </summary>
119 /// <param name="sm"></param>
120 public void SetStream1(byte[] sm)
121 {
122 body_Stream1 = sm;
123 }
124 /// <summary>
125 /// GetStream1
126 /// </summary>
127 /// <returns></returns>
128 public byte[] GetStream1()
129 {
130 return body_Stream1;
131 }
132 /// <summary>
133 /// SetStream2
134 /// </summary>
135 /// <param name="sm"></param>
136 public void SetStream2(Stream sm)
137 {
138 body_Stream2 = sm;
139 }
140 /// <summary>
141 /// body_Stream2
142 /// </summary>
143 /// <returns></returns>
144 public Stream GetStream2()
145 {
146 return body_Stream2;
147 }
148 /// <summary>
149 /// GetHeaderBytes
150 /// </summary>
151 /// <returns></returns>
152 public byte[] GetHeaderBytes()
153 {
154 int offset = 0;
155 byte[] bytes = new byte[ByteLength_HeaderFlag + ByteLength_HeaderCmd + ByteLength_HeaderStream1Len + ByteLength_HeaderStream2Len];
156
157 Array.Copy(header_Flag, 0, bytes, 0, ByteLength_HeaderFlag); offset += ByteLength_HeaderFlag;
158 Array.Copy(header_Cmd, 0, bytes, offset, ByteLength_HeaderCmd); offset += ByteLength_HeaderCmd;
159 Array.Copy(header_Stream1Len, 0, bytes, offset, ByteLength_HeaderStream1Len); offset += ByteLength_HeaderStream1Len;
160 Array.Copy(header_Stream2Len, 0, bytes, offset, ByteLength_HeaderStream2Len); offset += ByteLength_HeaderStream2Len;
161
162 return bytes;
163 }
164 /// <summary>
165 /// InitProtocolHeader
166 /// </summary>
167 /// <returns></returns>
168 public static CommunicateProtocol InitProtocolHeader(byte[] source)
169 {
170 if (source.Length < ByteLength_HeaderCmd + ByteLength_HeaderStream1Len + ByteLength_HeaderStream2Len)
171 {
172 throw new Exception("byte length is illegal");
173 }
174
175 byte[] header_cmd = new byte[ByteLength_HeaderCmd];
176 byte[] header_stream1len = new byte[ByteLength_HeaderStream1Len];
177 byte[] header_stream2len = new byte[ByteLength_HeaderStream2Len];
178 Array.Copy(source, 0, header_cmd, 0, ByteLength_HeaderCmd);
179 Array.Copy(source, ByteLength_HeaderCmd, header_stream1len, 0, ByteLength_HeaderStream1Len);
180 Array.Copy(source, ByteLength_HeaderCmd + ByteLength_HeaderStream1Len, header_stream2len, 0, ByteLength_HeaderStream2Len);
181
182 return new CommunicateProtocol
183 {
184 Command = BitConverter.ToInt32(header_cmd, 0),
185 Stream1Len = BitConverter.ToInt32(header_stream1len, 0),
186 Stream2Len = BitConverter.ToInt32(header_stream2len, 0),
187 };
188 }
189 #endregion Public Method
190
191 #region Private Method
192
193 #endregion Private Method
194
195 #region IDisposable 成員
196 /// <summary>
197 /// Dispose
198 /// </summary>
199 public void Dispose()
200 {
201 header_Cmd = null;
202 header_Stream1Len = null;
203 header_Stream2Len = null;
204 body_Stream1 = null;
205 body_Stream2 = null;
206 }
207
208 #endregion
209 }
View Code
通訊機制
傳統意義上的上傳與下載請求就是,一端發起Request請求,另一端接受並答復請求內容,這樣就完成了一次請求應答。然而,如果要實現更多的控制功能,就要在這“一去一回”上增加通信應答次數,類似於TCP的三次握手請求。
其中,當請求被拒絕時,應向請求端發送錯誤答復998,這樣就可以在這個過程中建立一個完善的請求答復機制。

![]()
1 public abstract class ContractAdapter
2 {
3 #region Public Method
4 /// <summary>
5 ///
6 /// </summary>
7 /// <param name="p"></param>
8 /// <param name="s"></param>
9 protected void ExecuteProtocolCommand(CommunicateProtocol p, SocketState s)
10 {
11 if (p == null) throw new ArgumentNullException("CommunicateProtocol is null");
12 switch (p.Command)
13 {
14 case 0: CommandWrapper(((ICommandFunc)new Command0()), p, s); break;
15 case 1: CommandWrapper(((ICommandFunc)new Command1()), p, s); break;
16 case 2: CommandWrapper(((ICommandFunc)new Command2()), p, s); break;
17 case 998: CommandWrapper(((ICommandFunc)new Command998()), p, s); break;
18 case 999: CommandWrapper(((ICommandFunc)new Command999()), p, s); break;
19 //
20 case 1001: CommandWrapper(((ICommandFunc)new Command1001()), p, s); break;
21 case 1002: CommandWrapper(((ICommandFunc)new Command1002()), p, s); break;
22 //
23 case 2001: CommandWrapper(((ICommandFunc)new Command2001()), p, s); break;
24 case 2002: CommandWrapper(((ICommandFunc)new Command2002()), p, s); break;
25 //
26 case 3001: CommandWrapper(((ICommandFunc)new Command3001()), p, s); break;
27
28 default: throw new Exception("Protocol type does not exist.");
29 }
30 }
31 /// <summary>
32 ///
33 /// </summary>
34 /// <param name="func"></param>
35 /// <param name="p"></param>
36 /// <param name="s"></param>
37 protected abstract void CommandWrapper(ICommandFunc func, CommunicateProtocol p, SocketState s);
38 #endregion Public Method
39 }
View Code
以及在“命令”中封裝,下一個命令。

![]()
1 /// <summary>
2 ///
3 /// </summary>
4 public class Command1002 : ICommandFunc
5 {
6 #region ICommandFunc 成員
7 public CommandProfile profile { get; set; }
8
9 /// <summary>
10 ///
11 /// </summary>
12 /// <param name="protocol"></param>
13 /// <param name="state"></param>
14 /// <param name="sobj"></param>
15 /// <returns></returns>
16 public SocketState Execute(CommunicateProtocol protocol, SocketState state, IHSSocket sobj)
17 {
18 state.IsReceiveThreadAlive = false;
19
20 // Check File
21 if (!FileHelper.IsFileExist(profile.UpLoadPath + profile.UpLoadFName))
22 {
23 var p = ProtocolMgmt.InitProtocolHeader(998, System.Text.Encoding.UTF8.GetBytes("服務端文件不存在"), null);
24 ProtocolMgmt.SendProtocol(state, p, sobj);
25 state.OutPutMsg = string.Format("Command 1002 :服務端文件不存在");
26 return state;
27 }
28 if (!FileHelper.CanRead(profile.UpLoadPath + profile.UpLoadFName))
29 {
30 var p = ProtocolMgmt.InitProtocolHeader(998, System.Text.Encoding.UTF8.GetBytes("文件已被打開或占用,請稍後重試"), null);
31 ProtocolMgmt.SendProtocol(state, p, sobj);
32 state.OutPutMsg = string.Format("Command 1002 :文件已被打開或占用");
33 return state;
34 }
35
36 FileInfo fi = new FileInfo(profile.UpLoadPath + profile.UpLoadFName);
37 using (FileStream fs = new FileStream(profile.UpLoadPath + profile.UpLoadFName, FileMode.Open))
38 {
39 var p = ProtocolMgmt.InitProtocolHeader(2002, System.Text.Encoding.UTF8.GetBytes(fi.Name), fs);
40 ProtocolMgmt.SendProtocol(state, p, sobj);
41 state.OutPutMsg = string.Format("Command 1002 :發送文件 {0} 成功。", fi.FullName);
42 }
43
44 return state;
45 }
46
47
48 #endregion
49 }
View Code
代碼結構
項目分為客戶端和服務端,其中都依賴於BLL和Core核心類庫,Core中封裝的是協議頭的解析方式、抽象/接口方法、Socket緩沖讀取、枚舉委托、異步調用基類等,BLL中主要封裝的是協議命令,如Command1、Command2001等的具體實現方式。
Core:

BLL:

UI.Client:

UI.Server:

核心思想是將Command的業務需求整合進BLL層次中,而Socket基本的通訊方式等公用功能整合進Core,將UI層釋放開,在UI層用Log4net等開源插件進行組合。
總結
基於文字限制,不能講代碼中的每個細節都將到,分析到,還請各位諒解,其中如有不妥之處還請不吝賜教。沒有質疑,就沒有進步;只有不斷的思考才能更好的掌握知識。
最後將程序的源碼奉上,希望對您有幫助。
源碼下載
之前做項目時,項目參考的很多引用沒有記住,希望以後有時間補上。
項目中的IP地址,需要根據您的本機IP進行配置,請在WinformServer和WinformClient的Setting文件中更改,同時,還要更改其默認的上傳和下載文件,這裡沒有寫成基於OpenFile的方式只是為了演示異步Socket通訊。
引用
Socket通信錯誤碼:http://www.cnblogs.com/Aricc/archive/2010/01/29/1659134.html
異步通訊:http://www.cnblogs.com/2018/archive/2011/05/10/2040333.html