【Unity网络编程知识】使用Socket实现简单TCP通讯

发布于:2025-03-26 ⋅ 阅读:(20) ⋅ 点赞:(0)

1、Socket的常用属性和方法

创建Socket TCP流套接字

Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

1.1 常用属性

1)套接字的连接状态

socketTcp.Connected

2)获取套接字的类型

socketTcp.SocketType

3)获取套接字的协议类型

socketTcp.ProtocolType

4)获取套接字的寻址方案

socketTcp.AddressFamily

5)从网络中获取准备读取的数据数据量

socketTcp.Available

6)获取本机EndPoint对象(注意:IPEndPoint继承EndPoint)

socketTcp.LocalEndPoint as IPEndPoint

7)获取远程EndPoint对象

socketTcp.RemoteEndPoint as IPEndPoint;

1.2 同步常用方法 

1.2.1 主要用于服务端方法

1)绑定IP和端口, Bind(ip地址和端口)

        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socketTcp.Bind(ipPoint);

2)设置客户端连接的最大数量 , Listen(最大连接数)

socketTcp.Listen(99);

3)等待客户端连入,Accept()

socketTcp.Accept();
1.3.2 主要用于客户端方法

1)连接远程服务端,Connect(ip地址和端口)

socketTcp.Connect(IPAddress.Parse("118.12.123.1"), 8080);
1.4.3 客户端服务端都会用的方法

1)同步发送和接收数据,Send()和Receive()

发送

socketTcp.Send

接收

socketTcp.Receive()

2)释放连接并关闭socket,先于close调用

socketTcp.Shutdown(SocketShutdown.Both);

3)关闭连接,释放所有Socket管理资源

socketTcp.Close();

1.3 异步常用方法

1.3.1 主要用于服务端方法

1)等待客户端连入方式1,BeginAccept和EndAccept

    socketTcp.BeginAccept(AcceptCallback, socketTcp);

    private void AcceptCallback(IAsyncResult result)
    {
        try
        {
            //获取传入参数
            Socket s = result.AsyncState as Socket;
            //通过调用EndAccept就可以得到连入的客户端Socket
            Socket clientSocket = s.EndAccept(result);

            s.BeginAccept(AcceptCallback, s);
        }
        catch (SocketException e)
        {
            print(e.SocketErrorCode);
        }
    }

2)等待客户端连入方式2, AcceptAsync

        SocketAsyncEventArgs e = new SocketAsyncEventArgs();
        e.Completed += (socket, args) =>
        {
            //首先判断是否成功
            if(args.SocketError == SocketError.Success)
            {
                Socket clientSocket = args.AcceptSocket;

                (socket as Socket).AcceptAsync(args);
            }
            else
            {
                print("连入客户端失败" + args.SocketError);
            }
        };
        socketTcp.AcceptAsync(e);
1.3.2 主要用于客户端方法

1)连接远程服务端方式1,BeginConnect和EndConnect

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

2) 连接远程服务端方式2,ConnectAsync

        SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
        IPEndPoint ipPoint2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        e.RemoteEndPoint = ipPoint2;
        e.Completed += (socket, args) =>
        {
            if(args.SocketError == SocketError.Success)
            {
                //连接成功
            }
            else
            {
                //连接失败
                print(args.SocketError);
            }
        };
        socketTcp.ConnectAsync(e2);
1.3.3 客户端服务端都会用的方法

1)接受消息

接受消息方式1,BeginReceive和EndReceive

    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,SendAsync

        SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();
        byte[] bytes2 = Encoding.UTF8.GetBytes("你好呀,好兄弟");
        e3.SetBuffer(bytes2, 0, bytes2.Length);
        e3.Completed += (socket, args) =>
        {
            if(args.SocketError != SocketError.Success)
            {
                print("发送成功");
            }
            else
            {

            }
        };
        socketTcp.SendAsync(e3);

2)发送消息

发送消息方式1,BeginSend和EndSend 

        byte[] bytes = Encoding.UTF8.GetBytes("41414214241");
        socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
        {
            try
            {
                Socket s = result.AsyncState as Socket;
                s.EndSend(result);
            }
            catch (SocketException e)
            {
                print("发送错误" + e.SocketErrorCode + e.Message);
            }
        }, socketTcp);

