Linux Socketの仕組みを図解|Port・listen・accept・Socket Buffer・ssコマンド

はじめに

Linux上のApplicationは、NICやTCP/IP Stackを直接操作するのではなく、Socketを通じてNetwork通信を行います。Web ServerはSocketを作成してTCP Port 80や443などで接続を待ち、ClientはSocketからServerへ接続してDataを送受信します。

Socketを理解するには、SocketとFile Descriptorの関係、IP AddressとPort、bind()、listen()、accept()、connect()、Listen SocketとConnected Socketの違いを整理する必要があります。

「Processは動いているのに接続できない」「特定のIP Addressからしか接続できない」「ESTABLISHEDやCLOSE-WAITが大量に残る」といった現象も、Socketの状態とSystem Callの流れを理解すると切り分けやすくなります。

本記事ではSocketの基本構造、TCP ServerとClientの処理、Listen Queue、送受信Buffer、BlockingとNon-blocking I/O、Linux上での確認方法を順番に解説します。

Socketとは

Socketは、ApplicationがKernelのNetwork機能を利用するための通信Endpointです。ApplicationはSocket APIを使ってSocketを作成し、接続、送信、受信、切断などの処理をKernelへ依頼します。

SocketにはProtocol Family、Socket Type、Local Address、Local Port、Remote Address、Remote Port、Protocol State、送受信Bufferなどが関連付けられます。これらすべての情報が作成直後から決まっているわけではなく、bind()やconnect()などの処理に伴って設定されます。

Linux Socketの基本構造
要素例役割
Protocol FamilyAF_INET、AF_INET6IPv4やIPv6などのAddress体系を指定
Socket TypeSOCK_STREAM、SOCK_DGRAMStreamやDatagramなどの通信方式を指定
Local Address / Port192.0.2.10:443Local側のEndpointを示す
Peer Address / Port198.51.100.20:53000通信相手のEndpointを示す

SocketとFile Descriptor

Linuxでsocket()を実行すると、成功時には非負の整数であるFile Descriptorが返ります。Applicationは以後、このFile Descriptorを使ってSocketを指定します。

SocketとFile Descriptorの関係

File DescriptorはProcess内で使用される番号であり、Socketそのものではありません。ProcessのFile Descriptor TableにあるEntryを通じてKernel内のFile ObjectやSocketへ到達します。同じSocketをdup()やfork()などによって複数のFile Descriptorが参照する場合もあります。

SocketもFile Descriptorとして扱われるため、read()、write()、close()、poll()、epoll()など、Fileと共通するSystem Callを利用できます。ただし、seekできないなど通常のDisk Fileとは異なる性質があります。

Protocol FamilyとSocket Type

socket()でSocketを作るときは、使用するProtocol FamilyやSocket Typeを指定します。

  • Protocol Family:IPv4、IPv6、UNIX Domainなど、Socketが使用するAddress体系を指定
  • Socket Type:Stream型、Datagram型などSocketの通信特性を指定

代表的な組み合わせは次のとおりです。

socket(AF_INET, SOCK_STREAM, 0) // IPv4 + TCP
socket(AF_INET6, SOCK_STREAM, 0) // IPv6 + TCP
socket(AF_INET, SOCK_DGRAM, 0) // IPv4 + UDP
socket(AF_UNIX, SOCK_STREAM, 0) // 同一Host内の通信

※厳密にはSOCK_STREAM = TCP、SOCK_DGRAM = UDPそのものではありませんが、Internet通信の代表的な組み合わせとしては、この対応で理解すると分かりやすいです。

Port Numberとは

Port Numberは、受信したTCP SegmentやUDP Datagramを適切なApplicationのSocketへ届けるための識別情報です。16 bitの値で、0から65535までの範囲があります。

範囲名称概要
0〜1023Well-known Ports代表的なServiceへ割り当てられる範囲
1024〜49151Registered PortsApplicationやServiceが登録して利用する範囲
49152〜65535Dynamic / Private Ports一時的なClient Portなどに使われる範囲

Linuxが自動選択するEphemeral Portの実際の範囲はSystem設定によって決まり、必ず上表のDynamic / Private Portsだけが使われるわけではありません。Linuxでは次の設定で確認できます。

LinuxのEphemeral Port範囲

このLinuxでは32768〜60999がLocal Portの自動割り当て範囲となります。

TCP Port 53とUDP Port 53は別のProtocol空間です。同じPort NumberでもTCPとUDPで別のSocketをBindできます。また、PortだけでなくLocal IP Addressとの組み合わせもBindの競合判定に関係します。

TCP ServerのSocket処理

TCP Serverはsocket()、bind()、listen()、accept()の順に処理し、ClientとのConnectionを受け付けます。Data送受信後はclose()でFile Descriptorを閉じます。

