ネットワーク通信の裏側と Socket API
- Authors

- Name
- ごとれん
- X
- @ren510dev
目次
- 目次
- はじめに
- ネットワークソケットとは
- スタンダードソケット
- Raw ソケット
- 主要な Socket API
- socket()
- bind()
- listen()
- accept()
- connect()
- send() と recv()
- close()
- ソケットプログラミング
- 1. ソケットの作成
- 2. ソケットの設定
- 3. コネクションの確立
- 4. データの送受信
- 5. ソケットクローズ
- Socket API による echo サーバの実装
- Socket API の活用例
- まとめ
- 参考・引用
はじめに
インターネットを介して提供されるサービスはどのように構築・実現されるのでしょうか。
現代のネットワークアプリケーション開発において、Socket API は非常に重要な役割を果たします。 Socket API は、異なるコンピュータ間でデータを送受信するための低レベルインターフェースを提供し、TCP/IP に則った通信を実現します。
今回のブログでは、ネットワークソケットについて触れ、Socket API の概要から実際の使用法について紹介します。
ネットワークソケットとは
ネットワークソケットとは、アプリケーションがインターネットを介してデータを送受信するための仕組みを抽象化したものです。 ネットワークインターフェース(NIC:Network Interface Card)や IP アドレス、ポート番号等、アプリケーションが通信をする際にコンピュータの出入り口となるものがソケットに該当します。

