23.TCP同步区分消息类型

  1. 23.网络通信-套接字Socket-TCP通信-同步-区分消息类型
    1. 23.1 知识点
      1. 如何发送之前的自定义类的二进制信息
        1. 问题:当将序列化的二进制数据发送给对象时,对方如何区分?
      2. 如何区分消息类型
        1. 解决方案:
        2. 举例说明:
      3. 实践区分消息类型
        1. 创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性
        2. 让想要被发送的消息继承消息基类,实现序列化反序列化方法
        3. 修改客户端和服务端收发消息的逻辑
        4. 服务端
        5. 客户端
      4. 总结
    2. 23.2 知识点代码
      1. BaseMessage
      2. PlayerData
      3. PlayerMessage
      4. Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型服务端
      5. Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型
    3. 23.3 练习题
      1. 修改之前的服务端综合练习2和客户端综合练习,让他们收发的消息都是区分了消息类型的BaseMessage
        1. 在TcpNetManager把存储收发消息的队列都改成BaseMessage类型,同时收发方法也改成BaseMessage类型
        2. 客户端主脚本点击发送按钮时改成发送PlayerMessage这个自定义数据结构
        3. ClientSocket收发消息改成使用BaseMessage类型
        4. ServerSocket对客户端广播消息改成广播BaseMessage类型的消息
        5. 服务端主脚本监听到输入B:1001命令,对客户单进行广播一个自定义PlayerMessage 类
    4. 23.4 练习题代码
      1. TcpNetManager
      2. Lesson23_练习题
      3. ClientSocket
      4. ServerSocket
      5. Lesson23_练习题服务端

23.网络通信-套接字Socket-TCP通信-同步-区分消息类型


23.1 知识点

如何发送之前的自定义类的二进制信息

  • 继承 BaseData
  • 实现其中的序列化、反序列化、获取字节数等相关方法
  • 发送自定义类数据时,进行序列化
  • 接收自定义类数据时,进行反序列化

问题:当将序列化的二进制数据发送给对象时,对方如何区分?

举例:

  • PlayerInfo: 玩家信息
  • ChatInfo: 聊天信息
  • LoginInfo: 登录信息
  • 等等

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

如何区分消息类型

解决方案:

为发送的信息添加标识,比如添加消息 ID。在所有发送的消息的头部加上消息 ID(可以是 int、short、byte、long,根据实际情况选择)。

举例说明:

消息构成:

  • 如果选用 int 类型作为消息 ID 的类型
  • 前 4 个字节为消息 ID
  • 后面的字节为数据类的内容

这样每次收到消息时,先把前 4 个字节取出来解析为消息 ID,再根据 ID 进行消息反序列化即可。

实践区分消息类型

创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性

public class BaseMessage : BaseData
{
    public override int GetBytesNum()
    {
        throw new System.NotImplementedException();
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        throw new System.NotImplementedException();
    }

    public override byte[] Writing()
    {
        throw new System.NotImplementedException();
    }

    public virtual int GetID()
    {
        return 0;
    }
}

让想要被发送的消息继承消息基类,实现序列化反序列化方法

/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData//0
{
    //1
    public string name;
    public int atk;
    public int lev;


    //2
    public override int GetBytesNum()
    {
        int num = 0;
        num += 4 + Encoding.UTF8.GetBytes(name).Length;//name
        num += 4;//atk
        num += 4;//lev
        return num;
    }

    //4
    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        int index = beginIndex;
        name = ReadString(bytes, ref index);
        atk = ReadInt(bytes, ref index);
        lev = ReadInt(bytes, ref index);
        return index - beginIndex;
    }

    //3
    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteString(bytes, name, ref index);
        WriteInt(bytes, atk, ref index);
        WriteInt(bytes, lev, ref index);
        return bytes;
    }
}


