這篇文章主要是依據以前的一篇文章做了些改進而已,無服務器端的UDP群聊功能剖析。
主要調整了信息傳送的組織方式以及利用匿名方式來簡化線程和UI的交互。
主要實現的功能就是你打開軟件,就能自動加載局域網中的其他用戶並且實現群聊,不需要任何中轉服務器。
其實現的原理是:首先在主窗體開一個監聽線程,監聽請求。其次,在主窗體中,通過不同的操作,向外發出操作標記,比如0x00代表上線,0x01代表聊天,0x03代表下線等等。
重構的內容包括:
實體類序列化為二進制數據及還原方式
1.傳送數據不再是字符串拼接,而是將實體類序列化為二進制數據,然后進行傳輸。其實現過程離不開BinaryFormatter類,這個類可以將Object類型的數據源轉換為二進制的數據流存儲在內存中或者自定義的文件中。具體的轉換代碼如下:
/// <summary> /// TODO: Convert object source to byte array. /// </summary> public static byte[] ToByteArray(object source) { var formatter = new BinaryFormatter(); stream = new MemoryStream(); try { formatter.Serialize(stream, source); return stream.ToArray(); } catch (SerializationException e) { throw new Exception(e.Message); } catch (ArgumentNullException ANEx) { throw new Exception(ANEx.Message); } catch (SecurityException SEx) { throw new Exception(SEx.Message); } catch (ArgumentException AEx) { throw new Exception(AEx.Message); } catch (ObjectDisposedException ODEx) { throw new Exception(ODEx.Message); } finally { stream.Close(); } }
其中,需要注意的是,當我們將object類型的source傳入的時候,需要保證其為可序列的對象,也就是可以Serializable的。
[Serializable] public class MessageModel { public string Flag { get; set; } public string UserIp { get; set; } public string UserName { get; set; } public string MsgContent { get; set; } }
然后我們就可以進行轉換了:
private void SendInfo(byte[] data) { try { foreach (string s in lstUsers.Items) //遍歷列表 { if (s.Contains(".")) //確定包含的是ip地址 { string _ip = s.Split('-')[0]; if (!_ip.Equals(localIP)) //將自身排除在外 { IPEndPoint iepe = new IPEndPoint(IPAddress.Parse(_ip), lanPort); //套接字申明 //這里我們必須申明一個新的實例以避免重復接收問題。(如果利用原來的listenClient實例,將會造成重復接收。) UdpClient udp = new UdpClient(); udp.Send(data, data.Length, iepe); //發送 } } } } catch (Exception ex) { MessageBox.Show(ex.Message); } } /// <summary> /// “發送按鈕”點擊事件 /// </summary> private void btnSend_Click(object sender, EventArgs e) { if (rSendContent.Text == "") { MessageBox.Show("Please fill in some words!","Notification",MessageBoxButtons.OK,MessageBoxIcon.Information); } else { messageModel.Flag = "0x01"; messageModel.UserIp = localIP; messageModel.UserName = localName; messageModel.MsgContent = rSendContent.Text; byte[] byteData = Parse.ToByteArray(messageModel); SendInfo(byteData); //發送消息 AddTextBox(localIP + " " + DateTime.Now + "\r\n", 1, 1); //將發送的消息添加到窗體中 AddTextBox(rSendContent.Text + "\r\n", 2, 1); //將發送的消息添加到窗體中 this.rSendContent.Text = string.Empty; //清空發送內容 } this.rAllContent.ScrollToCaret(); }
當接收到數據的時候,通過斷點查看,確實為二進制數據。
既然能夠將實體類轉換成二進制數據,然后在網絡上傳輸,那么還原該如何進行呢?其實,這個也是利用BinaryFormatter類,具體的還原方式如下:
/// <summary> /// 方法:處理接到的數據 /// </summary> private void DealWithAcceptedInfo(byte[] recData) { BinaryFormatter formatter = new BinaryFormatter(); MessageModel recvMessage; MemoryStream ms = new MemoryStream(recData); try { recvMessage = (MessageModel)formatter.Deserialize(ms); } catch (SerializationException e) { throw new Exception(e.Message); } switch (recvMessage.Flag) { case "0x00": //用戶上線 //這里很關鍵,當檢測到一個新的用戶上線,那么我們需要給這個新用戶發送自己的機器消息,以便新用戶能夠自動添加進列表中。 SendInfoOnline(recvMessage.UserIp); if (lstUsers.FindString(recvMessage.UserIp + "---" + recvMessage.UserName) <= 0) //如果用戶不存在 { lstUsers.Invoke((Action)(() => { lstUsers.Items.Add(recvMessage.UserIp + "---" + recvMessage.UserName); })); lsbLog.Invoke((Action)(() => { lsbLog.Items.Add("User[" + recvMessage.UserIp + "] is online now!"); })); } break; case "0x01": //用戶聊天 rAllContent.Invoke((Action)(() => { AddTextBox(recvMessage.UserIp + " " + DateTime.Now + "\r\n", 1, 2); //這是接收到了別人發來的信息 AddTextBox(recvMessage.MsgContent + "\r\n", 2, 2);//將發送的消息添加到窗體中 })); break; case "0x03": //用戶下線 if (lstUsers.FindString(recvMessage.UserIp + "---" + recvMessage.UserName) > 0) //如果用戶已經存在 { lstUsers.Invoke((Action)(() => { lstUsers.Items.Remove(recvMessage.UserIp + "---" + recvMessage.UserName); })); lsbLog.Invoke((Action)(() => { lsbLog.Items.Add("User[" + recvMessage.UserIp + "] is offline now!"); })); } break; default: break; } }
注意黃色標明部分,正是利用了Deserialize方法來實現的。更多的使用方式可以參見msdn。
線程和UI交互
2.說到線程和UI交互,可以算上是老生常談的問題了。有多種方式可以解決,不過,自.net 提供了匿名方式以來,大大簡化了編碼設計。以前的線程和UI交互,需要先聲明一個委托,然后利用控件的Control.InvokeRequired方式來判斷是否需要跨線程調用,如果需要,則利用Control.Invoke方式進行調用,如果不需要,則直接調用:
#region ListBox線程與UI交互委托,用於添加系統日志 public delegate void AddLogDelegate(string info); private void AddLogIntoListBox(string info) { if (lsbLog.InvokeRequired) { lsbLog.Invoke(new AddLogDelegate(AddLogIntoListBox), info); } else { lsbLog.Items.Add(info); } } #endregion
真是好不麻煩。但是現在,直接利用Action委托可以一步搞定,省時省力。更多使用方式,請參見我所知道的.NET異步。
lsbLog.Invoke((Action)(() => { lsbLog.Items.Add("User[" + recvMessage.UserIp + "] is offline now!"); }));
這樣寫,既簡潔,又方便。
軟件截圖
源代碼下載