2025-05-07 Unity 网络基础7——TCP异步通信

文章目录

  • [1 同步与异步](#1 同步与异步)
  • [2 常用异步方法](#2 常用异步方法)
    • [2.1 Beign / End 方法](#2.1 Beign / End 方法)
      • [BeginConnect() / EndConnect()](#BeginConnect() / EndConnect())
      • [BeginAccept() / EndAccept()](#BeginAccept() / EndAccept())
      • [BeginSend() / EndSend()](#BeginSend() / EndSend())
      • [BeginReceive() / EndReceive()](#BeginReceive() / EndReceive())
    • [2.2 Async 方法](#2.2 Async 方法)
  • [3 实战](#3 实战)
    • [3.1 服务端配置](#3.1 服务端配置)
      • [3.1.1 ClientSocket.cs](#3.1.1 ClientSocket.cs)
      • [3.1.2 ServerSocket.cs](#3.1.2 ServerSocket.cs)
      • [3.1.3 Program.cs](#3.1.3 Program.cs)
    • [3.2 客户端配置](#3.2 客户端配置)
      • [3.2.1 NetAsyncMgr.cs](#3.2.1 NetAsyncMgr.cs)
      • [3.2.2 MainAsync.cs](#3.2.2 MainAsync.cs)
      • [3.2.3 Lesson13.cs](#3.2.3 Lesson13.cs)
    • [3.3 代码](#3.3 代码)
  • [4 测试](#4 测试)

1 同步与异步

  • 同步方法

    方法中逻辑执行完毕后,再继续执行后面的方法。

  • 异步方法

    方法中逻辑可能还没有执行完毕,就继续执行后面的内容。

    往往异步方法当中都会使用多线程执行某部分逻辑,因为不需要等待方法中逻辑执行完毕就可以继续执行下面的逻辑。

注意

​ Unity 中协同程序中的某些异步方法,有的使用的是多线程,有的使用的是迭代器分步执行。

2 常用异步方法

2.1 Beign / End 方法

IAsyncResult

IAsyncResult 接口由包含可异步作的方法的类实现,是启动异步作的方法的返回类型。

​ 当异步调用完成时,将向 WaitHandle 发出信号,可以通过调用 WaitOne 方法来等待它。

  • AsyncState:调用异步方法时传入的参数,需要转换为传入的参数类型。
  • AsyncWaitHandle:用于同步等待。
  • CompletedSynchronously:异步操作是否同步完成。
  • IsCompleted:异步操作是否完成。

BeginConnect() / EndConnect()

BeginConnect()

​ 当调用BeginConnect方法时,该方法立即返回一个IAsyncResult对象,而不会等待连接完成。调用者可以继续执行其他操作,直到连接操作完成,然后通过EndConnect方法获取连接结果。

  • remoteEP:表示远程主机的终结点(EndPoint)。
  • callback:表示异步操作完成时要调用的回调方法。
  • state:表示与异步操作关联的状态对象,可以是任何对象(通常传递 Socket 实例)。
  • address:表示远程主机的 IP 地址。
  • port:表示远程主机的端口号。
  • requestCallback:表示异步操作完成时要调用的回调方法。
  • host:表示远程主机的域名或 IP 地址。

EndConnect()

​ 通常与BeginConnect方法成对使用。

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        var socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        socketTcp.BeginAccept(AcceptCallBack, socketTcp);
        
        var ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socketTcp.BeginConnect(ipPoint, result =>
        {
            Socket s = result.AsyncState as Socket;
            
            try
            {
                s.EndConnect(result);
                print("连接成功!");
            }
            catch (SocketException e)
            {
                print("连接出错:" + e.SocketErrorCode + e.Message);
            }

        }, socketTcp);
    }
    
    private void AcceptCallBack(IAsyncResult result)
    {
        ...
    }
}

BeginAccept() / EndAccept()

BeginAccept()

​ 异步开始接受传入的连接请求,避免阻塞主线程。需传入回调函数和状态对象(通常是原始 Socket 实例)。

  • callback:连接建立后的回调函数。
  • state:状态对象(通常是原始 Socket 实例)。
  • receiveSize:缓冲区大小。
  • acceptSocket:接受套接字。

EndAccept()

​ 在回调函数中调用,用于完成异步连接操作并返回新的客户端 Socket。

​ 会返回一个新的 Socket 实例,用于与客户端通信。

  • buffer:输出参数,用于接收客户端发送的数据。这个缓冲区的大小应该足够大,以容纳客户端发送的数据。
  • asyncResultIAsyncResult对象,表示异步操作的状态。
  • bytesTransferred:输出参数,用于返回实际传输的字节数。
csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        var socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        ...
            
        socketTcp.BeginAccept(AcceptCallBack, socketTcp);
    }
    
    private void AcceptCallBack(IAsyncResult result)
    {
        try
        {
            // 获取传入的参数
            Socket s = result.AsyncState as Socket;
            
            // 通过调用 EndAccept 得到连入的客户端 Socket
            Socket clientSocket = s.EndAccept(result);

            // do something with clientSocket
            // ...
        }
        catch (SocketException e)
        {
            print(e.SocketErrorCode);
        }
    }
}

BeginSend() / EndSend()

BeginSend()

  • buffer:接收数据的缓冲区。
  • offset:缓冲区中开始接收数据的偏移量。
  • size:要接收的最大字节数。
  • socketFlags:控制接收操作的标志。
  • errorCode:输出参数,表示操作的结果。
  • callback:异步操作完成时调用的回调方法。
  • state:与异步操作关联的用户状态对象。

EndSend()

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        var bytes = Encoding.UTF8.GetBytes("1231231231223123123");
        socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
        {
            try
            {
                socketTcp.EndSend(result);
                print("发送成功");
            }
            catch (SocketException e)
            {
                print("发送错误" + e.SocketErrorCode + e.Message);
            }
        }, socketTcp);
    }
}

BeginReceive() / EndReceive()

BeginReceive()

EndReceive()

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        socketTcp.BeginReceive(_resultBytes, 0, _resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);

    }
    
    private void ReceiveCallBack(IAsyncResult result)
    {
        try
        {
            Socket s = result.AsyncState as Socket;
            
            // 这个返回值是你受到了多少个字节
            int num = s.EndReceive(result);
            
            // 进行消息处理
            Encoding.UTF8.GetString(_resultBytes, 0, num);

            // 我还要继续接受
            s.BeginReceive(_resultBytes, 0, _resultBytes.Length, SocketFlags.None, ReceiveCallBack, s);
        }
        catch (SocketException e)
        {
            print("接受消息处问题" + e.SocketErrorCode + e.Message);
        }
    }
}

2.2 Async 方法

​ Async 方法相比于 Begin / End 方法,参数相对少一些。主要参数配置集中在 SocketAsyncEventArgs 中,该类封装了异步操作所需的参数和结果。

ConnectAsync()

  • SocketTypeProtocolType参数用于指定 Socket 的类型和协议。
  • SocketAsyncEventArgs对象e包含了连接所需的参数。
csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
        e2.Completed += (socket, args) =>
        {
            if (args.SocketError == SocketError.Success)
            {
                //连接成功
            }
            else
            {
                //连接失败
                print(args.SocketError);
            }
        };
        socketTcp.ConnectAsync(e2);
    }
}

AcceptAsync()

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        var e = new SocketAsyncEventArgs();
        e.Completed += (socket, args) =>
        {
            //首先判断是否成功
            if (args.SocketError == SocketError.Success)
            {
                //获取连入的客户端socket
                Socket clientSocket = args.AcceptSocket;

                (socket as Socket).AcceptAsync(args);
            }
            else
            {
                print("连入客户端失败" + args.SocketError);
            }
        };
        socketTcp.AcceptAsync(e);
    }
}

SendAsync()

​ 发送前需要调用 SocketAsyncEventArgsSetBuffer() 方法设置发送数组。

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        var e3     = new SocketAsyncEventArgs();
        var bytes2 = Encoding.UTF8.GetBytes("123123的就是拉法基萨克两地分居");
        e3.SetBuffer(bytes2, 0, bytes2.Length);
        e3.Completed += (socket, args) =>
        {
            if (args.SocketError == SocketError.Success)
            {
                print("发送成功");
            }
            else
            { }
        };
        socketTcp.SendAsync(e3);
    }
}

ReceiveAsync()

​ 接收也需要调用 SocketAsyncEventArgsSetBuffer() 方法设置接收到哪个数组中。

csharp 复制代码
public class Lesson12 : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        ...
        
        var e4 = new SocketAsyncEventArgs();

        // 设置接受数据的容器,偏移位置,容量
        e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);
        e4.Completed += (socket, args) =>
        {
            if (args.SocketError == SocketError.Success)
            {
                // 收取存储在容器当中的字节
                // Buffer 是容器
                // BytesTransferred 是收取了多少个字节
                Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);

                // 接收完消息 再接收下一条
                args.SetBuffer(0, args.Buffer.Length);
                (socket as Socket).ReceiveAsync(args);
            }
            else
            { }
        };
        socketTcp.ReceiveAsync(e4);
    }
}

