Unity —TCP网络协议

Unity —TCP网络协议

命令格式

客户端和服务器的通讯协议都我们自己定义的,项目开始的时候客户端和服务器都是定义好了命令格式的。之后的所有协议格式都根据我们定义好的来做。我这里约定 :在这里插入图片描述
Tcp的网络传输主要分Protobuf和Json,我目前用的是Protobuf,至于它有什么优点,想深入了解的同学可以自行百度。

Protobuf导入

由于最新protobuf c#代码不能直接在unity里面使用,需要做一些修改和调整;网络有最新的版本: https://github.com/bitcraftCoLtd/protobuf3-for-unity ,最新的protobuf 的protoc生成工具在生成c#代码的时候有bug, 采用proto2标准,我是用C#老版本工具来生成; unity protobuf的运行时的库我们放到3rd文件夹下, 只支持proto 2; unity prototools生成协议代码: ProtoGen放到项目目录下;
在这里插入图片描述
还记得上面说的命令格式吗? 客户端和服务的收发都是严格按照约定好的格式来操作的:
在这里插入图片描述
由于在Unity中二进制是用byte来表示,所有我们需要编辑一个类来把C# 对象数据序列化成二进制数据,

data_viewer

1: 读取byte[]里面的unsigned short, unsigned int 数据 ,
小尾: 低位存在低内存地址, 高位存在高内存地址;
大尾: 低位存在高内存地址地方;
2: write_ushort_le: 写入无符号2个字节;
3: write_uint_le: 写入无符号4个字节;
4: write_bytes: 写入数组;

using System;

public class data_viewer {
    public static void write_ushort_le(byte[] buf, int offset, ushort value) { 
        // value ---> byte[];
        byte[] byte_value = BitConverter.GetBytes(value);
        // 小尾,还是大尾?BitConvert 系统是小尾还是大尾;
        if (!BitConverter.IsLittleEndian) {
            Array.Reverse(byte_value);
        }

        Array.Copy(byte_value, 0, buf, offset, byte_value.Length);
    }

    public static void write_uint_le(byte[] buf, int offset, uint value) {
        // value ---> byte[];
        byte[] byte_value = BitConverter.GetBytes(value);
        // 小尾,还是大尾?BitConvert 系统是小尾还是大尾;
        if (!BitConverter.IsLittleEndian)
        {
            Array.Reverse(byte_value);
        }

        Array.Copy(byte_value, 0, buf, offset, byte_value.Length);
    }

    public static void write_bytes(byte[] dst, int offset, byte[] value) {
        Array.Copy(value, 0, dst, offset, value.Length);
    }

    public static ushort read_ushort_le(byte[] data, int offset) {
        int ret = (data[offset] | (data[offset + 1] << 8));

        return (ushort)ret;
    }
}

收发数据编码|解码

编写proto_man模块, 主要负责编码/解码数据包,它会它一个proto对象序列化为一个二进制数据,客户端拿到这个数组后再进行封包处理,最终向服务器发起请求。

using System;
using System.IO;
using System.Text;
using ProtoBuf;

public class cmd_msg {
    public int stype;
    public int ctype;
    public byte[] body; // protobuf, utf8 string json byte;
}

//主要负责编码/解码数据包
public class proto_man {
    private const int HEADER_SIZE = 8; // 2 stype, 2 ctype, 4utag, msg--> body;

    //protobuf 标准代码 数据序列化
    private static byte[] protobuf_serializer(ProtoBuf.IExtensible data)
    {
        using (MemoryStream m = new MemoryStream())
        {
            byte[] buffer = null;
            Serializer.Serialize(m, data);
            m.Position = 0;
            int length = (int)m.Length;
            buffer = new byte[length];
            m.Read(buffer, 0, length);
            return buffer;
        }
    }

    public static byte[] pack_protobuf_cmd(int stype, int ctype, ProtoBuf.IExtensible msg) {
        int cmd_len = HEADER_SIZE;
        byte[] cmd_body = null;
        if (msg != null) {
            cmd_body = protobuf_serializer(msg);
            cmd_len += cmd_body.Length;
        }
        
        byte[] cmd = new byte[cmd_len];
        // stype, ctype, utag(4保留), cmd_body
        data_viewer.write_ushort_le(cmd, 0, (ushort)stype);
        data_viewer.write_ushort_le(cmd, 2, (ushort)ctype);
        if (cmd_body != null) {
            data_viewer.write_bytes(cmd, HEADER_SIZE, cmd_body);
        }
        return cmd;
    }

    public static byte[] pack_json_cmd(int stype, int ctype, string json_msg) {
        int cmd_len = HEADER_SIZE;
        byte[] cmd_body = null;

        if (json_msg.Length > 0) { // utf8
            cmd_body = Encoding.UTF8.GetBytes(json_msg);
            cmd_len += cmd_body.Length;
        }

        byte[] cmd = new byte[cmd_len];
        // stype, ctype, utag(4保留), cmd_body
        data_viewer.write_ushort_le(cmd, 0, (ushort)stype);
        data_viewer.write_ushort_le(cmd, 2, (ushort)ctype);
        if (cmd_body != null)
        {
            data_viewer.write_bytes(cmd, HEADER_SIZE, cmd_body);
        }
        return cmd;
    }

