38.UDP异步服务端

38.网络通信-套接字Socket-UDP通信-异步-服务端综合练习题


38.1 知识点

使用UDP通信中Begin相关异步方法,将UDP同步通信中服务端综合练习改为使用异步方法通信

ServerSocket 类

定义必要的变量
// 服务器端Socket
public Socket serverSocket;

// 用于标识服务器是否关闭的标志
private bool isServerClose;

// 我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
private Dictionary<string, Client> clientDictionary = new Dictionary<string, Client>();

// 用于接收消息的容器
private byte[] cacheBytes = new byte[512];
定义启动服务器方法,传入IP和端口并绑定。处理消息和检查连接。定义关闭服务器方法。
// 启动服务器
public void Start(string ipString, int port)
{
    // 创建一个表示IP地址和端口的对象
    EndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ipString), port);

    //声明一个用于UDP通信的Socket
    serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    try
    {
        serverSocket.Bind(ipPoint);
        isServerClose = false;

        // 消息接收的处理 
        // BeginXXX系列方法都是异步方法,此处使用BeginReceiveFrom异步接收消息
        // cacheBytes为接收消息的缓存区,0为偏移量,cacheBytes.Length为接收消息的最大长度
        // SocketFlags.None表示不使用任何选项
        // ref ipPoint表示接收消息的IP地址和端口号
        // ReceiveMsg为接收完成后回调的方法
        serverSocket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);

        //定时检测超时线程
        ThreadPool.QueueUserWorkItem(CheckTimeOut);
    }
    catch (Exception e)
    {
        Console.WriteLine("UDP开启出错" + e.Message);
    }
}

// 关闭服务器
public void Close()
{
    if (serverSocket != null)
    {
        isServerClose = true;
        serverSocket.Shutdown(SocketShutdown.Both);
        serverSocket.Close();
        serverSocket = null;
    }
}
定义收发、广播消息,检查连接和移除客户端方法。超时则移除指定客户端。收消息时发现是新的客户端要加入到客户端字典中。
// 定期检测是否有超时客户端需要移除
private void CheckTimeOut(object obj)
{
    long nowTime = 0;
    List<string> delList = new List<string>();
    while (true)
    {
        //每30s检测一次 是否移除长时间没有接收到消息的客户端信息
        Thread.Sleep(30000);
        //得到当前系统时间
        nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
        foreach (Client c in clientDictionary.Values)
        {
            //超过10秒没有收到消息的 客户端信息 需要被移除
            if (nowTime - c.frontTime >= 10)
                delList.Add(c.clientStrID);
        }
        //从待删除列表中移除 超时的客户端信息
        for (int i = 0; i < delList.Count; i++)
            RemoveClient(delList[i]);
        delList.Clear();
    }
}

// 异步接收到消息后的处理
private void ReceiveMsg(IAsyncResult asyncResult)
{
    //接收消息的容器
    //记录谁发的
    //用于拼接字符串 位移ID 是由 IP + 端口构成的

    // 获取IP地址和端口号
    EndPoint ipPoint = asyncResult.AsyncState as IPEndPoint;
    string ip = (ipPoint as IPEndPoint).Address.ToString();
    int port = (ipPoint as IPEndPoint).Port;
    string strID = ip + port;//拼接成一个唯一ID 这个是我们自定义的规则

    try
    {
        // 调用EndReceiveFrom方法获取接收到的消息
        serverSocket.EndReceiveFrom(asyncResult, ref ipPoint);

        // 判断有没有记录这个客户端信息 如果有 用它直接处理消息
        if (clientDictionary.ContainsKey(strID))
            clientDictionary[strID].ReceiveMsg(cacheBytes);


        else
        {
            // 如果没有,直接添加并且处理消息
            clientDictionary.Add(strID, new Client(ip, port));
            clientDictionary[strID].ReceiveMsg(cacheBytes);
        }

        //继续接受消息
        serverSocket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);
    }
    catch (SocketException s)
    {
        Console.WriteLine("接受消息出错" + s.SocketErrorCode + s.Message);
    }
    catch (Exception e)
    {
        Console.WriteLine("接受消息出错(非Socket错误)" + e.Message);
    }
}

