mirror of
https://github.com/MeowCraftMC/CrystalConnector
synced 2025-05-22 02:43:00 +00:00
✨ TestClient, but wait, what are we actually need?
Signed-off-by: 秋雨落 <i@rain.cx>
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Nakov.IO.Cin" Version="2.0.2.1" />
|
||||||
|
<PackageReference Include="System.Formats.Cbor" Version="8.0.0-rc.1.23419.4" />
|
||||||
|
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using System.Formats.Cbor;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
var webSocket = new ClientWebSocket();
|
||||||
|
var cborWriter = new CborWriter();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Process();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Receive()
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
while (!result.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
var reader = new CborReader(new ReadOnlyMemory<byte>(buffer, 0, result.Count));
|
||||||
|
Console.WriteLine(reader.BytesRemaining);
|
||||||
|
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Process()
|
||||||
|
{
|
||||||
|
Console.Write("> ");
|
||||||
|
var operation = Console.ReadLine();
|
||||||
|
|
||||||
|
if (operation == "")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation == "connect")
|
||||||
|
{
|
||||||
|
var uri = Console.ReadLine();
|
||||||
|
await webSocket.ConnectAsync(new Uri(uri!), CancellationToken.None);
|
||||||
|
Receive();
|
||||||
|
}
|
||||||
|
else if (operation == "message")
|
||||||
|
{
|
||||||
|
cborWriter.Reset();
|
||||||
|
cborWriter.WriteStartArray(null);
|
||||||
|
}
|
||||||
|
else if (operation == "send")
|
||||||
|
{
|
||||||
|
cborWriter.WriteEndArray();
|
||||||
|
var bytes = cborWriter.Encode();
|
||||||
|
await webSocket.SendAsync(bytes, WebSocketMessageType.Binary, true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
else if (operation == "bye")
|
||||||
|
{
|
||||||
|
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
webSocket = new ClientWebSocket();
|
||||||
|
}
|
||||||
|
else if (operation == "help")
|
||||||
|
{
|
||||||
|
Console.WriteLine("Every space should use enter.");
|
||||||
|
Console.WriteLine("Connect to a server: connect <uri>");
|
||||||
|
Console.WriteLine("New message: message");
|
||||||
|
Console.WriteLine("Send message: send");
|
||||||
|
Console.WriteLine("Close websocket: bye");
|
||||||
|
Console.WriteLine("Insert data: data <type> <value>");
|
||||||
|
}
|
||||||
|
else if (operation == "data")
|
||||||
|
{
|
||||||
|
var type = Console.ReadLine();
|
||||||
|
var valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
if (type == "string")
|
||||||
|
{
|
||||||
|
cborWriter.WriteTextString(valueStr!);
|
||||||
|
}
|
||||||
|
else if (type == "int")
|
||||||
|
{
|
||||||
|
var result = int.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = int.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteInt32(value);
|
||||||
|
}
|
||||||
|
else if (type == "long")
|
||||||
|
{
|
||||||
|
var result = long.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = long.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteInt64(value);
|
||||||
|
}
|
||||||
|
else if (type == "bool")
|
||||||
|
{
|
||||||
|
var result = bool.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = bool.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteBoolean(value);
|
||||||
|
}
|
||||||
|
else if (type == "float")
|
||||||
|
{
|
||||||
|
var result = float.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = float.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteSingle(value);
|
||||||
|
}
|
||||||
|
else if (type == "double")
|
||||||
|
{
|
||||||
|
var result = double.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = double.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteDouble(value);
|
||||||
|
}
|
||||||
|
else if (type == "decimal")
|
||||||
|
{
|
||||||
|
var result = decimal.TryParse(valueStr, out var value);
|
||||||
|
while (!result)
|
||||||
|
{
|
||||||
|
valueStr = Console.ReadLine();
|
||||||
|
|
||||||
|
result = decimal.TryParse(valueStr, out value);
|
||||||
|
}
|
||||||
|
cborWriter.WriteDecimal(value);
|
||||||
|
}
|
||||||
|
else if (type == "null")
|
||||||
|
{
|
||||||
|
cborWriter.WriteNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Done!");
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector", "CrystalConnector\CrystalConnector.csproj", "{9DA4D124-899F-4946-8BFC-510654340357}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector", "CrystalConnector\CrystalConnector.csproj", "{9DA4D124-899F-4946-8BFC-510654340357}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorTest", "ConnectorTest\ConnectorTest.csproj", "{7DA8728D-146A-4E85-AEB8-A48130836CF7}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -12,5 +14,9 @@ Global
|
|||||||
{9DA4D124-899F-4946-8BFC-510654340357}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9DA4D124-899F-4946-8BFC-510654340357}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9DA4D124-899F-4946-8BFC-510654340357}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7DA8728D-146A-4E85-AEB8-A48130836CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DA8728D-146A-4E85-AEB8-A48130836CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7DA8728D-146A-4E85-AEB8-A48130836CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7DA8728D-146A-4E85-AEB8-A48130836CF7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CrystalConnector.Handlers;
|
||||||
|
|
||||||
|
public class HandlerConstants
|
||||||
|
{
|
||||||
|
public static readonly string MessageTypeAuthenticate = "Authenticate";
|
||||||
|
public static readonly string MessageTypeRegisterHandler = "Register";
|
||||||
|
public static readonly string MessageTypeEmit = "Emit";
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace CrystalConnector.Handlers;
|
||||||
|
|
||||||
|
public class MessageException : Exception
|
||||||
|
{
|
||||||
|
public MessageException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,17 +20,34 @@ public class WebSocketHandler
|
|||||||
|
|
||||||
public void Handle(WebSocket webSocket, CborReader reader)
|
public void Handle(WebSocket webSocket, CborReader reader)
|
||||||
{
|
{
|
||||||
|
reader.ReadStartArray();
|
||||||
|
|
||||||
var type = reader.ReadTextString();
|
var type = reader.ReadTextString();
|
||||||
if (type == "Authenticate")
|
if (type == HandlerConstants.MessageTypeAuthenticate)
|
||||||
{
|
{
|
||||||
if (!webSocket.GetConnectionInfo().Authorized)
|
if (!webSocket.GetConnectionInfo().Authorized)
|
||||||
{
|
{
|
||||||
var secret = reader.ReadTextString();
|
var secret = reader.ReadTextString();
|
||||||
if (secret == Config.GetValue<string>("Connector:Auth:Secret"))
|
if (secret == Config.GetValue<string>("Connector:Auth:Secret"))
|
||||||
{
|
{
|
||||||
|
Logger.LogInformation("Client {Id} authorized", webSocket.GetConnectionInfo().Id);
|
||||||
webSocket.GetConnectionInfo().Authorized = true;
|
webSocket.GetConnectionInfo().Authorized = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new MessageException("Auth failed!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new MessageException("You are already authorized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == "")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.ReadEndArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,11 +24,21 @@ public static class WebSocketExtension
|
|||||||
|
|
||||||
public static void Disconnect(this WebSocket webSocket)
|
public static void Disconnect(this WebSocket webSocket)
|
||||||
{
|
{
|
||||||
var data = GetConnectionInfo(webSocket);
|
var info = GetConnectionInfo(webSocket);
|
||||||
data.CompletionSource.SetResult(WebSocketHandleResult.Successful);
|
info.CompletionSource.SetResult(WebSocketHandleResult.Successful);
|
||||||
|
|
||||||
webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
|
||||||
WebSocketConnectionManager.Info.Remove(webSocket);
|
WebSocketConnectionManager.Info.Remove(webSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task Send(this WebSocket webSocket, byte[] data)
|
||||||
|
{
|
||||||
|
await webSocket.Send(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task Send(this WebSocket webSocket, byte[] data, int startIndex, int length)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ReadOnlyMemory<byte>(data, startIndex, length), WebSocketMessageType.Binary, true, CancellationToken.None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using System.Net.WebSockets;
|
|||||||
using CrystalConnector.Handlers;
|
using CrystalConnector.Handlers;
|
||||||
using CrystalConnector.Utilities;
|
using CrystalConnector.Utilities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CrystalConnector.WebSockets;
|
namespace CrystalConnector.WebSockets;
|
||||||
@@ -12,11 +13,14 @@ public class WebSocketManager : IWebSocketManager
|
|||||||
{
|
{
|
||||||
private ILogger<WebSocketManager> Logger { get; }
|
private ILogger<WebSocketManager> Logger { get; }
|
||||||
|
|
||||||
|
private IConfiguration Config { get; }
|
||||||
|
|
||||||
private WebSocketHandler Handler { get; }
|
private WebSocketHandler Handler { get; }
|
||||||
|
|
||||||
public WebSocketManager(ILogger<WebSocketManager> logger, WebSocketHandler handler)
|
public WebSocketManager(ILogger<WebSocketManager> logger, IConfiguration config, WebSocketHandler handler)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
|
Config = config;
|
||||||
Handler = handler;
|
Handler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +40,32 @@ public class WebSocketManager : IWebSocketManager
|
|||||||
{
|
{
|
||||||
var reader = new CborReader(new ReadOnlyMemory<byte>(buffer, 0, receiveResult.Count));
|
var reader = new CborReader(new ReadOnlyMemory<byte>(buffer, 0, receiveResult.Count));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
Handler.Handle(webSocket, reader);
|
Handler.Handle(webSocket, reader);
|
||||||
|
}
|
||||||
|
catch (CborContentException ex)
|
||||||
|
{
|
||||||
|
if (Config.GetValue<bool>("Connector:Debug"))
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Malformed message!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Disconnect(webSocket);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
if (Config.GetValue<bool>("Connector:Debug"))
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Bad message!");
|
||||||
|
}
|
||||||
|
Disconnect(webSocket);
|
||||||
|
}
|
||||||
|
catch (MessageException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Why do that?");
|
||||||
|
Disconnect(webSocket);
|
||||||
|
}
|
||||||
|
|
||||||
receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Connector": {
|
"Connector": {
|
||||||
|
"Debug": false,
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"Secret": "qyl27IsHere!"
|
"Secret": "<I am an example secret!>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,62 @@
|
|||||||
# CrystalConnector
|
# CrystalConnector / 水晶互联
|
||||||
【水晶互联】猫猫之间的三次握爪,四次摇尾巴!
|
猫猫之间的三次握爪,四次摇尾巴!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 传输协议
|
||||||
|
|
||||||
|
水晶互联采用 WebSocket 进行消息传输,使用 CBOR 进行消息编码。
|
||||||
|
|
||||||
|
为了贴近 Guava 中 `ByteArrayDataInput` / `ByteArrayDataOutput` 的使用习惯,规定以单层基本类型的数组形式使用 CBOR 。(Todo:支持更复杂的结构。)
|
||||||
|
|
||||||
|
参考代码:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
var writer = new CborWriter();
|
||||||
|
writer.WriteStartArray(null); // 不定长数组
|
||||||
|
// 消息序列化
|
||||||
|
writer.WriteEndArray();
|
||||||
|
|
||||||
|
var reader = new CborReader(buffer);
|
||||||
|
reader.ReadStartArray();
|
||||||
|
// 消息反序列化
|
||||||
|
reader.ReadEndArray();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文档约定
|
||||||
|
|
||||||
|
文档中的代码块,以行首的 `>` 代表客户端向服务端传输的消息;以行首的 `<` 代表服务端向客户端传输的消息。其后的消息以纯文本形式发送。如需某一端进行操作,在尖括号后以 `!` 标志体现。
|
||||||
|
|
||||||
|
服务端是指水晶互联的服务端,一个 C# 程序用来完成消息转发操作。
|
||||||
|
|
||||||
|
客户端是指所有通过 WebSocket 连接服务端的消息提供或消费者。
|
||||||
|
|
||||||
|
文档中使用尖括号对 `<type:Name>` 代表必填参数,方括号对 `[type:Name]` 代表可选参数。 `type` 是参数类型,如 `string` 、 `int` 等。 `Name` 是参数名,不在具体传输过程中体现,是方便开发人员记忆的名称。
|
||||||
|
|
||||||
|
### 客户端 to 服务端
|
||||||
|
|
||||||
|
#### 认证客户端
|
||||||
|
|
||||||
|
为了防止未授权的传输,水晶互联需要一个 SecretKey 来信任客户端,它由服务端配置文件指定。(这个设计没有任何安全性的保证,请保证不在公网中使用它。)
|
||||||
|
|
||||||
|
认证请求:
|
||||||
|
|
||||||
|
```
|
||||||
|
> Authenticate
|
||||||
|
> <string:SecretKey>
|
||||||
|
```
|
||||||
|
|
||||||
|
认证成功:
|
||||||
|
|
||||||
|
```
|
||||||
|
< Authenticated
|
||||||
|
```
|
||||||
|
|
||||||
|
认证失败:
|
||||||
|
|
||||||
|
```
|
||||||
|
<! Close
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注册接收器
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user