public class PlayerMessage : BaseMessage
{
    //成员变量
    public int playerID;
    public PlayerData playerData;
    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        //先写消息ID
        WriteInt(bytes, GetID(), ref index);
        //写这个消息的成员变量
        WriteInt(bytes, playerID, ref index);
        WriteData(bytes, playerData, ref index);
        return bytes;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        //反序列化不需要去解析ID 因为在这一步之前 就应该把ID反序列化出来
        //用来判断到底使用哪一个自定义类来反序化
        int index = beginIndex;
        playerID = ReadInt(bytes, ref index);
        playerData = ReadData<PlayerData>(bytes, ref index);
        return index - beginIndex;
    }

    public override int GetBytesNum()
    {
        return 4 + //消息ID的长度
                4 + //playerID的字节数组长度
                playerData.GetBytesNum();//playerData的字节数组长度
    }

    /// <summary>
    /// 自定义的消息ID 主要用于区分是哪一个消息类
    /// </summary>
    /// <returns></returns>
    public override int GetID()
    {
        return 1001;
    }
}

修改客户端和服务端收发消息的逻辑

服务端

//发送
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 666;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "我是韬老狮的服务端";
playerMessage.playerData.atk = 99;
playerMessage.playerData.lev = 50;

socketClient.Send(playerMessage.Writing());


//发送字符串转成的字节数组给客户端
socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));

客户端

//接收数据 
//声明接收数据字节数组
byte[] receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
int receiveNum = socketTcp.Receive(receiveBytes);

//首先解析消息的ID
//使用字节数组中的前四个字节 得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
switch (msgID)
{
    case 1001:
        PlayerMessage playerMessage = new PlayerMessage();
        playerMessage.Reading(receiveBytes, 4);
        print(playerMessage.playerID);
        print(playerMessage.playerData.name);
        print(playerMessage.playerData.atk);
        print(playerMessage.playerData.lev);
        break;
}

//重新声明接收数据字节数组 接收字符串 服务端分了两次发 我们也要分两次接
receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
receiveNum = socketTcp.Receive(receiveBytes);
print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));

总结

区分消息的关键点是在数据字节数组头部加上消息 ID。只要前后端定义好统一的规则,通过 ID 就可以决定如何反序列化消息,并且可以决定应该如何处理该消息。


23.2 知识点代码

BaseMessage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BaseMessage : BaseData
{
    public override int GetBytesNum()
    {
        throw new System.NotImplementedException();
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        throw new System.NotImplementedException();
    }

    public override byte[] Writing()
    {
        throw new System.NotImplementedException();
    }

    public virtual int GetID()
    {
        return 0;
    }
}

PlayerData

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData//0
{
    //1
    public string name;
    public int atk;
    public int lev;


    //2
    public override int GetBytesNum()
    {
        int num = 0;
        num += 4 + Encoding.UTF8.GetBytes(name).Length;//name
        num += 4;//atk
        num += 4;//lev
        return num;
    }

    //4
    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        int index = beginIndex;
        name = ReadString(bytes, ref index);
        atk = ReadInt(bytes, ref index);
        lev = ReadInt(bytes, ref index);
        return index - beginIndex;
    }

    //3
    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteString(bytes, name, ref index);
        WriteInt(bytes, atk, ref index);
        WriteInt(bytes, lev, ref index);
        return bytes;
    }
}

PlayerMessage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMessage : BaseMessage
{
    //成员变量
    public int playerID;
    public PlayerData playerData;
    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        //先写消息ID
        WriteInt(bytes, GetID(), ref index);
        //写这个消息的成员变量
        WriteInt(bytes, playerID, ref index);
        WriteData(bytes, playerData, ref index);
        return bytes;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        //反序列化不需要去解析ID 因为在这一步之前 就应该把ID反序列化出来
        //用来判断到底使用哪一个自定义类来反序化
        int index = beginIndex;
        playerID = ReadInt(bytes, ref index);
        playerData = ReadData<PlayerData>(bytes, ref index);
        return index - beginIndex;
    }

    public override int GetBytesNum()
    {
        return 4 + //消息ID的长度
                4 + //playerID的字节数组长度
                playerData.GetBytesNum();//playerData的字节数组长度
    }

    /// <summary>
    /// 自定义的消息ID 主要用于区分是哪一个消息类
    /// </summary>
    /// <returns></returns>
    public override int GetID()
    {
        return 1001;
    }
}

Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型服务端

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