// 指定发送一个消息给某个目标
public void SendTo(BaseMessage baseMessage, IPEndPoint ipPoint)
{
    try
    {
        byte[] bytes = baseMessage.Writing();
        // BeginXXX系列方法都是异步方法,此处使用BeginSendTo异步发送消息
        // bytes为要发送的消息数据,0为偏移量,bytes.Length为消息数据的长度
        // SocketFlags.None表示不使用任何选项
        // ipPoint表示要发送的IP地址和端口号
        // 回调方法中处理如果发送失败的情况
        serverSocket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, (result) =>
        {
            try
            {
                serverSocket.EndSendTo(result);
            }
            catch (SocketException s)
            {
                Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
            }
        }, null);
    }
    catch (SocketException s)
    {
        Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
    }
    catch (Exception e)
    {
        Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
    }

}

// 广播消息给所有客户端
public void Broadcast(BaseMessage baseMessage)
{
    //广播消息 给谁广播
    foreach (Client c in clientDictionary.Values)
    {
        SendTo(baseMessage, c.clientIPandPort);
    }
}

// 移除客户端信息
public void RemoveClient(string clientID)
{
    if (clientDictionary.ContainsKey(clientID))
    {
        Console.WriteLine("客户端{0}被移除了" + clientDictionary[clientID].clientIPandPort);
        clientDictionary.Remove(clientID);
    }
}

Client类

定义客户端必要的标识和变量,在构造函数初始化
public IPEndPoint clientIPandPort; // 客户端的IP地址和端口
public string clientStrID; // 客户端的唯一标识

// 上一次收到消息的时间
public long frontTime = -1;

public Client(string ip, int port)
{
    // 规则和外面一样,记录唯一ID,通过 ip + port 拼接的形式
    clientStrID = ip + port;
    // 将客户端的信息记录下来
    clientIPandPort = new IPEndPoint(IPAddress.Parse(ip), port);
}
使用多线程处理客户端发送过来的消息
// 接收消息
public void ReceiveMsg(byte[] bytes)
{
    // 为了避免处理消息时又接受到了其它消息,所以我们需要在处理之前先把信息拷贝出来
    // 处理消息和接收消息用不同的容器,避免出现问题
    byte[] cacheBytes = new byte[512];
    bytes.CopyTo(cacheBytes, 0);
    // 记录收到消息的系统时间,单位为秒
    frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
    ThreadPool.QueueUserWorkItem(ReceiveHandle, cacheBytes);
}

