📜 ⬆️ ⬇️

MMO from scratch. Using Netty and the Unreal Engine. Part 1

Hello! In a few articles I would like to share the experience of creating a similarity of MMO games using the Unreal Engine and Netty. Perhaps the architecture and my experience will come in handy for someone and help you start building your game server as opposed to an unreal dedicated server that is slightly voracious or replace frameworks for developing multiplayer games such as Photon.

In the end, we will have a client who logs in or registers in the game, can create game rooms, use chat and start games, the connection will be encrypted, clients will synchronize through the server, there will be one weapon in the game - a laser, a shot will be checked for verification server. I did not want to make beautiful graphics, there will be only the necessary minimum, further functionality is added by analogy. You can easily expand the logic on the server, for example, add random games and a balancer. For me it was important to create an MMO base and figure out what is needed to create a full-fledged mobile MMO game.

Part 1. The overall picture, the assembly of libraries, preparing the client and server for messaging
Part 2. Building game functionality + Diamond Square algorithm
')


General architecture, how it works


In the beginning I will outline, and then we will write everything step by step. The client-server communication is built on sockets, the Protobuf messaging format, each message after entering the game is encrypted using the AES algorithm using the OpenSSL library on the client and javax.crypto * on the server, the key exchange takes place using the Diffie-Hellman protocol. Netty is used as an asynchronous server, we will store the data in MySQL and use Hibernate to select it. I aimed to support the game on Android, so we will pay a little attention to porting to this platform. I called the project Spiky - prickly, and for good reason:

As a first C ++ programmer, Unreal Engine 4 is not "fun" to develop with.

If I missed something or something does not converge, feel free to refer to the source code:

Spiky source code

Ultimately, this is what we get:


Let's start with the communication between the client and the server. Both have MessageDecoder and DecryptHandler, these are entry points for messages, after reading a packet, messages are decrypted, their type is determined, and the type is sent to some handler. The exit points are MessageEncoder and EncryptHandler, client and server respectively. When we send a message to Netty, it will pass through the EncryptHandler. Here you decide whether to encrypt, and how to wrap.

Each message is wrapped in the protobraf Wrapper, the recipient checks that inside the Wrapper, to select a handler, it can be a CryptogramWrapper - encrypted bytes or open messages. The Wrapper message will look something like this (part of it):

message CryptogramWrapper { bytes registration = 1; } message Wrapper { Utility utility = 1; CryptogramWrapper cryptogramWrapper = 2; } 

All messaging is based on the principle of Decoder-Encoder, if we need to add a new team to the game, we need to update the conditions. For example, the client wants to register, the message enters the MessageEncoder, where it is encrypted, turned around and sent to the server. On the server, the message arrives on DecryptHandler, is decrypted if necessary, the type is read by the presence of fields of the message and sent for processing

 if(wrapper.hasCryptogramWrapper()) { if(wrapper.getCryptogramWrapper().hasField(registration_cw)) { byte[] cryptogram = wrapper.getCryptogramWrapper().getRegistration().toByteArray(); byte[] original = cryptography.Decrypt(cryptogram, cryptography.getSecretKey()); RegModels.Registration registration = RegModels.Registration.parseFrom(original); new Registration().saveUser(ctx, registration); } else if (wrapper.getCryptogramWrapper().hasField(login_cw)) {} } 

In order to find a field in a message using .hasField, we will need a set of descriptors (registration_cw, login_cw) we will store them separately in the Descriptors class.

So, if we need a new functionality, then we

1. Create a new type of Protobuf message, attach it to the Wrapper / CryptogramWrapper
2. Declare fields that need access in the descriptors of the client and server
3. Create a logic class in which, after determining the type, send the message
4. Add a condition defining a new type in the Decode-Encoder client and server
5. We process

This is a key point that will have to be repeated many times.

In this project, I used the TCP protocol, of course it is better to write my add-on over UDP, which I tried to do at the beginning, but all I had was like TCP. The only drawback of which, in my situation, is the inability to disable packet acknowledgment, TCP is waiting for confirmation before continuing to send, it creates delays, and it will be difficult to get ping less than 100, if the packet is lost during transmission over the network, the game stops and waits until the packet is delivered again. Unfortunately, it is impossible to change such TCP behavior, and it is not necessary, since the meaning of TCP lies in it. The choice of the type of sockets depends entirely on the genre of the game, in games of the genre of action, it’s important not what happened a second ago, but the most relevant state of the game world is important. We need to get the data from client to server as quickly as possible, and we don’t want to wait for the data to be sent again. This is why you should not use TCP for multiplayer games.

But if we want to make a reliable udp, difficulties await us, we need to implement ordering, the ability to turn off the delivery confirmation, control channel congestion, send large messages, more than 1,400 bytes. Action games should use UDP for those who want to read more about this I advise you to start with these articles and books:

Network programming for game developers. Part 1: UDP vs. Tcp
Implementing a Reliable Udp Protocol for .Net
Joshua Glazer - Multiplayer Games. Chapter 7 delay, fluctuation and reliability.

I needed a reliable, serial connection to transfer commands, encrypted messages and files (captcha). TCP gives me such opportunities out of the box. To transfer game data that is frequently updated and not very important, such as moving players, UDP is the best option, I added the ability to send UDP messages for completeness and to begin where to start, but in this project all communication will take place via TCP. Perhaps you should use TCP and UDP together? However, then the number of lost UDP packets increases, as TCP takes precedence. UDP remained in the area of ​​further improvements. In this article, I follow the principle of "Done in better when pefect"



At the core of the server is Netty, it takes on work with sockets, implementing a convenient architecture. You can connect multiple handlers for incoming data. In the first handler, we deserialize the incoming message using ProtobufDecoder, and then process the game data directly. You can flexibly manage the settings of the library itself, allocate the required number of threads or memory to it. Using Netty, you can quickly and easily write any client-server application that will be easily expanded and scaled. If there is not enough one thread to process clients, you just need to pass the required number of threads to the EventLoopGroup constructor. If at some stage of development of the project additional data processing is required, no need to rewrite the code, it is enough to add a new handler to ChannelPipeline, which greatly simplifies application support.

The general architecture when using Netty with us looks like this:

 public class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); /*  */ //pipeline.addLast(new LoggingHandler(LogLevel.INFO)); /*   */ // Decoders protobuf pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufDecoder(MessageModels.Wrapper.getDefaultInstance())); /*   */ // Encoder protobuf pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder()); /*          30  */ pipeline.addLast(new IdleStateHandler(30, 0, 0)); /*    */ pipeline.addLast(new EncryptHandler()); /*    */ pipeline.addLast(new DecryptHandler()); } } 

The advantage of this approach is that the server and the handlers can be distributed to different machines by getting a cluster for calculating game data, we get a fairly flexible structure. While the load is small, you can keep everything on one server. With increasing load, logic can be separated into a separate machine.

To check hits, I created a special Unreal Engine client, whose task is to take shot parameters, to place an object in the world, based on where it was at the moment of the shot, to simulate a shot by returning to the main server information about the hit, the name of the overlap object, a bone if present, or that missed.

Let's start from scratch


I tried to write in detail, but I carried a lot under the spoiler.

Create an empty project with a code called it Spiky. First of all we will delete the GameMode created by default (this is the class that defines the rules of the current game, can be overridden for each specific level, which we will use later, there is only one GameMode instance) - we will delete the Spiky_ClientGameModeBase automatically created. Next, open Spiky_Client.Build.cs, this is part of the Unreal Build System in which we connect various modules, third-party libraries and also configure various assembly variables, by default, starting with version 4.16, the SharedPCH mode (Sharing precompiled headers) is used, as well as Include- What-You-Use (IWYU), allowing you not to include heavy headers Engine.h. In previous versions of the Unreal Engine, most of the functionality of the engine was included through files with a module header, such as Engine.h and UnrealEd.h, and the compilation time depended on how quickly these files could be compiled via Precompiled Header (PCH). As the engine grew, it became a bottleneck.

IWYU Reference Guide

In Spiky_Client.Build.cs we see

PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

It works well on fast machines with ssd (to work with unreal - you must have a different headache, I also advise you to disable IntelliSense and use VisualAssist instead) but not ssd machines, for convenience and speed I would advise to switch to another mode that writes less to disk, which we will do by turning on PCHUsageMode.Default, thereby turning off the generation of Precompiled Header.

All possible PCHUsage values:

PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PCHUsage = PCHUsageMode.UseSharedPCHs;
PCHUsage = PCHUsageMode.NoSharedPCHs;
PCHUsage = PCHUsageMode.Default;

Now our file contains the following:

