この記事では、クライアント→DNS→アプリケーションサーバの通信の流れを解説します。
軽く勉強していた人や、情報処理技術者試験で触れている人も多いと思います。
しかし、実際の内部の仕組みを詳しく説明できる人は少ないのではないでしょうか。
今回はOS周りの動作まで掘り下げて話していこうと思います。
1. ネットワークの基礎
1.1そもそもの通信ってどういう仕組??
まず、インターネットを経由してサーバと通信するとき、コンピュータは何の情報をもとに情報をサーバに届けていけばよいでしょうか。
人間で考えると、手紙を送付したいとき、相手の住所をもとに手紙を届けていくと思います。
コンピュータも原理は同じで、コンピュータの場合IPアドレスがその住所の代わりになります。
最終地点に届けるには厳密にはIPアドレスだけでなくポート番号というものが必要。
これは、IPアドレスだけだと、マンションの場所までしか特定できなくて、最終終着点の部屋番号を特定する必要がある。その部屋番号に相当するものがポート番号です。

この状態だとIPアドレスを使用してサーバ自体にアクセスしていますが、ポート番号を指定していないため、サーバが提供するどのアプリケーションにアクセスするのかわかりません。
-
ポート番号を指定しない場合、多くのアプリは「決められたデフォルトポート」に自動で接続する
-
デフォルトポート以外でサービスが動いている場合は、明示的にポート番号を指定しないと接続できない
-
サーバには複数のサービスがあるため、「IPアドレスだけ」ではどのサービスにアクセスしたいのか判断できない
なので、IPアドレスとポート番号のセットで特定のアプリケーションにアクセスできるというわけです。
1.2 IPアドレスだけでは人間が不便という話
さて、IPアドレスとポート番号の説明が終わったところで、
皆さん、ブラウザからAppleのサイトにアクセスするとき、検索窓にいちいちIPアドレスを入力してアクセスしますか??
毎回Appleにアクセスする度、AppleのIPアドレスである 23.214.76.246 を打つの正直だるくないですか..?
法則性があるわけでもないですし、現代の情報社会で、サービスごとにこの数字の羅列を覚えるのは人間のできる所業ではありません。
しかしインターネットが普及する前、研究機関や軍事用途で限られた人だけが使っていた時代(1960年代~1980年代初頭)はドメインやDNS(ドメインネームシステム)が開発されていなかったので、IPを手打ちで入力し、サーバにアクセスしていました。
インターネットの歴史をわかりやすく解説!世界と日本の通信事情まとめ
2.1 DNS(ドメインネームシステム)の誕生
そこで、1983年に人間が視覚的にわかるようにしようと考えられ、誕生したのがDNS(ドメイン・ネーム・システム)です!
例えばAppleのサイトだと、ドメイン名はwww.apple.comです。
ドメインとIPアドレスが紐づいていれば、人間がAppleサイトのIPアドレスを知らなくても、www.apple.comさえ覚えていれば、このサービスにIPアドレスを使用してアクセスすることができるのです。
2.2 クライアントコンピュータがDNSサーバに問い合わせる処理
しかし、コンピュータは0,1しか扱えないコンピュータです。
どうやってこの文字列のドメインからIPアドレスを入手するのでしょう。
答えは簡単で、DNSサーバというところに、www.apple.comのIPアドレスを教えて下さい。と問い合わせをすればIPアドレスを入手する事ができます。
この問い合わせをする処理、つまりプログラムのことをDNSリゾルバといいます。
このDNSリゾルバはOSやネットワークの機器に組み込まれており、ユーザが直接操作することはありません。
せっかくなので、このDNSリゾルバが、どういうソースコードになってるのか見てみましょう。
import socket
domain = "www.apple.com"
try:
ip_address = socket.gethostbyname(domain)
print(f"{domain} のIPアドレスは {ip_address} です")
except socket.gaierror:
print(f"{domain} のIPアドレスを取得できませんでした")
www.apple.com のIPアドレスは 23.214.76.246 です
まず、socketというライブラリをインポートしています。
つまりDNSリゾルバはsocketライブラリに包括されてる機能ということがわかります。
このsocketライブラリはネットワーク機能を呼び出すためのプログラムのライブラリ(部品集)です。
このsocketライブラリを使用して、
ip_address=socket.gethostbyname(domain)
と書くと、gethostbynameの処理の中でOS(プロトコル・スタック) がDNSサーバに問い合わせを行い、DNSサーバから返ってきたIPアドレスがプロトコル・スタックを経由して、リゾルバに渡され、変数ip_addressに戻り値として格納され、アプリケーションで利用できるようになります。
プロトコル・スタックとは、OS内部に組み込まれたネットワーク制御ソフトウェアです。
大まかなフローは下の図のようになってます。

