Unity-TCP-网络聊天功能(一): API、客户端服务器、数据格式、粘包拆包

1.TCP相关API介绍与服务端编写

TCP是面向连接的。因此需要创建监听器,监听客户端的连接。当连接成功后,会返回一个TcpClient对象。通过TcpClient可以接收和发送数据。

VS创建C# .net控制台应用

项目中创建文件夹Net,Net 下添加TCPServer.cs类,用来创建TCPListener和Accept客户端连接,实例化一个TCPServcer放在Main函数执行。Client用来管理每一个客户端的接收发送消息。

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            TCPServer tcpServer = new TCPServer();
            tcpServer.Start();

            while(true)
            {
                Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
            }
        }
    }
}
namespace Server.Net
{
    internal class TCPServer
    {
        TcpListener tcpListener;
        //启动服务器,创建监听器
        public void Start()
        {
            try
            {
                //创建监听器,如果不指定IP会以本机的IP为服务器的IP
                tcpListener = TcpListener.Create(7788);//1-65535
                tcpListener.Start(500);//启动监听,传递最大接收多少客户端连接。

                Console.WriteLine("TCP Server Start");

                //启动服务器后,还要调用接收连接
                Accept();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            
        }

        //监听客户端连接
        //由于内部使用的方法是AcceptTcpClientAsync异步,因此方法也要是async
        //这样就可以通过await的方式等待
        public async void Accept()
        {
            try
            {
                //监听TCP客户端连接的返回的对象
                TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
                Console.WriteLine("客户端已连接:" + tcpClient.Client.RemoteEndPoint);//打印IP
                                                                               //使用构建的Client类来缓存这些TcpClient
                Client client = new Client(tcpClient);
                Accept();//继续接受来自客户端的连接即可
            }
            catch (Exception e)
            {
                Console.WriteLine($"Accept:{e.Message}");
                tcpListener.Stop();
            }
        }
    }
}
namespace Server.Net
{
    //每一个客户端都是一个独立的Client
    internal class Client
    {
        TcpClient client;
        public Client(TcpClient tcpClient)
        {
            client = tcpClient;
            Receive();
        }

        //接收消息
        public async void Receive()
        {
            //处于连接状态就持续接收信息。
            while(client.Connected)
            {
                try
                {
                    byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
                    //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
                    int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

                    if(length > 0)//表示有效信息
                    {
                        //$作用是将{}内容当做表达式
                        Console.WriteLine($"接收到的数据长度:{length}");
                        Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串
                    }
                    else
                    {
                        //客户端关闭了
                        client.Close();
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine($"Receive Error:{e.Message}");
                    client.Close();//出现错误断开客户端连接。
                }
            }            
        }

        //发送消息
        public async void Send(byte[] data)
        {
            try
            {
                //数据写入网络数据流中。就是发送
                await client.GetStream().WriteAsync(data, 0, data.Length);
                Console.WriteLine("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
            }
            catch (Exception e)
            {
                client.Close();//关闭客户端
                Console.WriteLine($"send error:{e.Message}");
            }
        }
    }
}

运行测试

2.实现客户端和服务器的消息收发

编写Unity客户端,Client项目,创建Script/Net/Client.cs

注意收发消息都必须是异步的async,因为不能阻塞我们的程序。

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

public class Client
{
    private static Client instance = new Client();
    public static Client Instance => instance;//单例模式便于调用
    
    private TcpClient client;//跟服务器通信需要调用client
    
    public  void Start()
    {
        client = new TcpClient();
        Connect();
    }
    
    //连接服务器接口
    public async void Connect()
    {
        try
        {
            await client.ConnectAsync("127.0.0.1", 7788);
            Debug.Log("TCP 连接成功");
            Receive();
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }
    }
    
