C#实现本地服务器客户端私聊通信

(一)需求

       在游戏中我们经常能够看到玩家与玩家之间可以进行私聊,在QQ或微信中最基本的功能就是用户与用户之间的通信。抽象成计算机网络,就是两个客户端通过服务器进行私聊通信,两个客户端可以互相看到对方发送过来的信息。这种两个客户端的私聊通信是如何实现的呢?在本篇文章我们就来探讨一下。

(二)解决思路

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

(三)设计思路

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

(四)代码实现

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


       客户端

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
/*
自定义网络通信共享库NetShare,包括通用数据包DataPacket、私聊频道服务器数据包PSDataPacket、
服务器数据包ServerDataPacket和私聊频道客户端数据包PCDataPacket等。
*/
using NetShare;
using System.Threading;
using System.Linq;//私聊频道客户端
public class PersonalChannelClient : MonoBehaviour
{public Text BaseInfo;//显示Socket连接基本信息的文本public Text EchoContents;//Socket回显信息的文本public Text ChatContents;//聊天信息的文本public Dropdown Friends;//目标客户端名称下拉菜单public Dropdown ClientMenu;//客户端名称下拉菜单public Button Connect;//连接按钮public Button DisConnect;//断开连接按钮public InputField SendInput;//聊天消息输入框public Button Send;//聊天信息发送按钮static string ipAddressStr;//IP地址字符串static int port;//端口static IPAddress iPAddress;//IP地址对象static IPEndPoint iPEndPoint;//IP端点对象string clientName, sendStr;//客户端名称和发送信息字符串Socket currentClientSocket;//当前客户端Socketbool isLockSend;//是否锁定聊天信息发送按钮byte[] buffer;//消息接收缓冲区Queue<string> echoContentQueue, chatContentQueue;//回显信息队列和聊天信息队列PCDataPacket dataPacket;//通用数据包List<string> desClientNames;//目标客户端名称合集//反映Socket是否与服务器有效连接的属性bool isConnected{get{if (currentClientSocket == null) return false;return !currentClientSocket.Poll(10, SelectMode.SelectRead) && currentClientSocket.Connected;}}void Start(){//初始化ipAddressStr = "127.0.0.1";clientName = ClientMenu.options.Count > 0 ? ClientMenu.options[0].text : "";port = 5500;iPAddress = IPAddress.Parse(ipAddressStr);iPEndPoint = new IPEndPoint(iPAddress, port);buffer = new byte[1024];echoContentQueue = new Queue<string>();chatContentQueue = new Queue<string>();desClientNames = new List<string>() { "None" };//为UI控件添加监听事件ClientMenu.onValueChanged.AddListener((index) =>{clientName = ClientMenu.options[index].text;});Connect.onClick.AddListener(() =>{Thread thread = new Thread(new ThreadStart(ConnectDeal));thread.Start();});DisConnect.onClick.AddListener(() =>{Thread thread = new Thread(new ThreadStart(DisConnectDeal));thread.Start();});Send.onClick.AddListener(() =>{sendStr = SendInput.text;Thread thread = new Thread(new ThreadStart(SendDeal));thread.Start();SendInput.text = string.Empty;});}void Update(){//不断更新Socket基本信息BaseInfo.text = $"ClientName:{clientName}" +string.Format("\nSocketHashCode:{0}", currentClientSocket == null ? "None" : currentClientSocket.GetHashCode().ToString()) +$"\nisLock:{isLockSend}" +string.Format("\nPoll:{0}", currentClientSocket == null ? "None" : (!currentClientSocket.Poll(10, SelectMode.SelectRead)).ToString()) +string.Format("\nIsConnected:{0}", currentClientSocket == null ? "False" : currentClientSocket.Connected.ToString());//更新回显信息if (echoContentQueue.Count > 0){while (echoContentQueue.Count > 0){SetEchoContents(echoContentQueue.Dequeue());}}//更新聊天信息if (chatContentQueue.Count > 0){while (chatContentQueue.Count > 0){SetChatContents(chatContentQueue.Dequeue());}}//更新目标客户端名称下拉菜单if (desClientNames?.Count > 0){Friends.AddOptions(desClientNames);desClientNames.Clear();}}//设置回显信息相关UI的内容void SetEchoContents(string text){EchoContents.text += text;}//设置聊天信息相关UI的内容void SetChatContents(string text){ChatContents.text += text;}//执行逻辑:Socket异步连接处理void ConnectDeal(){echoContentQueue.Enqueue($"\n客户端{clientName}正在请求服务器连接...");Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);clientSocket.BeginConnect(iPEndPoint, ConnectCallback, clientSocket);}//执行逻辑:Socket异步断开连接处理void DisConnectDeal(){echoContentQueue.Enqueue($"\n客户端{clientName}正在断开与服务器的连接...");if (isConnected){currentClientSocket.Shutdown(SocketShutdown.Both);currentClientSocket.BeginDisconnect(false, DisConnectCallback, currentClientSocket);}else echoContentQueue.Enqueue($"\n客户端{clientName}未与服务器建立连接,无法进行断开连接的操作...");}//执行逻辑:Socket异步接收信息处理void ReceiveDeal(){echoContentQueue.Enqueue($"\n客户端{clientName}开始监听服务器响应...");currentClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, currentClientSocket);}//执行逻辑:Socket异步发送信息处理void SendDeal(){if (!isLockSend && !string.IsNullOrEmpty(sendStr)){dataPacket.mContent = sendStr;string v_desClientName = Friends.options[Friends.value].text;if (!v_desClientName.Equals("None")) dataPacket.mDestinationClientName = v_desClientName;byte[] bytes = dataPacket.ToBytes();currentClientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, currentClientSocket);}}//执行逻辑:Socket异步连接处理回调void ConnectCallback(IAsyncResult ar){try{Socket socket = ar.AsyncState as Socket;socket.EndConnect(ar);currentClientSocket = socket;if (isConnected){dataPacket = new PCDataPacket(){mLocalEndPointStr = socket.LocalEndPoint.ToString(),mClientName = clientName,mDestinationClientName = string.Empty,mContent = $"成功与服务器建立连接!"};byte[] bytes = dataPacket.ToBytes();socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, currentClientSocket);isLockSend = false;echoContentQueue.Enqueue($"\n<color=orange>客户端{clientName}与服务器连接成功!</color>");ReceiveDeal();}else echoContentQueue.Enqueue($"\n<color=red>客户端{clientName}与服务器连接失败!</color>");}catch (SocketException se){echoContentQueue.Enqueue($"\n<color=red>客户端{clientName}与服务器连接失败!</color>\n错误信息:{se.Message}");}}//执行逻辑:Socket异步断开连接处理回调void DisConnectCallback(IAsyncResult ar){try{isLockSend = true;Socket socket = ar.AsyncState as Socket;socket.EndDisconnect(ar);dataPacket = null;echoContentQueue.Enqueue($"\n<color=orange>客户端{clientName}与服务器断开连接操作成功!</color>");}catch (SocketException se){echoContentQueue.Enqueue($"\n客户端{clientName}与服务器断开连接操作失败!\n错误信息:{se.Message}");}}//执行逻辑:Socket异步发送信息处理回调void SendCallback(IAsyncResult ar){try{Socket socket = ar.AsyncState as Socket;socket.EndSend(ar);echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送了一条消息!");}catch (SocketException se){echoContentQueue.Enqueue($"\n客户端{clientName}向服务器发送信息操作失败!\n错误信息:{se.Message}");}}//执行逻辑:Socket异步接收信息处理回调void ReceiveCallback(IAsyncResult ar){try{Socket socket = ar.AsyncState as Socket;int count = socket.EndReceive(ar);DataPacket dataPacket = DataPacket.ToObject<DataPacket>(buffer.Take(count).ToArray());if (dataPacket is PSDataPacket psdp && psdp.mClientNames?.Length > 0){desClientNames.Clear();foreach (string name in psdp.mClientNames){if (Friends.options.FindIndex((od) => od.text.Equals(name)) == -1) desClientNames.Add(name);}}else if (dataPacket is ServerDataPacket sdp){string v_res = sdp.mContent;if (!string.IsNullOrEmpty(v_res)) chatContentQueue.Enqueue("\n" + v_res);}//若Socket连接有效则继续接收消息if (isConnected)socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, socket);}catch (SocketException se){echoContentQueue.Enqueue($"\n客户端{clientName}接收服务器消息失败!\n错误信息:{se.Message}");}}
}

        服务器

