python socketサーバーを作成して、マルチスレッドでcgiを利用する

##**はじめに**

cgiとは

>Common Gateway Interface（コモン・ゲートウェイ・インタフェース、CGI）は、ウェブサーバ上でユーザプログラムを動作させるための仕組み

[wikipedia](https://ja.wikipedia.org/wiki/Common_Gateway_Interface)より。
例えばLinuxコマンドのようなそのOS上で実行可能なプログラム・コマンドをHTTPを介して利用しよう！というものです。

私の知っている範囲では、cgiとして、**標準出力にHTTPレスポンスを出力すれば、出力内容をクライアントに送信してくれる**という仕組みが多いです。
lighttpdのcgiもそんな仕組みで、STDOUTをがっつり読み込んで結果を送信します。

シングルスレッドなのでこれでいいですが、マルチスレッドでcgiを実現するにはどうしよう。STDOUTは1つなのでバッティングしちゃうんですよね。
せっかくFDイベント待ち受けの仕組みがあるのでそれは活かしたいし、どうせならC言語以外も使いたいな
スクリプト言語でよくあるsocketの仕組みを使って設計してみるかってのが今回の主題になります。

##**案1:コマンドサーバーを作って実行してもらおう！**

ruby, python等のスクリプト言語で作るソケット通信例で、よく標準出力をソケットにリダイレクトする仕組みを見かけます。
そうか、HTTPサーバーはSTDOUTの代わりに別途作成したUnix Socketを待ち受けて、コマンド実行は別のサーバーに任せればいいか。
ということで設計。コマンドサーバーに実際のコマンドはお任せします。

<img width="510" alt="command.png" src="https://qiita-image-store.s3.amazonaws.com/0/252277/c94b5a48-86ab-6c61-1edd-3eb6a725e029.png">

サンプルもサクッと実装。備えの充実した言語、素晴らしいです。

`CommandService`というコマンドサーバー用クラスを作成。
HTTPサーバー⇒`send_to_server`を使ってsend
コマンドサーバー⇒`run_server`でrecv, 受信後コマンド実行, 応答送信という仕組みにしました。

ちなみに`CommandService`クラスにsend/recv両方導入したのは、コマンドサーバーが待ち受けるunix socketのファイルを隠ぺいしたかったから。
もちろんUnix socketの代わりにポートをあけて通信してもOKです。

2018/07/08 デリミタが","だとコマンド内で利用される可能性があったので、`self._delimiter`定義を追加しました。

```python:CommandService.py
#for socket
import os, sys, socket
#for exec command
import subprocess

class CommandService:

    #constructor, set socket path
    def __init__(self):
        self._socket_path="/var/run/command_service.sock"
        self._delimiter="__ComSvc__"

    #for server
    def run_server(self):
        print("Start server")
        waitsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        try:
            waitsock.bind(self._socket_path)
            while True:
                req_cmd_byte = waitsock.recv(1024)
                if not req_cmd_byte: break
                self._exec_request(req_cmd_byte.decode('utf-8').split(self._delimiter))
        except socket.error as msg:
            print(msg)
        except:
            print("Execute")
        finally:
            waitsock.close()
            os.unlink(self._socket_path)
            print("Exit server")

    #parse request, call command and send response
    def _exec_request(self, req_cmd):
        command=req_cmd[0]
        sockpath=req_cmd[1]
        print("comamnd:" + command)
        print("socket:" + sockpath)
        callresp = self._call_cmd(command.split(" "))
        if len(callresp) != 0 :
            self._send(callresp.encode('utf-8'), sockpath)

    #call system command
    def _call_cmd(self, args):
        try:
            res_byte = subprocess.check_output(args)
            res=res_byte.decode('utf-8')
        except:
            print('Failed, command:', args )
            res=''
        return res

    #for client
    def send_to_server(self, command, response_sockpath):
        request=command + self._delimiter + response_sockpath
        self._send(request.encode('utf-8'), self._socket_path)

    def _send(self, request, sockpath):
        sendsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        try:
            sendsock.sendto(request, sockpath)
            #sendsock.sendto(b"test, command", self._socket_path)
        except socket.error as msg:
            print(msg)
        finally:
            sendsock.close()
```

軽く解説を入れると、サーバー側`run_server`はこんな流れで動作しています。
1. `socket.socket`でソケット作成
2. `bind`で隠ぺいしているunix socketと紐づけ
3. `recv`
4. 受信データを`_exec_request`でコマンド実行。実際にコマンド実行している箇所は`_call_cmd`内`subprocess.check_output`で。["echo", "test"]みたいなコマンド引数の配列を入力としてコマンド実行します。
5. 結果を送信

注意点は以下です。
・`sendto`はbyteデータを扱うので`encode('utf-8')`や`b"文字列"`でbyte列に変換してあげること
・`recv`, `subprocess.check_output`は逆にbyte列で応答が返るのでdecodeしてあげること

後は以下のようにコマンドサーバーで`run_server`を実行する処理を書けばOK

```python:CommandServer.py
from CommandService import CommandService
def main():
    command=CommandService()
    command.run_server()

if __name__ == '__main__':
    main()
```

試しにechoコマンドを投げる以下のような`CommandClient`と、HTTPサーバー側の代わりに/tmp/resp_sockを待ち受けるサーバーをコピペで作成(省略)してテスト。環境内のpythonのバージョンはpython3.6です。

```python:CommandClient.py
from CommandService import CommandService
def main():
    command=CommandService()
    command.send_to_server("echo -n test", "/tmp/resp_sock")

if __name__ == '__main__':
    main()
```

サーバーを起動して`CommandClient.py`を実行すると、こんな感じに出力が出ます。
無事/tmp/resp_sock側には`echo -n test`の出力結果が渡りました。

```console:CommandServer.py側
# python3.6 CommandServer.py
Start server
comamnd:echo -n test
socket:/tmp/resp_sock
```

```console:/tmp/resp_sock待ち受け側
# python3.6 TestService.py
Start server
test
```

2018/07/07 抜け漏れがあったので続きを記載
[python 環境変数, stdinを制御してコマンドを実行する。](https://qiita.com/developer-kikikaikai/items/1a86b4a9faf5e52b8036)

##**案2:forkしてコマンド実行すればよくね？**

記事を書きつつ仕様を整理しながら思いました。これサーバーまで作る必要なくね？と。
…popenなりで直接`_exec_request`の処理を呼べるようにすりゃいいじゃん。

まあ自分は`fork`があまり好きじゃないので案1で行きますが。
いずれにせよ実現性が見えたのでC側は明日にしよう。満足したので寝ます

##**参考**
socket参考:
[PythonでUnixドメインソケットを使って通信する](https://tokibito.hatenablog.com/entry/20150927/1443286053)

リファレンス
https://docs.python.org/ja/3.6/library/socket.html#socket.socket.sendto

注意点の参考
[対処方法 TypeError: a bytes-like object is required, not 'str'](https://teratail.com/questions/26231)