Spiky_Client.Build.cs
 using UnrealBuildTool; public class Spiky_Client : ModuleRules { public Spiky_Client(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.Default; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" }); PrivateDependencyModuleNames.AddRange(new string[] { }); } } 


What is the difference between PublicDependencyModuleNames and PrivateDependencyModuleNames? In Unreal projects, it is advisable to use Source / Public and Source / Private for header interfaces and source code, then PublicDependencyModuleNames will be available in Public and Private folders, but PrivateDependencyModuleNames will only be available in the Private folder. You can change various other build parameters by overriding BuildConfiguration.xml, all parameters can be found here:

Configuring Unreal Build System

Minor editor settings for convenience
We include small icons, frame rate display and memory consumption:
General-> Miscellaneous-> Performance-> Show Frame Rate and Memory
General-> User Interface-> Use Small Tool Bar Icons

Moving on, we will add outside the game GameMode for login, registration and main menu screens.

Adding SpikyGameMode
File-> New C ++ Class-> Game Mode Base is called SpikyGameMode, select public and create the GameModes folder. The final path should look like this:

Spiky / Spiky_Client / Source / Spiky_Client / Public / GameModes

The task of SpikyGameMode will be to create a valid link to the world. The world is a top-level object representing the map in which the actors and components will exist and be visualized. Later we will create a DiffrentMix class inherited from the UObject in which we will manage the interface, to create widgets we need a link to the current world, which cannot be obtained from the UObject classes, therefore we will create a GameMode through which we initialize DiffrentMix and pass it a link to the world.

A single word about the interface, this refers to the client's architecture. We have access to all widgets, via DifferentMix singleton, all widgets are placed inside the WidgetsContainer, which we need to place widgets in layers whose depth you can set, the WidgetsContainer root is the Canvas unfortunately I have not found a way to change the order of the widgets using Viewport. This is convenient when you need for example, that the chat is guaranteed to be on top of everything else. To do this, we set its widget the maximum depth (priority) in our program mainMenuChatSlot-> SetZOrder (10), however, the priority can be any.

Add the DifferentMix class, the parent of the UObject base class for all objects, put it in the new Utils folder. Here we will store links to widgets, rare functions for which to create your classes would be superfluous, this is the singleton through which we will manage the user interface.

Add the SpikyGameInstance derived from the UGameInstance class, a generic UObject that can store any data that is transferred between levels. It is created when creating a game, and exists until the game is closed. We will use it to store unique game data, such as player login, game session id, encryption key, also here we start and stop listening sockets flows, and through it we will get access to the DifferentMix functions.

Location of new classes
Spiky_Client / Source / Spiky_Client / Private / GameModes / SpikyGameMode.h
Spiky_Client / Source / Spiky_Client / Private / Utils / DifferentMix.h
Spiky_Client / Source / Spiky_Client / Private / SpikyGameInstance.h

Spiky_Client / Source / Spiky_Client / Public / GameModes / SpikyGameMode.cpp
Spiky_Client / Source / Spiky_Client / Public / Utils / DifferentMix.cpp
Spiky_Client / Source / Spiky_Client / Public / SpikyGameInstance.cpp

Zemette, maybe from the editor, the game after adding new classes will refuse to assemble, this is due to the fact that we switched to a mode that requires the presence of #include "Spiky_Client.h" in all the source files, add it manually and collect it through the studio, I don’t add The new code through the editor, I copy, edit manually and click on the Spiky_Client.uproject pkm Generate Visual Studio project files.

Let's go back to the editor, create the Maps folder and save the standard map in it, let's call it MainMap later we will place the rotating fur on it (or the choice of the game character as in many MMOs).

Open Project Settings → Maps & Modes and set up the created GameMode / GameInstance / Map as in the picture:



Network part


With all the preparation, let's start to write the project from the network part, we implement the connection to the server, the restoration of the connection if it is lost, the listeners of the incoming messages and the stream checking the server availability. The main object on the client through which we work with the network, we serve sockets, the SocketObject will be derived from UObject, we will add it to the Net folder. Since we use the network, we need to add the modules “Networking”, “Sockets” to Spiky_Client.Build.cs

 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Networking", "Sockets" }); 

Add a destructor to the SocketObject header, a number of self-describing static functions and the necessary SocketSubsystem and Networking inclusions.

SocketObject.h
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "CoreMinimal.h" #include "UObject/NoExportTypes.h" #include "Networking.h" #include "SocketSubsystem.h" #include "SocketObject.generated.h" /** *   ,  ,   -  . */ UCLASS() class SPIKY_CLIENT_API USocketObject : public UObject { GENERATED_BODY() ~USocketObject(); public: // tcp static FSocket* tcp_socket; // tcp   static TSharedPtr<FInternetAddr> tcp_address; //   static bool bIsConnection; //     static void Reconnect(); //     static bool Alive(); // udp static FSocket* udp_socket; // udp   static TSharedPtr<FInternetAddr> udp_address; //       UDP  ,  unreal  FUdpSocketReceiver,       - static FUdpSocketReceiver* UDPReceiver; static void Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt); static void RunUdpSocketReceiver(); static int32 tcp_local_port; static int32 udp_local_port; //     ,  GameInstance static void InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port); }; 


Now in the source code, let's start by creating sockets in InitSocket, select the buffer, assign local ports, I know two ways to create sockets, one of them by the builder:

 tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); 

Or through the ISocketSubsystem:

 tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); 

These are the basic abstractions of various socket-specific platform-specific interfaces. Since we set the address somewhere in the configuration file or the code with the string, we should bring it into the necessary form, for this we use FIPv4Address :: Parse, then we connect and call bIsConnection = Alive (); The method sends empty messages to the server if it means there is a connection. Finally, we will create a UDP socket using FUdpSocketBuilder, the final view of InitSocket should be like this:

USocketObject :: InitSocket
 void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; // tcp /*  FTcpSocketBuilder tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); } 


Close the sockets and delete them in the destructor.

 if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; } 


The current state of the SocketObject is:

SocketObject.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SocketObject.h" FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; /* tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); } void USocketObject::RunUdpSocketReceiver() { } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; } 


Let's do the method of sending Alive messages, message format and server. At the core of the server, I used the Netty asynchronous framework written in java. The main advantage of which is simple reading and writing to sockets. Netty supports non-blocking asynchronous I / O, it scales easily, which is important for an online game if your system needs to be able to handle many thousands of connections at the same time. And what is also important - Netty is easy to use.

Create a server, use IntelliJ IDEA, create a Maven project:

 <groupId>com.spiky.server</groupId> <artifactId>Spiky server</artifactId> 

Add the dependencies we need, Netty

 <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.8.Final</version> </dependency> </dependencies> 

Now let's deal with the message serialization format. We use Protobuf. The size of the message is extremely small, and judging by the graphs, it exceeds JSON in all.

Size comparison


Performance comparison


* taken from here, good stuff, with examples of protobuff and different metrics

In order to determine the structure of the data being serialized, it is necessary to create a .proto file with the source code of this structure, for example:

 syntax = "proto3"; message Player { string player_name = 1; string team = 2; int32 health = 3; PlayerPosition playerPosition = 4; } message PlayerPosition {} 

After that, this data structure is compiled into classes by a special compiler, protoc, the compilation command looks like this:

./protoc --cpp_out=. --java_out=. GameModels.proto

Protobuff has good documentation which will help better understand the meaning of each field.
Protobaf is implemented for Java and C ++ used by our project. Add another dependency:

 <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.0.0-beta-4</version> </dependency> 

Now you need to add support for the protobuff in Unreal, this is not so easy, first we get a branch with github . Now you need to properly assemble, instructions on how to build through Visual Studio can be found here . To set the link type for the Filter through to Configuration Properties> C / C ++> Code Generation> Runtime Library, from the drop down list select Multi-threaded DLL (/ MD) ” see Linking Static Libraries Using the Build System and compiling libprotobuf.lib . After we add to the project, create the folder ThirdParty / Protobuf in the root in which you want to create Libs and Includes. Put /protobuf-3.0.0-beta-4/cmake/build/solution/Release/libprotobuf.lib in Libs. Put / proto-install / include / google in Includes.

Since my goal was to support mobile devices, we will need to build a library for Android using Android NDK, the list of files for compilation can be taken here , at the beginning of the lite, then the rest. The process itself looks like this, install the Android NDK, create the jni folder, put two Android.mk and Application.mk files in them, create src into which to copy src from protobuf-3.0.0-beta-4 / src and use ndk-build . Application.mk and Android.mk files are ready:

Application.mk
APP_OPTIM := release
APP_ABI := armeabi-v7a #x86 x86_64
APP_STL := gnustl_static

NDK_TOOLCHAIN_VERSION := clang

APP_CPPFLAGS += -D GOOGLE_PROTOBUF_NO_RTTI=1
APP_CPPFLAGS += -D __ANDROID__=1
APP_CPPFLAGS += -D HAVE_PTHREAD=1


Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := libprotobuf

