ソケットの分類
この分類は異なる使われ方のソケットを便宜的に分けたものであくまでこういう使われ方をするソケットをこういう名前で呼んでおく。というものである。しかし、listening socketが接続要求を送ることはできない(Error: Transport endpoint is already connected
)ことは確かであるから違う状態であることは明らか(つまりPassive mode)。
サーバー側 | クライアント側 | 説明 | |
---|---|---|---|
listening socket | listen()を実行することによってこのソケットになる。 | ---- | 特定ポートのリスニングを行っている。他のリスニングソケットに対して接続要求を送る(connect()する)ことはできない。ソケットがパッシブモードである、やパッシブソケットであるとも言う。 |
connected socket | accept()関数の戻り値でこのソケットが渡される。 | connect()を実行し、サーバー側にacceptされることによってこのソケットになる。 | 接続済みのソケット。sent, recvなどを行うことができる。 |
上記以外のすべてのsocket | listen()を実行する前のソケット | connect()がサーバー側によってacceptされる前のソケット | listen()もconnect()も可能。listen()の場合はbind()が必要。 |
#ソケットAPIの関数たちのフロー
一行が同時刻(物理学用語じゃないです)です。一行では無いがほぼ一行なものもありますが、わかりやすいように一行にしていません。
状態に関してはWiresharkでパケットを確認して、推測しているので間違ったところがあるかもしれません。切断に関してはいろいろな方法があるのであくまで一例となります。そして、厳密にはコネクションを切断する関数はshutdown
とclose
です。shutdown
はclose
、close
はdestroy
という名前のほうが実態に即しているらしいです。
(https://stackoverflow.com/questions/409783/socket-shutdown-vs-socket-close)
表でのソケットディスクリプタですが、
サーバー側はlistening_sock = socket.socket()
、connected_sock = listening_sock.accept()
です。しかし、listening_sock
の参照しているソケットがリスニング状態になるのはlisten()
の後です。ソケット生成の時点でlistening_sock
としているのはこの後、listen()
をしてリスニングソケットになるのが明らかだからです。
クライアント側はsock = socket.socket()
です。
ソケット生成から、3 WAY ハンドシェイクまで。
サーバー側の関数 | 説明 | サーバー側状態 | クライアント側の関数 | 説明 | クライアント側の状態 | |
---|---|---|---|---|---|---|
socket.socket() | リスニング用のソケットの生成。 | CLOSED | socket.socket() | 接続用のソケットの生成。 | CLOSED | |
listening_sock.bind() | ソケットの名前付け。 | CLOSED | ---- | ---- | CLOSED | |
listening_sock.listen() | ここで実際にソケットはリスニングソケットとなり、リスニングを開始する。OSが接続要求を受け付ける。 | LISTENING | ---- | ---- | CLOSED | |
〃 | ---- | LISTENING | sock.connect() | 指定されたアドレスに直接ソケットを接続する。 | SYN SENT | |
〃 | ---- | SYN RECV | 〃 | ---- | SYN SENT | |
〃 | ---- | ESTABLISHED | 〃 | ---- | ESTABLISHED |
3 WAY ハンドシェイクから切断前まで。
サーバー側の関数 | 説明 | サーバー側状態 | クライアント側の関数 | 説明 | クライアント側の状態 | |
---|---|---|---|---|---|---|
---- | ---- | ESTABLISHED | ---- | ---- | ESTABLISHED | |
listening_sock.accept() | 接続要求を引き受ける。 | 〃 | ---- | ---- | 〃 | |
---- | ---- | 〃 | sock.sendall() | サーバーにメッセージを送信。 | 〃 | |
connected_sock.recv() | クライアント側のメッセージを受信。 | 〃 | ---- | ---- | 〃 | |
connected_sock.sendall() | メッセージを送り返す。 | 〃 | ---- | ---- | 〃 | |
---- | ---- | 〃 | sock.recv() | サーバー側のメッセージを受信。 | 〃 |
コネクションの切断(4 WAY ハンドシェイク)
サーバー側の関数 | 説明 | サーバー側状態 | クライアント側の関数 | 説明 | クライアント側の状態 | |
---|---|---|---|---|---|---|
---- | ---- | ESTABLISHED | ---- | ---- | ESTABLISHED | |
connected_sock.close() | コネクションの終了させる。これ以下はsockに対していかなる処理(recv, send)も行えない。 | FIN-WAIT-1 | ---- | ---- | ESTABLISHED | |
〃 | ---- | FIN-WAIT-2 | ---- | ---- | CLOSE-WAIT | |
〃 | ---- | FIN-WAIT-2 | sock.close() | コネクションを終了させる。これ以下はsockに対していかなる処理(recv, send)も行えない。 | LAST-ACK | |
〃 | ---- | TIME-WAIT | 〃 | ---- | ??????? | |
---- | ---- | CLOSED | ---- | ---- | CLOSED |
コードを見ながら詳しく。
サーバー側
####socket()
まず、ソケット(これは接続要求を受け付けるためだけに使うつもりである。つまりリスニングソケットにするつもりである。)を生成する。
生成したソケットを表す(参照する)ファイルディスクリプタがreturnされる。
>>> listening_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> listening_sock
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
AF_INET
はIPv4を使った通信であること、SOCK_STREAM
はそのソケットがTCPのようなコネクション型通信(ストリーム型通信)用のソケットであることを表す。(※そして全二重通信を提供する)
<... laddr= ...>
のIPアドレスないしポート番号がすべて0であることが分かる、次でこれを設定する。
####bind()
次にそのソケットディスクリプタ(listening_sock)にアドレスを割り当てる。
>>> listening_sock.bind(('127.0.0.1', 8000))
>>> listening_sock
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000)>
<... laddr= ...>
に(ipアドレス, ポート番号)となっていることが分かる。
####listen()
生成したソケットはこの時点でリスニングを開始する。本来はこの時点ではリスニングしてるだけで受け入れはしていないが、OSがアプリケーションの代わりにクライアントの接続要求を受け付け(つまり代わりに接続してくれる)、アプリケーションの受け入れ待ち行列(キュー)に追加していく。OSがアプリケーションに代わって接続要求を受け入れることができるのはbacklog
引数で与えられた数までです。アプリケーションに一旦その接続が渡される(つまりアプリケーションがacceptする)と、受け入れ待ち行列には一つ空きが空く。そして、接続要求が受け付けられると、クライアント側のソケットと新たなリスニングソケットとは独立したコネクテッドソケット間に新しい接続が作成され、次の接続要求のためにリスニングソケットは再起動し、同じポートで再びリスニングを行う。
>>> listening_sock.listen(5)
このコードの場合、backlog
は5となっているため、OSが5つのクライアント側のソケットからの接続要求を受け付け、受け入れ待ち行列に待機させることができる。それ以上の接続要求はエラーとなる。
accept()
accept()を実行するとOSが代わりにに接続要求を受け入れている場合は待ち行列の先頭の接続のコネクテッドソケットのディスクリプタが、そうでない場合は来た接続要求を受け入れその接続のコネクテッドソケットのディスクリプタをreturnする。
>>> connected_sock, client_address = listening_sock.accept()
上の場合、listening_sockにきた接続要求を受け付け、connected_sockという新たなソケットを生成し、そのソケットとクライアント側のソケットの間に接続が確立される。(listening_sockもconnected_sockも実際はソケット本体ではない)[注釈要]
この辺のことは正直良くわからない。listening socketに[SYN]がきたら、新しいソケットから[SYN, ACK]を送り返して、接続を確立させるのだろうか?それとも3wayハンドシェイクのレベルにはソケットと言う概念は無いのだろうか?そもそもソケットはTCPの複雑な部分(ウィンドウ制御やフロー制御や再送制御など)やIP以下の部分を詳しく知らなくてもプログラマがそのアプリケーションに最適化された通信プログラムを作るためにあるAPIであるから3wayハンドシェイクの時点ではクライアント側のアドレスでの通信先の判別しか行っていないのかもしれない。
クライアント側
socket()
まず、ソケットを生成する。そして生成したソケットを表す(参照する)ファイルディスクリプタがreturnされる。
>>> listening_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> listening_sock
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
connect()
ここで実際にサーバーの(指定したポートをリスニングしている)リスニングソケットに対して、接続要求を送る。
>>> sock.connect(server_address)
###サーバークライアント両サイド
sendall()
データを送る。
####recv()
データを受け取る。
close()
接続に関わるリソースを開放する。(※実際にはこの前にshutdown()を行う必要がある。https://docs.python.org/ja/3/howto/sockets.html#disconnecting)
#参照
- http://research.nii.ac.jp/%7Eichiro/syspro98/server.html
- https://blog.stephencleary.com/2009/05/using-socket-as-server-listening-socket.html
- https://web.mit.edu/6.005/www/fa15/classes/21-sockets-networking/#network_sockets
- https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzab6/howdosockets.htm
- https://docs.microsoft.com/en-us/windows/win32/api/winsock2/
- https://docs.python.org/ja/3/library/socket.html
- https://docs.python.org/ja/3/howto/sockets.html
- https://ja.wikipedia.org/wiki/%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88_(BSD)
- https://linux.die.net/man/2/
- http://www.7key.jp/nw/tcpip/tcp/tcp2.html