namespace Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //1.创建套接字Socket(TCP)
            Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //2.用Bind方法将套接字与本地地址绑定
            try
            {
                IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);//把本机作为服务端程序 IP地址传入本机
                socketTcp.Bind(iPEndPoint);//绑定
            }
            catch (Exception e)
            {
                //如果IP地址不合法或者端口号被占用可能报错
                Console.WriteLine("绑定报错" + e.Message);
                return;
            }

            //3.用Listen方法监听
            socketTcp.Listen(1024);//最大接收1024个客户端
            Console.WriteLine("服务端绑定监听结束,等待客户端连入");

            //4.用Accept方法等待客户端连接

            //5.建立连接,Accept返回新套接字
            Socket socketClient = socketTcp.Accept();
            //Accept是阻塞式的方法 会把主线程卡主 一定要等到客户端接入后才会继续执行后面的代码
            //客户端接入后 返回新的Socket对象 这个新的Socket可以理解为客户段和服务端的通信通道
            Console.WriteLine("有客户端连入了");

            //6.用Send和Receive相关方法收发数据

            //发送
            PlayerMessage playerMessage = new PlayerMessage();
            playerMessage.playerID = 666;
            playerMessage.playerData = new PlayerData();
            playerMessage.playerData.name = "我是韬老狮的服务端";
            playerMessage.playerData.atk = 99;
            playerMessage.playerData.lev = 50;

            socketClient.Send(playerMessage.Writing());


            //发送字符串转成的字节数组给客户端
            socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));


            //声明接受客户端信息的字节数组 声明1024容量代表能接受1kb的信息
            byte[] result = new byte[1024];
            //接受客户端信息 返回值为接受到的字节数
            int receiveNum = socketClient.Receive(result);
            //打印 远程发送信息的客户端的IP和端口 以及 发送过来的字符串
            Console.WriteLine("接受到了{0}发来的消息:{1}",
                socketClient.RemoteEndPoint.ToString(),
                Encoding.UTF8.GetString(result, 0, receiveNum));


            //7.用Shutdown方法释放连接
            //注意断开的是客户段和服务端的通信通道
            socketClient.Shutdown(SocketShutdown.Both);

            //8.关闭套接字
            //注意关闭的是客户段和服务端的通信通道
            socketClient.Close();
        }
    }
}

Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型

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

