为了演示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
它们都属于同一个组,多播在进行。和广播一样,可以启动多个接收器和多个发送器。接收器将接收来自每个发送器的几乎所有消息。
本文深入探讨了UDP协议的特点,包括直接数据传输、广播和多播功能。通过创建控制台应用程序演示了如何实现UDP数据发送和接收,详细介绍了命令行参数解析、多播组加入和退出等关键操作。
399

被折叠的 条评论
为什么被折叠?



