当前位置: 首页 > ds >正文

Unity 客户端和服务器端 基于网络的账户管理系统

先明确一个概念,网络通信中的长连接短连接

长连接是指在客户端和服务器之间建立的连接在数据传输完成后仍然保持打开状态。这种连接方式适用于需要频繁进行数据交换的场景,如在线游戏、实时通信等。长连接的优点是可以减少连接建立和断开的开销,提高通信效率。然而,长连接会占用更多的系统资源,并且在连接中断时需要额外的机制来恢复连接。

短连接是指在客户端和服务器之间建立的连接在一次数据传输完成后就断开。这种连接方式适用于数据交换不频繁的场景,如HTTP请求、文件下载等。短连接的优点是资源占用较少,并且在连接中断时不需要额外的恢复机制。然而,频繁的连接建立和断开会导致较高的开销,影响通信效率。

在Unity实际开发中选择使用长连接还是短连接,需要根据具体的应用场景来决定。如果应用需要频繁的数据交换和实时性要求较高,长连接是更好的选择。如果数据交换不频繁且对实时性要求不高,短连接则更为合适。

整体架构

基于TCP/UDP实现局域网服务器端和客户端通信。
整个系统分为客户端和服务器端两部分:

客户端:具备发现服务器的功能,连接、重连和断开连接,负责用户登录、推送得分等操作,具备心跳检测。
服务器端:具备广播功能,给局域网所有客户端进行广播,负责管理账户信息,处理客户端的登录、得分更新等请求。

广播功能

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;// 网络发现类 - 用于服务端广播和客户端发现
public class NetworkDiscovery
{//指定广播使用的端口号private const int BROADCAST_PORT = 54321;//服务端广播的消息内容private const string BROADCAST_MESSAGE = "UnityAccountServer";//用于服务端广播的 UdpClient 对象private UdpClient serverUdpClient;//用于客户端接收广播的 UdpClient 对象private UdpClient clientUdpClient;//服务端广播消息的线程private Thread broadcastThread;//客户端接收广播消息的线程private Thread receiveThread;//用于控制线程的运行状态。private bool isRunning;// 服务端IP地址变更事件public event Action<string> OnServerIPFound;#region Server// 启动服务端广播public void StartServerBroadcast(){try{isRunning = true;//创建 UdpClient 对象并启用广播功能serverUdpClient = new UdpClient();serverUdpClient.EnableBroadcast = true;broadcastThread = new Thread(BroadcastMessage);broadcastThread.Start();Debug.Log("服务端广播已启动");}catch (Exception e){Debug.LogError("启动服务端广播时出错: " + e.Message);}}// 停止服务端广播public void StopServerBroadcast(){isRunning = false;broadcastThread?.Abort();serverUdpClient?.Close();Debug.Log("服务端广播已停止");}// 发送广播消息private void BroadcastMessage(){//创建一个广播端点 ,使用 IPAddress.Broadcast 表示广播地址,端口为 BROADCAST_PORT。var broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, BROADCAST_PORT);//广播消息转换为字节数组var messageBytes = Encoding.UTF8.GetBytes(BROADCAST_MESSAGE);while (isRunning){try{serverUdpClient.Send(messageBytes, messageBytes.Length, broadcastEndPoint);Thread.Sleep(1000); // 每秒发送一次广播}catch (Exception e){if (isRunning)Debug.LogError("发送广播消息时出错: " + e.Message);}}}#endregion#region Client// 启动客户端发现public void StartClientDiscovery(){try{isRunning = true;//创建 UdpClient 对象并绑定到 BROADCAST_PORT 端口。clientUdpClient = new UdpClient(BROADCAST_PORT);// 确保主线程调度器已初始化UnityMainThreadDispatcher.EnsureInstance();receiveThread = new Thread(ReceiveBroadcast);receiveThread.Start();Debug.Log("客户端发现已启动");}catch (Exception e){Debug.LogError("启动客户端发现时出错: " + e.Message);}}// 停止客户端发现public void StopClientDiscovery(){isRunning = false;receiveThread?.Abort();clientUdpClient?.Close();Debug.Log("客户端发现已停止");}// 接收广播消息private void ReceiveBroadcast(){//创建一个 IPEndPoint 对象 anyIP,使用 IPAddress.Any 表示接收任意 IP 地址的广播消息,端口为 BROADCAST_PORTvar anyIP = new IPEndPoint(IPAddress.Any, BROADCAST_PORT);while (isRunning){try{if (clientUdpClient.Available > 0){var data = clientUdpClient.Receive(ref anyIP);var message = Encoding.UTF8.GetString(data);if (message == BROADCAST_MESSAGE){// 找到服务端,触发事件OnServerIPFound?.Invoke(anyIP.Address.ToString());}}else{Thread.Sleep(100);}}catch (Exception e){if (isRunning)Debug.LogError("接收广播消息时出错: " + e.Message);}}}#endregion
}