3 实战

3.1 服务端配置

​ 依次创建如下 3 个脚本:

  • ClientSocket.cs
  • ServerSocket.cs
  • Program.cs

3.1.1 ClientSocket.cs

  1. 异步连接管理:自动为每个客户端连接分配唯一 ID,并立即开始异步接收消息。
  2. 异步消息发送:提供 Send() 方法异步发送字符串消息到服务器。
  3. 异步消息接收:通过回调机制持续接收服务器发送的消息。
  4. 错误处理:捕获并处理套接字操作中的异常。

1)成员变量

csharp 复制代码
private static int _BeginId = 1;  // 静态ID计数器
public Socket Client { get; private set; }  // 底层Socket对象
public int Id { get; set; }  // 客户端唯一标识
private byte[] _buffer = new byte[1024];  // 接收缓冲区
private int _bufferOffset = 0;  // 缓冲区偏移量

2)构造函数

​ 构造函数接收一个已连接的 Socket 对象,为其分配 ID,并立即开始异步接收数据。

csharp 复制代码
public ClientSocket(Socket client)
{
    Client = client;
    Id = _BeginId++;  // 分配唯一ID
    
    // 立即开始异步接收消息
    Client.BeginReceive(_buffer, _bufferOffset, _buffer.Length - _bufferOffset, 
                       SocketFlags.None, ReceiveCallback, Client);
}

