前言


上一篇实现了Unity客户端的TCP通信,这篇把服务端的TCP也实现一下,并使客户端和服务端进行联调。

对于客户端来说,一个应用(一个设备)对应一个Socket。

但服务端不同,一个服务端需要处理许多个客户端的请求,每有一个客户端和服务端成功建立连接都需要创建一个新的socket的对象,这也体现了Tcp协议中一对一通讯的这一特点。

服务端Socket的创建流程和客户端类似,依然需要三个参数

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

不明白每个参数代表什么意思可以看我的上一篇客户端的部分,里面有详细的介绍。

下面开始服务端Socket的工作流程,这里服务端使用.NetFramework创建控制台应用来进行开发。


建立连接


服务的连接分为以下几个步骤:

  • 调用Bind方法绑定一个IP和端口,成功之后调用Listen方法设置最大监听数,当连接的客户端超过这个数值就不再建立新的连接

  • 另起一个线程并进入阻塞状态,调用Accept方法等待客户端连接

  • 有客户端成功连接,创建对应客户端的消息处理实例,并把该客户端加入一个表中以备广播时使用

  • 服务端程序关闭同时关闭所有客户端的连接


Bind


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
static void Main(string[] args)
{
m_Socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
m_AcceptThread = new Thread(OnAccept);//另起一个线程进入阻塞状态等待客户端连接
m_Clients = new List<RoleClient>();//所有连接的客户端
m_Socket.Bind(new IPEndPoint(IPAddress.Parse(m_IP), m_Port));//绑定ip端口
m_Socket.Listen(50);//最大监听数50
m_AcceptThread.Start();//进入等待连接状态
AppDomain.CurrentDomain.ProcessExit += OnApplicatonQuit;
Console.WriteLine("服务器启动成功!");
Console.WriteLine("监听IP:" + m_IP + ",端口:" + m_Port);
//广播代码
while(true)
{
string str = Console.ReadLine();


if (string.IsNullOrEmpty(str)) continue;

if (str.Equals("close all"))
{
for (int i = m_Clients.Count - 1; i >= 0 ; i--)
{
m_Clients[i].Close(true);
}
}
else
{
for (int i = 0; i < m_Clients.Count; i++)
{
m_Clients[i].Send(1, Encoding.UTF8.GetBytes(str));
}

}
}
}

Accept


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void OnAccept()
{
while(true)
{
try
{
Socket client = m_Socket.Accept();//尝试接收一个客户端的连接
IPEndPoint clientPoint = client.RemoteEndPoint as IPEndPoint;
m_Clients.Add(new RoleClient(client, m_Clients));//为客户端建立一个请求处理实例,并加入到表中
Console.WriteLine("客户端:"+ clientPoint.Address.ToString() +"已经连接!");
}
catch
{
continue;
}
}
}

Abort


1
2
3
4
5
6
7
8
9
10
private static void OnApplicatonQuit(object sender, EventArgs e)
{
for (int i = m_Clients.Count - 1; i > -1 ; i--)
{
m_Clients[i].Close();
}

m_AcceptThread.Abort();
m_Clients.Clear();
}

到这里客户端请求连接,服务端接受连接请求并创建对应的请求实例的功能已经实现完了,然后简单测试一下。

先启动服务器,如图,成功监听了本地127.0.0.1的ip和8888端口测试
客户端制作一个测试的界面,把SocketMgr挂到一个空物体上,并编写对应的测试代码

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
public Button button;
public InputField inputField;

void Start ()
{
button.onClick.AddListener(onClick);
inputField.gameObject.SetActive(false);

SocketMgr.Instance.OnConnectSuccess = delegate ()
{
inputField.gameObject.SetActive(true);
button.transform.Find("Text").GetComponent<Text>().text = "发送";
};

SocketMgr.Instance.OnDisConnect = delegate ()
{
inputField.gameObject.SetActive(false);
button.transform.Find("Text").GetComponent<Text>().text = "连接";
};

SocketMgr.Instance.onReceive = OnReceive;
}

private void OnReceive(ushort arg1, byte[] arg2)
{
inputField.text = arg1 + "," + Encoding.UTF8.GetString(arg2);
}