このとき、問い合わせるDNSサーバって世の中に無数にあるけど、クライアント側(PC側)はどうやって問い合わせるDNSサーバを決定しているのでしょう。
これは、デフォルトでPCに問い合わせ先のDNSサーバを設定します。
socket.gethostbyname("www.apple.com") で取得したIPアドレス(例: 23.214.76.246)をブラウザのアドレスバーに直接入力しても、
多くの場合、期待通りに www.apple.com のWebページにはアクセスできません。
考えられる主な理由を挙げます。
-
- CDNやロードバランサによるIP分散
-
- バーチャルホスト(SNI/Hostヘッダ)問題
-
- HTTPSの証明書エラー
- 今回は解説のため、アクセスできるという前提で話を進めていきます。
--- 発展 ---
実は、Appleも1つのIPアドレスで複数のサービスを運用しています。そのため、IPアドレスだけではどのサービスにアクセスしたいのかを特定できません。
仕組みの流れ:
- DNSで
www.apple.comを調べると、AppleのIPアドレスが返ってきます。 - ブラウザやクライアントは、そのIPアドレスにアクセスします。
- このとき、HTTPリクエストの中には
Host: www.apple.comというヘッダー情報が含まれています。 - Apple側のサーバは、このHostヘッダーを確認して「このリクエストはwww.apple.com用だ」と判断し、正しいWebページを返します。
このような仕組みを「バーチャルホスト」と呼び、Webサービスの世界ではとても一般的です。
今回は、IPアドレスを直接入力してもAppleのWebサイトにアクセスできる前提で解説しています。
2.3 DNSサーバの構成
次はDNSサーバってどうなってるの?について触れていきます。
DNSサーバには中に、DNSデータベース(ゾーンファイルとも呼ばれる)というものを持っています。
一般的なRDMSのテーブルを想像してもらうとわかりやすいです。
テーブルがあり、各レコードが1データとして存在しています。
実際にLinuxでDNS確認コマンドを打ってみて確認してみましょう。
dig www.apple.comを打ち込んでみます。
情報が少し多いので注目するべきところのみ抜粋します。
;; ANSWER SECTION:
www.apple.com. 278 IN CNAME www-apple-com.v.aaplimg.com.
www-apple-com.v.aaplimg.com. 278 IN CNAME www.apple.com.edgekey.net.
www.apple.com.edgekey.net. 278 IN CNAME e6858.dsce9.akamaiedge.net.
e6858.dsce9.akamaiedge.net. 5 IN A 23.214.76.246
;; Query time: 30 msec
;; SERVER: 10.255.255.254#53(10.255.255.254) (UDP)
上記の4つANSWER SECTIONの中の、e6858.dsce9.akamaiedge.net
のレコードを見ていきましょう。
まず一番右のにあるe6858.dsce9.akamaiedge.netは問い合わせ内容のドメイン名にあたります。
その隣、5はTTLです。
さらに隣、INはクラスと呼ばれるものです。
DNSの仕組みが考案されたとき、インターネット以外のネットワークの利用も想定されていて、それを識別するためインターネットを表すINがデータとしてあります。
今はインターネット以外のネットワークは消滅したらしいのでクラスは常にINとなります。
その隣のAはタイプになります。
これは名前にどういうタイプ(種類)の情報が対応づけられているのかを表します。
Aなら名前にIPアドレスがひもづきます。
MXならメールの配送先、CNAMEはのドメイン名は、別の正規ドメイン名(カノニカルネーム)の別名(エイリアス)ですよ」と示すものです。
つまり一番左の項目が問い合わせのドメイン名、一番右がその応答になるということです。
これが大まかなDNSサーバの構成になります。
しかし、PCに設定されてるDNSサーバが必ずしもほしいドメインの情報が記録されてるとは限りません。ドメインは無限にあるので事実上不可能です。
DNSを使った問い合わせは、階層構造を利用して実現しています。
2.4 DNSサーバを使った名前解決の流れ(階層構造)
www.apple.comは以下の階層に分かれます。
普段私達がドメインを見るとき、
www.apple.comを見ますが、実はcomの次にルート・ドメインがあるのですが、省略されています。
明示する場合はwww.apple.com.とします。
最後にピリオドを付けるとそれがルート・ドメインを表すということですね。
前の方に出した、
;; ANSWER SECTION:
www.apple.com. 278 IN CNAME www-apple-com.v.aaplimg.com.
www-apple-com.v.aaplimg.com. 278 IN CNAME www.apple.com.edgekey.net.
www.apple.com.edgekey.net. 278 IN CNAME e6858.dsce9.akamaiedge.net.
e6858.dsce9.akamaiedge.net. 5 IN A 23.214.76.246
;; Query time: 30 msec
;; SERVER: 10.255.255.254#53(10.255.255.254) (UDP)
にも、www.apple.com.とピリオドがついてることがわかりますね。
上記図のように、上から問い合わせが走るというわけです。
つまりwww.apple.comの場合、後ろからcom->apple->wwwとなるわけです。
察しが良い方は気づいたかもしれませんが、DNSサーバはルート・ドメインの情報を知る必要があります。
でも大丈夫です、ルート・ドメインは世界に6個しかないので、インターネットに存在するDNSサーバにすべて記録可能です。こうすることで、クライアントからどこかのDNSサーバにアクセスすれば、ルート・ドメインのサーバから目的のサーバにアクセス可能というわけです。
そして、DNSサーバにはキャッシュという便利な機能があります。
DNSキャッシュとは、一度問い合わせたDNSの結果(ドメイン名とIPアドレスの対応情報)を、一定時間サーバやPCに保存しておく仕組みです。
一度調べたドメイン名のIPアドレスをキャッシュしておくことで、次回同じドメインにアクセスするときに、DNSサーバに再度問い合わせる必要がなくなります。そのため、Webページの表示が速くなります。
そして、毎回全ての問い合わせを上位のDNSサーバに投げていたら、世界中のDNSサーバがパンクしてしまいます。キャッシュがあれば、同じ問い合わせは自分の中で解決できるので、全体のトラフィックや負荷が減ります。
DNSサーバを使用した名前解決の説明が終わりました。
次はDNSサーバから取得したIPアドレスを使用してどのように通信を行うのか見ていきましょう。
3.1 プロトコル・スタックを利用してデータを送受信する
DNSサーバから調べてきたIPアドレスをもとに、OS内部にあるプロトコル・スタックに依頼します。
先程のDNSサーバの問い合わせと同じように、Socketライブラリから、プロトコル・スタックを呼び出し、宛先IPアドレスまで通信します。
まずは図でイメージしてみましょう。
このパイプのようなもの、出入り口をソケットと呼びますが、
通信にはソケットを作り、パイプを確立する必要があります。
実際にはサーバがソケットを作り、クライアントからの接続要求を待ちます。そしてこのソケットに対応するよう紐づけてポート番号で管理します。
そしてクライアントは自身のソケットを作り、そのソケットからパイプを伸ばしてサーバのソケットにつなげてパイプを確立します。
そうして、クライアント - サーバ間でデータのやり取りをして、終了したらパイプを外して通信終了です。このとき、どちらから外しても良いです。
簡単なまとめ
- ソケットを作る
- サーバ側のソケットに繋いでパイプを確立
- データを送受信する
- パイプを外してソケットを消す
実際のソースコードでどういうものなのか見てみましょう
まずはサーバ側からです。
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080)) # 8080番ポートで待ち受け
server.listen(1) # 接続待ち
print('Waiting for connection...')
conn, addr = server.accept()
print('Connected by', addr)
まずは、Socketライブラリをインポートします。
そして、
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
で、サーバ用のソケットオブジェクトを作成します。
第一引数に、IPv4を使う指定(IPアドレス割当てに関するプロトコル)します。
第二引数に、TCPを使うと指定しています。
server.bind(('0.0.0.0', 8080))
これはこのソケットを、0.0.0.0(すべてのIPアドレス)の8080ポートに紐づけますとバインド、つまり自身のサーバに届いた8080ポート宛の通信はこのソケットで受け取るという意味です。
server.listen(1)
これは、ソケットが接続要求を受け付ける状態になります。
1は「最大で1つの接続要求を待ち受ける」という意味。
print('Waiting for connection...')
通信を待っています、と出力
conn, addr = server.accept()
クライアントから接続要求が来るまで待機します。
接続が来たら、conn(クライアントとの通信に使う新しいソケット)とaddr(接続元のアドレス情報)が返されます。
print('Connected by', addr)
接続してきたクライアントのアドレス(IPとポート)を表示します。
サーバ側はこのようなプログラムになっています。
では、クライアント側(PC側)も見てみましょう。
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('サーバのIPアドレス', 8080)) # サーバのIPとポートに接続
同じくSocketライブラリをインポートして、ソケットを作成。
そしてDNSサーバから取得してきたIPアドレス、使うサービスのポート番号を指定して、.connect()メソッドを使用し接続します。
ちなみに、この
client.connect(('サーバのIPアドレス', 8080))の部分で、OS内部で
3ウェイハンドシェイクというものが行われています。
TCP 3ウェイハンドシェイク
上記リンクがわかりやすいので、ご参照ください。
ディスクリプタとは??
次に、ディスクリプタについても説明しようと思います。
HTTPのポート番号80でサーバが待ち受けている場合、複数のクライアントが同時にアクセスしてくるのが一般的です。
このとき、サーバはポート番号80で接続要求を受け付けるためのソケット(リッスンソケット)を持っています。
クライアントが接続してくると、OSは新しい接続ごとに新しいディスクリプタ(整数値のID)を発行します。
サーバアプリケーションは、このディスクリプタを使って、どのクライアントとの通信なのかを識別し、やりとりを行うことができます。
つまり、ポート番号80で待っているソケットは1つですが、アクセスしてきたクライアントごとにディスクリプタが割り当てられるため、複数の通信を同時に管理できるのです。割り当てられるため、複数の通信を同時に管理できるのです。
Socketライブラリやプロトコル・スタックのデータの送受信。
パイプがつながれば、その先にデータを送れば相手に届くということになります。
しかしアプリケーションはソケットに直接アクセスすることはできないので、Socketライブラリを通して、プロトコル・スタックに依頼することになります。
具体的には、アプリケーションは送信データを持っています。そしてSocketライブラリからwriteメソッドを呼び出すときに、発行したディスクリプタと送信データを指定します。
そうすると、プロトコル・スタックが送信データをサーバに向けて送信します。
そしたら応答がサーバから来ます。
その応答を格納するためのメモリ領域を確保し、データを保存するというわけです。
import socket
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.bind(('0.0.0.0', 80))
server_sock.listen()
# クライアントAの接続受付
client_sock1, addr1 = server_sock.accept()
# クライアントBの接続受付
client_sock2, addr2 = server_sock.accept()
# それぞれのソケット(ディスクリプタ)から受信
data1 = client_sock1.recv(1024) # クライアントAからの応答
data2 = client_sock2.recv(1024) # クライアントBからの応答
上記コードではディスクリプタ番号は明示されていませんが、Socketオブジェクトであるclient_sock1、client_sock2には内部にディスクリプタを保持しています。
そして、data1 = client_sock1.recv(1024)でサーバからの応答を対応するソケットオブジェクト(ディスクリプタも内包)のrecvで受け取るというわけです。
ちなみに1024は受信データのバッファ数です。
つまり、最大1024バイトのデータを受信するということになります。
そして、データのやり取りが一通り終わったら、切断フェーズに入ります。
# クライアントAのソケットをクローズ
client_sock1.close()
# クライアントBのソケットをクローズ
client_sock2.close()
# サーバソケットもクローズ(待ち受け終了)
server_sock.close()
クライアントのソケットとサーバのソケット、それぞれの違いは
- サーバソケットは「待ち受け専用」(listen/accept用)
- クライアントソケットは「通信専用」(send/recv用)
ここまでが通信の基礎技術の一部になります。
具体的にもっと掘り下げることもできるのですが、Qiitaの記事としてだいぶ大規模になってしまうので、また記事をかけたらなと思います。
技術書とかで見るネットワークの知識だけでは、深堀りすると、なかなか抽象的で理解が難しいところもあります。
実際のソースコードを見てみると、それらが視覚的にわかりやすくなるのではないかと思います。
かなり文章が長くなりましたが、学習の一助になれば幸いです!
下記に参考資料をまとめます。
どれもわかりやすい良記事でした。
よかったら読んでみてください!
インターネットの歴史をわかりやすく解説!世界と日本の通信事情まとめ
書籍: ネットワークはなぜつながるのか(第二版)


