25 网络_03_使用UDP、套接字

    • 使用UDP
      • 建立UDP接收器
      • 建立UDP发送器
        • 调用发送器和接收器
      • 使用多播
    • 使用套接字
      • 使用套接字创建侦听器
      • 使用NetWorkStream和套接字
      • 通过套接字使用读取器和写入器
      • 使用套接字实现接收器

使用UDP

建立UDP接收器

public class UdpReceiver
{
    /// 
    /// 从接收应用程序开始。该应用程序使用命令行参数来控制应用程序的不同功能。
    /// 所需的命令行参数是-p,它指定接收器可以接收数据的端口号。
    /// 可选参数-g与一个组地址用于多播。
    /// 
    /// 
    public void ReveiverStart(string[] args)
    {
        int port;
        string groupAddress;
        if (!ParseCommandLine(args, out port, out groupAddress))
        {
            ShowUsage();
            return;
        }
        ReadAsync(port, groupAddress).Wait();
        Console.ReadLine();
    }

    private void ShowUsage()
    {
        Console.WriteLine("Usage:UdpReceiver -p port [-g groupAddress]");
    }

    /// 
    /// ParseCommandLine方法解析命令行参数,并将结果放入变量port和groupAddress中
    /// 
    /// 
    /// 
    /// 
    /// 
    private 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;
        }

        // 端口号
        string port1 = GetValueForKey(args, "-p");
        if (port1 == null || !int.TryParse(port1, out port))
        {
            return false;
        }

        // 群组地址
        groupAddress = GetValueForKey(args, "-g");
        return true;
    }

    private 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];
    }

    private async Task ReadAsync(int port, string groupAddress)
    {
        using (var client = new UdpClient())
        {
            if (groupAddress != null)
            {
                client.JoinMulticastGroup(IPAddress.Parse(groupAddress));
                Console.WriteLine($"{IPAddress.Parse(groupAddress)}已添加到多播广播组");
            }

            bool completed = false;

            do
            {
                Console.WriteLine("UDP开始接受");
                UdpReceiveResult result = await client.ReceiveAsync();
                byte[] datagram = result.Buffer;
                string received = Encoding.UTF8.GetString(datagram);
                Console.WriteLine($"已接受{received}");
                if (received == "bye")
                {
                    completed = true;
                }
            } while (!completed);

            Console.WriteLine("receiver closing");

            if (groupAddress != null)
            {
                client.DropMulticastGroup(IPAddress.Parse(groupAddress));
            }
        }
    }
}

建立UDP发送器

public class UdpSender
{
    public void SenderStart(string[] args)
    {
        int port;
        string hostname;
        bool broadcast;
        string groupAddress;
        bool ipv6;
        if (!ParseCommandLine(args, out port, out hostname, out broadcast, out groupAddress, out ipv6))
        {
            ShowUsage();
            Console.ReadLine();
            return;
        }
        IPEndPoint endpoint = GetIPEndPoint(port, hostname, broadcast, groupAddress, ipv6).Result;
        SenderStart(endpoint, broadcast, groupAddress);
        Console.WriteLine("请按任意键退出");
        Console.ReadLine();
    }

