Daemon/client protocol proposal issue #11

Open
opened 2026-10-19 13:48:50 +02:00 by kayprish · 0 comments
Owner

Daemon/client protocol proposal issue

Let's discuss in the issue comments below.

Definition of done:

  • When all sections marked as "unresolved" have been commented on below this issue and an agreement has been reached regarding each dilemma in the protocol definition.
  • A "completed" version of this markdown definition has been merged as a source file in the resshko repo

Purpose of the daemon/client protocol

We've come to an agreement that entirely separate resshko processes, each one loading it's own copy of the dictionaries, or simply rereading and refiltering the same disk data for every request is unreasonable (one of these if wasteful in terms of memory, the other in terms of processor time and IO).

However, every time a user logs into the system over SSH, a new process is spawned, usually started from the same executable.

For this problem, several solutions could be proposed (resolved dilemma, but listed here for the curious):

  1. One server process and slave clients (The solution we're going with)
  2. All processes use a shared embedded database, such as SQLite
  3. First spawned process loads the JSON into an object in shared memory, all other process manage the shared memory together
  4. various others

All of these have pros and cons, but we'll only address the first approach here, the biggest con is of course that we have to create a completely different server/daemon program which we have to manage in the codebase. However, the pros are significant:

  1. Odin's TUI or SSH interface libraries suck, so we can write the clients in Go, which has excellent support for creating elegant UIs over SSH, so we can leverage both Go and Odin for what each one is good at
  2. The library we were planning to use for SSH required a thin Go process anyway
  3. The master daemon makes cross-session features such as score boards, accounts and even chat much simpler to implement

Therefore we're going with solution 1.

+-------------------------------------------------------+
| SERVER                                                |
|                                                       |
|  +-------------------------------------------------+  |
|  | [B] DAEMON PROCESS (Written in Odin)            |  |
|  |-------------------------------------------------|  |
|  | 1. Loads giant JSON once into memory            |  |
|  | 2. Listens for connections on /tmp/game.sock    |  |
|  |    (or another method)                          |  |
|  | 3. Queries data for clients                     |  |
|  +-------------------------^-----------------------+  |
|                            |                          |
|                            | UNIX DOMAIN SOCKET       |
|                            | (IPC)                    |
|                            |                          |
|  +-------------------------v-----------------------+  |
|  | [C] CLIENT PROCESSES (Written in Go)            |  |
|  | (Thin clients spawned for each SSH session)     |  |
|  +-------------------------------------------------+  |
|                                                       |
| +-----------+       +-----------+       +-----------+ |
| | [A] User 1|       | [A] User 2|       | [A] User N| |
| | SSH Client|       | SSH Client|       | SSH Client| |
| +-----^-----+       +-----^-----+       +-----^-----+ |
|       |                   |                   |       |
|       |                   |                   |       |
|       |                   |                   |       |
|       |                   |                   |       |
|       | SSH Connection 1  | SSH Connection 2  | ...   |
|       |                   |                   |       |
+-------|-------------------|-------------------|-------+
        |                   |                   |
  +-----|-----+       +-----|-----+       +-----|-----+
  | [A] User 1|       | [A] User 2|       | [A] User N|
  | SSH Client|       | SSH Client|       | SSH Client|
  +-----------+       +-----------+       +-----------+

[A] Users connect to the server via SSH.
[B] The persistent DAEMON (Odin) holds all the data and manages the game logic.
[C] The thin CLIENT (Go) is spawned per SSH session, communicates with the DAEMON, and renders output to the user's terminal.

However, because we're creating two different types of process that need to communicate with each other we need to come up with a communication protocol.

Parts of the definition

I think a useful way to think about defining the protocol is to think of the definition in 2 or 3 layers (this isn't TCP/IP networking so let's not get too crazy):

  1. System layer (which OS facilities will be used, sockets, signals, shared memory, named pipes, message queues, TCP/IP loopback, etc.)
  2. General data format (will we be sending JSON objects, s-expressions, binary blobs, or a kind of bespoke text format)
  3. Specific commands/messages/response data formatting

Also, we have to clearly define the general "flow of conversation" that we want to achieve.

Flow of conversation (UNRESOLVED)

The client sends requests, the server responds with data:

The following graph covers what the game should essentially look like for the involved processes.

Question: does this flow seem like it generally makes sense, is there something important to the gameplay that's not covered here, discuss in the comments.

Note: In the following diagram, the letters represent: G-green, Y-yellow,
g-grey.  However in the real protocol these might be coded as digits, whole
words, or even just bitfields.
Client                             Server
 startsession        ------->
         Server sets up session data like:
         user id (maybe IP, maybe key used to log in)
         (maybe returns the game options??)
                     <-------        ACK
 startgame(RESSHKO-DAILY)
                     ------->
         Server sets up initial game state
         Finds what the daily solutions word is, etc.
         Let's say it's `OSEKA`
                     <-------        ACK
 check(OKEAN)        ------->
                     <-------      GYGYg 
 check(PUOUO)        ------->
                     <------- INVALID WORD
 check(OSEKA)        ------->
         Server marks as victory in long term memory,
         records number of attempts, etc.
                     <-------      GGGGG 
 endsession          ------->
         Server closes the communication channel, if
         the client hasn't already done so.
                     <-------        ACK

System layer (UNRESOLVED)

Here, we're concerned with which system facilities we will be using to facilitate the communication. And there are plenty of options:

  1. Unix sockets
  2. Shared memory solutions
  3. Named pipes
  4. Message queues
  5. TCP/IP loopback

My current proposition: Unix sockets

First I'll elaborate most of the options, then I'll argue for Unix sockets, but keep in mind each one of these options can be split into at least 2 or 3 sub-options, for example there are 4 different ways to use shared memory for communication on Linux, 2 types of message queues, etc.

  1. Unix sockets: there are several options here, stream sockets which are more similar to general files/streams of bytes, messages are not delineated unless we explicitly delineate them, and datagram (UDP) sockets where they are, so the UDP ones function a lot like message queues.
  2. Shared memory is when a single page of memory is marked as mapped to multiple different processes, this allows for any process to communicate to others simply by writing in the same piece of memory, where the change will become instantly visible. Because it bypasses system calls and context switching it's primarily used when you want to exchange very large amounts of data very fast, which is not out use case, also coordination is manual unlike streams, pipes or sockets, so it puts more burden on the programmer. It sounds like an interesting learning possibility though.
  3. Essentially a special in-memory file which one process can read, and another can write to, which (if my understanding is correct), means you have to open two for a two way communication, sounds appropriate, but doesn't sounds like we'd learn much
  4. Message queues: there are two types of these on Linux, System V and POSIX, POSIX is the flashy/new (if Linux 2.6.6/2004 is new to you) system. And in general, it's better designed, has more features, so I see no reason to use the old one. Generally, message queues allow you to send out discrete messages to other processes, they can be assigned priorities in POSIX and integer types in SysV (although I think we won't need this), one queue can be listened to by multiple processes (might not need this either), POSIX also has features that allow a process to be asynchronously notified when a message is available on a queue.
  5. TCP/IP loopback: The idea here is that our server process is an actual internet server to the others (albeit, firewalled off the rest of the web), we could come up with a TCP/IP application protocol and use it for bidirection communication, I guess it seems interesting, we might a bit of network programming, but I'm not sure how much considering that a loopback interface seems pretty simple to work with, but I don't know exactly what it entails

