6. 使用UDP(用户数据报协议)

本文深入探讨了UDP协议的特点,包括直接数据传输、广播和多播功能。通过创建控制台应用程序演示了如何实现UDP数据发送和接收,详细介绍了命令行参数解析、多播组加入和退出等关键操作。

为了演示UDP,创建两个控制台应用程序(包)项目,显示UDP的各种特性:直接将数据发送到主机,在本地网络上把数据广播到所有主机上,把数据多播到属于同一个组的节点上。

UdpSender和UdpReceiver项目使用以下名称空间:

System

System.Linq

System.Net

System.Net.Sockets

System.Text

System.Threading.Tasks

1.建立UDP接收器

从接收应用程序开始。该应用程序使用命令行参数来控制程序的不同功能。所需的命令行参数是-p,它指定接收器可以接收数据的端口号。可选参数-g与一个组地址用于多播。ParseCommandLine方法解析命令行参数,并将结果放入变量prot和groupAddress中:

        static async Task Main(string[] args)
        {
            if (!ParseCommandLine(args,out int port,out string groupAddress))
            {
                ShowUsage();
                return;
            }
            await ReaderAsync(port,groupAddress);
        }
        static void ShowUsage()
        {
            Console.WriteLine("Usage: UdpReceiver -p port -g groupAddress");
        }
        static bool ParseCommandLine(string[] args,out int port,out string groupAddress)
        {
            port = 0;
            groupAddress = string.Empty;
            if (args.Length < 2 || args.Length > 5)
            {
                return false;
            }
            if (args.SingleOrDefault(a=>a == "-p") == null)
            {
                Console.WriteLine("-p required");
                return false;
            }
            //get port number
            string port1 = GetValueForKey(args,"-p");
            if (port1 == null || !int.TryParse(port1,out port))
            {
                return false;
            }
            //get optional group address
            groupAddress = GetValueForKey(args,"-g");
            return true;
        }
        static string GetValueForKey(string[] args,string key)
        {
            int? nextIndex = args.Select((a, i) => new { Arg = a, Index = i }).SingleOrDefault(a => a.Arg == key)
                ?.Index + 1;
            if (!nextIndex.HasValue)
            {
                return null;
            }
            return args[nextIndex.Value];
        }

Reader方法使用在程序中传入的端口号创建一个UdpClient对象。ReceiveAsync方法等到一些数据的到来。这些数据可以使用UdpReceiveResult和Buffer属性找到。数据编码为字符串后,写入控制台,继续循环,等待下一个要接收的数据:

        static async Task ReaderAsync(int port,string groupAddress)
        {
            using (var client = new UdpClient(port))
            {
                if (groupAddress != null)
                {
                    client.JoinMulticastGroup(IPAddress.Parse(groupAddress));
                    Console.WriteLine($"joining the multicast group {IPAddress.Parse(groupAddress)}");
                }
                bool completed = false;
                do
                {
                    Console.WriteLine("starting the receiver");
                    UdpReceiveResult result = await client.ReceiveAsync();
                    byte[] datagram = result.Buffer;
                    string received = Encoding.UTF8.GetString(datagram);
                    Console.WriteLine($"received {received}");
                    if (received.ToUpper() == "BYE")
                    {
                        completed = true;
                    }
                } while (!completed);
                Console.WriteLine("receiver closing");
                if (groupAddress != null)
                {
                    client.DropMulticastGroup(IPAddress.Parse(groupAddress));
                }
            }
        }

启动应用程序时,它等待发送方发送数据。目前,忽略多播组,只使用参数和端口号,因为多播在创建发送器后讨论。

2. 创建UDP发送器

