[Python] SocketにおけるBlocking、Non-blocking、Timeoutの違い

Pythonの`socket.send()`や`socket.recv()`メソッドによってデータを送受信する際に、リソースのブロッキングを管理することができます。管理の種類として主にblocking、non-blocking、timemoutがあります。その他にも`select`や`selectors`モジュールで多重化I/Oを組み合わせて管理する方法もあります。前に書いた記事を参考にして下さい ➔ [[Python] selectとselectorsモジュールによる多重化I/O - Qiita](https://qiita.com/homines22/items/a161ac652d9585d7273a)
今回はblocking、non-blocking、timemoutの3つのモードについて掘り下げたいと思います。

OSはLinuxを対象とし、Pythonのバージョンは3.6を対象とします。細かいところはバージョンによって違いますが、3系なら基本的な部分は変わらないでしょう。
しかし、OSによってソケットのプロセスフローが変わるのでご注意を！

# 送受信の流れ
かなり省略して説明します。
[Chapter 6. I/O Multiplexing: The select and poll Functions - Shichao's Notes](https://notes.shichao.io/unp/ch6/)で図などを用いて説明されているので、詳細を知りたい方は参照してくだい。

## 送信
1. `socket.send()`を実行することでシステムコールを発行
2. リソースをブロック  
   この時は他のファイルI/Oの操作が不能となります
3. kernel-bufferが書き込み待機
4. ユーザによってデータ入力
5. kernel-bufferにコピー
6. 送信

## 受信
1. `socket.recv()`を実行することでシステムコールを発行
2. リソースをブロック  
   この時は他のファイルI/Oの操作が不能となります
3. kernel-bufferが読み込み待機
4. 受信
3. 受信したデータをkernel-buffer(カーネル空間)に格納
4. ユーザ空間(アプリケーション)にデータコピー

# 各モードの説明
話をシンプルにするために受信における動作の違いにフォーカスします。

## Blocking
ソケットオブジェクトのデフォルトモードはBlockingです。
Blockingモードはkernel-bufferに読み込み可能なデータが到着するまでリソースをブロックします。メインプロセス自体が強制終了、または例外が発生するまでブロックし続けます。

Blocking関連のメソッドを以下に挙げました。

|メソッド|説明|
|:--:|:--:|
|socket.setblocking(flag)|flagがTrueならBlocking、FalseならNon-blockingモードに設定|

## Non-blocking
Non-blockingモードは読み込み可能なデータがなければ即座に`EWOULDBLOCK`というエラーを吐いてリソースをリリースします。読み込み可能なデータがあれば、kernel-bufferからユーザ空間にデータがコピーされます。
このような特徴からNon-blockingモードはI/O Multiplexingと併用されます。

Non-blockingモードを設定する方法は、Blockingモードで紹介した`socket.setblocking(False)`を使用するか、ソケットオブジェクト作成時にtype引数に値を渡す方法です。

- socket.setblocking(False)

```python

>>> import socket
>>> socket.SOCK_STREAM
<SocketKind.SOCK_STREAM: 1>
>>> socket.SOCK_NONBLOCK
<SocketKind.SOCK_NONBLOCK: 2048>
>>> sock = socket.socket()
>>> sock
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
>>> sock.setblocking(False)
>>> sock
<socket.socket fd=3, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('0.0.0.0', 0)>
```

`socket.setblocking`メソッドでNon-blockingモードに設定すると、ソケットオブジェクトのtypeが`2049`となりました。

- type引数

```python

>>> import socket
>>> socket.socket(type=(socket.SOCK_STREAM | socket.SOCK_NONBLOCK))
<socket.socket fd=3, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('0.0.0.0', 0)>
```

ビット演算`socket.SOCK_STREAM | socket.SOCK_NONBLOCK`によってNon-blockingを設定します。作成されたソケットオブジェクトのtypeが`2049`となり、`socket.setblocking`メソッドと同様の結果になりました。

## Timeout
blockingモードと併用して使用します。
指定した時間が経過する(プロセスがタイムアウトする)まで、リソースをブロックして読み込み可能なデータが到着するまで待機します。
タイムアウトしたら、`socket.timeout`という例外が発生します。この例外はversion 3.3からOSErrorのサブクラスとなりました。

> socket_timeout = PyErr_NewException("socket.timeout", PyExc_OSError, NULL);[^1]

Timeout関連のメソッドを以下に挙げてみました。

|メソッド|説明|
|:--:|:--:|
|socket.getdefaulttimeout()|新規に作成されたsocketオブジェクトのtimeout値を取得|
|socket.setdefaulttimeout(timeout)|新規に作成されるsocketオブジェクトのtimeout値を設定|
|socket.gettimeout()|特定のsocketオブジェクトのtimeout値を取得|
|socket.settimeout(value)|特定のsocketオブジェクトのtimeout値を設定|

`socket.setdefaulttimeout`と`socket.settimeout`で渡す引数はfloat型で単位は秒となります。
`None`を引数として渡すとtimeoutは無効になります。つまり、通常のblockingモードとなるわけです。
`0`を指定すればnon-blockingモードとなります。

`default`が含まれている方は全てのsocketオブジェクトに共通したメソッドです。
逆に含まれていない方は作成されたsocketオブジェクト毎に個別に実行できるメソッドです。サーバーサイドではクライアントごとにソケットが生成されるので、これらのメソッドを使用することでクライアントごとの制御が可能となります。

# 参考
- [Chapter 6. I/O Multiplexing: The select and poll Functions - Shichao's Notes](https://notes.shichao.io/unp/ch6/)
- [18.1. socket — 低水準ネットワークインターフェイス — Python 3.6.5 ドキュメント](https://docs.python.jp/3/library/socket.html)

[^1]: cpython/socketmodule.c at 3.6 · python/cpython 
 (https://github.com/python/cpython/blob/3.6/Modules/socketmodule.c#L6607)