以前一直不多接觸多線程這塊。此次項目中恰好用到了網絡編程TCP這塊,作一個服務端,須要使用到多線程,因此記錄下過程。但願能夠幫到本身的同時能給別人帶來一點點收穫~html
關於TCP的介紹就很少講,神馬經典的三次握手、四次握手,能夠參考下面幾篇博客學習瞭解:git
TCP三次握手掃盲github
客戶端是一個門禁設備,主要是向服務端發送實時數據(200ms)。服務端解析出進出人數並打印顯示。編程
由於主要是在服務器上監聽各設備的鏈接請求以及迴應並打印出入人數,因此界面我設計成這樣:數組
能夠在窗體事件中綁定本地IP,代碼以下:緩存
//獲取本地的IP地址 string AddressIP = string.Empty; foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList) { if (_IPAddress.AddressFamily.ToString() == "InterNetwork") { AddressIP = _IPAddress.ToString(); } } //給IP控件賦值 txtIp.Text = AddressIP;
首先咱們須要定義幾個全局變量服務器
Thread threadWatch = null; // 負責監聽客戶端鏈接請求的 線程; Socket socketWatch = null;
Dictionary<string, Socket> dict = new Dictionary<string, Socket>();//存放套接字
Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();//存放線程
而後能夠開始咱們的點擊事件啓動服務啦網絡
首先咱們建立負責監聽的套接字,用到了 System.Net.Socket 下的尋址方案AddressFamily ,而後後面跟套接字類型,最後是支持的協議。數據結構
在Bind綁定後,咱們建立了負責監聽的線程。代碼以下:多線程
// 建立負責監聽的套接字,注意其中的參數; socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 得到文本框中的IP對象; IPAddress address = IPAddress.Parse(txtIp.Text.Trim()); // 建立包含ip和端口號的網絡節點對象; IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim())); try { // 將負責監聽的套接字綁定到惟一的ip和端口上; socketWatch.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socketWatch.Bind(endPoint); } catch (SocketException se) { MessageBox.Show("異常:" + se.Message); return; } // 設置監聽隊列的長度; socketWatch.Listen(10000); // 建立負責監聽的線程; threadWatch = new Thread(WatchConnecting); threadWatch.IsBackground = true; threadWatch.Start(); ShowMsg("服務器啓動監聽成功!");
其中 WatchConnecting方法是負責監聽新客戶端請求的
相信圖片中註釋已經很詳細了,主要是監聽到有客戶端的鏈接請求後,開闢一個新線程用來接收客戶端發來的數據,有一點比較重要就是在Start方法中傳遞了當前socket對象
/// <summary> /// 監聽客戶端請求的方法; /// </summary> void WatchConnecting() { ShowMsg("新客戶端鏈接成功!"); while (true) // 持續不斷的監聽客戶端的鏈接請求; { // 開始監聽客戶端鏈接請求,Accept方法會阻斷當前的線程; Socket sokConnection = socketWatch.Accept(); // 一旦監聽到一個客戶端的請求,就返回一個與該客戶端通訊的 套接字; var ssss = sokConnection.RemoteEndPoint.ToString().Split(':'); //查找ListBox集合中是否包含此IP開頭的項,找到爲0,找不到爲-1 if (lbOnline.FindString(ssss[0]) >= 0) { lbOnline.Items.Remove(sokConnection.RemoteEndPoint.ToString()); } else { lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString()); } // 將與客戶端鏈接的 套接字 對象添加到集合中; dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection); Thread thr = new Thread(RecMsg); thr.IsBackground = true; thr.Start(sokConnection); dictThread.Add(sokConnection.RemoteEndPoint.ToString(), thr); // 將新建的線程 添加 到線程的集合中去。 } }
其中接收數據 RecMsg方法以下:
解釋如圖,一目瞭然,代碼以下
void RecMsg(object sokConnectionparn) { Socket sokClient = sokConnectionparn as Socket; while (true) { // 定義一個緩存區; byte[] arrMsgRec = new byte[1024]; // 將接受到的數據存入到輸入 arrMsgRec中; int length = -1; try { length = sokClient.Receive(arrMsgRec); // 接收數據,並返回數據的長度; if (length > 0) { //主業務 } else { // 從 通訊套接字 集合中刪除被中斷鏈接的通訊套接字; dict.Remove(sokClient.RemoteEndPoint.ToString()); // 從通訊線程集合中刪除被中斷鏈接的通訊線程對象; dictThread.Remove(sokClient.RemoteEndPoint.ToString()); // 從列表中移除被中斷的鏈接IP lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString()); ShowMsg("" + sokClient.RemoteEndPoint.ToString() + "斷開鏈接\r\n"); //log.log("碰見異常"+se.Message); break; } } catch (SocketException se) { // 從 通訊套接字 集合中刪除被中斷鏈接的通訊套接字; dict.Remove(sokClient.RemoteEndPoint.ToString()); // 從通訊線程集合中刪除被中斷鏈接的通訊線程對象; dictThread.Remove(sokClient.RemoteEndPoint.ToString()); // 從列表中移除被中斷的鏈接IP lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString()); ShowMsg("" + sokClient.RemoteEndPoint.ToString() + "斷開,異常消息:" + se.Message + "\r\n"); //log.log("碰見異常"+se.Message); break; } catch (Exception e) { // 從 通訊套接字 集合中刪除被中斷鏈接的通訊套接字; dict.Remove(sokClient.RemoteEndPoint.ToString()); // 從通訊線程集合中刪除被中斷鏈接的通訊線程對象; dictThread.Remove(sokClient.RemoteEndPoint.ToString()); // 從列表中移除被中斷的鏈接IP lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString()); ShowMsg("異常消息:" + e.Message + "\r\n"); // log.log("碰見異常" + e.Message); break; } } }
其中那個ShowMsg方法主要是在窗體中打印當前接收狀況和一些異常狀況,方法以下:
void ShowMsg(string str) { if (!BPS_Help.ChangeByte(txtMsg.Text, 2000)) { txtMsg.Text = ""; txtMsg.AppendText(str + "\r\n"); } else { txtMsg.AppendText(str + "\r\n"); } }
其中用到了一個方法判斷ChangeByte ,若是文本長度超過2000個字節,就清空再從新賦值。具體實現以下:
/// <summary> /// 判斷文本框混合輸入長度 /// </summary> /// <param name="str">要判斷的字符串</param> /// <param name="i">長度</param> /// <returns></returns> public static bool ChangeByte(string str, int i) { byte[] b = Encoding.Default.GetBytes(str); int m = b.Length; if (m < i) { return true; } else { return false; } }
心得體會:其實整個流程並不複雜,但我遇到一個問題是,客戶端每200毫秒發一次鏈接過來後,服務端會報一個遠程主機已經強制關閉鏈接,開始我覺得是我這邊服務器線程間的問題或者是阻塞神馬的,後來和客戶端聯調才發現問題,原來是服務器迴應客戶端心跳包的長度有問題,服務端定義的是1024字節,可是客戶端只接受32字節的心跳包迴應纔會正確解析~因此,對接協議要溝通清楚,溝通清楚,溝通清楚,重要的事情說說三遍
還有幾個點值得注意
1,有時候會遇到窗體間的控件訪問異常,須要這樣處理
Control.CheckForIllegalCrossThreadCalls = false;
2 多線程調試比較麻煩,能夠採用打印日誌的方式,例如:
具體實現能夠參考個人另外一篇博客:點我跳轉
3 ,接收解析客戶端數據的時候,要注意大小端的問題,好比下面這個第9位和第8位若是解出來和實際不相符,能夠把兩邊顛倒一下。
public int Get_ch2In(byte[] data) { var ch2In = (data[9] << 8) | data[8]; return ch2In; }
4 在接收到客戶端數據的時候,有些地方要注意轉換成十六進制再看結果是否正確
public int Get_ch3In(byte[] data) { int ch3In = 0; for (int i = 12; i < 14; i++) { ch3In = int.Parse(ch3In + BPS_Help.HexOf(data[i])); } return ch3In; }
上面這個方法在對data[i]進行了十六進制的轉換,轉換方法以下:
/// <summary> /// 轉換成十六進制數 /// </summary> /// <param name="AscNum"></param> /// <returns></returns> public static string HexOf(int AscNum) { string TStr; if (AscNum > 255) { AscNum = AscNum % 256; } TStr = AscNum.ToString("X"); if (TStr.Length == 1) { TStr = "0" + TStr; } return TStr; }
5 還有個能夠了解的是將數組轉換成結構,參考代碼以下:
/// <summary> /// Byte數組轉結構體 /// </summary> /// <param name="bytes">byte數組</param> /// <param name="type">結構體類型</param> /// <returns>轉換後的結構體</returns> public static object BytesToStuct(byte[] bytes, Type type) { //獲得結構體的大小 int size = Marshal.SizeOf(type); //byte數組長度小於結構體的大小 if (size > bytes.Length) { return null; } IntPtr structPtr = Marshal.AllocHGlobal(size); Marshal.Copy(bytes, 0, structPtr, size); object obj = Marshal.PtrToStructure(structPtr, type); //釋放內存空間 Marshal.FreeHGlobal(structPtr); return obj; }
調用方法以下,注意,此處的package的結構應該和協議中客戶端發送的數據結構一致才能轉換
如協議中是這樣的定義的話:
那在代碼中就能夠這樣定義一個package結構體
/// <summary> /// 數據包結構體 /// </summary> [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] public struct Package { /// <summary> /// 肯定爲命令包的標識 /// </summary> public int commandFlag; /// <summary> /// 命令 /// </summary> public int command; /// <summary> ///數據長度(數據段不包括包頭) /// </summary> public int dataLength; /// <summary> /// 通道編號 /// </summary> public short channelNo; /// <summary> /// 塊編號 /// </summary> public short blockNo; /// <summary> /// 開始標記 /// </summary> public int startFlag; /// <summary> /// 結束標記0x0D0A爲結束符 /// </summary> public int finishFlag; /// <summary> /// 校驗碼 /// </summary> public int checksum; /// <summary> /// 保留 char數組,SizeConst表示數組個數,在轉成 /// byte數組前必須先初始化數組,再使用,初始化 /// 的數組長度必須和SizeConst一致,例:test=new char[4]; /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public char[] reserve; }
TCP多線程服務器及客戶端Demo
點我跳去下載 密碼:3hzs
git一下:我要去Git
2017.3.11 補充
若是服務器和客戶端公用一個實體類,那還好說,若是服務器和客戶端分別使用結構相同但不是同一個項目下的實體類,該如何用正確的姿式收發呢?
首先簡單看看效果以下:
具體實現:
由於前面提到不在同一項目下,若是直接序列化和反序列化,就會反序列化失敗,由於不能對不是同一命名空間下的類進行此類操做,那麼解決辦法能夠新建一個類庫Studnet,而後從新生成dll,在服務器和客戶端分別引用此dll,就能夠對此dll進行序列化和反序列化操做了。
項目結構以下圖(這裏是做爲演示,將客戶端和服務器放在同一解決方案下,實際上這種狀況解決的就是客戶端和服務器是兩個單獨的解決方案)
客戶端發送核心代碼:
void showClient() { address = IPAddress.Parse("127.0.0.1"); endpoint = new IPEndPoint(address, 5000); socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { socketClient.Connect(endpoint); Console.WriteLine("鏈接服務端成功\r\n準備發送實體Student"); Student.Studnet_entity ms = new Student.Studnet_entity() { ID = 1, Name = "張三", Phone = "13237157517", sex = 1, Now_Time = DateTime.Now }; using (MemoryStream memory = new MemoryStream()) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(memory, ms); Console.WriteLine("發送長度:" + memory.ToArray().Length); socketClient.Send(memory.ToArray()); Console.WriteLine("我發送了 學生實體對象\r\n"); } } catch (Exception) { throw; } }
服務端接收並解析實體對象核心代碼
/// <summary> /// 服務端負責監聽客戶端發來的數據方法 /// </summary> void RecMsg(object socketClientPara) { byte[] arrMsgRec = new byte[1024];//手動準備空間 Socket socketClient = socketClientPara as Socket; List<byte> listbyte = new List<byte>(); while (true) { //將接受到的數據存入arrMsgRec數組,並返回真正接收到的數據長度 int length = socketClient.Receive(arrMsgRec); if (length > arrMsgRec.Length) { listbyte.AddRange(arrMsgRec); } else { for (int i = 0; i < length; i++) listbyte.Add(arrMsgRec[i]); break; } } //建立內存流 using (MemoryStream m = new MemoryStream(listbyte.ToArray())) { //建立以二進制格式對對象進行序列化和反序列化 BinaryFormatter bf = new BinaryFormatter(); Console.WriteLine("m.length" + m.ToArray().Length); //反序列化 object dataObj = bf.Deserialize(m); //獲得解析後的實體對象 Student.Studnet_entity dt = dataObj as Studnet_entity; Console.WriteLine("接收客戶端長度:" + listbyte.Count + " 反序列化結果是:ID:" + dt.ID + " 姓名:" + dt.Name + " 當前時間:" + dt.Now_Time); } }
點我前去下載Demo 密碼:x2ke