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

            服务器

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

    (五)测试

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

    本地服务器客户端单独通信

    (六)总结

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

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

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

  • 相关阅读:
    红黑树的由来及其底层原理
    Spark的部署与使用
    NuGet私有服务器ProGet Docker搭建和公司中实战用法
    算法9-动态规划
    P5194 [USACO05DEC]Scales S
    操作系统 day11(进程调度时机、切换、调度方式)
    赛力斯上半年营收124亿亏17亿:与华为深度捆绑 已推两款车型
    wordpress建网站主题案例推荐
    奇异矩阵、非奇异矩阵
    springboot毕设项目4S店车辆管理系统4n9r4(java+VUE+Mybatis+Maven+Mysql)
  • 原文地址:https://blog.csdn.net/hgf1037882434/article/details/134540785