发送消息方式2, ReceiveAsync

        e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);
        e4.Completed += (socket, args) =>
        {
            if(args.SocketError!= SocketError.Success)
            {
                //收取存储在容器当中的字节
                //Buffer是容器
                //BytesTranferred是收取了多少个字节
                Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);

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

2、Socket实现TCP通讯流程

2.1 服务端

1)创建套接字socket(TCP)

Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

2)用Bind方法将套接字与本地地址绑定

IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);

3)用Listen方法监听

socketTcp.Listen(1024);

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

Socket socketClient = socketTcp.Accept();

5)建立连接,Accept返回新套接字

Socket socketClient = socketTcp.Accept();

6)用send和Receive相关方法收发数据

            //发送
            PlayerMsg msg = new PlayerMsg();
            msg.playerID = 666;
            msg.playerData = new PlayerData();
            msg.playerData.name = "我是NBB服务端";
            msg.playerData.atk = 99;
            msg.playerData.lev = 120;

            socketClient.Send(msg.Writing());
            //接受
            byte[] res = new byte[1024];
            int receiveNum = socketClient.Receive(res);

7)用shutdown方法释放连接

socketClient.Shutdown(SocketShutdown.Both);

8)关闭套接字

socketClient.Close();

2.2 客户端

1)创建套接字socket

Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

2)用connect方法与服务端相连

IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Connect(ipPoint);

3)用send和Receive相关方法收发数据

        //接受数据
        byte[] receiveBytes = new byte[1024];
        int receiveNum = socket.Receive(receiveBytes);

        //首先解析消息的ID
        int msgID = BitConverter.ToInt32(receiveBytes, 0);
        switch (msgID)
        {
            case 1001:
                PlayerMsg msg = new PlayerMsg();
                msg.Reading(receiveBytes, 4);
                Debug.Log(msg.playerID);
                Debug.Log(msg.playerData.name);
                Debug.Log(msg.playerData.atk);
                Debug.Log(msg.playerData.lev);
                break;
            default:
                break;
        }

        Debug.Log("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));

        //发送数据
        socket.Send(Encoding.UTF8.GetBytes("你好,我是客户端"));

4)用shutdown方法释放连接

socket.Shutdown(SocketShutdown.Both);

5)关闭套接字

socket.Close();

3、简单实现TCP通讯完整代码

实现功能:消息区分、分包黏包、心跳消息客户端长时间不发送消息自动断开

3.1 消息类型区分代码

1)BaseData
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

public abstract class BaseData
{
    /// <summary>
    /// 用于子类重写的 获取字节数组容器大小的方法
    /// </summary>
    /// <returns></returns>
    public abstract int GetBytesNum();

    /// <summary>
    /// 把成员变量序列化为 对应的字节数组
    /// </summary>
    /// <returns></returns>
    public abstract byte[] Writing();

    /// <summary>
    /// 把二进制字节数组 反序列化 到成员变量当中
    /// </summary>
    /// <param name="bytes">反序列化使用的字节数组</param>
    /// <param name="beginIndex">从该字节数组的第几个位置开始解析 默认是 0</param>
    public abstract int Reading(byte[] bytes, int beginIndex = 0);

