Skip to content

Commit 0d68cd7

Browse files
committed
refactor(client): 重构 Bot 客户端支持参数化配置和断线重连
- 新增 BotRunOptions 支持命令行参数解析(机器人数量、TCP 地址、 断线间隔、运行时长等) - BotClient 从 HTTP 登录流程改为纯 TCP RPC 登录流程 - BotTcpClient 支持外部配置 Host/Port,新增 Disconnect 方法和 CancellationToken 取消支持 - Program 使用 BotRunOptions 参数化启动,支持预热序列化和 优雅停机
1 parent d204307 commit 0d68cd7

4 files changed

Lines changed: 286 additions & 103 deletions

File tree

GameFrameX.Client/Bot/BotClient.cs

Lines changed: 98 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
using GameFrameX.NetWork.Messages;
3131
using GameFrameX.Proto.Proto;
3232
using GameFrameX.Foundation.Logger;
33-
using GameFrameX.Foundation.Extensions;
3433
using ErrorEventArgs = GameFrameX.SuperSocket.ClientEngine.ErrorEventArgs;
3534

3635
namespace GameFrameX.Client.Bot;
@@ -41,35 +40,36 @@ namespace GameFrameX.Client.Bot;
4140
public sealed class BotClient
4241
{
4342
private readonly BotTcpClient m_TcpClient;
44-
private readonly BotHttpClient m_HttpClient;
4543
private readonly string m_BotName;
4644
private readonly BotTcpClientEvent m_BotTcpClientEvent;
47-
private const string m_LoginUrl = "http://127.0.0.1:28080/game/api/";
45+
private readonly BotRunOptions _options;
46+
private int _disconnectScheduled;
47+
private long _accountId;
4848

4949
/// <summary>
5050
/// 初始化机器人客户端
5151
/// </summary>
5252
/// <param name="botName">机器人名称</param>
53-
public BotClient(string botName)
53+
public BotClient(string botName, BotRunOptions options)
5454
{
5555
m_BotName = botName;
56+
_options = options;
5657
m_BotTcpClientEvent.OnConnectedCallback += ClientConnectedCallback;
5758
m_BotTcpClientEvent.OnClosedCallback += ClientClosedCallback;
5859
m_BotTcpClientEvent.OnErrorCallback += ClientErrorCallback;
5960
m_BotTcpClientEvent.OnReceiveMsgCallback += ClientReceiveCallback;
60-
m_TcpClient = new BotTcpClient(m_BotTcpClientEvent);
61-
m_HttpClient = new BotHttpClient();
61+
m_TcpClient = new BotTcpClient(m_BotTcpClientEvent, options.TcpHost, options.TcpPort);
6262
}
6363

6464
/// <summary>
6565
/// 启动机器人客户端
6666
/// </summary>
6767
/// <returns>异步任务</returns>
68-
public async Task EntryAsync()
68+
public async Task EntryAsync(CancellationToken cancellationToken = default)
6969
{
7070
try
7171
{
72-
await m_TcpClient.EntryAsync();
72+
await m_TcpClient.EntryAsync(cancellationToken);
7373
}
7474
catch (Exception e)
7575
{
@@ -85,6 +85,15 @@ private void OnReceiveMsg(MessageObject messageObject)
8585
{
8686
switch (messageObject)
8787
{
88+
case RespLogin msg:
89+
OnAccountLoginSuccess(msg);
90+
break;
91+
case RespPlayerList msg:
92+
OnPlayerListSuccess(msg);
93+
break;
94+
case RespPlayerCreate msg:
95+
OnPlayerCreateSuccess(msg);
96+
break;
8897
case RespPlayerLogin msg:
8998
OnPlayerLoginSuccess(msg);
9099
break;
@@ -98,7 +107,7 @@ private void OnReceiveMsg(MessageObject messageObject)
98107
/// </summary>
99108
private void ClientConnectedCallback()
100109
{
101-
SendLoginMessage();
110+
SendAccountLoginMessage();
102111
}
103112

104113
/// <summary>
@@ -132,82 +141,78 @@ private void ClientReceiveCallback(MessageObject outerMsg)
132141
/// <summary>
133142
/// 发送登录消息并处理登录流程
134143
/// </summary>
135-
private async void SendLoginMessage()
144+
private void SendAccountLoginMessage()
136145
{
137146
try
138147
{
139-
//请求登录验证
140148
var reqLogin = new ReqLogin
141149
{
142150
UserName = m_BotName,
143151
Password = "12312",
144152
Platform = "LoginPlatform.Custom",
145153
};
154+
m_TcpClient.SendToServer(reqLogin);
155+
}
156+
catch (Exception e)
157+
{
158+
LogHelper.Error($"SendAccountLoginMessage Error: {e.Message}| Thread ID:{Thread.CurrentThread.ManagedThreadId} ");
159+
}
160+
}
146161

147-
string respLoginUrl = $"{m_LoginUrl}{nameof(ReqLogin).ConvertToSnakeCase()}";
148-
var respLogin = await m_HttpClient.Post<RespLogin>(respLoginUrl, reqLogin);
149-
if (respLogin.ErrorCode != 0)
150-
{
151-
LogHelper.Error("请求登录验证,错误信息:" + respLogin.ErrorCode);
152-
return;
153-
}
162+
#endregion
154163

155-
LogHelper.Info($"机器人-{m_BotName}账号验证成功,id:{respLogin.Id}");
156164

157-
//请求角色列表
158-
var reqPlayerList = new ReqPlayerList();
159-
reqPlayerList.Id = respLogin.Id;
160-
string reqPlayerListUrl = $"{m_LoginUrl}{nameof(ReqPlayerList).ConvertToSnakeCase()}";
161-
var respPlayerList = await m_HttpClient.Post<RespPlayerList>(reqPlayerListUrl, reqPlayerList);
165+
#region 消息接收
162166

163-
if (respPlayerList.ErrorCode != 0)
164-
{
165-
LogHelper.Error("请求角色列表,错误信息:" + respPlayerList.ErrorCode);
166-
return;
167-
}
167+
private void OnAccountLoginSuccess(RespLogin msg)
168+
{
169+
if (msg.ErrorCode != 0)
170+
{
171+
LogHelper.Error($"机器人-{m_BotName}账号登录失败,错误码:{msg.ErrorCode}");
172+
return;
173+
}
168174

169-
PlayerInfo player;
170-
if (respPlayerList.PlayerList.Count == 0)
171-
{
172-
LogHelper.Info("角色列表为空");
173-
174-
//请求创建角色
175-
var reqCreatePlayer = new ReqPlayerCreate();
176-
reqCreatePlayer.Id = respLogin.Id;
177-
178-
string reqCreatePlayerUrl = $"{m_LoginUrl}{nameof(ReqPlayerCreate).ConvertToSnakeCase()}";
179-
var respPlayerCreator = await m_HttpClient.Post<RespPlayerCreate>(reqCreatePlayerUrl,
180-
reqCreatePlayer);
181-
if (respPlayerCreator.ErrorCode != 0)
182-
{
183-
LogHelper.Error("请求创建角色,错误信息:" + respPlayerCreator.ErrorCode);
184-
return;
185-
}
186-
187-
player = respPlayerCreator.PlayerInfo;
188-
LogHelper.Info($"创建角色 Id:{player.Id}-昵称:{player.Name}-等级:{player.Level}-角色状态:{player.State}");
189-
}
190-
else
191-
{
192-
player = respPlayerList.PlayerList[0];
193-
LogHelper.Info($"角色列表 Id:{player.Id}-昵称:{player.Name}-等级:{player.Level}-角色状态:{player.State}");
194-
}
175+
_accountId = msg.Id;
176+
LogHelper.Info($"机器人-{m_BotName}账号验证成功,id:{msg.Id}");
177+
m_TcpClient.SendToServer(new ReqPlayerList { Id = _accountId });
178+
}
195179

196-
var reqPlayerLogin = new ReqPlayerLogin();
197-
reqPlayerLogin.Id = player.Id;
198-
// reqPlayerLogin.Name = m_BotName;
199-
m_TcpClient.SendToServer(reqPlayerLogin);
180+
private void OnPlayerListSuccess(RespPlayerList msg)
181+
{
182+
if (msg.ErrorCode != 0)
183+
{
184+
LogHelper.Error($"机器人-{m_BotName}请求角色列表失败,错误码:{msg.ErrorCode}");
185+
return;
200186
}
201-
catch (Exception e)
187+
188+
if (msg.PlayerList.Count <= 0)
202189
{
203-
LogHelper.Error($"SendLoginMessage Error: {e.Message}| Thread ID:{Thread.CurrentThread.ManagedThreadId} ");
190+
LogHelper.Info($"机器人-{m_BotName}角色列表为空,开始创建角色。");
191+
m_TcpClient.SendToServer(new ReqPlayerCreate
192+
{
193+
Id = _accountId,
194+
Name = m_BotName,
195+
});
196+
return;
204197
}
205-
}
206198

207-
#endregion
199+
var player = msg.PlayerList[0];
200+
LogHelper.Info($"角色列表 Id:{player.Id}-昵称:{player.Name}-等级:{player.Level}-角色状态:{player.State}");
201+
m_TcpClient.SendToServer(new ReqPlayerLogin { Id = player.Id });
202+
}
208203

204+
private void OnPlayerCreateSuccess(RespPlayerCreate msg)
205+
{
206+
if (msg.ErrorCode != 0)
207+
{
208+
LogHelper.Error($"机器人-{m_BotName}创建角色失败,错误码:{msg.ErrorCode}");
209+
return;
210+
}
209211

210-
#region 消息接收
212+
var player = msg.PlayerInfo;
213+
LogHelper.Info($"创建角色 Id:{player.Id}-昵称:{player.Name}-等级:{player.Level}-角色状态:{player.State}");
214+
m_TcpClient.SendToServer(new ReqPlayerLogin { Id = player.Id });
215+
}
211216

212217
/// <summary>
213218
/// 处理玩家登录成功的响应
@@ -216,7 +221,35 @@ private async void SendLoginMessage()
216221
private void OnPlayerLoginSuccess(RespPlayerLogin msg)
217222
{
218223
LogHelper.Info($"机器人-{m_BotName}登录成功,id:{msg.PlayerInfo.Id}");
224+
ScheduleDisconnectIfNeeded();
225+
}
226+
227+
private void ScheduleDisconnectIfNeeded()
228+
{
229+
if (!_options.EnableDisconnectLoop || _options.DisconnectAfterLoginSeconds <= 0)
230+
{
231+
return;
232+
}
233+
234+
if (Interlocked.CompareExchange(ref _disconnectScheduled, 1, 0) != 0)
235+
{
236+
return;
237+
}
238+
239+
_ = Task.Run(async () =>
240+
{
241+
try
242+
{
243+
await Task.Delay(TimeSpan.FromSeconds(_options.DisconnectAfterLoginSeconds));
244+
LogHelper.Info($"机器人-{m_BotName}主动断开连接,模拟离线。");
245+
m_TcpClient.Disconnect();
246+
}
247+
finally
248+
{
249+
Interlocked.Exchange(ref _disconnectScheduled, 0);
250+
}
251+
});
219252
}
220253

221254
#endregion
222-
}
255+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
namespace GameFrameX.Client.Bot;
2+
3+
/// <summary>
4+
/// 机器人运行参数。
5+
/// </summary>
6+
public sealed class BotRunOptions
7+
{
8+
public int BotCount { get; init; } = 50;
9+
public string BotNamePrefix { get; init; } = "BotClient";
10+
public string TcpHost { get; init; } = "127.0.0.1";
11+
public int TcpPort { get; init; } = 49100;
12+
public string LoginUrl { get; init; } = "http://127.0.0.1:48080/game/api/";
13+
public int ConnectStaggerMilliseconds { get; init; } = 20;
14+
public bool EnableDisconnectLoop { get; init; } = true;
15+
public int DisconnectAfterLoginSeconds { get; init; } = 15;
16+
public int RunSeconds { get; init; } = 0;
17+
18+
public static BotRunOptions Parse(string[] args)
19+
{
20+
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
21+
foreach (var arg in args)
22+
{
23+
if (!arg.StartsWith("--", StringComparison.Ordinal))
24+
{
25+
continue;
26+
}
27+
28+
var segment = arg.Substring(2);
29+
var splitIndex = segment.IndexOf('=');
30+
if (splitIndex <= 0 || splitIndex == segment.Length - 1)
31+
{
32+
continue;
33+
}
34+
35+
var key = segment.Substring(0, splitIndex).Trim();
36+
var value = segment.Substring(splitIndex + 1).Trim();
37+
if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value))
38+
{
39+
values[key] = value;
40+
}
41+
}
42+
43+
return new BotRunOptions
44+
{
45+
BotCount = ReadInt(values, "bot-count", 50),
46+
BotNamePrefix = ReadString(values, "bot-prefix", "BotClient"),
47+
TcpHost = ReadString(values, "tcp-host", "127.0.0.1"),
48+
TcpPort = ReadInt(values, "tcp-port", 49100),
49+
LoginUrl = EnsureEndWithSlash(ReadString(values, "login-url", "http://127.0.0.1:48080/game/api/")),
50+
ConnectStaggerMilliseconds = ReadInt(values, "connect-stagger-ms", 20),
51+
EnableDisconnectLoop = ReadBool(values, "disconnect-loop", true),
52+
DisconnectAfterLoginSeconds = ReadInt(values, "disconnect-after-login-seconds", 15),
53+
RunSeconds = ReadInt(values, "run-seconds", 0),
54+
};
55+
}
56+
57+
private static string ReadString(IDictionary<string, string> values, string key, string defaultValue)
58+
{
59+
return values.TryGetValue(key, out var value) ? value : defaultValue;
60+
}
61+
62+
private static int ReadInt(IDictionary<string, string> values, string key, int defaultValue)
63+
{
64+
if (!values.TryGetValue(key, out var value))
65+
{
66+
return defaultValue;
67+
}
68+
69+
return int.TryParse(value, out var parsed) ? parsed : defaultValue;
70+
}
71+
72+
private static bool ReadBool(IDictionary<string, string> values, string key, bool defaultValue)
73+
{
74+
if (!values.TryGetValue(key, out var value))
75+
{
76+
return defaultValue;
77+
}
78+
79+
return bool.TryParse(value, out var parsed) ? parsed : defaultValue;
80+
}
81+
82+
private static string EnsureEndWithSlash(string value)
83+
{
84+
if (string.IsNullOrWhiteSpace(value))
85+
{
86+
return "http://127.0.0.1:48080/game/api/";
87+
}
88+
89+
return value.EndsWith("/", StringComparison.Ordinal) ? value : $"{value}/";
90+
}
91+
}

0 commit comments

Comments
 (0)