private void onClick()
{
if(!SocketMgr.Instance.IsConnected)
{
SocketMgr.Instance.Connect("127.0.0.1", 8888);
return;
}

SocketMgr.Instance.Send(1, Encoding.UTF8.GetBytes(inputField.text));
}

SocketMgr就是我上一篇客户端部分封装的Socket通信框架,具体的代码已经全部都贴到到了博客中。

接下来点击运行,见证奇迹的时刻就要到了测试
点击连接按钮就会通过Connect方法向服务端发送建立连接的请求,测试
如图,这里成功的建立的了连接,此处应有掌声雷动。
测试


数据通信


服务端数据通信部分与客户端基本类似,不同的地方就在于服务端不存在主线程和非主线程的区分,拆包后直接在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
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
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
public class RoleClient
{
Timer timer;
public RoleClient(Socket socket, List<RoleClient> otherClients)
{
m_OtherClients = otherClients;
m_ReceiveBuffer = new byte[1024 * 512];
m_ReceiveStream = new MemoryStream();
m_ReceiveQueue = new Queue<byte[]>();
m_SendQueue = new Queue<byte[]>();
m_Socket = socket;
m_IsConnected = true;
//m_ReceiveThread = new Thread(CheckReceive);
timer = new Timer(CheckReceiveBuffer, 0, 0,200);
//m_ReceiveThread.Start();
StartReceive();
}

public void Send(ushort msgCode, byte[] buffer)
{
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();
}
}

public void Close(bool isForce = false)
{
try { m_Socket.Shutdown(SocketShutdown.Both); }
catch { }

if (isForce)
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("强制关闭与客户端:" + endPoint.Address.ToString() + "的连接");
}

m_IsConnected = false;
m_Socket.Close();
m_ReceiveStream.SetLength(0);
m_ReceiveQueue.Clear();
m_SendQueue.Clear();
timer.Dispose();

if (m_OtherClients != null)
{
m_OtherClients.Remove(this);
}

timer = null;
m_SendQueue = null;
m_ReceiveQueue = null;
m_ReceiveStream = null;
m_ReceiveBuffer = null;
m_OtherClients = null;
}

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)
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("客户端:" + endPoint.Address.ToString() + "已断开连接");
Close();
return;
}

m_ReceiveStream.Position = m_ReceiveStream.Length;
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);

if (m_ReceiveStream.Length < 3)
{
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
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("客户端:" + endPoint.Address.ToString() + "已断开连接");
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(object state)
{
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1) return;
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);
}

Console.WriteLine("消息编号:" + msgCode + ",内容:" + Encoding.UTF8.GetString(msgContent));
}
}

private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}

private bool m_IsConnected = false;
private Queue<byte[]> m_ReceiveQueue = null;
private Queue<byte[]> m_SendQueue = null;
private MemoryStream m_ReceiveStream = null;
private byte[] m_ReceiveBuffer = null;
private Socket m_Socket = null;
private List<RoleClient> m_OtherClients = null;
}
}

接下来进行最后的测试,使客户端和服务端相互进行实际的数据通信。


测试


客户端输入任意字符串,然后点击发送,这里消息编码写死为1,但实际开发中每一个消息都有自己的编号要根据实际情况来决定要发送哪条消息。

测试

服务端成功的接收到数据并解析出消息编码和具体内容

测试

服务端输入任意字符串,看看客户端能否接到消息

测试

客户端也成功的解析出了内容

测试

服务端输入close all看能否跟客户端断开连接

测试

客户端打印日志,断开连接

测试

强制关闭客户端,服务端也能检测到客户端的断开

测试


结语


到这里整个TCP通信的客户端和服务端已经基本实现完了。

但是实际开发中,通信内容可不仅仅是字符串,而是十分复杂的一些数据结构。检测客户端断开也不能单单凭借endreceive的长度为0就确定客户端断开,各个模块间的数据要分别派发不能造成耦合,那这些是怎么实现的呢?

下面几篇我就依次来写写序列化工具Protobuf、观察者消息派发、以及心跳机制,详细阐述这些功能是如何实现的。