本文只是初步实现了一个简单的TCP自定协议,更为复杂的协议可以根据这种方式去扩展。

TCP协议,通俗一点的讲,它是一种基于socket传输的由发送方和接收方事先协商好的一种消息包组成结构,主要由消息头和消息体组成。

众所周知,基于socket的信息交互有两个问题:

第一、接收方不能主动识别发送方发送的信息类型,例如A方(客户端)向B方(服务器)发送了一条信息:123,没有事先经过协议规定的话,B方不可能知道这条信息123到底是一个int型123还是一个string型123,甚至他根本就不知道这条信息解析出来是123,所以B方找不到处理这条信息的方式;

第二、接收方不能主动拆分发送方发送的多条信息,例如A方连续向B方发送了多条信息:123、456、789,由于网络延迟或B方接收缓冲区大小的不同设置,B方收到的信息可能是:1234、5678、9,也可能是123456789,也可能是1、2、3、4、5、6、7、8、9,还可能是更多意想不到的情况......

所以TCP协议就是为了解决这两个问题而存在的,当然为消息包加密也是它的另一个主要目的。

TCP协议的格式一般都是:消息头+消息体,消息头的长度是固定的,A方和B方都事先知道消息头长度,以及消息头中各个部位的值所代表的意义,其中包含了对消息体的描述,包括消息体长度,消息体里的消息类型,消息体的加密方式等。

B方在收到A方消息后,先按协议中规定的方式解析消息头,获取到里面对消息体的描述信息,他就可以知道消息体的长度是多少,以便于跟这条消息后面所紧跟的下一条消息进行拆分,他也可以从描述信息中得知消息体中的消息类型,并按正确的解析方式进行解析,从而完成信息的交互。

这里以一个简单的TCP协议作为例子:

第一步:定义协议(我们将协议定义如下)

消息头(28字节):(int)消息校验码4字节 + (int)消息体长度4字节 + (long)身份ID8字节 + (int)主命令4字节 + (int)子命令4字节 + (int)加密方式4字节

消息体:(int)消息1长度4字节 + (string)消息1 + (int)消息2长度4字节 + (string)消息2 + (int)消息3长度4字节 + (string)消息3 + ......

第二步:服务器建立监听

    //SocketTCPServer.cs
private static string ip = "127.0.0.1";
private static int port = 5690;
private static Socket socketServer;
public static List<Socket> listPlayer = new List<Socket>();
private static Socket sTemp;
///<summary>
///绑定地址并监听
///</summary>
///ip地址 端口 类型默认为TCP
public static void init(string ipStr, int iPort)
{
try
{
ip = ipStr;
port = iPort;
socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketServer.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
Thread threadListenAccept = new Thread(new ThreadStart(ListenAccept));
threadListenAccept.Start();
}
catch (ArgumentNullException e)
{
Debug.Log(e.ToString());
}
catch (SocketException e)
{
Debug.Log(e.ToString());
}
}
///<summary>
///监听用户连接
///</summary>
private static void ListenAccept()
{
socketServer.Listen(0); //对于socketServer绑定的IP和端口开启监听
sTemp = socketServer.Accept(); //如果在socketServer上有新的socket连接,则将其存入sTemp,并添加到链表
listPlayer.Add(sTemp);
Thread threadReceiveMessage = new Thread(new ThreadStart(ReceiveMessage));
threadReceiveMessage.Start();
while (true)
{
sTemp = socketServer.Accept();
listPlayer.Add(sTemp);
}
}


第三步:客户端连接服务器

 //SocketTCPClient.cs
private static string ip = "127.0.0.1";
private static int port = 5690;
private static Socket socketClient;
public static List<string> listMessage = new List<string>();
///<summary>
///创建一个SocketClient实例
///</summary>
///ip地址 端口 类型默认为TCP
public static void CreateInstance(string ipStr, int iPort)
{
ip = ipStr;
port = iPort;
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ConnectServer();
}
/// <summary>
///连接服务器
/// </summary>
private static void ConnectServer()
{
try
{
socketClient.Connect(IPAddress.Parse(ip), port);
Thread threadConnect = new Thread(new ThreadStart(ReceiveMessage));
threadConnect.Start();
}
catch (ArgumentNullException e)
{
Debug.Log(e.ToString());
}
catch (SocketException e)
{
Debug.Log(e.ToString());
}
}