UDP发送器应用程序还允许通过命令行选项进行配置。它比接收应用程序有更多的选项。除了命令行参数-p指定端口号之外,发送方还允许使用-b在本地网络中广播到所有节点,使用-h识别特定的主机,使用-g指定一个组,使用-ipv6表明应该使用IPv6:

        static async Task Main(string[] args)
        {
            if (!ParseComamndLine(args,out int port,out string hostName,out bool broadCast,out string groupAddress,
                out bool ipv6))
            {
                ShowUsage();
                return;
            }
            IPEndPoint endpoint = await GetIPEndPointAsync(port,hostName,groupAddress,ipv6);
            await SenderAsync(endpoint,broadCast,groupAddress);
            Console.WriteLine("Press return to exit...");
        }
        static void ShowUsage()
        {
            Console.WriteLine("Usage: UdpSender -p port, -h hostName | -b broadCast | -g groupAddress , -ipv6 ipv6 ");
            Console.WriteLine("\t-p port number\tEnter a port number for the sender");
            Console.WriteLine("\t-h hostName\tUse the hostname option if the message should be send to a single host");
            Console.WriteLine("\t-b broadCast\tFor a broadcast");
            Console.WriteLine("\t-g group address\tGroup address in the range 224.0.0.0 to 239.255.255.255");
        }
        static bool ParseComamndLine(string[] args,out int port,out string hostName,out bool broadCast,
            out string groupAddress,out bool ipv6)
        {
            port = 0;
            hostName = string.Empty;
            broadCast = false;
            groupAddress = string.Empty;
            ipv6 = false;
            if (args.Length < 2 || args.Length > 5)
            {
                return false;
            }
            if (args.SingleOrDefault(a=>a == "-p") == null)
            {
                Console.WriteLine("-p required");
                return false;
            }
            string[] requiredOneOf = { "-h","-b","-g"};
            if (args.Intersect(requiredOneOf).Count() != 1)
            {
                Console.WriteLine("either one (and only one) of -h -b -g required");
                return false;
            }
            // get port nunber
            string port1 = GetValueForKey(args,"-p");
            if (port1 == null || !int.TryParse(port1,out port))
            {
                return false;
            }
            //get optional host name
            hostName = GetValueForKey(args,"-h");
            broadCast = args.Contains("-b");
            ipv6 = args.Contains("-ipv6");
            //get optional group address
            groupAddress = GetValueForKey(args,"-g");
            return true;
        }
        static string GetValueForKey(string[] args,string key)
        {
            int? nextIndex = args.Select((a, i) => new { Arg = a, Index = i }).SingleOrDefault(a => a.Arg == key)?.Index + 1;
            if (!nextIndex.HasValue)
            {
                return null;
            }
            return args[nextIndex.Value];
        }

发送数据时,需要一个IPEndPoint。根据程序参数,以不同的方式创建它。对于广播,IPv4定义了从IPAddress.Broadcast返回的地址255.255.255.255。没有用于广播的IPv6地址,因为IPv6不支持广播。IPv6用多播代替广播。多播也添加到IPv4中。

传递主机名时,主机名使用DNS查找功能和Dns类来解析。GetHostEntryAsync方法返回一个IPHostEntry,其中IPAddress可以从AddressList属性中检索。根据使用IPv4还是IPv6,从这个列表中提取不同的IPAddress。根据网络环境,只有一个地址类型是有效的。如果把一个组地址传递给方法,就使用IPAddress.Parse解析地址:

        static async Task<IPEndPoint> GetIPEndPoint(int port,string hostName,bool broadCast,string groupAddress,bool ipv6)
        {
            IPEndPoint endPoint = null;
            try
            {
                if (broadCast)
                {
                    endPoint = new IPEndPoint(IPAddress.Broadcast,port);
                }
                else if (hostName != null)
                {
                    IPHostEntry hostEntry = await Dns.GetHostEntryAsync(hostName);
                    IPAddress address = null;
                    if (ipv6)
                    {
                        address = hostEntry.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetworkV6).FirstOrDefault();
                        //address = hostEntry.AddressList.SingleOrDefault(a => a.AddressFamily == AddressFamily.InterNetworkV6);
                    }
                    else
                    {
                        address = hostEntry.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetwork).FirstOrDefault();
                    }
                    if (address == null)
                    {
                        //string ipVersion =  ipv6 ? "IPv6" : "IPv4";
                        Func<string> ipVersion = () => ipv6 ?"IPv6":"IPv4";
                        Console.WriteLine($"no {ipVersion} address for {hostName}");
                        return null;
                    }
                    endPoint = new IPEndPoint(address,port);
                }
                else if (groupAddress != null)
                {
                    endPoint = new IPEndPoint(IPAddress.Parse(groupAddress),port);
                }
                else
                {
                    throw new InvalidOperationException($"{nameof(hostName)}, {nameof(broadCast)}, or {nameof(groupAddress)} must be set");
                }
            }
            catch (SocketException ex)
            {
                Console.WriteLine(ex.Message);
            }
            return endPoint;
        }

