54
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソケット通信、本当に理解していますか??

Posted at

はじめに

「ソケット通信ってなんやねん」、そう思った経験はみなさんもあると思います。
私もそのうちの一人です 👍
個人的に初学者の方がつまづくポイントが多い概念なのかなと感じていました。
本記事では、そのモヤモヤを解消すべくソケットの基礎概念に触れていこうと思います。
この記事を読み終わった後、皆さんはソケットを理解し、ウキウキになれるはずです!

では、一緒に「見えない通信」の扉を開きましょう〜 👽

対象読者

  • ソケット通信を基礎から理解したい方!!

ソケット通信とは何か?

まず、ソケットとは何なのかみていきましょう。

ソケットとは

ソケット は、オペレーティングシステム (OS) が提供する、プロセス間通信のエンドポイント、つまり「出入り口」のことです。
同じコンピュータ内の異なるプログラム同士、あるいは、ネットワークを介した異なるコンピュータ上のプログラム同士がデータのやり取りをする際に、この「ソケット」が窓口の役割を果たします。

各種OSは「ソケットライブラリ」を持っており、そのライブラリ内のプログラムを呼び出すことでソケット通信を実現しています。

より噛み砕いてみていきましょう。
電話で例えてみると、、、

  • ソケット = 電話機そのもの
  • IPアドレス = 電話番号
  • ポート番号 = 内線番号
  • プロトコル(TCP/UDPなど) = 会話のルール

通信を行うためには、相手の「電話番号(IPアドレス)」と「内線番号(ポート番号)」を知り、適切な「会話のルール(プロトコル)」で「電話機(ソケット)」を使って接続するというイメージです。

まずは、ソケット通信が『ソケット』という出入り口を使ってデータをやり取りする仕組みだということがイメージできればOKです!

次に理解しておきたいのが、「OSI参照モデル」になります。

OSI参照モデルとの関係性

ネットワークの仕組みを理解する際に欠かせないのが、「OSI参照モデル」。
これは、ネットワークの機能や仕組みを7つの階層(レイヤー:L)に分類したもので、多くの方が下のイメージを見たことがあるのではないでしょうか。

各層について軽く触れておきます。

  • アプリケーション層(L7)

    • ユーザーが操作するアプリケーションが提供する機能に必要な通信サービスを実現するためのプロトコル群
    • HTTP, DNS など
  • プレゼンテーション層(L6)

    • データの表現形式を整形するプロトコル群
    • MPEG, JPEG など
  • セッション層(L5)

    • 通信セッションの確立・維持・終了を管理するプロトコル群
    • NetBIOS, WebSocket など
  • トランスポート層(L4)

    • データ転送の信頼性やフロー制御、ポート番号によるプロセスの識別を行うプロトコル群
    • TCP, UDPなど
  • ネットワーク層(L3)

    • 起点から終点までの通信を実現するプロトコル群
    • IP, ICMP など
  • データリンク層(L2)

    • 隣接するノード間の通信を実現するプロトコル群
    • Ethernet, MACアドレスなど
  • 物理層(L1)

    • 通信のための物理的な仕様についてのプロトコル群
    • IEEE, ISDN など

ソケットは主に、トランスポート層(第4層)で動作します。アプリケーション層(第7層)からのデータ送受信のリクエストを受け、TCPやUDPといったプロトコルを使ってデータをセグメント化し、ヘッダー情報を付加してネットワーク層へと渡します。
これにより、アプリケーションはネットワークの複雑な詳細を意識することなく、ソケットという窓口を通じて通信できるようになっています。

TCP と UDPの違い

ソケット通信の具体的なプロトコルとして TCPUDP があげられます。
まずは、TCP, UDP についてざっくり解説します!

TCP について

TCP(Transmission Control Protocol)は、データの信頼性を最も重視した通信プロトコルです。送信元と宛先の双方で情報の確認のやり取りをする スリーウェイハンドシェイク1 を必ず行うため高い信頼性が確保されています。

この スリーウェイハンドシェイク によって通信経路が確立された後、「データの順序付け(順番通りに届ける)」、「エラー検出」、「再送(届かなかったデータを送り直す)」といった仕組みが働き、正確なデータ伝送を保証します。

このようなプロトコルを、「コネクション型のプロトコル 」と表現します。

TCPは主に、以下の用途で使用されます。

  • Webの閲覧
  • メールの送受信
  • ファイル転送
    などなど、、、

