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

自由学习记录(34)

服务端相对特殊处理一下,因为是多线程处理,而客户端的断开连接会造成socket容器里socket数量减少

 

这样的处理方式有很多问题,但解决的话甚至要从根本上修改,所以,在这更多是知识点的演示

客户端是正常的模式

VS的服务端代码里,每次访问clientDic的时候都加锁,不让别的线程调用,停住等待

防止多线程操作出现的对同一区域的同时修改,造成不必要的异常,所以加锁

TCP 通信中----TCP 面向字节流------数据在发送过程中可能会发生分包(一个完整消息被拆分成多个 TCP 包)或者黏包(多个消息在接收端一次性收到在同一个 TCP 包中)的现象。

为了解决这类问题,通常需要在应用层设计一种消息边界协议,最简单的方式就是在每个消息前面加上一个固定长度的头部,用来表示消息体的长度。下面提供一个使用 4 字节头部(代表消息体长度)的 C# 例子,演示如何处理分包和黏包问题:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;

public class TcpReceiver
{
    // 用于存储从 Socket 接收到的原始数据
    private List<byte> buffer = new List<byte>();

    /// <summary>
    /// 处理从 Socket 中接收到的数据,解决分包和黏包问题。
    /// 约定:每个消息前 4 个字节表示消息体的长度(Int32),后续为消息数据(采用 UTF8 编码)。
    /// </summary>
    /// <param name="socket">已经建立连接的 Socket 对象</param>
    public void ProcessReceivedData(Socket socket)
    {
        byte[] tempBuffer = new byte[1024];
        try
        {
            while (true)
            {
                // 从 Socket 接收数据
                int bytesRead = socket.Receive(tempBuffer);
                if (bytesRead <= 0)
                {
                    // 没有数据了或者连接关闭,退出循环
                    break;
                }

                // 将接收到的数据添加到缓存中
                buffer.AddRange(tempBuffer.Take(bytesRead));

                // 检查缓存中是否存在至少一个完整的消息
                while (buffer.Count >= 4) // 4 字节消息头
                {
                    // 前4个字节为消息长度(假设采用小端模式)
                    int messageLength = BitConverter.ToInt32(buffer.ToArray(), 0);
                    
                    // 如果缓存中数据足够构成一个完整消息(头部+消息体)
                    if (buffer.Count >= 4 + messageLength)
                    {
                        // 提取消息体
                        byte[] messageBytes = buffer.GetRange(4, messageLength).ToArray();
                        string message = Encoding.UTF8.GetString(messageBytes);
                        Console.WriteLine("接收到消息: " + message);

                        // 从缓存中移除已经处理的字节(头部和消息体)
                        buffer.RemoveRange(0, 4 + messageLength);
                    }
                    else
                    {
                        // 数据还不够,跳出循环等待更多数据
                        break;
                    }
                }
            }
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket 异常: " + ex.Message);
        }
        finally
        {
            // 关闭 Socket
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
        }
    }
}

如何区分消息
发送的信息添加标识,比如添加消息ID
所有发送的消息的头部加上消息ID(intshort、byte、long都可以,根据实际情况选择)

举例:
如果选用int类型作为消息ID的类型
前4个字节为消息ID,后面的字节才是为数据类的内容
这样每次收到消息时,先把前4个字节取出来解析为消息ID
再根据ID进行消息反序列化即可

区分消息里的字节数组要转换的类型是什么,可以通过这种继承解决方法

对于这种信息传输管理类,过场景的时候一般不会删掉

为了提高网络数据的传输效率,系统会为 Socket 分配发送缓冲区和接收缓冲区。这些缓冲区用于暂存待发送的数据以及接收到的数据直到应用程序调用相应的发送或接收函数。

通常建议在确保所有数据都已经传输完毕、对方也已准备好结束通信时再调用 Shutdown

如果调用 Shutdown 时,数据已经在发送缓冲区中,TCP 会尝试将缓冲区中的数据发送出去,然后再发送 FIN 信号。但如果对方还在等待数据,而你提前调用了 Shutdown,可能会导致对方无法接收到剩余数据(或者收到“连接已关闭”的信号),这取决于你程序的设计和双方协议的处理方式。

