提起Tcp,相信不管是老鸟还是萌新多多少少都听说过一些概念,在网络编程中,Tcp也是一个必须掌握的内容。
而在Unity3D的开发当中,Tcp通讯更是重中之重,不懂Tcp,日常开发工作就会变得尤为艰难甚至寸步难行。
本篇文章我就详细的记录一下我所了解的Unity中的Tcp通讯,并逐步去实现一个比较常用的Tcp通讯框架。
首先了解两条比较基础的东西:
- Tcp的概念:Tcp是网络通讯协议中的一种,学过计算机网络就应该知道,网络协议模型共有5层,Tcp位列运输层中,是一种面向连接的安全可靠全双工通信协议。具体概念不多做介绍,如果对此有些迷惑可以看这篇文章,https://blog.youkuaiyun.com/Sqdmn/article/details/103581960
- Tcp通信过程:这里主要了解3次握手和4次挥手就足够了,可以深入了解一下3次握手的过程,以及为什么要3次握手。依然看https://blog.youkuaiyun.com/Sqdmn/article/details/103581960
要实现C#的Tcp通讯,需要使用System.Net.Sockets这个命名空间下的Socket类:
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
可以看到,创建Socket对象需要3个参数,下面介绍这个3个参数的含义。
1.AddressFamily 枚举:
AppleTalk | 16 | AppleTalk 地址。 |
Atm | 22 | 本机 ATM 服务地址。 |
Banyan | 21 | Banyan 地址。 |
Ccitt | 10 | CCITT 协议(如 X.25)的地址。 |
Chaos | 5 | MIT CHAOS 协议的地址。 |
Cluster | 24 | Microsoft 群集产品的地址。 |
DataKit | 9 | Datakit 协议的地址。 |
DataLink | 13 | 直接数据链接接口地址。 |
DecNet | 12 | DECnet 地址。 |
Ecma | 8 | 欧洲计算机制造商协会 (ECMA) 地址。 |
FireFox | 19 | FireFox 地址。 |
HyperChannel | 15 | NSC Hyperchannel 地址。 |
Ieee12844 | 25 | IEEE 1284.4 工作组地址。 |
ImpLink | 3 | ARPANET IMP 地址。 |
InterNetwork | 2 | IP 版本 4 的地址。 |
InterNetworkV6 | 23 | IP 版本 6 的地址。 |
Ipx | 6 | IPX 或 SPX 地址。 |
Irda | 26 | IrDA 地址。 |
Iso | 7 | ISO 协议的地址。 |
Lat | 14 | LAT 地址。 |
Max | 29 | MAX 地址。 |
NetBios | 17 | NetBios 地址。 |
NetworkDesigners | 28 | 支持网络设计器 OSI 网关的协议的地址。 |
NS | 6 | Xerox NS 协议的地址。 |
Osi | 7 | OSI 协议的地址。 |
Pup | 4 | PUP 协议的地址。 |
Sna | 11 | IBM SNA 地址。 |
Unix | 1 | Unix 本地到主机地址。 |
Unknown | -1 | 未知的地址族。 |
Unspecified | 0 | 未指定的地址族。 |
VoiceView | 18 | VoiceView 地址。 |
2.SocketType 枚举:
Dgram | 2 | 支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。 消息可能会丢失或重复并可能在到达时不按顺序排列。 Socket 类型的 Dgram 在发送和接收数据之前不需要任何连接,并且可以与多个对方主机进行通信。 Dgram 使用数据报协议 ( |
Raw | 3 | 支持对基础传输协议的访问。 通过使用 Raw,可以使用 Internet 控制消息协议 ( |
Rdm | 4 | 支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。 RDM(以可靠方式发送的消息)消息会依次到达,不会重复。 此外,如果消息丢失,将会通知发送方。 如果使用 Rdm 初始化 Socket,则在发送和接收数据之前无需建立远程主机连接。 利用 Rdm,您可以与多个对方主机进行通信。 |
Seqpacket | 5 | 在网络上提供排序字节流的面向连接且可靠的双向传输。 Seqpacket 不重复数据,它在数据流中保留边界。 Seqpacket 类型的 Socket 与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。 |
Stream | 1 | 支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。 此类型的 Socket 与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。 Stream 使用传输控制协议 ( |
Unknown | -1 | 指定未知的 Socket 类型。 |
3.ProtocolType 枚举:
Ggp | 3 | 网关到网关协议。 |
Icmp | 1 | 网际消息控制协议。 |
IcmpV6 | 58 | 用于 IPv6 的 Internet 控制消息协议。 |
Idp | 22 | Internet 数据报协议。 |
Igmp | 2 | 网际组管理协议。 |
IP | 0 | 网际协议。 |
IPSecAuthenticationHeader | 51 | IPv6 身份验证头。 有关详细信息,请参阅 https://www.ietf.org 上的 RFC 2292,第 2.2.1 节。 |
IPSecEncapsulatingSecurityPayload | 50 | IPv6 封装式安全措施负载头。 |
IPv4 | 4 | Internet 协议版本 4。 |
IPv6 | 41 | Internet 协议版本 6 (IPv6)。 |
IPv6DestinationOptions | 60 | IPv6 目标选项头。 |
IPv6FragmentHeader | 44 | IPv6 片段头。 |
IPv6HopByHopOptions | 0 | IPv6 逐跳选项头。 |
IPv6NoNextHeader | 59 | IPv6 No Next 头。 |
IPv6RoutingHeader | 43 | IPv6 路由头。 |
Ipx | 1000 | Internet 数据包交换协议。 |
ND | 77 | 网络磁盘协议(非正式)。 |
Pup | 12 | PARC 通用数据包协议。 |
Raw | 255 | 原始 IP 数据包协议。 |
Spx | 1256 | 顺序包交换协议。 |
SpxII | 1257 | 顺序包交换协议第 2 版。 |
Tcp | 6 | 传输控制协议。 |
Udp | 17 | 用户数据报协议。 |
Unknown | -1 | 未知协议。 |
Unspecified | 0 | 未指定的协议。 |
上面分别列举了3个枚举所有的值及对应的含义,实际上
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
这行代码的意思就是使用IPV4地址,全双工安全可靠通讯,Tcp协议来创建 一个Socket对象。
Socket工作流程如下:
1.调用Connect方法连接服务器,连接失败则跳出
public void Connect(string ip, int port)
{
m_IP = ip;
m_Port = port;
m_Socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
try
{
m_Socket.Connect(new IPEndPoint(IPAddress.Parse(ip), port));
m_ReceiveStream = new MemoryStream();
m_IsConnected = true;
StartReceive();
if (OnConnectSuccess != null)
{
OnConnectSuccess();
}
Debug.Log("连接服务器:" + ip + "成功!");
}
catch (Exception e)
{
if (OnConnectFail != null)
{
OnConnectFail();
}
Debug.Log(e.Message);
}
}
2.使用BeginReceive方法,使当前进入阻塞状态,等待接收服务端发送的消息,成功接收到消息后对应的数据会写入到一个字节流中等待处理
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer,0,m_ReceiveBuffer.Length,SocketFlags.None,OnReceive, m_Socket);
}
3.当接收到消息时,调用EndReceive方法结束本次数据接收,然后开始解包,解包成功再次调用BeginReceive方法开始新一轮数据接
private void OnReceive(IAsyncResult ir)
{
if (!m_IsConnected) return;
try
{
int length = m_Socket.EndReceive(ir);
if (length < 1)//包长为0
{
Debug.Log("服务器断开连接");
Close();
return;
}
//1.设置数据流指针的到尾部
m_ReceiveStream.Position = m_ReceiveStream.Length;
//2.把接收到的数据全部写入数据流
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);
//3.一个数据包至少包含包长,包的编码两部分信息,这两部分信息都用ushort表示,而一个
// ushort占2个byte,所以一个包的长度至少是4
if (m_ReceiveStream.Length < 4)
{
StartReceive();
return;
}
//4.循环解包
while (true)
{
m_ReceiveStream.Position = 0;
byte[] msgLenBuffer = new byte[2];
m_ReceiveStream.Read(msgLenBuffer, 0, 2);
//5.整个数据的包体中是包含了包体编码这部分数据的,所以需要+2
int msgLen = BitConverter.ToUInt16(msgLenBuffer, 0) + 2;
//6.整个消息的包体长度包含了包长,包的编码及具体数据,所以这个实际长度需要在msgLen
// 的基础上再+2
int fullLen = 2 + msgLen;
//7.接收到的包体长度小于实际长度,说明这不是一个完整包,跳出循环继续下一次接收
if (m_ReceiveStream.Length < fullLen)
{
break;
}
byte[] msgBuffer = new byte[msgLen];
m_ReceiveStream.Position = 2;
m_ReceiveStream.Read(msgBuffer, 0, msgLen);
lock (m_ReceiveQueue)
{
m_ReceiveQueue.Enqueue(msgBuffer);//把真实数据入队,等待主线程处理
}
int remainLen = (int)m_ReceiveStream.Length - fullLen;
if (remainLen < 1)
{
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
break;
}
m_ReceiveStream.Position = fullLen;
byte[] remainBuffer = new byte[remainLen];
m_ReceiveStream.Read(remainBuffer, 0, remainLen);
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Write(remainBuffer, 0, remainLen);
remainBuffer = null;
}
}
catch(Exception e)
{
Debug.Log("++服务器断开连接," + e.Message);
Close();
return;
}
StartReceive();
}
这里包含了粘包处理的代码。粘包问题可能比较难理解,这里进行一下分析:
- 什么是粘包:一次通讯包含了多条数据
- 为什么会产生粘包:当数据包很小时,Tcp协议会把较小的数据包合并到一起,使一些零散的小包通过一次通讯就可以传输完毕。
- 如何解决粘包:这里采用我最熟悉的也是最常用的方式,包体定长。包体定长就是指无论客户端还是服务端,在发送数据包之前,需要把这个包的长度写入到包头,在解包的时候首先读出包体长度msgLen,通过计算得出本次通讯实际的包体长度fullLen = msgLen+2,如果接收到的包体长度m_ReceiverBuffer.Length大于实际长度fullLen,则可以认为发生粘包,此时只处理msgLen这个长度的包即可,剩余的数据重新写入m_ReceiverBuffer,下一次接收的包会和这个剩余包重新组成一个完整包。
4.得到真实的数据,把真实数据入队,并在Unity主线程的update中去处理
private void Update()
{
if (m_IsConnected)
CheckReceiveBuffer();
}
private void CheckReceiveBuffer()
{
while (true)
{
if (m_CheckCount > 5)//每帧处理5条数据
{
m_CheckCount = 0;
break;
}
m_CheckCount++;
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1)
{
break;
}
byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;
using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);//读包的编码
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);//得到包编码
ms.Read(msgContent, 0, msgContent.Length);
}
if (onReceive != null)
{
onReceive(msgCode, msgContent);
}
}
}
}
为什么需要在Update中去处理呢?因为BeginReceive是多线程异步接收到数据的,而unity的api不允许在非主线程中去访问,所以要把在非主线程中得到的数据入队,并在unity主线程中去处理。
以上是Tcp通讯在Unity中的发起连接,收包,拆包的过程。
下面来了解发包的过程。
上面提到过为了解决粘包,需要把消息包体进行定长,所以发包第一步就是先把包体长度写入数据流,然后把消息编码写入数据流,最后才写入真实的要发送的数据内容,调用BeginSend进行异步发送。
public void Send(ushort msgCode, byte[] buffer)
{
if (!m_IsConnected) return;
byte[] sendMsgBuffer = null;
using (MemoryStream ms = new MemoryStream())
{
int msgLen = buffer.Length;
byte[] lenBuffer = BitConverter.GetBytes((ushort)msgLen);
byte[] msgCodeBuffer = BitConverter.GetBytes(msgCode);
ms.Write(lenBuffer, 0, lenBuffer.Length);
ms.Write(msgCodeBuffer, 0, msgCodeBuffer.Length);
ms.Write(buffer, 0, msgLen);
sendMsgBuffer = ms.ToArray();
}
lock (m_SendQueue)
{
m_SendQueue.Enqueue(sendMsgBuffer);
CheckSendBuffer();
}
}
private void CheckSendBuffer()
{
lock (m_SendQueue)
{
if (m_SendQueue.Count > 0)
{
byte[] buffer = m_SendQueue.Dequeue();
m_Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, m_Socket);
}
}
}
private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}
这里为了保证线程安全仍然需要把数据入队,在确认到消息成功发送后才进行下一次数据的发送。
以上就是Unity中实现Tcp的全部内容。下面贴上整个通讯框架的代码,直接调用Connect方法进行连接,连接成功后调用Send方法进行发送
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Policy;
using UnityEngine;
using UnityEngine.SocialPlatforms;
public class SocketMgr : MonoBehaviour
{
public static SocketMgr Instance = null;
public Action<ushort, byte[]> onReceive = null;
public Action OnConnectSuccess = null;
public Action OnConnectFail = null;
public Action OnDisConnect = null;
public bool IsConnected
{
get
{
return m_IsConnected;
}
}
private void Awake()
{
Instance = this;
m_ReceiveBuffer = new byte[1024 * 512];
m_SendQueue = new Queue<byte[]>();
m_ReceiveQueue = new Queue<byte[]>();
m_OnEventCallQueue = new Queue<Action>();
}
public void Connect(string ip, int port)
{
m_IP = ip;
m_Port = port;
m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
m_Socket.Connect(new IPEndPoint(IPAddress.Parse(ip), port));
m_ReceiveStream = new MemoryStream();
m_IsConnected = true;
StartReceive();
if (OnConnectSuccess != null)
{
OnConnectSuccess();
}
Debug.Log("连接服务器:" + ip + "成功!");
}
catch (Exception e)
{
if (OnConnectFail != null)
{
OnConnectFail();
}
Debug.Log(e.Message);
}
}
public void Close()
{
if (!m_IsConnected) return;
m_IsConnected = false;
try { m_Socket.Shutdown(SocketShutdown.Both); }
catch { }
m_Socket.Close();
m_SendQueue.Clear();
m_ReceiveQueue.Clear();
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Close();
m_Socket = null;
m_ReceiveStream = null;
m_OnEventCallQueue.Enqueue(OnDisConnect);
}
public void Send(ushort msgCode, byte[] buffer)
{
if (!m_IsConnected) return;
byte[] sendMsgBuffer = null;
using (MemoryStream ms = new MemoryStream())
{
int msgLen = buffer.Length;
byte[] lenBuffer = BitConverter.GetBytes((ushort)msgLen);
byte[] msgCodeBuffer = BitConverter.GetBytes(msgCode);
ms.Write(lenBuffer, 0, lenBuffer.Length);
ms.Write(msgCodeBuffer, 0, msgCodeBuffer.Length);
ms.Write(buffer, 0, msgLen);
sendMsgBuffer = ms.ToArray();
}
lock (m_SendQueue)
{
m_SendQueue.Enqueue(sendMsgBuffer);
CheckSendBuffer();
}
}
private void Update()
{
if (m_IsConnected)
CheckReceiveBuffer();
if(m_OnEventCallQueue.Count > 0)
{
Action a = m_OnEventCallQueue.Dequeue();
if (a != null) a();
}
}
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer, 0, m_ReceiveBuffer.Length, SocketFlags.None, OnReceive, m_Socket);
}
private void OnReceive(IAsyncResult ir)
{
if (!m_IsConnected) return;
try
{
int length = m_Socket.EndReceive(ir);
if (length < 1)
{
Debug.Log("服务器断开连接");
Close();
return;
}
m_ReceiveStream.Position = m_ReceiveStream.Length;
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);
if (m_ReceiveStream.Length < 4)
{
StartReceive();
return;
}
while (true)
{
m_ReceiveStream.Position = 0;
byte[] msgLenBuffer = new byte[2];
m_ReceiveStream.Read(msgLenBuffer, 0, 2);
int msgLen = BitConverter.ToUInt16(msgLenBuffer, 0) + 2;
int fullLen = 2 + msgLen;
if (m_ReceiveStream.Length < fullLen)
{
break;
}
byte[] msgBuffer = new byte[msgLen];
m_ReceiveStream.Position = 2;
m_ReceiveStream.Read(msgBuffer, 0, msgLen);
lock (m_ReceiveQueue)
{
m_ReceiveQueue.Enqueue(msgBuffer);
}
int remainLen = (int)m_ReceiveStream.Length - fullLen;
if (remainLen < 1)
{
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
break;
}
m_ReceiveStream.Position = fullLen;
byte[] remainBuffer = new byte[remainLen];
m_ReceiveStream.Read(remainBuffer, 0, remainLen);
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Write(remainBuffer, 0, remainLen);
remainBuffer = null;
}
}
catch(Exception e)
{
Debug.Log("++服务器断开连接," + e.Message);
Close();
return;
}
StartReceive();
}
private void CheckSendBuffer()
{
lock (m_SendQueue)
{
if (m_SendQueue.Count > 0)
{
byte[] buffer = m_SendQueue.Dequeue();
m_Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, m_Socket);
}
}
}
private void CheckReceiveBuffer()
{
while (true)
{
if (m_CheckCount > 5)
{
m_CheckCount = 0;
break;
}
m_CheckCount++;
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1)
{
break;
}
byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;
using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);
ms.Read(msgContent, 0, msgContent.Length);
}
if (onReceive != null)
{
onReceive(msgCode, msgContent);
}
}
}
}
private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}
private void OnDestroy()
{
Close();
m_SendQueue = null;
m_ReceiveQueue = null;
m_ReceiveStream = null;
m_ReceiveBuffer = null;
m_OnEventCallQueue.Clear();
m_OnEventCallQueue = null;
}
private Queue<Action> m_OnEventCallQueue = null;
private Queue<byte[]> m_SendQueue = null;
private Queue<byte[]> m_ReceiveQueue = null;
private MemoryStream m_ReceiveStream = null;
private byte[] m_ReceiveBuffer = null;
private bool m_IsConnected = false;
private string m_IP = string.Empty;
private int m_CheckCount = 0;
private int m_Port = int.MaxValue;
private Socket m_Socket = null;
}
这是我在优快云的第一篇博客,文笔不是很好,写的也比较乱
下一篇就去实现服务端的Tcp,把这篇内容真正的跑起来
也希望我的文笔通过不断的写作能逐渐得到提高。