# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/UnityGameDemo/tree/main/A simple web chat room
# 网络聊天室
本章将制作一个基于 UDP 的网络聊天室,客户端与服务端都用 Unity 来实现(实际的开发中,服务端一定不是用 Unity 做的)
需求:
- 用户可以自由选择登录服务端或客户端。
- 客户端:输入名字、服务端的 IP 和端口后即可进行多人聊天。
- 服务端:输入服务端的 IP 和端口后即可开启服务,无法进行聊天,可以看到谁在线,以及他们的 IP 和端口
# 需求分析
- 使用 ClientSever 架构![img]() 
- 向服务端发送数据时,不能发送自定义的类,只能发送基本的数据类型,例如:bool/int/float/string...
- 自定义协议:按照指定的格式发送数据和读取数据![img]() 
具体内容
- 发送的消息可能有很多种,使用枚举区分
- 将消息的内容封装到一个类中,自定义协议,提供字节数组与对象之间的转换的功能
- 制作客户端网络服务类,实现接收和发送消息的功能,将网络部分与其他部分隔离
- 实现登录功能,通知服务端上线
- 制作客户端的脚本,实现基本的功能
- 制作服务端网络服务类,与客户端网络服务类相同
- 制作服务端的脚本,实现基本的功能
- 仅有三个类是最重要的:消息内容、客户端网络服务类、服务端网络服务类。其他的类都不是重点。