因为再怎么说也是unity挂在对象上的脚本,所以可以再加一个生命周期函数,ondestroy调用close关闭连接

也可以换线程池处理函数任务treadpool.queueUserWorkItem()

而被encoding成字符串的塞入了receive队列,里面装的都是类型已经转换处理好的数据

唐老师于是在update函数里使用了这些(也就是正常的打印掉,实际上这些收到的数据也是 要在某些地方把数据用了)

针对于receive函数的信息队列,是也是string类型,因为是socket.Receive()已经接下了字节数组,然后 唐老师 在那处理了字节数组转string

这里Send和receive各用了一个队列容器存储需要处理的信息,而各开了一个线程while循环处理自己的Send和receive函数

把需要处理的信息先用队列装起来(这里先把信息全部当做字符串,所以queue也是原信息string的一个大容器

发送函数为了防止Send对主线程的可能阻塞,所以就用了queue的容器来解决

客户端要连接服务器其实非常简单,只需要声明一个socket。然后去调用它的connect,连接成功过后,我们就可以用它的send和receive方法来进行这个消息的收发了。

需要注意的就是receive和send是阻塞式的方法,也就是说连接建立后,如果我们去收消息和发消息,如果在主线程里面调用的话,它可能会影响我们主线程的执行

把unity的客户端的信息收发管理单例写了

对于客户端处理连接的函数

在开发测试中:如果你想在本机调试多个 ASP.NET Core 项目,Visual Studio 会自动给每个项目分配不同的本地端口(例如 5000, 5001, 5002 等)来避免冲突

同一台计算机上,同一个 IP 地址 和 端口 组合只允许一个进程监听

也就是说,如果一个应用程序(进程)已经成功绑定(Bind)到 127.0.0.1:8080,那么其他应用程序就无法再绑定到同样的 127.0.0.1:8080,否则会发生冲突导致绑定失败(抛出异常)。这并不是说“不同程序用的 8080 都是指自己且不冲突”,而是操作系统只允许其中一个占用该端口。

打包出去,同时开启多个应用程序,这样也可以有多个客户端

利用了元组的知识,和线程池的使用

返回的是字节的个数,receive里的参数是会被注入数据的字节数组

  • 线程资源的来源

    • 内存分配
      每创建一个线程,操作系统都会为该线程分配一块独立的栈内存(默认大小通常在数百 KB 到 1 MB 不等,具体取决于操作系统和配置)。这块内存是从进程的虚拟地址空间中分配的,与主线程的栈是独立的
    • 内核对象和调度数据
      操作系统还会为每个线程维护调度信息、线程控制块(TCB)以及相关的内核对象。这些都是线程管理所必需的开销。
  • 主线程和新线程的关系

    • 新创建的线程的资源是独立分配的,不会“夺走”主线程已经使用的资源。主线程和其他线程都是共享进程的整体资源(例如内存、CPU 时间),但各自有独立的栈和调度上下文。
    • 也就是说,主线程并不会因此而“少”了什么资源,新线程所使用的资源是额外分配的。
  • CPU 使用情况

    • 阻塞状态下的线程
      当一个线程调用 Accept() 或其他阻塞调用等待网络连接时,它处于阻塞状态,此时该线程不会占用 CPU 资源,因为操作系统会将其挂起,直到有事件发生(例如有新的连接到来)时再唤醒。
    • CPU 占用
      CPU 是根据线程的活动状态来调度的。一个空闲或阻塞的线程不会频繁地占用 CPU 时间,仅在被唤醒、开始执行代码时才会真正使用 CPU。
    • 因此,虽然线程总是会占用一定的内存和系统资源,但在等待状态下,它们不会造成 CPU 的持续高负载。
  • 资源浪费与优化

    • 资源占用的成本
      如果每个等待操作都创建一个新的线程,确实会占用一定的内存和内核对象等资源。在高并发的场景下,如果不加控制,大量线程可能会带来额外的资源消耗
    • 常用优化手段
      • 线程池:可以复用已有的线程,避免频繁创建和销毁线程,从而降低开销。
      • 异步 I/O 模型:比如 .NET 中的 async/await、I/O 完成端口(IOCP)等机制,可以在不为每个等待操作专门分配一个线程的情况下,高效地处理大量并发连接。
        这些优化方式能更高效地利用系统资源,尤其是在需要同时处理大量连接时。

脱离了unity写在vs里的那些代码都是服务端的Socket的配置

服务端需要可以receive多个和自己建立了连接的客户端Socket Send的消息,

而且要可以同时同时收到,不因为Socket的accept方法的长时间等待而阻塞主线程,所以要

运用线程的知识,额外开线程,while等待accept,而收消息的话,则也可以通过再开一个线程来不停receive新的字节流

发送数据(Send 方法):

  • 当客户端调用 socket.Send("你好") 时,它会把字符串 "你好"(经过编码成字节数组)写入该 Socket 的发送缓冲区,并通过 TCP 连接发送出去。
  • 数据的目的地是已经与这个 Socket 建立连接的远程端点(例如服务端的某个 Socket 对象),而不是直接指定的某个 IP 地址。连接在建立时就已经确定了通信的双方

数据传输:

  • TCP 协议会负责将发送的数据从客户端传输到服务端。这一过程包括数据包的封装、网络传输、重传(如果出现丢包)等机制,确保数据能够可靠地到达。

接收数据(Receive 方法):

  • 服务端必须在合适的时机调用 socket.Receive(或其他类似方法)来读取其接收缓冲区中的数据。
  • 如果服务端没有调用 Receive,那么客户端发送的数据将一直在服务端的接收缓冲区中等待,直到服务端读取或者缓冲区满(可能导致网络拥塞或数据丢失)。

这是在写服务端的逻辑

绑定(Bind):将 Socket 绑定到一个特定的 IP 地址和端口(这里是 127.0.0.1:8080),使得该 Socket 可以接收发送到这个地址端口连接请求

监听(Listen):将 Socket 设置为监听状态,等待客户端的连接

接受连接(Accept):阻塞等待并接受一个客户端

            Socket socket=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
            socket.Bind(ipPoint);
            socket.Listen(1000);
            socket.Accept();

客户端与服务端在技术实现上都使用 Socket,但它们在连接建立前后的行为和责任不同。

数据交换上是双向的,双方都是端点,只不过在连接建立的过程中,一个主动发起连接(客户端),另一个被动等待(服务端)。

如果觉得直接使用 Socket 太复杂,.NET 提供了更高级的封装类,比如 TcpClientTcpListenerUdpClient,它们在底层依然使用 Socket,但对开发者隐藏了部分复杂性,让常见的操作更加直观和易用。

使用 Socket,可以对传输过程中的各种细节(比如发送缓冲区大小、超时时间、是否采用非阻塞模式等)进行精细的控制。这在开发高性能网络应用或处理特殊网络需求时非常重要。

TCP 等协议是面向连接的,必须有一个明确的连接建立、数据传输和连接关闭的过程。Socket API 提供了 bind、listen、accept、connect 等方法来帮助管理这些状态

每个客户端连接会创建一个独立的 Socket 来进行后续的通信。这样设计有助于同时管理多个并发连接,而不是所有数据都混在一起。

Socket 不仅仅是发送数据,它还负责管理连接、维护传输状态、处理数据包的拆分与组装、错误处理、以及网络拥塞控制等。这些都是简单的 IP 地址类无法涵盖的。

如果不调用 Listen,即使绑定了地址和端口,Socket 也不会主动接受任何连接请求

结合多线程知识点实现服务端服务多个客户
1.允许多个客户端连入服务端
2.可以分别和多个客户端进行通信

服务端需要做的事情
1.创建套接字Socket
2.用Bind方法将套接字与本地地址绑定
3.用Listen方法监听
4.用Accept方法等待客户端连接
5.建立连接,Accept返回新套接字
6.用Send和Receive相关方法收发据
7.用shutdown方法释放连接
8.关闭套接字

服务端不需要和unity有关系,直接在vs里新建项目就可以了

套接字Socket

--

二进制的持久化,自己写一个持久化的基类,唐老师讲的这个解决方法,大概是说,

一个基类BaseData

里面有int float bool string (需要的可以自己加list和Dic)的转字符串方法

里面内置了一个总的 字符串writing成字节数组,以及 总的 字符串reading成字节数组

继承了这个基类的各个自己写的类,则是统一把 自己这个实例的数据转字节数组 和 把字节数组转成自己这个实例写成了自己的方法

添加上了泛型以让不同的类之间的 转换方法不同,

熟练利用了ref对index的改造,用泛型接住了需要转换的类型是什么,然后调用这个同样是basedata子类的类里的reading方法,写到这个引用类型的byte数组里,而ref下的index,同样会在其他类如果包含自己的情况下,让自己也能像一个int float 那样正常读成对象

这里用的真巧妙,list和dic按这样来也的确可以自定义了,也不需要什么别的,

既可以写在这个基类里面,每个继承该类的子类都可以当list 或者dic成基本的int float bool一样,需要的是自己定个逻辑遍历里面的所有数据,

还有就是要带上泛型而已,这样存和取也分的明白

这样看来,为了存取可以分明白 ,特意写成两个分开的writing 和reading的各个基本类型的使用,强调了这两个步骤的分开,其实也不坏

BitConverter解决除字符串外的所有类型的转换

字节数组转非字符串类型   --关键类 BitConverter

最基本的写法,自己的每一个成员变量都写一个转字节数组的方法,但这里可以优化,可以用泛型和反射----但这里依然是使用了正常的类型单独转换,一个类型对应一个转 字节数组的的函数

字符串一般先存长度再存对应的 字符串转的字节数组

所以这两个类在网络通信中很重要,担任了二进制格式化的任务

我们不会使用BinaryFormatter类来进行数据的序列化和反序列化
因为客户端和服务端的开发语言大多数情况下是不同的
BinaryFormatter类序列化的数据无法兼容其它语言

网络通信的最终目的,是数据的通讯

网页有默认端口号

比如百度,IP地址有两个,但是域名的别名就没有

主机别名(Alias) 指的是某个域名的 别名

IP别名不见得都有,甚至一般都没有

async 方法和普通方法几乎一样,唯一不同点,它支持 await,可以暂停执行等待异步任务完成后继续执行

Task 线程修饰后的这个异步函数,就可以await 该函数

async void MyMethod1() // ❌ 不推荐
{
    await Task.Delay(1000);
    Console.WriteLine("MyMethod1 执行完成");
}

async Task MyMethod2() // ✅ 推荐
{
    await Task.Delay(1000);
    Console.WriteLine("MyMethod2 执行完成");
}

async Task<int> MyMethod3() // ✅ 适用于返回值
{
    await Task.Delay(1000);
    return 100;
}

async void Start()
{
    MyMethod1(); // 无法 await,可能会导致意外行为
    await MyMethod2(); // 正确
    int result = await MyMethod3();
    Console.WriteLine(result);
}

Task<T> 表示有返回值的异步方法(例如 Task<int>

Task 代表异步操作让调用者知道该方法 不是立即返回值

一般异步方法都需要 TaskTask<T> 作为返回类型

await 不是返回值,而是 让异步任务暂停,等待它完成后继续执行

域名解析       域名到IP地址的转换过程。

域名的解析工作由DNS服务器完成      进行通信时有时会有需求通过域名获取IP

端口类(也带了IP)

IP类

唉,看到了一些可悲的命运差距,也讲了一些本不该由我讲的东西,不过我既然如此的渺小和轻,我对这些事情投入关注点,关注那些所谓的自我要求,也变得不重要了,,在这种天生差距之前,这些担心有什么用呢,,我还疑惑着犹豫着不敢去试自己想试的,这样真的在这种大是大非之前,是多么的轻啊,,,只试自己想试的,,可是我又。。,灰色空间的问题晾一边先

visualStudio当服务端,unity当客户端

代码示例(最基础的,仅仅是连接上了,而一些问题的解决,比如信息的封装啊,序列化,分包黏包等等,都先不管,能连上再说)

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace SocketServerExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 定义服务器 IP 和端口(这里用回环地址和 8080 端口)
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            int port = 8080;
            IPEndPoint localEndPoint = new IPEndPoint(ip, port);

            // 创建一个 TCP Socket
            Socket listener = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                // 绑定并开始监听
                listener.Bind(localEndPoint);
                listener.Listen(10);
                Console.WriteLine("服务器已启动,等待客户端连接...");

                // 阻塞等待客户端连接
                Socket handler = listener.Accept();

                // 接收客户端发送的数据
                byte[] buffer = new byte[1024];
                int bytesRec = handler.Receive(buffer);
                string data = Encoding.UTF8.GetString(buffer, 0, bytesRec);
                Console.WriteLine("接收到客户端数据: " + data);

                // 向客户端发送响应数据
                string reply = "你好,客户端,我是服务器";
                byte[] msg = Encoding.UTF8.GetBytes(reply);
                handler.Send(msg);

                // 优雅地关闭连接
                handler.Shutdown(SocketShutdown.Both);
                handler.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("发生异常:" + ex.ToString());
            }

            Console.WriteLine("按任意键退出...");
            Console.ReadKey();
        }
    }
}

