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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector", "CrystalConnector\CrystalConnector.csproj", "{9DA4D124-899F-4946-8BFC-510654340357}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorTest", "ConnectorTest\ConnectorTest.csproj", "{7DA8728D-146A-4E85-AEB8-A48130836CF7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
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)
|
||||
{
|
||||
reader.ReadStartArray();
|
||||
|
||||
var type = reader.ReadTextString();
|
||||
if (type == "Authenticate")
|
||||
if (type == HandlerConstants.MessageTypeAuthenticate)
|
||||
{
|
||||
if (!webSocket.GetConnectionInfo().Authorized)
|
||||
{
|
||||
var secret = reader.ReadTextString();
|
||||
if (secret == Config.GetValue<string>("Connector:Auth:Secret"))
|
||||
{
|
||||
Logger.LogInformation("Client {Id} authorized", webSocket.GetConnectionInfo().Id);
|
||||
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)
|
||||
{
|
||||
var data = GetConnectionInfo(webSocket);
|
||||
data.CompletionSource.SetResult(WebSocketHandleResult.Successful);
|
||||
var info = GetConnectionInfo(webSocket);
|
||||
info.CompletionSource.SetResult(WebSocketHandleResult.Successful);
|
||||
|
||||
webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
|
||||
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.Utilities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CrystalConnector.WebSockets;
|
||||
@@ -12,11 +13,14 @@ public class WebSocketManager : IWebSocketManager
|
||||
{
|
||||
private ILogger<WebSocketManager> Logger { get; }
|
||||
|
||||
private IConfiguration Config { get; }
|
||||
|
||||
private WebSocketHandler Handler { get; }
|
||||
|
||||
public WebSocketManager(ILogger<WebSocketManager> logger, WebSocketHandler handler)
|
||||
public WebSocketManager(ILogger<WebSocketManager> logger, IConfiguration config, WebSocketHandler handler)
|
||||
{
|
||||
Logger = logger;
|
||||
Config = config;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
@@ -36,7 +40,32 @@ public class WebSocketManager : IWebSocketManager
|
||||
{
|
||||
var reader = new CborReader(new ReadOnlyMemory<byte>(buffer, 0, receiveResult.Count));
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
]
|
||||
},
|
||||
"Connector": {
|
||||
"Debug": false,
|
||||
"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