Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protobuf Support for .NET Socket #47

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b81e736
download proto files and generate them
lugehorsam Dec 10, 2020
3eb3a28
make generated protobuf classes internal to .dll
lugehorsam Dec 10, 2020
d5c731b
move protobuf source to src directory
lugehorsam Dec 10, 2020
842b4c4
add protobuf csproj and move internal socket classes to own folder
lugehorsam Dec 11, 2020
c3c5056
replicate api messages that are used in socket under internal namespace
lugehorsam Dec 11, 2020
84037fc
restore protobuf generation
lugehorsam Dec 15, 2020
75b947b
split some channel types into SocketInternal
lugehorsam Dec 15, 2020
f467977
create web socket test data and move awaitedsockettasktest
lugehorsam Dec 15, 2020
ac37f4b
remove unsed socket from grouptest
lugehorsam Dec 15, 2020
811eda9
convert websocketchanneltest to theory
lugehorsam Dec 15, 2020
34f280a
convert websocketmatchtest to use theory
lugehorsam Dec 15, 2020
c61b281
convert websocket matchmaker test
lugehorsam Dec 15, 2020
4bfbeac
convert notification test
lugehorsam Dec 15, 2020
7876c39
convert websocket rpc test
lugehorsam Dec 15, 2020
0c813fa
convert websockettest
lugehorsam Dec 15, 2020
6cdf696
conert websocketuserstatustest
lugehorsam Dec 15, 2020
6465c34
move socket adapter to socket internal
lugehorsam Dec 15, 2020
5fd6a47
refactor websocketadapter to take an envelope rather than an array of…
lugehorsam Dec 15, 2020
3094449
websockettestdata now returns web and protobuf adapters
lugehorsam Dec 15, 2020
647853a
Remove protobuf generation and instead depend on protobuf-net
lugehorsam Dec 15, 2020
74ddad7
implement DeserializeEnvelope in SocketAdapter
lugehorsam Dec 15, 2020
3aa54eb
reorder datamember indices
lugehorsam Dec 16, 2020
a1efe39
add grpc well known types
lugehorsam Dec 18, 2020
ddbdfc5
apply well known types to socket models
lugehorsam Dec 18, 2020
49e7bfa
add ternary check to socket model getters based on parse type
lugehorsam Dec 18, 2020
fa61b87
remove stray logs and newlines
lugehorsam Dec 18, 2020
316c9fd
require name for datamember fields in tinyjson
lugehorsam Dec 19, 2020
2bf194b
update changelog
lugehorsam Dec 19, 2020
105baa8
restore jsonwriter datamemberattribute check
lugehorsam Dec 19, 2020
248491b
Allow json parsing of private members with DataMember attribute
lugehorsam Feb 5, 2021
747d5b9
restore protobuf param to tests
lugehorsam Feb 5, 2021
b6e0916
null check json parse get method checks
lugehorsam Feb 9, 2021
6dd87ef
force hard requirement to use DataMember attribute rather than parsin…
lugehorsam Feb 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed
- TinyJson DataMember attributes now require a `Name` field to be passed in order for them to be parsed.
- TinyJson public fields and properties will no longer automatically get parsed without a DataMember attribute.

### Added
- Added Protobuf support for WebSockets via the `Nakama.Protobuf` namespace.