unity中的客户端

using UnityEngine;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class SocketClient : MonoBehaviour
{
    // 设置服务器的 IP 和端口(必须和服务端一致)
    public string serverIP = "127.0.0.1";
    public int serverPort = 8080;
    private Socket clientSocket;

    void Start()
    {
        // 建立连接最好在 Start 中调用(注意:同步调用可能会导致主线程短暂阻塞,
        // 实际项目中建议使用异步或线程处理网络通信)
        ConnectToServer();
    }

    void ConnectToServer()
    {
        try
        {
            // 解析服务器地址
            IPAddress ip = IPAddress.Parse(serverIP);
            IPEndPoint remoteEP = new IPEndPoint(ip, serverPort);

            // 创建一个 TCP Socket 并连接到服务器
            clientSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            clientSocket.Connect(remoteEP);
            Debug.Log("成功连接到服务器");

            // 发送数据到服务器
            string message = "你好,服务器,我是 Unity 客户端";
            byte[] msg = Encoding.UTF8.GetBytes(message);
            clientSocket.Send(msg);
            Debug.Log("已发送数据: " + message);

            // 接收来自服务器的响应(这里直接调用 Receive,会阻塞直到收到数据)
            byte[] buffer = new byte[1024];
            int bytesRec = clientSocket.Receive(buffer);
            string response = Encoding.UTF8.GetString(buffer, 0, bytesRec);
            Debug.Log("接收到服务器响应: " + response);

            // 关闭连接
            clientSocket.Shutdown(SocketShutdown.Both);
            clientSocket.Close();
        }
        catch (Exception ex)
        {
            Debug.LogError("连接异常: " + ex.ToString());
        }
    }
}