    /// <summary>
    /// 存储int类型变量到指定的字节数组
    /// </summary>
    /// <param name="bytes">指定字节数组</param>
    /// <param name="value">具体的int值</param>
    /// <param name="index">每次存储后用于记录当前索引位置的变量</param>
    protected void WriteInt(byte[] bytes, int value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(int);
    }
    protected void WriteShort(byte[] bytes, short value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(short);
    }
    protected void WriteLong(byte[] bytes, long value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(long);
    }
    protected void WriteFloat(byte[] bytes, float value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(float);
    }
    protected void WriteByte(byte[] bytes, byte value, ref int index)
    {
        bytes[index] = value;
        index += sizeof(byte);
    }
    protected void WriteBool(byte[] bytes, bool value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, (int)index);
        index += sizeof(bool);
    }
    protected void WriteString(byte[] bytes, string value, ref int index)
    {
        //先存储string字节数组长度
        byte[] strBytes = Encoding.UTF8.GetBytes(value);
        int num = strBytes.Length;
        BitConverter.GetBytes(num).CopyTo(bytes, index);
        index += sizeof(int);
        //在存储 string 字节数组
        strBytes.CopyTo(bytes, index);
        index += num;
    }
    protected void WriteData(byte[] bytes, BaseData data, ref int index)
    {
        data.Writing().CopyTo(bytes, index);
        index += data.GetBytesNum();
    }

    /// <summary>
    /// 根据字节数组读取整形
    /// </summary>
    /// <param name="bytes">读取数组</param>
    /// <param name="index">开始读取的索引位置</param>
    /// <returns></returns>
    protected int ReadInt(byte[] bytes, ref int index)
    {
        int value = BitConverter.ToInt32(bytes, index);
        index += sizeof(int);
        return value;
    }
    protected short ReadShort(byte[] bytes, ref int index)
    {
        short value = BitConverter.ToInt16(bytes, index);
        index += sizeof(short);
        return value;
    }
    protected long ReadLong(byte[] bytes, ref int index)
    {
        long value = BitConverter.ToInt64(bytes, index);
        index += sizeof(long);
        return value;
    }
    protected float ReadFloat(byte[] bytes, ref int index)
    {
        float value = BitConverter.ToSingle(bytes, index);
        index += sizeof(float);
        return value;
    }
    protected byte ReadByte(byte[] bytes, ref int index)
    {
        byte b = bytes[index];
        index++;
        return b;
    }
    protected bool ReadBool(byte[] bytes, ref int index)
    {
        bool value = BitConverter.ToBoolean(bytes, index);
        index += sizeof(bool);
        return value;
    }
    protected string ReadString(byte[] bytes, ref int index)
    {
        int length = BitConverter.ToInt32(bytes, index);
        index += sizeof(int);
        string value = Encoding.UTF8.GetString(bytes, index, length);
        index += length;
        return value;
    }
    protected T ReadData<T>(byte[] bytes, ref int index) where T : BaseData, new()
    {
        T value = new T();
        index += value.Reading(bytes, index);
        return value;
    }

}
2)BaseMsg
using System.Collections;
using System.Collections.Generic;

public class BaseMsg : 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;
    }
}
 3)PlayerData
using System.Collections;
using System.Collections.Generic;
using System.Text;

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

    public override int GetBytesNum()
    {
        return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;
    }

    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;
    }

    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;
    }
}
4)PlayerMsg
using System.Collections;
using System.Collections.Generic;

public class PlayerMsg : BaseMsg
{
    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 + //palyerID
               playerData.GetBytesNum(); //playerData
    }

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

public class QuitMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return 0;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }

    public override int GetID()
    {
        return 1003;
    }
}
6)HeartMsg
using System.Collections;
using System.Collections.Generic;

public class HeartMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return 0;
    }

    public override int GetID()
    {
        return 999;
    }
}

3.2 服务端

