在C#領域或者說.net通訊領域中有着衆多的解決方案,WCF,HttpRequest,WebAPI,Remoting,socket等技術。這些技術都有着本身擅長的領域,或者被合併或者仍然應用於某些場合。本文主要介紹Socket通信,因其有着跨平臺、跨語言、高性能等優點,適合某些狀況的應用以及性能優越的解決方案。html
本文是基於一個小項目中的應用,使用了異步方式的Socket通信,性能上達到多客戶端多點通信,大文件(M-G級別)的文件傳輸,異步長鏈接上的性能優點,但此項目也有些不足:未進行大量的外網長時間傳輸測試,對不穩定的網絡情況未作很好的處理,例如加入斷點續傳功能,對Socket傳輸錯誤(其中微軟提供了大量的Socket通信錯誤代碼指示錯誤類型,請參考http://www.cnblogs.com/Aricc/archive/2010/01/29/1659134.html)未作一個很好的分類處理,只簡單的統一爲一個類型的錯誤。因此,此項目介紹只是想拋磚引玉介紹異步Socket通信,若是有不足或改進之處,還請各位不吝指出。git
這裏的同步和異步指的是服務端Accept接受客戶端鏈接請求的方式。在同步模式下,服務端要開啓一個Thread線程循環監聽可能來自客戶端的服務,若是沒有則阻塞,若是有鏈接則接受鏈接並存入Connection Pool鏈接池,這樣一個鏈接(或者說一個鏈接線程,通常佔用系統內存約爲2M,具體緣由請參考《CLR via C#》中線程章節),這樣32位系統下單一應用程序最大內存不超過2G,也就是說,單一鏈接服務端所能接受客戶端最大的請求數爲1000(實測下爲700+);而異步鏈接則應用非阻塞式異步鏈接機制(http://www.cnblogs.com/2018/archive/2011/05/10/2040333.html)BeginAccept異步接受客戶端請求並執行相應請求邏輯,在執行完畢後系統能自動優化,並當再次應用時喚醒,從而達到可接受大量的客戶端請求,但也限於「同時」執行的客戶端數量,對於某些長鏈接客戶端巨大,但併發性小的情景適用。github
衆所周知,在Socket通信中傳輸的普通的字符串或者二進制數據流,不適用於一些複雜的狀況,例如約定一個可擴展的協議,可變長協議等,因此本項目採用自定義協議類型來知足可擴展需求:網絡
Header併發 |
協議頭app |
命令異步 |
流1長度socket |
流2長度ide |
2字節性能 |
4字節 |
4字節 |
4字節 |
|
Body |
流1 |
流2 |
||
N字節 |
N字節 |
說明:
協議頭爲:FF 7E ,2字節固定值
命令爲:命令編號,如1001
流1長度:Int32型指示Body中的流1的長度
流2長度:Int32型指示Body中的流2的長度
流1:字節流
流2:字節流
這樣,基於上述協議可自定義流一、流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 }
傳統意義上的上傳與下載請求就是,一端發起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 }
以及在「命令」中封裝,下一個命令。
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 }
項目分爲客戶端和服務端,其中都依賴於BLL和Core核心類庫,Core中封裝的是協議頭的解析方式、抽象/接口方法、Socket緩衝讀取、枚舉委託、異步調用基類等,BLL中主要封裝的是協議命令,如Command一、Command2001等的具體實現方式。
Core:
BLL:
UI.Client:
UI.Server:
核心思想是將Command的業務需求整合進BLL層次中,而Socket基本的通信方式等公用功能整合進Core,將UI層釋放開,在UI層用Log4net等開源插件進行組合。
基於文字限制,不能講代碼中的每一個細節都將到,分析到,還請各位諒解,其中若有不妥之處還請不吝賜教。沒有質疑,就沒有進步;只有不斷的思考才能更好的掌握知識。
最後將程序的源碼奉上,但願對您有幫助。
GitHub Project: Here
以前作項目時,項目參考的不少引用沒有記住,但願之後有時間補上。
項目中的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