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

2025-03-25 Unity 网络基础4——TCP同步通信

文章目录

  • 1 Socket
    • 1.1 Socket 类型
    • 1.2 构造 Socket
    • 1.3 常用属性
    • 1.4 常用方法
  • 2 TCP 通信
    • 2.1 服务端配置
    • 2.2 客户端配置
    • 2.3 进行通信
    • 2.4 多设备通信
  • 3 区分消息

1 Socket

​ Socket 是 C# 提供的网络通信类(其它语言也有对应的 Socket 类),是支持 TCP/IP 网络通信的基本操作单位。

  • 类名:Socket
  • 命名空间:System.Net.Sockets

​ 一个套接字对象包含以下关键信息:

  1. 本机的 IP 地址和端口。
  2. 对方主机的 IP 地址和端口。
  3. 双方通信的协议信息。

​ 一个 Sccket 对象表示一个本地或者远程套接字信息,可被视为一个数据通道,连接与客户端和服务端,数据的发送和接受均通过这个通道进行。

​ 一般长连接游戏会使用 Socket 套接字作为通信方案。

1.1 Socket 类型

​ Socket 套接字有 3 种不同的类型:

  1. 流套接字

    主要用于实现 TCP 通信,提供面向连接、可靠的、有序的、数据无差错且无重复的数据传输服务。

  2. 数据报套接字

    主要用于实现 UDP 通信,提供无连接的通信服务,数据包长度不能大于 32KB,不提供正确性检查,不保证顺序,可能出现重发、丢失等情况。

  3. 原始套接字(不常用)

    主要用于实现 IP 数据包通信,用于直接访问协议的较低层,常用于侦听和分析数据包。

1.2 构造 Socket

public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType);

  • 参数 1:AddressFamily

    网络寻址 枚举类型,决定寻址方案。

    • InterNetwork:IPv4 寻址(常用)
    • InterNetwork6:IPv6 寻址(常用)
    • UNIX:UNIX 本地到主机地址
    • ImpLink:ARPANETIMP 地址
    • Ipx:IPX 或 SPX 地址
    • Iso:ISO 协议的地址
    • Osi:OSI 协议的地址
    • NetBios:NetBios 地址
    • Atm:本机 ATM 服务地址
  • 参数 2:SocketType

    套接字枚举类型,决定使用的套接字类型。

    • Dgram:支持数据报,最大长度固定的无连接、不可靠的消息(常用,主要用于 UDP 通信)
    • Stream:支持可靠、双向、基于连接的字节流(常用,主要用于 TCP 通信)
    • Raw:支持对基础传输协议的访问
    • Rdm:支持无连接、面向消息、以可靠方式发送的消息
    • Seqpacket:提供排序字节流的面向连接且可靠的双向传输
  • 参数 3:ProtocolType

    协议类型枚举类型,决定套接字使用的通信协议。

    • TCP:TCP 传输控制协议(常用)
    • UDP:UDP 用户数据报协议(常用)
    • IP:IP 网际协议
    • Icmp:Icmp 网际消息控制协议
    • Igmp:Igmp 网际组管理协议
    • Ggp:网关到网关协议
    • IPv4:Internet 协议版本 4
    • Pup:PARC 通用数据包协议
    • Idp:Internet 数据报协议
    • Raw:原始 IP 数据包协议
    • Ipx:Internet 数据包交换协议
    • Spx:顺序包交换协议
    • IcmpV6:用于 IPv6 的 Internet 控制消息协议

参数 2、3 的常用搭配:

  1. SocketType.Dgram + ProtocolType.Udp = UDP 协议通信(常用)
  2. SocketType.Stream + ProtocolType.Tcp = TCP 协议通信(常用)
  3. SocketType.Raw + ProtocolType.Icmp = Internet 控制报文协议
  4. SocketType.Raw + ProtocolType.Raw = 简单 IP 包通信
// TCP 流套接字
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

// UDP 数据报套接字
Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

1.3 常用属性

// 1.套接字的连接状态
if (socketTcp.Connected)
{ }

// 2.获取套接字的类型
print(socketTcp.SocketType);

// 3.获取套接字的协议类型
print(socketTcp.ProtocolType);

// 4.获取套接字的寻址方案
print(socketTcp.AddressFamily);

// 5.从网络中获取准备读取的数据数据量
print(socketTcp.Available);

// 6.获取本机 EndPoint 对象(IPEndPoint 继承 EndPoint)
// socketTcp.LocalEndPoint as IPEndPoint