ソケット通信は、一般にサーバとクライアント間で用いられます。 Socket API(Application Programming Interface) と呼ばれる低レイベルインターフェースを使用することで、アプリケーションに対して異なるホスト間での通信をサポートするためのトランスポート層以下の機能を提供します。
ネットワークソケットを用いたクライアント・サーバ間におけるメッセージの送受信は以下の手続きに基づいて行われます。
- サーバ側
- ①
socket()はソケット(FD)を作成 - ②
bind()はソケットをローカルのアドレス(FD や IP アドレス + Port)にバインド - ③
listen()でソケットに接続を待ち受けるように命令 - ④
accept()で外部からの接続に対して新しいソケットを作成 - ⑤⑥
send(),receive()でデータの送受信を実行 - ⑦
close()でソケットをクローズして FD も削除
- ①
- クライアント側
- ①
socket()でソケット(FD)を作成 - ②
connect()でサーバ側のソケットに接続 - ③④
send(),receive()でデータの送受信を実行 - ⑤
close()でソケットをクローズして FD も削除
- ①
ここで、listen(), connect(), accept() 等が Socket API に該当します。
データの送受信は、アプリケーションが ファイルディスクリプタ(FD:File Descriptor)と呼ばれる記述子・ファイルを開き、内蔵ディスクに対してデータを読み書きするのと同じ原理で行われます。 アプリケーションはソケットを使ってネットワークに接続することで、同じくネットワークに接続されている別のアプリケーションと通信を行うことが可能となります。 一方のマシンでアプリケーションがソケットに書き込んだ情報を、相手のマシンで動作するアプリケーションが読み取ることで、インターネットを通じた相互通信が成立するわけです。
ソケットは大きく 2 種類 存在します。
まず、一般的に利用される TCP/IP プロトコルファミリを扱うストリームソケットとデータグラムソケットです。 これらは スタンダードソケット と呼ばれます。
また、既存のプロトコルを利用しつつ、新たなプロトコルを実装する際に利用される Raw ソケット というのが存在します。
以下に、スタンダードソケットと Raw ソケットについて紹介します。
スタンダードソケット
スタンダードソケットのうち、ストリームソケットは IP の上位層でエンドツーエンドプロトコルとして TCP を使用するため、信頼性の高いストリームサービスが利用できます。 また、データグラムソケットは IP の上位プロトコルとして UDP を使用するため、ベストエフォート型のデータグラムサービスを利用することができます。
スタンダードソケットの挙動によって TCP の正確さ や UDP の軽快さ といった特性を産みます。
TCP は コネクション指向型 であるため、相手を確認し、事前にコネクションを確立してから通信を開始するため、確実にメッセージが相手に届きます。 1 度の通信でエラーが生じてしまった場合は、接続できるまで繰り返し接続を試みるため、エラーを解消する時間は掛かりますが、確実に送信したメッセージを相手が受け取ることができます。
一方の UDP は コネクションレス型 であるため、通信相手を確認することなくメッセージを送信します。 そのため、送信されたメッセージが相手に確実に届くとは限りません。 しかし、再送制御に伴う手続きがない分高速に動作することが可能であり、データの欠落が許される範囲内で、大容量なメッセージを高速に送信したい場合等に利用されます。
以下にストリームソケットとスタンダードソケットの仕組みを示します。
ストリームソケットは確実な通信が可能な一方で、通信制御に伴うオーバーヘッドが大きくなるという特徴があります。 一方の、データグラムソケットは高速な通信を実現可能な上、最大で一度に約 65,500 バイトものメッセージを送信することができますが、データの到達性(データが欠落していないか)については保証されないという特徴があります。
スタンダードソケットにおける、ストリームソケットおよびデータグラムソケットは、いずれも送信するペイロードが、トランスポート層のプロトコル(TCP または UDP)でカプセル化されます。
Linux カーネルにおけるネットワークスタックの実装
Raw ソケット
Raw ソケットは、通常、カーネル空間(アプリケーションが処理する前段)で処理されてしまう IP ヘッダや TCP/UDP ヘッダを含んだ Raw パケットを送受信します。
前述のスタンダードソケットと Raw ソケットが取り扱うデータの違いを図に示します。
Raw ソケットは、ユーザ空間で新たなプロトコル層を実装することが可能なため、通信プロトコルの開発や既存プロトコルにアクセスする際に使用されます。 Raw ソケットを使用するプログラムにおいて、生成したソケットからパケット操作を行う関数の動作は下図のようになっています。
Raw ソケットの場合、ソケットインターフェースは AF_PACKET を指定することにより、変更が一切加えられていない受信直後の Raw パケットを、そのままユーザ空間のアプリケーションで受け取ることが可能となります。 ソケットオプションとして SOCK_DGRAM を指定した場合、IP 層からアプリケーション層までのデータを一括で取得します。 また、SOCK_RAW を指定した場合、ソケットは IEEE 802.3 フレームの IEEE 802.2 LLC(Logic Link Control)ヘッダの生成や解析を行うことが可能なため、データリンク層からデータの取得が可能となります。
| アドレスファミリ | 説明 | 指定するアドレス |
|---|---|---|
AF_UNIX | • ローカル通信に使用 • 同一マシン上で効率的なプロセス間通信を可能とする • Unix ドメインソケット通信 | ソケットファイルのファイルパス |
AF_INET | • IPv4 通信に使用 • TCP ソケット通信 | ホスト名, ポート番号 |
AF_INET6 | • IPv6 通信に使用 • TCP ソケット通信 | ホスト名, ポート番号 |
一方で、Raw ソケットを用いた場合にも、自身の MAC(Media Access Control)アドレス以外の宛先が指定されたフレームを受信した場合、受信ソケットで読み出し可能なデータになる前に、NIC もしくは、カーネル空間のネットワークスタックフィルタリングで破棄されてしまいます。
そのため、ルータやブリッジ、リピータ等のネットワーク中継機を実装する場合は、自身のアドレスとは無関係の宛先が指定されているパケットを受信できるように、Promiscuous Mode が実装されます。
Promiscuous Mode が実装されたネットワーク機器は、受信した全てのフレームの宛先 MAC アドレスの確認をスキップし、そのままカーネルへ引き渡して処理します。
一般に、Promiscuous Mode はパケットキャプチャツールや IDS(Intrusion Detection System)/ IPS(Intrusion Prevention System) 等のネットワーク侵入検知システム、ネットワークブリッジで利用されます。
主要な Socket API
socket()
- 新しいソケットを作成する
- この関数は通信のためのエンドポイントを作成し、そのソケットの FD を返す
bind()
- ソケットにローカルアドレスを割り当てる
- 主にサーバ側で使用される
listen()
- ソケットをリッスン状態にして接続要求を待機する
accept()
- リッスンソケットに接続要求を待機し、クライアントとの接続を受け入れる
connect()
- ソケットを使ってサーバに接続する
- 主にクライアント側で使用される
send() と recv()
- データを送受信するための関数
send()はデータを送信し、recv()はデータを受信する
close()
- 通信を終了し、ソケットを閉じる
ソケットプログラミング
ソケットプログラミング(ネットワークプログラミング)では、ソケットという通信の終点を表す抽象化オブジェクト(前述の Socket API)をコールすることで、アプリケーション間のデータ通信を定義します。
具体的には、以下の段階を経て通信が実現されます。
今回は、標準的に使用される、ストリームソケットを例に挙げて紹介します。
1. ソケットの作成
まず、アプリケーションは通信を確立するためにソケットを作成します。 ソケットの生成は、socket() 関数を用います。
AF_INET- IPv4 インターネットプロトコルを使用することを指定
SOCK_STREAM- ストリームソケット(TCP ソケット)を指定
0- この引数は通常 0 が指定され、デフォルトのプロトコルを意味します
- TCP ソケットの場合、
SOCK_STREAMとAF_INETの組み合わせにおける標準プロトコルが自動的に選択されます
ソケットの生成に成功すると、sockfd は新しく作成されたソケットの FD(識別番号)を受け取ります。 後続の処理はこの FD を通じて、システムコール(bind(), listen(), accept(), connect(), send(), recv() 等)を使用し、ソケットに対するオペレーションを実行します。
2. ソケットの設定
次に、送受信するデータのプロトコルやポート、IP アドレスを設定します。 これは bind() 関数や connect() 関数で行います。
struct sockaddr_in- IPv4 アドレスを処理するための構造体
- ソケットのアドレス情報を格納するために使用される
- 実際には
server_addrにサーバ側の IP アドレス情報が格納される
server_addr.sin_family- アドレスファミリとして IPv4(
AF_INET)を指定
- アドレスファミリとして IPv4(
server_addr.sin_port- サーバがリッスンするポート番号を指定
htons(Host TO Network Short)関数は、ネットワークバイトオーダ(ビッグエンディアン)に変換するための処理を担う- ポート番号は通常ホストバイトオーダ(マシン依存だが、だいたいはリトルエンディアン)で指定されるため、ネットワークバイトオーダに変換する必要がある
host & 0x00FFは下位 8 ビットを取り出す<< 8は下位 8 ビットを左に 8 ビットシフト演算するhost & 0xFF00は上位 8 ビットを取り出す>> 8は上位 8 ビットを右に 8 ビットシフト演算する- これらを
|演算子で結合して新しい 16 ビットの数値を生成する
- ポート番号は通常ホストバイトオーダ(マシン依存だが、だいたいはリトルエンディアン)で指定されるため、ネットワークバイトオーダに変換する必要がある
server_addr.sin_addr.s_addr- サーバの IP アドレスを格納する
sin_addrフィールドはstruct in_addr型であり、s_addrはその中で具体的な IP アドレスを保持するメンバ変数inet_addr関数で、IPv4 アドレスを文字列形式からネットワークバイトオーダーのバイナリ形式に変換
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));- 作成したソケットを特定の IP アドレスとポートにバインドするためのシステムコール(ユーザ空間からカーネル空間へのリソースリクエスト)を実行
- システムコールによって、サーバは指定されたアドレス上の指定されたポートで接続を待機する
3. コネクションの確立
クライアントアプリケーションはサーバと接続を確立するために connect() を呼び出し、サーバ側は listen() と accept() を用います。
connect- ソケットを特定のリモートアドレスとポートに接続する
- 生成した FD に対して、アドレス情報(IP アドレス + ポート番号)を含む構造体へのポインタを渡す
4. データの送受信
接続が確立された後に、send() や recv() もしくは read() や write() を使用してデータの送受信が行われます。
send- 生成した FD に対して、『Hello, World!』という文字列を書き込む
- 第 3 引数の
strlen("Hello, World!")は送信するデータのバイト長(13 byte)を返す strlen関数を呼び出すことで、送信するデータ長を動的に計算できる
5. ソケットクローズ
処理が完了すると、close() を使用してソケットを閉じます。
close- クローズする FD を渡す
ほとんどの OS のカーネル(Linux、Windows、BSD)は C 言語で記述されています。 また、Python、JavaScript、Java、Go、Rust をはじめ大半の高級言語では、高レベルな抽象化 API を提供していますが、最終的には、ネットワークライブラリも内部的にはシステムコールや C 言語で実装されたライブラリ(LIBC)に依存していることが多いです。
Socket API による echo サーバの実装
ストリームソケット(TCP)を使用して、クライアントサーバ間の通信を検証します。
今回は Linux(Debian)を使用します。
サーバの実装
クライアントの実装
- サーバ側の実行結果
- クライアント側の実行結果
- クライアント側の実行結果(サーバがリッスンしていない場合)
このプログラムはクライアントからのリエクスとを一度処理すると、ソケットをクローズします。
サーバプログラムがクライアントからのリクエストを一度に一つずつ処理する方式は、一般に Iterative Server(反復サーバ)と呼ばれます。
Socket API の活用例
Socket API は様々なネットワークアプリケーションで利用されています。
- ウェブサーバ
ウェブブラウザとサーバ間で HTTP リクエストとレスポンスを送受信するために Socket API は必須です。 Apache や Nginx 等の Web サーバは、どんな言語で実装したとしても、最終的には内部で Socket API を呼び出しています。
- チャットアプリ
リアルタイムでメッセージをやり取りするチャットアプリケーションもソケットを使ってユーザ間のメッセージを送受信します。 チャットアプリでは、WebSocket 等のプロトコルが一般的に使用されています。
- ゲームネットワーク
オンラインゲームでは、プレーヤー間の状態同期やイベント通知のためにソケットが利用されます。 リアルタイム性が要求されるため、このような場面ではデータグラムソケット(UDP)を使用することが多いです。
まとめ
今回のブログでは、ネットワーク通信の裏側で利用されている Socket API について紹介しました。 Socket API は、ネットワークプログラミングの基礎を形成する非常にコアなインターフェースです。
近年では、高級言語が普及したことで、普段の開発の中でソケットを意識することはほとんどなくなりましたが、どんな Web サービスも Socket API 無しでは通信の仕組みを説明できません。 ネットワークソケットの実装は、C 言語の知識や TCP/IP、コンピュータネットワークへの理解も求められるため非常にややこしい分野ではありますが、Web サービスの開発者は理解しておくことが重要です。
一度、C 言語で API を実装して理解を深めておけば、高級言語を使った HTTP サーバの構築はそれほど難しくないと思います。