TCP は、データの欠損や順序の狂いが許されない場面 で広く利用されています。

UDP について

UDP(User Datagram Protocol)は、送受信を開始する前に通信相手との接続を確立をしません。
単にデータパケット(データグラム) を目的地に一方的に送信するだけを行います。そのため、TCPよりも 高速 ですが、データの到着保証や順序保証がないため、信頼性には劣ります。

このようなプロトコルを、「コネクションレス型のプロトコル」と表現します。

UDPは主に、以下の用途で使用されます。

  • 動画ストリーミング
  • オンラインゲーム
  • VoIP(Voice over Internet Protocol)
  • DNS(Domain Name System)
    などなど、、、

UDP は、リアルタイム性や速度が最優先され、多少のデータ欠損や順序の入れ替わりが許容される場面で広く利用されています。

ポイント
TCPとUDPについて触れる際、よく「信頼性」という言葉が登場します。
これは「スリーウェイハンドシェイク 」の有無だけでなく、
TCP はシーケンス番号や確認応答番号(ACK)など、さまざまな情報が含まれているヘッダを持っているのに対し、UDP のヘッダには必要最低限の情報しか含まれていないといという違いを理解しておくと良いでしょう。

ソケット通信のライフサイクル

続いてソケット通信が開始されてから終了するまでのライフサイクルについて解説していこうと思います。
このライフサイクルを理解する上で重要となってくるのが、サーバー側とクライアント側の関係性です。
全体像は下の画像の通りです。

それでは順を追ってみていきましょう!

ソケット操作のフロー

1. ソケットの作成

まずは、通信の出入り口になるソケットを socket() 関数を用いて、ソケットオブジェクトの作成をしていきます。
この時、サーバー側クライアント側 の両方にソケットを作成する必要があります。

2. IPアドレスとポート番号へのバインド

ソケットを作成しただけでは、サーバーはまだクライアントからの接続を受け付けられません。サーバーはクライアントが接続できる「場所」を明確にする必要があります。
そこで、ソケットを特定のIPアドレスとポートに紐付ける bind() 関数を用意します。
もしこのバインドに失敗した場合(例えば、すでにポートが使われている場合など)、エラーが発生し、処理は続行できません。

クライアント側 は、この段階では、特に何も行いません。サーバーの準備が整うのを待ちます。

3. 接続の待ち受け

バインドされたソケットを、クライアントからの接続を待ち受ける リッスン状態 にします。

  • サーバー側

    • listen() 関数を実行し、接続待ち状態にします
    • このlisten() 自体は通常ブロック(処理停止)しません
  • クライアント側

    • こちらは引き続き、特に何も行いません

4. クライアント側からの接続要求とサーバーの受け入れ

サーバーの準備が整ったら、クライアントがサーバーへ接続を試み、サーバーがそれを受け入れる通信が確立されます。

  • クライアント側

    • サーバーのIPアドレスとポート番号を指定して、connect() 関数を実行し、接続要求を送信します
    • TCP の場合、ここでスリーウェイハンドシェイク が行われ、サーバーとの間で通信経路の確立を試みます
    • 接続に失敗した際は、エラーとなります
  • サーバー側

    • リッスンしているソケットがクライアントからのconnect() 要求を受け取ると、accept() 関数を実行します。このaccept()関数は、接続要求が来るまで処理をブロックします
    • 接続要求が正常に受け入れられると、そのクライアント専用の新しいソケットインスタンスが生成されます
    • 元のリスニングソケットは、引き続き次のクライアントからの接続を待ち続けます
      👉🏻 サーバーは多数の異なるクライアントを同時に処理でき、それぞれが独自のソケットを持つことが可能となります

補足:多数の同時接続をするサーバー

ソケット通信は、サーバーとクライアント間で確立された接続を通じてデータをやり取りします。
特にサーバーでは、多数のクライアントからの同時接続に対応する必要があるため、その処理方法はパフォーマンスに大きく影響します。

従来の同期的な ブロッキングI/Oモデル2 では、各クライアント接続を処理するために、個別のスレッドやプロセスを割り当てることが一般的です。これは実装がシンプルですが、問題点があります。
接続数が増えると、各スレッドやプロセスがそれぞれメモリを消費し、OSはそれらの間でCPUの実行権を頻繁に切り替えると、オーバーヘッドを多発させます

