65.9K
CodeProject 正在变化。 阅读更多。
Home

在局域网中发送和接收广播消息

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2012年3月1日

MIT

9分钟阅读

viewsIcon

97957

downloadIcon

11969

轻量级 .Net UDP 局域网广播库。

库 (v2)

示例

目录 

  • 介绍  
  • 什么是广播 
  • 我的东西在哪里  
  • 快速入门 
  • 关于附加示例   
  • v2 库内部的魔法  
  • v1 库内部的魔法 (前一个版本) 
  • 结论  

引言

UDP 允许我们在 TCP/IP 局域网中广播消息。一些应用场景可能包括:

  • 局域网测试。
  • P2P 逻辑。
  • 局域网聊天。
  • 局域网独立游戏。
  • 服务器发现说明。客户端请求服务器,然后服务器会回复客户端可以连接的 IP 和端口。 
  • 等等。

本文提出了一种组织选定的核心对象和线程的方法,以便构建一个封装的类,该类公开干净的方法和属性。它还解决了开发人员在尝试利用 UDP 广播时遇到的主要困难。

  • 同时发送和接收会导致“端口被占用”异常。
  • receive 方法会阻塞执行直到收到内容。
  • 广播的数据包在局域网中是公开的,但需要隐私。

可以从本文中找到解决这些困难以及其他一些问题的方案。

  • 使用正确的参数供给 Microsoft UdpClient 对象。
  • 为接收者和监听者创建单独的线程。
  • 为接收者设置超时/使用 BeginReceive 而不是 Receive。 
  • 加密消息。
  • 通过线程安全访问的方法和属性来解耦线程。 

什么是广播

“广播”一词用于描述一种通信方式,即将信息从一个点发送到所有其他点。在网络情况下,只有一个发送者,但信息被发送到所有连接的接收者。广播主要用于本地子网络。为了传输广播包,目标 MAC 地址设置为 FF:FF:FF:FF:FF:FF,所有此类数据包都将被其他网卡接收。” (1)

与组播相对:“组播(点对多点)是一种通信模式,其中源主机向目标主机组发送消息。组的概念对组播的概念至关重要。根据定义,组播消息从源发送到目标主机组。” (1)

广播有两种类型

  • 有限广播 - 发送到与源网卡相同的网络段上的所有网卡。它用 255.255.255.255 的 TCP/IP 地址表示。此广播不会被路由器转发,因此只会出现在一个网络段上。
  • 定向广播 - 发送到网络上的所有主机。路由器可以配置为在大型网络上转发定向广播。对于网络 192.168.0.0,广播地址是 192.168.255.255。 

在本文中,我们将构建一个用于执行有限广播的库。 

我的东西在哪里

如果您正在寻找一种快速广播消息的方法,您可能想下载 v2 dll 二进制文件,将其添加到您的项目中并开始广播。简单的示例代码如下面的“快速入门”部分所示,并附有一些说明。如果您想查看更复杂的示例,可以下载“聊天”或“战舰游戏”并查看效果。

如果您来这里是为了学习 UDP 广播或进行相关实验,我建议下载 v1 源代码。等效的“快速入门”示例包含在文件中。 “v1 库内部的魔法”部分将介绍 v1 库。您很快就会发现该库还有很大的改进空间,并且允许一些 UDP 参数组合和优化。您可能想在 IPv6 环境、定向广播等方面进行测试。 您会从中获得乐趣。 如果您写了一些令您引以为豪并且可以共享的内容,我的好奇心会很高兴看到它。 

快速入门   

使用库  

库的使用非常简单。

  1. 添加库:下载 .dll 文件并将其放在您的项目中。添加对该库的引用。
  2. 声明和初始化
  3. 广播消息
  4. 接收广播消息
  5. 释放 

小型示例 

我们可以编写的最简单的程序之一是控制台聊天。该程序能够接收发送到局域网的任何消息,解密并显示在控制台上。它还可以发送用户消息到局域网。该程序使用一个线程来接收消息并立即显示。该库是线程安全的。以下是这个小型示例的代码。

using System;
using System.Threading;
using Orekaria.Lib.P2P;