public class Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 如何发送之前的自定义类的2进制信息

        //1.继承BaseData类
        //2.实现其中的序列化、反序列化、获取字节数等相关方法
        //3.发送自定义类数据时 序列化
        //4.接受自定义类数据时 反序列化

        //抛出问题:
        //当将序列化的2进制数据发送给对象时,对方如何区分?
        //举例:
        //PlayerInfo:玩家信息
        //ChatInfo:聊天信息
        //LoginInfo:登录信息
        //等等
        //这些数据对象序列化后是长度不同的字节数组
        //将它们发送给对象后,对方如何区分出他们分别是什么消息
        //如何选择对应的数据类反序列化它们?

        #endregion

        #region 知识点二 如何区分消息类型

        //解决方案:
        //为发送的信息添加标识,比如添加消息ID
        //在所有发送的消息的头部加上消息ID(int、short、byte、long都可以,根据实际情况选择)

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

        #endregion

        #region 知识点三 实践区分消息类型

        //实践步骤
        //1.创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性
        //2.让想要被发送的消息继承消息基类,实现序列化反序列化方法
        //3.修改客户端和服务端收发消息的逻辑

        //客户端逻辑
        //1.创建套接字Socket Tcp
        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);


        //2.用Connect方法与服务端相连
        //确定服务端的IP和端口 正常来说填的应该是远端服务器的ip地址以及端口号
        //由于只有一台电脑用于测试 本机也当做服务器 所以传入当前电脑的ip地址
        IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        try
        {
            //连接
            socketTcp.Connect(iPEndPoint);
        }
        catch (SocketException e)
        {
            //如果连接没有开启或者服务器异常 会报错 不同的返回码代表不同报错
            if (e.ErrorCode == 10061)
                print("服务器拒绝连接");
            else
                print("连接服务器失败" + e.ErrorCode);
            return;
        }


        //3.用Send和Receive相关方法收发数据

        //接收数据 
        //声明接收数据字节数组
        byte[] receiveBytes = new byte[1024];
        //Receive方法接受数据 返回接收多少字节
        int receiveNum = socketTcp.Receive(receiveBytes);

        //首先解析消息的ID
        //使用字节数组中的前四个字节 得到ID
        int msgID = BitConverter.ToInt32(receiveBytes, 0);
        switch (msgID)
        {
            case 1001:
                PlayerMessage playerMessage = new PlayerMessage();
                playerMessage.Reading(receiveBytes, 4);
                print(playerMessage.playerID);
                print(playerMessage.playerData.name);
                print(playerMessage.playerData.atk);
                print(playerMessage.playerData.lev);
                break;
        }

        //重新声明接收数据字节数组 接收字符串 服务端分了两次发 我们也要分两次接
        receiveBytes = new byte[1024];
        //Receive方法接受数据 返回接收多少字节
        receiveNum = socketTcp.Receive(receiveBytes);
        print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));

        //发送数据
        socketTcp.Send(Encoding.UTF8.GetBytes("你好,我是韬老狮的客户端"));


        //4.用Shutdown方法释放连接
        socketTcp.Shutdown(SocketShutdown.Both);


        //5.关闭套接字
        socketTcp.Close();
        #endregion

        #region 总结
        //区分消息的关键点,是在数据字节数组头部加上消息ID
        //只要前后端定义好统一的规则
        //那么我们可以通过ID来决定如何反序列化消息
        //并且可以决定我们应该如何处理该消息
        #endregion
    }
}

23.3 练习题

修改之前的服务端综合练习2和客户端综合练习,让他们收发的消息都是区分了消息类型的BaseMessage

在TcpNetManager把存储收发消息的队列都改成BaseMessage类型,同时收发方法也改成BaseMessage类型

private Queue<BaseMessage> sendMsgQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储待发送的消息
private Queue<BaseMessage> receiveQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储接收到的消息

// 发送消息
public void Send(BaseMessage baseMessage)
{
    sendMsgQueue.Enqueue(baseMessage); // 将消息添加到发送消息队列
}

// 在独立线程中处理发送消息的逻辑
private void SendMsg(object obj)
{
    while (isConnected) // 只要连接有效
    {
        if (sendMsgQueue.Count > 0) // 如果发送消息队列中有待发送的消息
        {
            // 从队列中取出消息并发送到服务器
            socket.Send(sendMsgQueue.Dequeue().Writing());
        }
    }
}

// 在独立线程中处理接收消息的逻辑
private void ReceiveMsg(object obj)
{
    while (isConnected) // 只要连接有效
    {
        if (socket.Available > 0) // 如果有可接收的数据
        {
            // 接收从服务器发送来的数据,并将数据转换成字符串后存储到接收消息队列
            receiveNum = socket.Receive(receiveBytes);
            //首先把收到字节数组的前4个字节  读取出来得到ID
            int msgID = BitConverter.ToInt32(receiveBytes, 0);
            BaseMessage baseMessage = null;
            switch (msgID)
            {
                case 1001:
                    PlayerMessage playerMessage = new PlayerMessage();
                    playerMessage.Reading(receiveBytes, 4);
                    baseMessage = playerMessage;
                    break;
            }
            //如果消息为空 那证明是不知道类型的消息 没有解析
            if (baseMessage == null)
                continue;
            //收到消息 解析消息为字符串 并放入公共容器
            receiveQueue.Enqueue(baseMessage);
        }
    }
}

