Skip to content

Commit 886b705

Browse files
committed
feat(network): 添加 KCP 网络模块
- 新增 GameFrameX.NetWork.Kcp 项目 - 实现 KcpServer、KcpSession、KcpSessionManager 等核心组件 - 添加 KCP 单元测试和集成测试 - 更新解决方案和项目引用
1 parent 8df001f commit 886b705

16 files changed

Lines changed: 2577 additions & 3 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="../Version.props" Label="版本号定义"/>
3+
4+
<PropertyGroup>
5+
<Description>GameFrameX.NetWork.Kcp,GameFrameX 框架的 KCP 网络模块.框架文档主页: https://gameframex.doc.alianblank.com</Description>
6+
<PackageTags>GameFrameX,Lib,NetWork,Kcp,Server,GameServer</PackageTags>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<!-- 框架依赖 -->
12+
<ProjectReference Include="..\GameFrameX.NetWork.Abstractions\GameFrameX.NetWork.Abstractions.csproj"/>
13+
<ProjectReference Include="..\GameFrameX.NetWork.Message\GameFrameX.NetWork.Message.csproj"/>
14+
<ProjectReference Include="..\GameFrameX.NetWork\GameFrameX.NetWork.csproj"/>
15+
<ProjectReference Include="..\GameFrameX.Utility\GameFrameX.Utility.csproj"/>
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<!-- SuperSocket UDP 扩展 -->
20+
<PackageReference Include="GameFrameX.SuperSocket.Udp" Version="1.2.0"/>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<!-- KCP 库 (KumoKyaku/KCP) -->
25+
<PackageReference Include="Kcp" Version="2.7.0"/>
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Net;
2+
3+
namespace GameFrameX.NetWork.Kcp;
4+
5+
/// <summary>
6+
/// KCP session interface / KCP 会话接口
7+
/// </summary>
8+
public interface IKcpSession
9+
{
10+
/// <summary>
11+
/// Conversation ID / 会话 ID
12+
/// </summary>
13+
uint ConversationId { get; }
14+
15+
/// <summary>
16+
/// Remote endpoint / 远程端点
17+
/// </summary>
18+
EndPoint RemoteEndPoint { get; }
19+
20+
/// <summary>
21+
/// Is connection active / 连接是否活跃
22+
/// </summary>
23+
bool IsConnected { get; }
24+
25+
/// <summary>
26+
/// Last active time / 最后活跃时间
27+
/// </summary>
28+
DateTime LastActiveTime { get; }
29+
30+
/// <summary>
31+
/// Input data received from UDP / 输入从 UDP 接收的数据
32+
/// </summary>
33+
void Input(ReadOnlySpan<byte> data);
34+
35+
/// <summary>
36+
/// Send data through KCP / 通过 KCP 发送数据
37+
/// </summary>
38+
int Send(ReadOnlySpan<byte> data);
39+
40+
/// <summary>
41+
/// Receive data from KCP / 从 KCP 接收数据
42+
/// </summary>
43+
int Recv(Span<byte> buffer);
44+
45+
/// <summary>
46+
/// Try peek receive size / 尝试获取接收大小
47+
/// </summary>
48+
int PeekSize();
49+
50+
/// <summary>
51+
/// Close connection / 关闭连接
52+
/// </summary>
53+
void Close();
54+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Net;
2+
using GameFrameX.SuperSocket.Server.Abstractions.Session;
3+
4+
namespace GameFrameX.NetWork.Kcp;
5+
6+
/// <summary>
7+
/// KCP game app session wrapper / KCP 游戏应用会话包装器
8+
/// Implements IGameAppSession interface for compatibility with the existing framework
9+
/// </summary>
10+
public sealed class KcpGameAppSession : IGameAppSession
11+
{
12+
private readonly IKcpSession _kcpSession;
13+
private bool _disposed;
14+
15+
/// <summary>
16+
/// Session unique ID / 会话唯一 ID
17+
/// </summary>
18+
public string SessionID
19+
{
20+
get { return _kcpSession.ConversationId.ToString(); }
21+
}
22+
23+
/// <summary>
24+
/// Session is connected / 会话是否已连接
25+
/// </summary>
26+
public bool IsConnected
27+
{
28+
get { return _kcpSession.IsConnected && !_disposed; }
29+
}
30+
31+
/// <summary>
32+
/// Remote endpoint / 远程端点
33+
/// </summary>
34+
public EndPoint RemoteEndPoint
35+
{
36+
get { return _kcpSession.RemoteEndPoint; }
37+
}
38+
39+
/// <summary>
40+
/// KCP session / KCP 会话
41+
/// </summary>
42+
public IKcpSession KcpSession
43+
{
44+
get { return _kcpSession; }
45+
}
46+
47+
/// <summary>
48+
/// Creates a new KCP game app session / 创建新的 KCP 游戏应用会话
49+
/// </summary>
50+
/// <param name="kcpSession">KCP session / KCP 会话</param>
51+
public KcpGameAppSession(IKcpSession kcpSession)
52+
{
53+
_kcpSession = kcpSession ?? throw new ArgumentNullException(nameof(kcpSession));
54+
}
55+
56+
/// <summary>
57+
/// Send data to client / 发送数据到客户端
58+
/// </summary>
59+
/// <param name="data">Data to send / 要发送的数据</param>
60+
/// <param name="cancellationToken">Cancellation token / 取消令牌</param>
61+
public async ValueTask SendAsync(byte[] data, CancellationToken cancellationToken = default)
62+
{
63+
if (_disposed || !_kcpSession.IsConnected)
64+
{
65+
return;
66+
}
67+
68+
_kcpSession.Send(data);
69+
await ValueTask.CompletedTask;
70+
}
71+
72+
/// <summary>
73+
/// Send data to client / 发送数据到客户端
74+
/// </summary>
75+
/// <param name="data">Data to send / 要发送的数据</param>
76+
/// <param name="cancellationToken">Cancellation token / 取消令牌</param>
77+
public async ValueTask SendAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
78+
{
79+
if (_disposed || !_kcpSession.IsConnected)
80+
{
81+
return;
82+
}
83+
84+
_kcpSession.Send(data.Span);
85+
await ValueTask.CompletedTask;
86+
}
87+
88+
/// <summary>
89+
/// Close the session / 关闭会话
90+
/// </summary>
91+
public void Close()
92+
{
93+
if (_disposed)
94+
{
95+
return;
96+
}
97+
98+
_disposed = true;
99+
_kcpSession.Close();
100+
}
101+
102+
/// <summary>
103+
/// Dispose resources / 释放资源
104+
/// </summary>
105+
public void Dispose()
106+
{
107+
Close();
108+
}
109+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System.Buffers;
2+
using GameFrameX.NetWork.Abstractions;
3+
4+
namespace GameFrameX.NetWork.Kcp;
5+
6+
/// <summary>
7+
/// KCP message pipeline filter / KCP 消息管道过滤器
8+
/// </summary>
9+
public sealed class KcpMessagePipelineFilter
10+
{
11+
/// <summary>
12+
/// Parse messages from KCP session / 从 KCP 会话解析消息
13+
/// </summary>
14+
/// <param name="session">KCP session / KCP 会话</param>
15+
/// <returns>List of parsed messages / 解析的消息列表</returns>
16+
public List<IMessage> Filter(IKcpSession session)
17+
{
18+
var messages = new List<IMessage>();
19+
20+
while (session.IsConnected)
21+
{
22+
var peekSize = session.PeekSize();
23+
if (peekSize <= 0)
24+
{
25+
break;
26+
}
27+
28+
var buffer = ArrayPool<byte>.Shared.Rent(peekSize);
29+
try
30+
{
31+
var bytesRead = session.Recv(buffer.AsSpan(0, peekSize));
32+
if (bytesRead <= 0)
33+
{
34+
break;
35+
}
36+
37+
var message = ParseMessage(buffer.AsSpan(0, bytesRead));
38+
if (message != null)
39+
{
40+
messages.Add(message);
41+
}
42+
}
43+
finally
44+
{
45+
ArrayPool<byte>.Shared.Return(buffer);
46+
}
47+
}
48+
49+
return messages;
50+
}
51+
52+
/// <summary>
53+
/// Parse a single message from buffer / 从缓冲区解析单条消息
54+
/// </summary>
55+
/// <param name="buffer">Message buffer / 消息缓冲区</param>
56+
/// <returns>Parsed message or null / 解析的消息或 null</returns>
57+
public IMessage ParseMessage(ReadOnlySpan<byte> buffer)
58+
{
59+
if (buffer.Length < sizeof(int))
60+
{
61+
return null;
62+
}
63+
64+
// Read total length (big-endian)
65+
var totalLength = ReadInt32BigEndian(buffer);
66+
if (buffer.Length < totalLength)
67+
{
68+
return null;
69+
}
70+
71+
// Convert to Sequence for decoding
72+
var messageData = buffer.Slice(0, totalLength).ToArray();
73+
var sequence = new ReadOnlySequence<byte>(messageData);
74+
75+
return MessageHelper.DecoderHandler.Handler(ref sequence);
76+
}
77+
78+
/// <summary>
79+
/// Try parse message header / 尝试解析消息头
80+
/// </summary>
81+
/// <param name="buffer">Message buffer / 消息缓冲区</param>
82+
/// <param name="totalLength">Total message length / 消息总长度</param>
83+
/// <returns>True if header was parsed / 如果头部解析成功则返回 true</returns>
84+
public bool TryParseHeader(ReadOnlySpan<byte> buffer, out int totalLength)
85+
{
86+
totalLength = 0;
87+
88+
if (buffer.Length < sizeof(int))
89+
{
90+
return false;
91+
}
92+
93+
totalLength = ReadInt32BigEndian(buffer);
94+
return totalLength > 0;
95+
}
96+
97+
/// <summary>
98+
/// Read int32 in big-endian format / 以大端序读取 int32
99+
/// </summary>
100+
private static int ReadInt32BigEndian(ReadOnlySpan<byte> buffer)
101+
{
102+
var value = BitConverter.ToInt32(buffer.Slice(0, 4));
103+
if (BitConverter.IsLittleEndian)
104+
{
105+
// Convert from little-endian to big-endian
106+
value = (int)(((uint)value >> 24) | (((uint)value >> 8) & 0xFF00) | (((uint)value << 8) & 0xFF0000) | ((uint)value << 24));
107+
}
108+
109+
return value;
110+
}
111+
}

0 commit comments

Comments
 (0)