Unity3D Socket同步连接并且处理粘包(附带一个案例)

Socket同步连接是比较常见的,

Socket是什么

 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。


什么是TCP/IP、UDP?(本文是TCP)

         TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
         UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。

为啥会出现粘包问题

首先要将自己的数据通过套接字发送,首先要调用一个write方法:(将应用进程缓冲区中的数据拷贝到套接口发送缓冲区SO_SNDBUF,有一个大小的限制),如果应用进程缓冲区的一条消息的字节的大小超过了发送缓冲区的大小,就有可能产生粘包问题,因为消息已经被分割了,有可能一部分已经被发送出去了,对方已经接受了,但是另外一部分可能刚放入套接口发送缓冲区里准备进一步发送,就直接导致接受的后一部分,直接导致了粘包问题的出现。

什么是状态同步
将其他玩家的状态行为同步的方式,一般情况下AI逻辑,技能逻辑,战斗计算都由服务器运算,将运算的结果同步给客户端,客户端只需要接受服务器传过来的状态变化,然后更新自己本地的动作状态、Buff状态,位置等就可以了,但是为了给玩家好的体验,减少同步的数据量,客户端也会做很多的本地运算,减少服务器同步的频率以及数据量。

什么是帧同步

帧同步从名字我们不难看出就去每一帧都发消息,虽然给性能增加不小的压力,但现在市面上大部分都使用的是帧同步:如王者荣耀等等

扫描二维码关注公众号,回复: 15682319 查看本文章

(本文需要PB插件大家也需要对PB有一些了解)

好讲完这些我们开始写代码

首先是服务器

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

/// <summary>
/// 服务器端网络的管理
/// 绑定
/// 侦听
/// 接受客户端的连接请求
/// 接受客户端 数据
/// 
/// </summary>
public class NetManager : Singleton<NetManager>
{

    private Socket mySocket;

    /// <summary>
    /// 所有连接进来的客户端对象
    /// </summary>
    private List<Client> allClient;
    public List<Client> AllClient { get => allClient; }

    private Queue<NetMsg> msgQueue;



    public void Init()
    {
        allClient = new List<Client>();
        msgQueue = new Queue<NetMsg>();

        mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPEndPoint ipe = new IPEndPoint(IPAddress.Any, 15151); ///any   parse方法解析ip地址
        mySocket.Bind(ipe);
        mySocket.Listen(100);


        Thread th = new Thread(acceptClientCallback);
        th.Start();


        Thread th1 = new Thread(handlerData);
        th1.Start();
        Console.WriteLine("服务器启动成功。。。。");


    }

    private void handlerData(object obj)
    {
        while (true)
        {
            Thread.Sleep(100);
            while (msgQueue.Count > 0)
            {
                NetMsg msg = msgQueue.Dequeue();
                ///数据处理
                int msgID = BitConverter.ToInt32(msg.fullMsg, 0);
                byte[] body = new byte[msg.fullMsg.Length - 4];

                Buffer.BlockCopy(msg.fullMsg, 4, body, 0, body.Length);
                msg.msgID = msgID;
                Console.WriteLine(msgID);
                msg.msgBody = body;

                MessageManager.Ins.OnSend(msgID, msg,msg.cli);
            }
        }
    }

    private void acceptClientCallback(object obj)
    {

        while (true)
        {
        
            Socket clientSocket = mySocket.Accept();  ///阻塞线程,当接收到一个客户端的连接请求后,才执行完毕

            IPEndPoint remoteIPE = clientSocket.RemoteEndPoint as IPEndPoint;
            Console.WriteLine(remoteIPE.ToString() + "连接进来了");

            Client cli = new Client(clientSocket);
            cli.closeEvent += Cli_closeEvent;

            allClient.Add(cli);


            clientSocket.BeginReceive(cli.buffer, 0, cli.buffer.Length, SocketFlags.None, receiveCallback, cli);

            Console.WriteLine("开始收数据");


        }

    }

    private void Cli_closeEvent(Client cli)
    {
        CloseClient(cli);
    }