提供一种在非主线程中调度代码在主线程上执行的机制。通过将需要在主线程上执行的操作添加到队列中,并在 Update 方法中依次执行这些操作,确保了线程安全和操作的正确执行。这在处理网络请求、异步操作等场景中非常有用,因为许多 Unity 的 API 必须在主线程上调用。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;public class UnityMainThreadDispatcher : MonoBehaviour
{private static UnityMainThreadDispatcher instance = null;private readonly Queue<Action> executionQueue = new Queue<Action>();public static void EnsureInstance(){if (instance == null){if (Thread.CurrentThread.ManagedThreadId == 1){CreateInstance();}else{Debug.LogError("UnityMainThreadDispatcher 必须在主线程中初始化!");}}}private static void CreateInstance(){GameObject obj = new GameObject("UnityMainThreadDispatcher");instance = obj.AddComponent<UnityMainThreadDispatcher>();DontDestroyOnLoad(obj);}public static UnityMainThreadDispatcher Instance(){if (instance == null){CreateInstance();}return instance;}private void Awake(){if (instance == null){instance = this;DontDestroyOnLoad(this.gameObject);}else{Destroy(gameObject);}}private void Update(){lock (executionQueue){while (executionQueue.Count > 0){executionQueue.Dequeue().Invoke();}}}public void Enqueue(Action action){if (action == null){throw new ArgumentNullException("action");}lock (executionQueue){executionQueue.Enqueue(action);}}public IEnumerator Enqueue(IEnumerator action){if (action == null){throw new ArgumentNullException("action");}Enqueue(() =>{StartCoroutine(action);});return action;}
}

心跳包

长连接维持的重要手段-心跳包,通常用于检测连接状态、保持连接活跃以及进行断线重连处理。

定义心跳包协议
心跳包通常是一个简单的数据包,可以仅包含一个标识符(例如消息编号为1),用于服务器和客户端识别此包为心跳包。在Unity中,可以通过定义一个简单的结构体或类来封装心跳包的数据格式。

发送心跳包
客户端需要每隔一段时间向服务器发送一次心跳包。这个时间间隔通常设置为5-30秒,具体取决于应用的实时性需求。可以使用Unity的协程(Coroutine)来定时发送心跳包。

处理心跳包
在服务器端收到心跳包后,应立即回复一个确认消息,表示连接仍然有效。客户端在收到确认消息后,可以重置计时器,避免误判断线。

断线检测与重连机制
客户端在发送心跳包后,如果在一定时间内没有收到服务器的确认响应,则可以认为连接已经断开。此时可以触发重连机制,尝试重新建立连接。断线判定时间通常设置为30-40秒,若对实时性要求较高,可以缩短至6-9秒。

防止重复发送
在某些情况下,玩家可能会在短时间内多次触发某些操作(例如点击按钮)。为了避免因频繁发送心跳包而导致的网络拥堵,可以在网络协议中加入限制机制,确保某些协议(如心跳包)不会在短时间内重复发送。例如,可以在发送心跳包之前检查上一次发送的时间,确保两次发送之间的时间间隔大于预设值。