    public static bool unpack_cmd_msg(byte[] data, int start, int cmd_len, out cmd_msg msg) {
        msg = new cmd_msg();
        msg.stype = data_viewer.read_ushort_le(data, start);
        msg.ctype = data_viewer.read_ushort_le(data, start + 2);

        int body_len = cmd_len - HEADER_SIZE;
        msg.body = new byte[body_len];
        Array.Copy(data, start + HEADER_SIZE, msg.body, 0, body_len);

        return true;
    }

    public static T protobuf_deserialize<T>(byte[] _data) {
        using (MemoryStream m = new MemoryStream(_data))
        {
            return Serializer.Deserialize<T>(m);
        }
    }

}

TCP数据包的封包/拆包协议

using System;
//TCP数据包的封包/拆包协议
public class tcp_packer {
    private const int HEADER_SIZE = 2;
    public static byte[] pack(byte[] cmd_data) {
        int len = cmd_data.Length;
        if (len > 65535 - 2) {
            return null;
        }

        int cmd_len = len + HEADER_SIZE;
        byte[] cmd = new byte[cmd_len];
        data_viewer.write_ushort_le(cmd, 0, (ushort)cmd_len);
        data_viewer.write_bytes(cmd, HEADER_SIZE, cmd_data);

        return cmd;
    }

    public static bool read_header(byte[] data, int data_len, out int pkg_size, out int head_size) {
        pkg_size = 0;
        head_size = 0;

        if (data_len < 2) {
            return false;
        }

        head_size = 2;
        pkg_size = (data[0] | (data[1] << 8));

        return true;
    }
}

简单说明一下 :在proto_man模块中,我们要去掉头8个字节,是因为和服务器定义,服务号占两个字节(stype),命令号占2个字节(ctype),用户标识占4个字节(utag)共8个字节,后面接的才是数据体。
在这里插入图片描述
在tcp_package模块中,因为需要判断这条消息是否为一个完整的包,所以需要占用头2个字节来标识。
在这里插入图片描述

network

network是挂在场景上的,不需要被删除的,主要用于连接服务器和收发数据,比较简单,都是一些固定的形式。

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

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

public class network : MonoBehaviour {
    public string server_ip;
    public int port;

    
    private Socket client_socket = null;
    private bool is_connect = false;
    private Thread recv_thread = null;

    private const int RECV_LEN = 8192;
    private byte[] recv_buf = new byte[RECV_LEN];
    private int recved;
    private byte[] long_pkg = null;
    private int long_pkg_size = 0;

    // event queque
    private Queue<cmd_msg> net_events = new Queue<cmd_msg>();
    // event listener, stype--> 监听者;
    public delegate void net_message_handler(cmd_msg msg);
    // 事件和监听的map
    private Dictionary<int, net_message_handler> event_listeners = new Dictionary<int, net_message_handler>(); 

    public static network _instance;
    public static network instance {
        get {
            return _instance;
        }
    }

    void Awake() {
        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

	// Use this for initialization
	void Start () {
        this.connect_to_server();
	}

    void OnDestroy() {
        // Debug.Log("network onDestroy!");
        this.close();
    }

    void OnApplicaitonQuit() {
        // Debug.Log("OnApplicaitonQuit");
        this.close();
    }
	
	// Update is called once per frame
	void Update () {
        lock (this.net_events) {
            while (this.net_events.Count > 0) { 
                cmd_msg msg = this.net_events.Dequeue();
                //  收到了一个命令包;
                if (this.event_listeners.ContainsKey(msg.stype)) {
                    this.event_listeners[msg.stype](msg);
                }
                // end 
            }
        }
	}

    void on_conntect_timeout() { 
    }

    void on_connect_error(string err) { 
    }

    void connect_to_server() {
        try {
            this.client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipAddress = IPAddress.Parse(this.server_ip);
            IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, this.port);

            IAsyncResult result = this.client_socket.BeginConnect(ipEndpoint, new AsyncCallback(this.on_connected), this.client_socket);
            bool success = result.AsyncWaitHandle.WaitOne(5000, true);              //超时
            if (!success) { // timeout;
                this.on_conntect_timeout();
            }
        }
        catch (System.Exception e) {
            Debug.Log(e.ToString());
            this.on_connect_error(e.ToString());
        }
    }

    void on_recv_tcp_cmd(byte[] data, int start, int data_len) {
        cmd_msg msg;
        proto_man.unpack_cmd_msg(data, start, data_len, out msg);
        if (msg != null) { 
            lock (this.net_events) { // recv thread
                this.net_events.Enqueue(msg);
            }
        }
    }