オーバーヘッドとなることで、処理が断片化し全体のパフォーマンスが急速に低下する原因につながります。

余談
人間も頻繁にタスクを切り替える作業をしていると全体としての効率を低下させてしまい、一度中断されると元の作業の集中力をとり戻すまで平均23分15秒かかると言われています。
私もよく脳内スタックオーバーフローを起こしてしまいます、、🫣
It takes 23 mins to recover after an interruption

高パフォーマンスなサーバーはどのように実現しているのか?

リアルタイムAPIのような高性能が求められるシステムでは、このスケーリングの限界を克服するために、ノンブロッキングI/Oイベント駆動型アーキテクチャ といった技術が採用されています。

これらのアプローチでは、ソケットの読み書きがすぐに完了しない場合でも、処理をブロックせずに他のソケットの処理へ移ります
そして、OSが特定のソケットがデータの読み書き準備ができたことを検知した際にのみ、アプリケーションに通知する仕組みをとります。

これにより、単一または少数のスレッドで数千、数万といった多数のオープンソケットを効率的に管理することが可能になります。
これらのイベント通知メカニズムは、アイドル待機(I/O操作の完了をCPUが待つ無駄な時間) や冗長なポーリング(アプリケーションがI/Oの状態を繰り返し確認する非効率な処理) を回避することで、CPU使用率とレイテンシを大幅に削減します。

5. データの送受信

接続が確立されたら、いよいよクライアントとサーバー間でデータのやり取りが始まります。
このデータ送受信は、双方向で行われます。

  • サーバー側

    • accept() で得られた新しいソケットを使って、クライアントからデータを受信(read())したり、クライアントへデータを送信(write())したりします
  • クライアント側

    • connect() で確立されたソケットを使って、サーバーへデータを送信(write())したり、サーバーからデータを受信(read())したりします

ソケットとファイルディスクリプタについて

Unix系システムでは、ソケットはファイルディスクリプタ3 として扱われます。
これはファイルを開いて操作するのと同様に、ソケットへの書き込みや読み取りができます。
つまり、read()write() といった一般的なシステムコールがソケットにも使用できるのはこのためです。

プロセスがファイルを開くと、OSはそのファイルを表すエントリを作成し、そのファイルに関する情報をカーネル内に保存します。
このエントリが整数で表され、これがファイルディスクリプタとなります。
例えば、1つのプロセスが複数のファイルやソケットを開くと、それぞれにファイルディスクリプタと呼ばれる整数(たとえば 3, 4, 5...)が割り当てられます。

0, 1, 2 はそれぞれ標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)として予約されています

カーネル内部では、ファイルディスクリプタ(整数)をキーとして、該当するオープンファイルの情報(ファイルの種類、位置、モードなど)が管理されています。

以下の画像は、ファイルディスクリプタのイメージです。

同様に、ネットワークソケットを開いた場合も、それは整数で表され、ソケットディスクリプタと呼ばれます。
つまり、プロセスにとって、開いているファイルもネットワークソケットも、OSから見れば本質的には「整数で表現されたリソース」として一貫して扱われています。

6. サーバー ・ クライアント間の接続を閉じる

クライアントとサーバー間の通信が終了したら、利用していたソケットを close() 関数で閉じます。
これはメモリリークを防ぎます。
特に負荷が高いサーバーではリソースの枯渇に繋がるのでそのような事態を防ぐための重要な処理となっています。

  • サーバー側

    • クライアントとの通信が終了した専用ソケットを閉じます
    • これにより、そのソケットが使用していたシステムリソースが解放され、OSに返却されます
  • クライアント側

    • サーバーとの通信が終了したら、使用していたソケットを閉じます
    • 同様にリソースを解放します

ソケットの内部構造を意識する

TCPソケットの場合、システムは各接続のステートマシンを維持しています。
これは、ソケットが単にオープンやクローズされるだけでなく、そのライフサイクル中に複数の状態に遷移しています。
一般的なTCPソケットの状態には、以下の画像の状態が存在し、接続の確立、維持、および切断のさまざまなフェーズを表しています。
画像をよくみていただくと、TCPのスリーウェイハンドシェイク でよく見る「SYN」・「ACK」・「FIN」といったフラグのやり取りを確認できると思います。

引用:TCP (Transmission Control Protocol) RFC 793

状態遷移の知識が単なる理論ではなく、ポート枯渇ソケットリークの診断, 予期せぬリソースの消費といった実用的な問題解決をする上で重要な役割を担っています。