void Update()
{
    // 在Unity的每一帧中检查是否有待处理的接收消息,如果有,则打印出来
    if (receiveQueue.Count > 0)
    {
        BaseMessage baseMessage = receiveQueue.Dequeue();
        if (baseMessage is PlayerMessage)
        {
            PlayerMessage playerMessage = (baseMessage as PlayerMessage);
            print(playerMessage.playerID);
            print(playerMessage.playerData.name);
            print(playerMessage.playerData.lev);
            print(playerMessage.playerData.atk);
        }
    }
}

客户端主脚本点击发送按钮时改成发送PlayerMessage这个自定义数据结构

sendButton.onClick.AddListener(() =>
{
    PlayerMessage playerMessage = new PlayerMessage();
    playerMessage.playerID = 1111;
    playerMessage.playerData = new PlayerData();
    playerMessage.playerData.name = "韬老狮客户端发送的信息";
    playerMessage.playerData.atk = 22;
    playerMessage.playerData.lev = 10;
    TcpNetManager.Instance.Send(playerMessage);
});

ClientSocket收发消息改成使用BaseMessage类型

// 发送消息给客户端
public void Send(BaseMessage baseMessage)
{
    if (clientSocket != null)
    {
        try
        {
            clientSocket.Send(baseMessage.Writing());  // 将消息编码为UTF-8字节数组并发送给客户端
        }
        catch (Exception e)
        {
            Console.WriteLine("发消息出错" + e.Message);
            Close();  // 如果发送出现异常,关闭套接字连接
        }
    }
}

// 接收来自客户端的消息
public void Receive()
{
    if (clientSocket == null)
        return;
    try
    {
        if (clientSocket.Available > 0)// 如果套接字中有可读数据
        {
            byte[] result = new byte[1024 * 5];  // 创建一个缓冲区来存储接收到的数据
            int receiveNum = clientSocket.Receive(result);// 从套接字接收数据并存储在缓冲区中

            //收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
            int msgID = BitConverter.ToInt32(result, 0);
            BaseMessage baseMessage = null;
            switch (msgID)
            {
                case 1001:
                    baseMessage = new PlayerMessage();
                    baseMessage.Reading(result, 4);
                    break;
            }
            if (baseMessage == null)
                return;
            ThreadPool.QueueUserWorkItem(HandleMessage, baseMessage);
        }
    }


    catch (Exception e)
    {
        Console.WriteLine("收消息出错" + e.Message);
        Close(); // 如果接收出现异常,关闭套接字连接
    }
}

// 处理接收到的消息
private void HandleMessage(object obj)
{
    BaseMessage baseMessage = obj as BaseMessage;
    if (baseMessage is PlayerMessage)
    {
        PlayerMessage playerMessage = baseMessage as PlayerMessage;
        Console.WriteLine(playerMessage.playerID);
        Console.WriteLine(playerMessage.playerData.name);
        Console.WriteLine(playerMessage.playerData.lev);
        Console.WriteLine(playerMessage.playerData.atk);
    }
}

ServerSocket对客户端广播消息改成广播BaseMessage类型的消息

// 向所有客户端广播消息
public void Broadcast(BaseMessage baseMessage)
{
    foreach (ClientSocket client in clientSocketDictionary.Values)
    {
        client.Send(baseMessage);
    }
}

服务端主脚本监听到输入B:1001命令,对客户单进行广播一个自定义PlayerMessage 类

while (true)
{
    // 从控制台读取用户输入
    string input = Console.ReadLine();

    // 如果用户输入 "Quit",则关闭服务器
    if (input == "Quit")
    {
        serverSocket.Close();
    }
    // 如果用户输入以 "B:" 开头,表示要广播消息给所有客户端
    else if (input.Substring(0, 2) == "B:")
    {
        if (input.Substring(2) == "1001")
        {
            PlayerMessage playerMessage = new PlayerMessage();
            playerMessage.playerID = 9876;
            playerMessage.playerData = new PlayerData();
            playerMessage.playerData.name = "服务器端发来的消息";
            playerMessage.playerData.lev = 99;
            playerMessage.playerData.atk = 80;
            serverSocket.Broadcast(playerMessage);
        }
    }
}