服务器主类和信息数据接口


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;//账户数据结构
[Serializable]
public class AccountData
{public string username;public string password;public string score;public AccountData(string username, string password){this.username = username;this.password = password;this.score = 0.ToString();}
}//服务器主类
public class AccountServer
{private TcpListener server;private Thread serverThread;private bool isRunning;private object accountsLock;public Dictionary<string, AccountData> accounts = new Dictionary<string, AccountData>();private string dataFilePath = "data.dat";private const string HeartbeatMessage = "HEARTBEAT";private const string HeartbeatResponse = "ACK";private const int HeartbeatTimeoutSeconds = 10; // 心跳超时时间,单位:秒//初始化public void Start(int port){dataFilePath = Application.persistentDataPath + "/" + "data.dat";try{// 加载已保存的账户LoadAccounts();server = new TcpListener(IPAddress.Any, port);server.Start();isRunning = true;serverThread = new Thread(ListenForClients);serverThread.Start();Debug.Log("服务器已启动,等待客户端连接...");}catch (Exception e){Debug.LogError("启动服务器时出错: " + e.Message);}}//停止服务器public void Stop(){isRunning = false;serverThread?.Abort();server?.Stop();SaveAccounts();Debug.Log("服务器已停止");}//监听客户端连接private void ListenForClients(){while (isRunning){try{TcpClient client = server.AcceptTcpClient();Thread clientThread = new Thread(() => HandleClient(client));clientThread.Start();Debug.Log("接受客户端连接");}catch (Exception e){if (isRunning)Debug.LogError("接受客户端连接时出错: " + e.Message);}}}//处理客户端请求private void HandleClient(TcpClient client){using (NetworkStream stream = client.GetStream())using (BinaryReader reader = new BinaryReader(stream))using (BinaryWriter writer = new BinaryWriter(stream)){try{DateTime lastHeartbeatTime = DateTime.Now;while (client.Connected){// 检查心跳超时if ((DateTime.Now - lastHeartbeatTime).TotalSeconds > HeartbeatTimeoutSeconds){Debug.Log("心跳超时,断开客户端连接");break;}if (!stream.DataAvailable)continue;string command = reader.ReadString();Debug.Log($"收到命令: {command}");switch (command){case "LOGIN"://登录HandleLogin(reader, writer);break;case "UPDATE_SCORE"://更新得分HandleUpdateScore(reader, writer);break;case "EXIT":return;case "HEARTBEAT"://心跳包HandleHeartbeat(writer);lastHeartbeatTime = DateTime.Now; // 重置心跳时间break;}}}catch (Exception e){Debug.LogWarning($"客户端断开连接: {e.Message}");Debug.LogError($"处理客户端连接时出错: {e.StackTrace}");}}}//处理登录请求private void HandleLogin(BinaryReader reader, BinaryWriter writer){string username = reader.ReadString();string password = reader.ReadString();Debug.Log($"收到登录请求,用户名: {username},密码: {password}");bool success = false;string message = "";lock (accountsLock)if (!accounts.ContainsKey(username)){message = "账户不存在";}else if (accounts[username].password != password){message = "密码错误";}else{success = true;message = "登录成功";}Debug.Log(message);writer.Write(success);writer.Write(message);}//更新得分private void HandleUpdateScore(BinaryReader reader, BinaryWriter writer){string username = reader.ReadString();string score = reader.ReadString();Debug.Log($"收到得分更新请求,用户名: {username},得分: {score}");bool success = false;string message = "";lock (accountsLock)if (accounts.ContainsKey(username)){accounts[username].score = score;SaveAccounts();success = true;message = "得分更新成功";}else{message = "账户不存在";}Debug.Log(message);writer.Write(success);writer.Write(message);}//处理心跳包private void HandleHeartbeat(BinaryWriter writer){writer.Write(HeartbeatResponse);writer.Flush();Debug.Log("发送心跳包响应");}//保存账户private void SaveAccounts(){try{using (FileStream fileStream = new FileStream(dataFilePath, FileMode.Create))using (BinaryWriter writer = new BinaryWriter(fileStream)){writer.Write(accounts.Count);foreach (var account in accounts.Values){writer.Write(account.username);writer.Write(account.password);writer.Write(account.score);}}Debug.Log("账户数据已保存");}catch (Exception e){Debug.LogError("保存账户数据时出错: " + e.Message);}}//加载账户private void LoadAccounts(){try{if (File.Exists(dataFilePath)){using (FileStream fileStream = new FileStream(dataFilePath, FileMode.Open))using (BinaryReader reader = new BinaryReader(fileStream)){int count = reader.ReadInt32();accounts.Clear();for (int i = 0; i < count; i++){string username = reader.ReadString();string password = reader.ReadString();string score = reader.ReadString(); // 加载得分accounts[username] = new AccountData(username, password) { score = score };}}Debug.Log($"已加载 {accounts.Count} 个账户");}else{Debug.Log("账户数据文件不存在,将创建新文件");}}catch (Exception e){Debug.LogError("加载账户数据时出错: " + e.Message);}}//创建账户public string CreateAccount(string username, string password){if (!accounts.ContainsKey(username)){accounts[username] = new AccountData(username, password);SaveAccounts();Debug.Log($"账户 {username} 创建成功");return $"账户 {username} 创建成功";}else{Debug.Log($"账户 {username} 已存在,无法重复创建");return $"账户 {username} 已存在,无法重复创建";}}//删除账户public bool DeleteAccount(string username, string password){if (accounts.ContainsKey(username) && accounts[username].password == password){accounts.Remove(username);SaveAccounts();return true;}return false;}//修改账户密码public bool AmendPassword(string username, string newPassword){if (accounts.ContainsKey(username)){accounts[username].password = newPassword;SaveAccounts();return true;}return false;}//修改账户成绩public bool AmendScore(string username, string newScore){if (accounts.ContainsKey(username)){accounts[username].score = newScore;SaveAccounts();return true;}return false;}//获取所有账户public List<AccountData> GetAllAccounts(){return new List<AccountData>(accounts.Values);}public void ClearAllAccounts(){accounts.Clear();SaveAccounts();Debug.Log("所有账户信息已清空");}
}

