systemdのsocketを使ってserverを作る


## 論よりコード

TCP localhost:18080 で待ち受けて、 `implementation.bash` を実行した結果を journal に書き込む例です。

※ systemd は一般ユーザ権限でも利用可能です。今回は一般ユーザ権限で行いました。
~~その際、systemd 関連のファイルは `$HOME/.config/systemd/user/` に配置する必要があります。ない場合は `mkdir -p $HOME/.config/systemd/user/` として作ってからファイルを配置してください。~~
→ 実は `systemctl --user enable $PWD/server.socket` と、パスを指定すれば自動的にディレクトリを作ってくれてsymlinkでファイルを配置してくれることがわかりました。

### `server.socket` ファイル

`server.socket` ファイルはSocket を作成して待ち受けをし、クライアントから接続を受け付けたら `server@.service` を呼び出します。 (man [systemd.socket](https://www.freedesktop.org/software/systemd/man/systemd.socket.html))

`server@.service` というファイル名は `Service =` で指定可能です。指定しない場合は `.socket` のファイル名から導出されます(今回の例だと `server`)。

このファイルは `systemctl --user enable` コマンドで自動的に `$HOME/.config/systemd/user/` に配置されます。

```ini:server.socket
[Unit]
Description = Simple TCP server (listener)

[Socket]
ListenStream = 127.0.0.1:18080
Accept = yes

[Install]
WantedBy=sockets.target
```

### `server@.service` ファイル

ファイル名の `@` はテンプレートユニットであることの宣言となります。 `**.socket` からの呼び出し毎に fork するような形で実体が作られて実行されます。

このファイルは `systemctl --user enable` コマンドで自動的に `$HOME/.config/systemd/user/` に配置されます。

```ini:server@.service
[Unit]
Description = Simple TCP server (implementation)

[Service]
Environment = LANG=C
ExecStart = /bin/bash /home/USER/implementation.bash
StandardInput = socket
StandardOutput = journal
StandardError = journal

[Install]
WantedBy = server.socket
```

`[Install]` の `WantedBy =` は、呼び出し元である `server.socket` を指定します。

`StandardInput` は、 `implementation.bash` への標準入力のソースを指定しています。
_socket_ となっていれば `**.socket` で受け付けた接続の入力を STDIN として渡すことができます。

`StandardOutput` および `StandardError` は `implementation.bash` の出力をどこに返すかという指定です。
_socket_ となっていれば `**.socket` で受け付けた接続に返します。 _journal_ となっていれば `journalctl` で見ることができます。
その他出力先の指定は [systemd.exec](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#StandardOutput=) をご覧ください。複数同時は指定できないようです。

### 実装部

今回は bash で実装しました。socketからの入力は `**.service` の `StandardInput` を経由して標準入力に渡されますので `read` で読み込むことができます。

このファイルは `server@.service` の `ExecStart` で指定されています。

```bash:$HOME/implementation.bash
#!/bin/bash

read INPUT
echo "Your input is $INPUT"
echo -n "Today is "
date
exit 0
```

### systemd へ登録する

３つのファイルの配置が終わったら `systemctl` を使って systemd に登録します。start するのは .socket のみです。

```console
$ systemctl --user enable $PWD/server@.service
$ systemctl --user enable $PWD/server.socket
$ systemctl --user start server.socket
```

journalctl で確認してみます。

```console
$ journalctl -n 1
-- Logs begin at Thu 2019-11-07 21:17:45 JST, end at Fri 2019-11-08 00:00:30 JST. --
Nov 08 00:00:30 raspberrypi systemd[506]: Listening on Simple TCP server (listener).
```

### テスト

```console
$ echo "hello!" | nc 127.0.0.1 18080
$ journalctl -n 4
-- Logs begin at Thu 2019-11-07 21:17:45 JST, end at Fri 2019-11-08 00:01:39 JST. --
Nov 08 00:01:39 raspberrypi systemd[506]: Started Simple TCP server (implementation) (127.0.0.1:50772).
Nov 08 00:01:39 raspberrypi bash[8284]: Your input is hello!
Nov 08 00:01:39 raspberrypi bash[8284]: Today is Fri Nov  8 00:01:39 JST 2019
Nov 08 00:01:39 raspberrypi systemd[506]: server@0-127.0.0.1:18080-127.0.0.1:50772.service: Succeeded.
```

### `server@.service` や実装部の変更の反映

`server@.service` は `server.socket` からの新規接続のタイミングで都度読み込まれます。そのため `server@.service` の変更は次回呼び出しから即適用されます。実装部も同様です。

ここから先はどうでもいい話です。

## サーバの実装方法について

TCP/UDPやUnix Domain Socketといった待ち受けをするサーバを構築する方法は、概ね以下の通りです。

* [listen(2)](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/listen.2.html) を利用
    * 待ち受け部、実装部を包括して実現する自己完結モデル
* [inetd(8)](https://linuxjm.osdn.jp/html/netkit/man8/inetd.8.html) を利用
    * 待ち受け部を inetd が担当し、実装部は STDIN/STDOUT を介して実行されるプログラムとなる分業モデル

今回の systemd によるサーバの実現は inetd(8) のモダン実装ということになります。

### listen(2) を利用する場合

たとえば Ruby で書く場合はこんな感じです。極力 `implementation.bash` と合わせてみました。

```ruby
require "socket"
server = TCPServer.open("127.0.0.1", 18080)
while true
  socket = server.accept
  Thread.start(socket) do |s|
    while buffer = socket.gets
      socket.puts "Your input is #{buffer}"
      socket.puts "Today is #{Time.now}"
    end
    socket.close
  end
end
server.close
```

### 考察

listen(2) を利用する場合は何から何まで自力実装が可能であるため、細かい実装やケイパビリティ、速度面でのメリットが得られやすい。その代わり「実装しなければならない」という問題もある。

今回の systemd.socket を利用する場合は、例えば [MaxConnectionsPerSource](https://www.freedesktop.org/software/systemd/man/systemd.socket.html#MaxConnectionsPerSource=) といった設定があるため、それを利用するだけで良いが、どの程度受けられるかは未知数であるため、要検証。

## どのような時に必要となるの？

いわゆるプロセス間通信を容易に実現する方法ということになります。これにより、「最初は定時起動する要件だけだったけど、他からの割込みで起動する要件を追加したい」に対しても容易に増やすことできます。まさにマイクロサービス的なことが可能となるわけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22754/24371af0-be1e-0455-f891-81e70bf8c828.png)


## あとがき

ファイルが多いのが難点だけど、慣れると結構便利。
一番のポイントは、全部ファイル名を同じにして、拡張子だけが異なる状態が管理しやすい。

さて、、、仕事するか。

EoT