3)消息发送

Send()方法将字符串编码为 UTF-8 字节数组,然后异步发送。

csharp 复制代码
public void Send(string message)
{
    try
    {
        var bytes = Encoding.UTF8.GetBytes(message);
        Client.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, Client);
    }
    catch (SocketException e)
    {
        Console.WriteLine($"客户端 Id 发送消息时发生错误:e.Message");
    }
}

4)发送回调

​ 发送完成后调用此回调,处理发送结果。

csharp 复制代码
private void SendCallback(IAsyncResult ar)
{
    try
    {
        var client = (Socket) ar.AsyncState!;
        client.EndSend(ar);  // 完成异步发送
    }
    catch (SocketException e)
    {
        Console.WriteLine($"客户端 Id 发送消息时发生错误:e.Message");
    }
}

5)接收回调

​ 这是核心的接收逻辑,处理接收到的数据并保持持续接收状态。

csharp 复制代码
private void ReceiveCallback(IAsyncResult ar)
{
    try
    {
        var client = (Socket) ar.AsyncState!;
        _bufferOffset = client.EndReceive(ar);  // 完成接收
        
        // 处理消息
        var message = Encoding.UTF8.GetString(_buffer, 0, _bufferOffset);
        Console.WriteLine($"收到客户端 {Id} 的消息:{message}");
        
        _bufferOffset = 0;  // 重置缓冲区
        
        if (client.Connected)
        {
            // 继续接收下一条消息
            client.BeginReceive(_buffer, _bufferOffset, _buffer.Length - _bufferOffset, 
                              SocketFlags.None, ReceiveCallback, client);
        }
        else
        {
            Console.WriteLine($"客户端 {Id} 断开连接!");
        }
    }
    catch (SocketException e)
    {
        Console.WriteLine($"客户端 {Id} 接收消息时发生错误:{e.Message}");
    }
}