第四步:封包以及发送消息包 

    /// <summary>
/// 构建消息数据包
/// </summary>
/// <param name="Crccode">消息校验码,判断消息开始</param>
/// <param name="sessionid">用户登录成功之后获得的身份ID</param>
/// <param name="command">主命令</param>
/// <param name="subcommand">子命令</param>
/// <param name="encrypt">加密方式</param>
/// <param name="MessageBody">消息内容(string数组)</param>
/// <returns>返回构建完整的数据包</returns>
public static byte[] BuildDataPackage(int Crccode,long sessionid, int command,int subcommand, int encrypt, string[] MessageBody)
{
//消息校验码默认值为0x99FF
Crccode = 65433;
//消息头各个分类数据转换为字节数组(非字符型数据需先转换为网络序 HostToNetworkOrder:主机序转网络序)
byte[] CrccodeByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Crccode));
byte[] sessionidByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionid));
byte[] commandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(command));
byte[] subcommandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(subcommand));
byte[] encryptByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(encrypt));
//计算消息体的长度
int MessageBodyLength = 0;
for (int i = 0; i < MessageBody.Length; i++)
{
if (MessageBody[i] == "")
break;
MessageBodyLength += Encoding.UTF8.GetBytes(MessageBody[i]).Length;
}
//定义消息体的字节数组(消息体长度MessageBodyLength + 每个消息前面有一个int变量记录该消息字节长度)
byte[] MessageBodyByte = new byte[MessageBodyLength + MessageBody.Length*4];
//记录已经存入消息体数组的字节数,用于下一个消息存入时检索位置
int CopyIndex = 0;
for (int i = 0; i < MessageBody.Length; i++)
{
//单个消息
byte[] bytes = Encoding.UTF8.GetBytes(MessageBody[i]);
//先存入单个消息的长度
BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bytes.Length)).CopyTo(MessageBodyByte, CopyIndex);
CopyIndex += 4;
bytes.CopyTo(MessageBodyByte, CopyIndex);
CopyIndex += bytes.Length;
}
//定义总数据包(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 + 消息体)
byte[] totalByte = new byte[28 + MessageBodyByte.Length];
//组合数据包头部(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节)
CrccodeByte.CopyTo(totalByte,0);
BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MessageBodyByte.Length)).CopyTo(totalByte,4);
sessionidByte.CopyTo(totalByte, 8);
commandByte.CopyTo(totalByte, 16);
subcommandByte.CopyTo(totalByte, 20);
encryptByte.CopyTo(totalByte, 24);
//组合数据包体
MessageBodyByte.CopyTo(totalByte,28);
Debug.Log("发送数据包的总长度为:"+ totalByte.Length);
return totalByte;
}
///<summary>
///发送信息
///</summary>
public static void SendMessage(byte[] sendBytes)
{
//确定是否连接
if (socketClient.Connected)
{
//获取远程终结点的IP和端口信息
IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint;
socketClient.Send(sendBytes, sendBytes.Length, 0);
}
}

第五步:接收消息以及解析消息包 

  ///<summary>
