前言


TCP通信承载着一个应用程序的数据传输,而在Unity的开发当中,TCP通信也是重中之重,不懂TCP通信、不懂网络编程,日常开发工作就会变得尤为艰难。

本篇文章我就详细的记录一下我所了解的Unity中的TCP通信,并逐步去实现一个比较常用的TCP通信框架。

首先了解两条比较基础的东西:

  • TCP的概念:TCP是网络通讯协议中的一种,学过计算机网络就应该知道,网络协议模型共有5层,TCP议位列运输层,是一种面向连接的安全可靠全双工通信协议。

  • TCP通信过程:这里主要了解3次握手和4次挥手就足够了,可以深入了解一下3次握手的过程,以及为什么要3次握手。

具体概念不多做介绍了,如果对此有迷惑可以看这篇文章

下面开始使用Socket一步一步实现Unity客户端的TCP连接。


构建Socket对象


要实现C#的TCP通信,需要使用System.Net.Sockets这个命名空间下的Socket类:

1
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

可以看到,创建Socket对象需要3个参数,下面介绍这个3个参数的含义。


AddressFamily 枚举


枚举名含义
Unknown-1未知的地址族
Unspecified0未指定的地址族
Unix1Unix本地到主机地址
InterNetwork2IP版本4的地址
ImpLink3ARPANET IMP地址
Pup4PUP协议的地址
Chaos5MIT CHAOS协议的地址
NS6Xerox NS协议的地址
Ipx6IPX或SPX地址
Iso7ISO协议的地址
Osi7OSI协议的地址
Ecma8欧洲计算机制造商协会(ECMA)地址
DataKit9Datakit协议的地址
Ccitt10CCITT协议(如 X.25)的地址
Sna11IBM SNA地址
DecNet12DECnet地址
DataLink13直接数据链接接口地址
Lat14LAT地址
HyperChannel15NSC Hyperchannel地址
AppleTalk16AppleTalk
NetBios17NetBios地址
VoiceView18VoiceView地址
FireFox19FireFox地址
Banyan21Banyan地址
Atm22本机ATM服务地址
InterNetworkV623IP版本6的地址
Cluster24Microsoft群集产品的地址
Ieee1284425IEEE 1284.4工作组地址
Irda26IrDA地址
NetworkDesigners28支持网络设计器OSI网关的协议的地址
Max29MAX地址

SocketType 枚举


枚举名含义
Unknown-1指定未知的Socket类型
Stream1支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。此类型的Socket与单个对方主机通信,并且在通信开始之前需要建立远程主机连接。Stream使用传输控制协议 (ProtocolType.Tcp)和AddressFamily。InterNetwork地址族
Dgram2支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。消息可能会丢失或重复并可能在到达时不按顺序排列。Socket类型的Dgram在发送和接收数据之前不需要任何连接,并且可以与多个对方主机进行通信。Dgram使用数据报协议(ProtocolType.Udp)和AddressFamily.InterNetwork地址族
Raw3支持对基础传输协议的访问。通过使用Raw,可以使用Internet控制消息协议(ProtocolType.Icmp)和Internet组管理协议(ProtocolType.Igmp)这样的协议来进行通信。在发送时,您的应用程序必须提供完整的IP标头。所接收的数据报在返回时会保持其IP标头和选项不变
Rdm4支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。RDM(以可靠方式发送的消息)消息会依次到达,不会重复。此外,如果消息丢失,将会通知发送方。如果使用Rdm初始化Socket,则在发送和接收数据之前无需建立远程主机连接。利用Rdm,您可以与多个对方主机进行通信
Seqpacket5在网络上提供排序字节流的面向连接且可靠的双向传输。Seqpacket不重复数据,它在数据流中保留边界。Seqpacket类型的Socket与单个对方主机通信,并且在通信开始之前需要建立远程主机连接

ProtocolType 枚举


枚举名含义
Unknown-1未知协议
Icmp-1网际消息控制协议
Unspecified0未指定的协议
IP0网际协议
IPv6HopByHopOptions0IPv6 逐跳选项头
Igmp2网际组管理协议
Ggp3网关到网关协议
IPv44Internet协议版本4
Tcp6传输控制协议
Pup12PARC通用数据包协议
Udp17用户数据报协议
Idp22Internet数据报协议
IPv641Internet协议版本6(IPv6)
IPv6RoutingHeader43IPv6路由头
IPv6FragmentHeader44IPv6片段头
IPSecEncapsulatingSecurityPayload50IPv6封装式安全措施负载头
IPSecAuthenticationHeader51IPv6 身份验证头。有关详细信息,请参阅https://www.ietf.org上的 RFC 2292,第 2.2.1 节
IcmpV658用于IPv6的Internet 控制消息协议
IPv6NoNextHeader59IPv6 No Next头
IPv6DestinationOptions60IPv6目标选项头
ND77网络磁盘协议(非正式)
Raw255原始IP数据包协议
Ipx1000Internet数据包交换协议
Spx1256顺序包交换协议
SpxII1257顺序包交换协议第 2 版

上面分别列举了3个枚举所有的值及对应的含义,实际上

1
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

这行代码的意思就是使用IPV4地址,全双工安全可靠通讯,TCP协议来创建一个Socket对象。


建立连接


建立连接的整个过程是:

  • 调用Connect方法与服务端建立连接

  • 成功后调用BeginReceive方法开始等待数据传输

  • 数据传输完毕调用EndReceive方法结束本次传输并开始等待下一次数据传输


Connect


调用Connect方法连接服务器,连接失败则跳出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}
}

BeginReceive


使用BeginReceive方法,使当前进入阻塞状态,等待接收服务端发送的消息,成功接收到消息后对应的数据会写入到一个字节流中等待处理

1
2
3
4
5
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer,0,m_ReceiveBuffer.Length,SocketFlags.None,OnReceive, m_Socket);
}

EndReceive


当接收到消息时,调用EndReceive方法结束本次数据接收,然后开始解包,解包成功再次调用BeginReceive方法开始新一轮数据接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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,下一次接收的包会和这个剩余包重新组成一个完整包。

得到真实的数据后,把真实数据入队,并在Unity主线程的update中去处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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主线程中去处理。


发送数据


上面提到过为了解决粘包,需要把消息包体进行定长,所以发包第一步就是先把包体长度写入数据流,然后把消息编码写入数据流,最后才写入真实的要发送的数据内容,调用BeginSend进行异步发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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();
}

这里为了保证线程安全仍然需要把数据入队,在确认到消息成功发送后才进行下一次数据的发送。




下面贴上整个TCP通信框架的代码,直接调用Connect方法进行连接,连接成功后调用Send方法进行发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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;
}

结语


以上就是在Unity中实现TCP通信的全部内容,下一篇就去实现服务端的TCP,把这篇内容真正的跑起来。