http://www.kler.cn/a/534967.html

相关文章:

  • uniapp访问django目录中的图片和视频,2025[最新]中间件访问方式
  • 剑指 Offer II 014. 字符串中的变位词
  • 设计模式——策略模式
  • Docker 国内最新可用镜像源20250205
  • react18新增了哪些特性
  • 【Uniapp-Vue3】创建DB schema数据表结构
  • 深入浅出 DeepSeek V2 高效的MoE语言模型
  • 2.6学习总结
  • 概念AIGC
  • 56. Uboot移植实验
  • 【银河麒麟高级服务器操作系统】系统日志Call trace现象分析及处理全流程
  • Redis持久化-秒杀系统设计
  • flappy-bird-gymnasium
  • 【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途
  • 深入理解 Unix Shell 管道 Pipes:基础和高级用法 xargs tee awk sed等(中英双语)
  • 第二节 程序设计的基本结构
  • 无人机在铁路隧道检查应用技术详解
  • DeepSeek之python实现API应用
  • 【LLM运用】在Ubuntu上Cosyvoice的部署
  • java异常分类,异常处理,面试中常见异常问题!
  • Java并发面试题(题目来源JavaGuide)
  • 算法设计与分析三级项目--管道铺设系统
  • css-根据不同后端返回值返回渲染不同的div样式以及公共组件设定
  • Spring JDBC模块解析 -深入SqlParameterSource
  • 论文解读 | NeurIPS'24 Spotlight ChronoMagic-Bench 评估文本到视频生成的质变幅度评估基准...
  • B站自研的第二代视频连麦系统(上)