// 多线程处理消息
private void ReceiveHandle(object obj)
{
    try
    {
        // 取出传进来的字节
        byte[] bytes = obj as byte[];
        int nowIndex = 0;
        // 先处理消息ID
        int msgID = BitConverter.ToInt32(bytes, nowIndex);
        nowIndex += 4;
        // 再处理消息长度
        int msgLength = BitConverter.ToInt32(bytes, nowIndex);
        nowIndex += 4;
        // 再解析消息体
        switch (msgID)
        {
            case 1001:
                PlayerMessage playerMessage = new PlayerMessage();
                playerMessage.Reading(bytes, nowIndex);
                Console.WriteLine(playerMessage.playerID);
                Console.WriteLine(playerMessage.playerData.name);
                Console.WriteLine(playerMessage.playerData.atk);
                Console.WriteLine(playerMessage.playerData.lev);
                break;
            case 1003:
                QuitMessage quitMessage = new QuitMessage();
                // 由于它没有消息体,所以不用反序列化
                // quitMessage.Reading(bytes, nowIndex);
                // 处理退出
                Program.serverSocket.RemoveClient(clientStrID);
                break;
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("处理消息时出错" + e.Message);
        // 如果出错,就不用记录这个客户端信息
        Program.serverSocket.RemoveClient(clientStrID);
    }
}

服务端入口

定义服务端静态变量,方便全局获得,检查服务端输入,广播消息
public static ServerSocket serverSocket;
static void Main(string[] args)
{
    serverSocket = new ServerSocket();
    serverSocket.Start("127.0.0.1", 8080);

    Console.WriteLine("UDP服务器启动了");

    while (true)
    {
        string input = Console.ReadLine();
        if (input.Substring(0, 2) == "B:")
        {
            PlayerMessage msg = new PlayerMessage();
            msg.playerData = new PlayerData();
            msg.playerID = 1001;
            msg.playerData.name = "韬老狮的UDP异步服务器";
            msg.playerData.atk = 88;
            msg.playerData.lev = 66;
            serverSocket.Broadcast(msg);
        }
    }
}

38.2 知识点代码

ServerSocket

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Lesson38_网络通信_套接字Socket_UDP通信_异步_服务端综合练习题
{
    class ServerSocket
    {
        // 服务器端Socket
        public Socket serverSocket;

        // 用于标识服务器是否关闭的标志
        private bool isServerClose;

        //我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
        private Dictionary<string, Client> clientDictionary = new Dictionary<string, Client>();

        //用于接收消息的容器
        private byte[] cacheBytes = new byte[512];

        // 启动服务器
        public void Start(string ipString, int port)
        {
            // 创建一个表示IP地址和端口的对象
            EndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ipString), port);

            //声明一个用于UDP通信的Socket
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            try
            {
                serverSocket.Bind(ipPoint);
                isServerClose = false;

                // 消息接收的处理 
                // BeginXXX系列方法都是异步方法,此处使用BeginReceiveFrom异步接收消息
                // cacheBytes为接收消息的缓存区,0为偏移量,cacheBytes.Length为接收消息的最大长度
                // SocketFlags.None表示不使用任何选项
                // ref ipPoint表示接收消息的IP地址和端口号
                // ReceiveMsg为接收完成后回调的方法
                serverSocket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);

                //定时检测超时线程
                ThreadPool.QueueUserWorkItem(CheckTimeOut);
            }
            catch (Exception e)
            {
                Console.WriteLine("UDP开启出错" + e.Message);
            }
        }

        // 关闭服务器
        public void Close()
        {
            if (serverSocket != null)
            {
                isServerClose = true;
                serverSocket.Shutdown(SocketShutdown.Both);
                serverSocket.Close();
                serverSocket = null;
            }
        }

        // 定期检测是否有超时客户端需要移除
        private void CheckTimeOut(object obj)
        {
            long nowTime = 0;
            List<string> delList = new List<string>();
            while (true)
            {
                //每30s检测一次 是否移除长时间没有接收到消息的客户端信息
                Thread.Sleep(30000);
                //得到当前系统时间
                nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                foreach (Client c in clientDictionary.Values)
                {
                    //超过10秒没有收到消息的 客户端信息 需要被移除
                    if (nowTime - c.frontTime >= 10)
                        delList.Add(c.clientStrID);
                }
                //从待删除列表中移除 超时的客户端信息
                for (int i = 0; i < delList.Count; i++)
                    RemoveClient(delList[i]);
                delList.Clear();
            }
        }

        // 异步接收到消息后的处理
        private void ReceiveMsg(IAsyncResult asyncResult)
        {
            //接收消息的容器
            //记录谁发的
            //用于拼接字符串 位移ID 是由 IP + 端口构成的

            // 获取IP地址和端口号
            EndPoint ipPoint = asyncResult.AsyncState as IPEndPoint;
            string ip = (ipPoint as IPEndPoint).Address.ToString();
            int port = (ipPoint as IPEndPoint).Port;
            string strID = ip + port;//拼接成一个唯一ID 这个是我们自定义的规则

            try
            {
                // 调用EndReceiveFrom方法获取接收到的消息
                serverSocket.EndReceiveFrom(asyncResult, ref ipPoint);

                // 判断有没有记录这个客户端信息 如果有 用它直接处理消息
                if (clientDictionary.ContainsKey(strID))
                    clientDictionary[strID].ReceiveMsg(cacheBytes);
                else
                {
                    // 如果没有,直接添加并且处理消息
                    clientDictionary.Add(strID, new Client(ip, port));
                    clientDictionary[strID].ReceiveMsg(cacheBytes);
                }

                //继续接受消息
                serverSocket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);
            }
            catch (SocketException s)
            {
                Console.WriteLine("接受消息出错" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("接受消息出错(非Socket错误)" + e.Message);
            }
        }

        // 指定发送一个消息给某个目标
        public void SendTo(BaseMessage baseMessage, IPEndPoint ipPoint)
        {
            try
            {
                byte[] bytes = baseMessage.Writing();
                // BeginXXX系列方法都是异步方法,此处使用BeginSendTo异步发送消息
                // bytes为要发送的消息数据,0为偏移量,bytes.Length为消息数据的长度
                // SocketFlags.None表示不使用任何选项
                // ipPoint表示要发送的IP地址和端口号
                // 回调方法中处理如果发送失败的情况
                serverSocket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, (result) =>
                {
                    try
                    {
                        serverSocket.EndSendTo(result);
                    }
                    catch (SocketException s)
                    {
                        Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
                    }
                }, null);
            }
            catch (SocketException s)
            {
                Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
            }

        }

        // 广播消息给所有客户端
        public void Broadcast(BaseMessage baseMessage)
        {
            //广播消息 给谁广播
            foreach (Client c in clientDictionary.Values)
            {
                SendTo(baseMessage, c.clientIPandPort);
            }
        }

        // 移除客户端信息
        public void RemoveClient(string clientID)
        {
            if (clientDictionary.ContainsKey(clientID))
            {
                Console.WriteLine("客户端{0}被移除了" + clientDictionary[clientID].clientIPandPort);
                clientDictionary.Remove(clientID);
            }
        }
    }
}

