TCP Server/Client を車輪の再発明することで,ネットワーク通信の下の方を勉強してみようという試み.ついでに用語の整理もしてみます.
Stream
stream を使用しない場合のデータのよみ出しは,read_block()
というシステムコールによって以下のように行える.
1 2 |
|
上記では,hello.c
の先頭から32byte目から256bytes分読み込まれる.つまり,固定の範囲(ブロック)でデータにランダムにアクセスできる.
上記方法は,扱う対象がメモリやハードディスク上であれば同様の処理で十分だが,例えばキーボードからの入力等を読み込む場合には適さない.前者のようなデバイスを block device .後者のようなデバイスを character device という.
- block device
- データにブロック単位でアクセス可能なデバイス.データのバッファリングやランダムアクセスが可能.
- 例: メモリ,ハードディスク
- character device
- データにbyte単位でアクセス可能なデバイス.データはバッファリングされず,ランダムアクセスもできない.
- 例: キーボード,マウス
2つのデバイスを同じシステムコールで扱うためには,使用するシステムコールを character device に合わせておけば良い.ここで使われている抽象化が stream である.
- stream
- データの供給元(ex: ハードディスク)と受け手(ex: プログラム)の間に入り,データの一時保存を行う抽象データ構造
- 1byte単位でデータを受け取り,FIFO方式で受け手に渡す
stream の良いところは,供給元と受け手の転送量の違いを吸収できる部分.供給元は一度に100byteのデータを送信したいが受け手は一度に10byteしか受け取れない場合, stream は余った90byteを一時的に保存できる.
stream を使用したデータのよみ出しは以下のように行える.open システムコールによって OS によみ出し開始を通知し,同時に stream の識別子である file descriptor を得る.それ以降は file descriptor を使用して read,write システムコールによって読み書きを行い,close によって終了する.
1 2 3 4 5 6 7 8 |
|
Socket
TCP/IPによるネットワーク通信も,結局はデータのやりとりなので, stream の概念を流用できるとわかりやすい.しかしそのまま適用はできなかったので,特別な stream として socket を定義した.socket は通常の stream と異なり,作成時には open ではなく socket システムコールを使用する.また,作成時の処理が多少複雑になっている.
TCP socket
TCPによる通信では以下の約束事がある.
- メッセージが届いたかチェックし,届いてなければ再送する
- 到着順序は保障されている
- それが失敗した場合はエラーになることが保障されている
TCP通信用の socket の作成方法は以下の通り.
1
|
|
第一引数はアドレスファミリを表している.アドレスファミリとは,socket が使用するアドレス体系のこと.よく使用されるのは以下.
アドレスファミリ | 内容 |
---|---|
AF_INET | IPv4用 |
AF_INET6 | IPv6用 |
AF_UNIX | ローカルなプロセス間通信用 |
AF_PACKET | デバイスレベルのパケットインタフェース |
第二引数はソケットタイプを表している.ソケットタイプはソケットの性質を表しており,Linuxで使用可能な代表的なものは以下.
ソケットタイプ | 解説 |
---|---|
SOCK_STREAM | 順序性,信頼性を備え双方向接続された byte stream を提供する |
SOCK_DGRAM | データグラム(接続,信頼性なし,固定最大長メッセージ)をサポートする |
第三引数は使用するプロトコルを表す.0にしておくとデフォルトのものが使用される.基本的には上記二つの組み合わせによって通信方式が決定される.例えば,以下のように.
アドレスファミリ | ソケットタイプ | 通信方式 |
---|---|---|
AF_INET | SOCK_STREAM | IPv4 + TCP |
AF_INET6 | SOCK_DGRAM | IPv6 + UDP |
通信手順
クライアントとサーバがそれぞれ以下の手順でシステムコールを呼び出し,通信する.クライアントサーバモデルは,同期方法が非対称的であり,サーバ側とクライアント側でプログラムが異なる.これは電話の呼び出し方式と似ている.
No | Client | Server |
---|---|---|
1 | socket(ソケット生成) | socket(ソケット生成) |
2 | bind(ポート指定) | |
3 | listen(待ち受け) | |
4 | accept(接続待ち) | |
5 | connect(ソケット接続要求) | |
6 | send(送信) | |
7 | recv(受信) | |
8 | send(送信) | |
9 | recv(受信) | |
10 | shutdown(切断) | shutdown(切断) |
11 | close(ソケット切断) | close(ソケット切断) |
Client 側は socket 作成後,connect で同期を行う.この時,通信相手のサーバを引数(host名,port番号)で指定する.
Server 側は socket 作成後が多少複雑.socket は作成時点ではアドレスが割り当てられていないので,bindによってアドレスを割りあてる.この操作は伝統的に「ソケットに名前をつける」と呼ばれる.
ここでいうアドレスは,接続に必要なネットワーク層レベルの情報を保持する構造体として定義されている.保持する情報と定義は以下の通り.
- 通信プロトコル
- アドレス
- ポート番号
1 2 3 4 5 6 7 8 9 10 |
|
bind 後,socket を passive socket(接続待ちソケット),すなわちサーバ側の socket であることをOSに通知する.ここでいう passive socket とは,accept による接続要求を受け付けるのに使用するソケットのこと.accept では passive socket から接続要求を取り出し,それを元に接続済みソケットを作成し,その socket を参照する新しい file descriptor を返す.
なぜわざわざ passive socket を用意しているのかというと,通常サーバに対してクライアントは複数存在し,その要求をさばくためには接続要求をためておく stream が必要であるため.
通信終了時には close 処理を行うが,多少注意が必要.sockt は送信データが正しく受信したことを確認しないと破棄されないので,close しても即座に破棄はされない.一般には,サーバよりも先にクライアントを close すればよい.サーバ側から強制的に close したい時には,close の直前に以下を呼べば良い.
1
|
|
もしくは bind の前に以下を設定する.
1 2 |
|
setsockopt
は,与えられた socket のオプションを設定するもので,以下のように定義されている.
1 2 |
|
よく使われるオプションは SO_REUSEADDR
であり,これを設定することで再度同じポート番号で bind できる.
実装
ネット上のいろいろを参考に TCP サーバを C++ で書いた.C++は全然書いたことない初心者なのでひどいコードになっているかもしれない…飽くまで学習用なので,あしからず.
Server,Client の順で起動し,サーバ側から文字列を送信できる.実行結果は以下のような感じ
参考
Lecture Notes
setsockopt - 車輪のx発明 ~B.G's Blog~
Linuxネットワークプログラミング : あきみち : 本 : Amazon.co.jp
socket(2), bind(2), listen(2), accept(2) - kotaroito's notes
LinuxにおけるTCPソケット通信を利用したプロセス間通信(C++) - MyEnigma