// 7.获取远程 EndPoint 对象
// socketTcp.RemoteEndPoint as IPEndPoint

1.4 常用方法

  1. 主要用于服务端
// 1-1: 绑定IP和端口
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);

// 1-2: 设置客户端连接的最大数量
socketTcp.Listen(10);

// 1-3: 等待客户端连入
socketTcp.Accept();
  1. 主要用于客户端
// 2-1: 连接远程服务端
socketTcp.Connect(IPAddress.Parse("118.12.123.11"), 8080);
  1. 客户端服务端都会用
// 3-1: 同步发送和接收数据
// socketTcp.Send(...);
// socketTcp.Receive(...);

// 3-2: 异步发送和接收数据
// socketTcp.SendAsync(...);
// socketTcp.ReceiveAsync(...);

// 3-3: 释放连接并关闭 Socket,先于 Close 调用
socketTcp.Shutdown(SocketShutdown.Both);

// 3-4: 关闭连接,释放所有Socket关联资源
socketTcp.Close();

2 TCP 通信

2.1 服务端配置

​ 以 Rider IDE 为例,创建控制台程序。

image-20250321155739669
  1. 创建 TCP 套接字
// 创建一个TCP套接字
var socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
    // 创建一个IP地址和端口号的终结点
    var ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080); // 填写服务器本机的 IP 地址和端口号
    // 绑定套接字到指定的终结点
    socketTcp.Bind(ipPoint);
}
catch (Exception e)
{
    // 如果绑定失败,输出错误信息
    Console.WriteLine("绑定报错:" + e);
    return;
}
  1. 连接客户端
// 开始监听连接
socketTcp.Listen(1024);
Console.WriteLine("服务器已启动,等待客户端连接");

// 接受客户端连接
var socketClient = socketTcp.Accept(); // Accept() 会同步等待连接
Console.WriteLine("客户端已连接");
  1. 发送消息并等待回复
// 向客户端发送消息
socketClient.Send(Encoding.UTF8.GetBytes("你好,客户端!"));

// 接收客户端发送的消息
var result = new byte[1024];
var receiveLength = socketClient.Receive(result);
  1. 输出接受内容并中断连接
// 输出客户端发送的消息
Console.WriteLine($"客户端 {socketClient.RemoteEndPoint} 发送的消息:" + Encoding.UTF8.GetString(result, 0, receiveLength));

// 关闭套接字
socketClient.Shutdown(SocketShutdown.Both);
socketClient.Close();

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

2.2 客户端配置

​ 进入 Unity,创建脚本并挂载到场景上。

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

public class Lesson6 : MonoBehaviour
{
    private void Start()
    {
        ... // 编写代码
    }
}
image-20250321164944693
  1. 与服务器建立连接
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

try // 使用 try 块包裹防止连接失败报错
{
    socket.Connect("127.0.0.1", 8080); // 填写服务器的 ip 地址和端口号
}
catch (SocketException e) // 网络通信异常
{
    if (e.ErrorCode == 10061) // 10061 错误码表示服务器拒绝连接
    {
        Debug.Log("服务器拒绝连接");
    }
    else
    {
        Debug.Log("连接失败");
    }

    return;
}
  1. 接受与发送消息
var receiveBytes = new byte[1024];
var receiveLength = socket.Receive(receiveBytes); // 接收一条消息后才继续工作
print("接收到数据:" + System.Text.Encoding.UTF8.GetString(receiveBytes, 0, receiveLength));

socket.Send(Encoding.UTF8.GetBytes("Hello World!"));
  1. 断开连接
socket.Shutdown(SocketShutdown.Both);
socket.Close();

2.3 进行通信

​ 首先运行服务器。

image-20250321164715953

​ 进入 Unity,点击运行,可看到通信结果。

image-20250321165031472

​ 服务器端输出结果如下:

image-20250321165111589

2.4 多设备通信

Socket.Accept() 方法会阻塞当前线程,直至接收到设备通信为止。因此,上述方法只能与一台指定设备进行通信。

​ 为实现多设备通信,需创建新线程监听客户端的连接。

class Program
{
    // TCP 套接字
    private static Socket _SocketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    // 客户端套接字列表
    private static List<Socket> _ClientSockets = new List<Socket>();

    // 运行标志
    private static bool _Running = false;
    
    ...
}

​ Main 函数的处理步骤包括以下 4 步:

  1. 绑定 IP 和端口,开始监听