    private 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];
    }

    private static bool ParseCommandLine(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 number
        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;
    }

    private static void ShowUsage()
    {
        Console.WriteLine("Usage: UdpSender -p port [-g groupaddress | -b | -h hostname] [-ipv6]");
        Console.WriteLine("\t-p port number\tEnter a port number for the sender");
        Console.WriteLine("\t-g group address\tGroup address in the range 224.0.0.0 to 239.255.255.255");
        Console.WriteLine("\t-b\tFor a broadcast");
        Console.WriteLine("\t-h hostname\tUse the hostname option if the message should be sent to a single host");
    }
    /// 
    /// 发送数据时,需要一个IPEndPoint0根据程序参数,以不同的方式创建它。
    /// 对于广播,IPv4定义了从IPAddress.Broadcast返回的地址255.255.255.255。
    /// 没有用于广播的IPv6地址,因为IPv6不支持广播。IPv6用多播替代广播。多播也添加到IPv4中。
    /// 传递主机名时,主机名使用DNS查找功能和Dns类来解析。
    /// GetHostEntryAsync方法返回一个IPHostEntry,其中IPAddress可以从AddressList属性中检索。
    /// 根据使用IPv4还是IPv6,从这个列表中提取不同的IPAddress。根据网络环境,只有一个地址类型是有效的。
    /// 如果把一个组地址传递给方法,就使用IPAddress.Parse解析地址
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    private async Task 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;
                if (ipv6)
                {
                    address = hostEntry.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetworkV6).FirstOrDefault();
                }
                else
                {
                    address = hostEntry.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetwork).FirstOrDefault();
                }

                if (address != null)
                {
                    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)} 或者 {nameof(groupAddress)}");
            }
        }
        catch (SocketException ex)
        {
            Console.WriteLine(ex.Message);
        }
        return endpoint;
    }

    /// 
    /// 在创建一个UdpClient实例,并将字符串转换为字节数组后,就使用SendAsync方法发送数据。
    /// 请注意接收器不需要侦听,发送方也不需要连接。UDP是很简单的。
    /// 然而,如果发送方把数据发送到未知的地方一一一无人接收数据,也不会得到任何错误消息
    /// 
    /// 
    /// 
    /// 
    private void SenderStart(IPEndPoint endpoint, bool broadcast, string groupAddress)
    {
        try
        {
            string locahost = Dns.GetHostName();
            using (var client = new UdpClient())
            {
                client.EnableBroadcast = broadcast;
                if (groupAddress != null)
                {
                    client.JoinMulticastGroup(IPAddress.Parse(groupAddress));
                }

                bool completed = false;

                do
                {
                    Console.WriteLine("请输入信息或者输入bye退出");
                    string input = Console.ReadLine();
                    Console.WriteLine();
                    completed = input == "bye";
                    byte[] datagram = Encoding.UTF8.GetBytes($"{input} from {locahost}");
                } while (!completed);

                if (groupAddress !=null)
                {
                    client.DropMulticastGroup(IPAddress.Parse(groupAddress));
                }
            }
        }
        catch (SocketException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }

}

调用发送器和接收器

启动发送器

UdpSender udpSender = new UdpSender();
udpSender.SenderStart(new string[] { "-p", "9400", "-h", "localhost" });

启动接收器

UdpReceiver receiver = new UdpReceiver();
receiver.ReveiverStart(new string[] { "-p", "9400" });

使用多播

广播不跨越路由器,但多播可以跨越。多播用于将消息发送到一组系统一上一一一所有节点都属于同一个组。在IPv4中,为使用多播保留了特定的IP地址。地址是从224.0.0.0到239.255.255.253。

这些地址中的许多都保留给具体的协议,例如用于路由器,但239.0.0.0/8可以私下在组织中使用。这非常类似于IPv6,它为不同的路由协议保留了著名的IPv6多播地址。地址f::/16是组织中的本地地址,地址e::/16有全局作用域,可以在公共互联网上路由。

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

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

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

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

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

new string[] { "-p", "9400", "-g", "230.0.0.1" }

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

使用套接字

HTTP协议基于TCP,因此HttpXX类在TcpXX类上提供了一个抽象层。然而TcpXX类提供了更多的控制。使用套接字,甚至可以获得比Tcpxx或Udpxx类更多的控制。通过套接字,可以使用不同的协议,不仅是基于TCP或UDP的协议,还可以创建自己的协议。更重要的是,可以更多地控制基于TCP或UDP的协议。

使用套接字创建侦听器

Socket对象的给构造函数提供AddressFamily、SocketType和ProtocolType。
AddressFamily是一个人型枚举.提供了许多不同的网络。
SocketType指定套接字的类型。例如用于TCP的Stream、用于UDP的Dgram或用于原始套接字的Raw。
ProtocolType是枚举。例如IP、Ucmp、Udp、IPv6和Raw。所选的设置需要匹配。