LOCAL_SRC_FILES :=\
src/google/protobuf/arena.cc \
src/google/protobuf/arenastring.cc \
src/google/protobuf/extension_set.cc \
src/google/protobuf/generated_message_util.cc \
src/google/protobuf/io/coded_stream.cc \
src/google/protobuf/io/zero_copy_stream.cc \
src/google/protobuf/io/zero_copy_stream_impl_lite.cc \
src/google/protobuf/message_lite.cc \
src/google/protobuf/repeated_field.cc \
src/google/protobuf/stubs/bytestream.cc \
src/google/protobuf/stubs/common.cc \
src/google/protobuf/stubs/int128.cc \
src/google/protobuf/stubs/once.cc \
src/google/protobuf/stubs/status.cc \
src/google/protobuf/stubs/statusor.cc \
src/google/protobuf/stubs/stringpiece.cc \
src/google/protobuf/stubs/stringprintf.cc \
src/google/protobuf/stubs/structurally_valid.cc \
src/google/protobuf/stubs/strutil.cc \
src/google/protobuf/stubs/time.cc \
src/google/protobuf/wire_format_lite.cc \
src/google/protobuf/any.cc \
src/google/protobuf/any.pb.cc \
src/google/protobuf/api.pb.cc \
src/google/protobuf/compiler/importer.cc \
src/google/protobuf/compiler/parser.cc \
src/google/protobuf/descriptor.cc \
src/google/protobuf/descriptor.pb.cc \
src/google/protobuf/descriptor_database.cc \
src/google/protobuf/duration.pb.cc \
src/google/protobuf/dynamic_message.cc \
src/google/protobuf/empty.pb.cc \
src/google/protobuf/extension_set_heavy.cc \
src/google/protobuf/field_mask.pb.cc \
src/google/protobuf/generated_message_reflection.cc \
src/google/protobuf/io/gzip_stream.cc \
src/google/protobuf/io/printer.cc \
src/google/protobuf/io/strtod.cc \
src/google/protobuf/io/tokenizer.cc \
src/google/protobuf/io/zero_copy_stream_impl.cc \
src/google/protobuf/map_field.cc \
src/google/protobuf/message.cc \
src/google/protobuf/reflection_ops.cc \
src/google/protobuf/service.cc \
src/google/protobuf/source_context.pb.cc \
src/google/protobuf/struct.pb.cc \
src/google/protobuf/stubs/mathlimits.cc \
src/google/protobuf/stubs/substitute.cc \
src/google/protobuf/text_format.cc \
src/google/protobuf/timestamp.pb.cc \
src/google/protobuf/type.pb.cc \
src/google/protobuf/unknown_field_set.cc \
src/google/protobuf/util/field_comparator.cc \
src/google/protobuf/util/field_mask_util.cc \
src/google/protobuf/util/internal/datapiece.cc \
src/google/protobuf/util/internal/default_value_objectwriter.cc \
src/google/protobuf/util/internal/error_listener.cc \
src/google/protobuf/util/internal/field_mask_utility.cc \
src/google/protobuf/util/internal/json_escaping.cc \
src/google/protobuf/util/internal/json_objectwriter.cc \
src/google/protobuf/util/internal/json_stream_parser.cc \
src/google/protobuf/util/internal/object_writer.cc \
src/google/protobuf/util/internal/proto_writer.cc \
src/google/protobuf/util/internal/protostream_objectsource.cc \
src/google/protobuf/util/internal/protostream_objectwriter.cc \
src/google/protobuf/util/internal/type_info.cc \
src/google/protobuf/util/internal/type_info_test_helper.cc \
src/google/protobuf/util/internal/utility.cc \
src/google/protobuf/util/json_util.cc \
src/google/protobuf/util/message_differencer.cc \
src/google/protobuf/util/time_util.cc \
src/google/protobuf/util/type_resolver_util.cc \
src/google/protobuf/wire_format.cc \
src/google/protobuf/wrappers.pb.cc

LOCAL_CPPFLAGS := -std=c++11
LOCAL_LDLIBS := -llog

ifeq ($(TARGET_ARCH),x86)
LOCAL_SRC_FILES := $(LOCAL_SRC_FILES) \
src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
endif

ifeq ($(TARGET_ARCH),x86_64)
LOCAL_SRC_FILES := $(LOCAL_SRC_FILES) \
src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
endif

LOCAL_C_INCLUDES = $(LOCAL_PATH)/src

include $(BUILD_SHARED_LIBRARY)


If successful, we will get a "bipod" / android / proto / libs / armeabi-v7a - libprotobuf.so. Copy it into the project / Spiky / Spiky_Client / Source / Spiky_Client / armv7.

Possible difficulties and mistakes
:

ThirdParty/Protobuf/Includes\google/protobuf/arena.h(635,25) : error: cannot use typeid with -fno-rtti

arena.h

#define GOOGLE_PROTOBUF_NO_RTTI

, — error: "error C3861: 'check': identifier not found , check (AssertionMacros.h), check (type_traits.h), check , check check_UnrealFix, , #undef check. unreal answers — Error C3861 (identifier not found) when including protocol buffers .

 template<typename B, typename D> struct is_base_of { typedef char (&yes)[1]; typedef char (&no)[2]; // BEGIN GOOGLE LOCAL MODIFICATION -- check is a #define on Mac. #undef check // END GOOGLE LOCAL MODIFICATION static yes check(const B*); static no check(const void*); enum { value = sizeof(check(static_cast<const D*>(NULL))) == sizeof(yes), }; }; 

type_traits.h :

 template<typename B, typename D> struct is_base_of { typedef char (&yes)[1]; typedef char (&no)[2]; // BEGIN GOOGLE LOCAL MODIFICATION -- check is a #define on Mac. //#undef check // END GOOGLE LOCAL MODIFICATION static yes check_UnrealFix(const B*); static no check_UnrealFix(const void*); enum { value = sizeof(check_UnrealFix(static_cast<const D*>(NULL))) == sizeof(yes), }; }; 


, OpenSSL . Android NDK ++ 11, chrono , , .

I advise you to test the functionality of external libraries, before adding to the project, separately, outside of Unreal, it is much faster.

While we postpone the protobuf connection, let's compile OpenSSL in order not to return to this topic and not to repeat it. I am using OpenSSL-1.0.2k. To build a library, use this guide ( Building the 64-bit static libraries with debug symbols ). A couple of tips if you have any difficulties:

  1. Find in the folder with the studio ml64.exe and copy to the folder with OpenSSL, do not use NASM - this is only for x32
  2. Use clean sources (no build attempts)
  3. openssl fatal error LNK1112: module machine type 'x64' conflicts with target machine type 'X86'- open Developer Command Prompt for VS2015, go to E: \ Program Files (x86) \ Microsoft Visual Studio 14.0 \ VC and run vcvarsall.bat x64 ( source )
  4. Name Conflict with Unreal comment out the 172 line: openssl/ossl_typ.h(172): error C2365: 'UI': redefinition; previous definition was 'namespace'

As for the compilation for android, the easiest way to do this is from under Ubuntu, using the scripts for armv7 and x86 that you can find in the source code of the project.

OpenSSL Android
How to add a shared library (.so) in android project

Solving possible problems
, , :

E/AndroidRuntime( 1574): java.lang.UnsatisfiedLinkError: dlopen failed: could not load library "libcrypto.so.1.0.0" needed by "libUE4.so"; caused by library "libcrypto.so.1.0.0" not found
, Ubuntu :
rpl -R -e .so.1.0.0 "_1_0_0.so" /path/to/libcrypto.so

Copy the bipod to Source / Spiky_Client / armv7, libraries, titles in ThirdParty / OpenSSL and compile.

We connect libraries in Spiky_Client.Build.cs. For convenience, we add two ModulePath and ThirdPartyPath functions, the first one returns the path to the project, the second to the folder with the link libraries.

 public class Spiky_Client : ModuleRules { private string ModulePath { get { return ModuleDirectory; } } private string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, "../../ThirdParty/")); } } ... } 

Specific to each platform, we add a library and headers. When compiling, the library required by the platform is selected:

Spiky_Client.Build.cs
 // Copyright (c) 2017, Vadim Petrov - MIT License using UnrealBuildTool; using System.IO; using System; public class Spiky_Client : ModuleRules { private string ModulePath { get { return ModuleDirectory; } } private string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, "../../ThirdParty/")); } } public Spiky_Client(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.Default; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Networking", "Sockets" }); PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" }); string IncludesPath = Path.Combine(ThirdPartyPath, "Protobuf", "Includes"); PublicIncludePaths.Add(IncludesPath); IncludesPath = Path.Combine(ThirdPartyPath, "OpenSSL", "Includes"); PublicIncludePaths.Add(IncludesPath); if ((Target.Platform == UnrealTargetPlatform.Win64)) { string LibrariesPath = Path.Combine(ThirdPartyPath, "Protobuf", "Libs"); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "libprotobuf.lib")); LibrariesPath = Path.Combine(ThirdPartyPath, "OpenSSL", "Libs"); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "libeay32.lib")); } if (Target.Platform == UnrealTargetPlatform.Android) { string BuildPath = Utils.MakePathRelativeTo(ModuleDirectory, BuildConfiguration.RelativeEnginePath); AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(BuildPath, "APL.xml"))); PublicAdditionalLibraries.Add(BuildPath + "/armv7/libprotobuf.so"); PublicAdditionalLibraries.Add(BuildPath + "/armv7/libcrypto_1_0_0.so"); } } } 


To add bipods to the assembly, you need to create the APL.xml (AndroidPluginLanguage) file in the source folder, which describes where the libraries should be copied from and where, and under what platform armv7, x86. Examples and other parameters can be found here .

APL
 <?xml version="1.0" encoding="utf-8"?> <root xmlns:android="http://schemas.android.com/apk/res/android"> <resourceCopies> <isArch arch="armeabi-v7a"> <copyFile src="$S(PluginDir)/armv7/libcrypto_1_0_0.so" dst="$S(BuildDir)/libs/armeabi-v7a/libcrypto_1_0_0.so" /> <copyFile src="$S(PluginDir)/armv7/libprotobuf.so" dst="$S(BuildDir)/libs/armeabi-v7a/libprotobuf.so" /> </isArch> </resourceCopies> </root> 


