美文网首页
Unity网络编程(三)TCP 1VN聊天室 封包拆包

Unity网络编程(三)TCP 1VN聊天室 封包拆包

作者: 罗卡恩 | 来源:发表于2019-12-15 15:15 被阅读0次

    在之前的基础上改成多人聊天

    服务器

    using System;
    
    namespace TalkRoomTCP
    {
        class Program
        {
            static void Main(string[] args)
            {
                new TalkSever().Init();
                // 接收一个键盘输入的字符,目的是不让命令行自动关闭
                Console.ReadKey();
            }
        }
    }
    
    
    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    namespace TalkRoomTCP
    {
        //每一个客户端的结构
        class Client
        {
            public const ushort Buffer_Length = 1024;
            public Socket socket;
            public byte[] buffer = new byte[Buffer_Length];
        }
        class TalkSever
        {
            //存放每一个客户端
            Dictionary<Socket, Client> clientList = new Dictionary<Socket, Client>();
            public void Init()
            {
                //创建socket using 代替Close 用完不关闭会占用端口
                Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    
                //绑定IP 端口号
                //IPAddress.Any:相当于"0.0.0.0"的IP地址侦听本地所有网络接口上的客户端活动 有几个侦听几个
                //IPAddress.Broadcast:相当于"255.255.255.255"的IP地址,通常用于Udp的数据包广播。
                //IPAddress.Loopback:相当于"127.0.0.1"的IP地址,用于指代本机。监听"127.0.0.1"时,只能从本机连接到服务端。
                socket.Bind(new IPEndPoint(IPAddress.Any, 9999));
                //开启监听 参数是最大接受队列的长度 多于这个就只响应100个 其他拒绝
                socket.Listen(100);
    
                //开启异步 第二个参数用于传递一些数据
                socket.BeginAccept(AcceptCallBack, socket);
                Console.WriteLine("服务器启动");
            }
    
            private void AcceptCallBack(IAsyncResult ar)
            {
                var socket = ar.AsyncState as Socket;
                var clientSocket = socket.EndAccept(ar);
                Console.WriteLine($"{clientSocket.RemoteEndPoint}客户端连接");
    
                Client client = new Client();
                client.socket = clientSocket;
                clientList.Add(clientSocket, client);
    
                //客户端接收消息 如果客户端不发送数据 服务器程序阻塞(挂起)这个位置
                var buffer = client.buffer;
                clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
    
                // 递归继续Accept
                socket.BeginAccept(AcceptCallBack, socket);
            }
    
            private void ReceiveCallBack(IAsyncResult ar)
            {
                //
                Client client = ar.AsyncState as Client;
                Socket clientSocket = client.socket;
                byte[] buffer = client.buffer;
                try
                {
                    int length = clientSocket.EndReceive(ar);
                    //小于0客户端就关闭了
                    if (length > 0)
                    {
                        Console.WriteLine($"接收到客户端的消息:{Encoding.UTF8.GetString(buffer, 0, length)}");
                        foreach (var item in clientList)
                        {
                            item.Key.Send(buffer, length, SocketFlags.None);
                        }
    
                       
                        //递归重新开始接收
                        clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
                    }
                    else
                    {
                        OnClientDisconnect(clientSocket);
                    }
                }
                catch (SocketException ex)
                {
                    // 如果服务端有向客户端A未发送完的数据,客户端A主动断开时会触发10054异常,在此捕捉
                    if (ex.SocketErrorCode == SocketError.ConnectionReset)
                        OnClientDisconnect(clientSocket);
                }
    
            }
    
            private void OnClientDisconnect(Socket clientSocket)
            {
                Console.WriteLine($"{clientSocket.RemoteEndPoint}断开连接");
                clientList.Remove(clientSocket);
                clientSocket.Close();
            }
        }
    }
    
    
    image.png

    客户端

    然后改造客户端 客户端不是实时的 而且顺序有问题
    先解决 不能实时刷新的问题
    还有会有时候接收到连在一起的字符 那是粘包问题 后面改造
    主要是我们点击时候发送又输入到屏幕上
    其实应该是 点击后 发给服务器等服务器返回才输入屏幕上

    /**
     *Copyright(C) 2019 by #COMPANY#
     *All rights reserved.
     *FileName:     #SCRIPTFULLNAME#
     *Author:       #AUTHOR#
     *Version:      #VERSION#
     *UnityVersion:#UNITYVERSION#
     *Date:         #DATE#
     *Description:   
     *History:
    */
    using UnityEngine;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using UnityEngine.UI;
    using System;
    using System.Collections.Generic;
    public class TalkClient : MonoBehaviour
    {
        public InputField input;
        public Text text;
        public Button btn;
        byte[] buffer = new byte[1024];
        Socket socket;
    
        List<string> msg = new List<string>();
        // Start is called before the first frame update
        void Start()
        {
            socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            //连接服务器
            socket.Connect("127.0.0.1", 9999);
    
            socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
            btn.onClick.AddListener(() =>
            {
                //发送数据
                socket.Send(Encoding.UTF8.GetBytes(input.text));
            });
        }
    
        private void Update()
        {
            if (msg.Count>0)
            {
                foreach (var item in msg)
                {
                    //因为unity不能在子线程调用unity大部分API Debug.Log可以 socket内部异步为我们开了线程
                    //UniRx插件中有一个MainThreadDispatcher类,可以很方便地用来处理子线程到主线程的转换
                    text.text += item + "\n";
                }
                //清除处理过的消息
                msg.Clear();
            }
        }
        void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                //接受数据
                int length = socket.EndReceive(ar);
                if (length > 0)
                {
                    var str = Encoding.UTF8.GetString(buffer, 0, length);
                    Debug.Log($"接收到服务端的消息:{str}");
                    msg.Add(str);
    
                    //重新开始接受
                    socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
                }
                else
                {
                    OnClientDisconnect();
                }
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    OnClientDisconnect();
            }
        }
    
        void OnClientDisconnect()
        {
            Debug.Log("与服务端断开连接");
            socket.Close();
        }
    }
    
    

    然后就OK了


    image.png

    区分玩家 解决粘包

    这样不知道哪个消息是谁发的
    有三种解决方法
    1、 加入注册登录功能
    2、 在连接服务端成功后先给服务端发送消息设置昵称
    3、 在每次发送消息的时候发送昵称
    可以设计个数据包
    用名字:信息
    上面的放名字


    image.png

    这里一改就行

     btn.onClick.AddListener(() =>
            {
                //发送数据
                socket.Send(Encoding.UTF8.GetBytes(inputName.text+":"+input.text));
            });
    

    然后是粘包
    服务器压力大的时候会出现
    发送5次然后返回一次 黏在一起
    原因 TCP是个"流"协议,所谓流,就是没有界限的一串数据
    会有以下4种情况 234都是粘包
    先接收到data1,然后接收到data2。
    先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部。
    先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据。
    一次性接收到了data1和data2的全部数据。
    相比UDP UDP是个数据包协议 他要么完整要么全丢
    服务器客户端都可能发生粘包
    1、由Nagle算法造成的发送端的粘包
    我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去
    2.接收端接收不及TCP缓存区缓存了多个数据时造成的接收端粘包
    解决方法 封包拆包


    image.png

    封包
    1.数据转为json字符串
    2.把json转为byte[]数组A
    3.创建一个长度为数组A长度+2字节的字节数组B,依次将2字节的长度和json字节数组A先后输入写入到这个B中
    4.将这个字节数组B(数据包)发送给服务端

    拆包
    1.将最新数据放入DataCache
    2、尝试从DataCache中解析数据包,具体代码见上面的Decode
    3、一直尝试解析,直到数据不足

    image.png

    一般来说XML json会很大 一般用自定义的二进制格式
    数据一般分为这两种 定长的数据和不定长的数据
    定长的数据比如:byte,short,int,long,char之类的简单数据,以及仅包含这些类型的类或结构体
    不定长的数据比如:字符串string、列表List、字典Dictionary等等,这些都需要进行特殊处理,一般是在数据内容的开头加上一个长度数据。比如写入一个字符串string时,先写入2字节的string的长度,再写入string的具体内容,类似我们上面处理的json字符串。
    比如现在我们的数据要改二进制的话


    image.png

    然后谷歌出了个protobuf比较好用 也不用自己写这么多处理

    相关文章

      网友评论

          本文标题:Unity网络编程(三)TCP 1VN聊天室 封包拆包

          本文链接:https://www.haomeiwen.com/subject/zfehnctx.html