public class SocketServer
{
    /// 
    /// 侦听器创建一个新的Socket对象。
    /// 例如,用TCP与IPv4,地址系列就必须是InterNetwork,流套接字类型Stream、协议类型TCP。
    /// 要使用IPv4创建一个UDP通信,地址系列就需要设置为InterNetwork、套接字类型Dgram和协议类型Udp。
    /// 
    /// 
    public void Listener(int port)
    {
        var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listener.ReceiveTimeout = 5000;
        listener.SendTimeout = 5000;

        listener.Bind(new IPEndPoint(IPAddress.Any, port));
        // 定义了服务器的缓冲区队列的大小一一在处理连接之前,可以同时连接多少客户端
        listener.Listen(backlog: 15);
        Console.WriteLine($"侦听器在端口{port}正在侦听");


        var cts = new CancellationTokenSource();

        var tf = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
        tf.StartNew(() =>
        {
            Console.WriteLine("侦听任务开始");
            while (true)
            {
                if (cts.Token.IsCancellationRequested)
                {
                    cts.Token.ThrowIfCancellationRequested();
                    break;
                }
                Console.WriteLine("等待接受");
                // 等待客户端连接在Socket类的方法Accept中进行。这个方法阻塞线程,直到客户机连接为止。
                // 客户端连接后,需要再次调用这个方法,来满足其他客户端的请求;所以在while循环中调用此方法。
                Socket client = listener.Accept();
                if (!client.Connected)
                {
                    Console.WriteLine("没有连接上");
                    continue;
                }

                Console.WriteLine($"客户端已连接当前地址 {((IPEndPoint)client.LocalEndPoint).Address} 端口号 {((IPEndPoint)client.LocalEndPoint).Port}" +
                    $"路由地址{((IPEndPoint)client.RemoteEndPoint).Address} 路由端口{((IPEndPoint)client.RemoteEndPoint).Port}");
                // 为了进行侦听,启动一个单独的任务,该任务可以在调用线程中取消。在方法CommunicateWithClientUsingSocketAsync中执行使用套接字读写的任务。
                // 这个方法接收绑定到客户端的Socket实例,进行读写:
                Task t = CommunicateWithClientUsingSocketAsync(client);
            }
            listener.Dispose();
            Console.WriteLine("侦听任务关闭");
        }, cts.Token);

        Console.WriteLine("请按任意键退出");
        Console.ReadLine();
        cts.Cancel();
    }
    /// 
    /// 为了与客户端沟通,创建一个新任务。
    /// 这会释放侦听器任务,立即进行下一次迭代,等待下一个客户端连接。
    /// Socket类的Receive方法接受一个缓冲,其中的数据和标志可以读取,用于套接字。
    /// 这个字节数组转换为字符串,使用Send方法,连同一个小变化一起发送回客户机
    /// 
    /// 
    /// 
    private Task CommunicateWithClientUsingSocketAsync(Socket socket)
    {
        return Task.Run(() =>
        {
            try
            {
                using (socket)
                {
                    bool completed = false;
                    do
                    {
                        byte[] readbuffer = new byte[1024];
                        int read = socket.Receive(readbuffer, 0, readbuffer.Length, SocketFlags.None);
                        string fromClient = Encoding.UTF8.GetString(readbuffer, 0, read);
                        Console.WriteLine($"read:{read} bytes:{fromClient}");
                        if (string.Compare(fromClient, "shutdown", ignoreCase: true) == 0)
                        {
                            completed = true;
                        }

                        byte[] writebuffer = Encoding.UTF8.GetBytes($"echo{fromClient}");

                        int send = socket.Send(writebuffer);
                        Console.WriteLine($"send:{send} bytes");
                    } while (!completed);
                }
            }
            catch (SocketException ex)
            {
                Console.WriteLine(ex.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        });
    }
}

使用NetWorkStream和套接字

NetworkStream构造函数允许传递Socket,所以可以使用流方法Read和Write替代套接字的Send和Receive方法。在NetworkStream的构造函数中,可以定义流是否应该拥有套接字。

private async Task CommunicateWithClientUsingReadersAndWritersAsync(Socket socket)
{
    try
    {
        using (var stream = new NetworkStream(socket, ownsSocket: true))
        {
            bool completed = true;
            do
            {
                byte[] readbuffer = new byte[1024];
                int read = await stream.ReadAsync(readbuffer, 0, 1024);
                string fromClient = Encoding.UTF8.GetString(readbuffer, 0, read);
                Console.WriteLine($"read{read}bytes:{fromClient}");
                if (string.Compare(fromClient, "shutdown", ignoreCase: true) == 0)
                {
                    completed = true;
                }
                byte[] writeBuffer = Encoding.UTF8.GetBytes($"echo{fromClient}");

                await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length);
            } while (!completed);
        }
        Console.WriteLine($"关闭流和socket客户端");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

通过套接字使用读取器和写入器

private async Task CommunicateWithClientUsingReadersAndWritersAsync(Socket socket)
{
    try
    {
        // 因为NetworkStream派生于stream类,还可以使用读取器和写入器访问套接字。
        // 只需要注意读取器和写入器的生存期。调用读取器和与入器的Dispose方法,还会销毁底层的流。
        // 所以要选择StreamReader和Streamwriter的构造函数,其中leaveOption参数可以设置为true。
        // 之后,在销毁读取器和写入器时,就不会销毁底层的流了。NetworkStream在外层using语句的最后销毁,这又会关闭套接字,因为它拥有套接字。
        using (var stream = new NetworkStream(socket, ownsSocket: true))
        using (var reader = new StreamReader(stream, Encoding.UTF8, false, 8192, leaveOpen: true))
        using (var writer = new StreamWriter(stream, Encoding.UTF8, 8192, leaveOpen: true))
        {
            // 通过套接字使用写入器时,默认情况下,写入器不新数据,所以它们保存在缓存中,直到缓存己满。
            // 使用网络流,可能需要更快的回应。这里可以把AutoFIush属性设置为true也可以调用FlushAsync方法
            writer.AutoFlush = true;

            bool completed = true;
            do
            {
                string fromClient = await reader.ReadLineAsync();
                Console.WriteLine($"read{fromClient}");
                if (string.Compare(fromClient, "shutdown", ignoreCase: true) == 0)
                {
                    completed = true;
                }
                await writer.WriteLineAsync($"echo {fromClient}");
            } while (!completed);
        }
        Console.WriteLine($"关闭流和socket客户端");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

使用套接字实现接收器

public class SocketClient
{
    /// 
    /// 使用DNS名称解析,从主机名中获得IPHostEnfiy。
    /// 这个IPHostEnfiy用来得到主机的I4地址。
    /// 创建Socket实例后(其方式与为服务器创建代码相同),Connect方法使用该地址连接到服务器。
    /// 连接完成后,调用Sender和Receiver方法,创建不同的任务,这允许同时运行这些方法——接收方客户端可以同时读写服务器
    /// 
    /// 
    /// 
    /// 
    public async Task SendAndRecieive(string hostname, int port)
    {
        try
        {
            IPHostEntry ipHost = await Dns.GetHostEntryAsync(hostname);
            IPAddress ipAddress = ipHost.AddressList.Where(address => address.AddressFamily == AddressFamily.InterNetwork).FirstOrDefault();
            if (ipAddress == null)
            {
                Console.WriteLine("no IPv4 address");
                return;
            }

            using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                client.Connect(ipAddress, port);

                Console.WriteLine("客户端已经连接");
                var stream = new NetworkStream(client);
                var cts = new CancellationTokenSource();

                Task tSender = Sender(stream, cts);
                Task tReceiver = Receiver(stream, cts.Token);
                await Task.WhenAll(tSender, tReceiver);
            }

        }
        catch (SocketException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
    /// 
    /// Sender方法要求用户输入数据,并使用WriteAsync方法将这些数据发送到网络流。
    /// 
    /// 
    /// 
    /// 
    private async Task Sender(NetworkStream stream, CancellationTokenSource cts)
    {
        Console.WriteLine("Sender任务");
        while (true)
        {
            Console.WriteLine("输入一个字符串,此字符串用来发送(输入shutdown,可以退出)");
            string line = Console.ReadLine();
            byte[] buffer = Encoding.UTF8.GetBytes($"{line}\r\n");
            await stream.WriteAsync(buffer, 0, buffer.Length);
            await stream.FlushAsync();
            if (string.Compare(line, "shutdown", ignoreCase: true) == 0)
            {
                cts.Cancel();
                Console.WriteLine("Sender任务关闭");
                break;
            }
        }
    }

    /// 
    /// Receiver方法用ReadAsync方法接收流中的数据。当用户进入终止字符串时,通过CancellationToken从sender任务中发送取消信息:
    /// 
    /// 
    /// 
    /// 
    private async Task Receiver(NetworkStream stream, CancellationToken token)
    {
        try
        {
            stream.ReadTimeout = 5000;
            Console.WriteLine("Receiver 任务");

            byte[] readbuffer = new byte[1024];
            while (true)
            {
                Array.Clear(readbuffer,0,1024);
                int read = await stream.ReadAsync(readbuffer,0,readbuffer.Length);
                string receivedLine = Encoding.UTF8.GetString(readbuffer,0,read);
                Console.WriteLine($"received{receivedLine}");
            }
        }
        catch (OperationCanceledException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
  1. TCP客户机和服务器。TCP需要一个连接,才能发送和接收数据;为此要调用Connect方法。
  2. UDP,也可以调用Connect连接方法,但它不建立连接。使用UDP时,不是调用Connect方法,而可以使用SendTo和ReceiveFrom方法代替。这些方法需要一个EndPoint参数,在发送和接收时定义端点。
  3. 取消标记参见第21章。
  4. 作为经验规则,在使用System.Net名称空间中的类编程时,应尽可能一直使用最通用的类。例如,使用TCPClient类代替Socket类,可以把代码与许多低级套接字细节分离开来。更进一步,HttpClient类是利用HTTP协议的一种简单方式。

你可能感兴趣的:(看书笔记_C#高级编程)