    private void receiveCallback(IAsyncResult ar)
    {
        int ID = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("收到数据了" + ID);

        Client cli = ar.AsyncState as Client;  ///就是开始收数据时候的 传过来的state,最后一个参数
        int len = 0;
        try
        {
            len = cli.clientSocket.EndReceive(ar);
        }
        catch (Exception ex)
        {
            Console.WriteLine("和客户端失去连接了" + ex.Message);
            CloseClient(cli);

        }

        if (len > 0)
        {
            byte[] data = new byte[len];
            Buffer.BlockCopy(cli.buffer, 0, data, 0, len);
            cli.receiveMemoryStream.Position = cli.receiveMemoryStream.Length;  ///磁头放到最后
            cli.receiveMemoryStream.Write(data, 0, data.Length);
            while (true)  ///处理粘包的循环
            {
                if (cli.receiveMemoryStream.Length > 2) ///至少有个不完整的包
                {
                    cli.receiveMemoryStream.Position = 0;  ///磁头到开头。
                    Console.WriteLine("前"+cli.receiveMemoryStream.Length);
                    ushort bodyLen = cli.receiveMemoryStream.ReadUshort();
                    Console.WriteLine("后"+cli.receiveMemoryStream.Length);
                    Console.WriteLine(bodyLen);
                    int fullLen = bodyLen + 2;
                    if (cli.receiveMemoryStream.Length >= fullLen) ///有个完整包
                    {
                        cli.receiveMemoryStream.Position = 2;
                        byte[] tmp = new byte[bodyLen];
                        cli.receiveMemoryStream.Read(tmp, 0, tmp.Length);///压入队列等待处理(根据消息id进行消息中心派发)
                        this.msgQueue.Enqueue(new NetMsg(cli, tmp));///看有没有剩余部分,把剩余部分保留,处理完的删除
                        int remainlen = (int)cli.receiveMemoryStream.Length - fullLen;
                        if (remainlen > 0)
                        {
                            byte[] remainData = new byte[remainlen];///把剩余数据拷贝出来
                            cli.receiveMemoryStream.Position = fullLen;
                            cli.receiveMemoryStream.Read(remainData, 0, remainData.Length);
                            cli.receiveMemoryStream.Position = 0;//把流清空
                            cli.receiveMemoryStream.SetLength(0); //把流里数据清除                                                                  //剩余部分拷贝回来
                            cli.receiveMemoryStream.Write(remainData, 0, remainData.Length);
                        }
                        else
                        {
                            cli.receiveMemoryStream.Position = 0; //把流清空
                            cli.receiveMemoryStream.SetLength(0); //把流里数据清除
                            break;
                        }

                    }
                    else
                    {
                        ///不够完整包
                        break;

                    }
                }

            }
      
            ///很重要,再次收下一条数据,否则服务器只能从这个客户端收一次数据,
            cli.clientSocket.BeginReceive(cli.buffer, 0, cli.buffer.Length, SocketFlags.None, receiveCallback, cli);
        }
        else
        {
            Console.WriteLine("和客户端失去连接了");
            CloseClient(cli);
        }



    }



    /// <summary>
    /// 关闭一个客户端
    /// </summary>
    /// <param name="cli"></param>
    public void CloseClient(Client cli)
    {
        if (allClient.Contains(cli))
        {
            allClient.Remove(cli);
        }
        cli.Close();
    }
}


public class NetMsg
{
    public Client cli;
    public byte[] fullMsg;

    public int msgID;
    public byte[] msgBody;

    public NetMsg(Client cli, byte[] msg)
    {
        this.cli = cli;
        this.fullMsg = msg;
    }

}


public static class NetMsgIDDefine
{
    public const int C2S_Chat = 10001;  ///上行 协议
    public const int S2C_Chat = 20001; ///下行协议id



}


客户端:

using Google.Protobuf;
using System;
using System.Net.Sockets;
using System.Text;

/// <summary>
/// 代表一个客户端的对象。
/// 封装一个向客户端发送消息的方法
/// 
/// </summary>
public class Client
{
    public byte[] buffer;
    public Socket clientSocket;
    public MyMemoryStream receiveMemoryStream;

    public event Action<Client> closeEvent;
    public Client(Socket sc)
    {

        this.clientSocket = sc;
        buffer = new byte[2048];
        receiveMemoryStream = new MyMemoryStream();
    }

    public void Send(byte[] data)
    {

        data = makeData(data);
        if (data != null && this.clientSocket != null && this.clientSocket.Connected)
        {

            //this.clientSocket.Send(data); ///同步方法,,阻塞

            this.clientSocket.BeginSend(data, 0, data.Length, SocketFlags.None, sendCallback, null);
        }
    }

    private byte[] makeData(byte[] data)
    {
        using (MyMemoryStream my = new MyMemoryStream())
        {
            my.WriteUShort((ushort)data.Length);
            my.Write(data, 0, data.Length);
            return my.ToArray();
        }
    }


