这篇文章主要是依据以前的一篇文章做了些改进而已,无服务器端的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!"); }));
这样写,既简洁,又方便。
软件截图
源代码下载