You can test OpenSSL for windows and android by creating a test hud and output hash into it (missing in source code)
 // OpenSSL tests #include <openssl/evp.h> #include <sstream> #include <iomanip> void ADebugHUD::DrawHUD() { Super::DrawHUD(); FString hashTest = "Hash test (sha256): " + GetSHA256_s("test", strlen("test")); DrawText(hashTest, FColor::White, 50, 50, HUDFont); } FString ADebugHUD::GetSHA256_s(const void * data, size_t data_len) { EVP_MD_CTX mdctx; unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestInit(&mdctx, EVP_sha256()); EVP_DigestUpdate(&mdctx, data, (size_t)data_len); EVP_DigestFinal_ex(&mdctx, md_value, &md_len); EVP_MD_CTX_cleanup(&mdctx); std::stringstream s; s.fill('0'); for (size_t i = 0; i < md_len; ++i) s << std::setw(2) << std::hex << (unsigned short)md_value[i]; return s.str().c_str(); } 


When we add compiled .proto messages, anrial gives out various warnings, which can be disabled by either sorting out the engine sources, or suppressing them. To do this, create DisableWarnings.proto and ./protoc --cpp_out=. --java_out=. DisableWarnings.protothen compile the resulting header DisableWarnings.pb.h suppress warnings, we will include DisableWarnings in each proto file. There are only three lines in DisableWarnings.proto, the protobuff version, the name of the java package, and the name of the generated class.

#define PROTOBUF_INLINE_NOT_IN_HEADERS 0
#pragma warning(disable:4100)
#pragma warning(disable:4127)
#pragma warning(disable:4125)
#pragma warning(disable:4267)
#pragma warning(disable:4389)

DisableWarnings.proto
syntax = "proto3";

option java_package = "com.spiky.server.protomodels";
option java_outer_classname = "DisableWarnings";


DisableWarnings.pb.h
 // Generated by the protocol buffer compiler. DO NOT EDIT! // source: DisableWarnings.proto #define PROTOBUF_INLINE_NOT_IN_HEADERS 0 #pragma warning(disable:4100) #pragma warning(disable:4127) #pragma warning(disable:4125) #pragma warning(disable:4267) #pragma warning(disable:4389) #ifndef PROTOBUF_DisableWarnings_2eproto__INCLUDED #define PROTOBUF_DisableWarnings_2eproto__INCLUDED #include <string> #include <google/protobuf/stubs/common.h> #if GOOGLE_PROTOBUF_VERSION < 3000000 #error This file was generated by a newer version of protoc which is #error incompatible with your Protocol Buffer headers. Please update #error your headers. #endif #if 3000000 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION #error This file was generated by an older version of protoc which is #error incompatible with your Protocol Buffer headers. Please #error regenerate this file with a newer version of protoc. #endif #include <google/protobuf/arena.h> #include <google/protobuf/arenastring.h> #include <google/protobuf/generated_message_util.h> #include <google/protobuf/metadata.h> #include <google/protobuf/repeated_field.h> #include <google/protobuf/extension_set.h> // @@protoc_insertion_point(includes) // Internal implementation detail -- do not call these. void protobuf_AddDesc_DisableWarnings_2eproto(); void protobuf_AssignDesc_DisableWarnings_2eproto(); void protobuf_ShutdownFile_DisableWarnings_2eproto(); // =================================================================== // =================================================================== // =================================================================== #if !PROTOBUF_INLINE_NOT_IN_HEADERS #endif // !PROTOBUF_INLINE_NOT_IN_HEADERS // @@protoc_insertion_point(namespace_scope) // @@protoc_insertion_point(global_scope) #endif // PROTOBUF_DisableWarnings_2eproto__INCLUDED 


We put all our protobuffs in the Protobufs folder (Source / Spiky_Client / Protobufs), but it is better to configure automatic placement of the generated files by specifying full paths to --cpp_out =. --java_out =.

Let's go further, we will configure Spiky server!

We create the com.spiky.server package and add the ServerMain class, the entry point of our server, here we will store global variables, initialize and run two Netty servers for tcp and udp connections (but I remind you that the project uses only tcp). We will definitely need a configuration file where we could store the ports of the servers (the server of the logic is Netty and the checker on Unreal), as well as the ability to enable disabling of cryptography. In the Recources folder, create configuration.properties.

Let's add server initialization to ServerMain, and read the settings file:

 /*   */ private static final ResourceBundle configurationBundle = ResourceBundle.getBundle("configuration", Locale.ENGLISH); /*   */ private static final int tcpPort = Integer.valueOf(configurationBundle.getString("tcpPort")); private static final int udpPort = Integer.valueOf(configurationBundle.getString("udpPort")); private static void run_tcp() { EventLoopGroup bossGroup = new NioEventLoopGroup(); // 1 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // 2 b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 3 .childHandler(new com.spiky.server.tcp.ServerInitializer()) // 4 .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true); ChannelFuture f = b.bind(tcpPort).sync(); // 5 f.channel().closeFuture().sync(); // 6 } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } 

, udp main()
 /* * Copyright (c) 2017, Vadim Petrov - MIT License */ package com.spiky.server; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.util.Locale; import java.util.ResourceBundle; public class ServerMain { /*   */ private static final ResourceBundle configurationBundle = ResourceBundle.getBundle("configuration", Locale.ENGLISH); /*   */ private static final int tcpPort = Integer.valueOf(configurationBundle.getString("tcpPort")); private static final int udpPort = Integer.valueOf(configurationBundle.getString("udpPort")); private static void run_tcp() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new com.spiky.server.tcp.ServerInitializer()) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true); ChannelFuture f = b.bind(tcpPort).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } private static void run_udp() { final NioEventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group).channel(NioDatagramChannel.class) .handler(new com.spiky.server.udp.ServerInitializer()); bootstrap.bind(udpPort).sync(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(ServerMain::run_tcp).start(); new Thread(ServerMain::run_udp).start(); } } 


  1. NioEventLoopGroup — , -. Netty EventLoopGroup . , NioEventLoopGroup. , «», . , «», , . , EventLoopGroup
  2. ServerBootstrap — , . . , ,
  3. NioServerSocketChannel,
  4. Handler, EventLoop. , , ,
  5. ,

How Netty works, with simple examples of an echo server, with explanations can be found in the documentation . I also strongly advise you to read the book Netty in Action, it is small.

Our server is almost ready to start, we will add ServerInitializer for both protocols:

 /*  UDP  TCP*/ public class ServerInitializer extends ChannelInitializer<NioDatagramChannel> public class ServerInitializer extends ChannelInitializer<SocketChannel> 

Let's create two packages com.spiky.server.tcpand com.spiky.server.udpin each of them we will create a ServerInitializer class (with excellent NioDatagramChannel / SocketChannel) with the following contents:

 package com.spiky.server.tcp; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); } } 

Pipeline is what each message goes through, it contains a list of ChannelHandlers that process incoming and outgoing messages. For example, one of the handlers can only accept string data, another protobuff, if we call write (string), then a handler will be called for the strings, in which we decide to process the message further, send it to the other handler corresponding to the new type or send to the client. Each handler has a type that determines for which messages it is incoming or outgoing.

Add a standard debugging handler to ServerInitializer, which is very useful, you can see the size of incoming messages and in what form they are presented, also the addressee:

 ... ChannelPipeline pipeline = ch.pipeline(); /*  */ pipeline.addLast(new LoggingHandler(LogLevel.INFO)); ... 

Processing protobaf messages sent via TCP is different from those sent via UDP, Netty has prepared handlers for protobuff, but they only work for streaming connections such as TCP, when we send a message we need to know where to finish reading, so at the beginning of each message it should go length, then the body itself. Let's start with UDP, add and test the reception and sending of messages by the server and client. Add a debug handler to the ServerInitializer, then create the com.spiky.server.udp.handlers package. Add the public class ProtoDecoderHandler extends SimpleChannelInboundHandler to it. ChannelInboundHandlerAdapter, allows you to explicitly process only certain types of incoming messages. For example, ProtoDecoderHandler processes only messages of type DatagramPacket.

Throw in the PackageHandler - class logic, after decoding (and then we will have to decode and decrypt) messages come here we used protobaf format class PackageHandler the extends the public SimpleChannelInboundHandler <MessageModels.Wrapper>

MessageModels is a wrapper class of the upper level, which will contain encrypted and unencrypted data. All messages turn into it, here is its final form, some types are not yet familiar to us:

 message Wrapper { Utility utility = 1; InputChecking inputChecking = 2; Registration registration = 3; Login login = 4; CryptogramWrapper cryptogramWrapper = 5; } 

When we send a message, the receiving party reads the wrapper and looks at what fields it has. Login, registration? Or maybe the encrypted bytes of cryptogramWrapper? Thereby choosing the flow of execution.

Let's define and describe all the proto-model of the model in our project so as not to be distracted anymore.