using System.Net.Sockets;
using System.Net;/*
自定义网络通信共享库NetShare,其中包括了私聊频道服务器数据包PSDataPacket、通用数据包DataPacket、
服务器数据包ServerDataPacket和私聊频道客户端数据包PCDataPacket等。*/using NetShare;namespace UnityServer
{//私聊频道服务器internal class PersonalChannelServer{private static string ipAddress = "127.0.0.1";//IP地址字符串private static int port = 5500;//端口private static int maxConnect = 20;//最大连接数private static byte[] buffer = new byte[1024];//消息缓冲区//客户端Socket合集,key为IPEndPoint字符串,value为服务器为客户端分配的Socketprivate static Dictionary<string, Socket> clients = new Dictionary<string, Socket>();private static Socket? serverSocket;//服务器Socket//客户端键值对,key为客户端名称,value为IPEndPoint字符串private static Dictionary<string, string> clientKVs = new Dictionary<string, string>();private static void Main(string[] args){Thread thread = new Thread(new ThreadStart(ServerDeal));thread.Start();Console.ReadLine();}//判断Socket是否进行有效连接private static bool IsConnected(Socket socket){if (socket == null) return false;return !socket.Poll(10, SelectMode.SelectRead) && socket.Connected;}//执行逻辑:服务器处理private static void ServerDeal(){serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress v_ipAddress = IPAddress.Parse(ipAddress);serverSocket.Bind(new IPEndPoint(v_ipAddress, port));serverSocket.Listen(maxConnect);Console.WriteLine($"开启服务器[{serverSocket.LocalEndPoint}]...");serverSocket.BeginAccept(AcceptCallback, null);}//执行逻辑:Socket异步接收消息private static void ReceiveDeal(object? clientSocket){Console.WriteLine("********************");if (clientSocket == null) return;Socket? v_clientSocket = clientSocket as Socket;if (v_clientSocket == null) return;Console.WriteLine("接收到客户端的连接请求!");if (IsConnected(v_clientSocket))v_clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, v_clientSocket);}//添加客户端Socket到客户端Socket合集private static void AddClient(Socket clientSocket){if (clientSocket == null) return;EndPoint? endPoint = clientSocket.RemoteEndPoint;if (endPoint != null){string? v_endPointStr = endPoint.ToString();if (v_endPointStr != null) clients[v_endPointStr] = clientSocket;}}//向所有客户端发送指定信息private static void SendToAll(PSDataPacket dataPacket){if (dataPacket == null) return;byte[] bytes = dataPacket.ToBytes();foreach (Socket clientSocket in clients.Values){if (IsConnected(clientSocket)){Thread thread = new Thread(() =>{clientSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, clientSocket);});thread.Start();}}}//向指定的客户端发送服务器数据包private static void SendTo(ServerDataPacket dataPacket, Socket? destinationSocket){if (dataPacket == null || destinationSocket == null) return;byte[] bytes = dataPacket.ToBytes();if (IsConnected(destinationSocket)){Thread thread = new Thread(() =>{destinationSocket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, destinationSocket);});thread.Start();}}//Socket监听请求回调private static void AcceptCallback(IAsyncResult ar){try{if (serverSocket != null){Socket clientSocket = serverSocket.EndAccept(ar);AddClient(clientSocket);Thread thread = new Thread(new ParameterizedThreadStart(ReceiveDeal));thread.Start(clientSocket);serverSocket.BeginAccept(AcceptCallback, null);}}catch (SocketException se){Console.WriteLine("AcceptException:" + se.Message);}}//Socket发送信息回调private static void SendCallback(IAsyncResult ar){try{Socket? clientSocket = ar.AsyncState as Socket;if (clientSocket != null) clientSocket.EndSend(ar);}catch (SocketException se){Console.WriteLine("SendException:" + se.Message);}}//Socket接收信息回调private static void ReceiveCallback(IAsyncResult ar){try{Socket? clientSocket = ar.AsyncState as Socket;if (clientSocket != null){int bytesCount = clientSocket.EndReceive(ar);PCDataPacket? dataPacket = DataPacket.ToObject<PCDataPacket>(buffer.Take(bytesCount).ToArray());if (dataPacket != null){if (clientSocket.RemoteEndPoint != null){string? v_endPointStr = clientSocket.RemoteEndPoint.ToString();if (!string.IsNullOrEmpty(v_endPointStr)){clientKVs[dataPacket.mClientName] = v_endPointStr;SendToAll(new PSDataPacket(){mClientNames = clientKVs.Keys.ToArray()});}}string v_content = $"客户端{dataPacket.mClientName}:{dataPacket.mContent}";Socket? destinationSocket;string? endPointStr;clientKVs.TryGetValue(dataPacket.mDestinationClientName, out endPointStr);if (!string.IsNullOrEmpty(endPointStr) && clients.TryGetValue(endPointStr, out destinationSocket))SendTo(new ServerDataPacket() { mContent = v_content }, destinationSocket);}if (IsConnected(clientSocket))clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, clientSocket);}}catch (SocketException se){Console.WriteLine("ReceiveException:" + se.Message);}}}
}