TCP ServerのSocket処理

socket()でSocketを作る

socket()は指定されたProtocol FamilyとSocket Typeに対応するKernel Socketを作成し、File Descriptorを返します。この時点では通常、ServerのLocal Portはまだ割り当てられていません。

bind()でAddressとPortを割り当てる

bind()はSocketへLocal IP AddressとPortを割り当てます。Serverが192.0.2.10:8080へBindすると、そのAddressとPortを使用して接続を受け付ける準備ができます。

listen()で接続を待つ

listen()はTCP SocketをPassive Open側のListen Socketへ変更します。引数のbacklogは接続待ちQueueの上限に関係しますが、Applicationが指定した値だけで実際のQueue長が決まるわけではなく、Kernel設定による上限なども影響します。

Linuxではbacklogは主に3-way Handshake完了後、Applicationのaccept()を待つAccept Queueの上限に関係します。Handshake途中のConnectionを管理するSYN Queueには別のKernel設定なども関係します。

accept()でConnectionを取り出す

3-way Handshakeが完了したConnectionをaccept()すると、新しいConnected SocketのFile Descriptorが返ります。元のListen Socketはそのまま残り、新しい接続要求を待ち続けます。ServerはClientごとに作られたConnected Socketを使ってDataを送受信します。

Listen SocketとConnected Socket

Listen SocketとConnected Socketの違い

Listen Socketは新しい接続要求を受け付けるためのSocketで、通常Application Dataの送受信には使用しません。一方、accept()で返されたConnected Socketは、特定のClientとのData送受信に使用します。

複数のConnected Socketが同じServer側Local AddressとPortを持てるのは、Remote AddressやRemote Portが異なるためです。TCP ConnectionはLocal Address、Local Port、Remote Address、Remote Portの組み合わせによって区別されます。

Bind Addressの違い

Bind Addressは、SocketがどのLocal IP AddressとPortで通信を受け付けるかを指定します。

Bind Address主な意味
127.0.0.1同一HostのIPv4 Loopbackからのみ接続できる
192.0.2.10指定したIPv4 Address宛ての接続を受け付ける
0.0.0.0IPv4の利用可能なLocal Addressで接続を受け付ける
::1IPv6 Loopbackからのみ接続できる
::IPv6のUnspecified Address。IPv4も受け付けるかは設定に依存する

Serviceが127.0.0.1だけでListenしている場合、同一Hostからは接続できてもRemote Hostからは接続できません。一方、0.0.0.0でListenしていてもFirewall、Routing、Network Namespaceなどの影響によって外部から接続できない場合があります。

IPv6の::へBindしたSocketがIPv4-mapped Addressも受け付けるかは、IPV6_V6ONLY OptionやSystem設定に依存します。ssの表示だけで判断せず、Application設定と実際の接続結果を確認します。

TCP ClientのSocket処理

Clientがconnect()を実行すると、KernelはRouteなどに基づいてSource Addressを選び、Local Portが未指定であればEphemeral Portを割り当てます。その後、TCPはSYNを送信して3-way Handshakeを開始します。

TCP Clientのconnect処理

Blocking Socketでは、Handshakeが成功するかErrorになるまでconnect()が待つのが基本です。Non-blocking Socketでは接続処理中を示すEINPROGRESSが返ることがあり、poll()やepoll()などで完了を待った後、SO_ERRORを確認します。

Connection refusedは相手からRSTが返された場合などに発生し、Connection timed outは一定時間応答が得られない場合などに発生します。実際の原因を特定する際は、tcpdump、Server側のListen状態、Firewall、Routingなどを組み合わせて確認します。

Listen Queueの仕組み

LinuxのTCP Listen処理では、Handshake途中の接続要求と、Handshake完了後にApplicationのaccept()を待つConnectionが管理されます。一般にSYN QueueとAccept Queueという考え方で説明できます。

TCP Listen Queueの仕組み

SYN Queueでは主に3-way Handshake途中のConnectionを管理します。Handshakeが完了するとConnectionはAccept Queue側で管理され、Applicationがaccept()を実行するのを待ちます。

Applicationのaccept()処理が追いつかずAccept Queueが満杯に近づくと、新しい接続の遅延や失敗につながる場合があります。原因はTCP設定だけでなく、Application Thread不足、CPU高負荷、Downstream待ち、File Descriptor上限なども考えられます。

また、SYN Cookieが使用される状況では、SYN Floodなどに対してHandshake途中の状態を通常とは異なる方法で扱う場合があります。Listen Queueを調査する際は、Kernel設定や負荷状況も含めて確認します。

Socket Send BufferとReceive Buffer

Socketには送信Dataを管理するSend Bufferと、受信DataをApplicationが読み取るまで保持するReceive Bufferがあります。TCPのSend Bufferでは、Applicationから受け取った送信Dataに加え、送信済みでもACKを受信していないDataが再送に備えて管理されます。

