Pythonのasyncioを用いたUNIXドメインソケットでreadが終わらない事例

# 概要

+ Python3では、[asyncio](https://docs.python.org/ja/3/library/asyncio.html "asyncio --- 非同期 I/O — Python 3.8.1 ドキュメント")モジュールを利用することで**UNIXドメインソケット**による**プロセス間通信**(IPC)が可能である。
+ クライアントプロセスからソケットに書き込まれたバイト列を読み込む際に、書き込みが終わっているにもかかわらず**サーバープロセス側の読み込みが終わらない**という事態が発生した。
+ **書き込み終了時に`write_eof()`を呼び出す**ことでサーバープロセス側の読み込みが終了した。

# 環境

+ Windows Subsystem for Linuxを使用
    + ホストOS: Windows 10 Home 64bit
    + ゲストOS: Ubuntu 18.04.3 LTS
+ Python 3.8.0


#事例

　Responderで作成するサイトと自作の計算プログラムのプロセス間通信にUNIXドメインソケットを使おうと考え、前段階としてソケットの挙動を確認するスクリプトを書いていました。
　テストの処理の流れは

1. ```asyncio.start_unix_server(コールバック, ソケットのパス)```でソケットを作成し、サーバーを起動する
1. クライアントは```asyncio.open_unix_connection(ソケットのパス)```でサーバーと接続する
1. クライアントがソケットに小文字の文字列を書き込む
1. サーバーがソケットから小文字の文字列を読み込む
1. 読み込んだ文字列を大文字に変換する
1. 変換した文字列をソケットに書き込む
1. クライアントがソケットから大文字の文字列を読み込んで表示する

としました。
***
クライアントサイド：

```python:socket_client_badcase.py
import asyncio
import datetime

def now():
    return "[{}]".format(datetime.datetime.now().time())

async def client(message):
    "socketサーバーに文字列を送って応答を読みだす"
    # コネクションの作成
    reader, writer = await asyncio.open_unix_connection(path="./sock")

    #メッセージの送信
    writer.write(message.encode())
    print(now(), "send {}".format(message)
    
    await writer.drain()

    # メッセージの受信
    res = await reader.read(-1)
    print(now(), "receive {}".format(res))

    # 処理の終了
    writer.close()
    await writer.wait_closed()

if __name__ == "__main__":
    asyncio.run(client("hogehoge"))
```

サーバーサイド：

```python:socket_server.py
import asyncio
import datetime
import os

def now():
    return "[{}]".format(datetime.datetime.now().time())

async def handler(reader, writer):
    """
    ドメインソケットに接続があった時のコールバック関数
    受信した文字列を大文字にして返す
    """
    print(now(), "handler called.")

    # メッセージの受信
    data = await reader.read(-1) # 引数を-1にするとEOFまで読む
    print(now(), "recieve {}".format(data))

    # メッセージの処理と返信
    res = str(data.decode()).upper()
    writer.write(res.encode())
    await writer.drain()
    print(now(), "write complete.")

    #終了処理
    writer.close()
    await writer.wait_closed()

async def main():
    #ソケットの作成と読み書き権限の付与
    server = await asyncio.start_unix_server(handler, path="./sock")
    os.chmod("./sock", 0o600)

    #サーバーの起動
    async with server:
        await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())
```
***
実行結果：

```:ターミナル1
user@DESKTOP-XXX:~$ python socket_client_badcase.py
[11:39:59.574727] send hogehoge
```

```:ターミナル2
user@DESKTOP-XXX:~$ python socket_server.py
[11:39:59.574847] handler called.
```

　このように、ソケットへのメッセージの書き込みは終了し、メッセージハンドラも呼び出されているものの、サーバーサイドでのソケットからの読み込みが終わらず処理がストップしてしまいました。

# 解決方法
　[公式リファレンスのStreamsのページ](https://docs.python.org/ja/3/library/asyncio-stream.html "Streams — Python 3.8.1 ドキュメント")を読み返すと、

>*class* asyncio.**StreamWriter**
>　　(中略)
>**write_eof()**
>　　Close the write end of the stream after the buffered write data is flushed.

(https://docs.python.org/ja/3/library/asyncio-stream.html より引用)

という記述がありました。書き込み終了時にこれを呼び出し、読み込んでいる側にファイルの終わりを伝える必要があるようです。

　これを踏まえてクライアントサイドのスクリプトを書き直すと：

```python:socket_client.py
import asyncio
import datetime

def now():
    return "[{}]".format(datetime.datetime.now().time())

async def client(message):
    "socketサーバーに文字列を送って応答を読みだす"
    # コネクションの作成
    reader, writer = await asyncio.open_unix_connection(path="./sock")

    #メッセージの送信
    writer.write(message.encode())
    print(now(), "send {}".format(message))
    writer.write_eof() # ここを追加
    await writer.drain()

    # メッセージの受信
    res = await reader.read(-1) # 引数を-1にするとEOFまで読む
    print(now(), "receive {}".format(res))

    # 処理の終了
    writer.close()
    await writer.wait_closed()

if __name__ == "__main__":
    asyncio.run(client("hogehoge"))
```

***

実行結果：

```:ターミナル1
user@DESKTOP-XXX:~$ python socket_client.py
[12:14:17.147107] send hogehoge
[12:14:17.153610] receive b'HOGEHOGE'
```

```:ターミナル2
user@DESKTOP-XXX:~$ python socket_server.py
[12:14:17.147750] handler called.
[12:14:17.150608] recieve b'hogehoge'
[12:14:17.151640] write complete.
```

このようにクライアントとサーバーの間でデータのやり取りが完了しました。

#まとめ
Pythonのasyncioモジュールを利用したUNIXドメインソケットでは、書き込み終了を読み込んでいるプロセスに伝えるために```write_eof()```を呼び出す必要があります。これにより、読み出し側に書き込みの終了が通知されるようです。

#後書き
　初投稿です。脱ROM専:v: