UN2L11——服务端和客户端同步通信
本章代码关键字
1 2 socket.SendTo() socket.ReceiveFrom()
UDP同步收发消息API
同步发送消息
UDP同步通信中,一般用SendTo
来发送消息,使用前不需要建立连接,只需要绑定好本机IP即可
该方法有多种重载,最基础的重载参数包括:发送的字节数组、要发送的IP地址和端口号
参数一:发送的字节数组(必须)
参数二:偏移量,即从第几位开始发送
参数三:要发送多少字节
参数四:套接字标识
参数五:要发送的IP地址和端口号(必须)
1 2 3 4 5 6 7 8 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8080 ); socket.Bind(ipPoint); IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8081 ); socket.SendTo(Encoding.UTF8.GetBytes("唐老狮来了" ), remoteIpPoint);
同步接收消息
UDP同步通信中,一般用ReceiveFrom
来接收消息,使用前不需要建立连接,只需要绑定好本机IP即可
该方法有多种重载,最基础的重载参数包括:用来接收数据的字节数组、用于记录发送者IP地址和端口的EndPoint
该方法会返回一共接收了多少字节
参数一:用来接收数据的字节数组(必须)
参数二:偏移量,即从第几位开始接收数据
参数三:要接收多少字节
参数四:套接字标识
参数五:用于记录发送者IP地址和端口的EndPoint
(必须)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8081 ); socket.Bind(ipPoint); Console.WriteLine("服务器开启" ); byte [] bytes = new byte [512 ];EndPoint remoteIpPoint = new IPEndPoint(IPAddress.Any, 0 ); int receiveNum = socket.ReceiveFrom(bytes, ref remoteIpPoint);Console.WriteLine("IP: " + (remoteIpPoint as IPEndPoint)?.Address.ToString() + "port: " + (remoteIpPoint as IPEndPoint)?.Port.ToString() + "发来了: " + Encoding.UTF8.GetString(bytes, 0 , receiveNum));
服务端和客户端Socket的UDP同步通信
服务端和客户端的UDP通信基本思路是一致的:
创建套接字Socket
用Bind
方法将套接字与本地地址进行绑定
用ReceiveFrom
和SendTo
方法在套接字上收发消息
用Shutdown
方法释放连接
关闭套接字
因此它们的最基础的UDP同步通信实现也大同小异
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 using System.Net;using System.Net.Sockets;using System.Text;namespace TeachUdpServer { internal class Program { static void Main (string [] args ) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8081 ); socket.Bind(ipPoint); Console.WriteLine("服务器开启" ); byte [] bytes = new byte [512 ]; EndPoint remoteIpPoint = new IPEndPoint(IPAddress.Any, 0 ); int receiveNum = socket.ReceiveFrom(bytes, ref remoteIpPoint); Console.WriteLine("IP: " + (remoteIpPoint as IPEndPoint)?.Address.ToString() + "port: " + (remoteIpPoint as IPEndPoint)?.Port.ToString() + "发来了: " + Encoding.UTF8.GetString(bytes, 0 , receiveNum)); socket.SendTo(Encoding.UTF8.GetBytes("欢迎发送给服务器" ), remoteIpPoint); socket.Shutdown(SocketShutdown.Both); socket.Close(); Console.ReadKey(); } } }
实现一个相对完善的UDP服务端
以上的代码一次只能发送和接受一条消息,随后服务端的运行就会终止
我们可以通过开启一个新线程并使用循环来反复监听消息接收,同时做到不卡主线程,
接收到消息就将发送消息的客户端的IP地址和端口号记录起来,做到主动对发送过消息的客户端发消息
同时记录该客户端上一次发送消息的时间,如果长时间没有收到消息,就移除这个客户端的记录
同时,还要在收消息的同时区分消息类型
注意!我们目前写的服务端只是为了学习用,实际上这样的服务端是不安全的
因为在UDP的通信模式下,只要知道了服务端的IP地址就可以向该服务端发送消息
如果有人恶意发送不按照规定序列化的消息或者恶意发送垃圾信息,就有可能会出现处理消息的问题,增加服务器压力
实际上,一般根本不会使用UDP来记录客户端消息,而是先建立一层TCP连接,在根据这个TCP连接再使用UDP进行通信
Program.cs
socket
:让外部可以调用唯一的服务端socket
Main
:用于初始化服务端Socket,绑定其IP地址,然后检测终端输入,输入B:
就广播消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace TeachUdpServerExercises { internal class Program { public static ServerSocket? socket; static void Main (string [] args ) { socket = new ServerSocket(); socket.Start("127.0.0.1" , 8080 ); Console.WriteLine("UDP服务器启动了!" ); while (true ) { string ? input = Console.ReadLine(); if (string .IsNullOrEmpty(input)) continue ; if (input.Substring(0 , 2 ) == "B:" ) { PlayerMessage message = new PlayerMessage(); message.playerData = new PlayerData(); message.playerID = 1001 ; message.playerData.name = "唐老狮的UDP服务器" ; message.playerData.atk = 88 ; message.playerData.lev = 66 ; socket.Broadcast(message); } } } } }
ServerSocket.cs
isClose
:用于控制多线程循环是否结束,让多线程在socket存在时才继续执行
clientDic
:以发送者的IP地址和端口号为键,记录发送过消息的客户端的字典,用于广播时主动对发送过消息的客户端发消息
Start
:初始化服务端,绑定IP地址和端口号,开启接收消息和检测超时的线程
CheckTimeOut
:检测超时的线程主函数,每隔一段时间就检测一次客户端是否超时未发消息,过久没法消息的客户端消息会被删除
ReceiveMessage
:接收消息的线程主函数,循环接收数据,并调用发送者对应的Client
处理数据,将新发消息的客户端记录下来
SendTo
:向某个IP地址和端口号发送消息
Broadcast
:向所有记录过的客户端发消息
Close
:关闭并释放Socket
RemoveClient
:将记录过的客户端从字典内移除出去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 using System.Net;using System.Net.Sockets;namespace TeachUdpServerExercises { internal class ServerSocket { private Socket? socket; private bool isClose = true ; private Dictionary<string , Client> clientDic = new Dictionary<string , Client>(); public void Start (string ip, int port ) { IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); try { socket.Bind(ipPoint); isClose = false ; ThreadPool.QueueUserWorkItem(ReceiveMessage); ThreadPool.QueueUserWorkItem(CheckTimeOut); } catch (Exception e) { Console.WriteLine("Udp开启出错" + e.Message); } } private void CheckTimeOut (object ? obj ) { long nowTime = 0 ; List<string > delList = new List<string >(); while (true ) { Thread.Sleep(30000 ); nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond; foreach (Client client in clientDic.Values) { if (nowTime - client.frontTime >= 10 ) delList.Add(client.cliendStrID); } for (int i = 0 ; i < delList.Count; i++) { RemoveClient(delList[i]); } delList.Clear(); } } private void ReceiveMessage (object ? obj ) { if (socket == null ) return ; byte [] bytes = new byte [512 ]; EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0 ); string strID; string ip; int port; while (!isClose) { if (socket.Available > 0 ) { lock (socket) { socket.ReceiveFrom(bytes, ref ipPoint); } ip = ((IPEndPoint)ipPoint).Address.ToString(); port = ((IPEndPoint)ipPoint).Port; strID = ip + ":" + port; if (clientDic.ContainsKey(strID)) clientDic[strID].ReceiveMsg(bytes); else { clientDic.Add(strID, new Client(ip, port)); clientDic[strID].ReceiveMsg(bytes); } } } } public void SendTo (BaseMessage message, IPEndPoint ipPoint ) { if (socket == null ) return ; try { lock (socket) socket.SendTo(message.Writeing(), ipPoint); } catch (SocketException e) { Console.WriteLine("发消息出现问题:" + e.SocketErrorCode + e.Message); } catch (Exception e) { Console.WriteLine("发送消息出问题(可能是序列化问题):" + e.Message); } } public void Broadcast (BaseMessage message ) { foreach (Client client in clientDic.Values) { SendTo(message, client.clientIPAndPort); } } public void Close () { if (socket != null ) { if (socket.Connected) socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null ; } } public void RemoveClient (string clientID ) { if (clientDic.ContainsKey(clientID)) { Console.WriteLine($"客户端{clientDic[clientID].clientIPAndPort} 被移除了" ); clientDic.Remove(clientID); } } } }
Client.cs
它是用于记录和服务器通信过的客户端的IP和端口
IPEndPoint
:通信过的客户端的IP和端口
cliendStrID
:由客户端的IP和端口组成的ID,也是在ServerSocket
的clientDic
内相对应的键
frontTime
:上一次收到消息的时间
ReceiveMsg
:从线程池中开启处理消息的线程,并记录这次发消息的时间
ReceiveHandle
:处理消息线程的主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 using System.Net;namespace TeachUdpServerExercises { internal class Client { public IPEndPoint clientIPAndPort; public string cliendStrID; public long frontTime = -1 ; public Client (string ip, int port ) { this .cliendStrID = 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 { if (obj is not byte [] bytes) return ; int nowIndex = 0 ; 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(); Program.socket?.RemoveClient(cliendStrID); break ; } } catch (Exception e) { Console.WriteLine("处理消息时出错!:" + e.Message); Program.socket?.RemoveClient(cliendStrID); } } } }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System.Net;using System.Net.Sockets;using System.Text;using UnityEngine;public class Lesson14 : MonoBehaviour { void Start () { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8080 ); socket.Bind(ipPoint); IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8081 ); socket.SendTo(Encoding.UTF8.GetBytes("唐老狮来了" ), remoteIpPoint); byte [] bytes = new byte [512 ]; EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0 ); int receiveNum = socket.ReceiveFrom(bytes, ref remoteIpPoint2); print("IP: " + (remoteIpPoint2 as IPEndPoint).Address.ToString() + "port: " + (remoteIpPoint2 as IPEndPoint)?.Port.ToString() + "发来了: " + Encoding.UTF8.GetString(bytes, 0 , receiveNum)); socket.Shutdown(SocketShutdown.Both); socket.Close(); } }
实现一个相对完善的UDP客户端
以上的代码一次只能发送和接受一条消息,随后服务端的运行就会终止
我们可以通过开启一个新线程并使用循环来反复监听消息接收,同时做到不卡主线程,
接收到消息就验证该消息是否是服务端发来的,如果不是就不处理这条消息,同时还要做到处理消息类型
sendMessageQueue
:用于发送消息的队列,公共容器,主线程往sendMessageQueue
放,发送线程从sendMessageQueue
取
receiveMessageQueue
:用于接收消息的队列,公共容器,接收线程往receiveMessageQueue
放,主线程从receiveMessageQueue
取
isClose
:用于控制多线程循环是否结束,让多线程在socket存在时才继续执行
Update
:每帧都检测receiveMessageQueue
内是否存在消息,若存在,就将消息输出到控制台和屏幕上
StartClient
:初始化客户端Socket,绑定本机IP和一个端口号,开始发送和接收消息的线程
SendMsg
:发送消息线程主函数,循环检测sendMessageQueue
内是否有要发送的消息,有就转换为字节数组发送到服务端
ReceiveMsg
:接收消息线程主函数,循环检测是否有消息需要接收,有就接收,验证是否是服务端的消息,将消息传入到receiveMessageQueue
内
Send
:将外部要发送的消息传入到sendMessageQueue
内,等待发送消息线程将消息转换为字节数组发送到服务端
Close
:发送一条断联消息,然后关闭并释放客户端Socket,在管理器被销毁时会调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 using System;using System.Collections;using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Threading;using UnityEngine;public class UdpNetMgr : MonoBehaviour { private static UdpNetMgr instance; public static UdpNetMgr Instance => instance; private EndPoint serverIpPoint; private Socket socket; private bool isClose = true ; private Queue<BaseMessage> sendQueue = new Queue<BaseMessage>(); private Queue<BaseMessage> receiveQueue = new Queue<BaseMessage>(); private byte [] cacheBytes = new byte [512 ]; private void Awake () { instance = this ; DontDestroyOnLoad(gameObject); } private void Update () { if (receiveQueue.Count > 0 ) { BaseMessage baseMsg = receiveQueue.Dequeue(); switch (baseMsg) { case PlayerMessage msg: print(msg.playerID); print(msg.playerData.name); print(msg.playerData.atk); print(msg.playerData.lev); break ; } } } private void OnDestroy () { Close(); } public void StartClient (string ip, int port ) { if (!isClose) return ; serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port); IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1" ), 8082 ); try { socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.Bind(clientIpPort); isClose = false ; print("客户端网络启动" ); ThreadPool.QueueUserWorkItem(ReceiveMsg); ThreadPool.QueueUserWorkItem(SendMsg); } catch (System.Exception e) { print("启动Socket出问题:" + e.Message); } } private void ReceiveMsg (object obj ) { EndPoint tempIpPoint = new IPEndPoint(IPAddress.Any, 0 ); int nowIndex; int msgID; int msgLength; while (!isClose) { if (socket != null && socket.Available > 0 ) { try { socket.ReceiveFrom(cacheBytes, ref tempIpPoint); if (!tempIpPoint.Equals(serverIpPoint)) continue ; nowIndex = 0 ; msgID = BitConverter.ToInt32(cacheBytes, nowIndex); nowIndex += 4 ; msgLength = BitConverter.ToInt32(cacheBytes, nowIndex); nowIndex += 4 ; BaseMessage msg = null ; switch (msgID) { case 1001 : msg = new PlayerMessage(); msg.Reading(cacheBytes, nowIndex); break ; } if (msg != null ) receiveQueue.Enqueue(msg); } catch (SocketException e) { print("接收消息出错:" + e.SocketErrorCode + e.Message); } catch (System.Exception e) { print("接收消息出错(非网络原因):" + e.Message); } } } } private void SendMsg (object obj ) { while (!isClose) { lock (sendQueue) { if (socket != null && sendQueue.Count > 0 ) { try { socket.SendTo(sendQueue.Dequeue().Writeing(), serverIpPoint); } catch (SocketException e) { print("发送消息出错:" + e.SocketErrorCode + e.Message); } } } } } public void Send (BaseMessage msg ) { lock (sendQueue) { sendQueue.Enqueue(msg); } } public void Close () { if (socket != null ) { isClose = true ; QuitMessage msg = new QuitMessage(); socket.SendTo(msg.Writeing(), serverIpPoint); socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null ; } } }