3.1.2 ServerSocket.cs

  1. 服务器管理:创建并管理 TCP 服务器套接字,监听指定 IP 和端口
  2. 客户端连接处理:异步接受客户端连接,为每个连接创建ClientSocket实例
  3. 消息广播:向所有已连接客户端发送消息
  4. 客户端管理:使用字典存储所有连接的客户端套接字

1)成员变量

csharp 复制代码
public Socket Server { get; private set; }  // 服务器Socket对象
private Dictionary<int, ClientSocket> _clientSockets = new();  // 存储所有客户端连接

2)启动服务器

Start()方法初始化服务器 Socket,绑定到指定 IP 和端口,并开始监听连接请求。

csharp 复制代码
public void Start(string ip, int port, int num)
{
    Server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    var ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
    
    Server.Bind(ipPoint);
    Server.Listen(num);  // 设置最大挂起连接数
    
    Server.BeginAccept(AcceptCallback, Server);  // 开始异步接受连接
}

3)接受客户端连接

​ 这是核心的异步连接处理逻辑,使用回调机制处理新连接。

csharp 复制代码
private void AcceptCallback(IAsyncResult ar)
{
    var server = (Socket) ar.AsyncState!;
    var client = server.EndAccept(ar);  // 完成异步接受
    
    var clientSocket = new ClientSocket(client);  // 创建客户端Socket包装
    _clientSockets.Add(clientSocket.Id, clientSocket);  // 存储客户端
    
    Console.WriteLine($"客户端 {clientSocket.Id} 连接成功!");
    
    server.BeginAccept(AcceptCallback, server);  // 继续接受新连接
}

4)消息广播

​ 该方法遍历所有已连接客户端,发送相同消息。

csharp 复制代码
public void BroadCast(string msg)
{
    foreach (var clientSocket in _clientSockets.Values)
    {
        clientSocket.Send(msg);  // 向每个客户端发送消息
    }
}

3.1.3 Program.cs

  1. 服务器启动:初始化并启动 TCP 服务器。
  2. 交互式控制:通过控制台输入管理服务器。
  3. 消息广播:向所有连接客户端发送消息。
  4. 退出机制:提供优雅关闭服务器的选项。

1)服务器初始化

  • 创建ServerSocket实例。
  • 启动服务器监听本地回环地址(127.0.0.1)的 8080 端口。
  • 设置最大挂起连接数为 10。
  • 输出启动信息。
csharp 复制代码
var serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080, 10);
Console.WriteLine("服务器已启动,等待客户端连接...");

2)主控制循环

​ 这是一个无限循环,等待控制台输入并处理命令:

  1. exit 命令:退出循环,结束程序。
  2. B:前缀消息:广播消息给所有客户端(去除"B:"前缀)。
  3. 其他输入:当前版本忽略。
csharp 复制代码
while (true)
{
    var input = Console.ReadLine();
    if (input == "exit")
    {
        break;
    }
    else if (input?[..2] == "B:")
    {
        serverSocket.BroadCast(input[2..]);
    }
}

3)使用说明

  1. 启动服务器:运行程序即启动服务器。
  2. 广播消息:输入"B:消息内容"广播给所有客户端。
  3. 退出服务器:输入"exit"关闭服务器。

3.2 客户端配置

​ 依次创建如下 3 个脚本:

  • NetAsyncMgr.cs
  • MainAsync.cs
  • Lesson13.cs