1)ClientSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TeachTcpServerExercises2
{
    class ClientSocket
    {
        private static int CLIENT_BIGIN_ID = 1;
        public int clientID;
        public Socket socket;


        //用于处理分包时 缓存的 字节数组 和字节数组长度
        private byte[] cacheBytes = new byte[1024 * 1024];
        private int cacheNum = 0;

        //上一次收到消息的时间
        private long frontTime = -1;
        //超时时间
        private static int TIME_OUT_TIME = 10;

        public ClientSocket(Socket socket)
        {
            this.clientID = CLIENT_BIGIN_ID;
            this.socket = socket;
            ++CLIENT_BIGIN_ID;

            //为了方便理解 所有开一个线程专门计时 但是这种方式比较消极性能 不建议这样使用
            ThreadPool.QueueUserWorkItem(CheckTimeOut);
        }

        /// <summary>
        /// 间隔一段时间 检测一次超时 如果超时就会主动断开该客户端的连接
        /// </summary>
        /// <param name="obj"></param>
        private void CheckTimeOut(object obj)
        {
            while (Connected)
            {
                if (frontTime != -1 &&
                DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
                {
                    Program.socket.AddDelSocket(this);
                    break;
                }

                Thread.Sleep(5000);
            }
        }

        /// <summary>
        /// 是否是连接状态
        /// </summary>
        public bool Connected => this.socket.Connected;

        //我们应该封装一些方法
        //关闭
        public void Close()
        {
            if(socket != null)
            {
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
                socket = null;
            }
        }
        //发送
        public void Send(BaseMsg info)
        {
            if(Connected)
            {
                try
                {
                    socket.Send(info.Writing());
                }
                catch (Exception e)
                {
                    Console.WriteLine("发消息出错" + e.Message);
                    Program.socket.AddDelSocket(this);
                    //Close();
                }
            }
            else
            {
                Program.socket.AddDelSocket(this);
            }
        }
        //接收
        public void Receive()
        {
            if (!Connected)
            {
                Program.socket.AddDelSocket(this);
                return;
            }

            try
            {
                if(socket.Available > 0)
                {
                    byte[] res = new byte[1024 * 5];
                    int receiveNum = socket.Receive(res);
                    HandleReceiveMsg(res, receiveNum);

                    收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
                    //int msgID = BitConverter.ToInt32(res, 0);
                    //BaseMsg msg = null;
                    //switch (msgID)
                    //{
                    //    case 1001:
                    //        msg = new PlayerMsg();
                    //        msg.Reading(res, 4);
                    //        break;
                    //    default:
                    //        break;
                    //}
                    //if (msg == null)
                    //    return;
                    //ThreadPool.QueueUserWorkItem(MsgHandle, msg);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("收消息出错" + e.Message);
                //解析消息错误 也认为 要把socket断开了
                Program.socket.AddDelSocket(this);
                //Close();
            }
        }


        //处理接收消息 分包、黏包问题的方法
        private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
        {
            int msgID = 0;
            int msgLength = 0;
            int nowIndex = 0;

            //收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
            receiveBytes.CopyTo(cacheBytes, cacheNum);
            cacheNum += receiveNum;

            while (true)
            {
                //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
                msgLength = -1;
                //处理解析一条消息
                if (cacheNum - nowIndex >= 8)
                {
                    //解析ID
                    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析长度
                    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                }

                if (cacheNum - nowIndex >= msgLength && msgLength != -1)
                {
                    //解析消息体
                    BaseMsg baseMsg = null;
                    switch (msgID)
                    {
                        case 1001:
                            baseMsg = new PlayerMsg();
                            baseMsg.Reading(cacheBytes, nowIndex);
                            break;
                        case 1003:
                            baseMsg = new QuitMsg();
                            //由于该消息没有消息体 所有都不用反序列化
                            break;
                        case 999:
                            baseMsg = new HeartMsg();
                            //由于该消息没有消息体 所有都不用反序列化
                            break;
                        default:
                            break;
                    }
                    if (baseMsg != null)
                        ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
                    //receiveQueue.Enqueue(baseMsg);
                    nowIndex += msgLength;
                    if (nowIndex == cacheNum)
                    {
                        cacheNum = 0;
                        break;
                    }
                }
                else
                {
                    //如果不满足 证明有分包
                    //那么我们需要把当前收到的内容 记录下来
                    //有待下次接收到消息后 再做处理
                    //receiveBytes.CopyTo(cacheBytes, 0);
                    //cacheNum = receiveNum;

                    //如果进行了 ID和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
                    if (msgLength != -1)
                        nowIndex -= 8;

                    //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                    Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                    cacheNum = cacheNum - nowIndex;
                    break;
                }

            }


        }

        private void MsgHandle(object obj)
        {
            BaseMsg msg = obj as BaseMsg;
            if(msg is PlayerMsg)
            {
                Console.WriteLine("PlayerMeg");

                PlayerMsg playerMsg = msg as PlayerMsg;
                Console.WriteLine(playerMsg.playerID);
                Console.WriteLine(playerMsg.playerData.name);
                Console.WriteLine(playerMsg.playerData.atk);
                Console.WriteLine(playerMsg.playerData.lev);
            }
            else if(msg is QuitMsg)
            {
                //收到断开连接消息 把字迹添加到待移除的列表当中
                Program.socket.AddDelSocket(this);
            }
            else if(msg is HeartMsg)
            {
                //收到心跳消息 记录收到消息的时间
                frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                Console.WriteLine("收到心跳消息");
            }
        }

    }
}
2)ServerSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TeachTcpServerExercises2
{
    class ServerSocket
    {
        //服务端Socket
        public Socket socket;
        //客户端连接的所有Socket
        public Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();

        //有待移除的客户端socket 避免在foreach时直接从字典中移除 出现问题
        private List<ClientSocket> delList = new List<ClientSocket>();

        private bool isClose;

        //开启服务器端
        public void Start(string ip, int port, int num)
        {
            isClose = false;
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            socket.Bind(ipPoint);
            socket.Listen(num);
            ThreadPool.QueueUserWorkItem(Accept);
            ThreadPool.QueueUserWorkItem(Receive);
        }

        //关闭服务器端
        public void Close()
        {
            isClose = true;
            foreach (ClientSocket client in clientDic.Values)
            {
                client.Close();
            }
            clientDic.Clear();

            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }

        //接受客户端连入
        private void Accept(object obj)
        {
            while (!isClose)
            {
                try
                {
                    //连入一个客户端
                    Socket clientSocket = socket.Accept();
                    ClientSocket client = new ClientSocket(clientSocket);
                    lock (clientDic)
                    {
                        clientDic.Add(client.clientID, client);
                    }
                    Console.WriteLine("客户端" + clientSocket.RemoteEndPoint + "连入服务器");
                }
                catch (Exception e)
                {
                    Console.WriteLine("客户端连入报错" + e.Message);
                }
            }
        }

        //接收客户端消息
        private void Receive(object obj)
        {
            while (!isClose)
            {
                if(clientDic.Count > 0)
                {
                    lock (clientDic)
                    {
                        foreach (ClientSocket client in clientDic.Values)
                        {
                            client.Receive();
                        }

                        CloseDelListSocket();
                    }
                }
            }
        }

        public void Broadcast(BaseMsg info)
        {
            lock (clientDic)
            {
                foreach (ClientSocket client in clientDic.Values)
                {
                    client.Send(info);
                }
            }
        }

        //添加待移除的 socket内容
        public void AddDelSocket(ClientSocket socket)
        {
            if (!delList.Contains(socket))
            {
                delList.Add(socket);
            }
        }

        //判断有没有 断开连接的 把其 移除
        public void CloseDelListSocket()
        {
            //判断有没有 断开连接的 把其 移除
            for (int i = 0; i < delList.Count; i++)
            {
                CloseClientSocket(delList[i]);
            }
            delList.Clear();
        }

        //关闭客户端连接 从字典中移除
        public void CloseClientSocket(ClientSocket socket)
        {
            lock (clientDic)
            {
                socket.Close();
                if (clientDic.ContainsKey(socket.clientID))
                {
                    clientDic.Remove(socket.clientID);
                    Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
                }
            }
        }
    }
}
 3)启动代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TeachTcpServerExercises2
{
    class Program
    {
        public static ServerSocket socket;

        static void Main(string[] args)
        {
            socket = new ServerSocket();
            socket.Start("127.0.0.1", 8080, 1024);
            Console.WriteLine("服务器开启成功");
            while (true)
            {
                string input = Console.ReadLine();
                if(input == "Quit")
                {
                    socket.Close();
                }
                else if(input.Substring(0, 2) == "B:")
                {
                    if(input.Substring(2) == "1001")
                    {
                        PlayerMsg msg = new PlayerMsg();
                        msg.playerID = 9833;
                        msg.playerData = new PlayerData();
                        msg.playerData.name = "服务器端发来的消息";
                        msg.playerData.atk = 1313;
                        msg.playerData.lev = 9999;
                        socket.Broadcast(msg);
                    }
                }
            }
        }
    }
}