23.4 练习题代码

TcpNetManager

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;

public class TcpNetManager : BaseSingletonInMonoBehaviour<TcpNetManager>
{
    private Socket socket; // 创建Socket对象,用于网络通信
    private Queue<BaseMessage> sendMsgQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储待发送的消息
    private Queue<BaseMessage> receiveQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储接收到的消息
    private byte[] receiveBytes = new byte[1024 * 1024]; // 创建一个字节数组,用于存储接收到的数据
    private int receiveNum; // 用于存储接收到的字节数
    private bool isConnected = false; // 用于标识是否已连接到服务器

    // 连接服务器
    public void Connect(string ip, int port)
    {
        if (isConnected) // 如果已连接,则直接返回
            return;

        if (socket == null) // 如果套接字为空,创建一个套接字对象
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); // 创建一个IP终结点对象
        try
        {
            // 尝试连接到指定的IP地址和端口
            socket.Connect(ipPoint);
            isConnected = true; // 标记已连接
            ThreadPool.QueueUserWorkItem(SendMsg); // 创建并启动发送消息的线程
            ThreadPool.QueueUserWorkItem(ReceiveMsg); // 创建并启动接收消息的线程
        }
        catch (SocketException e)
        {
            if (e.ErrorCode == 10061) // 如果连接被服务器拒绝
                print("服务器拒绝连接");
            else
                print("连接失败" + e.ErrorCode + e.Message); // 打印连接失败的信息
        }
    }

    // 关闭连接
    public void Close()
    {
        if (socket != null) // 如果套接字对象存在
        {
            socket.Shutdown(SocketShutdown.Both); // 关闭套接字的发送和接收
            socket.Close(); // 关闭套接字连接
            isConnected = false; // 标记连接已关闭
        }
    }

    // 当对象被销毁时,确保关闭连接
    private void OnDestroy()
    {
        Close(); // 调用关闭连接的方法
    }

    // 发送消息
    public void Send(BaseMessage baseMessage)
    {
        sendMsgQueue.Enqueue(baseMessage); // 将消息添加到发送消息队列
    }

    // 在独立线程中处理发送消息的逻辑
    private void SendMsg(object obj)
    {
        while (isConnected) // 只要连接有效
        {
            if (sendMsgQueue.Count > 0) // 如果发送消息队列中有待发送的消息
            {
                // 从队列中取出消息并发送到服务器
                socket.Send(sendMsgQueue.Dequeue().Writing());
            }
        }
    }

    // 在独立线程中处理接收消息的逻辑
    private void ReceiveMsg(object obj)
    {
        while (isConnected) // 只要连接有效
        {
            if (socket.Available > 0) // 如果有可接收的数据
            {
                // 接收从服务器发送来的数据,并将数据转换成字符串后存储到接收消息队列
                receiveNum = socket.Receive(receiveBytes);
                //首先把收到字节数组的前4个字节  读取出来得到ID
                int msgID = BitConverter.ToInt32(receiveBytes, 0);
                BaseMessage baseMessage = null;
                switch (msgID)
                {
                    case 1001:
                        PlayerMessage playerMessage = new PlayerMessage();
                        playerMessage.Reading(receiveBytes, 4);
                        baseMessage = playerMessage;
                        break;
                }
                //如果消息为空 那证明是不知道类型的消息 没有解析
                if (baseMessage == null)
                    continue;
                //收到消息 解析消息为字符串 并放入公共容器
                receiveQueue.Enqueue(baseMessage);
            }
        }
    }

    void Update()
    {
        // 在Unity的每一帧中检查是否有待处理的接收消息,如果有,则打印出来
        if (receiveQueue.Count > 0)
        {
            BaseMessage baseMessage = receiveQueue.Dequeue();
            if (baseMessage is PlayerMessage)
            {
                PlayerMessage playerMessage = (baseMessage as PlayerMessage);
                print(playerMessage.playerID);
                print(playerMessage.playerData.name);
                print(playerMessage.playerData.lev);
                print(playerMessage.playerData.atk);
            }
        }
    }
}