DisableWarnings is an empty protobaf, whose only task is to disable warnings.

DisableWarnings.proto
syntax = "proto3";

option java_package = "com.spiky.server.protomodels";
option java_outer_classname = "DisableWarnings";


MessageModels - contains the main wrapper Wrapper, inside which can be unencrypted messages Utility, InputChecking, Registration, Login and encrypted CryptogramWrapper. CryptogramWrapper contains encrypted bytes, for example, after we exchanged keys and began to encrypt data, this data is assigned as one of the CryptogramWrapper fields. The recipient received, checked whether there is encrypted data, decrypted, determined the type by the field name and sent further for processing.

MessageModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "MessageModels"; import "UtilityModels.proto"; import "RegLogModels.proto"; import "DisableWarnings.proto"; message CryptogramWrapper { bytes registration = 1; bytes login = 2; bytes initialState = 3; bytes room = 4; bytes mainMenu = 5; bytes gameModels = 6; } message Wrapper { Utility utility = 1; InputChecking inputChecking = 2; Registration registration = 3; Login login = 4; CryptogramWrapper cryptogramWrapper = 5; } 


UtilityModels is the only task for this model to send alive messages.

UtilityModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "UtilityModels"; import "DisableWarnings.proto"; message Utility { bool alive = 1; } 


RegLogModels - contains the models necessary for registration and logging, as well as checking user input and receiving captcha from the server.

RegLogModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "RegistrationLoginModels"; import "DisableWarnings.proto"; import "GameRoomModels.proto"; message InputChecking { string login = 1; string mail = 2; string captcha = 3; bool getCaptcha = 4; bytes captchaData = 5; oneof v1 { bool loginCheckStatus = 6; bool mailCheckStatus = 7; bool captchaCheckStatus = 8; } } message Login { string mail = 1; string hash = 2; string publicKey = 3; oneof v1 { int32 stateCode = 4; } } message Registration { string login = 1; string hash = 2; string mail = 3; string captcha = 4; string publicKey = 5; oneof v1 { int32 stateCode = 6; } } message InitialState { string sessionId = 1; string login = 2; repeated CreateRoom createRoom = 3; } 


MainMenuModels - the data we need in the main menu, here only chat.

MainMenuModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "MainMenuModels"; import "DisableWarnings.proto"; message ChatMessage { int64 time = 1; string name = 2; string text = 3; } message Chat { int64 time = 1; string name = 2; string text = 3; oneof v1 { bool subscribe = 4; } repeated ChatMessage messages = 5; } message MainMenu { Chat chat = 1; } 


GameRoomModels - everything you need to create and update game rooms.

GameRoomModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "GameRoomModels"; import "DisableWarnings.proto"; import "MainMenuModels.proto"; message Room { CreateRoom createRoom = 1; RoomsListUpdate roomsListUpdate = 2; SubscribeRoom subscribeRoom = 3; RoomUpdate roomUpdate = 4; bool startGame = 5; string roomName = 6; } message CreateRoom { string roomName = 1; string mapName = 2; string gameTime = 3; string maxPlayers = 4; string creator = 5; } message RoomsListUpdate { bool deleteRoom = 1; bool addRoom = 2; string roomName = 3; string roomOwner = 4; } message SubscribeRoom { oneof v1 { bool subscribe = 1; } string roomName = 2; int32 stateCode = 3; RoomDescribe roomDescribe = 4; string player = 5; string team = 6; } message RoomDescribe { repeated TeamPlayer team1 = 1; repeated TeamPlayer team2 = 2; repeated TeamPlayer undistributed = 3; string roomName = 4; string mapName = 5; string gameTime = 6; string maxPlayers = 7; string creator = 8; Chat chat = 9; } message TeamPlayer { string player_name = 1; } message RoomUpdate { RoomDescribe roomDescribe = 1; string targetTeam = 2; string roomName = 3; } 


GameModels - model for the game, the position of the player, the parameters of the shot, the initial state, ping.

GameModels.proto
 syntax = "proto3"; option java_package = "com.spiky.server.protomodels"; option java_outer_classname = "GameModels"; import "DisableWarnings.proto"; message GameInitialState { bool startGame = 1; repeated Player player = 2; } message Player { string player_name = 1; string team = 2; int32 health = 3; PlayerPosition playerPosition = 4; } message PlayerPosition { Location loc = 1; Rotation rot = 2; message Location { int32 X = 1; int32 Y = 2; int32 Z = 3; } message Rotation { int32 Pitch = 1; int32 Roll = 2; int32 Yaw = 3; } string playerName = 3; int64 timeStamp = 4; } message Ping { int64 time = 1; } message Shot { Start start = 1; End end = 2; PlayerPosition playerPosition = 3; message Start { int32 X = 1; int32 Y = 2; int32 Z = 3; } message End { int32 X = 1; int32 Y = 2; int32 Z = 3; } int64 timeStamp = 4; string requestFrom = 5; string requestTo = 6; string roomOwner = 7; oneof v1 { bool result_hitState = 8; } string result_bonename = 9; } message GameData { GameInitialState gameInitialState = 1; PlayerPosition playerPosition = 2; Ping ping = 3; Shot shot = 4; } 


All models you can find in Spiky / Spiky_Protospace.

To determine the type of message and how it should be processed, we learn that in it by the presence of named fields: And in order not to clutter up the code, create separate classes with a set of descriptors, add the Descriptors class on the client and on the server to Utils.

// java
if(wrapper.getCryptogramWrapper().hasField(registration_cw)) // -
// cpp
if (wrapper->cryptogramwrapper().GetReflection()->HasField(wrapper->cryptogramwrapper(), Descriptors::registration_cw)) // -



Descriptors.java
 // Copyright (c) 2017, Vadim Petrov - MIT License package com.spiky.server.utils; import com.spiky.server.protomodels.*; /** *         * */ public class Descriptors { public static com.google.protobuf.Descriptors.FieldDescriptor registration_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("registration"); public static com.google.protobuf.Descriptors.FieldDescriptor login_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor initialState_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("initialState"); public static com.google.protobuf.Descriptors.FieldDescriptor room_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("room"); public static com.google.protobuf.Descriptors.FieldDescriptor mainMenu_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("mainMenu"); public static com.google.protobuf.Descriptors.FieldDescriptor gameModels_cw = MessageModels.CryptogramWrapper.getDefaultInstance().getDescriptorForType().findFieldByName("gameModels"); public static com.google.protobuf.Descriptors.FieldDescriptor getCaptcha_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("getCaptcha"); public static com.google.protobuf.Descriptors.FieldDescriptor login_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor mail_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("mail"); public static com.google.protobuf.Descriptors.FieldDescriptor captcha_ich = RegistrationLoginModels.InputChecking.getDefaultInstance().getDescriptorForType().findFieldByName("captcha"); public static com.google.protobuf.Descriptors.FieldDescriptor login_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("login"); public static com.google.protobuf.Descriptors.FieldDescriptor mail_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("mail"); public static com.google.protobuf.Descriptors.FieldDescriptor captcha_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("captcha"); public static com.google.protobuf.Descriptors.FieldDescriptor publicKey_reg = RegistrationLoginModels.Registration.getDefaultInstance().getDescriptorForType().findFieldByName("publicKey"); public static com.google.protobuf.Descriptors.FieldDescriptor publicKey_log = RegistrationLoginModels.Login.getDefaultInstance().getDescriptorForType().findFieldByName("publicKey"); public static com.google.protobuf.Descriptors.FieldDescriptor subscribe_chat = MainMenuModels.Chat.getDefaultInstance().getDescriptorForType().findFieldByName("subscribe"); public static com.google.protobuf.Descriptors.FieldDescriptor chat_mm = MainMenuModels.MainMenu.getDefaultInstance().getDescriptorForType().findFieldByName("chat"); public static com.google.protobuf.Descriptors.FieldDescriptor deleteRoom_room = GameRoomModels.RoomsListUpdate.getDefaultInstance().getDescriptorForType().findFieldByName("deleteRoom"); public static com.google.protobuf.Descriptors.FieldDescriptor startGame_room = GameRoomModels.Room.getDefaultInstance().getDescriptorForType().findFieldByName("startGame"); public static com.google.protobuf.Descriptors.FieldDescriptor requestTo_shot_gd = GameModels.Shot.getDefaultInstance().getDescriptorForType().findFieldByName("requestTo"); } 