3.3 客户端

1)NetMgr
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 NetMgr : MonoBehaviour
{
    private static NetMgr instance;
    public static NetMgr Instance => instance;

    //客户端Socket
    private Socket socket;
    //用于发送消息的队列 公共容器 主线程里面放 发送线程从里面取
    private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
    //用于接受消息的队列 公共容器 子线程往里面放 主线程从里面取
    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

    用于收消息的水桶(容器)
    //private byte[] receiveBytes = new byte[1024 * 1024];
    返回收到的字节数
    //private int receiveNum;

    //用于处理分包时 缓存的 字节数组 和字节数组长度
    private byte[] cacheBytes = new byte[1024 * 1024];
    private int cacheNum = 0;

    //是否连接
    private bool isConneted = false;

    //发送心跳消息间隔时间
    private int SEND_HEARTMSG_TIME = 2;
    private HeartMsg heartMsg = new HeartMsg();

    private 

    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);

        //客户端循环定时给服务端发送心跳消息
        InvokeRepeating("SendHeartMsg", 0, SEND_HEARTMSG_TIME);
    }

    private void SendHeartMsg()
    {
        if(isConneted)
            Send(heartMsg);
    }

    // Update is called once per frame
    void Update()
    {
        if(receiveQueue.Count > 0)
        {
            BaseMsg msg = receiveQueue.Dequeue();
            if(msg is PlayerMsg)
            {
                PlayerMsg playerMsg = (PlayerMsg)msg;
                Debug.Log(playerMsg.playerID);
                Debug.Log(playerMsg.playerData.name);
                Debug.Log(playerMsg.playerData.atk);
                Debug.Log(playerMsg.playerData.lev);
            }
        }
    }

    //连接服务端
    public void Connect(string ip, int port)
    {
        //如果是连接状态直接返回
        if (isConneted)
            return;

        if(socket == null)
           socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //连接服务端
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        try
        {
            socket.Connect(ipPoint);
            isConneted = true;
            //开启发送线程
            ThreadPool.QueueUserWorkItem(SendMsg);
            //开启接收线程
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
        }
        catch (SocketException e)
        {
            if (e.ErrorCode == 10061)
                Debug.Log("服务拒绝连接");
            else
                Debug.Log("连接失败:" + e.ErrorCode + e.Message);
        }
    }

    //发送消息
    public void Send(BaseMsg msg)
    {
        sendMsgQueue.Enqueue(msg);
    }

    /// <summary>
    /// 用于测试直接发字节数组的方法
    /// </summary>
    /// <param name="bytes"></param>
    public void SendTest(byte[] bytes)
    {
        socket.Send(bytes);
    }

    private void SendMsg(object obj)
    {
        while (isConneted)
        {
            if(sendMsgQueue.Count > 0)
            {
                socket.Send(sendMsgQueue.Dequeue().Writing());
            }
        }
    }


    //不停的接受消息
    private void ReceiveMsg(object obj)
    {
        while (isConneted)
        {
            if(socket.Available > 0)
            {
                byte[] receiveBytes = new byte[1024 * 1024];
                int receiveNum = socket.Receive(receiveBytes);

                首先把收到字节数组的前4个字节 读取出来得到ID
                //int msgID = BitConverter.ToInt32(receiveBytes, 0);
                //BaseMsg baseMsg = null;

                //switch (msgID)
                //{
                //    case 1001:
                //        PlayerMsg msg = new PlayerMsg();
                //        msg.Reading(receiveBytes, 4);
                //        baseMsg = msg;
                //        break;
                //    default:
                //        break;
                //}
                如果消息为空 那证明是不知道类型的消息 没有解析
                //if (baseMsg == null)
                //    continue;
                收到消息 解析消息为字符串 并放入公共容器
                //receiveQueue.Enqueue(baseMsg);


                HandleReceiveMsg(receiveBytes, receiveNum);
            }
        }
    }

    //处理接收消息 分包、黏包问题的方法
    private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
    {
        int msgID = 0;
        int msgLength = 0;
        int nowIndex = 0;

        //收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
        receiveBytes.CopyTo(cacheBytes, cacheNum);
        cacheNum += receiveNum;

        while (true)
        {
            //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
            msgLength = -1;
            //处理解析一条消息
            if(cacheNum - nowIndex >= 8)
            {
                //解析ID
                msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
                //解析长度
                msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
            }
            
            if(cacheNum - nowIndex >= msgLength && msgLength != -1)
            {
                //解析消息体
                BaseMsg baseMsg = null;
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg msg = new PlayerMsg();
                        msg.Reading(cacheBytes, nowIndex);
                        baseMsg = msg;
                        break;
                    default:
                        break;
                }
                if (baseMsg != null)
                    receiveQueue.Enqueue(baseMsg);
                nowIndex += msgLength;
                if (nowIndex == cacheNum)
                {
                    cacheNum = 0;
                    break;
                }
            }
            else
            {
                //如果不满足 证明有分包
                //那么我们需要把当前收到的内容 记录下来
                //有待下次接收到消息后 再做处理
                //receiveBytes.CopyTo(cacheBytes, 0);
                //cacheNum = receiveNum;

                //如果进行了 ID和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
                if (msgLength != -1)
                    nowIndex -= 8;

                //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                cacheNum = cacheNum - nowIndex;
                break;
            }
            
        }
        

    }

    public void Close()
    {
        if(socket != null)
        {
            Debug.Log("客户端主动断开连接");

            //主动发送一条断开连接的消息给服务端
            //QuitMsg quitMsg = new QuitMsg();
            //socket.Send(quitMsg.Writing());

            //socket.Shutdown(SocketShutdown.Both);
            //socket.Disconnect(false);
            //socket.Close();
            socket = null;

            isConneted = false;
        }
    }

    private void OnDestroy()
    {
        Close();
    }

}
2)启动代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Main : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        if(NetMgr.Instance == null)
        {
            GameObject obj = new GameObject("Net");
            obj.AddComponent<NetMgr>();
        }

        NetMgr.Instance.Connect("127.0.0.1", 8080);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