var ipAddress = IPAddress.Parse("127.0.0.1");
var port = 8080;
var endPoint = new IPEndPoint(ipAddress, port);
_SocketTcp.Bind(endPoint);
_SocketTcp.Listen(1024);
_Running = true;
  1. 启动线程等待客户端连接
var acceptThread = new Thread(AcceptClientThread);
acceptThread.Start();
  1. 启动线程接收客户端消息
var receiveThread = new Thread(ReceiveMessageThread);
receiveThread.Start();
  1. 主线程处理用户输入

    这里规定,输入“exit”退出服务器,输入“send”向所有客户端发送消息。

while (true)
{
    var input = Console.ReadLine();
    if (input == "exit") // 输入命令关闭服务器
    {
        _Running = false;
        for (int i = 0; i < _ClientSockets.Count; i++)
        {
            _ClientSockets[i].Shutdown(SocketShutdown.Both);
            _ClientSockets[i].Close();
        }
        _ClientSockets.Clear();
        break;
    }
    else if (input == "send") // 输入命令向所有客户端发送消息
    {
        for (int i = 0; i < _ClientSockets.Count; i++)
        {
            _ClientSockets[i].Send("Hello, client!"u8.ToArray());
            Console.WriteLine("Send Hello");
        }
    }
}

​ 连接客户端 AcceptClientThread、接受消息 ReceiveMessage 的线程工作如下:

private static void AcceptClientThread()
{
    while (_Running)
    {
        var clientSocket = _SocketTcp.Accept();
        _ClientSockets.Add(clientSocket);
        clientSocket.Send("Welcome to the server!"u8.ToArray()); // 由于客户端规定为接收一条消息后才继续工作,因此这里需要发送一条消息
    }
}

// 接收客户端消息的线程
private static void ReceiveMessageThread()
{
    var buffer = new byte[1024 * 1024];
    int receiveLength;
    Socket clientSocket;

    while (_Running)
    {
        for (int i = 0; i < _ClientSockets.Count; i++)
        {
            clientSocket = _ClientSockets[i];

            // 判断是否有可接收的消息
            if (clientSocket.Available > 0)
            {
                receiveLength = clientSocket.Receive(buffer);

                // 使用线程池处理接收到的消息,而不是立即处理
                // 防止用户等待时间过长
                ThreadPool.QueueUserWorkItem(ReceiveMessage, (clientSocket, Encoding.UTF8.GetString(buffer, 0, receiveLength)));
            }
        }
    }
}

​ 在 ReceiveMessage 中,使用线程池处理接收到的消息,而不是立即处理,防止用户等待时间过长。接收消息后的工作通过 ReceiveMessage 方法定义:

// 处理接收到的消息
private static void ReceiveMessage(object? state)
{
    if (state == null) return;

    (Socket socket, string str) info = ((Socket socket, string str)) state;
    Console.WriteLine($"Receive message from client {info.socket}: {info.str}");
}

3 区分消息

​ 数据对象序列化后是长度不同的字节数组,将它们发送出去后,对方如何区分是什么消息?如何选择对应的数据类进行反序列化?

解决方案

​ 为发送的信息添加标识 ID。

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