    //接收接口
    public async void Receive()
    {
        while (client.Connected)
        {
            try
            {
                byte[] buff = new byte[4096];
                int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
                if (length > 0)
                {
                    Debug.Log($"接收到的数据长度:{length}");
                    Debug.Log($"接收到的数据内容:{Encoding.UTF8.GetString(buff, 0, length)}");
                }
                else
                {
                    client.Close();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }
    
    //发送接口
    public async void Send(byte[] data)
    {
        try
        {
            await client.GetStream().WriteAsync(data, 0, data.Length);
            Debug.Log("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

Unity创建Scripts/GameManager.cs来启动客户端连接服务器

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

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Client.Instance.Start();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            Client.Instance.Send(Encoding.UTF8.GetBytes("login..."));
        }
    }
}
public async void Receive()
{
    //处于连接状态就持续接收信息。
    while(client.Connected)
    {
        try
        {
            byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
            //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
            int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

            if(length > 0)//表示有效信息
            {
                //$作用是将{}内容当做表达式
                Console.WriteLine($"接收到的数据长度:{length}");
                Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串

                Send(Encoding.UTF8.GetBytes("测试返回..."));
            }
            else
            {
                //客户端关闭了
                client.Close();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"Receive Error:{e.Message}");
            client.Close();//出现错误断开客户端连接。
        }
    }            
}

启动服务器和客户端显示连接成功,测试收发消息。

注意接收消息成功之后,如果需要返回给客户端消息,必须要返回给特定的客户端,不能搞错。需要通过ClientManager写一个字典管理所有Client,或者直接在Client里面写一个tempClient

public Client tempClient;//缓存客户端发送消息
public async void Accept()
    {
        try
        {
            //监听TCP客户端连接的返回的对象
            TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
            Console.WriteLine("客户端已连接:" + tcpClient.Client.RemoteEndPoint);//打印IP
                                                                           //使用构建的Client类来缓存这些TcpClient
            Client client = new Client(tcpClient);
            tempClient = client;
            Accept();//继续接受来自客户端的连接即可
        }
        catch (Exception e)
        {
            Console.WriteLine($"Accept:{e.Message}");
            tcpListener.Stop();
        }
    }
static void Main(string[] args)
{
    TCPServer tcpServer = new TCPServer();
    tcpServer.Start();

    while(true)
    {
        var str = Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
        tcpServer.tempClient.Send(Encoding.UTF8.GetBytes($"测试主动发送数据:{str}"));
    }
}

3.使用LitJSON更好的组织网络数据

在Server项目的VS中打开NuGet包管理器安装LitJson

在PM输入Install-Package LitJson -Version 0.18.0

每个包都由其所有者许可给你。NuGet 不负责第三方包,也不授予其许可证。一些包可能包括受其他许可证约束的依赖关系。单击包源(源) URL 可确定任何依赖关系。

程序包管理器控制台主机版本 5.10.0.7240

键入 "get-help NuGet" 可查看所有可用的 NuGet 命令。

PM> Install-Package LitJson -Version 0.18.0


正在尝试收集与目标为“.NETFramework,Version=v4.7.2”的项目“Server”有关的包“LitJson.0.18.0”的依赖项信息
收集依赖项信息花费时间 1.81 sec
正在尝试解析程序包“LitJson.0.18.0”的依赖项,DependencyBehavior 为“Lowest”
解析依赖项信息花费时间 0 ms
正在解析操作以安装程序包“LitJson.0.18.0”
已解析操作以安装程序包“LitJson.0.18.0”
从“nuget.org”检索包“LitJson 0.18.0” 
GET https://api.nuget.org/v3-flatcontainer/litjson/0.18.0/litjson.0.18.0.nupkg
OK https://api.nuget.org/v3-flatcontainer/litjson/0.18.0/litjson.0.18.0.nupkg 137 毫秒
已通过内容哈希 zVK/1iUURxvEZH1eiviFpS8Qbh1fZe926ie18vhHuisVYcNbdVjgG85X8BsGt7WBU5Ka3gZvaQXRX6EsoD2hrw== 从 https://api.nuget.org/v3/index.json 安装 LitJson 0.18.0 。
正在将程序包“LitJson.0.18.0”添加到文件夹“E:\graduate\learn\Unity\NetProject\Server\packages”
已将程序包“LitJson.0.18.0”添加到文件夹“E:\graduate\learn\Unity\NetProject\Server\packages”
已将程序包“LitJson.0.18.0”添加到“packages.config”
已将“LitJson 0.18.0”成功安装到 Server
执行 nuget 操作花费时间 8.44 sec
已用时间: 00:00:11.4081423

创建Helper/JsonHelper.cs

using System.Text;

using LitJson;

namespace Server.Helper
{
internal class JsonHelper
    {
        //Object转换为String
        public static string ToJson(object x)//object C#所有类派生于此,所以C#类及自己创建的类都有ToString()
        {
            string str = JsonMapper.ToJson(x);
            return str;
        }

        //String转换为Object
        public static T ToObject<T>(string x)
        {
            return JsonMapper.ToObject<T>(x);
        }

        public static T ToObject<T>(byte[] b)
        {
            string x = Encoding.UTF8.GetString(b, 0, b.Length);
            return ToObject<T>(x);
        }

        public static string GetTestToString()
        {
            JsonTest jsonTest = new JsonTest();
            jsonTest.id = 1;
            jsonTest.name = "jsonTest";
            return ToJson(jsonTest);
        }
    }

    public class JsonTest
    {
        public int id;
        public string name;
    }
}
while(true)
{
    var str = Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
    //tcpServer.tempClient.Send(Encoding.UTF8.GetBytes($"测试主动发送数据:{str}"));
    var jsonStr = JsonHelper.GetTestToString();
    tcpServer.tempClient.Send(Encoding.UTF8.GetBytes(jsonStr));
}

运行测试,,目前Unity收到的消息还没有反序列化成原本的Object,因为服务器目前发送的是Json格式的String,Unity收到的也是Json格式的String

Unity Client项目创建Scripts/Helper/JsonHelper.cs,然后找到\NetProject\Server\packages\LitJson.0.18.0\lib\net45\LitJson.dll复制进Unity的Plugins文件夹,,,,重开一下脚本就重新编译好了,不会报错LitJson缺失的问题了

using System.Text;
using LitJson;
using UnityEngine;

public class JsonHelper
{
    //Object转换为String
    public static string ToJson(object x)//object C#所有类派生于此,所以C#类及自己创建的类都有ToString()
    {
        string str = JsonMapper.ToJson(x);
        return str;
    }

    //String转换为Object
    public static T ToObject<T>(string x)
    {
        return JsonMapper.ToObject<T>(x);
    }

    public static T ToObject<T>(byte[] b)
    {
        string x = Encoding.UTF8.GetString(b, 0, b.Length);
        return ToObject<T>(x);
    }

    public static string GetTestToString()
    {
        JsonTest jsonTest = new JsonTest();
        jsonTest.id = 1;
        jsonTest.name = "jsonTest";
        var jsonStr = ToJson(jsonTest);

        var jsonTestObj = ToObject<JsonTest>(ToJson(jsonTest));
        Debug.Log($"{jsonTestObj.id}   /  {jsonTestObj.name}");
        return jsonStr;
    }
    
    public class JsonTest
    {
        public int id;
        public string name;
    }
}
//接收接口
public async void Receive()
{
    while (client.Connected)
    {
        try
        {
            byte[] buff = new byte[4096];
            int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
            if (length > 0)
            {
                Debug.Log($"接收到的数据长度:{length}");
                Debug.Log($"接收到的数据内容:{Encoding.UTF8.GetString(buff, 0, length)}");
                var jsonTest = JsonHelper.ToObject<JsonHelper.JsonTest>(Encoding.UTF8.GetString(buff, 0, length));
                Debug.Log(jsonTest.id + " " + jsonTest.name);
            }
            else
            {
                client.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

4.粘包和拆包处理

粘包是因为计算机底层将多条小的数据合并成一条大的发送给另一端,减少网络传输的基本开销,需要拆包将多条数据拆分

用4个字节表示包体大小和消息ID,包体内容可能是账号、密码、业务所需要用到的数据(邮箱、移动(方向轴 目标点))

接收到消息看长度是单条消息没接收完还是一下粘包接受了多条消息。

先看Server端的Client

internal class Client
{
    TcpClient client;
    public Client(TcpClient tcpClient)
    {
        client = tcpClient;
        Receive();
    }

    byte[] data = new byte[4096];//接收消息的缓冲区
    int msgLength = 0;//接收到的消息长度

    //接收消息
    public async void Receive()
    {
        //处于连接状态就持续接收信息。
        while(client.Connected)
        {
            try
            {
                byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
                //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
                int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

                if(length > 0)//表示有效信息
                {
                    //$作用是将{}内容当做表达式
                    Console.WriteLine($"接收到的数据长度:{length}");
                    Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串
                    //Send(Encoding.UTF8.GetBytes("测试返回..."));                    
                    Array.Copy(buffer, 0, data, msgLength, length);//把接收到length长度的消息复制到数据缓冲区中msgLength索引后的位置
                    msgLength += length;//每次收到网络数据都要加上数据的长度
                    Handle();
                }
                else
                {
                    //客户端关闭了
                    client.Close();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine($"Receive Error:{e.Message}");
                client.Close();//出现错误断开客户端连接。
            }
        }            
    }

    private void Handle()
    {
        //包体大小(4) 协议ID(4) 包体(byte[])
        if (msgLength >= 8)
        {
            byte[] _size = new byte[4];
            Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
            int size = BitConverter.ToInt32(_size, 0);//获得包体大小

            //本次要拿的长度
            var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

            if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
            {
                //拿出id
                byte[] _id = new byte[4];
                Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                //包体
                byte[] body = new byte[size];
                Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                {
                    for (int i = 0; i < msgLength - _length; i++)
                    {
                        data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                    }
                }
                msgLength -= _length;//减掉已经取完的消息长度
                Console.WriteLine($"收到客户端请求:{id}");
                //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                switch (id)
                {
                    case 1001://注册请求
                        RegisterMsgHandle(body);
                        break;
                    case 1002://登录业务
                        LoginMsgHandle(body);
                        break;
                    case 1003://聊天业务
                        ChatMsgHandle(body);
                        break;

                }
            }
        }
    }

    //处理注册请求
    private void RegisterMsgHandle(byte[] obj)
    {
        
    }

    //处理登录请求
    private void LoginMsgHandle(byte[] obj)
    {

    }

    //处理聊天请求
    private void ChatMsgHandle(byte[] obj)
    {

    }

    //发送消息
    public async void Send(byte[] data)
    {
        try
        {
            //数据写入网络数据流中。就是发送
            await client.GetStream().WriteAsync(data, 0, data.Length);
            Console.WriteLine("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            client.Close();//关闭客户端
            Console.WriteLine($"send error:{e.Message}");
        }
    }
}

根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定

Unity客户端添加Scritps/Helper/MessageHelper.cs脚本

public class MessageHelper
{
    private static MessageHelper instance = new MessageHelper();
    public static MessageHelper Instance => instance;//单例
    
    byte[] data = new byte[4096];//接收消息的缓冲区
    int msgLength = 0;//接收到的消息长度

    //client接收到消息时,把buffer的数据复制到data数据缓冲区,数据长度加上接受的新有效数据流长度,handle处理数据
    public void CopyToData(byte[] buffer, int length)
    {
        Array.Copy(buffer, 0, data, msgLength, length);
        msgLength += length;
        Handle();
    }
    
    private void Handle()
    {
        //包体大小(4) 协议ID(4) 包体(byte[])
        if (msgLength >= 8)
        {
            byte[] _size = new byte[4];
            Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
            int size = BitConverter.ToInt32(_size, 0);//获得包体大小

            //本次要拿的长度
            var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

            if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
            {
                //拿出id
                byte[] _id = new byte[4];
                Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                //包体
                byte[] body = new byte[size];
                Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                {
                    for (int i = 0; i < msgLength - _length; i++)
                    {
                        data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                    }
                }
                msgLength -= _length;//减掉已经取完的消息长度
                Debug.Log($"收到客户端请求:{id}");
                //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                switch (id)
                {
                    case 1001://注册请求
                        RigisterMsgHandle(body);
                        break;
                    case 1002://登录业务
                        LoginMsgHandle(body);
                        break;
                    case 1003://聊天业务
                        ChatMsgHandle(body);
                        break;

                }
            }
        }
    }

    //按格式封装消息,发送到服务器
    public void SendToServer(int id, string str)
    {
        Debug.Log("ID:" + id);
        var body = Encoding.UTF8.GetBytes(str);
        byte[] send_buff = new byte[body.Length + 8];

        int size = body.Length;

        var _size = BitConverter.GetBytes(size);
        var _id = BitConverter.GetBytes(id);

        Array.Copy(_size, 0, send_buff, 0, 4);
        Array.Copy(_id, 0, send_buff, 4, 4);
        Array.Copy(body, 0, send_buff, 8, body.Length);

        Client.Instance.Send(send_buff);
    }
    
    //处理注册(结果)请求
    private void RigisterMsgHandle(byte[] obj)
    {
            
    }

    //处理登录(结果)请求
    private void LoginMsgHandle(byte[] obj)
    {

    }

    //处理聊天(转发)请求
    private void ChatMsgHandle(byte[] obj)
    {

    }
}
public async void Receive()
{
    while (client.Connected)
    {
        try
        {
            byte[] buff = new byte[4096];
            int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
            if (length > 0)
            {
                Debug.Log($"接收到的数据长度:{length}");
                //接收到处理CopyToData给MessageHelper处理信息
                MessageHelper.Instance.CopyToData(buff, length);
            }
            else
            {
                client.Close();
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            client.Close();
        }
    }
}

发送消息的时候必须按照格式发送

//按格式封装后,发送消息
public void SendToClient(int id, string str)
{
    //包体转换为byte[]
    var body = Encoding.UTF8.GetBytes(str);

    //包体大小(4) 协议ID(4) 包体内容
    byte[] send_buff = new byte[body.Length + 8];

    int size = body.Length;
    var _size = BitConverter.GetBytes(size);
    var _id = BitConverter.GetBytes(id);

    Array.Copy(_size, 0, send_buff, 0, 4);
    Array.Copy(_id, 0, send_buff, 4, 4);
    Array.Copy(body, 0, send_buff, 8, body.Length);

    Send(send_buff);
}

猜你喜欢

转载自blog.csdn.net/weixin_42264818/article/details/128831695