ソケットの一意性

ここまでソケットのライフサイクルを見てきましたが、OSは実際に多数の同時接続をどのように区別し、管理しているのか気になりますよね。

全てのソケットは以下の 5つのタプル によって一意に識別されます。

  • プロトコル (TCP / UDP)
  • 送信元IPアドレス
  • 送信元ポート番号
  • 宛先IPアドレス
  • 宛先ポート番号

この組み合わせにより、それらがすべて同じリモートサーバーとポートと通信していても、OSは複数の同時接続を区別することができます

Unixドメインソケットについて

ソケットにはもう1つの重要なタイプがあります。
それは『 Unixドメインソケット(UDS) 』というものです。

UDS はネットワークソケットではなく、同じホスト上のプロセス間通信(IPC: Inter-Process Communication) に使用されます。
IPアドレスとポート番号を使用する代わりに、UDSはファイルシステム上のファイルパス、例えば /tmp/app.sock のような拡張子に.sockを含んだファイルパスをアドレスとして使用します。(※ .sockがないこともあります)

引用:Getting Started With Unix Domain Sockets

UDS は、通信で通常必要となるIPルーティングやプロトコル処理(OSI参照モデルのレイヤー3以降)を完全にバイパスします。
そのため、ネットワークソケットよりも格段に高速で効率的な通信 を実現します。
実際、PostgreSQLRedis のような高性能アプリケーションでは、パフォーマンスとセキュリティを重視するローカル通信において、UDSをデフォルトで使用するケースが多いです。

暗号化の重要性

ここで重要な注意点として、ソケットは本質的に安全ではない ということです。
これは、明示的に暗号化されない限り、生のデータをそのまま送信してしまいます。

ソケットを介した安全な通信は、通常、TLS(Transport Layer Security) を介して実現されます。TLS は既存のソケット接続をラッピングし、その接続を介して送信されるすべてのデータが暗号化され、認証されることを保証します。
TLSを使用しない生のソケットや設定ミスのあるソケットは、MITM(中間者攻撃)パケット盗聴 の標的になる可能性があります。

現代のアーキテクチャによるソケットの役割

現代では、分散システムやマイクロサービスといったソフトウェアアーキテクチャが存在しますが、ソケットベースの通信は至る所で使用されています。

  • RESTful API

    • 通常は TCP 上の HTTP を通じて実装され、マイクロサービス間通信において非常に一般的な選択肢です
  • gRPC

    • HTTP/2(TCPベース)上に構築され、双方向ストリーミングや効率的なバイナリ通信を可能にしています
  • ロードバランサー

    • NginxEnvoy は TCPソケット経由で L7ルーティングを行います
  • サービスメッシュ

    • Istio などは Envoy を通じてアプリケーション層の通信を制御・監視します
  • 高性能システム

    • HFT(高頻度取引)やリアルタイムゲームなど、低レイテンシーが求められる場面では、TCP / UDP ソケット上に独自のバイナリプロトコルを直接実装することもあります
  • 分散システム

    • KafkaCassandra のような分散データベースやメッセージングシステムは、ノード間通信に TCP ソケットを用い、効率的なデータ同期と可用性を実現しています
    • Redis もクラスター構成において同様のアプローチをとります

まとめ

本記事は、「ソケット通信」の基礎概念から、OSI参照モデルにおける位置づけ、TCP/UDPの違い、ライフサイクル、そして現代の分散システムやマイクロサービスにおけるその重要性までをまとめました。
ソケットが、現代のネットワークコンピューティングのまさに基盤であることに気づけたのではないでしょうか。

私自身、ソケットがどのように機能するかを理解することで、今後のシステム開発においてネットワーク通信のパフォーマンス改善やスケーラブルなバックエンドシステムの構築に役に立つと強く感じました!

参考

  1. スリーウェイハンドシェイク
    こちらは私が以前投稿した「ブラウザでWebサイトが表示されるまでの仕組みを整理してみた」という記事のブラウザとWebサーバー間の通信というセクションで詳しく触れているのでそちらを参考にしてください。

  2. ブロッキングI/O
    入出力をやっている最中は他の処理を進めないで入出力が終わるのを待ってるI/O(入出力処理)のこと

  3. ファイルディスクリプタ
    プログラムからファイルを操作する際、操作対象のファイルを識別・同定するために割り当てられる一意の整数値

54
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?