35.UDP同步服务端接收多客户端

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


35.1 知识点

如同TCP通信一样让UDP服务端可以服务多个客户端

  • 需要具备的功能有:
    • 区分消息类型(不需要处理分包、黏包)
    • 能够接受多个客户端的消息
    • 能够主动给发送过消息给自己的客户端发消息(记录客户端信息)
    • 主动记录上一次收到客户端消息的时间,如果长时间没有收到消息,主动移除记录的客户端信息

ServerSocket 类

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

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

//保存客户端连接的所有Socket的字典 我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
private Dictionary<string, Client> clientDictionary = new Dictionary<string, Client>();
定义启动服务器方法,传入IP和端口并绑定。开两个线程分别处理消息和检查连接。定义关闭服务器方法。
// 启动服务器
public void Start(string ipString, int port)
{
    // 创建一个表示IP地址和端口的对象
    IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ipString), port);

    // 创建一个用于UDP通信的Socket
    serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

    try
    {
        // 绑定Socket到指定的IP地址和端口
        serverSocket.Bind(ipPoint);
        isServerClose = false;
        // 消息接收的处理 
        ThreadPool.QueueUserWorkItem(ReceiveMsg);
        // 定时检测超时线程
        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)
    {
        // 每30秒检测一次是否移除长时间没有接收到消息的客户端信息
        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(object obj)
{
    // 接收消息的容器
    byte[] bytes = new byte[512];
    // 记录消息发送者的IP地址和端口
    EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
    // 用于拼接字符串,唯一ID是由IP + 端口构成的
    string strID = "";
    string ip;
    int port;
    while (!isServerClose)
    {
        if (serverSocket.Available > 0)
        {
            // 接收消息
            lock (serverSocket)
                serverSocket.ReceiveFrom(bytes, ref ipPoint);
            // 处理消息,最好不要在这直接处理,而是交给客户端对象处理
            // 收到消息时,判断是否记录了这个客户端信息(IP和端口)
           // 取出发送消息给服务器的IP和端口
            ip = (ipPoint as IPEndPoint).Address.ToString();
            port = (ipPoint as IPEndPoint).Port;
            strID = ip + port; // 拼接成一个唯一ID,这个是自定义的规则
            // 判断是否记录了这个客户端信息,如果有,用它直接处理消息
            if (clientDictionary.ContainsKey(strID))
                clientDictionary[strID].ReceiveMsg(bytes);
            else // 如果没有,直接添加并且处理消息
            {
                clientDictionary.Add(strID, new Client(ip, port));
                clientDictionary[strID].ReceiveMsg(bytes);
            }
        }
    }
}

// 指定发送一个消息给某个目标
public void SendTo(BaseMessage baseMessage, IPEndPoint ipPoint)
{
    try
    {
        // 同步发送消息
        lock (serverSocket)
            serverSocket.SendTo(baseMessage.Writing(), ipPoint);
    }
    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 + 端口拼接而成
    clientStrID = ip + port;
    // 记录客户端的IP地址和端口
    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;
        // 根据消息ID解析消息体
        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 playerMessage = new PlayerMessage();
            playerMessage.playerData = new PlayerData();
            playerMessage.playerID = 1001;
            playerMessage.playerData.name = "韬老狮的UDP服务器";
            playerMessage.playerData.atk = 88;
            playerMessage.playerData.lev = 66;
            serverSocket.Broadcast(playerMessage);
        }
    }
}

注意

实际上这是为了练习才这样处理。这样非常不安全。因为别人知道了你的IP地址和端口,可能一直给你发垃圾消息导致解析失败或者阻塞。商业游戏中,通常先建立可靠的TCP连接,使用UDP收发消息前,会检查服务端是否和该客户端进行了TCP连接,保证安全可靠后在使用UDP通信


35.2 知识点代码

Client

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