    /// <summary>
    /// 发送消息,
    /// </summary>
    /// <param name="msgid">消息类型</param>
    /// <param name="body">内容</param>
    public void Send(int msgid, byte[] body)
    {

        byte[] data = null;
        using (MyMemoryStream my = new MyMemoryStream())  using
        {
            my.WriteInt(msgid);

            my.Write(body, 0, body.Length);
            data = my.ToArray();

        }

        if (data != null)
        {
            this.Send(data);
        }

    }

    public void Send(int msgid, string bd)
    {

        //  byte[] body = UTF8Encoding.UTF8.GetBytes(bd);
        byte[] data = null;
        using (MyMemoryStream my = new MyMemoryStream())  using
        {
            my.WriteInt(msgid);

            my.WriteUString(bd);

            data = my.ToArray();

        }

        if (data != null)
        {
            this.Send(data);
        }



    }

    /// <summary>
    /// 发送pb消息
    /// </summary>
    /// <param name="msgid"></param>
    /// <param name="msg"></param>
    public void Send(int msgid, IMessage msg)
    {
        byte[] data = msg.ToByteArray(); ///把pb序列化
        byte[] dt = null;
        using (MyMemoryStream my = new MyMemoryStream())
        {
            my.WriteInt(msgid);
            my.Write(data, 0, data.Length);
            dt = my.ToArray();
        }

        if (dt != null)
        {
            this.Send(dt);
        }
    }

    private void sendCallback(IAsyncResult ar)
    {

        try
        {
            int len = this.clientSocket.EndSend(ar);
            Console.WriteLine("发送成功了" + len);
        }
        catch (Exception ex)
        {
            Console.WriteLine("和客户端断开连接了 = " + this.clientSocket.RemoteEndPoint.ToString());
            closeEvent?.Invoke(this);
        }

    }

    /// <summary>
    /// 让这个客户端掉线
    /// </summary>
    public void Close()
    {
        this.buffer = null;
        if (this.receiveMemoryStream != null)
        {
            this.receiveMemoryStream.Close();
            this.receiveMemoryStream = null;
        }

        if (clientSocket != null && this.clientSocket.Connected)
        {
            this.clientSocket.Shutdown(SocketShutdown.Both);//禁止接收和发送
            this.clientSocket.Close();//关闭并释放与其相关的资源
        }

    }
}

消息中心:

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

using System;
/// <summary>
/// 消息中心
/// </summary>

public class MessageManager : Singleton<MessageManager>
{
    Dictionary<int, Action<object>> dic = new Dictionary<int, Action<object>>();
    //注册
    public void OnAddliston(int id, Action<object> action)
    {
        if (dic.ContainsKey(id))
        {
            dic[id] += action;
        }
        else
        {
            dic.Add(id, action);
        }
    }
    //移除
    public void OnRemove(int id, Action<object> action)
    {
        if (dic.ContainsKey(id))
        {
            dic[id] -= action;
            if (dic[id] == null)
            {
                dic.Remove(id);
            }
        }
    }
   //发送
    public void OnSend(int id, params object[] arr)
    {
        if (dic.ContainsKey(id))
        {
            dic[id](arr);
        }
    }

}

单例:

using System.Collections;
using System.Collections.Generic;
public class Singleton<T> where T : class, new()
{
    private static T ins;

    public static T Ins
    {
        get
        {
            if (ins == null)
            {
                ins = new T();
            }
            return ins;
        }
    }
}

消息绑定

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

public class ChatModule : Singleton<ChatModule>
{
    public ChatModule()
    {
        MessageManager.Ins.OnAddliston(NetMsgIDDefine.C2S_Chat, ChatHandler);//添加事件
       
    }

    private void ChatHandler(object obj)
    {
        object[] objArr = obj as object[];
        NetMsg msg = objArr[0] as NetMsg;
       
        MyGame.ChatMsg chatMsg = MyGame.ChatMsg.Parser.ParseFrom(msg.msgBody);
        Console.WriteLine("客户端 = {0}    聊天消息 = {1}   时间 = {2}", chatMsg.FromName, chatMsg.Msg, chatMsg.Time);

        List<Client> allclient = NetManager.Ins.AllClient;
        switch (chatMsg.ChatType)
        {
            case MyGame.ChatType.Public:
                foreach (var item in allclient)
                {
                    item.Send(NetMsgIDDefine.S2C_Chat, chatMsg);
                    
                }
                break;
            case MyGame.ChatType.Private:

                break;
            default:
                break;
        }

      

 
    }
}

下面是一个案例(百度网吧)

链接:https://pan.baidu.com/s/166aYMLrJzMWTR_KIZ742QA 
提取码:ldb4

有时间我也会写一下异步

猜你喜欢

转载自blog.csdn.net/qq_57896821/article/details/121915276