diff --git a/ConnectorTest/ConnectorTest.csproj b/ConnectorTest/ConnectorTest.csproj
new file mode 100644
index 0000000..b90ebde
--- /dev/null
+++ b/ConnectorTest/ConnectorTest.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/ConnectorTest/Program.cs b/ConnectorTest/Program.cs
new file mode 100644
index 0000000..f72da2c
--- /dev/null
+++ b/ConnectorTest/Program.cs
@@ -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(buffer), CancellationToken.None);
+
+ while (!result.CloseStatus.HasValue)
+ {
+ var reader = new CborReader(new ReadOnlyMemory(buffer, 0, result.Count));
+ Console.WriteLine(reader.BytesRemaining);
+
+ result = await webSocket.ReceiveAsync(new ArraySegment(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 ");
+ Console.WriteLine("New message: message");
+ Console.WriteLine("Send message: send");
+ Console.WriteLine("Close websocket: bye");
+ Console.WriteLine("Insert data: data ");
+ }
+ 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!");
+}
diff --git a/CrystalConnector.sln b/CrystalConnector.sln
index 49543a6..c0f31b2 100644
--- a/CrystalConnector.sln
+++ b/CrystalConnector.sln
@@ -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
diff --git a/CrystalConnector/Handlers/HandlerConstants.cs b/CrystalConnector/Handlers/HandlerConstants.cs
new file mode 100644
index 0000000..24b8bd5
--- /dev/null
+++ b/CrystalConnector/Handlers/HandlerConstants.cs
@@ -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";
+}
\ No newline at end of file
diff --git a/CrystalConnector/Handlers/MessageException.cs b/CrystalConnector/Handlers/MessageException.cs
new file mode 100644
index 0000000..b751f21
--- /dev/null
+++ b/CrystalConnector/Handlers/MessageException.cs
@@ -0,0 +1,9 @@
+namespace CrystalConnector.Handlers;
+
+public class MessageException : Exception
+{
+ public MessageException(string message)
+ : base(message)
+ {
+ }
+}
\ No newline at end of file
diff --git a/CrystalConnector/Handlers/WebSocketHandler.cs b/CrystalConnector/Handlers/WebSocketHandler.cs
index 09034fe..0f3ef15 100644
--- a/CrystalConnector/Handlers/WebSocketHandler.cs
+++ b/CrystalConnector/Handlers/WebSocketHandler.cs
@@ -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("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();
}
}
\ No newline at end of file
diff --git a/CrystalConnector/Utilities/WebSocketExtension.cs b/CrystalConnector/Utilities/WebSocketExtension.cs
index 5cef381..cda35c5 100644
--- a/CrystalConnector/Utilities/WebSocketExtension.cs
+++ b/CrystalConnector/Utilities/WebSocketExtension.cs
@@ -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(data, startIndex, length), WebSocketMessageType.Binary, true, CancellationToken.None);
+ }
}
\ No newline at end of file
diff --git a/CrystalConnector/WebSockets/WebSocketManager.cs b/CrystalConnector/WebSockets/WebSocketManager.cs
index e163bbb..a847a19 100644
--- a/CrystalConnector/WebSockets/WebSocketManager.cs
+++ b/CrystalConnector/WebSockets/WebSocketManager.cs
@@ -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;
@@ -11,12 +12,15 @@ namespace CrystalConnector.WebSockets;
public class WebSocketManager : IWebSocketManager
{
private ILogger Logger { get; }
+
+ private IConfiguration Config { get; }
private WebSocketHandler Handler { get; }
- public WebSocketManager(ILogger logger, WebSocketHandler handler)
+ public WebSocketManager(ILogger 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(buffer, 0, receiveResult.Count));
- Handler.Handle(webSocket, reader);
+ try
+ {
+ Handler.Handle(webSocket, reader);
+ }
+ catch (CborContentException ex)
+ {
+ if (Config.GetValue("Connector:Debug"))
+ {
+ Logger.LogWarning(ex, "Malformed message!");
+ }
+
+ Disconnect(webSocket);
+ }
+ catch (InvalidOperationException ex)
+ {
+ if (Config.GetValue("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(buffer), CancellationToken.None);
}
diff --git a/CrystalConnector/config.json b/CrystalConnector/config.json
index c4f45d7..a6229bc 100644
--- a/CrystalConnector/config.json
+++ b/CrystalConnector/config.json
@@ -14,8 +14,9 @@
]
},
"Connector": {
+ "Debug": false,
"Auth": {
- "Secret": "qyl27IsHere!"
+ "Secret": ""
}
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 756f7d8..78b65ce 100644
--- a/README.md
+++ b/README.md
@@ -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` 是参数类型,如 `string` 、 `int` 等。 `Name` 是参数名,不在具体传输过程中体现,是方便开发人员记忆的名称。
+
+### 客户端 to 服务端
+
+#### 认证客户端
+
+为了防止未授权的传输,水晶互联需要一个 SecretKey 来信任客户端,它由服务端配置文件指定。(这个设计没有任何安全性的保证,请保证不在公网中使用它。)
+
+认证请求:
+
+```
+> Authenticate
+>
+```
+
+认证成功:
+
+```
+< Authenticated
+```
+
+认证失败:
+
+```
+