Skip to content

Commit abebeb0

Browse files
committed
feat(ServiceClient): 添加游戏应用TCP服务客户端实现
实现游戏应用TCP服务客户端,包含连接管理、消息收发、RPC调用等功能 支持自动重连机制和错误处理,提供消息接收回调接口
1 parent 0c48b8a commit abebeb0

File tree

1 file changed

+312
-0
lines changed

1 file changed

+312
-0
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
// ==========================================================================================
2+
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
3+
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
4+
// 均受中华人民共和国及相关国际法律法规保护。
5+
// are protected by the laws of the People's Republic of China and relevant international regulations.
6+
//
7+
// 使用本项目须严格遵守相应法律法规及开源许可证之规定。
8+
// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
9+
//
10+
// 本项目采用 MIT 许可证与 Apache License 2.0 双许可证分发,
11+
// This project is dual-licensed under the MIT License and Apache License 2.0,
12+
// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
13+
// please refer to the LICENSE file in the root directory of the source code for the full license text.
14+
//
15+
// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
16+
// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
17+
// 侵犯他人合法权益等法律法规所禁止的行为!
18+
// or infringe upon the legitimate rights and interests of others, as prohibited by laws and regulations!
19+
// 因基于本项目二次开发所产生的一切法律纠纷与责任,
20+
// Any legal disputes and liabilities arising from secondary development based on this project
21+
// 本项目组织与贡献者概不承担。
22+
// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
23+
//
24+
// GitHub 仓库:https://github.com/GameFrameX
25+
// GitHub Repository: https://github.com/GameFrameX
26+
// Gitee 仓库:https://gitee.com/GameFrameX
27+
// Gitee Repository: https://gitee.com/GameFrameX
28+
// 官方文档:https://gameframex.doc.alianblank.com/
29+
// Official Documentation: https://gameframex.doc.alianblank.com/
30+
// ==========================================================================================
31+
32+
using System.Net;
33+
using GameFrameX.Foundation.Extensions;
34+
using GameFrameX.Foundation.Logger;
35+
using GameFrameX.NetWork;
36+
using GameFrameX.NetWork.Abstractions;
37+
using GameFrameX.NetWork.Messages;
38+
using GameFrameX.SuperSocket.ClientEngine;
39+
40+
namespace GameFrameX.StartUp.ServiceClient;
41+
42+
/// <summary>
43+
/// 游戏程序TCP客户端类,用于处理与服务器的TCP连接和消息收发
44+
/// </summary>
45+
internal sealed class GameAppServiceClient : IDisposable
46+
{
47+
/// <summary>
48+
/// 内部TCP会话实例,负责底层网络通信
49+
/// </summary>
50+
private readonly AsyncTcpSession _mTcpClient;
51+
52+
/// <summary>
53+
/// 当前重连次数计数器
54+
/// </summary>
55+
public int RetryCount { get; private set; }
56+
57+
/// <summary>
58+
/// RPC会话实例,用于处理RPC请求和响应
59+
/// </summary>
60+
private readonly IRpcSession _rpcSession;
61+
62+
/// <summary>
63+
/// 服务器终结点(IP与端口)
64+
/// </summary>
65+
private readonly EndPoint _serverHost;
66+
67+
/// <summary>
68+
/// 获取服务器终结点(IP与端口)
69+
/// </summary>
70+
public EndPoint ServerHost
71+
{
72+
get { return _serverHost; }
73+
}
74+
75+
/// <summary>
76+
/// 消息接收回调,当服务器发送消息时触发,
77+
/// 回调参数为服务器发送的消息对象
78+
/// 只有RPC没有正确处理时才会触发
79+
/// </summary>
80+
private readonly Action<MessageObject> _onMessageReceived;
81+
82+
/// <summary>
83+
/// 每次重连之间的延迟时间,单位毫秒
84+
/// </summary>
85+
private readonly int _retryDelay;
86+
87+
/// <summary>
88+
/// 最大重连次数,-1表示无限重试
89+
/// </summary>
90+
public int MaxRetryCount { get; }
91+
92+
/// <summary>
93+
/// 标记当前实例是否已被释放,防止重复释放或空操作
94+
/// </summary>
95+
private bool _isDisposed;
96+
97+
/// <summary>
98+
/// 初始化游戏服务TCP客户端
99+
/// </summary>
100+
/// <param name="endPoint">服务器端点信息(IP和端口)</param>
101+
/// <param name="onMessageReceived">消息接收回调,当服务器发送消息时触发</param>
102+
/// <param name="maxRetryCount">最大重连次数,-1表示无限重试</param>
103+
/// <param name="retryDelay">每次重连之间的延迟时间,单位毫秒</param>
104+
public GameAppServiceClient(EndPoint endPoint, Action<MessageObject> onMessageReceived, int maxRetryCount = -1, int retryDelay = 1000)
105+
{
106+
ArgumentNullException.ThrowIfNull(endPoint, nameof(endPoint));
107+
_serverHost = endPoint;
108+
_onMessageReceived = onMessageReceived;
109+
_rpcSession = new RpcSession();
110+
_retryDelay = retryDelay;
111+
MaxRetryCount = maxRetryCount;
112+
_mTcpClient = new AsyncTcpSession();
113+
_mTcpClient.Connected += OnClientOnConnected;
114+
_mTcpClient.Closed += OnClientOnClosed;
115+
_mTcpClient.DataReceived += OnClientOnDataReceived;
116+
_mTcpClient.Error += OnClientOnError;
117+
Task.Run(Handler);
118+
Task.Run(StartAsync);
119+
}
120+
121+
private async Task Handler()
122+
{
123+
DateTime lastTickTime = DateTime.UtcNow;
124+
while (_isDisposed == false)
125+
{
126+
await Task.Delay(1);
127+
var deltaTime = DateTime.UtcNow - lastTickTime;
128+
_rpcSession.Tick((int)deltaTime.TotalMilliseconds);
129+
130+
while (true)
131+
{
132+
var sessionData = _rpcSession.Handler();
133+
134+
if (sessionData != null)
135+
{
136+
MessageSendHandle(sessionData?.RequestMessage);
137+
}
138+
else
139+
{
140+
break;
141+
}
142+
}
143+
144+
lastTickTime = DateTime.UtcNow;
145+
}
146+
}
147+
148+
149+
/// <summary>
150+
/// 启动客户端并尝试连接服务器,处理消息编解码和压缩解压缩处理器的初始化。
151+
/// 内部采用无限循环,在连接成功前持续重试,并在连接成功后周期性发送心跳。
152+
/// </summary>
153+
/// <returns>表示异步操作的任务。</returns>
154+
private async Task StartAsync()
155+
{
156+
// 主循环:负责连接、重连
157+
while (true)
158+
{
159+
if (_isDisposed)
160+
{
161+
break;
162+
}
163+
164+
// 如果未连接且未处于连接中,则尝试连接
165+
if (!_mTcpClient.IsConnected && !_mTcpClient.IsInConnecting)
166+
{
167+
LogHelper.Debug($"try to connect to the target server...{_serverHost}");
168+
_mTcpClient.Connect(_serverHost);
169+
// 若连接成功或正在连接,则跳过本次循环
170+
if (_mTcpClient.IsConnected || _mTcpClient.IsInConnecting)
171+
{
172+
continue;
173+
}
174+
175+
// 未达到最大重连次数(或无限重试)则进行重连
176+
if (RetryCount < MaxRetryCount || MaxRetryCount < 0)
177+
{
178+
LogHelper.Info($"Not connecting to the target server, attempts to reconnect (number of attempts: {RetryCount + 1}/{(MaxRetryCount < 0 ? "∞" : MaxRetryCount.ToString())})...");
179+
_mTcpClient.Connect(_serverHost);
180+
RetryCount++;
181+
await Task.Delay(_retryDelay);
182+
}
183+
else
184+
{
185+
LogHelper.Info($"Reconnect attempts have reached the upper limit ({MaxRetryCount}), and no more attempts will be made.");
186+
break;
187+
}
188+
}
189+
else
190+
{
191+
// 连接成功,重置重连计数
192+
RetryCount = 0;
193+
}
194+
}
195+
}
196+
197+
/// <summary>
198+
/// 发送消息到服务器
199+
/// 内部使用MessageHelper编码后通过TcpClient发送
200+
/// </summary>
201+
/// <param name="messageObject">要发送的消息对象</param>
202+
public void Send(MessageObject messageObject)
203+
{
204+
_rpcSession.Send(messageObject as IRequestMessage);
205+
}
206+
207+
/// <summary>
208+
/// 发送消息到服务器
209+
/// 内部使用MessageHelper编码后通过TcpClient发送
210+
/// </summary>
211+
/// <param name="messageObject">要发送的消息对象</param>
212+
private void MessageSendHandle(INetworkMessage messageObject)
213+
{
214+
var buffer = MessageHelper.EncoderHandler.Handler(messageObject);
215+
if (buffer != null)
216+
{
217+
_mTcpClient.Send(buffer);
218+
}
219+
}
220+
221+
/// <summary>
222+
/// 发送消息到服务器
223+
/// 内部使用MessageHelper编码后通过TcpClient发送
224+
/// </summary>
225+
/// <param name="messageObject">要发送的消息对象</param>
226+
/// <param name="timeOut">超时时间,单位毫秒</param>
227+
/// <typeparam name="T">响应消息类型,必须实现IResponseMessage接口</typeparam>
228+
/// <returns>表示异步操作的任务,任务结果为IRpcResult对象</returns>
229+
public Task<IRpcResult> Call<T>(MessageObject messageObject, int timeOut = 10000) where T : IResponseMessage, new()
230+
{
231+
var result = _rpcSession.Call<T>(messageObject as IRequestMessage, timeOut);
232+
return result;
233+
}
234+
235+
/// <summary>
236+
/// 处理客户端错误事件
237+
/// 将错误信息通过GameAppClientEvent回调给上层
238+
/// </summary>
239+
/// <param name="client">触发事件的TcpSession对象</param>
240+
/// <param name="e">包含异常信息的错误事件参数</param>
241+
private void OnClientOnError(object client, SuperSocket.ClientEngine.ErrorEventArgs e)
242+
{
243+
LogHelper.Error($"Client error occurred: {e.Exception.Message}");
244+
}
245+
246+
/// <summary>
247+
/// 处理客户端连接关闭事件
248+
/// 记录日志并通过GameAppClientEvent通知上层连接已断开
249+
/// </summary>
250+
/// <param name="client">触发事件的TcpSession对象</param>
251+
/// <param name="e">事件参数</param>
252+
private void OnClientOnClosed(object client, EventArgs e)
253+
{
254+
LogHelper.Info($"Client disconnected from the server: {_serverHost}");
255+
}
256+
257+
/// <summary>
258+
/// 处理客户端连接成功事件
259+
/// 记录日志并通过GameAppClientEvent通知上层连接已建立
260+
/// </summary>
261+
/// <param name="client">触发事件的TcpSession对象</param>
262+
/// <param name="e">事件参数</param>
263+
private void OnClientOnConnected(object client, EventArgs e)
264+
{
265+
LogHelper.Info($"Client successfully connected to the server: {_serverHost}");
266+
}
267+
268+
/// <summary>
269+
/// 处理接收到数据事件
270+
/// 将接收到的二进制数据解码为消息对象,若为内部网络消息则反序列化后通过回调通知上层
271+
/// </summary>
272+
/// <param name="client">触发事件的TcpSession对象</param>
273+
/// <param name="e">包含接收数据的数据事件参数</param>
274+
private void OnClientOnDataReceived(object client, DataEventArgs e)
275+
{
276+
var message = MessageHelper.DecoderHandler.Handler(e.Data.ReadBytesValue(e.Offset, e.Length));
277+
// 只处理内部消息
278+
if (message is NetworkMessagePackage innerNetworkMessage && innerNetworkMessage.Header.MessageId < 0)
279+
{
280+
var messageObject = (MessageObject)innerNetworkMessage.DeserializeMessageObject();
281+
var reply = _rpcSession.Reply(messageObject as IResponseMessage);
282+
if (!reply)
283+
{
284+
_onMessageReceived?.Invoke(messageObject);
285+
}
286+
}
287+
}
288+
289+
/// <summary>
290+
/// 停止客户端
291+
/// </summary>
292+
public void Dispose()
293+
{
294+
if (_isDisposed)
295+
{
296+
return;
297+
}
298+
299+
Stop();
300+
}
301+
302+
/// <summary>
303+
/// 停止客户端连接,关闭底层TCP会话
304+
/// </summary>
305+
public async void Stop()
306+
{
307+
_isDisposed = true;
308+
await Task.Delay(5);
309+
_mTcpClient.Close();
310+
_rpcSession.Stop();
311+
}
312+
}

0 commit comments

Comments
 (0)