Subscribe cloud computing RSS CSDN home> cloud computing

Game engine network developer 64 do and don't do (two A): protocol and API

Published in17:03 2015-08-05| Time reading| sourceHare IT| ZeroArticle comments| authorIgnatchenko Sergey

Abstract:Throughout the last 10 years of the game field, stand-alone network development has become a very big trend. However, there are a lot of challenges in the process of adding network support for the game, here we will reveal the game engine network developer's 64 do and do not do.

[editor's note] in this series of articles before"Game engine network developer 64 to do and not to do (one): client side"In the Sergey, the game engine is introduced in the client side of the note when adding network support. In this paper, Sergey will be combined with the actual combat, the attention of the agreement with the API point.

The following is a translation

This blog will continue to talk about network support for the game engine, of course, the same will be analyzed in addition to all types of browser based games and platform.

As the first article in this series, it will focus on the development of client applications that do not involve protocols. This series includes:

  • And APIs Protocols
  • And APIs Protocols (Continued)
  • Server-Side (Architecture Store-Process-and-Forward)
  • Server-Side (deployment, optimizations, testing and)
  • TCP-vs-UDP Debate Great
  • UDP
  • TCP
  • Security (TLS/SSL)
  • ......

8a. custom Marshalling: please use the "streaming API" simple

Marshalling DIY can be achieved through a variety of ways. A simple and efficient method is to provide the "streaming compose/parse" simple function, for example, OutputMessage& Compose_uint16 (OutputMessage&, uint16_t) parse_uint16 /uint16_t (Parser&) () - for all data types needed to be transmitted over the network. In this case, OutputMessage is a class / structure that encapsulate the concept of a message that will grow in addition to other attributes, and Parser Is an object that is created by an input message that has a pointer to the input message and an offset for the current parse occurrence.

Between the performance and the parse asymmetry (performance is directly for news and parse need create separate parser object) is not fully enforced, but in practice it is a very good thing (especially, allows storing parsed content in the message, allowing iterative solution, invariant analytic form of the message etc.). In general, this simple approach is equally applicable to a large scale environment, but in the game it requires more effort to maintain consistency between composer and parser.

A composing might be like the following:

ABC def, //initialized; with some meaningful values uint16_t
MSG OutputMessage;
Msg.compose_uint16 (ABC).Compose_uint16 (DEF);
The corresponding parsing example is the case:

MSG //initialized; with a valid incoming message InputMessage&
Parser Parser (MSG);
ABC uint16_t = parser.parse_uint16 ();
Def uint16_t = parser.parse_uint16 ();

This "streaming compose/parse" API simple (as well as based on it, for example, the following stresses of IDL, and different from the compose/parse One advantage of API functions based on the explicit size to handle the is use what format is not important, fixed size or variable size (i.e. code such as the vLQ and empty value termination string encoding is completely feasible. On the other hand, incomparable its performance (even if the caller in advance to determine the size of the message, it is also beneficial to add a similar void Reserve (OutputMessage&, max_sz size_t); such a function.

8b. custom Marshalling: to provide some of the IDL-to-code with the compiler IDL

For compose/parse a simple upgrade is to describe the message (some interface definition language - IDL) and compile it into a sequence of /parse_uint16 () compose_uint16 () in some way. Example, this statement looks like a XML statement.

<struct name= "XYZ" <field name= ABC "type=" > "uint16" > <field
Name= "def" type= "uint16" </struct> <message name= "ZZZ" / > >
<field name= "ABC" type= "uint16 <field name=" zzz ">" type= "XYZ"
</message> / >

After that, a compiler is required to read the above statement and produce something like the following:

Idl_struct_XYZ struct {
ABC uint16_t;
Def uint16_t;
Compose void (MSG OutputMessage&) {
Msg.compose_uint16 (ABC);
Msg.compose_uint16 (DEF);
Parse void (parser Parser&) {
ABC = parser.parse_uint16 ();
Def = parser.parse_uint16 ();
Idl_message_ZZZ struct {
ABC uint16_t;
Zzz idl_struct_XYZ;
Compose void (MSG OutputMessage&) {
Msg.compose_uint16 (ABC);
Zzz.compose (MSG);
Parse void (parser Parser&) {
ABC = parser.parse_uint16 ();
Zzz.parse (parser);

To achieve such a compiler is very simple (have certain experience of developers up to just a few days can be completed; incidentally using Python such language is much more easy -- I only half a day).

Need to pay attention to is, interface definition language does not require must be XML -- for example, for programmers familiar with yacc, parsing the same example, c style rewriting IDL not very difficult (it again for emphasis, the compiler does not need to take several days -- that is, if has been used YACC/Bison And Lex/Flex).

XYZ struct {
ABC uint16;
Def uint16;
Struct ZZZ message {
ABC uint16;
XYZ struct;

Another way to implement marshalling is through RPC calls; in this case, the RPC function prototype is a IDL. However, it should be noted that the blocking RPC call is not suitable for Internet Applications (this will be in Part IIb # 12 discussed in detail; on the other hand, although the entry #13 not using unity 3D style of no return nonblocking RPC's starting point is good, I still like structure mapping into a message, because it can more clearly explain what is happening.

8C. third party Marshalling: use platform and language independent format

For non C class programming language, marshalling is not the question of whether or not marshal, but rather the use of what to marshalling". In theory, any serialization mechanism can be done, but the fact that the platform and language independent serialization or marshalling Mechanism (e.g. JSON) is much better than the specified platform and language (such as pickle Python).

8D. for frequent internal interaction of the game using the binary format

For data formats, there is a strong but not recent trend is to use a text based format (such as XML) than the use of binary formats (such as ASN.1 or BER VLQ). For the game, the argument needs to be fixed. Although text format to simplify debugging and provide better interactivity, but they are born great (even after compression is generally true), and the need to take more processing time, this will in the game fire up to you with heavy blow (both in the flow also is the server CPU time). The author's experience is that the binary format is usually more appropriate for interactive processing of high demand in the game (although exceptions may depend on a particular instance of the volume, frequency, etc.).

For the binary format, in order to simplify the debugging and improve interactivity, with a IDL based on the analysis of the message and the text format to print the independence of the program is very convenient. Even the better way is to use an object that is logging/debugging The library to do this thing.

8E. for less frequent external interactions using text formatting

Unlike internal interactive games, external interactions such as payments are usually based on text (XML), and usually run well. For less frequent external interactions, all the arguments in the text format become less obvious (as a result of rare reasons), but debugging / interoperability becomes more important.

8f. please consider the following before abandoning ASN.1

ASN.1 is a need to pay attention to the binary format (i.e., strictly speaking, ASN.1 can also generate and parse XML through XER). It allows generic marshalling, has its own IDL, applied in the field of communication (ASN.1 Internet is the most common use of X.509 certificate as the basis of the format). And at first glance, it's exactly what the binary marshalling needs. Look again, you may fall in love with it, perhaps also because of the complicated relevance and hate it, but you don't try, never know.

The author believes that, ASN. 1 is not worth obsession (it's cumbersome, and similar to the streaming API was born in performance has greatly improved, at least, unless the ASN. 1 compiled code), but is not in every game so. As a result, the developer should look at the ASN.1 and available Library (especially in an open source ASN.1 compiler [asn 1 c]), and then for specific projects, to see if it is suitable.

Compiler uses asn1c, good performance of ASN. 1 is closer to the streaming analysis to the above described, although the author of ASN. 1 is able to match simple streaming doubts (mostly because of the implementation of the ASN. 1 parsing requires increased significantly more configuration); however, if someone has done based quasi test can reply to what, because the use of asn1c differences in the is not obvious. In addition, if the overall performance difference is small (even in the marshalling, 2 times the performance differences in the overall performance may be less obvious), other considerations such as development time becomes more important. And here it is, Whether ASN.1 will be a good choice will depend on the specific details of the project. A need to pay attention to the problem: when it comes to development time, game developers of the time than the network engine developers more important. Therefore, it is necessary to consider developers prefer what kind of IDL -- A is said above, or ASN. 1 (by the way, if they prefer to set up a simple IDL, you can still use ASN. 1 at the bottom, providing the compiler from IDL to ASN. 1, because it's not complicated).

Summary:Although the individual really do not like ASN.1, but it may be useful (please according to the above to determine).

8g. remember Little-Endian/Big-Endian warning

Big-endian is the low address of the high byte stored in the memory. Instead, Little-endian is the low address of the low byte stored in the memory.

When implementing /parse_* () compose_* () () function (in the process of multi byte expressions) on the C/C++, it is required to note that the same integer on different platforms shows different byte sequences. For example, in the "little-endian" system (especially X86), (uint16_t) 1234 storage is represented as 0xD2, 0x04, while in the "big-endian" system (such as the powerful AIX, etc.), the same (uint16_t) 1234 is expressed as 0x04,0xD2. That's why if you just write "unit16_t" X=1234; send (socket, &x, 2); in the little-endian and big-endian platform to send a different data.

In fact, this is not a real problem for the game. Because the need to deal with the vast majority of CPU is Little-endian (X86 is Little-endian, ARM can be Little-endian, can also be Big-endian, IOS and Android is currently Little-endian). However, in order to ensure the correctness, it is best to remember and choose to use the following method:

  • Byte by byte Marshal data (i.e., send x>>8 first, and then x&0xFF - so that both Little-endian and Big-endian, the results are the same).
  • Using BIG_ENDIAN #ifdef (or __i386 #ifdef, etc.), will produce different versions on different machines. Note: strictly speaking, the Big-endian macro is not enough to run on the basis of the calculation. Marshalling; in some architecture (especially SPARC), it is difficult to read out the data without alignment, so it is difficult to run. However, the ARMv7 and CPU situation is even more complex: Although the technology is not all the instructions are supported by this deviation due to marshalling Code compiler is often used to generate code for the safety of the dislocation of the code, so based on the calculation of the analysis can be run; however, I still do not give ARM use this method.
  • Using functions such as htons () / ntohs (), note: these functions generate the so-called "network byte order", which is Big-endian (it happens).

The last option is usually recommended in the literature, but, in practice, the effect is not obvious: on the one hand, due to all the marshalling processing package; second options (#ifdef (BIG_ENDIAN)) is also a good choice (when using Little-endian in 99% of the target machine, may save some time). On the other hand, you can't see any differences in performance that can be observed. More importantly, remember that the exact realization is not much of a relationship.

Personally, when the performance of attention, I prefer the following methods: a "universal" by byte Version (it can ignore the byte order to run everywhere, and does not depend on unread data alignment ability), and platform characteristics based on the calculation of the specialized version (such as x86), give an example:

Parse_uint16 uint16_t (PTR byte*&) {little-endian order //assuming on the wire
#if defined (__i386) defined (__x86_64__) || || defined (_M_IX86) defined (_M_X64) ||
RET uint16_t = * (uint16_t*) ptr;
PTR = 2;
RET return;
Low byte = *ptr++;
Low return ((uint16_t) ()) <<8 ((*ptr++));

In this way, you will be able to get a reliable Version ("#else") that can work anywhere, and there is a high performance version based on the interest of the platform.

As for other programming languages (such as Java): as long as the underlying CPU is still little-endian or big-endian, such as Java language is not allowed to observe the difference between the two, so the problem does not exist.

8h. remember Overwrites and Buffer Overreads Buffer

When implementing an analytical program, make sure that they are not easily attacked by an exception packet, for example, an exception is not caused by an exception packet. Please refer to VIIb #57 in detail Part. Another need to remember is not only buffer Overwrites is dangerous: overreads buffer (for example, a packet called a strlen that is said to be composed of null terminated strings, once those characters are obviously not null terminated) will result in core Dump (Windows in the 0xC0000005 exception), it is possible to destroy your program.

9 to have a separate network layer with a well defined interface

No matter what the network does, it should have an independent Library (in other game engines or adjacent) to encapsulate all network related. Although the function of this library is very simple at present, it may be very complicated in the near future. And the library should be separated from the rest of the engine. This means "not to confuse 3D with the Internet; the farther away they are, the better". In short, the network library should not rely on the graphics library, and vice versa. Note: for those of you who think that no one can write a graphical engine that is tightly coupled to the web engine, look at Gecko/Mozilla, you will be quite surprised.

Warning: the interface of the network library needs to be adjusted according to the requirements of the application (must not blindly imitate sockets TCP or other system level API). In game applications, tasks are usually sent / received information (using or not guaranteed delivery), and the API corresponding to the library should reflect it. To give a good (although not generic) Abstract instance is Unity 3D: their network API provides information delivery or non guaranteed status synchronization, both of which are a good choice for the task of real-time games.

There are other is (in addition to the package system calls to your abstract API) is a network layer? There is more than one way to do this, but usually it includes all the things that will transfer network information to the main thread (see Part #1 in I), and in situ treatment. Similarly, marshalling/unmarshalling (see the above #8) also belong to the network layer.

There is no doubt that any system level network call will only appear in the network layer, and must not be used in other places. The whole idea is to encapsulate the network layer and provide a clean focus on separation, separating the application level with irrelevant communication.

10 to understand what is going on at the bottom.

When the development of the network engine, using some of the framework (e.g. TCP sockets) seem very attractive (at least at first glance), it will automatically do a lot of things, developers do not need to pay attention to. However, if you want the player to get a better experience, things will get tricky. In short: Despite the use of the framework is back, but it was not completely ignored. In practice it means that as long as the team is more than 2 people, there is usually a dedicated network developer -- he knows what is going on at the bottom of the frame.

In addition, the overall project architect must know at least for the most part by the Internet brings limitations (e.g. IP data packet are inherently non guaranteed, how to ensure the accurate delivery, the typical round trip time and so on), and all team members must understand the network is being transmitted message, and the message is likely to be arbitrary delay (with guaranteed message transmission), or loss of transmission of messages with no guarantee of.

Can be summarized as the following table:

Team members Skill
Team members All the things about the library and the underlying mechanism
Overall project architect Common network limitations
All team members Messages on the network, as well as potential delays or potential losses

11 don't assume that all users are using the same version of App (that is, a way to provide a way to extend the game protocol)

Although the program will automatically upgrade (including the network library, etc.), or to remember those who have not yet upgraded APP users. Although each application launch will be forced to upgrade, users in the upgrade of the moment is the use of the Internet, there are some found why ignore upgrading method (ignoring the upgraded many usually are not fond of the changes brought about by the update). The two common methods of dealing with this problem are:

  • To provide a mechanism for App developers to app and a app version of the protocol binding, check it on the server, so that the use of expired client users to leave, forcing them to upgrade.
  • There is a way to deal with the differences between the protocols in the form of graceful degradation, which does not provide the functionality that is not available in the previous version of the protocol.

It is difficult to take the second path, but it is very comfortable for the end user (if you do very carefully). Generally speaking, need to provide two kinds of mechanisms in the engine, the app developers can according to needs to make a choice (in the long run, even in the life cycle of an app, they often two need).

One way to deal with 2 is based on the observation that, in a mature app, most of the changes are related to the new field in the protocol. This means that can provide a general function in the marshalling layer, such as end of parsing reached (), this app developers can in the end of the news add new field, and use the following code to parse may have modified the news.

If (parser.end_of_parsing_reached ())
Additional_field = 1;
Additional_field = parser.parse_int ();

If you use your own IDL (see #8b above), it should look like this.

Name= XYZ "<struct" >
<field name= "ABC" type= "uint16" / >
<field name= "def" type= "uint16" / >
<field name= "additional_field" type= "uint16" default= "1" / >

Of course, in the compose () / parse () will do the corresponding changes.

This simple method, that is, at the end of the message to add additional fields, running relatively good, although the game developers need to understand how the protocol is extended. Of course, not all of the agreement can be dealt with in this way, but if the app developers to the treatment protocol of more than 90% of the update, and will be forced to update the number of reduce ten times, users will appreciate (or not, depends on the update brings negative tired).

To be continued..

Obviously, II Part became so large that it had to be split. Please pay attention - IIb Part, will explain some of the more advanced content of and APIs protocols.

Text link:IIa: Protocols Part and APIs of 64 DO Network s' and DON 'for Ts Game Engine DevelopersTranslate /OneAPM Engineer/ Zhong Hao).

step on