Descriptors.h / Descriptors.pp
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include <google/protobuf/descriptor.h> class Descriptors { public: static const google::protobuf::FieldDescriptor* captchaDataField_ich; static const google::protobuf::FieldDescriptor* loginCheckStatus_ich; static const google::protobuf::FieldDescriptor* mailCheckStatus_ich; static const google::protobuf::FieldDescriptor* captchaCheckStatus_ich; static const google::protobuf::FieldDescriptor* publicKey_reg; static const google::protobuf::FieldDescriptor* stateCode_reg; static const google::protobuf::FieldDescriptor* publicKey_log; static const google::protobuf::FieldDescriptor* stateCode_log; static const google::protobuf::FieldDescriptor* registration_cw; static const google::protobuf::FieldDescriptor* login_cw; static const google::protobuf::FieldDescriptor* initialState_cw; static const google::protobuf::FieldDescriptor* room_cw; static const google::protobuf::FieldDescriptor* mainMenu_cw; static const google::protobuf::FieldDescriptor* gameModels_cw; static const google::protobuf::FieldDescriptor* chat_mm; static const google::protobuf::FieldDescriptor* nameField_chat; static const google::protobuf::FieldDescriptor* player_sub; static const google::protobuf::FieldDescriptor* player_team; static const google::protobuf::FieldDescriptor* chat_room; }; // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "Descriptors.h" #include "Protobufs/RegLogModels.pb.h" #include "Protobufs/MessageModels.pb.h" #include "Protobufs/MainMenuModels.pb.h" const google::protobuf::FieldDescriptor* Descriptors::captchaDataField_ich = InputChecking::default_instance().descriptor()->FindFieldByName("captchaData"); const google::protobuf::FieldDescriptor* Descriptors::loginCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("loginCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::mailCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("mailCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::captchaCheckStatus_ich = InputChecking::default_instance().descriptor()->FindFieldByName("captchaCheckStatus"); const google::protobuf::FieldDescriptor* Descriptors::publicKey_reg = Registration::default_instance().descriptor()->FindFieldByName("publicKey"); const google::protobuf::FieldDescriptor* Descriptors::stateCode_reg = Registration::default_instance().descriptor()->FindFieldByName("stateCode"); const google::protobuf::FieldDescriptor* Descriptors::publicKey_log = Login::default_instance().descriptor()->FindFieldByName("publicKey"); const google::protobuf::FieldDescriptor* Descriptors::stateCode_log = Login::default_instance().descriptor()->FindFieldByName("stateCode"); const google::protobuf::FieldDescriptor* Descriptors::registration_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("registration"); const google::protobuf::FieldDescriptor* Descriptors::login_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("login"); const google::protobuf::FieldDescriptor* Descriptors::initialState_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("initialState"); const google::protobuf::FieldDescriptor* Descriptors::room_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("room"); const google::protobuf::FieldDescriptor* Descriptors::mainMenu_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("mainMenu"); const google::protobuf::FieldDescriptor* Descriptors::gameModels_cw = CryptogramWrapper::default_instance().descriptor()->FindFieldByName("gameModels"); const google::protobuf::FieldDescriptor* Descriptors::chat_mm = MainMenu::default_instance().descriptor()->FindFieldByName("chat"); const google::protobuf::FieldDescriptor* Descriptors::nameField_chat = Chat::default_instance().descriptor()->FindFieldByName("name"); const google::protobuf::FieldDescriptor* Descriptors::player_sub = SubscribeRoom::default_instance().descriptor()->FindFieldByName("player"); const google::protobuf::FieldDescriptor* Descriptors::player_team = SubscribeRoom::default_instance().descriptor()->FindFieldByName("team"); const google::protobuf::FieldDescriptor* Descriptors::chat_room = RoomDescribe::default_instance().descriptor()->FindFieldByName("chat"); 


Now we need to send from the client to the Utility server which contains the only alive field that always accepts true, the bool type will allow using the minimum message size, I want to note that in order to send a message with the false field, you need to wrap oneof v1 {bool alive = 1; }, if the field is false or zero, it is considered that it is missing when we receive a message and want to know if there is alive, we will not be able to recognize this false or there is simply no field (if there is no field, this is a signal to some actions eg). We also always import DisableWarnings to disable warnings. Each protobuff message has its own class, so that you do not have to recompile for any changes. Let's generate the command classes:

./protoc --cpp_out=c:/Spiky/Spiky_Client/Source/Spiky_Client/Protobufs --java_out=c:/Spiky/Spiky_Server/src/main/java *.proto

Headline DisableWarnings updated, do not forget to add error suppression again! (from the file add.txt).

add.txt
#define PROTOBUF_INLINE_NOT_IN_HEADERS 0

#pragma warning(disable:4100)
#pragma warning(disable:4127)
#pragma warning(disable:4125)
#pragma warning(disable:4267)
#pragma warning(disable:4389)

In order for the project in the studio to be updated and see the new files, you need to click RMB on .uproject and select “Generate Visual Studio project files”. Now with the PackageHandler class everything is in order, SimpleChannelInboundHandler <MessageModels.Wrapper> is found, we will redefine channelRead0 as we are required to, the method that processes all incoming messages.

 @Override protected void channelRead0(ChannelHandlerContext ctx, MessageModels.Wrapper wrapper) throws Exception { } 

Add handlers to the ServerInitializer pipeline:

 public class ServerInitializer extends ChannelInitializer<NioDatagramChannel> { @Override protected void initChannel(NioDatagramChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); /*  */ pipeline.addLast(new LoggingHandler(LogLevel.INFO)); /* proto  */ pipeline.addLast(new ProtoDecoderHandler()); /*   */ pipeline.addLast(new PackageHandler()); } 

Open ProtoDecoderHandler, add exceptionCaught, the method that is called in case of an error, it is convenient to close the channel or connection to the database and channelReadComplete, where we clear the stream after writing to it. Immediately update channelRead0, we read the packet, translate it into an array of bytes, which we then assemble into a message using parseDelimitedFrom - it reads the length, then the message itself. We do not send further by handler, we will send a message back echoed.

ProtoDecoderHandler
 /* * Copyright (c) 2017, Vadim Petrov - MIT License */ package com.spiky.server.udp.handlers; import com.spiky.server.protomodels.MessageModels; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import java.io.ByteArrayInputStream; public class ProtoDecoderHandler extends SimpleChannelInboundHandler<DatagramPacket> { @Override protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket datagramPacket) throws Exception { ByteBuf buf = datagramPacket.content(); byte[] bytes = new byte[buf.readableBytes()]; int readerIndex = buf.readerIndex(); buf.getBytes(readerIndex, bytes); ByteArrayInputStream input = new ByteArrayInputStream(bytes); MessageModels.Wrapper wrapper = MessageModels.Wrapper.parseDelimitedFrom(input); System.out.println("udp: "); System.out.println(wrapper.toString()); System.out.println(datagramPacket.sender().getAddress() + " " + datagramPacket.sender().getPort()); //   () ctx.write(new DatagramPacket(Unpooled.copiedBuffer(datagramPacket.content()), datagramPacket.sender())); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } 


On the client, we implement the listener and the sender, in the class SocketObject. We need to add new functions SendByUDP and ReadDelimitedFrom, the implementation in c ++ of which, unlike java, unfortunately, is missing.

SocketObject.cpp
 #include "Spiky_Client.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" #include <google/protobuf/message.h> #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); } void USocketObject::RunUdpSocketReceiver() { FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(30); UDPReceiver = new FUdpSocketReceiver(udp_socket, ThreadWaitTime, TEXT("UDP_RECEIVER")); UDPReceiver->OnDataReceived().BindStatic(&USocketObject::Recv); UDPReceiver->Start(); } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { GLog->Log("Reveived UDP data"); uint8_t * buffer = ArrayReaderPtr->GetData(); size_t size = ArrayReaderPtr->Num(); GLog->Log("Size of incoming data: " + FString::FromInt(size)); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); std::shared_ptr<Wrapper> wrapper(new Wrapper); ReadDelimitedFrom(&input, wrapper.get()); std::string msg; wrapper->SerializeToString(&msg); GLog->Log(msg.c_str()); } bool USocketObject::SendByUDP(google::protobuf::Message * message) { Wrapper wrapper; if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; sentState = udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *udp_address); delete[] buffer; return sentState; } bool USocketObject::ReadDelimitedFrom(google::protobuf::io::CodedInputStream * input, google::protobuf::MessageLite * message) { // Read the size. uint32_t size; if (!input->ReadVarint32(&size)) return false; // Tell the stream not to read beyond that size. google::protobuf::io::CodedInputStream::Limit limit = input->PushLimit(size); // Parse the message. if (!message->MergeFromCodedStream(input)) return false; if (!input->ConsumedEntireMessage()) return false; // Release the limit. input->PopLimit(limit); return true; } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; } 


RunUdpSocketReceiver - sets the speed for checking new messages, delegates incoming Recv data. Recv - reads the size, parses the bytes using ReadDelimitedFrom and creates a wrapper wrapper. SendByUDP - sends via UDP, sends messages of various formats to the input, we determine what the format is inside, we wrap, serialize, and send.

Open SpikyGameMode, send messages to the server by pressing the Q key.

 virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; void TestSendUPDMessage(); 

In BeginPlay, we add the ability to respond to user input by setting:

 EnableInput(GetWorld()->GetFirstPlayerController()); InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage); 

In EndPlay, just add a log message to see when game mode ends, or switch to another. TestSendUPDMessage - the function that is called when you press the Q key.
 void ASpikyGameMode::TestSendUPDMessage() { GLog->Log("send ->>>"); std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); USocketObject::SendByUDP(utility.get()); } 


SpikyGameMode.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SpikyGameMode.h" #include "SocketObject.h" #include "Runtime/Engine/Classes/Engine/World.h" #include "Protobufs/UtilityModels.pb.h" void ASpikyGameMode::BeginPlay() { Super::BeginPlay(); GLog->Log("AClientGameMode::BeginPlay()"); EnableInput(GetWorld()->GetFirstPlayerController()); InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage); } void ASpikyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); GLog->Log("AClientGameMode::EndPlay()"); } void ASpikyGameMode::TestSendUPDMessage() { GLog->Log("send ->>>"); std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); USocketObject::SendByUDP(utility.get()); } 