Anyway, message queues seem to me to be a good learning opportunity, while also not being very complicated and pretty appropriate to our task, so I would recommend them, Unix sockets share a lot of the same advantages but they were my second pick.

However, Unix sockets have by far the best support in both Odin and Go, while message queues have by far the worst, so Unix Sockets may in fact be the best option.

General data format (UNRESOLVED)

We could send the messages over Unix Sockets by passing JSON objects in a certain format, or XML objects, s-expressions, or some other "object-notation format". However I would propose that we use a simple bespoke text format which might be easier to parse and work with.

Perhaps a simple "one line" textual message could represent both the request and response in each case, for example:

There might be several options for what commands the client could issue, for example:
startsession
startgame:NAME-OF-GAME
check:WORD

This general format seems like it would be simple to parse.

Specific commands (UNRESOLVED)

Here, I will give the specific commands I propose, I will be using the bespoke textual format, but obviously, this same logic could be implemented with JSON or something else.
Clients sends:

startsession\n

Server responds:

ACK\n

Client could respond with one of:

startgame:RESSHKO-DAILY\n
startgame:RESSHKO-RANDOM\n

anything else is an error, at least until we implement more games.

Server then responds with:

ACK\n

After this, the protocol enters a game state, where different command options are allowed or not allowed depending on the specific game

Here, I'll give the example of a RESSHKO-DAILY run:

check:WORD\n

Where WORD is a five-letter Cyrillic word, the server keeps a counter of how
many of these are issued, and responds with one of:

win:GGGGG\n
notwin:XXXXX\n (where X is one of G,y,g, but the combination cannot be GGGGG)
lose:XXXXX\n   (again, can't be GGGGG, this message is sent when the attempt
                counter reaches the maximum)