Client

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading;

namespace Lesson38_网络通信_套接字Socket_UDP通信_异步_服务端综合练习题
{
    class Client
    {
        public IPEndPoint clientIPandPort; // 客户端的IP地址和端口
        public string clientStrID; // 客户端的唯一标识

        // 上一次收到消息的时间
        public long frontTime = -1;

        public Client(string ip, int port)
        {
            // 规则和外面一样,记录唯一ID,通过 ip + port 拼接的形式
            clientStrID = ip + port;
            // 将客户端的信息记录下来
            clientIPandPort = new IPEndPoint(IPAddress.Parse(ip), port);
        }

        // 接收消息
        public void ReceiveMsg(byte[] bytes)
        {
            // 为了避免处理消息时又接受到了其它消息,所以我们需要在处理之前先把信息拷贝出来
            // 处理消息和接收消息用不同的容器,避免出现问题
            byte[] cacheBytes = new byte[512];
            bytes.CopyTo(cacheBytes, 0);
            // 记录收到消息的系统时间,单位为秒
            frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
            ThreadPool.QueueUserWorkItem(ReceiveHandle, cacheBytes);
        }

        // 多线程处理消息
        private void ReceiveHandle(object obj)
        {
            try
            {
                // 取出传进来的字节
                byte[] bytes = obj as byte[];
                int nowIndex = 0;
                // 先处理消息ID
                int msgID = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                // 再处理消息长度
                int msgLength = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                // 再解析消息体
                switch (msgID)
                {
                    case 1001:
                        PlayerMessage playerMessage = new PlayerMessage();
                        playerMessage.Reading(bytes, nowIndex);
                        Console.WriteLine(playerMessage.playerID);
                        Console.WriteLine(playerMessage.playerData.name);
                        Console.WriteLine(playerMessage.playerData.atk);
                        Console.WriteLine(playerMessage.playerData.lev);
                        break;
                    case 1003:
                        QuitMessage quitMessage = new QuitMessage();
                        // 由于它没有消息体,所以不用反序列化
                        // quitMessage.Reading(bytes, nowIndex);
                        // 处理退出
                        Program.serverSocket.RemoveClient(clientStrID);
                        break;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("处理消息时出错" + e.Message);
                // 如果出错,就不用记录这个客户端信息
                Program.serverSocket.RemoveClient(clientStrID);
            }
        }
    }
}

Lesson38_网络通信_套接字Socket_UDP通信_异步_服务端综合练习题

namespace Lesson38_网络通信_套接字Socket_UDP通信_异步_服务端综合练习题
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {
            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080);

            Console.WriteLine("UDP服务器启动了");

            while (true)
            {
                string input = Console.ReadLine();
                if (input.Substring(0, 2) == "B:")
                {
                    PlayerMessage msg = new PlayerMessage();
                    msg.playerData = new PlayerData();
                    msg.playerID = 1001;
                    msg.playerData.name = "韬老狮的UDP异步服务器";
                    msg.playerData.atk = 88;
                    msg.playerData.lev = 66;
                    serverSocket.Broadcast(msg);
                }
            }
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