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网络协议工程