客户端主类

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UniRx;//客户端主类
public class AccountClient : MonoBehaviour
{public static AccountClient Instance { get; private set; }private TcpClient client;private NetworkStream stream;private BinaryReader reader;private BinaryWriter writer;public string serverIP = "127.0.0.1";public int serverPort = 8888;public TMP_InputField usernameInput;public TMP_InputField passwordInput;[HideInInspector]public string UserName;[HideInInspector]public string Password;public Text statusText;public Button loginButton;public Button reconnectButton;public Button discoverButton;public Dropdown serverDropdown;private NetworkDiscovery networkDiscovery;private List<string> discoveredServers = new List<string>();[HideInInspector]public bool isLogin;//心跳包相关private const string HeartbeatMessage = "HEARTBEAT";private const string HeartbeatResponse = "ACK";private const float HeartbeatInterval = 5f; // 心跳包发送间隔(秒)private IDisposable heartbeatTimer;private void Start(){Instance = this;loginButton.onClick.AddListener(OnLoginButtonClick);reconnectButton.onClick.AddListener(DiscoverAndReconnect);discoverButton.onClick.AddListener(DiscoverServers);//初始化网络发现networkDiscovery = new NetworkDiscovery();networkDiscovery.OnServerIPFound += OnServerFound;DiscoverServers();Observable.Timer(System.TimeSpan.FromSeconds(5.0f)).Subscribe(_ =>{if (discoveredServers.Count == 0){statusText.text = "未发现服务器,点击此文字重试。";}elseConnectToServer();}).AddTo(this);serverDropdown.ClearOptions();serverDropdown.AddOptions(new List<string> { "未发现服务器" });serverDropdown.onValueChanged.AddListener(OnServerSelected);StartHeartbeat();}//重连public void DiscoverAndReconnect(){DiscoverServers();Observable.Timer(System.TimeSpan.FromSeconds(5.0f)).Subscribe(_ =>{if (discoveredServers.Count == 0){statusText.text = "未发现服务器,点击此文字重试。";}elseConnectToServer();}).AddTo(this);}private void OnDestroy(){Disconnect();networkDiscovery.StopClientDiscovery();StopHeartbeat();}// 发现服务器private void DiscoverServers(){statusText.text = "正在搜索服务器...";discoveredServers.Clear();serverDropdown.ClearOptions();serverDropdown.AddOptions(new List<string> { "正在搜索..." });networkDiscovery.StartClientDiscovery();Invoke("StopDiscovery", 5f);}// 停止发现private void StopDiscovery(){networkDiscovery.StopClientDiscovery();if (discoveredServers.Count == 0){serverDropdown.ClearOptions();serverDropdown.AddOptions(new List<string> { "未发现服务器" });statusText.text = "未发现服务器";}else{statusText.text = $"发现 {discoveredServers.Count} 个服务器";serverIP = discoveredServers[0];}}// 服务器发现回调private void OnServerFound(string serverIP){if (!discoveredServers.Contains(serverIP)){discoveredServers.Add(serverIP);UpdateServerDropdown();}serverIP = discoveredServers[0];}// 更新服务器下拉框private void UpdateServerDropdown(){UnityMainThreadDispatcher.Instance().Enqueue(() =>{serverDropdown.ClearOptions();serverDropdown.AddOptions(discoveredServers);});}// 服务器选择回调private void OnServerSelected(int index){if (index >= 0 && index < discoveredServers.Count){serverIP = discoveredServers[index];statusText.text = $"已选择服务器: {serverIP}";}}// 连接到服务器private void ConnectToServer(){try{Disconnect();client = new TcpClient();client.Connect(serverIP, serverPort);int maxAttempts = 5;int attempt = 0;while (!client.Connected && attempt < maxAttempts){Thread.Sleep(200);attempt++;}if (client.Connected){stream = client.GetStream();reader = new BinaryReader(stream);writer = new BinaryWriter(stream);statusText.text = "已连接到服务器";Debug.Log("已连接到服务器");}else{statusText.text = "点击此文字重试,连接服务器失败";Debug.LogError("连接服务器失败");}}catch (Exception e){statusText.text = "点击此文字重试,连接服务器失败: " + e.Message;Debug.LogError("连接服务器失败: " + e.Message);}}// 修改 Disconnect 方法,确保安全释放资源private void Disconnect(){try{writer?.Close();reader?.Close();stream?.Close();client?.Close();isLogin = false;writer = null;reader = null;stream = null;client = null;if (statusText != null)statusText.text = "已断开连接";Debug.Log("已断开连接");}catch (Exception e){Debug.LogError("断开连接时出错: " + e.Message);}}private void Update(){if (Input.GetKeyDown(KeyCode.Return) && !isLogin){OnLoginButtonClick();}}// 登录按钮点击事件private void OnLoginButtonClick(){string username = usernameInput.text;string password = passwordInput.text;if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)){statusText.text = "用户名和密码不能为空";return;}SendLoginRequest(username, password);}// 发送登录请求private void SendLoginRequest(string username, string password){try{if (client == null || !client.Connected){statusText.text = "未连接到服务器";Debug.LogError("未连接到服务器");return;}if (writer == null){statusText.text = "通信对象未初始化";Debug.LogError("BinaryWriter 对象为空");return;}writer.Write("LOGIN");writer.Write(username);writer.Write(password);writer.Flush();int timeout = 5000; int elapsedTime = 0;while (!stream.DataAvailable && elapsedTime < timeout){Thread.Sleep(100);elapsedTime += 100;}if (stream.DataAvailable){bool success = reader.ReadBoolean();string message = reader.ReadString();statusText.text = message;if (success){Debug.Log("登录成功");Password = password;UserName = username;isLogin = true;//TODO 做一些登录成功后UI的展示之类Disconnect();}else{Debug.Log("登录失败: " + message);}}else{statusText.text = "未收到服务器响应";Debug.LogError("未收到服务器响应");}}catch (Exception e){statusText.text = "通信错误: " + e.Message;Debug.LogError("发送登录请求时出错: " + e.Message);Disconnect();}}// 推送得分public void PushScore(string score){try{int timeout = 5000; int elapsedTime = 0;client = new TcpClient();client.Connect(serverIP, serverPort);int maxAttempts = 5;int attempt = 0;while (!client.Connected && attempt < maxAttempts){Thread.Sleep(200);attempt++;}if (client.Connected){stream = client.GetStream();reader = new BinaryReader(stream);writer = new BinaryWriter(stream);if (statusText != null)statusText.text = "已连接到服务器";Debug.Log("已连接到服务器");writer.Write("UPDATE_SCORE");writer.Write(UserName);writer.Write(score);writer.Flush();while (!stream.DataAvailable && elapsedTime < timeout){Thread.Sleep(100);elapsedTime += 100;}if (stream.DataAvailable){bool success = reader.ReadBoolean();string message = reader.ReadString();if (statusText != null)statusText.text = message;if (success){Debug.Log("得分推送成功");}else{Debug.Log("得分推送失败: " + message);}}else{if (statusText != null)statusText.text = "得分推送超时";Debug.LogError("得分推送超时");}}else{if (statusText != null)statusText.text = "点击此文字重试,连接服务器失败";Debug.LogError("连接服务器失败");}}catch (Exception e){if (statusText != null)statusText.text = "通信错误: " + e.Message;Debug.LogError("发送得分更新请求时出错: " + e.Message);}}// 启动心跳包定时器private void StartHeartbeat(){heartbeatTimer = Observable.Interval(TimeSpan.FromSeconds(HeartbeatInterval)).Subscribe(_ =>{if (client != null && client.Connected){try{writer.Write(HeartbeatMessage);writer.Flush();int timeout = 2000;int elapsedTime = 0;while (!stream.DataAvailable && elapsedTime < timeout){Thread.Sleep(100);elapsedTime += 100;}if (stream.DataAvailable){string response = reader.ReadString();if (response != HeartbeatResponse){Debug.LogError("心跳包响应错误: " + response);}}else{Debug.LogError("未收到心跳包响应");Disconnect();}}catch (Exception e){Debug.LogError("发送心跳包时出错: " + e.Message);Disconnect();}}}).AddTo(this);}// 停止心跳包定时器private void StopHeartbeat(){heartbeatTimer?.Dispose();}
}
http://www.xdnf.cn/news/19067.html