3.2.1 NetAsyncMgr.cs

​ 实现 Unity 异步 Socket 网络管理器,用于处理与服务器的 TCP 连接、消息发送和接收。

  1. 单例模式 :通过Instance属性确保全局唯一访问点。
  2. 异步连接 :使用ConnectAsync实现非阻塞连接。
  3. 消息收发:支持异步发送和接收 UTF-8 编码的字符串消息。
  4. 连接管理:提供连接状态检查和关闭方法。

1)初始化与连接

​ 使用SocketAsyncEventArgs实现异步连接,连接完成后触发ConnectCallback回调。

csharp 复制代码
public void Connect(string ip, int port)
{
    var ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
    var args = new SocketAsyncEventArgs();
    args.RemoteEndPoint = ipPoint;
    args.Completed += ConnectCallback;
    _socket.ConnectAsync(args);
}

2)消息发送

​ 将字符串编码为 UTF-8 字节数组后异步发送,发送完成后触发SendCallback

csharp 复制代码
public void Send(string msg)
{
    var bytes = Encoding.UTF8.GetBytes(msg);
    var args = new SocketAsyncEventArgs();
    args.SetBuffer(bytes, 0, bytes.Length);
    args.Completed += SendCallback;
    _socket.SendAsync(args);
}

3)消息接收

​ 使用循环接收模式,每次接收完成后立即开始下一次接收。

csharp 复制代码
private void ReceiveCallback(object sender, SocketAsyncEventArgs e)
{
    print(Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred));
    socket.ReceiveAsync(e); // 继续接收下一条消息
}

3.2.2 MainAsync.cs

​ MainAsync.cs 用于初始化并连接网络管理器 NetAsyncMgr。

  1. 网络管理器初始化:确保NetAsyncMgr单例实例存在。
  2. 自动连接服务器:启动时自动连接本地 8080 端口。

1)网络管理器初始化

​ 这段代码实现了"懒加载"模式:

  • 检查NetAsyncMgr单例是否存在。
  • 如不存在则创建新的 GameObject 并附加NetAsyncMgr组件。
csharp 复制代码
if (NetAsyncMgr.Instance == null)
{
    var go = new GameObject("NetAsyncMgr");
    go.AddComponent<NetAsyncMgr>();
}

2)服务器连接

​ 使用单例实例连接本地服务器 (127.0.0.1) 的 8080 端口。

csharp 复制代码
NetAsyncMgr.Instance.Connect("127.0.0.1", 8080);

3.2.3 Lesson13.cs

  1. UI 组件绑定
    • BtnSend:发送按钮。
    • TMP_InputField:文本输入框(使用 TextMeshPro 实现)。
  2. 消息发送逻辑
    • Start()中注册按钮点击事件。
    • 点击按钮时调用NetAsyncMgr.Instance.Send()发送输入框内容。

3.3 代码

服务端

csharp 复制代码
// ------------------------------------------------------------
// @file       ClientSocket.cs
// ------------------------------------------------------------

namespace NetLearningTcpServerAsync;

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

public class ClientSocket
{
    private static int _BeginId = 1;

    public Socket Client { get; private set; }

    public int Id { get; set; }

    private byte[] _buffer       = new byte[1024];
    private int    _bufferOffset = 0;

    public ClientSocket(Socket client)
    {
        Client = client;
        Id     = _BeginId++;

        // 开始收消息
        Client.BeginReceive(_buffer, _bufferOffset, _buffer.Length - _bufferOffset, SocketFlags.None, ReceiveCallback, Client);
    }