Socket Send BufferとReceive Buffer

Applicationの送信速度がNetworkや受信側の処理速度より速いと、Send BufferへDataが溜まり、書き込みが待たされる場合があります。一方、受信ApplicationがDataを読み取らないとReceive Bufferが埋まり、TCPはReceive Windowを縮小して送信側の送信量を抑制します。

SO_SNDBUFとSO_RCVBUFでSocket単位のBuffer Sizeを指定できますが、Kernelの自動調整、System上限、内部Accountingなども関係します。Bufferを大きくすれば常に性能が向上するわけではなく、Memory使用量やQueueing Delayも含めて評価する必要があります。

BlockingとNon-blocking I/O

Blocking Socketでは、recv()を実行した時点でDataがなければ通常ProcessがSleepし、Data到着、Connection終了、Signal、Errorなどを待ちます。送信時もSend Bufferに十分な空きがなければ、書き込み可能になるまで待つことがあります。

Non-blocking Socketでは、すぐに処理できない場合にEAGAINまたはEWOULDBLOCKが返り、System Callは待たずに戻ります。Applicationはpoll()、select()、epoll()などを利用してFile Descriptorの準備状態を監視し、ReadyになったSocketを処理できます。

項目特徴主な用途
Blocking Socket処理可能になるまでSystem Callが待つ単純なClient、Thread型Server
Non-blocking Socket処理できなければすぐに戻るEvent-driven処理
epoll多数のFile DescriptorのReady状態を監視高Connection数のLinux Server

Readableという通知は、1回のread()やrecv()でApplication Message全体を取得できることを保証するものではありません。TCPはByte Streamなので、Application Protocolで定義された長さや区切りに従って、必要に応じて複数回読み取ります。また、Writableであっても任意量のDataを無制限に書き込めるわけではありません。

Socketの終了とError

close()はFile Descriptorを閉じるSystem Callです。最後の参照が閉じられたとき、TCPでは未送信DataやSocket Optionなどの状態に応じて、FINによる通常のConnection終了やRSTを伴う終了へ進みます。

shutdown()を使うと送信方向または受信方向を個別に閉じることができます。TCPはFull Duplex通信のため、送信を終了した後も相手からのDataを受信するHalf-closeが可能です。

recv()が0を返した場合、TCP PeerがFINを送信し、その前までのDataを読み終えたことを示します。Connection ResetではECONNRESET、切断後の書き込みではEPIPEやSIGPIPEが関係する場合があります。ApplicationではSystem Callの戻り値とerrnoを適切に処理する必要があります。

LinuxでSocketを確認する

Linuxでは、ssコマンドを使って現在のSocketやTCP Connectionを確認できます。

Server ApplicationがどのIP Address・Portで接続を待っているのかを調べる場合にもよく利用します。

TCPのListen Socketを確認する

TCPのListen Socketを確認するには、次のコマンドを実行します。

ss -lntp

各Optionの意味は次のとおりです。

Option意味
-lListen状態のSocketを表示
-nAddressやPortを名前解決せず数値で表示
-tTCP Socketを表示
-pSocketを使用しているProcessを表示

たとえば、次のような結果が表示されます。

ss -lntpの実行例

特に確認したいのがLocal Address:Portです。この例では127.0.0.1:8085でTCP Connectionを待ち受けています。

0.0.0.0:22のように表示されている場合は、IPv4では基本的に利用可能なすべてのLocal AddressのPort 22で待ち受けていることを意味します。

UDPの待受Socketを確認する

UDPの場合は-tの代わりに-uを指定します。

ss -lnup

各Optionの意味は次のとおりです。

Option意味
-l待受Socketを表示
-nAddressやPortを名前解決せず数値で表示
-uUDP Socketを表示
-pSocketを使用しているProcessを表示

たとえば、次のような結果が表示されます。

ss -lnupの実行例

UDPはTCPのようにConnectionを確立しないため、TCPのLISTENに相当するConnection Stateはありません。

ss -lntpはTCPのListen Socket、ss -lnupはUDPの待受Socketを確認するコマンド、と整理すると分かりやすいです。

特定のPortだけ確認する

たとえばPort 8080をListenしているTCP Socketを確認する場合は、対象Portに絞り込んで確認できます。

ssで特定Portを確認する例

Server Applicationが起動しているのに接続できない場合は、まずssで「想定したAddressとPortで本当にListenしているか」を確認すると、切り分けに役立ちます。

TCP Connectionと内部状態を確認する

コマンド何を見るか用途
ss -ntpTCP Connection一覧基本的なConnection確認
ss -ntp state establishedESTABLISHEDのみ通信中Connectionの確認
ss -ntp state close-waitCLOSE-WAITのみPeer切断後のclose処理などを調査
ss -ntmiTCP内部情報・Socket MemoryRTT、cwnd、Queue、Memoryなどの詳細調査

