• C#实现本地服务器多客户端同频道通信


    (一)需求

         在游戏中我们经常能够看到玩家在世界频道聊天,在QQ或微信中也有群聊功能。抽象成计算机网络,就是多个客户端通过服务器进行同频道通信,所有连接的客户端都可以看到其他客户端发送的消息。这种多客户端同频道通信是如何实现的呢?在本篇文章我们就来探讨一下。

    (二)解决思路

           这个需求的重点部分在于网络通信,需要我们掌握基本的计算机网络通信知识,具体到每种编程语言又有对应的API。如果把这个需求抽象到计算机网络中,我们就可以理解成多个客户端向服务器发送信息,服务器接收信息后又把信息发送给所有连接的客户端。这样,在各个客户端就可以接收到其他客户端发送的信息了。

    (三)设计思路

           服务器基于本地服务器开发,通过一个单独的C#控制台项目模拟,编程语言使用C#,客户端通过Unity3D构建GUI并编写客户端脚本。多客户端则通过打开多个Unity3D项目的可执行文件进行模拟,客户端的GUI需要有调试面板、客户端名称下拉菜单、连接和断开连接按钮、消息显示面板、消息输入框和消息发送按钮等。

    (四)代码实现

            由于代码中引用了自定义的网络通信共享库NetShare,关于NetShare请阅读这篇文章


           客户端

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Net;
    4. using System.Net.Sockets;
    5. using System.Text;
    6. using UnityEngine;
    7. using UnityEngine.UI;
    8. using NetShare;//自定义网络通信共享库,包括通用数据包DataPacket等
    9. using System.Threading;
    10. //世界频道客户端
    11. public class WorldChannelClient : MonoBehaviour
    12. {
    13. public Text BaseInfo;//显示Socket连接基本信息的文本
    14. public Text EchoContents;//Socket回显信息的文本
    15. public Text ChatContents;//聊天信息的文本
    16. public Dropdown ClientMenu;//客户端名称下拉菜单
    17. public Button Connect;//连接按钮
    18. public Button DisConnect;//断开连接按钮
    19. public InputField SendInput;//聊天消息输入框
    20. public Button Send;//聊天信息发送按钮
    21. static string ipAddressStr;//IP地址字符串
    22. static int port;//端口
    23. static IPAddress iPAddress;//IP地址对象
    24. static IPEndPoint iPEndPoint;//IP端点对象
    25. string clientName;//客户端名称
    26. Socket currentClientSocket;//当前客户端Socket
    27. bool isLockSend;//是否锁定聊天信息发送按钮
    28. byte[] buffer;//消息接收缓冲区
    29. Queue<string> echoContentQueue, chatContentQueue;//回显信息队列和聊天信息队列
    30. DataPacket dataPacket;//通用数据包
    31. //反映Socket是否与服务器有效连接的属性
    32. bool isConnected
    33. {
    34. get
    35. {
    36. if (currentClientSocket == null) return false;
    37. return !currentClientSocket.Poll(10, SelectMode.SelectRead) && currentClientSocket.Connected;
    38. }
    39. }
    40. void Start()
    41. {
    42. //初始化
    43. ipAddressStr = "8.137.8.206";
    44. clientName = ClientMenu.options.Count > 0 ? ClientMenu.options[0].text : "";
    45. port = 5500;
    46. iPAddress = IPAddress.Parse(ipAddressStr);
    47. iPEndPoint = new IPEndPoint(iPAddress, port);
    48. buffer = new byte[1024];
    49. echoContentQueue = new Queue<string>();
    50. chatContentQueue = new Queue<string>();
    51. //为UI控件添加监听事件
    52. ClientMenu.onValueChanged.AddListener((index) =>
    53. {
    54. clientName = ClientMenu.options[index].text;
    55. });
    56. Connect.onClick.AddListener(() =>
    57. {
    58. Thread thread = new Thread(new ThreadStart(ConnectDeal));
    59. thread.Start();
    60. });
    61. DisConnect.onClick.AddListener(() =>
    62. {
    63. Thread thread = new Thread(new ThreadStart(DisConnectDeal));
    64. thread.Start();
    65. });
    66. Send.onClick.AddListener(() =>
    67. {
    68. Thread thread = new Thread(new ThreadStart(SendDeal));
    69. thread.Start();
    70. });
    71. }
    72. void Update()
    73. {
    74. //不断更新Socket基本信息
    75. BaseInfo.text = $"ClientName:{clientName}" +
    76. string.Format("\nSocketHashCode:{0}", currentClientSocket == null ? "None" : currentClientSocket.GetHashCode().ToString()) +
    77. $"\nisLock:{isLockSend}" +
    78. string.Format("\nPoll:{0}", currentClientSocket == null ? "None" : (!currentClientSocket.Poll(10, SelectMode.SelectRead)).ToString()) +
    79. string.Format("\nIsConnected:{0}", currentClientSocket == null ? "False" : currentClientSocket.Connected.ToString());
    80. //更新回显信息
    81. if (echoContentQueue.Count > 0)
    82. {
    83. while (echoContentQueue.Count > 0)
    84. {
    85. SetEchoContents(echoContentQueue.Dequeue());
    86. }
    87. }
    88. //更新聊天信息
    89. if (chatContentQueue.Count > 0)
    90. {
    91. while (chatContentQueue.Count > 0)
    92. {
    93. SetChatContents(chatContentQueue.Dequeue());
    94. }
    95. }
    96. }
    97. //设置回显信息相关UI的内容
    98. void SetEchoContents(string text)
    99. {
    100. EchoContents.text += text;
    101. }
    102. //设置聊天信息相关UI的内容
    103. void SetChatContents(string text)
    104. {
    105. ChatContents.text += text;
    106. }
    107. //执行逻辑:Socket异步连接处理
    108. void ConnectDeal()
    109. {
    110. echoContentQueue.Enqueue($"\n客户端{clientName}正在请求服务器连接...");
    111. Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    112. clientSocket.BeginConnect(iPEndPoint, ConnectCallback, clientSocket);
    113. }
    114. //执行逻辑:Socket异步断开连接处理
    115. void DisConnectDeal()
    116. {
    117. echoContentQueue.Enqueue($"\n客户端{clientName}正在断开与服务器的连接...");
    118. if (isConnected)
    119. {
    120. currentClientSocket.Shutdown(SocketShutdown.Both);//关闭Socket的发送和接收消息功能
    121. currentClientSocket.BeginDisconnect(false, DisConnectCallback, currentClientSocket);
    122. }
    123. else echoContentQueue.Enqueue($"\n客户端{clientName}未与服务器建立连接,无法进行断开连接的操作...");
    124. }
    125. //执行逻辑:Socket异步接收信息处理
    126. void ReceiveDeal()
    127. {
    128. echoContentQueue.Enqueue($"\n客户端{clientName}开始监听服务器响应...");
    129. currentClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, currentClientSocket);
    130. }
    131. //执行逻辑:Socket异步发送信息处理
    132. void SendDeal()
    133. {
    134. if (!isLockSend && !string.IsNullOrEmpty(SendInput.text))
    135. {
    136. dataPacket.mContent = SendInput.text;
    137. SendInput.text = string.Empty;
    138. byte[] bytes = dataPacket.ToBytes();
    139. currentClientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, currentClientSocket);
    140. }
    141. }
    142. //执行逻辑:Socket异步连接处理回调
    143. void ConnectCallback(IAsyncResult ar)
    144. {
    145. try
    146. {
    147. Socket socket = ar.AsyncState as Socket;
    148. socket.EndConnect(ar);
    149. currentClientSocket = socket;
    150. if (isConnected)
    151. {
    152. dataPacket = new ClientDataPacket()
    153. {
    154. mLocalEndPointStr = socket.LocalEndPoint.ToString(),
    155. mClientName = clientName
    156. };
    157. isLockSend = false;
    158. echoContentQueue.Enqueue($"\n客户端{clientName}与服务器连接成功!");
    159. ReceiveDeal();
    160. }
    161. else echoContentQueue.Enqueue($"\n客户端{clientName}与服务器连接失败!");
    162. }
    163. catch (SocketException se)
    164. {
    165. echoContentQueue.Enqueue($"\n客户端{clientName}与服务器连接失败!\n错误信息:{se.Message}");
    166. }
    167. }
    168. //执行逻辑:Socket异步断开连接处理回调
    169. void DisConnectCallback(IAsyncResult ar)
    170. {
    171. try
    172. {
    173. isLockSend = true;
    174. Socket socket = ar.AsyncState as Socket;
    175. socket.EndDisconnect(ar);
    176. dataPacket = null;
    177. echoContentQueue.Enqueue($"\n客户端{clientName}与服务器断开连接操作成功!");
    178. }
    179. catch (SocketException se)
    180. {
    181. echoContentQueue.Enqueue($"\n客户端{clientName}与服务器断开连接操作失败!\n错误信息:{se.Message}");
    182. }
    183. }
    184. //执行逻辑:Socket异步发送信息处理回调
    185. void SendCallback(IAsyncResult ar)
    186. {
    187. try
    188. {
    189. Socket socket = ar.AsyncState as Socket;
    190. socket.EndSend(ar);
    191. echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送了一条消息!");
    192. }
    193. catch (SocketException se)
    194. {
    195. echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送信息操作失败!\n错误信息:{se.Message}");
    196. }
    197. }
    198. //执行逻辑:Socket异步接收信息处理回调
    199. void ReceiveCallback(IAsyncResult ar)
    200. {
    201. try
    202. {
    203. Socket socket = ar.AsyncState as Socket;
    204. int count = socket.EndReceive(ar);
    205. string res = Encoding.UTF8.GetString(buffer, 0, count);
    206. if (!string.IsNullOrEmpty(res)) chatContentQueue.Enqueue("\n" + res);
    207. //若Socket连接有效则继续接收消息
    208. if (isConnected)
    209. socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, socket);
    210. }
    211. catch (SocketException se)
    212. {
    213. echoContentQueue.Enqueue($"\n客户端{clientName}接收服务器消息失败!\n错误信息:{se.Message}");
    214. }
    215. }
    216. }

            服务器

    1. using System.Net.Sockets;
    2. using System.Net;
    3. using System.Text;
    4. using NetShare;//自定义网络通信共享库,其中包括了通用数据包DataPacket、客户端数据包ClientDataPacket等
    5. namespace UnityServer
    6. {
    7. //世界频道服务器
    8. internal class WorldChannelServer
    9. {
    10. private static string ipAddressStr = "127.0.0.1";//IP地址字符串
    11. private static int port = 5500;//端口
    12. private static int maxConnectCount = 20;//最大连接数
    13. private static byte[] buffer = new byte[1024];//消息缓冲区
    14. //客户端Socket合集,key为IPEndPoint字符串,value为服务器为客户端分配的Socket
    15. private static Dictionary<string, Socket> clients = new Dictionary<string, Socket>();
    16. private static Socket? serverSocket;//服务器Socket
    17. private static void Main(string[] args)
    18. {
    19. Thread thread = new Thread(new ThreadStart(ServerDeal));
    20. thread.Start();
    21. Console.ReadLine();
    22. }
    23. //判断Socket是否进行有效连接
    24. private static bool IsConnected(Socket socket)
    25. {
    26. if (socket == null) return false;
    27. return !socket.Poll(10, SelectMode.SelectRead) && socket.Connected;
    28. }
    29. //执行逻辑:服务器处理
    30. private static void ServerDeal()
    31. {
    32. serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    33. IPAddress v_ipAddress = IPAddress.Parse(ipAddressStr);
    34. serverSocket.Bind(new IPEndPoint(v_ipAddress, port));
    35. serverSocket.Listen(maxConnectCount);
    36. Console.WriteLine($"开启服务器[{serverSocket.LocalEndPoint}]...");
    37. serverSocket.BeginAccept(AcceptCallback, null);
    38. }
    39. //执行逻辑:Socket异步接收消息
    40. private static void ReceiveDeal(object? clientSocket)
    41. {
    42. Console.WriteLine("********************");
    43. if (clientSocket == null) return;
    44. Socket? v_clientSocket = clientSocket as Socket;
    45. if (v_clientSocket == null) return;
    46. Console.WriteLine("接收到客户端的连接请求!");
    47. if (IsConnected(v_clientSocket))
    48. v_clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, v_clientSocket);
    49. }
    50. //添加客户端Socket到客户端Socket合集
    51. private static void AddClient(Socket clientSocket)
    52. {
    53. if (clientSocket == null) return;
    54. EndPoint? endPoint = clientSocket.RemoteEndPoint;
    55. if (endPoint != null)
    56. {
    57. string? v_endPointStr = endPoint.ToString();
    58. if (v_endPointStr != null) clients[v_endPointStr] = clientSocket;
    59. }
    60. }
    61. //向所有客户端发送指定信息
    62. private static void SendToAll(string? content)
    63. {
    64. if (string.IsNullOrEmpty(content)) return;
    65. byte[] bytes = Encoding.UTF8.GetBytes(content);
    66. foreach (Socket clientSocket in clients.Values)
    67. {
    68. if (IsConnected(clientSocket))
    69. {
    70. Thread thread = new Thread(() =>
    71. {
    72. clientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, clientSocket);
    73. });
    74. thread.Start();
    75. }
    76. }
    77. }
    78. //Socket监听请求回调
    79. private static void AcceptCallback(IAsyncResult ar)
    80. {
    81. try
    82. {
    83. if (serverSocket != null)
    84. {
    85. Socket clientSocket = serverSocket.EndAccept(ar);
    86. AddClient(clientSocket);
    87. Thread thread = new Thread(new ParameterizedThreadStart(ReceiveDeal));
    88. thread.Start(clientSocket);
    89. serverSocket.BeginAccept(AcceptCallback, null);
    90. }
    91. }
    92. catch (SocketException se)
    93. {
    94. Console.WriteLine("AcceptException:" + se.Message);
    95. }
    96. }
    97. //Socket发送信息回调
    98. private static void SendCallback(IAsyncResult ar)
    99. {
    100. try
    101. {
    102. Socket? clientSocket = ar.AsyncState as Socket;
    103. if (clientSocket != null) clientSocket.EndSend(ar);
    104. }
    105. catch (SocketException se)
    106. {
    107. Console.WriteLine("SendException:" + se.Message);
    108. }
    109. }
    110. //Socket接收信息回调
    111. private static void ReceiveCallback(IAsyncResult ar)
    112. {
    113. try
    114. {
    115. Socket? clientSocket = ar.AsyncState as Socket;
    116. if (clientSocket != null)
    117. {
    118. int bytesCount = clientSocket.EndReceive(ar);
    119. ClientDataPacket? dataPacket = DataPacket.ToObject(buffer.Take(bytesCount).ToArray());
    120. if (dataPacket != null)
    121. {
    122. string v_content = $"客户端{dataPacket.mClientName}{dataPacket.mContent}";
    123. SendToAll(v_content);
    124. }
    125. if (IsConnected(clientSocket))
    126. clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, clientSocket);
    127. }
    128. }
    129. catch (SocketException se)
    130. {
    131. Console.WriteLine("ReceiveException:" + se.Message);
    132. }
    133. }
    134. }
    135. }

    (五)测试

           测试流程大概是先启动服务器,然后启动三个客户端,三个客户端分别以A、B、C的名称作为客户端名称与服务器建立连接,连接后再由客户端A、B、C分别向服务器发送信息,通过观察三个客户端的消息面板来确定测试结果,具体测试流程请观看下列视频:

    本地服务器多客户端通信

    (六)总结

           在服务器端,我们通过一个C#控制台项目来模拟服务器后台,服务器与客户端具有类似的功能,同样具有发送、接收消息的功能,不同的是服务器具有监听客户端连接的功能,而客户端具有向服务器发送连接请求的功能,本质上这些都是通过Socket实现的功能,人为划分成服务器端和客户端。在客户端我们通过GUI将用户的操作进行可视化构建,实现了回显、客户端名称选择、连接、断开连接、发送和显示消息等基本交互。

           为了模拟多客户端并发操作,所有功能我们都采用了异步的方式启动,对于真正的网络通信而言,这对我们来说才刚刚开始,不过通过这个案例也让我们了解了基本的网络通信流程。

    如果这篇文章对你有帮助,请给作者点个赞吧!

  • 相关阅读:
    【C++类型转换】4种类型转换:static_cast、reinterpret_cast、const_cast、dynamic_cast
    LeetCode刷题笔记【35】:动态规划专题-7(爬楼梯、零钱兑换、完全平方数)
    C语言典范编程
    HK32_UID中获取芯片唯一ID相同的问题
    使用任务定时执行软件的定时关机功能,控制电脑可用时间段
    06-集合
    今年跳槽成功测试工程师原来是掌握了这3个“潜规则”
    OpenCV(四十):图像分割—漫水填充
    Window10 安装 Lua
    Spring实战之Bean的主要装配机制之一-组件扫描、自动装配bean
  • 原文地址:https://blog.csdn.net/hgf1037882434/article/details/134539702