namespace Orekaria.Test.P2P.ConUI
{
    internal class Program : IDisposable
    {
        private readonly BroadcastHelper _broadcast;
        private readonly Thread _th;

        private Program() {
            _broadcast = new BroadcastHelper(8010, "K_*?gR@Ej");
            _th = new Thread(DequeueMessages);
            _th.Start();
        }

        #region IDisposable Members

        public void Dispose() {
            _th.Abort();
        }

        #endregion

        private void Run() {
            var isExit = false;
            do {
                var s = Console.ReadLine();
                if (s != null && s.ToLower() == "exit")
                    isExit = true;
                else
                    _broadcast.Send(string.Format("{0}: {1}", Environment.UserName, s));
            } while (!isExit);
        }

        private void DequeueMessages() {
            while (_th.IsAlive) {
                Thread.Sleep(100);
                while (_broadcast.Received.Count > 0)
                    Console.WriteLine(string.Format("{0}", _broadcast.Received.Dequeue()));
            }
        }

        private static void Main() {
            var p = new Program();
            p.Run();
            p.Dispose();
        }
    }
}
关键行都以粗体和斜体显示。库在第一行声明,并启动一个新线程来显示接收到的消息。然后,进程继续读取用户控制台,直到输入“exit”。send 方法负责广播消息。您可能想在局域网中测试此示例,看看所有人如何接收消息。 

当名为 DAM 和 ASIR 的两个用户执行此代码时的输出如下。第一行“ASIR: Hello DAM”是远程计算机,行“Hello”是本地用户写入的响应,而“DAM: Hello”是本地回显已广播并接收回来的“Hello”响应。

326868/p2pCon.png

关于附加示例 

包含的示例将库用于特定目标。第一个是一个 WPF 局域网聊天,第二个是一个窗体局域网游戏,名为战舰。聊天与上面的控制台示例基本相同,但已转换为非常基础的 WPF 代码。局域网游戏更复杂,因为它展示了该库如何用于发送打包的消息或序列化内容。以下是此局域网游戏中广播的一些解密消息。

326868/p2pBoatMsg.png

收到其中一条消息后,下面的代码会解包消息中的信息。消息如何打包并不重要。您应该使用自己的打包算法。这里仅为演示目的。

Private Sub ProcesaPaquete(messageReceived As String)
    Dim parts = messageReceived.Split(" ")
    Dim hostEmitter = CatalogHost(parts(0))
    If parts.Length = 2 Then
        hostEmitter.PingReceived(Convert.ToInt32(parts(1)))
        Return
    End If
    Dim target = parts(1)
    Dim hostTarget = LogHost(target)
    Dim mb = New processParts(messageReceived, Convert.ToInt32(parts(2)), parts(3))
    hostTarget.MessageReceived(mb)
End Sub 

在上面的代码中,接收到的消息被解包,信息被归类为 ping、shoot 或 impact 三个类别之一。每条消息包含不同的信息,这些信息稍后会被处理。

v2 库内部的魔法

v2 库包含两个类。一个称为 BroadcastHelper,另一个称为 UdpHelper。BroadcastHelper 是唯一公开的类。构造函数必须用 port 和一个可选的加密 pass phrase 调用。它公开一个名为 Send 的方法,允许您将消息广播到局域网,以及一个名为 Received 的属性,该属性维护一个包含局域网中所有已广播消息的队列。 

在 BroadcastHelper 内部,listenertalker 管理消息。它们都使用 UdpClient 对象和一个 IPEndPoint 地址。关键参数是 portIPAddress.Broadcast。请记住,我们正在执行有限广播,因此地址应为 255.255.255.255。listener 这样实例化:

var udp = new UdpClient(_port);
var remoteEP = new IPEndPoint(IPAddress.Broadcast, 0);
_listener = new UdpHelper(udp, remoteEP); 

 以及 talker 这样实例化: 

var udp = new UdpClient()
var remoteEP = new IPEndPoint(IPAddress.Broadcast, _port);
_talker = new UdpHelper(udp, remoteEP)

与 v1 版本相比,不再需要超时,因为我们将使用回调来接收可能的广播消息,而此回调不会阻塞执行流程。在继续之前,您可能想检查调试打印 (Visual Studio 中的结果窗口) 以查看监听者和发言者是如何延迟加载的以及它们使用的端口。