现在,关于UDP协议,讨论发送器最重要的部分。在创建一个UdpClient实例,并将字符串转换为字节数组后,就使用SendAsync方法发送数据。请注意接收器不需要侦听,发送方也不需要连接。UDP是很简单的。然而,如果发送方把数据发送到未知的地方——无人接收数据,也不会得到任何错误消息:

        static async Task SenderAsync(IPEndPoint endPoint,bool broadCast,string groupAddress)
        {
            try
            {
                string localHostName = Dns.GetHostName();
                using (var client = new UdpClient())
                {
                    client.EnableBroadcast = broadCast;
                    if (groupAddress != null)
                    {
                        client.JoinMulticastGroup(IPAddress.Parse(groupAddress));
                    }
                    bool completed = false;
                    do
                    {
                        Console.WriteLine("Enter a message or bye to exit");
                        string input = Console.ReadLine();
                        Console.WriteLine();
                        completed = input.ToUpper() == "BYE";
                        byte[] datagram = Encoding.UTF8.GetBytes(input);
                        int sent = await client.SendAsync(datagram,datagram.Length,endPoint);
                    } while (!completed);
                    if (groupAddress != null)
                    {
                        client.DropMulticastGroup(IPAddress.Parse(groupAddress));
                    }
                }
            }
            catch (SocketException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

现在可以用如下选项启动接收器:

-p 9400

用如下选项启动发送器:

-p 9400 -h localhost

可以在发送器中输入数据,发送到接收器。如果停止接收器,就可以继续发送,而不会检测到任何错误。也可以尝试使用主机名而不是localhost,并在另一个系统上运行接收器。

在发送器中,可以添加-b选项,删除主机名,给在同一个网络上的侦听端口9400的所有节点发送广播:

-p 9400 -b

请注意广播不跨越大多数路由,当然不能在互联网上使用广播。这种情况和多播不同不同,参见下面的讨论。

3. 使用多播

广播不跨越路由器,但多播可以跨越。多播用于将消息发送到一组系统上——所有节点都属于同一个组。在IPv4中,为使用多播保留了特定的IP地址。地址是224.0.0.0到239.255.255.253。这些地址中许多都保留给具体的协议,例如用于路由器,但239.0.0.0/8可以私下在组织中使用。这非常类似于IPv6,它为不同的路由协议保留了著名的IPv6多播地址。地址f::/16是组织中的本地地址,地址ffxe::/16有全局作用域,可以在公共互联网上路由。

对于使用多播的发送器和接收器,必须通过调用UdpClient的JoinMulticastGroup方法来加入一个多播组:

client.JoinMulticastGroup(IPAddress.Parse(groupAddress));

为了再次退出该组,可以调用方法DropMulticastGroup:

client.DropMulticastGroup(IPAddress.Parse(groupAddress));

用如下选项启动接收器和发送器:

-p 9400 -g 230.0.0.1

它们都属于同一个组,多播在进行。和广播一样,可以启动多个接收器和多个发送器。接收器将接收来自每个发送器的几乎所有消息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值