///接收消息
///</summary>
private static void ReceiveMessage()
{
while (true)
{
//接受消息头(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 = 28字节)
int HeadLength = 28;
//存储消息头的所有字节数
byte[] recvBytesHead = new byte[HeadLength];
//如果当前需要接收的字节数大于0,则循环接收
while (HeadLength > 0)
{
byte[] recvBytes1 = new byte[28];
//将本次传输已经接收到的字节数置0
int iBytesHead = 0;
//如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
if (HeadLength >= recvBytes1.Length)
{
iBytesHead = socketClient.Receive(recvBytes1, recvBytes1.Length, 0);
}
else
{
iBytesHead = socketClient.Receive(recvBytes1, HeadLength, 0);
}
//将接收到的字节数保存
recvBytes1.CopyTo(recvBytesHead, recvBytesHead.Length - HeadLength);
//减去已经接收到的字节数
HeadLength -= iBytesHead;
}
//接收消息体(消息体的长度存储在消息头的4至8索引位置的字节里)
byte[] bytes = new byte[4];
Array.Copy(recvBytesHead, 4, bytes, 0, 4);
int BodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0));
//存储消息体的所有字节数
byte[] recvBytesBody = new byte[BodyLength];
//如果当前需要接收的字节数大于0,则循环接收
while (BodyLength > 0)
{
byte[] recvBytes2 = new byte[BodyLength < 1024 ? BodyLength : 1024];
//将本次传输已经接收到的字节数置0
int iBytesBody = 0;
//如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
if (BodyLength >= recvBytes2.Length)
{
iBytesBody = socketClient.Receive(recvBytes2, recvBytes2.Length, 0);
}
else
{
iBytesBody = socketClient.Receive(recvBytes2, BodyLength, 0);
}
//将接收到的字节数保存
recvBytes2.CopyTo(recvBytesBody, recvBytesBody.Length - BodyLength);
//减去已经接收到的字节数
BodyLength -= iBytesBody;
}
//一个消息包接收完毕,解析消息包
UnpackData(recvBytesHead,recvBytesBody);
}
}
/// <summary>
/// 解析消息包
/// </summary>
/// <param name="Head">消息头</param>
/// <param name="Body">消息体</param>
public static void UnpackData(byte[] Head, byte[] Body)
{
byte[] bytes = new byte[4];
Array.Copy(Head, 0, bytes, 0, 4);
Debug.Log("接收到数据包中的校验码为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[8];
Array.Copy(Head, 8, bytes, 0, 8);
Debug.Log("接收到数据包中的身份ID为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 16, bytes, 0, 4);
Debug.Log("接收到数据包中的数据主命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 20, bytes, 0, 4);
Debug.Log("接收到数据包中的数据子命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 24, bytes, 0, 4);
Debug.Log("接收到数据包中的数据加密方式为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[Body.Length];
for (int i = 0; i < Body.Length;)
{
byte[] _byte = new byte[4];
Array.Copy(Body, i, _byte, 0, 4);
i += 4;
int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0)); _byte = new byte[num];
Array.Copy(Body, i, _byte, 0, num);
i += num;
Debug.Log("接收到数据包中的数据有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));
}
}


第六步:测试,同时发送两个包到服务器 

private string Ip = "127.0.0.1";
private int Port = 5690;
void Start()
{
SocketTCPServer.init(Ip, Port); //开启并初始化服务器
SocketTCPClient.CreateInstance(Ip, Port); //客户端连接服务器
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
string[] str = {"测试字符串1","test1","test11"};
SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 2, 3, 4,5, str));
string[] str2 = { "我是与1同时发送的测试字符串2,请注意我是否与其他信息粘包", "test2", "test22" };
SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 6, 7, 8, 9, str2));
}
}
void OnApplicationQuit()
{
SocketTCPClient.Close();
SocketTCPServer.Close();
}

输出结果如下,可见粘包问题已得到解决:



Unity C# 自定义TCP传输协议以及封包拆包、解决粘包问题的更多相关文章

  1. TCP传输协议使用

    TCP传输协议,也称之为套接字连接,比较安全,三次握手!,必须确保对方计算机存在,才能连接,而且是长时间连接. 缺点是传输速度有点慢. 你用 socket 去连接 ServiceSocaket 服务器 ...

  2. Netty系列(四)TCP拆包和粘包

    Netty系列(四)TCP拆包和粘包 一.拆包和粘包问题 (1) 一个小的Socket Buffer问题 在基于流的传输里比如 TCP/IP,接收到的数据会先被存储到一个 socket 接收缓冲里.不 ...

  3. Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题

    即使关闭nagle算法,也不能解决粘包问题 https://waylau.com/netty-4-user-guide/Getting%20Started/Dealing%20with%20a%20S ...

  4. Netty处理TCP拆包、粘包

    Netty实践(二):TCP拆包.粘包问题-学海无涯 心境无限-51CTO博客 http://blog.51cto.com/zhangfengzhe/1890577 2017-01-09 21:56: ...

  5. tcp粘包、解决粘包问题

    目录 subproess模块 TCP粘包问题 粘包两种情况 解决粘包问题 struct模块的使用 使用struct模块解决粘包 优化解决粘包问题 上传大文件 服务端 客户端 UDP协议 upd套接字 ...

  6. 深入了解Netty【八】TCP拆包、粘包和解决方案

    1.TCP协议传输过程 TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包: 发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由 ...

  7. TCP粘包问题的解决方案02——利用readline函数解决粘包问题

      主要内容: 1.read,write 与 recv,send函数. recv函数只能用于套接口IO ssize_t recv(int sockfd,void * buff,size_t len,i ...

  8. day31——recv工作原理、高大上版解决粘包方式、基于UDP协议的socket通信

    day31 recv工作原理 源码解释: Receive up to buffersize bytes from the socket. 接收来自socket缓冲区的字节数据, For the opt ...

  9. netty权威指南学习笔记三——TCP粘包/拆包之粘包现象

    TCP是个流协议,流没有一定界限.TCP底层不了解业务,他会根据TCP缓冲区的实际情况进行包划分,在业务上,一个业务完整的包,可能会被TCP底层拆分为多个包进行发送,也可能多个小包组合成一个大的数据包 ...

随机推荐

  1. Playground中格式注释语法

    类似于Ruby的ruby document,Xcode的Playground自身也提供一些嵌入文档中的格式注释的语法. 我们先定义一个简单的类: class A{ } 按住opt点击class A,你 ...

  2. 在ubuntu上搭建交叉编译环境---arm-none-eabi-gcc

    最近要开始搞新项目,基于arm的高通方案的项目. 那么,如何在ubuntu上搭建这个编译环境呢? 1.找到相关的安装包:http://download.csdn.net/download/storea ...

  3. 【安卓开发】Android为什么选择binder

    Binder (Android技术内幕): 在上面这些可供选择的方式中,Android使用得最多也最被认可的还是Binder机制. 为什么会选择Binder来作为进程之间的通信机制呢?因为Binder ...

  4. [nginx]查看安装了哪些模块

    有时候安装的时候不知道哪些模块忘了安装需要查看下已经安装的模块. 查看安装了哪些模块 $ nginx -V nginx version: nginx/1.4.6 (Ubuntu) built by g ...

  5. Struts 1 之配置文件

    web.xml中配置Struts的入口Servlet--ActionServlet,ActionServlet不负责任何的业务处理,它只是查找Action名单,找到path属性与URL属性一致的Act ...

  6. How to generate the complex data regularly to Ministry of Transport of P.R.C by DB Query Analyzer

    How to generate the complex data regularly to Ministry of Transport of P.R.C by DB Query Analyzer 1 ...

  7. CUSTOM.PLL的使用

    在开发中对系统标准form的修改一般不建议修改系统原有FORM,对所需要修改的内容一般写在CUSTOM.PLL里即可,应为每个form运行的时候都会调用CUSTOM.PLL具体概念性东西可参考网上资料 ...

  8. Swift基础语法(常量变量、数据类型、元组、可选、断言)

    本文来自Swift中文开发组,感谢翻译者的分享. 本文将分几部分对Swift对ios的语法做讲解.本文为第一节,主要讲解基础语法. 常量和变量 常量和变量把一个名字(比如maximumNumberOf ...

  9. Windows平台下的多线程编程

    线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件.信号标识及动态分配的内存等.一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度 ...

  10. 【iOS 开发】基本 UI 控件详解 (UIButton | UITextField | UITextView | UISwitch)

    博客地址 : http://blog.csdn.net/shulianghan/article/details/50051499 ; 一. UI 控件简介 1. UI 控件分类 UI 控件分类 : 活 ...