Lesson23_练习题

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Lesson23_练习题 : MonoBehaviour
{
    public InputField InputField;
    public Button sendButton;
    void Start()
    {
        TcpNetManager.Instance.Connect("127.0.0.1", 8080);

        sendButton.onClick.AddListener(() =>
        {
            PlayerMessage playerMessage = new PlayerMessage();
            playerMessage.playerID = 1111;
            playerMessage.playerData = new PlayerData();
            playerMessage.playerData.name = "韬老狮客户端发送的信息";
            playerMessage.playerData.atk = 22;
            playerMessage.playerData.lev = 10;
            TcpNetManager.Instance.Send(playerMessage);
        });
    }
}

ClientSocket

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

namespace Lesson23_练习题
{
    class ClientSocket
    {
        private static int CLIENT_BEGIN_ID = 1;  // 静态变量,用于为客户端分配唯一的客户端ID
        public int clientID;  // 客户端的唯一ID
        public Socket clientSocket;  // 与客户端通信的套接字对象

        /// <summary>
        /// 是否是连接状态
        /// </summary>
        public bool isClientConnected => this.clientSocket.Connected;  // 判断套接字是否处于连接状态

        public ClientSocket(Socket clientSocket)
        {
            this.clientID = CLIENT_BEGIN_ID;  // 初始化客户端ID
            this.clientSocket = clientSocket;  // 初始化套接字
            ++CLIENT_BEGIN_ID;  // 为下一个客户端分配不同的ID
        }


        // 关闭套接字连接
        public void Close()
        {
            if (clientSocket != null)
            {
                clientSocket.Shutdown(SocketShutdown.Both);  // 关闭套接字的读写
                clientSocket.Close();  // 关闭套接字连接
                clientSocket = null;
            }
        }

        // 发送消息给客户端
        public void Send(BaseMessage baseMessage)
        {
            if (clientSocket != null)
            {
                try
                {
                    clientSocket.Send(baseMessage.Writing());  // 将消息编码为UTF-8字节数组并发送给客户端
                }
                catch (Exception e)
                {
                    Console.WriteLine("发消息出错" + e.Message);
                    Close();  // 如果发送出现异常,关闭套接字连接
                }
            }
        }

        // 接收来自客户端的消息
        public void Receive()
        {
            if (clientSocket == null)
                return;
            try
            {
                if (clientSocket.Available > 0)// 如果套接字中有可读数据
                {
                    byte[] result = new byte[1024 * 5];  // 创建一个缓冲区来存储接收到的数据
                    int receiveNum = clientSocket.Receive(result);// 从套接字接收数据并存储在缓冲区中

                    //收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
                    int msgID = BitConverter.ToInt32(result, 0);
                    BaseMessage baseMessage = null;
                    switch (msgID)
                    {
                        case 1001:
                            baseMessage = new PlayerMessage();
                            baseMessage.Reading(result, 4);
                            break;
                    }
                    if (baseMessage == null)
                        return;
                    ThreadPool.QueueUserWorkItem(HandleMessage, baseMessage);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("收消息出错" + e.Message);
                Close(); // 如果接收出现异常,关闭套接字连接
            }
        }

        // 处理接收到的消息
        private void HandleMessage(object obj)
        {
            BaseMessage baseMessage = obj as BaseMessage;
            if (baseMessage is PlayerMessage)
            {
                PlayerMessage playerMessage = baseMessage as PlayerMessage;
                Console.WriteLine(playerMessage.playerID);
                Console.WriteLine(playerMessage.playerData.name);
                Console.WriteLine(playerMessage.playerData.lev);
                Console.WriteLine(playerMessage.playerData.atk);
            }
        }
    }
}

ServerSocket

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

namespace Lesson23_练习题
{
    class ServerSocket
    {
        // 服务器端Socket
        public Socket serverSocket;
        // 保存客户端连接的所有Socket的字典
        public Dictionary<int, ClientSocket> clientSocketDictionary = new Dictionary<int, ClientSocket>();