Open SpikyGameInstance and initialize the sockets when starting the game, add functions that are called when the game starts and ends:

 virtual void Init() override; virtual void Shutdown() override; 

We will need another Config class, where we will be storing various static settings on the server. We will create it (Spiky_Client / Source / Spiky_Client / Public), without a parent, put the addresses, ports and the flag that the cryptography is included (for the future).

Config.h / Config.cpp
 // .h // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include <string> class Config { public: static std::string address; static size_t tcp_local_port; static size_t tcp_server_port; static size_t udp_local_port; static size_t udp_server_port; static bool bEnableCrypt; }; // .cpp // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "Config.h" bool Config::bEnableCrypt = true; std::string Config::address = "127.0.0.1"; size_t Config::tcp_local_port = 7678; size_t Config::tcp_server_port = 7680; size_t Config::udp_local_port = 7679; size_t Config::udp_server_port = 7681; 


Now we initialize sockets in SpikyGameInstance :: Init ()

 void USpikyGameInstance::Init() { GLog->Log("UClientGameInstance::Init()"); USocketObject::InitSocket(Config::address.c_str(), Config::tcp_local_port, Config::tcp_server_port, Config::udp_local_port, Config::udp_server_port); //    udp  USocketObject::RunUdpSocketReceiver(); } 

It remains only to set the response to pressing the keys in the editor, to do this, go to Edit → Project Settings → Input → Action Mapping click + in the text field write the Q name we specified in the code, and add the Q button, that's it!

After starting the server and client, by clicking a button in the server log, thanks to the LoggingHandler, we will see something like the following: In the Unreal Engine: We will not return to the UDP theme anymore. Disable the functions and creation of udp_socket on the server and client UDP: The status of the SocketObject at the moment:

udp:
utility {
alive: true
}

/127.0.0.1 7679
10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] RECEIVED: DatagramPacket(/127.0.0.1:7679 => /0:0:0:0:0:0:0:0:7681, PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 2048)), 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 abcdef |
+--------+-------------------------------------------------+----------------+
|00000000| 04 0a 02 08 01 |..... |
+--------+-------------------------------------------------+----------------+
10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler write
INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] WRITE: DatagramPacket(=> /127.0.0.1:7679, UnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 5)), 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 abcdef |
+--------+-------------------------------------------------+----------------+
|00000000| 04 0a 02 08 01 |..... |
+--------+-------------------------------------------------+----------------+
10, 2017 4:42:30 PM io.netty.handler.logging.LoggingHandler flush
INFO: [id: 0x89373e1f, L:/0:0:0:0:0:0:0:0:7681] FLUSH


Reveived UDP data
Size of incoming data: 5



ServerMain
//new Thread(ServerMain::run_udp).start();
SpikyGameInstance
// udp
//USocketObject::RunUdpSocketReceiver();



SocketObject.cpp
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" FSocket* USocketObject::tcp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::tcp_address = nullptr; bool USocketObject::bIsConnection = false; FSocket* USocketObject::udp_socket = nullptr; TSharedPtr<FInternetAddr> USocketObject::udp_address = nullptr; FUdpSocketReceiver* USocketObject::UDPReceiver = nullptr; int32 USocketObject::tcp_local_port = 0; int32 USocketObject::udp_local_port = 0; USocketObject::~USocketObject() { GLog->Log("USocketObject::~USocketObject()"); if (tcp_socket != nullptr || udp_socket != nullptr) { tcp_socket->Close(); //UDPReceiver->Stop(); delete tcp_socket; delete udp_socket; } } void USocketObject::InitSocket(FString serverAddress, int32 tcp_local_p, int32 tcp_server_port, int32 udp_local_p, int32 udp_server_port) { int32 BufferSize = 2 * 1024 * 1024; tcp_local_port = tcp_local_p; udp_local_port = udp_local_p; /* tcp_socket = FTcpSocketBuilder("TCP_SOCKET") .AsNonBlocking() // Socket connect always success. Non blocking you say socket connect dont wait for response (Don?t block) so it will return true. .AsReusable() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ // tcp tcp_socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("TCP_SOCKET"), false); // create a proper FInternetAddr representation tcp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); // parse server address FIPv4Address serverIP; FIPv4Address::Parse(serverAddress, serverIP); // and set tcp_address->SetIp(serverIP.Value); tcp_address->SetPort(tcp_server_port); tcp_socket->Connect(*tcp_address); // set the initial connection state bIsConnection = Alive(); // udp udp_address = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); FIPv4Address::Parse(serverAddress, serverIP); udp_address->SetIp(serverIP.Value); udp_address->SetPort(udp_server_port); /* udp_socket = FUdpSocketBuilder("UDP_SOCKET") .AsReusable() .BoundToPort(udp_local_port) .WithBroadcast() .WithReceiveBufferSize(BufferSize) .WithSendBufferSize(BufferSize) .Build(); */ } void USocketObject::RunUdpSocketReceiver() { FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(100); UDPReceiver = new FUdpSocketReceiver(udp_socket, ThreadWaitTime, TEXT("UDP_RECEIVER")); UDPReceiver->OnDataReceived().BindStatic(&USocketObject::Recv); UDPReceiver->Start(); } void USocketObject::Recv(const FArrayReaderPtr& ArrayReaderPtr, const FIPv4Endpoint& EndPt) { GLog->Log("Reveived UDP data"); uint8_t * buffer = ArrayReaderPtr->GetData(); size_t size = ArrayReaderPtr->Num(); GLog->Log("Size of incoming data: " + FString::FromInt(size)); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); std::shared_ptr<Wrapper> wrapper(new Wrapper); ReadDelimitedFrom(&input, wrapper.get()); std::string msg; wrapper->SerializeToString(&msg); GLog->Log(msg.c_str()); } bool USocketObject::SendByUDP(google::protobuf::Message * message) { Wrapper wrapper; if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; sentState = udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *udp_address); delete[] buffer; return sentState; } void USocketObject::Reconnect() { } bool USocketObject::Alive() { return false; } bool USocketObject::ReadDelimitedFrom(google::protobuf::io::CodedInputStream * input, google::protobuf::MessageLite * message) { // Read the size. uint32_t size; if (!input->ReadVarint32(&size)) return false; // Tell the stream not to read beyond that size. google::protobuf::io::CodedInputStream::Limit limit = input->PushLimit(size); // Parse the message. if (!message->MergeFromCodedStream(input)) return false; if (!input->ConsumedEntireMessage()) return false; // Release the limit. input->PopLimit(limit); return true; } 


Do the same for TCP. Create the Handlers package and add there two empty DecryptHandler, EncryptHandler classes. All messages will arrive encrypted, go through DecryptHandler, decrypt and then, depending on the type, be sent further for processing. Open the ServerInitializer, we need to prepare a message using the prototyp decoders built into Netty. Add the protobuff encoders and decoders to the pipeline:

 // Decoders protobuf pipeline.addLast(new ProtobufVarint32FrameDecoder()); pipeline.addLast(new ProtobufDecoder(MessageModels.Wrapper.getDefaultInstance())); // Encoder protobuf pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder()); 

We extend the DecryptHandler extends MessageToMessageDecoder <MessageModels.Wrapper>, override the decode method and add it last to the pipeline:

 /*    */ pipeline.addLast(new DecryptHandler()); 

We do not process alive messages on the server in any way, we send back by an echo in DecryptHandler:

 ctx.writeAndFlush(wrapper); 

Let's return to sending messages on the client. We will check the server status by sending a message every second using a separate stream. In Unreal, there are several ways to create a stream, the simplest is to create a timer . There are also Task created for small tasks, an example with the search for prime numbers:

Implementing Multithreading in UE4
Multi-Threading: Task Graph System
Engine / Source / Runtime / Core / Public / Async / AsyncWork.h

And you can implement the FRunnable interface , which we do .

Multi-Threading: How to Create Threads in UE4

Let's create a class ServerStatusCheckingTh with parent FRunnable in the Net folder.