    void on_recv_tcp_data() { 
        byte[] pkg_data = (this.long_pkg != null) ? this.long_pkg : this.recv_buf;
        while (this.recved > 0) {
			int pkg_size = 0;
			int head_size = 0;

			if (!tcp_packer.read_header(pkg_data, this.recved, out pkg_size, out head_size)) {
				break;
			}

			if (this.recved < pkg_size) {
				break;
			}

			// unsigned char* raw_data = pkg_data + head_size;
            int raw_data_start = head_size;
            int raw_data_len = pkg_size - head_size;

            on_recv_tcp_cmd(pkg_data, raw_data_start, raw_data_len);
			// end 

			if (this.recved > pkg_size) {
                this.recv_buf = new byte[RECV_LEN];
				Array.Copy(pkg_data, pkg_size, this.recv_buf, 0, this.recved - pkg_size);
                pkg_data = this.recv_buf;
			}

			this.recved -= pkg_size;

			if (this.recved == 0 && this.long_pkg != null) {
				this.long_pkg = null;
                this.long_pkg_size = 0;
			}
		}
    }

    void thread_recv_worker() {
        if (this.is_connect == false) {
            return;
        }

        while (true) {
            if (!this.client_socket.Connected) {
                break;
            }

            try
            {
                int recv_len = 0;
                if (this.recved < RECV_LEN) {
                    recv_len = this.client_socket.Receive(this.recv_buf, this.recved, RECV_LEN - this.recved, SocketFlags.None);
                }
                else {
                    if (this.long_pkg == null) {
                        int pkg_size;
                        int head_size;
                        tcp_packer.read_header(this.recv_buf, this.recved, out pkg_size,  out head_size);
                        this.long_pkg_size = pkg_size;
                        this.long_pkg = new byte[pkg_size];
                        Array.Copy(this.recv_buf, 0, this.long_pkg, 0, this.recved);
                    }
                    recv_len = this.client_socket.Receive(this.long_pkg, this.recved, this.long_pkg_size - this.recved, SocketFlags.None);
                }

                if (recv_len > 0) {
                    this.recved += recv_len;
                    this.on_recv_tcp_data();
                }
            }
            catch (System.Exception e) {
                Debug.Log(e.ToString());
                this.client_socket.Disconnect(true);
                this.client_socket.Shutdown(SocketShutdown.Both);
                this.client_socket.Close();
                this.is_connect = false;
                break;
            }
        }
    }

    void on_connected(IAsyncResult iar) {
        try {
            Socket client = (Socket)iar.AsyncState;
            client.EndConnect(iar);

            this.is_connect = true;
            this.recv_thread = new Thread(new ThreadStart(this.thread_recv_worker));     //连接成功启用线程接收数据
            this.recv_thread.Start();

            Debug.Log("connect to server success" + this.server_ip + ":" + this.port + "!");
        }
        catch (System.Exception e) {
            Debug.Log(e.ToString());
            this.on_connect_error(e.ToString());
            this.is_connect = false;
        }
    }

    void close() {
        if (!this.is_connect) {
            return;
        }

        // abort recv thread
        if (this.recv_thread != null) {
            this.recv_thread.Abort();
        }
        // end

        if (this.client_socket != null && this.client_socket.Connected) {
            this.client_socket.Close();
        }
    }

    private void on_send_data(IAsyncResult iar)
    {
        try
        {
            Socket client = (Socket)iar.AsyncState;
            client.EndSend(iar);
        }
        catch (System.Exception e)
        {
            Debug.Log(e.ToString());
        }
    }

    public void send_protobuf_cmd(int stype, int ctype, ProtoBuf.IExtensible body) {
        byte[] cmd_data = proto_man.pack_protobuf_cmd(stype, ctype, body);
        if (cmd_data == null) {
            return;
        }

        byte[]tcp_pkg = tcp_packer.pack(cmd_data);

        // end 
        this.client_socket.BeginSend(tcp_pkg, 0, tcp_pkg.Length, SocketFlags.None, new AsyncCallback(this.on_send_data), this.client_socket);
        // end 
    }

    public void send_json_cmd(int stype, int ctype, string json_body)
    {
        byte[] cmd_data = proto_man.pack_json_cmd(stype, ctype, json_body);
        if (cmd_data == null) {
            return;
        }

        byte[] tcp_pkg = tcp_packer.pack(cmd_data);
        // end 
        this.client_socket.BeginSend(tcp_pkg, 0, tcp_pkg.Length, SocketFlags.None, new AsyncCallback(this.on_send_data), this.client_socket);
        // end 
    }

    public void add_service_listener(int stype, net_message_handler handler) {
        if (this.event_listeners.ContainsKey(stype)) {
            this.event_listeners[stype] += handler;
        }
        else {
            this.event_listeners.Add(stype, handler);
        }
    }

    public void remove_service_listener(int stype, net_message_handler handler) {
        if (!this.event_listeners.ContainsKey(stype)) {
            return;
        }
        this.event_listeners[stype] -= handler;
        if (this.event_listeners[stype] == null) {
            this.event_listeners.Remove(stype);
        }
    }
}

最近在和服务器对接这块相关,在编码和解码的问题上调了好久。有些东西太久没有去梳理真的会忘, 又从新复习了一遍,希望对想学习客户端和服务器网络对接的同学有所帮助。工程地址unity网络协议工程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值