在十年的软件开发生涯中,多次涉足TCP或UDP网络编程领域。最近,当再次面临开发此类工具的任务时,发现很多需求都似曾相识。因此,决定构建一个工具包,希望它在未来的大多数网络工具应用中都能派上用场。
从一开始,就设定了四个设计目标:
希望这个工具包能被各个水平的开发者使用。接触到的开发者在经验和技能水平上有很大的差异。使工具包易于使用,可以提高其重用性,即使是初级开发者也能像高级开发者一样轻松使用这个库。
为了实现易用性,将公开暴露的方法数量保持在最低限度。还为基本操作添加了许多重载,例如发送消息和实例化客户端。
考虑到重用性,工具包需要非常健壮。例如,在点对点环境和客户端-服务器环境中使用它,不需要对任一方法有特殊了解。可以发送任何数据,并且没有明确的尺寸限制。既可以发送有限的消息,也可以进行流式传输。
用户可以实现自己的网络协议,该协议可以附加在底层协议上。
对来说,一个非常重要的特性是工具包能够将大型消息分割成指定大小的块,发送它们,然后在呈现给消费者之前再次合并。这个特性对消费者是隐藏的,并且会根据用户选择的数据包大小自动发生。
UDP本质上是一个不可靠的协议,无法保证网络数据包是否被接收。为了易于使用,消息的可靠性必须是可选的,并且对消费者隐藏。为需要可靠性的消息实现了一个交付收据协议。这消除了开发者需要自己实现交付确认机制的必要。
包括一个BitConverter类。这个类使用标准的.NET BitConverter,但检查环境的字节序。例如,可能会在不都使用小端字节格式的主机之间发送网络数据包。BitConverter类总是将操作数转换为大端字节序。
对于发送操作,使用了内置的.NET ThreadPool。线程池提供了一种易于使用、优化的工作排队方法。ThreadPool对象决定使用的正确线程数量。
接收数据包是通过一个单独的后台线程实现的,该线程专门用于监听传入的数据包。
为了进一步实现易用性,使用了两个事件来通知消费者发送或接收的消息。稍后会有更多关于这方面的内容。
项目中包含的类非常简单且非常自解释。唯一需要实例化的类是Client类。要发送第一条消息,只需实例化Client类并调用适当的EnqueueMessage(...)方法。
C# Client client;
private void MainForm_Load(object sender, EventArgs e)
{
client = new Client(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5), 1024, 10);
client.MessageSent += new OnMessageSent(client_MessageSent);
client.MessageReceived += new OnMessageReceived(client_MessageReceived);
}
在上面的代码片段中,Windows表单的Load事件用于实例化Client类。对于这个特定的重载,本地主机I.P.地址被作为本地端点传递。第二个参数是数据包大小。如果发送的消息大于这个数字,消息将被分解成这个字节数的数据包。例如,如果发送的消息是3092字节长,消息将被发送为三个1024字节的数据包和一个20字节的第四个数据包。第三个参数是客户端在发送数据包后等待的时间(以秒为单位),然后假定数据包失败并重新发送。默认情况下,数据包将尝试发送三次。这个值可以通过使用构造函数的不同重载来覆盖。
Client类的两个事件在这里绑定到方法上。OnMessageSent绑定到client_MessageSent(); OnMessageReceived绑定到OnMessageReceived();。这些事件分别在消息发送和接收后触发。
C# void client_MessageReceived(object sender, MessageReceivedEventArgs args)
{
this.Invoke(new MethodInvoker(delegate {
ReceivedMessageTextBox.Text = "\r\n\r\n" + System.Text.Encoding.ASCII.GetString(args.Data);
}));
}
在上面的事件方法实现中,简单地将接收到的消息文本添加到Windows表单上的一个文本框中。
当然,如果发送的消息是二进制文件,这就没有意义了。传递给方法的MessageReceivedEventArgs包含作为字节数组接收到的数据。
C# void client_MessageSent(object sender, MessageSentEventArgs args)
{
switch (args.Status)
{
case SendStatus.Failed:
this.Invoke(new MethodInvoker(delegate {
ActivityListBox.Items.Add("Failed message: " + args.MessageID.ToString());
}));
break;
case SendStatus.Sent:
this.Invoke(new MethodInvoker(delegate {
ActivityListBox.Items.Add("Sent message: " + args.MessageID.ToString());
}));
break;
case SendStatus.Delivered:
this.Invoke(new MethodInvoker(delegate {
ActivityListBox.Items.Add("Delivered message: " + args.MessageID.ToString());
}));
break;
}
}
当发送的消息没有指定可靠的标志时,这个事件将在发送消息后立即触发。MessageSentEventArgs包含一个名为SendStatus的字段。这个字段将被设置为Sent,表示消息已发送,但不能进一步假设它已到达目的地。
如果消息以可靠方式发送,即,发送时将Reliable标志设置为true,MessageSentEventArgs将具有Delivered或Failed中的一个值。当值为Delivered时,消费者可以安全地假设整个消息已被收件人接收。相反,当值为Failed时,消息已发送,但没有收到交付确认。在这种情况下,消费者应该假设消息失败。
C# private void SendButton_Click(object sender, EventArgs e)
{
ActivityListBox.Items.Add("Attempt Send message: " + client.QueueMessage(System.Text.Encoding.ASCII.GetBytes(ToSendTextBox.Text), 64, new System.Net.IPEndPoint(IPAddress.Parse(textBox2.Text), 5), 5, checkBox1.Checked).ToString());
}