✨ TestClient, but wait, what are we actually need?

Signed-off-by: 秋雨落 <i@rain.cx>
This commit is contained in:
秋雨落
2023-09-13 16:12:27 +08:00
parent c43b48398e
commit 185b231e7d
10 changed files with 320 additions and 8 deletions
+16
View File
@@ -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>
+156
View File
@@ -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!");
}
+6
View File
@@ -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)
{
}
}
+18 -1
View File
@@ -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;
@@ -11,12 +12,15 @@ namespace CrystalConnector.WebSockets;
public class WebSocketManager : IWebSocketManager 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));
Handler.Handle(webSocket, reader); 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); receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
} }
+2 -1
View File
@@ -14,8 +14,9 @@
] ]
}, },
"Connector": { "Connector": {
"Debug": false,
"Auth": { "Auth": {
"Secret": "qyl27IsHere!" "Secret": "<I am an example secret!>"
} }
} }
} }
+62 -2
View File
@@ -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
```
#### 注册接收器