Client responds with:

ACK\n

In the end, a client will send an

end-game

and

end-session

To stop communication with the server, if this happens, or if the server notices a dropped connection with the client, in either case, the server will record the player results in long-term memory.

# Daemon/client protocol proposal issue Let's discuss in the issue comments below. ## Definition of done: - [ ] When all sections marked as "unresolved" have been commented on below this issue and an agreement has been reached regarding each dilemma in the protocol definition. - [ ] A "completed" version of this markdown definition has been merged as a source file in the resshko repo # Purpose of the daemon/client protocol We've come to an agreement that entirely separate resshko processes, each one loading it's own copy of the dictionaries, or simply rereading and refiltering the same disk data for every request is unreasonable (one of these if wasteful in terms of memory, the other in terms of processor time and IO). However, every time a user logs into the system over SSH, a new process is spawned, usually started from the same executable. For this problem, several solutions could be proposed (resolved dilemma, but listed here for the curious): 1. One server process and slave clients (The solution we're going with) 2. All processes use a shared embedded database, such as SQLite 3. First spawned process loads the JSON into an object in shared memory, all other process manage the shared memory together 4. various others All of these have pros and cons, but we'll only address the first approach here, the biggest con is of course that we have to create a completely different server/daemon program which we have to manage in the codebase. However, the pros are significant: 1. Odin's TUI or SSH interface libraries suck, so we can write the clients in Go, which has excellent support for creating elegant UIs over SSH, so we can leverage both Go and Odin for what each one is good at 2. The library we were planning to use for SSH required a thin Go process anyway 3. The master daemon makes cross-session features such as score boards, accounts and even chat much simpler to implement Therefore we're going with solution 1. ``` +-------------------------------------------------------+ | SERVER | | | | +-------------------------------------------------+ | | | [B] DAEMON PROCESS (Written in Odin) | | | |-------------------------------------------------| | | | 1. Loads giant JSON once into memory | | | | 2. Listens for connections on /tmp/game.sock | | | | (or another method) | | | | 3. Queries data for clients | | | +-------------------------^-----------------------+ | | | | | | UNIX DOMAIN SOCKET | | | (IPC) | | | | | +-------------------------v-----------------------+ | | | [C] CLIENT PROCESSES (Written in Go) | | | | (Thin clients spawned for each SSH session) | | | +-------------------------------------------------+ | | | | +-----------+ +-----------+ +-----------+ | | | [A] User 1| | [A] User 2| | [A] User N| | | | SSH Client| | SSH Client| | SSH Client| | | +-----^-----+ +-----^-----+ +-----^-----+ | | | | | | | | | | | | | | | | | | | | | | | SSH Connection 1 | SSH Connection 2 | ... | | | | | | +-------|-------------------|-------------------|-------+ | | | +-----|-----+ +-----|-----+ +-----|-----+ | [A] User 1| | [A] User 2| | [A] User N| | SSH Client| | SSH Client| | SSH Client| +-----------+ +-----------+ +-----------+ [A] Users connect to the server via SSH. [B] The persistent DAEMON (Odin) holds all the data and manages the game logic. [C] The thin CLIENT (Go) is spawned per SSH session, communicates with the DAEMON, and renders output to the user's terminal. ``` However, **because we're creating two different types of process that need to communicate with each other we need to come up with a communication protocol.** # Parts of the definition I think a useful way to think about defining the protocol is to think of the definition in 2 or 3 layers (this isn't TCP/IP networking so let's not get too crazy): 1. System layer (which OS facilities will be used, sockets, signals, shared memory, named pipes, message queues, TCP/IP loopback, etc.) 2. General data format (will we be sending JSON objects, s-expressions, binary blobs, or a kind of bespoke text format) 3. Specific commands/messages/response data formatting Also, we have to clearly define the general "flow of conversation" that we want to achieve. # Flow of conversation (UNRESOLVED) The client sends requests, the server responds with data: The following graph covers what the game should essentially look like for the involved processes. Question: does this flow seem like it generally makes sense, is there something important to the gameplay that's not covered here, discuss in the comments. ``` Note: In the following diagram, the letters represent: G-green, Y-yellow, g-grey. However in the real protocol these might be coded as digits, whole words, or even just bitfields. Client Server startsession -------> Server sets up session data like: user id (maybe IP, maybe key used to log in) (maybe returns the game options??) <------- ACK startgame(RESSHKO-DAILY) -------> Server sets up initial game state Finds what the daily solutions word is, etc. Let's say it's `OSEKA` <------- ACK check(OKEAN) -------> <------- GYGYg check(PUOUO) -------> <------- INVALID WORD check(OSEKA) -------> Server marks as victory in long term memory, records number of attempts, etc. <------- GGGGG endsession -------> Server closes the communication channel, if the client hasn't already done so. <------- ACK ``` # System layer (UNRESOLVED) Here, we're concerned with which system facilities we will be using to facilitate the communication. And there are plenty of options: 1. Unix sockets 2. Shared memory solutions 3. Named pipes 4. Message queues 5. TCP/IP loopback My current proposition: **Unix sockets** First I'll elaborate most of the options, then I'll argue for Unix sockets, but **keep in mind** each one of these options can be split into at least 2 or 3 sub-options, for example there are 4 different ways to use shared memory for communication on Linux, 2 types of message queues, etc. 1. Unix sockets: there are several options here, stream sockets which are more similar to general files/streams of bytes, messages are not delineated unless we explicitly delineate them, and datagram (UDP) sockets where they are, so the UDP ones function a lot like message queues. 2. Shared memory is when a single page of memory is marked as mapped to multiple different processes, this allows for any process to communicate to others simply by writing in the same piece of memory, where the change will become instantly visible. Because it bypasses system calls and context switching it's primarily used when you want to exchange very large amounts of data very fast, which is not out use case, also coordination is manual unlike streams, pipes or sockets, so it puts more burden on the programmer. It sounds like an interesting learning possibility though. 3. Essentially a special in-memory file which one process can read, and another can write to, which (if my understanding is correct), means you have to open two for a two way communication, sounds appropriate, but doesn't sounds like we'd learn much 4. Message queues: there are two types of these on Linux, System V and POSIX, POSIX is the flashy/new (if Linux 2.6.6/2004 is new to you) system. And in general, it's better designed, has more features, so I see no reason to use the old one. Generally, message queues allow you to send out discrete messages to other processes, they can be assigned priorities in POSIX and integer types in SysV (although I think we won't need this), one queue can be listened to by multiple processes (might not need this either), POSIX also has features that allow a process to be asynchronously notified when a message is available on a queue. 5. TCP/IP loopback: The idea here is that our server process is an actual internet server to the others (albeit, firewalled off the rest of the web), we could come up with a TCP/IP application protocol and use it for bidirection communication, I guess it seems interesting, we might a bit of network programming, but I'm not sure how much considering that a loopback interface seems pretty simple to work with, but I don't know exactly what it entails Anyway, message queues seem to me to be a good learning opportunity, while also not being very complicated and pretty appropriate to our task, so I would recommend them, Unix sockets share a lot of the same advantages but they were my second pick. **However**, Unix sockets have by far the best support in both Odin and Go, while message queues have by far *the worst*, so Unix Sockets may in fact be the best option. # General data format (UNRESOLVED) We could send the messages over Unix Sockets by passing JSON objects in a certain format, or XML objects, s-expressions, or some other "object-notation format". However I would propose that we use a simple bespoke text format which might be easier to parse and work with. Perhaps a simple "one line" textual message could represent both the request and response in each case, for example: ``` There might be several options for what commands the client could issue, for example: startsession startgame:NAME-OF-GAME check:WORD This general format seems like it would be simple to parse. ``` # Specific commands (UNRESOLVED) Here, I will give the specific commands I propose, I will be using the bespoke textual format, but *obviously, this same logic could be implemented with JSON or something else*. Clients sends: ``` startsession\n ``` Server responds: ``` ACK\n ``` Client could respond with one of: ``` startgame:RESSHKO-DAILY\n startgame:RESSHKO-RANDOM\n ``` anything else is an error, at least until we implement more games. Server then responds with: ``` ACK\n ``` After this, the protocol enters a game state, where different command options are allowed or not allowed depending on the specific game Here, I'll give the example of a RESSHKO-DAILY run: ``` check:WORD\n ``` Where WORD is a five-letter Cyrillic word, the server keeps a counter of how many of these are issued, and responds with one of: ``` win:GGGGG\n notwin:XXXXX\n (where X is one of G,y,g, but the combination cannot be GGGGG) lose:XXXXX\n (again, can't be GGGGG, this message is sent when the attempt counter reaches the maximum) ``` Client responds with: ``` ACK\n ``` In the end, a client will send an ``` end-game ``` and ``` end-session ``` To stop communication with the server, if this happens, or if the server notices a dropped connection with the client, in either case, the server will record the player results in long-term memory.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
brk/sshechko#11
No description provided.