# 代码部分
# 消息类型
代码如下:
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 消息类型 | |
|     /// </summary> | |
| public enum MessageType | |
|     { | |
|         /// <summary> | |
|         /// 上线 | |
|         /// </summary> | |
|         OnLine, | |
|         /// <summary> | |
|         /// 下线 | |
|         /// </summary> | |
|         OffLine, | |
|         /// <summary> | |
|         /// 普通消息 | |
|         /// </summary> | |
|         General, | |
|     } | |
| } | 
# 消息内容
- 封装聊天信息
- 字节数组的转换:可以单独处理每个数据类型的转换,但是这样做比较麻烦。所以,本文使用内存流、二进制写入器、二进制读取器
代码如下:
| using System; | |
| using System.IO; | |
| using System.Text; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 聊天信息 | |
|     /// </summary> | |
| public class ChatMessage | |
|     { | |
|         /// <summary> | |
|         /// 消息类型 | |
|         /// </summary> | |
| public MessageType Type; | |
|         /// <summary> | |
|         /// 发送者 | |
|         /// </summary> | |
| public string SenderName; | |
|         /// <summary> | |
|         /// 发送内容 | |
|         /// </summary> | |
| public string Content; | |
|         /// <summary> | |
|         /// ChatMessage | |
|         /// </summary> | |
| public ChatMessage() { } | |
|         /// <summary> | |
|         /// 赋值 | |
|         /// </summary> | |
|         /// <param name="messageType">messageType</param> | |
|         /// <param name="senderName">senderName</param> | |
|         /// <param name="content">content</param> | |
| public ChatMessage(MessageType messageType, string senderName, string content) | |
|         { | |
| Type = messageType; | |
| SenderName = senderName; | |
| Content = content; | |
|         } | |
|         /// <summary> | |
|         /// object -> byte[] | |
|         /// </summary> | |
|         /// <returns></returns> | |
| public byte[] ObjectToBytes() | |
|         { | |
|             //string/int/bool... -- 二进制写入器 -- 内存流 --> byte [] | |
|             // 内存流 | |
| using (MemoryStream stream = new MemoryStream()) | |
|             { | |
|                 // 二进制写入器 | |
|                 // 属性 -> 内存流 | |
| BinaryWriter writer = new BinaryWriter(stream); | |
| WriteString(writer, Type.ToString()); | |
| WriteString(writer, SenderName); | |
| WriteString(writer, Content); | |
|                 // 内存流 -> byte [] | |
| return stream.ToArray(); | |
|             } | |
|         } | |
|         /// <summary> | |
|         /// 写入二进制数据 | |
|         /// </summary> | |
|         /// <param name="writer">writer</param> | |
|         /// <param name="str">string</param> | |
| private void WriteString(BinaryWriter writer, string str) | |
|         { | |
|             // 不用这个,因为这个只支持一种编码 | |
|             //writer.Write(str); | |
|             // 编码 | |
| byte[] byte_Type = Encoding.UTF8.GetBytes(str); | |
|             // 写入长度 | |
| writer.Write(byte_Type.Length); | |
|             // 写入内容 | |
| writer.Write(byte_Type); | |
|         } | |
|         /// <summary> | |
|         /// byte[] -> object | |
|         /// </summary> | |
|         /// <param name="bytes">bytes</param> | |
|         /// <returns></returns> | |
| public static ChatMessage BytesToObject(byte[] bytes) | |
|         { | |
|             //byte [] -- 内存流 -- 二进制读取器 --> string/int/bool... | |
| using (MemoryStream stream = new MemoryStream(bytes)) | |
|             { | |
| BinaryReader reader = new BinaryReader(stream); | |
| MessageType type = (MessageType)Enum.Parse(typeof(MessageType), ReadString(reader)); | |
| string senderName = ReadString(reader); | |
| string content = ReadString(reader); | |
| return new ChatMessage(type, senderName, content); | |
|             } | |
|         } | |
|         /// <summary> | |
|         /// 读取二进制数据 | |
|         /// </summary> | |
|         /// <param name="reader">reader</param> | |
|         /// <returns></returns> | |
| private static string ReadString(BinaryReader reader) | |
|         { | |
|             // 不用这个,因为这个只支持一种编码 | |
|             //reader.ReadString(); | |
|             // 读 4 个字节:标记了接下来要读取的长度 | |
| int length = reader.ReadInt32(); | |
|             // 读指定的长度 | |
| byte[] bytes = reader.ReadBytes(length); | |
|             // 转字符串 | |
| return Encoding.UTF8.GetString(bytes); | |
|         } | |
|     } | |
| } | 
# 登录信息类
- 储存登录的信息,例如:IP、端口、名字
代码如下:
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 登录信息 | |
|     /// </summary> | |
| public class LogInContent | |
|     { | |
|         /// <summary> | |
|         /// Name | |
|         /// </summary> | |
| public string Name; | |
|         /// <summary> | |
|         /// IP | |
|         /// </summary> | |
| public string IP; | |
|         /// <summary> | |
|         /// Port | |
|         /// </summary> | |
| public string Port; | |
|     } | |
| } | 
# 客户端网络服务类
- 这个类只需要一个,所以使用单例模式
- 使用 UDP 协议
- 创建一个线程,用于接收消息
- 使用事件处理消息的接收:接收到消息后触发事件并传递参数
- 辅助线程不能访问 Unity 的 API,需要使用线程交叉访问助手解决这个问题
事件参数类:
| using System; | |
| using System.Net; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 接收事件的信息类 | |
|     /// </summary> | |
| public class ReceiveEventArgs : EventArgs | |
|     { | |
|         /// <summary> | |
|         /// ChatMessage | |
|         /// </summary> | |
| public ChatMessage Message; | |
|         /// <summary> | |
|         /// IPEndPoint | |
|         /// </summary> | |
| public IPEndPoint SourceRemote; | |
| public ReceiveEventArgs() { } | |
| public ReceiveEventArgs(ChatMessage message, IPEndPoint sourceRemote) | |
|         { | |
| Message = message; | |
| SourceRemote = sourceRemote; | |
|         } | |
|     } | |
| } | 
客户端网络服务类:
| using Common; | |
| using System; | |
| using System.Net; | |
| using System.Net.Sockets; | |
| using System.Threading; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 客户端 UDP 网络服务 | |
|     /// </summary> | |
| public class ClientUdpNetService : MonoSingleton<ClientUdpNetService> | |
|     { | |
|         /// <summary> | |
|         /// 接收到消息后触发 | |
|         /// </summary> | |
| public event EventHandler<ReceiveEventArgs> OnReceive; | |
|         /// <summary> | |
|         /// 登录信息 | |
|         /// </summary> | |
| public LogInContent Content; | |
|         /// <summary> | |
|         /// UdpClient | |
|         /// </summary> | |
| private UdpClient UDPClient; | |
|         /// <summary> | |
|         /// 客户端线程 | |
|         /// </summary> | |
| private Thread ClientThread; | |
| private void Start() | |
|         { | |
| ClientThread = new Thread(ReceiveMessage); | |
|         } | |
|         /// <summary> | |
|         /// 初始化 | |
|         /// 由登录窗口传递 IP 和端口 | |
|         /// </summary> | |
|         /// <param name="content"> 登录信息 & lt;/param> | |
| public void Initialize(LogInContent content) | |
|         { | |
| Content = content; | |
|             // 随机分配端口 | |
| UDPClient = new UdpClient(); | |
|             // 与服务端连接(仅仅是配置自身的 Socket) | |
|             // 单播:只能给服务器发数据 | |
| UDPClient.Connect(IPAddress.Parse(Content.IP), int.Parse(Content.Port)); | |
|             // 开启线程 | |
| ClientThread.Start(); | |
| NotiyfSever(MessageType.OnLine); | |
|         } | |
| private void NotiyfSever(MessageType type) | |
|         { | |
| if (Content == null) return; | |
| SendMessage(new ChatMessage(type, Content.Name, string.Empty)); | |
|         } | |
|         /// <summary> | |
|         /// 发送数据 | |
|         /// </summary> | |
|         /// <param name="message"> 信息 & lt;/param> | |
| public void SendMessage(ChatMessage message) | |
|         { | |
| byte[] bytes = message.ObjectToBytes(); | |
|             // 创建 Socket 对象时建立了链接,所以不能绑定终结点 | |
| UDPClient.Send(bytes, bytes.Length); | |
|         } | |
|         /// <summary> | |
|         /// 接收消息 | |
|         /// </summary> | |
|         /// <param name="message"></param> | |
| private void ReceiveMessage() | |
|         { | |
| while (true) | |
|             { | |
|                 // 任意 IP 地址,任意端口 | |
| IPEndPoint sourceRemote = new IPEndPoint(IPAddress.Any, 0); | |
|                 // 接收消息 | |
| byte[] bytes = UDPClient.Receive(ref sourceRemote); | |
|                 // 转化消息 | |
| ChatMessage message = ChatMessage.BytesToObject(bytes); | |
|                 // 事件参数 | |
| ReceiveEventArgs args = new ReceiveEventArgs(message, sourceRemote); | |
|                 // 在主线程中触发事件 | |
| ThreadCrossHelper.Instance.ExecuteOnMainThread(() => | |
|                 { | |
| OnReceive?.Invoke(this, args); | |
| }); | |
|             } | |
|         } | |
| private void OnApplicationQuit() | |
|         { | |
| NotiyfSever(MessageType.OffLine); | |
| ClientThread?.Abort(); | |
| UDPClient?.Dispose(); | |
|         } | |
|     } | |
| } | 
# 登录功能
- 登录界面分两部分制作,点击按钮切换![img]() ![img]() ![img]() 
- 使用变换组件助手类寻找物体
- 注册按钮事件,切换场景
- 保存用户输入的信息
- 会涉及到服务端网络服务类的代码,它与客户端网络服务类的代码相似,具体内容会在下文中讲到
登录信息输入,分别挂载到物体 “ClientContent” 和 “SeverContent” 上
| using Common; | |
| using TMPro; | |
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 登录信息输入 | |
|     /// </summary> | |
| public class LogInInput : MonoBehaviour | |
|     { | |
|         /// <summary> | |
|         /// Input_Name | |
|         /// </summary> | |
| private TMP_InputField Input_Name; | |
|         /// <summary> | |
|         /// Input_IP | |
|         /// </summary> | |
| private TMP_InputField Input_IP; | |
|         /// <summary> | |
|         /// Input_Port | |
|         /// </summary> | |
| private TMP_InputField Input_Port; | |
|         /// <summary> | |
|         /// 登录信息 | |
|         /// </summary> | |
| public LogInContent Content | |
|         { | |
|             get | |
|             { | |
| return new LogInContent() | |
|                 { | |
| Name = Input_Name?.text, | |
| IP = Input_IP.text, | |
| Port = Input_Port.text, | |
| }; | |
|             } | |
|         } | |
| private void Awake() | |
|         { | |
| Input_Name = this.transform.FindChildByName("Input_Name")?.GetComponent<TMP_InputField>(); | |
| Input_IP = this.transform.FindChildByName("Input_IP").GetComponent<TMP_InputField>(); | |
| Input_Port = this.transform.FindChildByName("Input_Port").GetComponent<TMP_InputField>(); | |
|         } | |
|     } | |
| } | 
登录按钮的处理,挂载到物体 “Canvas” 上
| using UnityEngine; | |
| using UnityEngine.SceneManagement; | |
| using UnityEngine.UI; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// LogInButton | |
|     /// </summary> | |
| public class LogInButtons : MonoBehaviour | |
|     { | |
|         /// <summary> | |
|         /// LogInType | |
|         /// </summary> | |
| private enum LogInType | |
|         { | |
|             /// <summary> | |
|             /// 客户端 | |
|             /// </summary> | |
|             Client, | |
|             /// <summary> | |
|             /// 服务端 | |
|             /// </summary> | |
|             Sever, | |
|         } | |
| [Header("Client")] | |
| [Tooltip("客户端登录")] | |
| public Button Client; | |
| [Tooltip("ClientContent")] | |
| public GameObject ClientContent; | |
| [Tooltip("ClientImage")] | |
| private Image ClientImage; | |
| [Header("Sever")] | |
| [Tooltip("服务端登录")] | |
| public Button Sever; | |
| [Tooltip("SeverContent")] | |
| public GameObject SeverContent; | |
| [Tooltip("SeverImage")] | |
| private Image SeverImage; | |
| [Header("Enter")] | |
| [Tooltip("登录按钮")] | |
| public Button Enter; | |
|         /// <summary> | |
|         /// LogInType | |
|         /// </summary> | |
| private LogInType LogType; | |
| private void Awake() | |
|         { | |
|             // 考虑到遍历物体的性能开销,所以不用代码找物体 | |
| ClientImage = Client.GetComponent<Image>(); | |
| SeverImage = Sever.GetComponent<Image>(); | |
|         } | |
| private void Start() | |
|         { | |
| LogType = LogInType.Client; | |
| Client.onClick.AddListener(OnClientButtonClick); | |
| Sever.onClick.AddListener(OnServerButtonClick); | |
| Enter.onClick.AddListener(OnEnterButtonClick); | |
|         } | |
|         /// <summary> | |
|         /// OnClientButtonClick | |
|         /// </summary> | |
| private void OnClientButtonClick() | |
|         { | |
| ClientImage.color = Color.red; | |
| ClientContent.SetActive(true); | |
| SeverImage.color = Color.white; | |
| SeverContent.SetActive(false); | |
| LogType = LogInType.Client; | |
|         } | |
|         /// <summary> | |
|         /// OnServerButtonClick | |
|         /// </summary> | |
| private void OnServerButtonClick() | |
|         { | |
| ClientImage.color = Color.white; | |
| ClientContent.SetActive(false); | |
| SeverImage.color = Color.red; | |
| SeverContent.SetActive(true); | |
| LogType = LogInType.Sever; | |
|         } | |
|         /// <summary> | |
|         /// OnEnterButtonClick | |
|         /// </summary> | |
| private void OnEnterButtonClick() | |
|         { | |
| switch (LogType) | |
|             { | |
| case LogInType.Client: | |
| GoClient(); | |
| break; | |
| case LogInType.Sever: | |
| GoServer(); | |
| break; | |
| default: | |
| break; | |
|             } | |
|         } | |
|         /// <summary> | |
|         /// GoClient | |
|         /// </summary> | |
| private void GoClient() | |
|         { | |
| LogInInput logInInput = ClientContent.GetComponent<LogInInput>(); | |
| ClientUdpNetService.Instance.Initialize(logInInput.Content); | |
| SceneManager.LoadScene("ChatDemoClient", LoadSceneMode.Single); | |
|         } | |
|         /// <summary> | |
|         /// GoServer | |
|         /// </summary> | |
| private void GoServer() | |
|         { | |
| LogInInput logInInput = SeverContent.GetComponent<LogInInput>(); | |
| SeverUdpNetService.Instance.Initialize(logInInput.Content); | |
| SceneManager.LoadScene("ChatDemoSever", LoadSceneMode.Single); | |
|         } | |
|     } | |
| } | 
# 客户端的脚本
- 制作 UI 界面![img]() ![img]() 
- 接收消息、发送消息、生成消息(预制件)
消息的拥有者
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 消息的拥有者 | |
|     /// </summary> | |
| public enum MessageOwer | |
|     { | |
|         My, | |
| Other | |
|     } | |
| } | 
挂载到预制件上
| using Common; | |
| using System; | |
| using TMPro; | |
| using UnityEngine; | |
| using UnityEngine.UI; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 客户端信息管理 | |
|     /// </summary> | |
| public class ClientMessageManger : MonoBehaviour | |
|     { | |
|         /// <summary> | |
|         /// 设置信息 | |
|         /// </summary> | |
|         /// <param name="message"> 信息 & lt;/param> | |
|         /// <param name="name"> 发送者 & lt;/param> | |
| public void SetMessage(string message, string name) | |
|         { | |
| this.transform.FindChildByName("Message").GetComponent<TextMeshProUGUI>().text = message; | |
| this.transform.FindChildByName("Name").GetComponent<TextMeshProUGUI>().text = name; | |
| this.transform.FindChildByName("Time").GetComponent<TextMeshProUGUI>().text = DateTime.Now.ToString(); | |
|         } | |
|     } | |
| } | 
挂载到 “Content” 上
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 创建客户端消息 | |
|     /// </summary> | |
| public class CreateClientMessage : MonoBehaviour | |
|     { | |
| [Tooltip("我的消息预制件")] | |
| public GameObject MessagePrefab_My; | |
| [Tooltip("其他人的消息预制件")] | |
| public GameObject MessagePrefab_Other; | |
|         /// <summary> | |
|         /// 增加新消息 | |
|         /// </summary> | |
|         /// <param name="ower"> 消息属于谁 & lt;/param> | |
|         /// <param name="chatMessage"> 消息 & lt;/param> | |
| public void AddMessage(MessageOwer ower, ChatMessage chatMessage) | |
|         { | |
| switch (ower) | |
|             { | |
| case MessageOwer.My: | |
| Create(MessagePrefab_My, chatMessage); | |
| break; | |
| case MessageOwer.Other: | |
| Create(MessagePrefab_Other, chatMessage); | |
| break; | |
| default: | |
| break; | |
|             } | |
|         } | |
|         /// <summary> | |
|         /// 创建物体 | |
|         /// </summary> | |
|         /// <param name="prefab"> 预制件 & lt;/param> | |
|         /// <param name="chatMessage"> 信息 & lt;/param> | |
| private void Create(GameObject prefab, ChatMessage chatMessage) | |
|         { | |
| GameObject obj = Instantiate(prefab, this.transform); | |
| obj.GetComponent<ClientMessageManger>().SetMessage(chatMessage.Content, chatMessage.SenderName); | |
|         } | |
|     } | |
| } | 
挂载到 “Canvas” 上
| using Common; | |
| using TMPro; | |
| using UnityEngine; | |
| using UnityEngine.UI; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 客户端逻辑 | |
|     /// </summary> | |
| public class ChatClient : MonoBehaviour | |
|     { | |
| [Tooltip("增加消息")] | |
| public CreateClientMessage Create; | |
|         /// <summary> | |
|         /// SendButton | |
|         /// </summary> | |
| private Button SendButton; | |
|         /// <summary> | |
|         /// 输入的信息 | |
|         /// </summary> | |
| private TMP_InputField InputMessage; | |
| private void Awake() | |
|         { | |
| SendButton = this.transform.FindChildByName("SendButton").GetComponent<Button>(); | |
| InputMessage = this.transform.FindChildByName("InputMessage").GetComponent<TMP_InputField>(); | |
|         } | |
| private void Update() | |
|         { | |
| if (Input.GetKey(KeyCode.LeftControl) && Input.GetKey(KeyCode.Return)) | |
|             { | |
| OnSendMessage(); | |
|             } | |
|         } | |
| private void OnEnable() | |
|         { | |
| SendButton.onClick.AddListener(OnSendMessage); | |
| ClientUdpNetService.Instance.OnReceive += OnReceiveMessage; | |
|         } | |
| private void OnDisable() | |
|         { | |
| SendButton.onClick.RemoveListener(OnSendMessage); | |
| ClientUdpNetService.Instance.OnReceive -= OnReceiveMessage; | |
|         } | |
|         /// <summary> | |
|         /// 按下发送消息按钮 | |
|         /// </summary> | |
| private void OnSendMessage() | |
|         { | |
| if (InputMessage.text == string.Empty) return; | |
| ChatMessage message = new ChatMessage() | |
|             { | |
| Type = MessageType.General, | |
| SenderName = ClientUdpNetService.Instance.Content.Name, | |
| Content = InputMessage.text | |
| }; | |
| Create.AddMessage(MessageOwer.My, message); | |
| ClientUdpNetService.Instance.SendMessage(message); | |
| InputMessage.text = string.Empty; | |
|         } | |
|         /// <summary> | |
|         /// 接收消息 | |
|         /// </summary> | |
|         /// <param name="sender"></param> | |
|         /// <param name="receive"></param> | |
| private void OnReceiveMessage(object sender, ReceiveEventArgs receive) | |
|         { | |
| if (receive.Message.SenderName != ClientUdpNetService.Instance.Content.Name) | |
|             { | |
| Create.AddMessage(MessageOwer.Other, receive.Message); | |
|             } | |
|         } | |
|     } | |
| } | 
# 服务端网络服务类
- 需要分配 IP 和端口
- 使用列表储存所有已经上线的客户端
- 其他部分与客户端网络服务类类似
代码如下:
| using Common; | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Net; | |
| using System.Net.Sockets; | |
| using System.Threading; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 服务端 UDP 网络服务 | |
|     /// </summary> | |
| public class SeverUdpNetService : MonoSingleton<SeverUdpNetService> | |
|     { | |
|         /// <summary> | |
|         /// 接收到消息后触发 | |
|         /// </summary> | |
| public event EventHandler<ReceiveEventArgs> OnReceive; | |
|         /// <summary> | |
|         /// 登录信息 | |
|         /// </summary> | |
| public LogInContent Content; | |
|         /// <summary> | |
|         /// UdpClient | |
|         /// </summary> | |
| private UdpClient UDPClient; | |
|         /// <summary> | |
|         /// 服务端线程 | |
|         /// </summary> | |
| private Thread SeverThread; | |
|         /// <summary> | |
|         /// 客户端列表 | |
|         /// </summary> | |
| public List<IPEndPoint> ClientRemotes { get; private set; } | |
| private void Start() | |
|         { | |
| ClientRemotes = new List<IPEndPoint>(); | |
| SeverThread = new Thread(ReceiveMessage); | |
|         } | |
|         /// <summary> | |
|         /// 初始化 | |
|         /// 由登录窗口传递 IP 和端口 | |
|         /// </summary> | |
|         /// <param name="content"> 登录信息 & lt;/param> | |
| public void Initialize(LogInContent content) | |
|         { | |
| Content = content; | |
|             // 分配端口 | |
| IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(Content.IP), int.Parse(Content.Port)); | |
| UDPClient = new UdpClient(iPEndPoint); | |
|             // 开启线程 | |
| SeverThread.Start(); | |
|         } | |
|         /// <summary> | |
|         /// 发送数据 | |
|         /// </summary> | |
|         /// <param name="message"> 信息 & lt;/param> | |
|         /// <param name="remote"> 目标终结点 & lt;/param> | |
| public void SendMessage(ChatMessage message, IPEndPoint remote) | |
|         { | |
| byte[] bytes = message.ObjectToBytes(); | |
| UDPClient.Send(bytes, bytes.Length, remote); | |
|         } | |
|         /// <summary> | |
|         /// 接收消息 | |
|         /// </summary> | |
| private void ReceiveMessage() | |
|         { | |
| while (true) | |
|             { | |
|                 // 任意 IP 地址,任意端口 | |
| IPEndPoint sourceRemote = new IPEndPoint(IPAddress.Any, 0); | |
|                 // 接收消息 | |
| byte[] bytes = UDPClient.Receive(ref sourceRemote); | |
|                 // 转化消息 | |
| ChatMessage message = ChatMessage.BytesToObject(bytes); | |
|                 // 根据消息类型执行对应的逻辑 | |
| MessageReceived(message, sourceRemote); | |
|                 // 事件参数 | |
| ReceiveEventArgs args = new ReceiveEventArgs(message, sourceRemote); | |
|                 // 在主线程中触发事件 | |
| ThreadCrossHelper.Instance.ExecuteOnMainThread(() => | |
|                 { | |
| OnReceive?.Invoke(this, args); | |
| }); | |
|             } | |
|         } | |
|         /// <summary> | |
|         /// 根据消息类型执行对应的逻辑 | |
|         /// </summary> | |
|         /// <param name="message"></param> | |
|         /// <param name="remote"></param> | |
| private void MessageReceived(ChatMessage message, IPEndPoint remote) | |
|         { | |
| switch (message.Type) | |
|             { | |
| case MessageType.OnLine: | |
| ClientRemotes.Add(remote); | |
| break; | |
| case MessageType.OffLine: | |
| ClientRemotes.Remove(remote); | |
| break; | |
| case MessageType.General: | |
| ClientRemotes.ForEach((item) => { SendMessage(message, item); }); | |
| break; | |
| default: | |
| break; | |
|             } | |
|         } | |
| private void OnApplicationQuit() | |
|         { | |
| SeverThread?.Abort(); | |
| UDPClient?.Dispose(); | |
|         } | |
|     } | |
| } | 
# 服务端的脚本
- 制作 UI![img]() ![img]() 
- 接收消息、生成消息(预制件)
- 消息的预制件会频繁地创建和销毁,可以使用对象池
挂载到预制件上
| using Common; | |
| using System.Net; | |
| using TMPro; | |
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 服务端信息管理 | |
|     /// </summary> | |
| public class SeverMessageManger : MonoBehaviour | |
|     { | |
|         /// <summary> | |
|         /// 设置信息 | |
|         /// </summary> | |
|         /// <param name="sourceRemote">source remote</param> | |
| public void SetMessage(IPEndPoint sourceRemote) | |
|         { | |
| this.transform.FindChildByName("SourceIPInformation").GetComponent<TextMeshProUGUI>().text = sourceRemote.Address.ToString(); | |
| this.transform.FindChildByName("SourcePortInformation").GetComponent<TextMeshProUGUI>().text = sourceRemote.Port.ToString(); | |
|         } | |
|     } | |
| } | 
挂载到 “Content” 上
| using Common; | |
| using System.Net; | |
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 创建服务端消息 | |
|     /// </summary> | |
| public class CreateSeverMessage : MonoBehaviour | |
|     { | |
| [Tooltip("服务端消息预制件")] | |
| public GameObject SeverMessage; | |
| public void AddMessage(IPEndPoint sourceRemote) | |
|         { | |
| for (int i = 0; i < this.transform.childCount; i++) | |
|             { | |
| ObjectPool.Instance.RecoverGameObject(this.transform.GetChild(i).gameObject); | |
|             } | |
| for (int i = 0; i < SeverUdpNetService.Instance.ClientRemotes.Count; i++) | |
|             { | |
| GameObject obj = ObjectPool.Instance.GetGameObject("SeverMessage", SeverMessage, this.transform); | |
| obj.GetComponent<SeverMessageManger>().SetMessage(sourceRemote); | |
|             } | |
|         } | |
|     } | |
| } | 
挂载到 “SeverWindow” 上
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 服务端逻辑 | |
|     /// </summary> | |
| public class ChatSever : MonoBehaviour | |
|     { | |
| [Tooltip("增加消息")] | |
| public CreateSeverMessage Create; | |
| private void OnEnable() | |
|         { | |
| SeverUdpNetService.Instance.OnReceive += OnReceiveMessage; | |
|         } | |
| private void OnDisable() | |
|         { | |
| SeverUdpNetService.Instance.OnReceive -= OnReceiveMessage; | |
|         } | |
|         /// <summary> | |
|         /// 接收消息 | |
|         /// </summary> | |
|         /// <param name="sender"></param> | |
|         /// <param name="receive"></param> | |
| private void OnReceiveMessage(object sender, ReceiveEventArgs receive) | |
|         { | |
| if (receive.Message.Type != MessageType.General) | |
|             { | |
| Create.AddMessage(receive.SourceRemote); | |
|             } | |
|         } | |
|     } | |
| } | 
# 切换场景不销毁物体
- 客户端网络服务类、服务端网络服务类、线程交叉访问助手类都挂载到同一个物体上
- 在切换场景后还需要使用这个物体,所以不能销毁

代码如下:
| using UnityEngine; | |
| namespace ChatDemo | |
| { | |
|     /// <summary> | |
|     /// 切换场景保留物体 | |
|     /// </summary> | |
| public class KeepObject : MonoBehaviour | |
|     { | |
| private void Start() | |
|         { | |
| DontDestroyOnLoad(this.gameObject); | |
|         } | |
|     } | |
| } | 
# 类图


# 测试











