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

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

服务器

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 clientList = new Dictionary();
        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 msg = new List();
    // 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聊天室 封包拆包)