    public void Send(string message)
    {
        try
        {
            var bytes = Encoding.UTF8.GetBytes(message);
            Client.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, Client);
        }
        catch (SocketException e)
        {
            Console.WriteLine($"客户端 Id 发送消息时发生错误:e.Message");
        }
    }

    private void SendCallback(IAsyncResult ar)
    {
        try
        {
            var client = (Socket) ar.AsyncState!;
            client.EndSend(ar);
        }
        catch (SocketException e)
        {
            Console.WriteLine($"客户端 Id 发送消息时发生错误:e.Message");
        }
    }

    private void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            var client = (Socket) ar.AsyncState!;
            _bufferOffset = client.EndReceive(ar);

            // 处理收到的消息
            var message = Encoding.UTF8.GetString(_buffer, 0, _bufferOffset);
            Console.WriteLine($"收到客户端 {Id} 的消息:{message}");

            // 清空缓冲区
            _bufferOffset = 0;

            if (client.Connected)
            {
                // 继续接收消息
                client.BeginReceive(_buffer, _bufferOffset, _buffer.Length - _bufferOffset, SocketFlags.None, ReceiveCallback, client);
            }
            else
            {
                Console.WriteLine($"客户端 {Id} 断开连接!");
            }
        }
        catch (SocketException e)
        {
            Console.WriteLine($"客户端 {Id} 接收消息时发生错误:{e.Message}");
        }
    }
}
csharp 复制代码
// ------------------------------------------------------------
// @file       ServerSocket.cs
// ------------------------------------------------------------

namespace NetLearningTcpServerAsync;

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

public class ServerSocket
{
    public Socket Server { get; private set; }

    private Dictionary<int, ClientSocket> _clientSockets = new();

    public void Start(string ip, int port, int num)
    {
        Server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        var ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        try
        {
            Server.Bind(ipPoint);
            Server.Listen(num);

            Server.BeginAccept(AcceptCallback, Server);
        }
        catch (SocketException e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    public void BroadCast(string msg)
    {
        foreach (var clientSocket in _clientSockets.Values)
        {
            clientSocket.Send(msg);
        }
    }
    
    private void AcceptCallback(IAsyncResult ar)
    {
        try
        {
            var server = (Socket) ar.AsyncState!;
            var client = server.EndAccept(ar);

            var clientSocket = new ClientSocket(client);
            _clientSockets.Add(clientSocket.Id, clientSocket);
            
            Console.WriteLine($"客户端 {clientSocket.Id} 连接成功!");

            // 继续让别的客户端连入
            server.BeginAccept(AcceptCallback, server);
        }
        catch (SocketException e)
        {
            Console.WriteLine(e);
            throw;
        }
    }
}
csharp 复制代码
// ------------------------------------------------------------
// @file       Program.cs
// ------------------------------------------------------------

using NetLearningTcpServerAsync;

var serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080, 10);
Console.WriteLine("服务器已启动,等待客户端连接...");

while (true)
{
    var input = Console.ReadLine();
    if (input == "exit")
    {
        break;
    }
    else if (input?[..2] == "B:")
    {
        serverSocket.BroadCast(input[2..]);
    }
}

客户端

csharp 复制代码
// ------------------------------------------------------------
// @file       NetAsyncMgr.cs
// ------------------------------------------------------------

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

public class NetAsyncMgr : MonoBehaviour
{
    public static NetAsyncMgr Instance { get; private set; }

    private Socket _socket; // 连接服务器的 Socket

    private byte[] _buffer       = new byte[1024 * 1024];
    private int    _bufferOffset = 0;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    }

    public void Connect(string ip, int port)
    {
        if (_socket != null && _socket.Connected)
        {
            return;
        }

        var ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        var args = new SocketAsyncEventArgs();
        args.RemoteEndPoint =  ipPoint;
        args.Completed      += ConnectCallback;
        _socket.ConnectAsync(args);
    }

    public void Send(string msg)
    {
        if (_socket == null || !_socket.Connected)
        {
            return;
        }

        var bytes = Encoding.UTF8.GetBytes(msg);

        var args = new SocketAsyncEventArgs();
        args.SetBuffer(bytes, 0, bytes.Length);
        args.Completed += SendCallback;
        _socket.SendAsync(args);
    }

    public void Close()
    {
        if (_socket != null)
        {
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Disconnect(false);
            _socket.Close();
            _socket = null;
        }
    }