ss -ntpで基本的なTCP Connectionを確認し、より詳しく調査したい場合はss -ntmiを使うと、TCP内部情報やSocket Memoryまで確認できます。

ProcessのFile Descriptorを確認する

/proc/<PID>/fdをlsコマンドで確認すると、特定Processが現在開いているFile Descriptorの一覧を表示できます。

proc PID fdでFile Descriptorを確認する例

上記の実行結果における14や15はTomcat Process内のFile Descriptor番号で、socket:[2799238332]の数値部分はKernel側でSocketに関連付けられたinode番号です。

実際の調査では、File DescriptorだけでなくAddressやPortなども確認できるlsofを利用すると分かりやすい場合があります。

lsof -nP -p <PID>
Option意味説明
-nNo hostname resolutionIP AddressをHost名へ名前解決しない
-PNo port name conversionPort番号をService名へ変換しない
-p <PID>Process ID指定したPIDのProcessだけを表示

コマンド実行例を記載します。

lsofによるSocket確認例

指定したProcessが開いているFile、Library、Device、Socketなどを一覧で確認できます。TCP/UDP Socketであれば、File Descriptorに加えてLocal / Remote Address、Port、Connection Stateなどを確認できるため、ProcessとNetwork Connectionの対応を調査する際に便利です。

Linux Socketでよくある障害と確認ポイント

現象主な確認ポイント
Connection refused対象PortのLISTEN、Bind Address、RST送信元
Connection timed outRouting、Firewall、SYN / SYN-ACKの到達
Address already in use同じAddress / Portを使用するSocket、Socket Option、残存状態
Too many open filesProcess / SystemのFile Descriptor上限、FD Leak
Acceptが遅いListen Queue、Application Thread、CPU、Downstream
Send-Qが増えるPeerの受信状況、Network Loss、Window、Application Protocol
Recv-Qが増えるLocal ApplicationがDataを十分な速度で読み取れているか
CLOSE-WAITが増えるPeer切断後にLocal ApplicationがSocketをcloseしているか

Processが起動しているだけではServiceが利用可能とは限りません。まず期待するNetwork Namespace内で、正しいAddressとPortへBindし、LISTENしているかを確認します。その後、Firewall、Routing、Packetの到達状況などを順番に調査します。

大量のSocketやQueue滞留が発生している場合、症状だけを解消する目的でTimeoutや上限値を変更すると、根本原因を隠してしまう可能性があります。ApplicationのConnection Lifecycle、Error処理、Queue、File Descriptor使用量などを先に確認することが重要です。

Socket処理の全体フロー

TCP Socket処理の全体フロー

ServerではListen SocketとConnected Socketが別に存在します。Clientのconnect()によってTCP 3-way Handshakeが開始され、Handshakeが完了したConnectionをServer Applicationがaccept()で取り出します。

ApplicationはConnected SocketのFile Descriptorを使ってDataを送受信します。その裏側ではKernelがSocket BufferやTCPのProtocol Stateを管理し、TCP/IP Stackを通じてNetworkとのData送受信を行います。

まとめ

LinuxのNetwork通信では、ApplicationはSocketを通じてKernelのTCP/IP Stackを利用します。Socketは単なるIP AddressとPortの組み合わせではなく、File Descriptor、Protocol State、Send / Receive Bufferなどと関連するKernelの通信Endpointです。

  • SocketはApplicationとKernelのNetwork機能をつなぐ通信Endpoint
  • ApplicationはFile Descriptorを使ってSocketを操作する
  • TCP Serverはsocket() → bind() → listen() → accept()の流れでConnectionを受け付ける
  • Listen SocketとConnected Socketは役割が異なる
  • Listen QueueやSocket Bufferの状態は接続性能や通信性能に影響する
  • Blocking / Non-blockingによってApplicationのSocket処理方式が異なる
  • Linuxではss、lsof、/proc/<PID>/fdなどを使ってSocketの状態を確認できる

Socketの仕組みを理解しておくと、「接続できない」「Connectionが大量に残る」「Send-QやRecv-Qが増える」といったNetwork障害を、Application、Socket、TCP/IP、Networkの各Layerに分けて切り分けやすくなります。

Linuxにおけるネットワークの仕組みについては、以下の書籍にまとめています。興味がありましたら、ぜひ読んでみてください。

Linux入門・図解

Linuxのネットワークの仕組みを
図解で理解したい方へ

Socket・TCP・ルーティング・NIC 現場で役立つ通信処理の基礎知識など Linuxにおけるネットワークの仕組みを図解を中心にわかりやすく解説しています。

Kindleで書籍を見る →

コメント