nmspec 1.5.0.pre → 1.5.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +19 -0
- data/README.md +301 -0
- data/lib/nmspec/gdscript.rb +392 -0
- data/lib/nmspec/parser.rb +84 -0
- data/lib/nmspec/ruby.rb +403 -0
- data/lib/nmspec/v1.rb +188 -0
- data/lib/nmspec/version.rb +1 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66f0ebb3eb93e07ad986a38e902277cc7987593d43dcdc19bbb54d346bdc224e
|
4
|
+
data.tar.gz: b877b1962c64ba409141d7b202bd674e189189799dbd6b3534af5a462f90a55d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfa77fccbe36e774f5bd50c3e362afc577a5a7a125dea9e42f1fc3b8712ff7bf2f6d8ba5089d74c749e60726c5a3da2d7b6f6f6437614e1dd5fd744a9ce28c59
|
7
|
+
data.tar.gz: e25c2d97af1d9c7aea5c5109638127d0923a9e19512d7181fd8febc55a8dfde8023f693f5d4d89043b305335b56ce9439994b7336b9dbbbb3aa322cbe195be36
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright 2022 Jeffrey Lunt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
# `nmspec`
|
2
|
+
|
3
|
+
`nmspec` (network message specification) is a combination of binary
|
4
|
+
serialization and network communication (two things that are usually treated
|
5
|
+
separately), designed to make creating TCP protocols between two ends of a
|
6
|
+
network connection easier to specify and keep consistent..
|
7
|
+
|
8
|
+
A centralized YAML file is used to describe the types and messages passed
|
9
|
+
between network peers, and from that description TCP peer code (a "messenger")
|
10
|
+
is generated in any supported output programming language. The messages
|
11
|
+
described in `nmspec` are used to create both the reading and writing sides
|
12
|
+
of the connection, so that a single source code file contains everything you
|
13
|
+
need for a network peer, regardless of if it's the client or server.
|
14
|
+
|
15
|
+
## Motivation
|
16
|
+
|
17
|
+
`nmspec` was specifically created to help with with a problem I was facing,
|
18
|
+
where I was designing some network peers in two different programming languages
|
19
|
+
that needed to talk to each other, and I found that keeping the two sides in
|
20
|
+
sync generated a lot of bugs that I thought might be avoided by centralizing the
|
21
|
+
description of their communication protocols. Without this, what was happening
|
22
|
+
regularly was:
|
23
|
+
|
24
|
+
1. I would change something on the server side in one programming language
|
25
|
+
2. I would change the same thing on the client side in a different programming
|
26
|
+
language
|
27
|
+
3. The serialization of data on one side of the network would get out of sync
|
28
|
+
with the deserialization on the other side
|
29
|
+
|
30
|
+
By describing the wire protocol in one place it was my hope that I would reduce
|
31
|
+
the amount of time I spent on synchronization issues.
|
32
|
+
|
33
|
+
## Results
|
34
|
+
|
35
|
+
My approach to making this constantly-shifting communication easier to develop
|
36
|
+
and debug was to come up with a language-agnostic representation of the network
|
37
|
+
protocols within the game, specifically in some kind of easily editable
|
38
|
+
configuration language. YAML fits that description. I then integrated this
|
39
|
+
tightly with TCP (a decent starting point).
|
40
|
+
|
41
|
+
The code generators are written in Ruby, which is reasonably expressive for this
|
42
|
+
purpose.
|
43
|
+
|
44
|
+
# Output language support
|
45
|
+
|
46
|
+
As a starting point this gem supports network messengers in these two languages:
|
47
|
+
|
48
|
+
* [Ruby 3.0.x][ruby-lang]
|
49
|
+
* [GDScript 3.4.stable][gdscript]
|
50
|
+
|
51
|
+
`nmspec` came out of a online game project where the backend was written in
|
52
|
+
Ruby, and the frontend build with the Godot game engine, which includes the
|
53
|
+
embedded scripting language, GDSCript.
|
54
|
+
|
55
|
+
# Sample usage
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# add 'nmspec' to your Gemfile
|
59
|
+
|
60
|
+
$ irb
|
61
|
+
|
62
|
+
> require 'nmspec'
|
63
|
+
=> true
|
64
|
+
> pp Nmspec::V1.gen({
|
65
|
+
'spec' => IO.read('generals.io.nmspec'),
|
66
|
+
'langs' => ['ruby', 'gdscript']
|
67
|
+
})
|
68
|
+
=> {
|
69
|
+
"valid"=>true,
|
70
|
+
"errors"=>[],
|
71
|
+
"warnings"=>[],
|
72
|
+
"code"=> {
|
73
|
+
"ruby"=> "< a string of generated Ruby code that you can save to a file>",
|
74
|
+
"gdscript"=> "< a string of generated GDSCript code that you can save to a file>",
|
75
|
+
}
|
76
|
+
}
|
77
|
+
```
|
78
|
+
|
79
|
+
# Main concepts
|
80
|
+
|
81
|
+
## Messenger
|
82
|
+
|
83
|
+
A `messenger` is the thing you're descripting in an .nmspec file. A `messenger`
|
84
|
+
has default support for reading and writing a number of numeric, string, and
|
85
|
+
array types.
|
86
|
+
|
87
|
+
## Built-in types
|
88
|
+
|
89
|
+
The following built-in types are supported by `nmspec`
|
90
|
+
|
91
|
+
```plaintext
|
92
|
+
bool # boolean true/false
|
93
|
+
i8 u8 i8_list u8_list # signed/unsigned 8-bit ints, and lists of the same
|
94
|
+
i16 u16 i16_list u16_list # signed/unsigned 16-bit ints, and lists of the same
|
95
|
+
i32 u32 i32_list u32_list # signed/unsigned 32-bit ints, and lists of the same
|
96
|
+
i64 u64 i64_list u64_list # signed/unsigned 64-bit ints, and lists of the same
|
97
|
+
float float_list # signed single-precision 32-bit floating point numbers, and a list of the same
|
98
|
+
double double_list # signed double-precision 64-bit floating point numbers, and a list of the same
|
99
|
+
str str_list # strings (arrays of bytes)
|
100
|
+
```
|
101
|
+
|
102
|
+
As of this writing, all types are sent with big-endian encoding.
|
103
|
+
|
104
|
+
`*_list` types are ordered lists of elements (i.e. arrays).
|
105
|
+
|
106
|
+
There is no support for mixed-type list, mostly because socket libraries seem to
|
107
|
+
be centered around efficiently encoding/decoding streams of bytes with known bit
|
108
|
+
widths. If you want to send multiple data types one after the other, place them
|
109
|
+
into separate messages (see examples below).
|
110
|
+
|
111
|
+
## Custom types
|
112
|
+
|
113
|
+
Custom types are a way for you to give a more domain-relevant name to the
|
114
|
+
built-in types. Custom types are not structs, nor are they similar to classes
|
115
|
+
from object-oriented programming. You could, however, write your own structs or
|
116
|
+
object classes to wrap the reading/writing of protocols, if you like, but that
|
117
|
+
would be extra work that you would need to do in your own program code.
|
118
|
+
|
119
|
+
A `Messenger` has many types.
|
120
|
+
|
121
|
+
## Protocols
|
122
|
+
|
123
|
+
A `protocol` is a list of `messages` that pass between two `Messenger` peers. A
|
124
|
+
`Messenger` has many protocols.
|
125
|
+
|
126
|
+
## Messages
|
127
|
+
|
128
|
+
Messages are either read (`r`) or writes (`w`) of types over a network
|
129
|
+
connection. `Messages` also define logical names for parameters and returned data.
|
130
|
+
|
131
|
+
# `nmspec` format
|
132
|
+
|
133
|
+
`nmspec` is a subset of YAML. So, first and foremost, if your `.nmspec` file is
|
134
|
+
not valid YAML, then it's definitely not valid `nmspec`.
|
135
|
+
|
136
|
+
## Required keys:
|
137
|
+
|
138
|
+
A minimal `messenger`, with only a name and default types supported must include:
|
139
|
+
|
140
|
+
* `version` - which currently must be set to `1`
|
141
|
+
* `msgr` - the top-level key for naming and describing the messenger
|
142
|
+
* `name` - the name of the messenger
|
143
|
+
* `desc` - a description of the messenger
|
144
|
+
* `bigendian` - (optional, defaults to `true`)
|
145
|
+
* if `true`, communication uses big-endian byte order
|
146
|
+
* if `false`, communication uses little-endian
|
147
|
+
* `nodelay` - (optional, defaults to `false`)
|
148
|
+
* if `true`, disables Nagle's algorithm, which prioritizes low-latency over
|
149
|
+
throughput efficiency
|
150
|
+
* if `false`, leaves Nagle's algorithm enabled
|
151
|
+
|
152
|
+
## Optional keys:
|
153
|
+
|
154
|
+
* `types` - if your `.nmspec` file creates custom sub-types, then this is where
|
155
|
+
you declare them
|
156
|
+
* `protos` - the top-level key for the list of messaging protocols
|
157
|
+
* for each protocol:
|
158
|
+
* `name` - the name of the protocol (converted to function/method name)
|
159
|
+
* `desc` - a description of the protocol
|
160
|
+
* `msgs` - a list of messages in the protocol
|
161
|
+
|
162
|
+
## Sample `.nmspec` file
|
163
|
+
|
164
|
+
`demo/minimal.nmspec` shows the absolute minimum amount of information needed
|
165
|
+
to get a basic messenger working.
|
166
|
+
|
167
|
+
```yaml
|
168
|
+
version: 1
|
169
|
+
|
170
|
+
msgr:
|
171
|
+
name: minimal
|
172
|
+
desc: this messenger only supports the built-in types, and has no custom protocols
|
173
|
+
```
|
174
|
+
|
175
|
+
`demo/base_types.nmspec` shows an example of a one-protocol messenger that is
|
176
|
+
used to ensure that all base types can be read and written correctly.
|
177
|
+
|
178
|
+
```yaml
|
179
|
+
version: 1
|
180
|
+
|
181
|
+
msgr:
|
182
|
+
name: base types
|
183
|
+
desc: this messenger supports the built-in types, and is mainly used for testing code generators
|
184
|
+
nodelay: true
|
185
|
+
bigendian: false
|
186
|
+
|
187
|
+
protos:
|
188
|
+
- name: all_base_types
|
189
|
+
desc: write all base types
|
190
|
+
msgs:
|
191
|
+
# type var name
|
192
|
+
# ----------------------------------------------------
|
193
|
+
- bool bool
|
194
|
+
- i8 i8
|
195
|
+
- u8 u8
|
196
|
+
- i8_list i8_list
|
197
|
+
- u8_list u8_list
|
198
|
+
- i16 i16
|
199
|
+
- u16 u16
|
200
|
+
- i16_list i16_list
|
201
|
+
- u16_list u16_list
|
202
|
+
- i32 i32
|
203
|
+
- u32 u32
|
204
|
+
- i32_list i32_list
|
205
|
+
- u32_list u32_list
|
206
|
+
- i64 i64
|
207
|
+
- u64 u64
|
208
|
+
- i64_list i64_list
|
209
|
+
- u64_list u64_list
|
210
|
+
- float float
|
211
|
+
- float_list float_list
|
212
|
+
- double double
|
213
|
+
- double_list double_list
|
214
|
+
- str str
|
215
|
+
- str_list str_list
|
216
|
+
```
|
217
|
+
|
218
|
+
`demo/generals.io.nmspec` contains a theoretical implementation of a messenger
|
219
|
+
for the game, [generals.io][generals.io]:
|
220
|
+
|
221
|
+
```yaml
|
222
|
+
version: 1
|
223
|
+
|
224
|
+
msgr:
|
225
|
+
name: generals.io
|
226
|
+
desc: demo nmspec file for generals.io
|
227
|
+
|
228
|
+
types:
|
229
|
+
- u8 player_id
|
230
|
+
- u8 serv_code
|
231
|
+
- str serv_msg
|
232
|
+
- u16 tile_id
|
233
|
+
- u8 terrain
|
234
|
+
|
235
|
+
protos:
|
236
|
+
- name: set_player_name
|
237
|
+
desc: client message sets the player name for a given player
|
238
|
+
msgs:
|
239
|
+
- str player_name
|
240
|
+
- name: resp_player_name
|
241
|
+
desc: server message to accept or reject the player name
|
242
|
+
msgs:
|
243
|
+
- serv_code resp_code
|
244
|
+
- serv_msg resp_msg
|
245
|
+
- name: set_player_id
|
246
|
+
desc: server message to client to set player id/color
|
247
|
+
msgs:
|
248
|
+
- player_id pid
|
249
|
+
- name: player_move
|
250
|
+
desc: client message to server to make a player move
|
251
|
+
msgs:
|
252
|
+
- tile_id from
|
253
|
+
- tile_id to
|
254
|
+
- u16 armies
|
255
|
+
- name: set_tile
|
256
|
+
desc: server message to client to set state of a tile
|
257
|
+
msgs:
|
258
|
+
- tile_id tid
|
259
|
+
- terrain ttype # 0 = hidden, 1 = blank, 2 = mountains, 3 = fort, 4 = home base
|
260
|
+
- player_id owner # 0 = no owner, 1 = player 1, 2 = player, etc.
|
261
|
+
- u16 armies
|
262
|
+
```
|
263
|
+
|
264
|
+
## How code is generated
|
265
|
+
|
266
|
+
Output program code is generated in the following manner:
|
267
|
+
|
268
|
+
1. `nmspec` file is read - the source YAML file is read
|
269
|
+
2. validity check - the YAML is checked to make sure it conforms to the `nmspec`
|
270
|
+
subset; useful errors and warnings in formatting may be added if mistakes are
|
271
|
+
found
|
272
|
+
3. If all is well, then the parsed YAML is converted into a data structure that
|
273
|
+
is designed to be easy for code generators to interpret
|
274
|
+
4. The data structure is passed on to one code generator per requested output
|
275
|
+
language
|
276
|
+
5. The resulting output code in all requested languages is gathered together and
|
277
|
+
returned to the user
|
278
|
+
|
279
|
+
## Preliminary research, and comparison to other methods
|
280
|
+
|
281
|
+
I started with researching how other people had designed network protocol
|
282
|
+
description languages/tools in the past, beginning with [Prolac][prolac]. This
|
283
|
+
lead me to other network messaging tools, binary serialization in general,
|
284
|
+
finally [Google's protocol buffers][protobuffs]. Protocol buffers were probably
|
285
|
+
the closest thing to what I wanted, and took care of binary
|
286
|
+
serialization/deserialization, but weren't packaaged with the networking layer,
|
287
|
+
which introduces additional considerations such as byte ordering, efficient
|
288
|
+
packet construction, TCP stack options, and communication retries and graceful
|
289
|
+
failover. While protocol buffers are a good design, and I think do a good job of
|
290
|
+
solving binary serialization as its own problem (becoming reusable for file I/O
|
291
|
+
as well as networks), I really wanted something that packaged serialization,
|
292
|
+
cross-language support, and TCP communication all in one package from a single
|
293
|
+
config file, so that a programmer needs only to write a single artifact (a
|
294
|
+
`.nmspec` file), and get the code for their target programming language(s)
|
295
|
+
generated automatically.
|
296
|
+
|
297
|
+
[ruby-lang]: https://www.ruby-lang.org/
|
298
|
+
[gdscript]: https://docs.godotengine.org/en/stable/getting_started/scripting/gdscript/gdscript_basics.html
|
299
|
+
[generals.io]: https://generals.io/
|
300
|
+
[prolac]: https://pdos.csail.mit.edu/archive/prolac/prolac-the.pdf
|
301
|
+
[protobuffs]: https://developers.google.com/protocol-buffers
|