相关文章:

  • 除自身以外数组的乘积是什么意思
  • 【OpenGL】LearnOpenGL学习笔记16 - 帧缓冲(FBO)、渲染缓冲(RBO)
  • 【JUC】——线程池
  • 点评项目(Redis中间件)第一部分Redis基础
  • docker run 后报错/bin/bash: /bin/bash: cannot execute binary file总结
  • 边缘计算:一场由物理定律发起的“计算革命”
  • 预测模型及超参数:2.传统机器学习:PLS及其改进
  • HarmonyOS 高效数据存储全攻略:从本地优化到分布式实战
  • 从 GRIT 到 WebUI:Chromium 内置资源加载与前端展示的完整链路解析
  • AI Agent 发展趋势与架构演进
  • 稳敏双态融合架构--架构师的练就
  • 【MES】工业4.0智能制造数字化工厂(数字车间、MES、ERP)解决方案:智能工厂体系架构、系统集成以及智能设计、生产、管理、仓储物流等
  • uvloop深度实践:从原理到高性能异步应用实战
  • http请求能支持多大内容的请求
  • 通义万相音频驱动视频模型Wan2.2-S2V重磅开源
  • 安卓接入通义千问AI的实现记录
  • 欧盟《人工智能法案》生效一年主要实施进展概览(二)
  • React 组件命名规范:为什么必须大写首字母蛊傲
  • 【Datawhale之Happy-LLM】Encoder-only模型篇 task05精华~
  • 计算神经科学数学建模编程深度前沿方向研究(下)
  • 医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(一)
  • 卷积神经网络CNN
  • Xposed框架实战指南:从原理到你的第一个模块
  • 面试之JVM
  • Java并发编程深度解析:从互斥锁到StampedLock的高性能锁优化之路
  • 计算机视觉:从 “看见” 到 “理解”,解锁机器感知世界的密码
  • 嵌入式(day34) http协议
  • 快速了解卷积神经网络
  • 【软考论文】论DevOps及其应用
  • C#由Dictionary不正确释放造成的内存泄漏问题与GC代系