    private void SendCallback(object sender, SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            Debug.Log("Send Success");
        }
        else
        {
            Debug.Log("Send Failed: " + e.SocketError);
            Close();
        }
    }

    private void ConnectCallback(object sender, SocketAsyncEventArgs eventArgs)
    {
        if (eventArgs.SocketError == SocketError.Success)
        {
            Debug.Log("Connect Success");

            var socket = sender as Socket;

            if (socket == null || !socket.Connected)
            {
                return;
            }

            // TODO: 连接成功后,开始接收数据
            var args2 = new SocketAsyncEventArgs();
            args2.SetBuffer(_buffer, 0, _buffer.Length);
            args2.Completed += ReceiveCallback;
            socket.ReceiveAsync(args2);
        }
        else
        {
            Debug.Log("Connect Failed: " + eventArgs.SocketError);
        }
    }

    private void ReceiveCallback(object sender, SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            print(Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred));

            // TODO: 处理接收到的数据

            var socket = sender as Socket;

            if (socket == null || !socket.Connected)
            {
                return;
            }

            // 继续接收数据
            socket.ReceiveAsync(e);
        }
        else
        {
            print("接受消息出错:" + e.SocketError);
            Close();
        }
    }
}
csharp 复制代码
// ------------------------------------------------------------
// @file       MainAsync.cs
// ------------------------------------------------------------

using UnityEngine;

public class MainAsync : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        if (NetAsyncMgr.Instance == null)
        {
            var go = new GameObject("NetAsyncMgr");
            go.AddComponent<NetAsyncMgr>();
        }
        
        NetAsyncMgr.Instance.Connect("127.0.0.1", 8080);
    }
}
csharp 复制代码
// ------------------------------------------------------------
// @file       Lesson13.cs
// ------------------------------------------------------------

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class Lesson13 : MonoBehaviour
{
    public Button         BtnSend;
    public TMP_InputField TxtMessage;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        BtnSend.onClick.AddListener(() =>
        {
            NetAsyncMgr.Instance.Send(TxtMessage.text);
        });
    }
}

4 测试

  1. Unity 中创建新场景,新建空物体 Main,将 MainAsync.cs 脚本挂载上去。
  1. 创建 Canvas 画布,添加 InputField 和 Button 并关联至 Lesson13.cs。
  1. 首先运行服务器。
  1. 服务器启动后,运行 Unity。服务器显示连接成功。

    输入命令"B:Hello!",回车。

  1. Unity 中接收到消息并输出。
  1. Unity 中在 InputField 中输入文字,点击发送。
  1. 服务器中接收到了消息并打印输出。

​ 到此,完成了服务器和客户端的双向通信。

相关推荐
惜.己1 小时前
appium中urllib3.exceptions.LocationValueError: No host specified. 的错误解决办法
网络·appium
吉凶以情迁1 小时前
window服务相关问题探索 go语言服务开发探索调试
linux·服务器·开发语言·网络·golang
专注VB编程开发20年1 小时前
UDP受限广播地址255.255.255.255的通信机制详解
网络·udp·智能路由器
189228048612 小时前
NX947NX955美光固态闪存NX962NX966
大数据·服务器·网络·人工智能·科技
Sadsvit3 小时前
Linux 进程管理与计划任务
linux·服务器·网络
一碗白开水一4 小时前
【模型细节】FPN经典网络模型 (Feature Pyramid Networks)详解及其变形优化
网络·人工智能·pytorch·深度学习·计算机视觉
什么都想学的阿超4 小时前
【网络与爬虫 38】Apify全栈指南:从0到1构建企业级自动化爬虫平台
网络·爬虫·自动化
D-海漠5 小时前
安全光幕Muting功能程序逻辑设计
服务器·网络·人工智能
都给我6 小时前
可计算存储(Computational Storage)与DPU(Data Processing Unit)的技术特点对比及实际应用场景分析
运维·服务器·网络·云计算
阿蒙Amon7 小时前
详解Python标准库之互联网数据处理
网络·数据库·python