FServerStatusCheckingTh
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "Runtime/Core/Public/HAL/Runnable.h" #include "Runtime/Core/Public/HAL/RunnableThread.h" class SPIKY_CLIENT_API FServerStatusCheckingTh : public FRunnable { // Singleton instance, can access the thread any time via static accessor, if it is active! static FServerStatusCheckingTh* Runnable; // Thread to run the worker FRunnable on FRunnableThread* Thread; // The way to stop static bool bThreadRun; public: FServerStatusCheckingTh(); ~FServerStatusCheckingTh(); // FRunnable interface virtual bool Init(); virtual uint32 Run(); // Logics static FServerStatusCheckingTh* RunServerChecking(); // Shuts down the thread. Static so it can easily be called from outside the thread context static void Shutdown(); }; // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "ServerStatusCheckingTh.h" #include "SocketObject.h" FServerStatusCheckingTh* FServerStatusCheckingTh::Runnable = nullptr; bool FServerStatusCheckingTh::bThreadRun = false; FServerStatusCheckingTh::FServerStatusCheckingTh() { Thread = FRunnableThread::Create(this, TEXT("ServerStatusChecking"), 0, TPri_BelowNormal); } FServerStatusCheckingTh::~FServerStatusCheckingTh() { delete Thread; Thread = nullptr; } bool FServerStatusCheckingTh::Init() { bThreadRun = true; return true; } uint32 FServerStatusCheckingTh::Run() { while (bThreadRun) { FPlatformProcess::Sleep(1.f); //    if (!USocketObject::bIsConnection) //   ,  { USocketObject::Reconnect(); } else { USocketObject::bIsConnection = USocketObject::Alive(); //    ,    } //    //GLog->Log("Connect state (bIsConnection) = " + FString::FromInt((int32)USocketObject::bIsConnection) + " | FServerStatusCheckingTh::CheckServer"); } return 0; } FServerStatusCheckingTh* FServerStatusCheckingTh::RunServerChecking() { if (!Runnable && FPlatformProcess::SupportsMultithreading()) { Runnable = new FServerStatusCheckingTh(); } return Runnable; } void FServerStatusCheckingTh::Shutdown() { bThreadRun = false; GLog->Log("FServerStatusCheckingTh::Shutdown()"); if (Runnable) { delete Runnable; Runnable = nullptr; } } 


We start the thread by calling RunServerChecking (), which passes through Init, Run and Exit. Finish shutdown (). Every second we send a message to Alive and if the messages do not reach, try reconnecting by calling Reconnect. Implement Reconnect and Alive in USocketObject. Reconnect - closes the socket, normalizes the address and initializes the sockets again. Alive - creates a message and immediately sends it:

 void USocketObject::Reconnect() { tcp_socket->Close(); uint32 OutIP; tcp_address->GetIp(OutIP); FString ip = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP); InitSocket(ip, tcp_local_port, tcp_address->GetPort(), udp_local_port, udp_address->GetPort()); } bool USocketObject::Alive() { std::shared_ptr<Utility> utility(new Utility); utility->set_alive(true); // Send  , : , ?  tcp? return UMessageEncoder::Send(utility.get(), false, true); } 

Create the Handlers folder and the MessageDecoder and MessageEncoder classes derived from UObject in the same way as the decoder and server encoder. This classes are engaged in decrypting / encrypting and scanning / wrapping incoming / outgoing messages. Add #include "MessageEncoder.h" to SocketObject and compile.

We need a listener for incoming messages, for this we will create a class in Net, a separate stream TCPSocketListeningTh with the parent FRunnable. Here we check the connection and set the speed of the stream so that it does not work in vain, we read, convert the bytes to protobuf, send for processing in the main game stream:

FTCPSocketListeningTh
 // Copyright (c) 2017, Vadim Petrov - MIT License #pragma once #include "Runtime/Core/Public/HAL/Runnable.h" #include "Runtime/Core/Public/HAL/RunnableThread.h" class SPIKY_CLIENT_API FTCPSocketListeningTh : public FRunnable { FRunnableThread* Thread; static FTCPSocketListeningTh* Runnable; static bool bThreadRun; public: FTCPSocketListeningTh(); ~FTCPSocketListeningTh(); virtual bool Init(); virtual uint32 Run(); static FTCPSocketListeningTh* RunSocketListening(); static void Shutdown(); }; #include "Spiky_Client.h" #include "TCPSocketListeningTh.h" #include "SocketObject.h" #include "MessageDecoder.h" #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> #include "Protobufs/MessageModels.pb.h" #include "Async.h" FTCPSocketListeningTh* FTCPSocketListeningTh::Runnable = nullptr; bool FTCPSocketListeningTh::bThreadRun = false; FTCPSocketListeningTh::FTCPSocketListeningTh() { Thread = FRunnableThread::Create(this, TEXT("TCP_RECEIVER"), 0, TPri_BelowNormal); } FTCPSocketListeningTh::~FTCPSocketListeningTh() { delete Thread; Thread = nullptr; } bool FTCPSocketListeningTh::Init() { bThreadRun = true; return true; } uint32 FTCPSocketListeningTh::Run() { while (bThreadRun) { //    if (USocketObject::bIsConnection == false) //   { FPlatformProcess::Sleep(1.f); //   ,  } else { FPlatformProcess::Sleep(0.03f); if (!USocketObject::tcp_socket) return 0; //Binary Array! TArray<uint8> ReceivedData; uint32 Size; while (USocketObject::tcp_socket->HasPendingData(Size)) //     { ReceivedData.Init(FMath::Min(Size, 65507u), Size); int32 Read = 0; USocketObject::tcp_socket->Recv(ReceivedData.GetData(), ReceivedData.Num(), Read); } if (ReceivedData.Num() > 0) { GLog->Log(FString::Printf(TEXT("Data Read! %d"), ReceivedData.Num()) + " | FTCPSocketListeningTh::Run"); //    protobuf uint8_t * buffer = ReceivedData.GetData(); size_t size = ReceivedData.Num(); google::protobuf::io::ArrayInputStream arr(buffer, size); google::protobuf::io::CodedInputStream input(&arr); bool protosize = true; /*        , tcp      */ while (protosize) { std::shared_ptr<Wrapper> wrapper(new Wrapper); protosize = USocketObject::ReadDelimitedFrom(&input, wrapper.get()); /*      ,    */ AsyncTask(ENamedThreads::GameThread, [wrapper]() { UMessageDecoder * Handler = NewObject<UMessageDecoder>(UMessageDecoder::StaticClass()); Handler->SendProtoToDecoder(wrapper.get()); }); } } } } return 0; } FTCPSocketListeningTh* FTCPSocketListeningTh::RunSocketListening() { if (!Runnable && FPlatformProcess::SupportsMultithreading()) { Runnable = new FTCPSocketListeningTh(); } return Runnable; } void FTCPSocketListeningTh::Shutdown() { bThreadRun = false; GLog->Log("FTCPSocketListeningTh::Shutdown()"); if (Runnable) { delete Runnable; Runnable = nullptr; } } 


Let's enable two new streams in SpikyGameInstance:

 ... #include "ServerStatusCheckingTh.h" #include "TCPSocketListeningTh.h" ... //     USpikyGameInstance::Init() //      FServerStatusCheckingTh::RunServerChecking(); //    tcp  FTCPSocketListeningTh::RunSocketListening(); //     USpikyGameInstance::Shutdown() //     FServerStatusCheckingTh::Shutdown(); //    tcp  FTCPSocketListeningTh::Shutdown(); 

We implement the encoder, the prototype message arrives, in the function we determine whether to encrypt it, type it, wrap it in the Wrapper, write the length and body to the buffer, then send it via TCP or UDP channel:

Messageencoder
 // Copyright (c) 2017, Vadim Petrov - MIT License #include "Spiky_Client.h" #include "MessageEncoder.h" #include "SocketObject.h" #include "Protobufs/MessageModels.pb.h" #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/coded_stream.h> bool UMessageEncoder::Send(google::protobuf::Message * message, bool bCrypt, bool bTCP) { Wrapper wrapper; //     if (bCrypt) { } else { if (message->GetTypeName() == "Utility") { Utility * mes = static_cast<Utility*>(message); wrapper.set_allocated_utility(mes); } } size_t size = wrapper.ByteSize() + 5; // include size, varint32 never takes more than 5 bytes uint8_t * buffer = new uint8_t[size]; google::protobuf::io::ArrayOutputStream arr(buffer, size); google::protobuf::io::CodedOutputStream output(&arr); //       buffer output.WriteVarint32(wrapper.ByteSize()); wrapper.SerializeToCodedStream(&output); //     utility if (wrapper.has_utility()) { wrapper.release_utility(); } int32 bytesSent = 0; bool sentState = false; if (bTCP) { //send by tcp sentState = USocketObject::tcp_socket->Send(buffer, output.ByteCount(), bytesSent); } else { //send by udp sentState = USocketObject::udp_socket->SendTo(buffer, output.ByteCount(), bytesSent, *USocketObject::udp_address); } delete[] buffer; return sentState; } 


Run the server and the client, and check that the messages and the echo come through. Server
utility { alive: true }
Log: Client Log:
Connect state (bIsConnection) = 1 | FServerStatusCheckingTh::CheckServer
Data Read! 5 | FTCPSocketListeningTh::Run


Conclusion


So with the necessary preparation, we are done. As a result, we have a client-server communicating with protobuff messages, compiled and connected to Android and Windows libraries, and the initial architecture over which we will further expand the functionality. Finally leave the bibliography list to help you better understand Netty and the architecture of online games.

Thank you for reading this place!



Norman Maurer "Netty in Action" - with Netty you can quickly and easily write any client-server application that will be easily expanded and scaled.

Josh Glazer "Multiplayer Game Programming: Architecting Networked Games" - this book, using real examples, tells about the features of developing online games and the basics of building a robust multi-user architecture.

Grenville Armitage "Networking and Online Games: Understanding and Engineering Multiplayer Internet Games" is a fairly old book, but a good book that explains how multiplayer games work.

Source: https://habr.com/ru/post/333788/


All Articles