3)消息发送测试代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;

public class Lesson7 : MonoBehaviour
{
    public Button btn;
    public Button btn1;
    public Button btn2;
    public Button btn3;
    public InputField input;

    // Start is called before the first frame update
    void Start()
    {
        btn.onClick.AddListener(() =>
        {
            if(input.text != "")
            {
                PlayerMsg ms = new PlayerMsg();
                ms.playerID = 123123;
                ms.playerData = new PlayerData();
                ms.playerData.name = "hellow";
                ms.playerData.atk = 0;
                ms.playerData.lev = 1;
                NetMgr.Instance.Send(ms);
            }
        });

        //黏包测试
        btn1.onClick.AddListener(() =>
        {
            PlayerMsg msg = new PlayerMsg();
            msg.playerID = 1001;
            msg.playerData = new PlayerData();
            msg.playerData.name = "当老师1";
            msg.playerData.atk = 1;
            msg.playerData.lev = 1;

            PlayerMsg msg2 = new PlayerMsg();
            msg2.playerID = 1002;
            msg2.playerData = new PlayerData();
            msg2.playerData.name = "当老师2";
            msg2.playerData.atk = 2;
            msg2.playerData.lev = 2;

            byte[] bytes = new byte[msg.GetBytesNum() + msg2.GetBytesNum()];
            msg.Writing().CopyTo(bytes, 0);
            msg2.Writing().CopyTo(bytes, msg.GetBytesNum());

            NetMgr.Instance.SendTest(bytes);


        });
        //分包测试
        btn2.onClick.AddListener(async () =>
        {
            PlayerMsg msg = new PlayerMsg();
            msg.playerID = 1001;
            msg.playerData = new PlayerData();
            msg.playerData.name = "当老师1";
            msg.playerData.atk = 1;
            msg.playerData.lev = 1;

            byte[] bytes = msg.Writing();
            //分包
            byte[] bytes1 = new byte[10];
            byte[] bytes2 = new byte[bytes.Length - 10];
            //分成第一个包
            Array.Copy(bytes, 0, bytes1, 0, 10);
            //分第二个包
            Array.Copy(bytes, 10, bytes2 , 0, bytes.Length - 10);


            NetMgr.Instance.SendTest(bytes1);
            await Task.Delay(500);
            NetMgr.Instance.SendTest(bytes2);

        });
        //分包、黏包测试
        btn3.onClick.AddListener(async () =>
        {
            PlayerMsg msg = new PlayerMsg();
            msg.playerID = 1001;
            msg.playerData = new PlayerData();
            msg.playerData.name = "当老师1";
            msg.playerData.atk = 1;
            msg.playerData.lev = 1;

            PlayerMsg msg2 = new PlayerMsg();
            msg2.playerID = 1002;
            msg2.playerData = new PlayerData();
            msg2.playerData.name = "当老师2";
            msg2.playerData.atk = 2;
            msg2.playerData.lev = 2;

            byte[] bytes1 = msg.Writing();//消息A
            byte[] bytes2 = msg2.Writing();//消息B

            byte[] bytes2_1 = new byte[10];
            byte[] bytes2_2 = new byte[bytes2.Length - 10];
            //分成第一个包
            Array.Copy(bytes2, 0, bytes2_1, 0, 10);
            //分第二个包
            Array.Copy(bytes2, 10, bytes2_2, 0, bytes2.Length - 10);

            //消息A和消息B前一段的 黏包
            byte[] bytes = new byte[bytes1.Length + bytes2_1.Length];
            bytes1.CopyTo(bytes, 0);
            bytes2_1.CopyTo(bytes, bytes1.Length);

            NetMgr.Instance.SendTest(bytes);
            await Task.Delay(1000);
            NetMgr.Instance.SendTest(bytes2_2);

        });
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到