## [2.7.0] - 2020-10-19
### Changed
- Upgrade code generator to new Swagger format.
Expand Down
15 changes: 15 additions & 0 deletions Nakama.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4CBC1D9A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama.Tests", "tests\Nakama.Tests\Nakama.Tests.csproj", "{AFD0AF63-EA45-49A6-8889-3E65A48F0521}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama.Protobuf", "src\Nakama.Protobuf\Nakama.Protobuf.csproj", "{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -48,9 +50,22 @@ Global
{AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x64.Build.0 = Release|Any CPU
{AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x86.ActiveCfg = Release|Any CPU
{AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x86.Build.0 = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x64.ActiveCfg = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x64.Build.0 = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x86.ActiveCfg = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x86.Build.0 = Debug|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|Any CPU.Build.0 = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x64.ActiveCfg = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x64.Build.0 = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x86.ActiveCfg = Release|Any CPU
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{10E81453-27EC-4D8E-82A7-DEDEFABBD648} = {651F309B-3615-44A7-A5B4-832B4ACE2258}
{AFD0AF63-EA45-49A6-8889-3E65A48F0521} = {4CBC1D9A-0727-4D31-A3BC-484EF98E7617}
{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D} = {651F309B-3615-44A7-A5B4-832B4ACE2258}
EndGlobalSection
EndGlobal
22 changes: 22 additions & 0 deletions src/Nakama.Protobuf/Nakama.Protobuf.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" Condition="'$(OS)'!='Windows_NT'" PrivateAssets="All" />
<PackageReference Include="protobuf-net" Version="3.0.73" />
<ProjectReference Include="..\Nakama\Nakama.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Net.Http" Condition="'$(TargetFramework)' == 'net46'" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Label="NuGet package definition" Condition=" '$(Configuration)' == 'Release' ">
<Authors>Nakama Authors &amp; contributors</Authors>
<Company>Heroic Labs</Company>
<Description>Nakama is an open-source server designed to power modern games and apps. This package adds Protobuf support to Websocket messages sent betweens the dotnet-client and the Nakama server.</Description>
<PackageId>NakamaClientProtobuf</PackageId>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>clientsdk;nakama;gameserver;backend;restapi</PackageTags>
<RepositoryUrl>https://github.com/heroiclabs/nakama-dotnet</RepositoryUrl>
</PropertyGroup>
</Project>
263 changes: 263 additions & 0 deletions src/Nakama.Protobuf/ProtobufAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* Copyright 2020 The Nakama Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Nakama.SocketInternal;
using Nakama.Ninja.WebSockets;
using ProtoBuf;
using System.IO;

[module: CompatibilityLevel(CompatibilityLevel.Level300)]

namespace Nakama.Protobuf
{
/// <summary>
/// A Protobuf adapter which uses the WebSocket protocol with Nakama server.
/// </summary>
public class ProtobufAdapter : ISocketAdapter
{
private const int KeepAliveIntervalSec = 15;
private const int MaxMessageSize = 1024 * 256;
private const int SendTimeoutSec = 10;

/// <inheritdoc cref="ISocketAdapter.Connected"/>
public event Action Connected;

/// <inheritdoc cref="ISocketAdapter.Closed"/>
public event Action Closed;

/// <inheritdoc cref="ISocketAdapter.ReceivedError"/>
public event Action<Exception> ReceivedError;

/// <inheritdoc cref="ISocketAdapter.Received"/>
public event Action<ArraySegment<byte>> Received;

/// <inheritdoc cref="ISocketAdapter.Format"/>
public string Format
{
get
{
return "protobuf";
}
}

/// <summary>
/// If the WebSocket is connected.
/// </summary>
public bool IsConnected { get; private set; }

/// <summary>
/// If the WebSocket is connecting.
/// </summary>
public bool IsConnecting { get; private set; }

private readonly WebSocketClientOptions _options;
private readonly TimeSpan _sendTimeoutSec;
private CancellationTokenSource _cancellationSource;
private WebSocket _webSocket;
private Uri _uri;

public ProtobufAdapter(int keepAliveIntervalSec = KeepAliveIntervalSec, int sendTimeoutSec = SendTimeoutSec) :
this(new WebSocketClientOptions
{
IncludeExceptionInCloseResponse = true,
KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalSec),
NoDelay = true
}, sendTimeoutSec) {}

public ProtobufAdapter(WebSocketClientOptions options, int sendTimeoutSec)
{
_options = options;
_sendTimeoutSec = TimeSpan.FromSeconds(sendTimeoutSec);
}

/// <inheritdoc cref="ISocketAdapter.Close"/>
public void Close()
{
_cancellationSource?.Cancel();

if (_webSocket == null) return;
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
_webSocket = null;
IsConnecting = false;
IsConnected = false;
}

/// <inheritdoc cref="ISocketAdapter.Connect"/>
public async void Connect(Uri uri, int timeout)
{
if (_webSocket != null)
{
ReceivedError?.Invoke(new SocketException((int) SocketError.IsConnected));
return;
}

_cancellationSource = new CancellationTokenSource();
_uri = uri;
IsConnecting = true;

var clientFactory = new WebSocketClientFactory();
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
var lcts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationSource.Token, cts.Token);
using (_webSocket = await clientFactory.ConnectAsync(_uri, _options, lcts.Token))
{
IsConnected = true;
IsConnecting = false;
Connected?.Invoke();

await ReceiveLoop(_webSocket, _cancellationSource.Token);
}
}
catch (TaskCanceledException)
{
// No error, the socket got closed via the cancellation signal.
}
catch (ObjectDisposedException)
{
// No error, the socket got closed.
}
catch (Exception e)
{
ReceivedError?.Invoke(e);
}
finally
{
Close();
Closed?.Invoke();
}
}

public WebSocketMessageEnvelope DeserializeEnvelope(ArraySegment<byte> buffer)
{
WebSocketMessageEnvelope envelope = null;

try
{
envelope = Serializer.Deserialize<WebSocketMessageEnvelope>(buffer.AsMemory<byte>());
}
catch (Exception e)
{
ReceivedError?.Invoke(new FormatException("Could not deserialize protocol buffer.", e));
}

return envelope;
}

public void Dispose()
{
_webSocket?.Dispose();
}

/// <inheritdoc cref="ISocketAdapter.Send"/>
public async void Send(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken,
bool reliable = true)
{

if (_webSocket == null)
{
ReceivedError?.Invoke(new SocketException((int) SocketError.NotConnected));
return;
}

try
{
var stream = new MemoryStream();
Serializer.Serialize(stream, envelope);

var asByteArray = stream.ToArray();

var sendTask = _webSocket.SendAsync(new ArraySegment<byte>(asByteArray), WebSocketMessageType.Binary, true, cancellationToken);

await Task.WhenAny(sendTask, Task.Delay(_sendTimeoutSec, cancellationToken));
}
catch (Exception e)
{
Close();
ReceivedError?.Invoke(e);
}
}

public override string ToString()
{
return
$"WebSocketAdapter(IsConnected={IsConnected}, IsConnecting={IsConnecting}, MaxMessageSize={MaxMessageSize}, Uri='{_uri}')";
}

private async Task ReceiveLoop(WebSocket webSocket, CancellationToken cancellationToken)
{
var buffer = new byte[MaxMessageSize];
while (true)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken)
.ConfigureAwait(false);
if (result == null)
{
break;
}

if (result.MessageType == WebSocketMessageType.Close)
{
break;
}

var data = await ReadFrames(result, webSocket, buffer);

if (data.Count == 0)
{
break;
}

try
{
Received?.Invoke(data);
}
catch (Exception e)
{
ReceivedError?.Invoke(e);
}
}
}

private async Task<ArraySegment<byte>> ReadFrames(WebSocketReceiveResult result, WebSocket webSocket,
byte[] buffer)
{
var count = result.Count;
while (!result.EndOfMessage)
{
if (count >= MaxMessageSize)
{
var closeMessage = $"Maximum message size {MaxMessageSize} bytes reached.";
await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage,
CancellationToken.None);
ReceivedError?.Invoke(new WebSocketException(WebSocketError.HeaderError));
return new ArraySegment<byte>();
}

result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, count, MaxMessageSize - count),
CancellationToken.None).ConfigureAwait(false);
count += result.Count;
}

return new ArraySegment<byte>(buffer, 0, count);
}
}
}
10 changes: 5 additions & 5 deletions src/Nakama/ApiClient.gen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ public interface IApiAccountFacebookInstantGame
{

/// <summary>
///
///
/// </summary>
string SignedPlayerInfo { get; }

Expand Down Expand Up @@ -3182,23 +3182,23 @@ public override string ToString()
}

/// <summary>
///
///
/// </summary>
public interface IRpcStatus
{

/// <summary>
///
///
/// </summary>
int Code { get; }

/// <summary>
///
///
/// </summary>
IEnumerable<IProtobufAny> Details { get; }

/// <summary>
///
///
/// </summary>
string Message { get; }
}
Expand Down