        // 用于标识服务器是否关闭的标志
        private bool isServerClose;

        // 开启服务器端
        public void Start(string ipString, int port, int clientSocketMaxNum)
        {
            // 初始化服务器关闭标志为假
            isServerClose = false;

            // 创建服务器套接字,指定地址族为IPv4、套接字类型为流套接字、协议类型为TCP
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 创建IP终结点,指定IP地址和端口号
            IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ipString), port);

            // 将套接字绑定到指定的IP终结点
            serverSocket.Bind(serverEndPoint);

            // 启动服务器套接字,同时指定同时等待连接的最大客户端数
            serverSocket.Listen(clientSocketMaxNum);

            // 启动线程池中的线程来处理客户端连接请求和消息接收
            ThreadPool.QueueUserWorkItem(AcceptClientConnect);
            ThreadPool.QueueUserWorkItem(ReceiveClientMessage);
        }

        // 关闭服务器端
        public void Close()
        {
            // 设置服务器关闭标志为真
            isServerClose = true;

            // 关闭所有客户端连接
            foreach (ClientSocket client in clientSocketDictionary.Values)
            {
                client.Close();
            }
            clientSocketDictionary.Clear();

            // 关闭服务器套接字的读写
            serverSocket.Shutdown(SocketShutdown.Both);

            // 关闭服务器套接字
            serverSocket.Close();

            // 将服务器套接字设置为null
            serverSocket = null;
        }

        // 接受客户端连接
        private void AcceptClientConnect(object obj)
        {
            while (!isServerClose)
            {
                try
                {
                    // 等待并接受一个客户端连接请求
                    Socket clientSocket = serverSocket.Accept();

                    // 创建一个新的ClientSocket对象来管理客户端连接
                    ClientSocket client = new ClientSocket(clientSocket);

                    // 向客户端发送欢迎消息
                    //client.Send("欢迎连入服务器");

                    // 将客户端Socket对象添加到字典中,以客户端ID作为键
                    clientSocketDictionary.Add(client.clientID, client);
                }
                catch (Exception e)
                {
                    Console.WriteLine("客户端连入报错" + e.Message);
                }
            }
        }

        // 接收客户端消息
        private void ReceiveClientMessage(object obj)
        {
            while (!isServerClose)
            {
                if (clientSocketDictionary.Count > 0)
                {
                    foreach (ClientSocket client in clientSocketDictionary.Values)
                    {
                        // 从每个客户端接收消息
                        client.Receive();
                    }
                }
            }
        }

        // 向所有客户端广播消息
        public void Broadcast(BaseMessage baseMessage)
        {
            foreach (ClientSocket client in clientSocketDictionary.Values)
            {
                client.Send(baseMessage);
            }
        }
    }
}

Lesson23_练习题服务端

namespace Lesson23_练习题
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 创建一个ServerSocket对象,用于处理服务器端的操作
            ServerSocket serverSocket = new ServerSocket();

            // 启动服务器,绑定到本地IP地址 127.0.0.1,监听端口 8080,允许最大连接数为 1024
            serverSocket.Start("127.0.0.1", 8080, 1024);

            // 输出服务器开启成功的消息
            Console.WriteLine("服务器开启成功");

            while (true)
            {
                // 从控制台读取用户输入
                string input = Console.ReadLine();

                // 如果用户输入 "Quit",则关闭服务器
                if (input == "Quit")
                {
                    serverSocket.Close();
                }
                // 如果用户输入以 "B:" 开头,表示要广播消息给所有客户端
                else if (input.Substring(0, 2) == "B:")
                {
                    if (input.Substring(2) == "1001")
                    {
                        PlayerMessage playerMessage = new PlayerMessage();
                        playerMessage.playerID = 9876;
                        playerMessage.playerData = new PlayerData();
                        playerMessage.playerData.name = "服务器端发来的消息";
                        playerMessage.playerData.lev = 99;
                        playerMessage.playerData.atk = 80;
                        serverSocket.Broadcast(playerMessage);
                    }
                }
            }
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