C#Socket编程服务器和多客户端通信,客户端和客户端通信

2023-10-27

        对于Socket编程,网上资料、博文一抓一大把,不过大多数都是简单讲解单客户端和服务端通信的实例,这里主要给大家展现一下在.net平台上用窗体程序实现的过程,不过比较有特点的是会告诉大家如何实现不同客户端之间的通信,它们如何通过一个服务端准确的找到对方进行通信,包括上线提醒、私信交流、昵称重名处理等功能。 

一、服务端架构

1、开启监听

        简单来说,服务端首先要创建一个监听线程,检测客户端的连接情况,这个部分的程序封装在一个按钮里。主要实现代码如下:

  Thread threadwatch = null;//负责监听客户端的线程       
        //创建一个监听线程
          threadwatch = new Thread(watchconnecting);
       //将窗体线程设置为与后台同步,随着主线程结束而结束
          threadwatch.IsBackground = true;
      //启动线程   
           threadwatch.Start();

        如果有仔细看我上面给出的代码细心的观众会发现有个watchconnecting()方法被委托进了监听线程,其实这就是一个监听客户端信息消息的函数,很老套的过程:

    Socket connection = null;
  
    while (true)  //持续不断监听客户端发来的请求   
    {
        try
        {
            connection = socketwatch.Accept();
        }

        其中

Socket socketwatch = null;//负责监听客户端的套接字

        也只是大家异常熟悉的Socket套接字,这部分绑定IP和端口到Point,然后

    //监听绑定的网络节点
    socketwatch.Bind(point);
    //将套接字的监听队列长度限制为20
    socketwatch.Listen(20);

        的过程应该在前面服务端开启监听线程的按钮里实现,这里就不细说了。

 

        然后创建一个通信线程,对服务端和客户端之间的交流进行一个完善:

//创建一个通信线程    
                ParameterizedThreadStart pts = new ParameterizedThreadStart(recv);
                Thread thread = new Thread(pts);
                thread.IsBackground = true;//设置为后台线程,随着主线程退出而退出   
                //启动线程   
                thread.Start(connection);

2、服务端通信过程

        由之前代码里创建的通信线程里委托的recv()方法,看字面意思也知道是接收客户端发送消息的接收函数。也是很常规的部分:

   Socket socketServer = socketclientpara as Socket;
            while (true)
            {

                //创建一个内存缓冲区 其大小为1024*1024字节  即1M   
                byte[] arrServerRecMsg = new byte[1024 * 1024];
                //将接收到的信息存入到内存缓冲区,并返回其字节数组的长度  
                try
                {
                    int length = socketServer.Receive(arrServerRecMsg);
                    //将机器接受到的字节数组转换为人可以读懂的字符串   
                    string strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 0, length);
                   //将字符串转化为二进制流进行其他操作
                    byte[] bytes = System.Text.Encoding.UTF8.GetBytes(strSRecMsg);

        注意,将接收到的信息转化为二进制流之后可以Send(bytes)转发消息了,当然,这一块之后会细讲,这里只是先大概介绍下流程。

二、客户端架构

        由于客户端基本架构和服务端大体相似,只是在监听部分转为尝试连接部分,大家记住这两个变量:

//创建 1个客户端套接字 和1个负责监听服务端请求的线程 
Thread threadclient = null; 
Socket socketclient = null;

        和服务端大同小异,不过注意要在客户端绑定的是之前在服务端绑定的相同IP和端口才能互通,同样将IP和端口绑定到Point上:

try
    {
        //客户端套接字连接到网络节点上,用的是Connect
        socketclient.Connect(point);
    }

        即服务端是Accept,客户端是Conncet,然后同样开启一个监听服务端的线程:

    threadclient = new Thread(recv);
    threadclient.IsBackground = true;
    threadclient.Start(socketclient);

        至于里面的recv,和前面的服务端完全一样,就不重述了。

三、获取本地IP4的方法

        大家是否为如何精确获得IP4的IP地址烦恼呢,又不想手动去查,好吧,这里有一个方法送给大家:

public static string GetLocalIP()
        {
            try
            {
                string HostName = Dns.GetHostName(); //得到主机名
                IPHostEntry IpEntry = Dns.GetHostEntry(HostName);
                for (int i = 0; i < IpEntry.AddressList.Length; i++)
                {
                    //从IP地址列表中筛选出IPv4类型的IP地址
                    //AddressFamily.InterNetwork表示此IP为IPv4,
                    //AddressFamily.InterNetworkV6表示此地址为IPv6类型
                    if (IpEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork)
                    {
                        return IpEntry.AddressList[i].ToString();
                    }
                }
                return "";
            }
            catch (Exception ex)
            {
                MessageBox.Show("获取本机IP出错:" + ex.Message);
                return "";
            }
        }

四、服务端转发消息

1、转发处理

        这个部分即为本文的核心所在,之前铺垫的也都是大家或多或少都了解的,而这个部分我在网上浏览资料时并没有发现比较鲜明的介绍。
        首先注意这两个字典集

Dictionary<string, Socket> dic = new Dictionary<string, Socket> { };   //定义一个集合,存储客户端信息
Dictionary<string, string> dicName = new Dictionary<string, string> { };   //昵称与客户端对应

        我们的服务端在客户端连接上服务端时是有办法知道客户端的信息的:

    //获取客户端的IP和端口号
    IPAddress clientIP = (connection.RemoteEndPoint as IPEndPoint).Address;
    //获取客户端的IP和端口号
    int clientPort = (connection.RemoteEndPoint as IPEndPoint).Port;
    RemoteEndPoint = connection.RemoteEndPoint.ToString(); //客户端网络结点号

        至于为什么我会建立两个字典集,dic里保存的是

RemoteEndPoint = connection.RemoteEndPoint.ToString(); //客户端网络结点号
connection = socketwatch.Accept();

        RemoteEndPoint是套接字Socket里的一个属性,能唯一的辨识出不同客户端连接时套接字的网络节点,当然,如果有其他类似的属性也可以代替。则RemoteEndPoint 就是Connection的唯一身份标识,那么我们在进行Socket里面一些属性操作时,比如用connection 发送、接收消息都可以以RemoteEndPoint 这个标识来鉴别身份。
        不过每个客户端都能自己给自己起个好听的昵称,不然全是网络节点号也记不住,分不清哪个是自己,所以我又用了dicName把网络节点号和客户端的昵称绑在了一起,相当于实现了一个双层的嵌套;

        要查找对应信息建议使用LINQ,十分简单,比如:

var name = dicName.Where(q => q.Value == socketServer.RemoteEndPoint.ToString()).Select(q => q.Key);
string leavemsg = name.FirstOrDefault()

        就可以轻松找出已知某个网络节点号的昵称。

        所以服务端转发消息就是当接收到一个客户端传来的消息时会自动将接受到的任何消息转发到其他所有客户端,这个在字典集里遍历就可以实现,比如:

//转发消息给其他客户端
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(strSRecMsg);

foreach (string Client in dicName.Values)
{
    if (Client != socketServer.RemoteEndPoint.ToString())//只要不是自己
    dic[Client].Send(bytes);   //发送数据
}

        要注意的是,将客户端消息存进字典里的代码是要写在服务端开启监听登录的按钮里,这样每次客户端一连接成功就会记录一次。

2、区别客户端

        也许有人会问,虽然转发实现了,但是那么多客户端之间发送的消息如何辨别呢,这里有两种处理方法:可以在服务端设置一个变量保存每一个连接时客户端的昵称,在转发消息时将这个变量放在消息头,这样就可以区分;不过我认为这个方法还是比较麻烦的,既然消息来源是客户端,那我们就在客户端发送的消息头加上每个客户端自己的昵称,这样所有客户端发送的消息都是带有自己的昵称的,这样就不需要我们再过多的处理了。

五、上线下线提醒

1、上线

        其实方法很简单,只要知道了转发的实现很容易想到,只要在客户端开启监听服务的按钮里写一段发送昵称消息给服务端的代码:

byte[] name = Encoding.UTF8.GetBytes(textBoxName.Text);
//调用客户端套接字发送字节数组   
 socketclient.Send(name);

        服务端监听客户端端登录时,会收到来自客户端发出的昵称消息,服务端将昵称存进字典集里就将昵称转发到其他客户端提醒“XXX,上线了”:

        //上线提醒
        string msg = strRecMsg +","+"上线了";
        byte[] Msg = Encoding.UTF8.GetBytes(msg);
        foreach (string Client in dicName.Values)
        {
            if (Client != RemoteEndPoint)
                dic[Client].Send(Msg);   //发送数据
        }

2、下线

        下线,我的处理是全部放在套接字监听连接中断异常里,即,只要Socket中断或者连接失败,就会在异常里转发下线消息:

catch (Exception ex)
{
    //下线处理

        道理和上线差不多,只是代码的位置不同。

六、昵称重名处理

        由前面的介绍,我们知道客户端在连接服务端时会发送一个昵称消息给服务端,服务端将昵称存起来后如果发现字典集里有相同的名字则会关闭正在通信Sock,发送一个提示消息给客户端:

   //获取昵称
                byte[] RecMsg = new byte[1024 * 1024];
                int length = connection.Receive(RecMsg); 
                string strRecMsg = Encoding.UTF8.GetString(RecMsg, 0, length);

                //检查昵称重名
                if (dicName.Count>0)              
                {                 
                    string ack = "昵称已存在,请重新输入昵称";
                    byte[] ackMsg = Encoding.UTF8.GetBytes(ack);
                    if (dicName.ContainsKey(strRecMsg))
                    {
                        Flag = true;
                        connection.Send(ackMsg);
                        //离线处理                                      
                        connection.Close();

        相应的,客户端在受到服务端传过来的提示消息时,关闭Sock下线:

    string strRevMsg = Encoding.UTF8.GetString(arrRecvmsg, 0, length);

                    string []s=strRevMsg.Split(',');
                     if (s[0] != textBoxName.Text)
                     {
                         if (strRevMsg == "昵称已存在")
                         {
                             socketServer.Close();
                             this.buttonStart.Enabled = true;
                             break;

        这里有两个要特别注意的地方,首先是if (s[0] != textBoxName.Text)是针对之前的上线提醒,在昵称重名时不提醒上线消息,我们接着看一段客户端开启监听的代码:

try
            {
                //SocketException exception;
                this.buttonStart.Enabled = false;
                //定义一个套接字监听
                socketclient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                //获取文本框中的IP地址
                IPAddress address = IPAddress.Parse(textBoxIP.Text.Trim());
                //将获取的IP地址和端口号绑定在网络节点上
                IPEndPoint point = new IPEndPoint(address, int.Parse(textBoxPort.Text.Trim()));

        很显然,我们在点击开启监听的按钮后,按钮会置灰,这也是一种将Socket连接状态反应在按钮上的一个同步,所以当Socket断开时,我们需要this.buttonStart.Enabled = true;这样才能重新点击按钮,输入正确要求的昵称再重新连接服务端。

七、私聊

        之前服务端转发实现的只是群聊,那么私聊怎么办呢。其实也很简单,我这里以服务端和其他不同客户端私聊为例。
        因为能分辨不同的客户端,那么想和谁私聊不是轻松的很。我的做法是将dicName的消息展示到一个ListBox里:

void OnlineList_Disp(string Info)
        {
            if(!listBoxOnlineList.Items.Contains(Info))
            listBoxOnlineList.Items.Add(Info);   //在线列表中显示连接的客户端
        }

        在服务端想私聊时,只要点击列表里一个客户端,将代码写在一个按钮里,详细实现如下:

string sendMsg = "管理员:"+richTextBoxSend.Text.Trim();         //要发送的信息
            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(sendMsg);   //将要发送的信息转化为字节数组,因为Socket发送数据时是以字节的形式发送的
            if (listBoxOnlineList.SelectedIndex == -1)
            {
                MessageBox.Show("请选择要发送的客户端!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
            else
            {
                string selectClient = listBoxOnlineList.Text;  //选择要发送的客户端
                var point = dicName.Where(q => q.Key == selectClient).Select(q => q.Value);
                dic[point.FirstOrDefault()].Send(bytes);   //发送数据
                richTextBoxSend.Clear();             
            }

        如果客户端之间想自己私聊呢,其实完全可以衍变过去不是吗,那我就把客户端接收上线提醒的昵称存到一个字典里进行类似操作不是也可以?所以方法都是通的,甚至可以像QQ、微信那样设计,在点击一个字典里的信息时跳转个页面到大对话框不也是美滋滋。

八、其他

        如果有时间,我会尝试突破局域网的限制,实现外网之间Socket通信,目前思路有两个:花生壳内网映射,去阿里云租个外网服务器,当然这些都是后话了,有机会再一起探讨~

————————————————
版权声明:本文为CSDN博主「Yujie_Yang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Yujie_Yang/article/details/53647258

https://www.csdn.net/gather_21/MtTaEgysNjctYmxvZwO0O0OO0O0O.html

https://blog.csdn.net/Yujie_Yang/article/details/53647258?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-53647258.nonecase

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C#Socket编程服务器和多客户端通信,客户端和客户端通信 的相关文章

  • Java制作报表系统流程_finereport报表制作详细过程

    一般来说 一个完整的报表设计流程 大体分为如下几个步骤 1 打开设计器 2 配置数据源 3 新建报表 4 配置私有数据源 5 报表设计 6 预览报表 7 发布报表 第一部分 配置数据源 打开报表设计器 在设计器最上方的菜单栏当中 选择服务器
  • Redis时单线程设计的,为什么还这么快

    单线程设计的考虑 Redis采用单线程好处在于避免了多线程对数据竞争的问题 加锁的问题 上下文切换的问题 据官方解释 redis的瓶颈不在cpu 而在内存或者网络的带宽 综合考虑然后就采用了单线程 Redis的性能非常高 每秒可以承受10W
  • a-cascader编辑绑定值数据变化,但页面dom不回显不更新问题($set用法)

    问题 编辑时 级联组件绑定数据变化 的dom不进行更新 原因 引用值类型 对象或数组 动态添加或删除某一个属性值 需要使用this set 或者 Object assign 不然数据触发不到getter和setter 对from直接赋值不会
  • Golang知识点五、数据类型

    数据类型 从本篇文章开始 记录Golang数据类型相关的内容 数据类型相关的知识点包括slice map string三个部分 1 切片 1 1 从数组说起 数组是具有固定长度具有零个或者多个相同数据类型元素的序列 由于数组长度固定 在Go
  • 家庭IOT监测之摄像头OV7670测试

    本篇目标 使用STM32F407驱动摄像头OV7670 并上位机显示照片结果 材料准备 STM32F4标准工程 stm32f407自建标准工程 stm32f4标准工程 git仓库地址 STM32F4摄像头测试工程 里面包含ov7670驱动文
  • C++与C#相比,哪个更适合开发大型游戏?

    我觉得这个问题倒过来回答比较合适 先解答一下目前主流的大型游戏 都是使用什么语言开发的 再说说哪种语言更适合开发大型游戏 首先 先说下 大部分游戏 甚至是应用 都极少只使用一种语言开发的 主流游戏的开发语言 LOL LOL登陆后的界面 是使

随机推荐

  • openwrt上如何进行串口加密

    实际生产的固件许多都对串口做了加密 这样就增加了别人觊觎你的生产固件的难度 毕竟没有几个厂商不忌惮对手对其固件内容的分析 当然这种串口加密并不能起到特别大的阻碍作用 因为只要别人想看你的固件 你是很难防住的 但串口加密至少会挡住以少部分人
  • C++ 虚函数

    如何在派生类中回避虚函数 借助于作用域运算符实现 int p derived gt base func 强行调用基类中定义的函数版本 而不管derived的动态类型到底是什么 通常当一个派生类的虚函数调用它覆盖的基类的虚函数版本时 基类版本
  • @Validated 返回参数太乱处理

    对 Validated 返回参数太乱 处理为只关注重点信息 1 处理前 有参数 通过接口校验时返回如下长串 2 处理后 处理代码 RestControllerAdvice Slf4j public class SysExceptionHan
  • 1001 害死人不偿命的(3n+1)猜想 (15 分)

    卡拉兹 Callatz 猜想 对任何一个正整数 n 如果它是偶数 那么把它砍掉一半 如果它是奇数 那么把 3n 1 砍掉一半 这样一直反复砍下去 最后一定在某一步得到 n 1 卡拉兹在 1950 年的世界数学家大会上公布了这个猜想 传说当时
  • Windows环境下安装PyMySQL(已安装Anaconda)

    我的Anaconda安装目录为D Anaconda3 在cmd中执行 会出现 随后输入pip 会出现等 随后输入
  • “真正的机会”:ChatGPT 如何帮助大学申请者

    随着平权行动的结束 生成式人工智能可以为没有导师或辅导员的学生提供帮助 从而实现招生 民主化 关于人工智能的争论主要分为三个基本类别 焦虑的不确定性 它会夺走我们的工作吗 存在的恐惧 它会杀死我们所有人吗 和简单的实用主义 人工智能可以写我
  • C语言实现Unix时间戳和北京时间的相互转换(已编译,亲试可用~)

    目录 一 什么是Unix时间戳 二 应用C语言库函数 三 C语言实现Unix时间戳与北京时间的相互转换 一 什么是Unix时间戳 1 Unix时间戳是从1970年1月1日 UTC GMT的午夜 开始所经过的秒数 不考虑闰秒 2 Unix时间
  • pyecharts0.5.x制作含地图的数据看板

    引言 pyecharts作为Python的数据可视化包 其强大的功能不言而喻 Python Echart 想想就觉得牛叉 目前pyecharts有两个大的版本 一个是0 5 x版本的 一个是1 0以后版本 而且这两个版本差别很大 如果是有的
  • excel中offset用法

    Excel 中的 Offset 函数用于在当前单元格的基础上 相对偏移指定的行数和列数 返回新单元格的值 该函数的语法如下 OFFSET reference rows cols height width reference 基准单元格 ro
  • python修饰器_Python修饰器

    Python的修饰器的英文名叫Decorator 当你看到这个英文名的时候 你可能会把其跟Design Pattern里的Decorator搞混了 其实这是完全不同的两个东西 虽然好像 他们要干的事都很相似 都是想要对一个已有的模块做一些
  • Failed to resolve packages: Package [com.unity.package-manager-ui@2.1.2] cannot be found. No package

    直接把E UnityProject test Packages manifest json 里面的依赖项删除 最后剩下 dependencies
  • 小红书破局品牌增长:4大阶段+8个种草建议

    品牌如何从激烈的竞争中突围 成为快速增长的 黑马 本文就和大家一起聊聊围绕产品面对不同阶段的人群 种草 策略 希望能够帮助品牌更好地与用户沟通并提升营销效率 实现品效合一 1 种草1 0 立住产品 抢占赛道 品牌现状 成立时间短 用户心中的
  • 基于协同过滤算法的商品推荐购物电商系统

    一 介绍 商品推荐是针对用户面对海量的商品信息而不知从何下手的一种解决方案 它可以根据用户的喜好 年龄 点击量 购买量以及各种购买行为来为用户推荐合适的商品 在本项目中采用的是基于用户的协同过滤的推荐算法来实现商品的推荐并在前台页面进行展示
  • Python图像处理【1】图像与视频处理基础

    图像与视频处理基础 0 前言 1 在 3D 空间中显示 RGB 图像颜色通道 1 1 图像表示 1 2 在 3D 空间中绘制颜色通道 2 使用 scikit video 读 写视频文件 2 1 scikit video 库 2 2 读 写视
  • 新引擎HeatWave将MySQL查询性能提升400倍

    参考资料 https www oracle com a ocom docs mysql heatwave technical brief pdf 前言 我刚去MySQL官方文档查个函数 顺便发现一个新东西 HeatWave 看起来很厉害的样
  • 垃圾代码是如何写出来的

    自我参加工作已经有几年了 接手过的项目也不少 包括安卓端和web前端的 在做这些项目的过程中 相当一部分的项目到最后都出现了一个现象 代码越写越乱 维护性越来越差 究其原因 我认为有如下几点 1 程序员自身能力 出现问题 首先得从自己身上找
  • Matlab实现基于BP神经网络的数据分类预测

    matlab语言实现基于BP神经网络的数据分类预测 四分类 其步骤如下 一 载入原始数据 这里以测试数据存放在Excel表格中为例 导入数据 res xlsread 数据集 xlsx 二 将数据集划分为训练集和测试集 temp randpe
  • Kafka3.0.0版本——手动调整分区副本示例

    目录 一 服务器信息 二 启动zookeeper和kafka集群 2 1 先启动zookeeper集群 2 2 再启动kafka集群 三 手动调整分区副本 3 1 手动调整分区副本的前提条件 3 2 手动调整分区副本的示例需求 3 3 手动
  • 实现FPGA Verilog HDL与NIOS II的通信数据交换——利用AVALON总线

    平时用FPGA基本都是全程用Verilog HDL编程 当遇到液晶的时候 发现Verilog的还不如C语言来的方便 但是用NIOS来编写的时候 实现NIOS与Verilog的通信又是一个问题 今天用了两种方法实现Verilog与NIOS通信
  • C#Socket编程服务器和多客户端通信,客户端和客户端通信

    对于Socket编程 网上资料 博文一抓一大把 不过大多数都是简单讲解单客户端和服务端通信的实例 这里主要给大家展现一下在 net平台上用窗体程序实现的过程 不过比较有特点的是会告诉大家如何实现不同客户端之间的通信 它们如何通过一个服务端准