(五)测试

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

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

(六)总结

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/158709.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis的高可用之持久化

1、redis的高可用考虑指标 &#xff08;1&#xff09;正常服务 &#xff08;2&#xff09;数据容量的扩展 &#xff08;3&#xff09;数据的安全性 2、redis实现高可用的四种方式 &#xff08;1&#xff09;持久化 &#xff08;2&#xff09;主从复制 &#xff08;3&…

这是基础校园二手交易框架

<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <title>校园二手交易</title> <style> /* Reset stylesheet */ * { margin: 0; padding: 0; box-s…

Hive小文件处理

MR任务 mr任务参考链接 set hive.exec.reducers.max3 set hive.exec.dynamic.partition.mode true; --使用动态分区时&#xff0c;设置为ture。 set hive.exec.dynamic.partition.mode nonstrict; --动态分区模式&#xff0c;默认值&#xff1a;strict&#xff0c;表示必须…

搜维尔科技:Movella Xsens MVN LINK 实际应用,一镜到底!

搜维尔科技&#xff1a;Movella Xsens MVN LINK 实际应用&#xff0c;一镜到底&#xff01;

项目经理面试经典问题大揭秘:聪明回答,轻松获得心仪职位!

作为一名申请了项目管理职位的求职者&#xff0c;要顺利入职必须过了面试这一关。然而&#xff0c;你可能会对面试官可能会问什么问题以及如何回答好感到迷茫。以下是我整理的一些关于项目经理面试问题及回答技巧&#xff0c;希望对你有所帮助&#xff01; 招聘方&#xff08;P…

与客户沟通过程中的30个实用技巧

1.使谈判对手做肯定答复的问题 —Is it important that …? 连续发问沟通对手给予肯定答复的问题&#xff0c;最后引导他对你的主要建议也作肯定的答复&#xff0c;是绝对需要花费一番心思的。通常沟通对手只对自己有利的问题&#xff0c;才会痛快地回答“Yes”。因此&#…

uniapp 富文本以及移动端富文本的展示问题

富文本展示有几种方式: 1.<view v-html"content"></view> 2. uniapp自带组件 rich-text rich-text | uni-app官网 <rich-text :nodes"content"></rich-text> 3.uView组件 u-parse Parse 富文本解析器 | uView 2.0 - 全面兼…

神经网络常用激活函数详解

&#x1f380;个人主页&#xff1a; https://zhangxiaoshu.blog.csdn.net &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️&#xff0c;如有错误敬请指正! &#x1f495;未来很长&#xff0c;值得我们全力奔赴更美好的生活&…

如何从命令行运行3dMax脚本(MAXScript或Python)?

3dMax允许您直接在命令行上输入脚本命令。从DOS命令行启动3dMax时&#xff0c;可以让它运行指定的启动MAXScript或Python脚本。这对于无人参与的批处理渲染等任务非常有用。 此功能使用现有的-U命令行开关&#xff0c;该开关命名3dMax启动时要运行的实用程序。-U开关允许一个可…

【科技素养】蓝桥杯STEMA 科技素养组模拟练习试卷2

单选题 1、两袋中分别有同样多的硬糖和酥糖&#xff0c;现将第一袋中的20块酥糖放到第二袋中&#xff0c;第二袋中的硬糖和酥糖相同&#xff0c;接着又将第二袋中的20块硬糖放到第一袋中&#xff0c;则第一袋中的硬糖是酥糖的4倍&#xff0c;问原来一袋中有&#xff08;&#…

基于STM32的电子时钟(论文+源码)

1. 系统设计 电子时钟是一种广泛使用的工具&#xff0c;其可以帮助人们准确掌握时间&#xff0c;本课题基于STM32的电子时钟系统的设计&#xff0c;在功能上设计如下&#xff1a; 具有电子时钟的基本功能&#xff0c;显示年月日&#xff0c;时分秒等基本信息&#xff1b;可以…

电脑盘符错乱,C盘变成D盘怎么办?

在一些特殊情况下&#xff0c;磁盘盘符会出现错乱&#xff0c;C盘可能会变成D盘。那么&#xff0c;这该怎么办呢&#xff1f;下面我们就来了解一下。 通过磁盘管理更改盘符 磁盘管理是Windows自带的工具&#xff0c;它位于“计算机管理”的控制台中。管理硬盘及其所包含的卷或…

梨花声音研修院,严肃与刚毅是音色核心

在为军旅剧提供配音服务时&#xff0c;配音员需捕捉并展现军事场合的严肃气氛、军人的刚毅品质以及他们对职责的忠诚。军旅剧往往围绕着军人的日常生活、战场经历、战友之情以及对祖国的热爱等主题展开&#xff0c;所以配音需能传递这些情感和价值。以下是进行军旅剧配音的一些…

倍福控制器搭建IgH环境

最近收到了倍福CX5230控制器&#xff0c;控制器上自带EBUS总线扩展的IO&#xff0c;使用的是CCAT网卡&#xff0c;在控制器上安装preempt-rt Linux系统&#xff0c;再安装IgH。 IgH正常识别到了扩展的IO模块。 运行控制程序&#xff0c;可以正常控制IO输出。

Qt应用开发(进阶篇)——线程 QThread

一、前言 QThread类继承于QObject基类&#xff0c;是Qt经典基础工具类&#xff0c;QThread类提供了一种独立于平台的方式来管理线程&#xff0c;让开发者能够快速的完成多线程的创建和使用。 正常情况下&#xff0c;一个PC程序使用到多线程的概率是非常高的&#xff0c;在不同方…

bclinux aarch64 ceph 14.2.10 云主机 4节点 fio

ceph -s 由于是基于底层分布式存储的云主机&#xff0c;数据仅供参考 本地云盘性能 direct1 1M读取 IOPS134, BW134MiB/s [rootceph-client rbd]# cd / [rootceph-client /]# fio -filenamefio.bin -direct1 -iodepth 128 -thread -rwread -ioenginelibaio -bs1M -size10G -n…

XXL-job-oracle 版本

XXL很流行但是原来的是MYSQL版本 &#xff0c; 现在 工作需要做了一个定时任务的服务器&#xff0c; 发现XXL是很合适的&#xff0c;主要是修改了 ### freemarker spring.freemarker.templateLoaderPathclasspath:/templates/ spring.freemarker.suffix.ftl spring.freemarker.…

【重装系统SSH连不上】 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

问题 centos在重装系统后再连接&#xff0c;无法连接上 [rootaisa ~]# ssh root192.168.0.3 ssh root192.168.0.3无法连接上&#xff0c;输出是&#xff1a; WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! S…

Java继承和多态

文章目录 继承成员继承构造方法super和this的区别代码块构造顺序 限定修饰符final继承和组合 多态向上转型重写动态绑定重写注意事项 向下转型多态优点注意 继承 成员继承 class Animal{public String name;public int age;public int a10;public void doSomething(){System.…

KDE 项目发布了 KDE Gear 23.08.3

导读KDE 项目发布了 KDE Gear 23.08.3&#xff0c;作为最新的 KDE Gear 23.08 开源集合的第三次维护更新&#xff0c;该集合包含了用于 KDE Plasma 桌面环境和其他平台的 KDE 应用程序。 KDE Gear 23.08.3 是在 KDE Gear 23.08.2 大约一个月之后发布的&#xff0c;包含了更多对…