离开 BroadcastHelper,进入 UdpHelper,消息会立即发送。

{ ...
    _udpClient.Send(packetBytes, packetBytes.Length, _remoteEP);
}

但接收者通过回调收集新消息,以避免阻塞执行流程。收到消息后,会排队另一个回调。

{ ...
    _udpClient.BeginReceive(ReceiveCallback, null)
}

private void ReceiveCallback(IAsyncResult ar) {
    var receiveBytes = _udpClient.EndReceive(ar, ref _remoteEP);
    _received.Enqueue(ChosenEncoder.GetString(receiveBytes));
    _udpClient.BeginReceive(ReceiveCallback, null);
}

最后,析构函数被指示自动关闭并释放资源。 

~UdpHelper() {
    _udpClient.Close();
}

v1 库内部的魔法  

库 (v1)

我真的很喜欢这个 v1 库。我知道...这可能不是最佳方式,但我喜欢它逻辑的简洁性。艺术和美也存在于我们的代码行中,不是吗? 

该库包含两个类。一个称为 P2PHelper,另一个称为 UDPHelper。顺便说一句,这是一个比这里介绍的项目更大的项目,所以 P2PHelper 这个名字也是如此。此类 P2PHelper 是唯一公开暴露的类。有三个方法和一个属性可见:constructorDisposeSendReceived

实例化 P2PHelper 时,会创建两个线程,每个线程包含一个 UDPHelper 类的实例。UDPHelper 类封装并简化了 System.Net.Sockets.UdpClientSystem.Text.Encoding。您可能希望在此处更改首选编码。

监听和与局域网通信

其中一个线程用于监听,另一个线程用于通信。我必须在这里指出,正如我在代码中所做的那样,两个线程都可以用于监听和通信,总共有 2x2。

监听线程(代码中称为 server)需要一些特定的参数才能进入广播监听模式。关键参数是 portIPAddress.Broadcasttimeout,以允许线程退出监听状态。

var udpServer = new UdpClient(_port) {Client = {ReceiveTimeout = TimeOut}};
var serverEP = new IPEndPoint(IPAddress.Broadcast, 0);
var server = new UDPHelper(udpServer, serverEP);

通信线程(代码中称为 client)虽然使用相同的 UDPHelper 类,但需要其他特定参数才能进入广播通信模式。关键参数是 IPAddress.Broadcast 和我们通信的 port

var udpClient = new UdpClient();
var clientEP = new IPEndPoint(IPAddress.Broadcast, _port);
var client = new UDPHelper(udpClient, clientEP); 

如果您有兴趣调整和改进此代码,您应该有足够的空间在此类中进行调整,特别是发送到上述 UDPHelper 类的参数。您会看到一些更改仍然有效,而另一些则会失败。

创建这两个线程后,P2PHelper 会继续监听并排队接收到的消息。如果启用了加密,消息将被解密。

while (!_disposing) {
    var receive = server.Receive();
    if (receive != Resources.TimeOut)
        _received.Enqueue(_encrypt ? EncryptHelper.DecryptString(receive, _passPhrase) : receive);
    Thread.Sleep(1);
}  

并且在通信。如果启用了加密,消息将被加密。

while (!_disposing) {
    while (_toSend.Count > 0) {
        var send = _toSend.Dequeue();
        client.Send(_encrypt ? EncryptHelper.EncryptString(send, _passPhrase) : send);
    }
    Thread.Sleep(10);
}  
释放

释放对象是释放资源的必要操作。您可能想改进此代码以满足您的需求,甚至无需显式释放即可工作。

public void Dispose()
{
    _disposing = true;
    _thClient.Join();
    _thServer.Join();
}

结论

本文让您更深入地了解如何构建代码以能够在局域网中广播和接收消息。希望得到您的反馈和建议。

参考文献 

(1) IP 网络中的广播和组播 

历史

2012 年 3 月 5 日:我的英语老师帮我修正了一些拼写和语法错误。

2012 年 3 月 6 日:添加了 v2 库。

© . All rights reserved.