namespace Lesson35_网络通信_套接字Socket_UDP通信_同步_服务端综合练习题
{
    // 用于记录和服务器通信过的客户端的IP和端口
    class Client
    {
        public IPEndPoint clientIPandPort; // 客户端的IP地址和端口信息
        public string clientStrID; // 客户端的唯一标识

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

        // 构造函数,初始化客户端信息
        public Client(string ip, int port)
        {
            // 创建唯一ID,由IP + 端口拼接而成
            clientStrID = ip + port;
            // 记录客户端的IP地址和端口
            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;
                // 根据消息ID解析消息体
                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);
            }
        }
    }
}

ServerSocket

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

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

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

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

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

            // 创建一个用于UDP通信的Socket
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            try
            {
                // 绑定Socket到指定的IP地址和端口
                serverSocket.Bind(ipPoint);
                isServerClose = false;
                // 消息接收的处理 
                ThreadPool.QueueUserWorkItem(ReceiveMsg);
                // 定时检测超时线程
                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)
            {
                // 每30秒检测一次是否移除长时间没有接收到消息的客户端信息
                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(object obj)
        {
            // 接收消息的容器
            byte[] bytes = new byte[512];
            // 记录消息发送者的IP地址和端口
            EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
            // 用于拼接字符串,唯一ID是由IP + 端口构成的
            string strID = "";
            string ip;
            int port;
            while (!isServerClose)
            {
                if (serverSocket.Available > 0)
                {
                    // 接收消息
                    lock (serverSocket)
                        serverSocket.ReceiveFrom(bytes, ref ipPoint);
                    // 处理消息,最好不要在这直接处理,而是交给客户端对象处理
                    // 收到消息时,判断是否记录了这个客户端信息(IP和端口)
                    // 取出发送消息给服务器的IP和端口
                    ip = (ipPoint as IPEndPoint).Address.ToString();
                    port = (ipPoint as IPEndPoint).Port;
                    strID = ip + port; // 拼接成一个唯一ID,这个是自定义的规则
                    // 判断是否记录了这个客户端信息,如果有,用它直接处理消息
                    if (clientDictionary.ContainsKey(strID))
                        clientDictionary[strID].ReceiveMsg(bytes);
                    else // 如果没有,直接添加并且处理消息
                    {
                        clientDictionary.Add(strID, new Client(ip, port));
                        clientDictionary[strID].ReceiveMsg(bytes);
                    }
                }
            }
        }

        // 指定发送一个消息给某个目标
        public void SendTo(BaseMessage baseMessage, IPEndPoint ipPoint)
        {
            try
            {
                // 同步发送消息
                lock (serverSocket)
                    serverSocket.SendTo(baseMessage.Writing(), ipPoint);
            }
            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);
            }
        }
    }
}

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

namespace Lesson35_网络通信_套接字Socket_UDP通信_同步_服务端综合练习题
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {
            #region UDP服务器要求
            //如同TCP通信一样让UDP服务端可以服务多个客户端
            //需要具备的功能有:
            //1.区分消息类型(不需要处理分包、黏包)
            //2.能够接受多个客户端的消息
            //3.能够主动给发送过消息给自己的客户端发消息(记录客户端信息)
            //4.主动记录上一次收到客户端消息的时间,如果长时间没有收到消息,主动移除记录的客户端信息

            //分析:
            //1.UDP是无连接的,我们如何记录连入的客户端?
            //2.UDP收发消息都是通过一个Socket来进行处理,我们应该如何处理收发消息?
            //3.如果不使用心跳消息,我们如何记录上次收到消息的时间?

            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 playerMessage = new PlayerMessage();
                    playerMessage.playerData = new PlayerData();
                    playerMessage.playerID = 1001;
                    playerMessage.playerData.name = "韬老狮的UDP服务器";
                    playerMessage.playerData.atk = 88;
                    playerMessage.playerData.lev = 66;
                    serverSocket.Broadcast(playerMessage);
                }
            }

            #endregion
        }
    }
}


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

×

喜欢就点赞,疼爱就打赏