实践

  1. 定义消息接口。

    public interface INetMessage
    {
        int MessageId { get; }
    
        int BytesLength { get; }
    
        byte[] ToBytes();
    
        int FromBytes(byte[] bytes, int index);
    }
    
  2. 创建消息类型

    public class PlayerMessage : INetMessage
    {
        public int    PlayerId;
        public string Name;
        public int    Atk;
        public int    Lev;
    
        public int MessageId { get => 1001; }
    
        public int BytesLength
        {
            get => this.GetBytesLength(MessageId) + // 消息长度
                   this.GetBytesLength(PlayerId) +
                   this.GetBytesLength(Name) +
                   this.GetBytesLength(Atk) +
                   this.GetBytesLength(Lev);
        }
    
        public byte[] ToBytes()
        {
            var bytes = new byte[BytesLength];
            var index = 0;
            index = this.Write(bytes, index, MessageId);
            index = this.Write(bytes, index, PlayerId);
            index = this.Write(bytes, index, Name);
            index = this.Write(bytes, index, Atk);
            index = this.Write(bytes, index, Lev);
            return bytes;
        }
    
        public int FromBytes(byte[] bytes, int index)
        {
            // 反序列化不需要解析 Id,在此之前应解析 Id 从而使用该方法
            index = this.Read(bytes, index, ref PlayerId);
            index = this.Read(bytes, index, ref Name);
            index = this.Read(bytes, index, ref Atk);
            index = this.Read(bytes, index, ref Lev);
            return index;
        }
    
        public override string ToString()
        {
            return $"PlayerMessage: {PlayerId}, {Name}, {Atk}, {Lev}";
        }
    }
    

    其中的 GetBytesLengthWriteRead 方法均由拓展类提供:

    public static class ByteLengthExtension
    {
        public static int GetBytesLength(this INetMessage message, int value)
        {
            return sizeof(int);
        }
    
        public static int GetBytesLength(this INetMessage message, string value)
        {
            return sizeof(int) + Encoding.UTF8.GetByteCount(value);
        }
    
        public static int GetBytesLength(this INetMessage message, bool value)
        {
            return sizeof(bool);
        }
    
        public static int GetBytesLength(this INetMessage message, float value)
        {
            return sizeof(float);
        }
    }
    
    public static class INetMessageExtension
    {
        public static int Write(this INetMessage message, byte[] bytes, int index, int value)
        {
            BitConverter.GetBytes(value).CopyTo(bytes, index);
            return index + sizeof(int);
        }
    
        public static int Read(this INetMessage message, byte[] bytes, int index, ref int value)
        {
            value = BitConverter.ToInt32(bytes, index);
            return index + sizeof(int);
        }
    
        public static int Write(this INetMessage message, byte[] bytes, int index, string value)
        {
            var strBytes = Encoding.UTF8.GetBytes(value);
    
            BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);
            index += sizeof(int);
    
            strBytes.CopyTo(bytes, index);
            return index + strBytes.Length;
        }
    
        public static int Read(this INetMessage message, byte[] bytes, int index, ref string value)
        {
            int length = BitConverter.ToInt32(bytes, index);
            index += sizeof(int);
    
            value = Encoding.UTF8.GetString(bytes, index, length);
            return index + length;
        }
    
        public static int Write(this INetMessage message, byte[] bytes, int index, bool value)
        {
            BitConverter.GetBytes(value).CopyTo(bytes, index);
            return index + sizeof(bool);
        }
    
        public static int Read(this INetMessage message, byte[] bytes, int index, ref bool value)
        {
            value = BitConverter.ToBoolean(bytes, index);
            return index + sizeof(bool);
        }
    
        public static int Write(this INetMessage message, byte[] bytes, int index, float value)
        {
            BitConverter.GetBytes(value).CopyTo(bytes, index);
            return index + sizeof(float);
        }
    
        public static int Read(this INetMessage message, byte[] bytes, int index, ref float value)
        {
            value = BitConverter.ToSingle(bytes, index);
            return index + sizeof(float);
        }
    
        public static int Write(this INetMessage message, byte[] bytes, int index, INetMessage value)
        {
            value.ToBytes().CopyTo(bytes, index);
            return index + value.BytesLength;
        }
    
        public static int Read(this INetMessage message, byte[] bytes, int index, ref INetMessage value)
        {
            value.FromBytes(bytes, index);
            return index + value.BytesLength;
        }
    }
    
  3. 创建消息类型 PlayerMessage

    public class PlayerMessage : INetMessage
    {
        public int    PlayerId;
        public string Name;
        public int    Atk;
        public int    Lev;
    
        public int MessageId { get => 1001; }
    
        public int BytesLength
        {
            get => this.GetBytesLength(MessageId) + // 消息长度
                   this.GetBytesLength(PlayerId) +
                   this.GetBytesLength(Name) +
                   this.GetBytesLength(Atk) +
                   this.GetBytesLength(Lev);
        }
    
        public byte[] ToBytes()
        {
            var bytes = new byte[BytesLength];
            var index = 0;
            index = this.Write(bytes, index, MessageId);
            index = this.Write(bytes, index, PlayerId);
            index = this.Write(bytes, index, Name);
            index = this.Write(bytes, index, Atk);
            index = this.Write(bytes, index, Lev);
            return bytes;
        }
    
        public int FromBytes(byte[] bytes, int index)
        {
            // 反序列化不需要解析 Id,在此之前应解析 Id 从而使用该方法
            index = this.Read(bytes, index, ref PlayerId);
            index = this.Read(bytes, index, ref Name);
            index = this.Read(bytes, index, ref Atk);
            index = this.Read(bytes, index, ref Lev);
            return index;
        }
    
        public override string ToString()
        {
            return $"PlayerMessage: {PlayerId}, {Name}, {Atk}, {Lev}";
        }
    }
    
  4. 进行通信。

    客户端:

    public class Lesson6 : MonoBehaviour
    {
        private void Start()
        {
            // 创建一个 Socket 对象,指定地址族、套接字类型和协议类型
            var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
            try // 使用 try 块包裹防止连接失败报错
            {
                socket.Connect("127.0.0.1", 8080); // 填写服务器的 ip 地址和端口号
            }
            catch (SocketException e) // 网络通信异常
            {
                if (e.ErrorCode == 10061) // 10061 错误码表示服务器拒绝连接
                {
                    Debug.Log("服务器拒绝连接");
                }
                else
                {
                    Debug.Log("连接失败");
                }
    
                return;
            }
    
            var receiveBytes = new byte[1024];
            var receiveLength = socket.Receive(receiveBytes); // 接收一条消息后才继续工作
    
            // 解析 Id
            var id = BitConverter.ToInt32(receiveBytes, 0);
            switch (id)
            {
                case 1001:
                    var playerMsg = new PlayerMessage();
                    playerMsg.FromBytes(receiveBytes, sizeof(int));
                    Debug.Log(playerMsg);
                    break;
            }
    
            // print("接收到数据:" + System.Text.Encoding.UTF8.GetString(receiveBytes, 0, receiveLength));
    
            socket.Send(Encoding.UTF8.GetBytes("Hello World!"));
    
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
        }
    }
    

    服务端:

    // See https://aka.ms/new-console-template for more information
    
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using Exercise;
    
    // 创建一个TCP套接字
    var socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    try
    {
        // 创建一个IP地址和端口号的终结点
        var ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080); // 填写服务器本机的 IP 地址和端口号
        // 绑定套接字到指定的终结点
        socketTcp.Bind(ipPoint);
    }
    catch (Exception e)
    {
        // 如果绑定失败,输出错误信息
        Console.WriteLine("绑定报错:" + e);
        return;
    }
    
    // 开始监听连接
    socketTcp.Listen(1024);
    Console.WriteLine("服务器已启动,等待客户端连接");
    
    // 接受客户端连接
    var socketClient = socketTcp.Accept();
    Console.WriteLine("客户端已连接");
    
    // 向客户端发送消息
    // socketClient.Send(Encoding.UTF8.GetBytes("你好,客户端!"));
    var playerMsg = new PlayerMessage()
    {
        PlayerId = 1,
        Name = "zheliku",
        Atk = 5,
        Lev = 10,
    };
    socketClient.Send(playerMsg.ToBytes());
    
    // 接收客户端发送的消息
    var result = new byte[1024];
    var receiveLength = socketClient.Receive(result);
    
    // 输出客户端发送的消息
    Console.WriteLine($"客户端 {socketClient.RemoteEndPoint} 发送的消息:" + Encoding.UTF8.GetString(result, 0, receiveLength));
    
    // 关闭套接字
    socketClient.Shutdown(SocketShutdown.Both);
    socketClient.Close();
    
    Console.WriteLine("按任意键退出");
    Console.ReadKey();
    

