* 🚧 WIP v2.

Signed-off-by: 秋雨落 <i@rain.cx>

* 🚧 WIP v2 * 2.

Signed-off-by: 秋雨落 <i@rain.cx>

* ✨ It works?

Signed-off-by: 秋雨落 <i@rain.cx>

* ✨ Publish to nexus.

Signed-off-by: 秋雨落 <i@rain.cx>

---------

Signed-off-by: 秋雨落 <i@rain.cx>
This commit is contained in:
秋 雨落
2024-01-10 09:52:50 +08:00
committed by GitHub
parent 3af3badcc5
commit 84f4e3543c
32 changed files with 2361 additions and 385 deletions
+5 -1
View File
@@ -9,8 +9,12 @@
<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.Formats.Cbor" Version="8.0.0" />
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CrystalConnector.Protocol\CrystalConnector.Protocol.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>qyl27,MeowCraftMC</Authors>
<RepositoryUrl>https://github.com/MeowCraftMC/CrystalConnector</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<IncludeSymbols>True</IncludeSymbols>
<PackageProjectUrl>https://github.com/MeowCraftMC/CrystalConnector</PackageProjectUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.25.1" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="proto\messages.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<Folder Include="Messages\" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,53 @@
using CrystalConnector.Protocol.Messages;
using Google.Protobuf;
namespace CrystalConnector.Protocol.Packets.C2S;
public class AuthenticatePacket : IPacket
{
public NamespacedName ClientId { get; set; }
public string ClientName { get; set; }
public string Key { get; set; }
public List<Channel> Channels { get; } = [];
public AuthenticatePacket(NamespacedName clientId, string clientName, string key, List<Channel> channels)
{
ClientId = clientId;
ClientName = clientName;
Key = key;
Channels.AddRange(channels);
}
public AuthenticatePacket(byte[] bytes)
{
Read(bytes);
}
public AuthenticatePacket()
{
}
public void Read(byte[] bytes)
{
var message = Authenticate.Parser.ParseFrom(bytes);
ClientId = message.ClientId;
ClientName = message.ClientName;
Key = message.SecretKey;
Channels.AddRange(message.Channels);
}
public byte[] Write()
{
var message = new Authenticate
{
ClientId = ClientId,
ClientName = ClientName,
SecretKey = Key
};
message.Channels.AddRange(Channels);
return message.ToByteArray();
}
}
@@ -0,0 +1,43 @@
using CrystalConnector.Protocol.Messages;
using Google.Protobuf;
namespace CrystalConnector.Protocol.Packets.C2S;
public class PublishPacket : IPacket
{
public NamespacedName Channel { get; set; }
public string Payload { get; set; }
public PublishPacket(NamespacedName channel, string payload)
{
Channel = channel;
Payload = payload;
}
public PublishPacket(byte[] bytes)
{
Read(bytes);
}
public PublishPacket()
{
}
public void Read(byte[] bytes)
{
var message = Publish.Parser.ParseFrom(bytes);
Channel = message.Channel;
Payload = message.Payload;
}
public byte[] Write()
{
var message = new Publish
{
Channel = Channel,
Payload = Payload
};
return message.ToByteArray();
}
}
@@ -0,0 +1,50 @@
using CrystalConnector.Protocol.Packets.C2S;
using CrystalConnector.Protocol.Packets.S2C;
using Google.Protobuf;
namespace CrystalConnector.Protocol.Packets;
public interface IPacket
{
public void Read(byte[] bytes);
public byte[] Write();
public static IPacket? From(byte[] data)
{
var result = TryForm<AuthenticatePacket>(data);
if (result is not null)
{
return result;
}
result = TryForm<PublishPacket>(data);
if (result is not null)
{
return result;
}
result = TryForm<ForwardPacket>(data);
if (result is not null)
{
return result;
}
result = TryForm<ResultPacket>(data);
return result;
}
private static IPacket? TryForm<T>(byte[] data) where T : IPacket, new()
{
try
{
var message = new T();
message.Read(data);
return message;
}
catch (InvalidProtocolBufferException ex)
{
return null;
}
}
}
@@ -0,0 +1,51 @@
using CrystalConnector.Protocol.Messages;
using Google.Protobuf;
namespace CrystalConnector.Protocol.Packets.S2C;
public class ForwardPacket : IPacket
{
public string Publisher { get; set; }
public NamespacedName Channel { get; set; }
public string Payload { get; set; }
public ForwardPacket(string publisher, (string Namespace, string Name) channel, string payload)
{
Publisher = publisher;
Channel = new NamespacedName
{
Namespace = channel.Namespace,
Name = channel.Name
};
Payload = payload;
}
public ForwardPacket(byte[] bytes)
{
Read(bytes);
}
public ForwardPacket()
{
}
public void Read(byte[] bytes)
{
var message = Forward.Parser.ParseFrom(bytes);
Publisher = message.Publisher;
Channel = message.Channel;
Payload = message.Payload;
}
public byte[] Write()
{
var message = new Forward
{
Publisher = Publisher,
Channel = Channel,
Payload = Payload
};
return message.ToByteArray();
}
}
@@ -0,0 +1,59 @@
using CrystalConnector.Protocol.Messages;
using Google.Protobuf;
namespace CrystalConnector.Protocol.Packets.S2C;
public class ResultPacket : IPacket
{
public Result.ResultOneofCase ResultOneofCase { get; set; }
public Error? Error { get; set; }
public ResultPacket()
{
ResultOneofCase = Result.ResultOneofCase.Successful;
}
public ResultPacket(Error error)
{
ResultOneofCase = Result.ResultOneofCase.Error;
Error = error;
}
public ResultPacket(byte[] bytes)
{
Read(bytes);
}
public void Read(byte[] bytes)
{
var message = Result.Parser.ParseFrom(bytes);
ResultOneofCase = message.ResultCase;
if (message.HasError)
{
Error = message.Error;
}
}
public byte[] Write()
{
var message = ResultOneofCase switch
{
Result.ResultOneofCase.None => new Result
{
Error = Messages.Error.Unknown
},
Result.ResultOneofCase.Successful => new Result
{
Successful = new Successful()
},
Result.ResultOneofCase.Error => new Result
{
Error = Error!.Value
},
_ => throw new ArgumentOutOfRangeException()
};
return message.ToByteArray();
}
}
+7
View File
@@ -0,0 +1,7 @@
# CrystalConnector.Protocol
## Generate Proto file
```shell
protoc.exe --proto_path=proto --csharp_out=Messages proto\messages.proto
```
@@ -0,0 +1,25 @@
using CrystalConnector.Protocol.Messages;
namespace CrystalConnector.Protocol.Utilities;
public static class PacketHelperExtensions
{
public static (string Namespace, string Name) ToTuple(this NamespacedName name)
{
return (name.Namespace, name.Name);
}
public static NamespacedName ToName(this (string Namespace, string Name) name)
{
return new NamespacedName
{
Namespace = name.Namespace,
Name = name.Name
};
}
public static string ToIdString(this (string Namespace, string Name) name)
{
return $"{name.Namespace}:{name.Name}";
}
}
@@ -0,0 +1,59 @@
syntax = "proto3";
option csharp_namespace = "CrystalConnector.Protocol.Messages";
package CrystalConnector;
message NamespacedName {
string namespace = 1;
string name = 2;
}
enum MessageDirection {
NONE = 0;
INCOMING = 1;
OUTGOING = 2;
ALL = 3;
}
message Successful {
}
enum Error {
UNKNOWN = 0;
AUTHENTICATED = 1;
NAME_EXISTS = 2;
UNAUTHENTICATED = 3;
UNREGISTERED_CHANNEL = 4;
INTERNAL = 5;
}
message Result {
oneof result {
Successful successful = 1;
Error error = 2;
}
}
message Authenticate {
NamespacedName client_id = 1;
string client_name = 2;
string secret_key = 3;
repeated Channel channels = 4;
}
message Channel {
NamespacedName id = 1;
MessageDirection direction = 2;
}
message Publish {
NamespacedName channel = 1;
string payload = 2;
}
message Forward {
string publisher = 1;
NamespacedName channel = 2;
string payload = 3;
}
+6
View File
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector", "Crystal
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorTest", "ConnectorTest\ConnectorTest.csproj", "{7DA8728D-146A-4E85-AEB8-A48130836CF7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrystalConnector.Protocol", "CrystalConnector.Protocol\CrystalConnector.Protocol.csproj", "{1769D402-129D-46D2-9347-70F3E875FE7B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{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
{1769D402-129D-46D2-9347-70F3E875FE7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1769D402-129D-46D2-9347-70F3E875FE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1769D402-129D-46D2-9347-70F3E875FE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1769D402-129D-46D2-9347-70F3E875FE7B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+4 -2
View File
@@ -1,8 +1,10 @@
namespace CrystalConnector.Connector;
using CrystalConnector.Protocol.Messages;
namespace CrystalConnector.Connector;
public class MessageChannel
{
public required string Id { get; set; }
public required (string Namespace, string Name) Id { get; set; }
public required MessageDirection Direction { get; set; }
}
@@ -1,10 +0,0 @@
namespace CrystalConnector.Connector;
[Flags]
public enum MessageDirection
{
None = 0,
Incoming = 1,
Outgoing = 2,
All = Incoming | Outgoing
}
@@ -1,14 +0,0 @@
using System.Formats.Cbor;
namespace CrystalConnector.Connector.Packet;
public interface IPacket
{
public void Write(CborWriter writer)
{
}
public void Read(CborReader reader)
{
}
}
@@ -1,15 +0,0 @@
using System.Formats.Cbor;
namespace CrystalConnector.Connector.Packet.S2C;
public abstract class PacketS2C : IPacket
{
public void Write(CborWriter writer)
{
writer.WriteStartArray(null);
WriteData(writer);
writer.WriteEndArray();
}
protected abstract void WriteData(CborWriter writer);
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CAuthenticatedPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorAuthenticated);
}
}
@@ -1,26 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CForwardMessagePacket : PacketS2C
{
private string Origin { get; }
private string ChannelId { get; }
private byte[] Payload { get; }
public S2CForwardMessagePacket(string origin, string channelId, byte[] payload)
{
Origin = origin;
ChannelId = channelId;
Payload = payload;
}
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseForward);
writer.WriteTextString(Origin);
writer.WriteTextString(ChannelId);
writer.WriteByteString(Payload);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CMalformedPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorMalformed);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CNameAuthenticatedPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorNameAuthenticated);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CSuccessfulPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseSuccessful);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CUnauthenticatedPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorUnauthenticated);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CUndefinedDirectionPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorUndefinedDirection);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CUnknownPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorUnknownPacket);
}
}
@@ -1,12 +0,0 @@
using System.Formats.Cbor;
using CrystalConnector.Handlers;
namespace CrystalConnector.Connector.Packet.S2C;
public class S2CUnregisteredDirectionPacket : PacketS2C
{
protected override void WriteData(CborWriter writer)
{
writer.WriteTextString(HandlerConstants.ResponseErrorUnregisteredDirection);
}
}
@@ -1,5 +1,6 @@
using System.Net.WebSockets;
using CrystalConnector.Connector.Packet.S2C;
using CrystalConnector.Protocol.Messages;
using CrystalConnector.Protocol.Packets.S2C;
using CrystalConnector.Utilities;
namespace CrystalConnector.Connector;
@@ -8,11 +9,11 @@ public class WebSocketConnectionManager
{
internal static Dictionary<WebSocket, WebSocketConnectionInfo> Info { get; } = new();
public static bool HaveNameRegistered(string name)
public static bool HaveClientRegistered((string Namespace, string Name) client)
{
foreach (var (webSocket, info) in Info)
foreach (var (_, info) in Info)
{
if (info.Name == name)
if (info.ClientId == client)
{
return true;
}
@@ -23,21 +24,20 @@ public class WebSocketConnectionManager
public static void DisconnectAll()
{
foreach (var (webSocket, info) in Info)
foreach (var (webSocket, _) in Info)
{
webSocket.Disconnect();
}
}
public static async Task Broadcast(string origin, string channelId, byte[] payload, bool allowUnauthenticated = false)
public static async Task Broadcast(string origin, (string Namespace, string Name) channel, string payload, bool allowUnauthenticated = false)
{
var packet = new S2CForwardMessagePacket(origin, channelId, payload);
var packet = new ForwardPacket(origin, channel, payload);
foreach (var (webSocket, info) in Info)
{
if (info.RegisteredChannels.TryGetValue(channelId, out var value)
&& value.Direction.HasFlag(MessageDirection.Incoming)
&& info.Name != origin)
if (info.RegisteredChannels.TryGetValue(channel, out var value)
&& value.Direction is MessageDirection.Incoming or MessageDirection.All)
{
if (webSocket.State != WebSocketState.Open)
{
@@ -45,7 +45,7 @@ public class WebSocketConnectionManager
continue;
}
await webSocket.Send(packet);
await webSocket.Send(packet.Write());
}
}
}
+13 -6
View File
@@ -8,14 +8,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-rc.1.23419.4" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="NLog" Version="5.2.4" />
<PackageReference Include="NLog.Extensions.Hosting" Version="5.3.4" />
<PackageReference Include="System.Formats.Cbor" Version="8.0.0-rc.1.23419.4" />
</ItemGroup>
<ItemGroup>
@@ -31,6 +30,14 @@
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CrystalConnector.Protocol\CrystalConnector.Protocol.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Connector\Packet\" />
</ItemGroup>
</Project>
+71 -84
View File
@@ -1,9 +1,11 @@
using System.Formats.Cbor;
using System.Net.WebSockets;
using System.Net.WebSockets;
using CrystalConnector.Configs;
using CrystalConnector.Connector;
using CrystalConnector.Connector.Packet;
using CrystalConnector.Connector.Packet.S2C;
using CrystalConnector.Protocol.Messages;
using CrystalConnector.Protocol.Packets;
using CrystalConnector.Protocol.Packets.C2S;
using CrystalConnector.Protocol.Packets.S2C;
using CrystalConnector.Protocol.Utilities;
using CrystalConnector.Utilities;
using Microsoft.Extensions.Logging;
@@ -21,111 +23,96 @@ public class WebSocketHandler
Logger = logger;
}
public async Task<PacketS2C> Handle(WebSocket webSocket, CborReader reader)
public async Task<IPacket> Handle(WebSocket webSocket, byte[] data)
{
reader.ReadStartArray();
// Todo: qyl27: Use C2SPacketReader.
var type = reader.ReadTextString();
if (type == HandlerConstants.OperationAuthenticate)
var packet = IPacket.From(data);
switch (packet)
{
if (!webSocket.GetConnectionInfo().Authenticated)
case AuthenticatePacket authenticatePacket:
{
var secret = reader.ReadTextString();
var name = reader.ReadTextString();
if (secret == Config.GetSecretKey())
var clientId = (authenticatePacket.ClientId.Namespace, authenticatePacket.ClientId.Name);
if (authenticatePacket.Key == Config.GetSecretKey())
{
if (WebSocketConnectionManager.HaveNameRegistered(name))
if (WebSocketConnectionManager.HaveClientRegistered(clientId))
{
Logger.LogInformation("Client {Id} tried authorize with name {Name}, but it is already authorized!", webSocket.GetConnectionInfo().Id, name);
return new S2CNameAuthenticatedPacket();
Logger.LogInformation("Client {Id} tried authorize with name {Name}, but it is already authorized!", clientId.ToIdString(), authenticatePacket.ClientName);
return new ResultPacket(Error.NameExists);
}
Logger.LogInformation("Client {Name}({Id}) authorized", name, webSocket.GetConnectionInfo().Id);
webSocket.GetConnectionInfo().Authenticated = true;
webSocket.GetConnectionInfo().Name = name;
return new S2CSuccessfulPacket();
Logger.LogInformation("Client {Name}({Id}) authorized", authenticatePacket.ClientName, clientId.ToIdString());
var connectionInfo = webSocket.GetConnectionInfo();
connectionInfo.Authenticated = true;
connectionInfo.ClientId = clientId;
connectionInfo.Name = authenticatePacket.ClientName;
var channels = connectionInfo.RegisteredChannels;
foreach (var channel in authenticatePacket.Channels)
{
var id = channel.Id.ToTuple();
if (channels.TryGetValue(id, out var chan))
{
chan.Direction = channel.Direction;
}
else
{
channels.Add(id, new MessageChannel
{
Id = id,
Direction = channel.Direction
});
}
}
return new ResultPacket();
}
Logger.LogWarning("Client {Name}({Id}) auth failed with wrong key!", name, webSocket.GetConnectionInfo().Id);
return new S2CUnauthenticatedPacket();
Logger.LogWarning("Client {Name}({Id}) auth failed with wrong key!", authenticatePacket.ClientName, clientId.ToIdString());
return new ResultPacket(Error.Unauthenticated);
}
return new S2CAuthenticatedPacket();
}
if (type == HandlerConstants.OperationRegisterChannel)
{
if (!webSocket.GetConnectionInfo().Authenticated)
case PublishPacket publishPacket:
{
Logger.LogWarning("Client {Id} is unauthenticated!", webSocket.GetConnectionInfo().Id);
return new S2CUnauthenticatedPacket();
}
var connectionInfo = webSocket.GetConnectionInfo();
var id = publishPacket.Channel.ToTuple();
var channelId = reader.ReadTextString();
var directionValue = reader.ReadInt32();
if (!Enum.IsDefined(typeof(MessageDirection), directionValue))
{
Logger.LogWarning("Client {Name}({Id}) tried to register channel {Channel} with undefined direction {Direction}!",
webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id, channelId, directionValue);
return new S2CUndefinedDirectionPacket();
}
var direction = (MessageDirection)directionValue;
var channels = webSocket.GetConnectionInfo().RegisteredChannels;
if (channels.TryGetValue(channelId, out var channel))
{
channel.Direction |= direction;
}
else
{
webSocket.GetConnectionInfo().RegisteredChannels.Add(channelId, new MessageChannel
if (!connectionInfo.Authenticated)
{
Id = channelId,
Direction = direction
});
}
return new ResultPacket(Error.Unauthenticated);
}
Logger.LogInformation("Client {Name}({Id}) registered channel {Channel}(Direction: {Direction})",
webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id, channelId, direction.ToString());
return new S2CSuccessfulPacket();
}
if (!connectionInfo.RegisteredChannels.TryGetValue(id, out var registered)
|| registered.Direction == MessageDirection.Outgoing
|| registered.Direction == MessageDirection.All)
{
Logger.LogWarning("Channel {Channel}(Direction: {Direction}) have not been registered by Client {Name}({Id})",
id.ToIdString(), MessageDirection.Outgoing, webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id);
return new ResultPacket(Error.UnregisteredChannel);
}
if (type == HandlerConstants.OperationPublish)
{
var channelId = reader.ReadTextString();
var payload = reader.ReadByteString();
if (!webSocket.GetConnectionInfo().RegisteredChannels.TryGetValue(channelId, out var value)
|| !value.Direction.HasFlag(MessageDirection.Outgoing))
{
Logger.LogWarning("Channel {Channel}(Direction: {Direction}) have not been registered by Client {Name}({Id})",
channelId, MessageDirection.Outgoing, webSocket.GetConnectionInfo().Name, webSocket.GetConnectionInfo().Id);
return new S2CUnregisteredDirectionPacket();
}
try
{
var name = webSocket.GetConnectionInfo().Name;
await WebSocketConnectionManager.Broadcast(name!, channelId, payload);
await WebSocketConnectionManager.Broadcast(name!, id, publishPacket.Payload);
if (Config.IsDebug())
{
Logger.LogDebug("Client {Name}({Id}) send data in {Channel}: {Data}",
name, webSocket.GetConnectionInfo().Id, channelId, Convert.ToBase64String(payload));
name, webSocket.GetConnectionInfo().ClientId!.Value.ToIdString(), id.ToIdString(), publishPacket.Payload);
}
return new S2CSuccessfulPacket();
return new ResultPacket();
}
catch (Exception ex)
case ForwardPacket forwardPacket:
{
Logger.LogError(ex, "Send error!");
break;
}
case ResultPacket resultPacket:
{
break;
}
}
reader.ReadEndArray();
return new S2CUnknownPacket();
return new ResultPacket();
}
}
@@ -13,7 +13,9 @@ public class WebSocketConnectionInfo
public string? Name { get; set; }
public (string Namespace, string Name)? ClientId { get; set; }
public bool Authenticated { get; set; } = false;
public Dictionary<string, MessageChannel> RegisteredChannels { get; } = new();
public Dictionary<(string Namespace, string Name), MessageChannel> RegisteredChannels { get; } = new();
}
@@ -1,8 +1,7 @@
using System.Formats.Cbor;
using System.Net;
using System.Net;
using System.Net.WebSockets;
using CrystalConnector.Connector;
using CrystalConnector.Connector.Packet.S2C;
using CrystalConnector.Protocol.Packets;
namespace CrystalConnector.Utilities;
@@ -34,11 +33,10 @@ public static class WebSocketExtension
Purge(webSocket);
}
public static async Task Send(this WebSocket webSocket, PacketS2C packet)
public static async Task Send(this WebSocket webSocket, IPacket packet)
{
var writer = new CborWriter();
packet.Write(writer);
await webSocket.Send(writer.Encode());
var bytes = packet.Write();
await webSocket.Send(bytes);
}
public static async Task Send(this WebSocket webSocket, byte[] data)
@@ -1,9 +1,9 @@
using System.Formats.Cbor;
using System.Net;
using System.Net;
using System.Net.WebSockets;
using CrystalConnector.Configs;
using CrystalConnector.Connector.Packet.S2C;
using CrystalConnector.Handlers;
using CrystalConnector.Protocol.Messages;
using CrystalConnector.Protocol.Packets.S2C;
using CrystalConnector.Utilities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -39,35 +39,21 @@ public class WebSocketManager : IWebSocketManager
while (!receiveResult.CloseStatus.HasValue)
{
var reader = new CborReader(new ReadOnlyMemory<byte>(buffer, 0, receiveResult.Count));
var bytes = new ReadOnlyMemory<byte>(buffer, 0, receiveResult.Count);
try
{
var resultPacket = await Handler.Handle(webSocket, reader);
var resultPacket = await Handler.Handle(webSocket, bytes.ToArray());
await webSocket.Send(resultPacket);
}
catch (CborContentException ex)
{
if (Config.IsDebug())
{
Logger.LogWarning(ex, "Unknown message!");
}
await webSocket.Send(new S2CUnknownPacket());
}
catch (InvalidOperationException ex)
catch (Exception ex)
{
if (Config.IsDebug())
{
Logger.LogWarning(ex, "Malformed message!");
}
await webSocket.Send(new S2CMalformedPacket());
}
catch (MessageException ex)
{
Logger.LogWarning(ex, "Why do that?");
await webSocket.Send(new S2CMalformedPacket());
await webSocket.Send(new ResultPacket(Error.Internal));
}
try
+64 -89
View File
@@ -5,54 +5,31 @@
## 传输协议
版本:**V1.0.1(2023.9.18 更新)**
版本:**V2.0(2024.1.9 更新)**
水晶互联采用 WebSocket 进行消息传输,使用 CBOR 进行消息编码。
水晶互联采用 WebSocket 进行消息传输,使用 Protobuf 进行消息编码。
为了贴近 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` 是参数名,不在具体传输过程中体现,是方便开发人员记忆的名称。
Proto 文件见 `CrystalConnector.Protocol/proto` 目录。
### 协议约定
传输的所有的字符串都是 UTF-8 编码。
**服务端不会主动断开客户端的链接。**
当客户端操作成功时,服务端必定响应:
```
< Successful
```
当操作失败时,服务端需要指出(供机器处理的)原因:
#### 服务端响应
```
< <string:Reason>
```protobuf
message Result {
oneof result {
Successful successful = 1;
Error error = 2;
}
}
```
@@ -61,15 +38,14 @@ reader.ReadEndArray();
目前已定义的原因列表如下:
| 原因 | 说明 |
| --------------------- | ------------------------------------------------------------ |
| Unknown | 服务端无法处理客户端所发送的数据包,产生此错误。 |
| Malformed | 客户端发给服务端的信息是 CBOR 数据包,但无法按协议规则解析时,产生此错误。 |
| Authenticated | 当已认证客户端再次申请认证时,产生此错误。 |
| NameAuthenticated | 当某个客户端尝试以一个已认证的客户端身份进行认证时,产生此错误。 |
| Unauthenticated | 当客户端认证失败或者未经认证的客户端执行除认证外的其他操作时,产生此错误。 |
| UndefinedDirection | 当客户端尝试注册的频道信息方向超过了信息方向枚举允许的值时,产生此错误。 |
| UnregisteredDirection | 当客户端未注册外发某个频道的信息,却想要发布对应频道信息时,产生此错误。 |
| 原因 | 说明 |
| ------------------- | ------------------------------------------------------------ |
| Unknown | 服务端无法理解客户端的需求,产生此错误。 |
| Authenticated | 当已认证客户端再次申请认证时,产生此错误。 |
| NameExists | 当某个客户端尝试以一个已认证的客户端身份进行认证时,产生此错误。 |
| Unauthenticated | 当客户端认证失败或者未经认证的客户端执行除认证外的其他操作时,产生此错误。 |
| UnregisteredChannel | 当客户端未注册外发某个频道的信息,却想要发布对应频道信息时,产生此错误。 |
| Internal | 服务端由于内部问题,无法完整处理客户端的请求时,产生此错误。 |
@@ -77,24 +53,22 @@ reader.ReadEndArray();
##### 消息方向(MessageDirection)
```
```protobuf
enum MessageDirection {
None = 0,
Incoming = 1,
Outgoing = 2,
All = Incoming | Outgoing // 3
NONE = 0;
INCOMING = 1;
OUTGOING = 2;
ALL = 3;
}
```
`Incoming` 代表服务端向客户端转发的信息,`Outgoing` 代表客户端向服务端发布的信息,`All` 代表双向通信。
注意:**在 WebSocket 传输过程中,枚举值使用对应的 Int 值进行传输。**
##### ID 字符串(IdString)
一个字符串,满足正则表达式 `/a-z0-9_/` 。
一个字符串,满足正则表达式 `/^[a-z0-9_]+$/` 。
例如,以下每一行都是合法的 ID 字符串:
@@ -114,10 +88,13 @@ Hello, world!
##### 频道 ID (ChannelId)
##### 命名空间名 (NamespacedName)
```
<IdString:Namespace>:<IdString:ChannelName>
```protobuf
message NamespacedName {
string namespace = 1;
string name = 2;
}
```
Namespace 可以是注册这一频道的项目名称,尽量避免冲突。
@@ -130,54 +107,52 @@ ChannelName 是自定义的频道名。
### 协议操作
#### 认证客户端
#### 认证客户端并注册频道
为了防止未授权的传输,水晶互联需要一个 SecretKey 来信任客户端,它由服务端配置文件指定。(这个设计没有任何安全性的保证,请保证不在公网中使用它。)
为了防止未授权的传输,水晶互联需要一个 Secret Key 来信任客户端,它由服务端配置文件指定。(这个设计没有任何安全性的保证,请保证不在公网中使用它。)
请求:
为了降低处理压力,客户端需要在启动时向服务端注册频道及所需的消息方向。否则将无法发送或接收消息。
```
> Authenticate
> <string:SecretKey>
> <string:ClientName>
```
```protobuf
message Authenticate {
NamespacedName client_id = 1;
string client_name = 2;
string secret_key = 3;
repeated Channel channels = 4;
}
#### 注册频道
为了降低处理压力,客户端需要在启动时向服务端注册频道及消息方向。否则将无法发送或接收消息。
请求:
```
> Register
> <ChannelId:ChannelId>
> <int32:MessageDirection>
message Channel {
NamespacedName id = 1;
MessageDirection direction = 2;
}
```
#### 发布消息
请求:
Payload 是一个字符串,会被原样发给所有注册了该频道 `Incoming` 方向消息的客户端。
```
> Publish
> <ChannelId:ChannelId>
> <byte[]:Payload>
```protobuf
message Publish {
NamespacedName channel = 1;
string payload = 2;
}
```
Payload 是一个字节型数组,会被原样发给其他的客户端(不会发回给自身)。
当服务端收到发布消息的请求时,所有注册了该频道的客户端都会收到如下信息:
```
< Forward
< <string:PublisherName>
< <ChannelId:ChannelId>
< <byte[]:Payload>
#### 接收消息
当服务端收到发布消息的请求时,所有注册了该频道的客户端都会收到转发的信息。
```protobuf
message Forward {
string publisher = 1;
NamespacedName channel = 2;
string payload = 3;
}
```
PublisherName 是先前注册这一频道的客户端。
`publisher` 是注册客户端时的 `client_name` 。