​ 先运行服务器,后运行 Unity,可看到通信成功:

image-20250325041231533

相关文章:

  • ElasticSearch常用优化点
  • React第三十一章(组件实战)
  • git使用经验(一)
  • 【数据结构初阶八大排序】---冒泡、选择、插入、希尔、堆排、快排、归并、计数
  • C语言【文件操作】详解上(会使用fopen和fclose打开和关闭文件)
  • OCR 识别案例
  • Java爬虫|爬虫爬jj榜单数据写入excel
  • ngx_http_escape_location_name
  • ETL与数据迁移:从传统系统到云平台的平稳过渡全攻略
  • 【C++网络编程】第5篇:UDP与广播通信
  • Java面试总结+算法
  • 适合各个层次的 7 个计算机视觉项目【1】:植物病害检测
  • Mamba 模型:深度学习序列建模的新突破​
  • s1: Simple test-time scaling 【论文阅读笔记】
  • 麒麟系统运维指令
  • 怎么快速部署Sock5代理到ubuntu云服务器
  • Dify本地安装部署笔记
  • Windows数字取证中ActivityCache(活动缓存)
  • 【计算机网络中的奈氏准则与香农定理】
  • Vscode HTML5新增元素及属性
  • 一般的电脑培训班要多少钱/seo中介平台
  • 淘宝客怎么做自己的网站/网络广告策划案例
  • 深圳好的网站建/市场推广方式有哪几种
  • 网站建设项目介绍/网站网络排名优化方法
  • 帝国cms做企业网站/西安搜建站科技网站
  • 山东省一带一路建设网站/哔哩哔哩b站在线看免费