Beej のインターネットソケットを利用したネットワークプログラミング入門
Brian “Beej Jorgensen” Hall
v3.1.5, Copyright © November 20, 2020
ここは何?
https://beej.us/guide/bgnet/html/ の日本語訳
参考
1 序説
おいおい、ソケットプログラミングで参ってるのか?このようなことは man
ページから理解するには少し難しすぎるのではありませんか?クールなインターネットプログラミングをしたいけど、connect()
の前に bind()
を呼ばないといけないのか、などなど、struct
のゴブをかき分けている時間はないでしょう。
さて、どうでしょう!?私はもうこの厄介なビジネスをやり遂げました。そして、この情報をみんなと共有したくてたまらないのです!あなたは正しい場所に来たのです。この文書は、平均的な有能なCプログラマーが、このネットワーク・ノイズに対処するために必要なエッジを与えてくれるはずです。
そして、チェックしてみてください。私はついに未来に追いつき、(ちょうどいいタイミングで!)IPv6 用にガイドを更新しました。お楽しみに!
1.1 対象読者
このドキュメントは、完全なリファレンスではなく、チュートリアルとして書かれています。 ソケットプログラミングを始めたばかりで、足がかりを探している人が読むと、おそらく最適なものになるでしょう。ソケットプログラミングの完全なガイドではありません。
でも、うまくいけば、あのマニュアルページが意味を持ち始めるかもしれませんね...。:-)
1.2 プラットフォームとコンパイラ
この文書に含まれるコードは、Gnu の gcc
コンパイラを使用して Linux PC でコンパイルされています。しかし、gcc
を使うプラットフォームであれば、ほぼ全てのプラットフォームでビルドできるはずです。当然ながら、Windows 用のプログラミングには適用されません。以下の Windows プログラマへの注意事項の章を参照してください。
1.3 公式ホームページと販売書籍
このドキュメントの正式な所在地は
また、サンプルコードや様々な言語への翻訳も掲載されています。
製本された印刷物("本"と呼ぶ人もいる)を購入するには、以下をご覧ください。
文書作成生活の維持に役立つので、購入はありがたいです!
1.4 Solaris/SunOS プログラマへの注意事項
Solaris または SunOS 用にコンパイルする場合、適切なライブラリをリンクするために、いくつかの特別なコマンドラインスイッチを指定する必要があります。これを行うには、コンパイルコマンドの最後に "-lnsl -lsocket -lresolv
" を以下のように追加するだけです。
$ cc -o server server.c -lnsl -lsocket -lresolv
それでもエラーが出るようなら、さらにそのコマンドラインの最後に -lxnet
を追加してみるといいでしょう。それが何をするのか正確にはわかりませんが、必要な人もいるようです。
もう一つ問題がありそうなのは、setsockopt()
の呼び出しのところです。プロトタイプは私の Linux ボックスのものとは異なるので、代わりに
int yes=1;
char yes='1';
を入力します。
私は Sunbox を持っていないので、上記の情報を検証したわけではありません---あくまでメールで教えてもらったことです。
1.5 Windows プログラマへの注意事項
このガイドの時点では、歴史的に、私が Windows をあまり好きではないという事実のために、Windows を少し非難しています。しかし、Windows は膨大なインストールベースがあり、明らかに完璧なオペレーティングシステムであることを公平にお伝えする必要があります。
不在は心を豊かにすると言いますが、この場合、私はそれが真実であると信じています。(ただ、言えることは、10数年前から個人的な仕事ではマイクロソフトの OS を使っていなかったので、ずっと幸せだということです だから、安心して、"どうぞ、ご自由に Windows を使ってください!" と言えるのです。 ...そう、歯ぎしりしながら言うんですけどね。
ですから私は、Linux や BSD、あるいは Unix のいくつかのフレーバーを、代わりに試してみることをお勧めします。
しかし、人は好きなものを好きになるものです。Windows の皆さんは、この情報が、もしあれば、いくつかの小さな変更を加えて、あなた方にも一般的に適用できることを知って喜んでいることでしょう。
Windows 用の Unix ツールの集合体である Cygwin をインストールするのもいい方法です。そうすると、これらのプログラムはすべて修正なしでコンパイルできるようになると、人づてに聞いたことがあります。
もうひとつ、Windows Subsystem for Linux も検討してみてください。これは基本的に、Windows 10 に Linux の VM 的なものをインストールすることができます。これもまた、間違いなく位置づけられるでしょう。
しかし、中には純粋な Windows のやり方で物事を進めたいと思う人もいるかもしれません。それはとてもガッツのあることで、こうすればいいんです。すぐに Unix を買ってきてください! いやいや---冗談です。最近は Windows フレンドリー(笑)なはずなんですが...。
これは、あなたがしなければならないことです(Cygwinをインストールしない限り!):まず、ここで私が言及するシステムヘッダーファイルのほとんどを無視してください。あなたがインクルードする必要があるのは、以下のものだけです。
#include <winsock.h>
待ってください!ソケットライブラリで何かする前に WSAStartup()
を呼び出す必要があります。これを行うためのコードは以下のようなものです。
#include <winsock.h>
{
WSADATA wsaData; // if this doesn't work
//WSAData wsaData; // then try this instead
// MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0:
if (WSAStartup(MAKEWORD(1,1), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed.\n");
exit(1);
}
また、Winsock ライブラリをリンクするようにコンパイラに指示する必要があります。通常、wsock32.lib
または winsock32.lib
、Winsock 2.0 の場合は ws2_32.lib
と呼ばれます。VC++ では、これは Project
メニューの Settings...
から行うことができます。Link
タブをクリックし、"Object/library modules" というタイトルのボックスを探します。そのリストに "wsock32.lib"(または、お好みの lib)を追加してください。
とか聞いています。
最後に、ソケットライブラリを使い終わったら、WSACleanup()
を呼び出す必要があります。詳しくはオンラインヘルプを参照してください。
一度これを行えば、このチュートリアルの残りの例は、いくつかの例外を除いて、概ね適用できるはずです。ひとつは、ソケットを閉じるために close()
を使うことができません。また、select()
はソケット記述子に対してのみ動作し、ファイル記述子に対しては動作しません(stdin
に対する 0
のように)。
また、CSocket
というソケットクラスもあります。詳細はコンパイラのヘルプを参照してください。
Winsock についての詳しい情報は、Winsock FAQ を読んで、そこから進んでください。
最後に、Windows には fork()
システムコールがないそうですが、これは残念ながら私の例のいくつかで使われているものです。多分、POSIX ライブラリか何かをリンクしないと動かないでしょうし、代わりに CreateProcess()
を使ってもいいでしょう。fork()
は引数を取らず、CreateProcess()
は約480億の引数を取ります。もし、そこまで気が回らないのであれば、CreateThread()
の方が少し消化しやすいでしょう。残念ながら、マルチスレッドに関する議論はこのドキュメントの範囲外です。私が語れることは限られているのですからね!
1.6 メールポリシー
メールでの質問には基本的に対応しますので、気軽に書き込んでください。ただし、返事を保証するものではありません。私はかなり忙しい生活を送っているので、どうしても質問に答えられないことがあります。そのような場合は、メッセージを削除します。個人的なことではなく、あなたが必要とする詳細な回答をする時間がないだけなのです。
原則として、質問が複雑であればあるほど、回答できる可能性は低くなります。質問を送る前に質問を絞り込み、関連する情報(プラットフォーム、コンパイラ、表示されるエラーメッセージなど、トラブルシューティングに役立ちそうなもの)を必ず記載していただければ、回答が得られる可能性は高くなります。より詳しい情報は、ESRのドキュメント、How To Ask Questions The Smart Way をお読みください。
返事がない場合は、もう少し調べてみて、答えが見つからなければ、また調べてみてください。そして調べたことまたを書き込んでください。その結果、私がお手伝いできるようになることを期待しています。
さて、手紙の書き方、書き方についておせっかいを焼いてしまいましたが、このガイドが何年にもわたって賞賛されてきたことに、私は十分に感謝していることをお伝えしておきたいと思います。このガイドが良いことに使われていると聞いて、私はとても嬉しくなりました。:-)
ありがとうございます。
1.7 ミラーリング
このサイトをミラーリングすることは、公的にも私的にも大歓迎です。もし、あなたがこのサイトを公的にミラーリングし、メインページからリンクしてほしい場合は、beej@beej.us
までご連絡ください。
1.8 翻訳者への注意事項
もし、このガイドを他の言語に翻訳したい場合は、beej@beej.us
までメールをいただければ、メインページからあなたの翻訳をリンクさせていただきます。翻訳にはあなたの名前と連絡先を自由に追加してください。
このソースマークダウン文書は UTF-8 エンコーディングを使用しています。
後述の 著作権・配布・法的事項 の章に記載されている使用許諾条件にご注意ください。
もし私に翻訳をホストして欲しいのであれば、言ってください。また、あなたがホストすることを望むなら、私はそれにリンクします;どちらでも構いません。
1.9 著作権・配布・法的事項
Beej's Guide to Network Programming is Copyright © 2019 Brian "Beej Jorgensen" Hall.
この作品は、以下のソースコードと翻訳に関する特定の例外を除き、クリエイティブ・コモンズ 表示-非営利-改変禁止 3.0 ライセンスの下に提供されています。このライセンスのコピーを見るには、以下をご覧ください。
https://creativecommons.org/licenses/by-nc-nd/3.0/
または、Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA に手紙を送ってください。
本使用許諾の "二次的著作物の禁止" 部分の具体的な例外は、次のとおりです:このガイドは、翻訳が正確である限り、どの言語にも自由に翻訳でき、ガイド全体を再印刷することができます。翻訳には、オリジナルのガイドと同じライセンスの制約が適用されます。翻訳には、翻訳者の名前と連絡先も含めることができます。
本書で紹介する C 言語のソースコードは、パブリックドメインとして許諾され、いかなるライセンス制限もありません。
教育関係者は、このガイドを生徒に勧めたり、コピーを提供したりすることを自由に推奨します。
書面による相互の合意がない限り、著者は作品を現状のまま提供し、作品に関して、明示的、黙示的、法定またはその他のいかなる種類の表明または保証(権利、商品性、特定目的への適合性、非侵害、潜在的またはその他の欠陥の不在、精度、または発見可能かどうかにかかわらず誤りの有無の保証を含むが、これに限定されない)も行わないものとします。
適用される法律で要求される範囲を除き、いかなる場合においても、著作者は、著作物の使用から生じる特別損害、付随的損害、結果的損害、懲罰的損害または典型的損害について、たとえ著作者がその損害の可能性を知らされていたとしても、いかなる法的理論によってもお客様に対して責任を負うことはないものとします。
お問い合わせは beej@beej.us
までお願いします。
1.10 献辞
このガイドを書くにあたり、過去から未来にわたって協力してくださった皆様に感謝します。そして、私がこのガイドを作るために使っているフリーソフトやパッケージを制作しているすべての人に感謝します。GNU、Linux、Slackware、vim、Python、Inkscape、pandoc、その他多数。そして最後に、文字通り何千もの人々が、改善の提案や励ましの言葉を書いてくれたことに、大きな感謝を捧げます。
このガイドを、コンピュータの世界における私の最大のヒーローであり、インスピレーターである何人かに捧げます。Donald Knuth、Bruce Schneier、W. Richard Stevens、The Woz、そして私の読者、さらにフリーおよびオープンソースソフトウェアのコミュニティ全体に捧げます。
1.11 出版情報
この本は、GNU ツールを搭載した Arch Linux のマシン上で vim エディタを使って、Markdown で書かれています。表紙の "アート" とダイアグラムは Inkscape で作成されています。Markdown は Python、Pandoc および XeLaTeX により、Liberation フォントを用いて HTML および LaTex/PDF に変換されます。このツールチェーンは 100% フリーおよびオープンソースソフトウェアで構成されています。
2 ソケットとは?
"ソケット" という言葉をよく耳にしますが、そもそも "ソケット" とは何なのでしょうか?それは、標準的な Unix のファイル記述子を使って他のプログラムと会話するための方法です。
なんと?
Ok--- Unix のハッカーが "Unix では何でもファイルなんだ!" と言ったのを聞いたことがあるかもしれません。その人が言っているのは、Unix のプログラムが何らかの I/O を行うとき、ファイル記述子に対して読み書きを行うという事実のことかもしれません。ファイル記述子は、単純に、開いているファイルに関連する整数です。しかし、このファイルは、ネットワーク接続、FIFO、パイプ、ターミナル、ディスク上のファイルなど、あらゆるものになり得ます(ここが重要)。Unix ではすべてがファイルなのです!だから、インターネット上で他のプログラムと通信したいときは、ファイル記述子を介して行うことになるんだ。と思ってください。
"ネットワーク通信のためのファイル記述子はどこで手に入るのですか、お利口さん?" というのが、今あなたが考えている最後の質問でしょうが、とにかくそれに答えてあげましょう。あなたは socket()
システムルーチンを呼び出すのです。このルーチンはソケット記述子を返すので、それを使って send()
と recv()
(man send
, man recv
)という特別なソケットコールを使って通信を行います。
"でもね!" あなたは今頃、そう叫んでいるかもしれません。" ファイル記述子なら、どうしてネプチューンの名において、通常の read()
と write()
の呼び出しでソケットを通して通信できないんだ?" と。短い答えは、"できる!" です。もっと長い答えは、"できるけど、send()
と recv()
はデータ転送をより大きく制御できる" です。
次は何?どうでしょう、ソケットにはいろいろな種類がありますね。DARPA インターネットアドレス(インターネットソケット)、ローカルノード上のパス名(Unix ソケット)、CCITT X.25 アドレス(X.25 ソケット、無視しても大丈夫)、そしておそらくあなたが実行する Unix のフレーバーに応じて他の多くの種類があります。この文書では、最初の "インターネットソケット" のみを扱います。
2.1 2種類のインターネットソケット
これは何?インターネットソケットには2種類ある?そうです。まあ、違うけど。嘘です。もっとあるんだけど、怖がらせたくなかったんだ。ここでは2種類しか話しません。ただし、この文章では、"Raw Sockets" も非常に強力なので、ぜひ調べてみてくださいと言うつもりです。
わかったよ、もう。この2つのタイプは何ですか?一つは "ストリームソケット"、もう一つは "データグラムソケット" で、以下、それぞれ "SOCK_STREAM
" "SOCK_DGRAM
" と呼ぶことがあります。データグラムソケットは "コネクションレス型ソケット" と呼ばれることもあります 。(ただし、本当に必要であれば connect()
を使用することができます。後述の connect()
を参照してください。)
ストリームソケットは、信頼性の高い双方向接続の通信ストリームです。ソケットに2つのアイテムを "1, 2" という順序で出力すると、反対側にも "1, 2" という順序で届きます。また、エラーも発生しません。実際、私はエラーフリーであることを確信しています。もし、そうでないと主張する人がいたら、耳に指を突っ込んで "ララララ" と唱えてやりたいくらいです。
何がストリーム・ソケットを使うのでしょうか?さて、皆さんは telnet
というアプリケーションをご存知でしょうか?あれはストリームソケットを使っているんです。あなたが入力した文字は、すべて入力した順番に到着する必要がありますよね?また、Webブラウザは HTTP(Hypertext Transfer Protocol)を使っていますが、これはストリームソケットを使ってページを取得します。実際、80番ポートで Web サイトに telnet して、"GET / HTTP/1.0
" と入力してリターンを2回押すと、HTML がダンプされて戻ってきますよ。
もし telnet
がインストールされておらず、インストールもしたくない場合、あるいは telnet
がクライアントとの接続にうるさい場合、ガイドには telnot
という telnet
に似たプログラムが付属しています。これは、このガイドで必要なものすべてに対してうまく機能するはずです。(なお、telnet は実際には spec'd networking protocol であり、telnot
はこのプロトコルを全く実装していません。)
ストリームソケットは、どのようにしてこの高いレベルのデータ伝送品質を実現しているのでしょうか。 それは、"TCP" として知られる "伝送制御プロトコル"(TCP の詳細については RFC 793 を参照)というプロトコルを使用しているからです。TCP はデータが順次、エラーなく到着することを確認します。"TCP" は "TCP/IP" の半分で、"IP" は "Internet Protocol"(RFC 791 を参照)の略だと聞いたことがあるかもしれません。IP は主にインターネット・ルーティングを扱い、一般にデータの完全性には責任を持ちません。
かっこいい。データグラムソケットについてはどうでしょうか?なぜコネクションレス型と呼ばれるのでしょうか?どうなっているんだ?なぜ信頼性が低いのでしょうか?データグラムを送ると、それが届くかもしれません。データグラムを送信すると、それは到着するかもしれません。もし到着すれば、パケット内のデータはエラーフリーです。
データグラムソケットもルーティングに IP を使いますが、TCP は使わず、"User Datagram Protocol"、つまり "UDP" を使います(RFC 768 を参照)。
なぜコネクションレスレスなのか?まあ、基本的には、ストリームソケットのようにオープンな接続を維持する必要がないからです。パケットを作り、その上に宛先情報を含む IP ヘッダを貼り付け、送信するだけでいいのです。コネクションは必要ありません。一般的には、TCP スタックが利用できないときや、パケットをいくつか落としても宇宙の終わりを意味しないときに使用されます。サンプルアプリケーション:tftp
(FTP の弟分のようなファイル転送プロトコル)、dhcpcd
(DHCP クライアント)、マルチプレイヤーゲーム、ストリーミングオーディオ、ビデオ会議、などなど。
"ちょっと待った!tftp
と dhcpcd
はバイナリアプリケーションをあるホストから別のホストに転送するために使われるんだ!アプリケーションが到着したときに動作することを期待するならば、データが失われることはありえない!これはどんな黒魔術なんだ?"
さて、私の人間の友人である tftp
やそれに類するプログラムは、UDP の上に独自のプロトコルを載せています。たとえば、tftp プロトコルは、送信されたパケットごとに、受信者は "受け取ったよ!" というパケット("ACK" パケット)を送り返さなければなりません。元のパケットの送信者は、例えば5秒間返信がない場合、最終的に ACK を得るまでパケットを再送信することになります。この確認手続きは、信頼性の高い SOCK_DGRAM
アプリケーションを実装する際に非常に重要です。
ゲーム、オーディオ、ビデオなどの信頼性の低いアプリケーションでは、ドロップしたパケットを無視するか、あるいは巧みに補うようにします。(Quake プレイヤーは、この効果の発現を呪われたラグという専門用語で知っていることでしょう。この場合の "呪われた" という単語は、非常に不敬な発言を意味します。)
なぜ信頼性の低い基礎プロトコルを使うのでしょうか?理由は2つ、速度とスピードです。何が無事に到着したかを追跡し、順序立てて確認したりするよりも、発射して忘れる方がずっと速いのです。チャットメッセージを送るなら、TCP は素晴らしいです。世界中のプレイヤーの位置情報を毎秒40件送るなら、1件や2件が落ちてもそれほど問題ではないので、UDP は良い選択だと思います。
2.2 低レベルのナンセンスとネットワーク理論
先ほどプロトコルの階層化について触れましたので、そろそろネットワークが実際にどのように動作するのか、そして SOCK_DGRAM
パケットがどのように構築されるのかについて、いくつかの例を挙げて説明しましょう。実際のところ、この章は読み飛ばしても大丈夫でしょう。しかし、良い背景にはなります。
子供たちよ、データカプセル化について学ぶ時間だ!これはとても重要なことです。あまりに重要なので、このチコステでネットワークの授業を受けると、このことを学ぶことになるかもしれません ;-)
。基本的にはこうです:パケットが生まれ、パケットは最初のプロトコル(例えば TFTP プロトコル)によってヘッダー(まれにフッターも)でラップ("カプセル化")され、次のプロトコル(例えば UDP)によって全体(TFTP ヘッダーも含む)が再びカプセル化され、さらに次のプロトコル(IP)によってカプセル化され、ハードウェア(物理)層(例えば Ethernet)の最終プロトコルによって再びカプセル化されます。
他のコンピュータがパケットを受信すると、ハードウェアがイーサネットヘッダを、カーネルが IP と UDP ヘッダを、TFTP プログラムが TFTP ヘッダを取り除き、ようやくデータを手に入れることができます。
これでやっと悪名高いレイヤードネットワークモデル(通称 "ISO/OSI")について語れるようになりました。このネットワークモデルは、他のモデルに比べて多くの利点を持つネットワーク機能のシステムを記述しています。例えば、データが物理的にどのように転送されるか(シリアル、シンイーサネット、AUI、何でも)を気にせずに、全く同じソケットプログラムを書くことができます。実際のネットワークハードウェアやトポロジーは、ソケットプログラマにとって透過的です。
さっそくですが、本格的なモデルのレイヤーを紹介します。ネットワーククラスの試験のために覚えておいてください。
- アプリケーション層
- プレゼンテーション層
- セッション層
- トランスポート層
- ネットワーク層
- データリンク層
- 物理層
物理層は、ハードウェア(シリアル、イーサネットなど)です。アプリケーション層は物理層から想像できる限り離れたところにあり、ユーザーがネットワークと相互作用する場所です。
さて、このモデルは、本当にやろうと思えば、自動車の修理ガイドとして使えるほど一般的なものです。Unix とより整合性のあるレイヤーモデルは、次のようなものでしょう。
- アプリケーション層 (telnet, ftp, etc.)
- ホスト間トランスポート層 (TCP, UDP)
- インターネット層 (IP and routing)
- ネットワークアクセス層 (Ethernet, wi-fi, or whatever)
この時点で、これらのレイヤーが元のデータのカプセル化に対応していることがお分かりいただけたと思います。
シンプルなパケットを作るのに、どれだけの労力が必要なのか、おわかりいただけたでしょうか?じぇじぇじぇ!そして、"cat
" を使って自分でパケットヘッダを入力しなければならないのです!冗談です。ストリームソケットでやるべきことは、データを send()
することだけです。データグラムソケットでは、あなたが選んだメソッドでパケットをカプセル化し、sendto()
で送り出すだけでいいのです。カーネルはあなたのためにトランスポート層とインターネット層を構築し、ハードウェアはネットワークアクセス層を構築します。ああ、現代の技術ですね。
というわけで、ネットワーク理論についての簡単な解説を終わります。そうそう、ルーティングについて言いたいことを全部言うのを忘れていました:何もありません!(笑)。その通り、全く話すつもりはありません。ルータはパケットを IP ヘッダに分解し、ルーティングテーブルを参照し、ブラブラブラブラ。もし本当に気になるなら、IP RFC をチェックしてみてください。もしあなたがそれについて学ぶことがなければ、まあ、あなたは生きていくでしょう。
3 IP アドレス, 構造体, データマンジング
ここからは気分転換にコードの話をするところです。
その前に、もっとノンコードの話をしましょう!イエーイ!まず最初に IP アドレスとポートについて少しお話したいと思いますので、それを整理します。それからソケット API がどのように IP アドレスや他のデータを保存し、操作するかについて話します。
3.1 IP アドレス、バージョン4と6
ベン・ケノービがまだオビワン・ケノービと呼ばれていた頃、インターネット・プロトコル・バージョン4(IPv4)と呼ばれる素晴らしいネットワーク・ルーティング・システムが存在しました。IPv4 は 4 バイト(4オクテット)で構成されるアドレスで、一般に "ドットと数字" で表記されるのが一般的でした。192.0.2.111
のように。
皆さんも一度は目にしたことがあるのではないでしょうか。
実際、この記事を書いている時点では、インターネット上のほぼすべてのサイトが IPv4 を使っています。
オビ・ワンをはじめ、誰もが幸せでした。しかし、ヴィント・サーフという名の否定的な人物が、IPv4 アドレスが足りなくなると警告を発したのです!
(ヴィント・サーフ氏は、IPv4 による "破滅と暗黒の黙示録" の到来を警告するとともに、"インターネットの父" としても有名です。だから、私は彼の判断に二の足を踏む立場にはないのです。)
アドレスが足りなくなる?そんなことがあるのでしょうか?つまり、32ビットの IPv4 アドレスには何十億もの IP アドレスが存在するのです。本当に何十億台ものコンピュータがあるのでしょうか?
Yes.
また、コンピュータが数台しかなく、10億という数字があり得ないほど大きいと誰もが思っていた当初、いくつかの大きな組織は、自分たちが使うために何百万という IP アドレスを惜しげもなく割り当てていたのです。(ゼロックス、MIT、フォード、HP、IBM、GE、AT&T、そしてアップルという小さな会社などです。)
実際、いくつかの応急処置がなかったら、とっくに使い果たしていたでしょう。
しかし今は、すべての人間が IP アドレスを持ち、すべてのコンピュータ、電卓、電話、パーキングメーター、そして(なぜか)子犬も、という時代です。
そして、IPv6 が誕生したのです。ヴィント・サーフはおそらく不死身なので(たとえ肉体がこの世を去ったとしても、おそらく彼はすでにインターネット2の奥深くにある超知的な ELIZA プログラムとして存在しているはずです)、もし次のバージョンのインターネットプロトコルで十分なアドレスが確保できなければ、誰も彼の "だから言っただろう" という言葉を再び聞きたくはないでしょう。
これは何を示唆しているのでしょうか?
もっとたくさんのアドレスが必要だということです。2倍どころか10億倍でもなく1000兆倍でもなく 7900万ビリオン・トリリオンの数のアドレスが必要なのです!そうこなくちゃ!
"Beej、それは本当なの?大きな数字を信じない理由があるんだ。" 32ビットと128ビットの差は大したことないように聞こえるかもしれない、96ビット多いだけだろ?しかし、私たちはここで累乗の話をしていることを忘れてはなりません。32ビットは約40億の数字(\(2^{32}\))を表し、128ビットは約340兆の数字(\(2^{128}\))を表します。これは、宇宙の星1つに対して、100万個の IPv4 インターネットがあるようなものです。
IPv4 のドットや数字も忘れて、16進数では、2バイトの塊をコロンで区切って、このように表現しています。
2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551
それだけではありません!多くの場合、IP アドレスにはたくさんのゼロが含まれていますが、それらを2つのコロンで区切って圧縮することができます。そして、各バイトペアの先頭のゼロを省くことができます。例えば、次のようなアドレスのペアは、それぞれ等価です。
2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::51
2001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::
0000:0000:0000:0000:0000:0000:0000:0001
::1
アドレス ::1
はループバックアドレスです。常に "今使っているこのマシン" という意味です。IPv4 では、ループバックアドレスは 127.0.0.1
です。
最後に、IPv6 アドレスの IPv4 互換モードですが、これは皆さんが遭遇する可能性のあるものです。例えば、192.0.2.33
という IPv4 アドレスを IPv6 アドレスとして表現したい場合、次のような表記をします。"::ffff:192.0.2.33
"。
本気で楽しみたいんです。
実際、IPv6 の開発者たちは、何兆個ものアドレスを軽率にも予約用に切り捨てたほど、IPv6 は楽しいものなのですが、数が多すぎて、正直言って、もう誰が数えているのでしょうか?銀河系のすべての惑星のすべての男性、女性、子供、子犬、そしてパーキングメーターのために十分な数が残されています。 信じてくれ、銀河系のどの星にもパーキングメーターはあるんだ。本当なんだ。
3.1.1 サブネット
組織の都合上、"この IP アドレスの先頭からこのビットまでがネットワーク部分、それ以外がホスト部分" と宣言するのが便利な場合があります。
例えば、IPv4 では 192.0.2.12
というように、最初の3バイトがネットワークで、最後の1バイトがホストと言えるでしょう。あるいは、別の言い方をすれば、ネットワーク 192.0.2.0
上のホスト 12
について話していることになります(ホストであるバイトをゼロにしているところをご覧ください)。
そして、さらに時代遅れの情報を!準備はいいですか?古代では、サブネットには "クラス" があり、アドレスの最初の1バイト、2バイト、3バイトがネットワーク部分でした。運良く1バイトがネットワーク、3バイトがホストの場合、ネットワーク上に24ビット分のホスト(1600万程度)を持つことができます。これが "クラス A" のネットワークです。一方、"クラス C" は、ネットワークが3バイト、ホストが1バイトで、256台のホスト(ただし、予約された数台は除く)を持ちます。
ご覧のように、クラス A がほんの少し、クラス C が大量に、そして真ん中にクラス B が何個かある状態でした。
IP アドレスのネットワーク部分は、ネットマスクと呼ばれるもので記述されており、IP アドレスとビット単位で AND することでネットワーク番号を取得します。ネットマスクは通常 255.255.255.0
のような形をしています。(例えば、このネットマスクでは、あなたの IP が 192.0.2.12
なら、あなたのネットワークは 192.0.2.12
AND 255.255.255.0
で、 192.0.2.0
となります。)
しかし、残念ながら、これではインターネットの最終的なニーズに対応できないことが判明した。C クラスのネットワークはすぐになくなってしまいました。そして、クラス A は間違いなく不足していました。ということは、わざわざ聞くまでもないでしょう。この問題を解決するために、権力者たちはネットマスクを 8、16、24 のどれでもなく、任意のビット数にすることを許可しました。例えば 255.255.255.252
というネットマスクは、30ビットのネットワークと2ビットのホストで、 ネットワーク上に4つのホストを置くことができます。(ネットマスクは常に1ビットの束と0ビットの束からなることに注意してください。)
しかし、255.192.0.0
のような大きな数字の羅列をネットマスクとして使うのは、少し扱いにくいです。まず、それが何ビットなのかが直感的にわからないし、コンパクトでもありません。そこで新スタイルが登場したのですが、これはもっとすっきりしています。IP アドレスの後にスラッシュを付けて、その後に10進数でネットワークのビット数を指定するだけです。こんな感じです。192.0.2.12/30
。
あるいは、IPv6 の場合、このようなものです。2001:db8::/32
または 2001:db8:5413:4028::9db9/64
です。
3.1.2 ポート番号
以前、インターネット層(IP)とホスト間トランスポート層(TCP と UDP)を分離したレイヤードネットワークモデルをご紹介しましたが、覚えていらっしゃいますか?次の段落の前に、そのことをしっかり覚えておいてください。
IP アドレス(IP 層で使われる)の他に、TCP(ストリームソケット)や、同時に UDP(データグラムソケット)で使われるアドレスがあることが判明したのです。それは、ポート番号です。これは16ビットの数字で、接続のためのローカルアドレスのようなものです。
IP アドレスはホテルの番地、ポート番号は部屋番号だと思ってください。後で自動車業界の例も考えてみましょう。
例えば、受信メールとウェブサービスの両方を扱うコンピュータを用意したい場合、1つの IP アドレスを持つコンピュータでその2つを区別する方法はあるでしょうか?
さて、インターネット上のサービスには、それぞれ異なるウェルノウン・ポート番号が設定されています。IANA のポート一覧か、Unix であれば /etc/services
ファイルで確認することができます。HTTP(ウェブ)はポート80、telnet はポート23、SMTP はポート25、ゲーム DOOM はポート666、などなど。1024以下のポートはしばしば特殊とみなされ、通常、使用するには OS の特別な権限が必要です。
といったところでしょうか。
3.2 バイトオーダー
レルムの命令で!バイトの並び順は2種類とします。今後、Lame and Magnificent と呼ばれるようになります。
というのは冗談ですが、本当にどちらか一方が優れているのです。:-)
あなたのコンピュータは、あなたの背後でバイトを逆順に保存しているかもしれないのです。そうなんです。誰もあなたに言いたくはなかったのです。
つまり、2バイトの16進数、たとえば b34f
を表現する場合、b3
と 4f
の2バイトに続けて格納する、というのがインターネットの世界の共通認識になっているのです。これは理にかなっているし、ウィルフォード・ブリムリーも言うように、正しい行為です。このように、大きい方の端が先になるように格納された数字をビッグエンディアン(Big-Endian)と呼びます。
残念ながら、世界中に散在する一部のコンピュータ、すなわちインテルまたはインテル互換のプロセッサを搭載したものは、バイトを逆に格納しているため、b34f
は 4f
と b3
の連続したバイトとしてメモリに格納されることになります。この記憶方式をリトルエンディアン(Little-Endian)と呼びます。
でも、ちょっと待ってください!用語の説明はまだ終わっていないのです。もっとまともなビッグエンディアンはネットワークバイトオーダーとも呼ばれ、私たちネットワーク系が好む順序だからです。
コンピュータはホストバイトオーダーで数字を記憶しています。インテル 80x86 であれば、ホストバイト順はリトルエンディアンです。モトローラ 68K の場合は、ビッグエンディアンです。PowerPC なら、ホストバイトの並びは......まあ、人それぞれですね。
パケットを作成するときやデータ構造を埋めるときに、2バイトや4バイトの数値がネットワークバイトオーダーになっていることを確認する必要があることがよくあります。しかし、ネイティブなホストバイトオーダーがわからない場合、どのようにすればよいのでしょうか。
朗報です。ホストのバイトオーダーが正しくないと仮定して、値をネットワークバ イトオーダーに設定するための関数を常に実行するようにすればよいのです。この関数は、必要であれば魔法のような変換を行い、エンディアンが異なるマシンにもコードを移植することができます。
よしよし。変換できる数値は、short
(2バイト)と long
(4バイト)の2種類です。これらの関数は、符号なしのバリエーションでも動作します。例えば、short
をホストバイトオーダーからネットワークバイトオーダーに変換したいとします。まず "h" でホスト、その後に "to" をつけます。そして、"n" は "network"、"s" は "short" を表します。h-to-n-s または htons()
(読み方: "ホストからネットワークへのショート")です。
簡単すぎるくらいに...。
"n"、"h"、"s"、"l" の組み合わせは、本当にくだらないものを除いて、すべて使うことができるのです。たとえば、stolh()
("Short to Long Host")という関数はありません---とにかく、このパーティーでは。しかし、あるのです。
関数 | 説明 |
---|---|
htons() | h ost to n etwork s hort |
htonl() | h ost to n etwork l ong |
ntohs() | n etwork to h ost s hort |
ntohl() | n etwork to h ost l ong |
基本的には、送出する前にネットワークバイトオーダーに変換し、送出後にホストバイトオーダーに変換します。
64bit のバリエーションは知らないです、すみません。また、浮動小数点をやりたい場合は、ずっと下のシリアライゼーションの章をチェックしてください。
この文書では、特に断らない限り、数値はホストバイトオーダーであると仮定しています。
3.3 構造体
さて、ついにここまで来ました。そろそろプログラミングの話をしましょう。この章では、ソケットインターフェイスで使用される様々なデータ型について説明します。
まず、簡単なものからです。ソケット記述子です。ソケット記述子は以下のような型です。
int
普通の int
です。
ここからは変な話なので、我慢して読んでください。
My First Struct™---struct addrinfo
。この構造体は最近開発されたもので、ソケットアドレス構造体を後で使用するために準備するために使用されます。また、ホスト名のルックアップやサービス名のルックアップにも使用されます。これは、後で実際の使い方を説明するときに、より意味をなすと思いますが、今は、接続を行うときに最初に呼び出されるものの1つであることを知っておいてください。
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};
この構造体を少し読み込んでから、getaddrinfo()
を呼び出します。この構造体のリンクリストへのポインタが返され、必要なものがすべて満たされます。
ai_family
フィールドで IPv4 か IPv6 を使うように強制することもできますし、AF_UNSPEC
のままにして何でも使えるようにすることも可能です。これは、あなたのコードが IP バージョンに依存しないので、クールです。
これはリンクされたリストであることに注意してください:ai_next
は次の要素を指しています---そこから選択するためにいくつかの結果があるかもしれません。私は最初にうまくいった結果を使いますが、あなたは異なるビジネスニーズを持っているかもしれません。何でもかんでも知ってるわけじゃないんです!
struct addrinfo
の ai_addr
フィールドは struct sockaddr
へのポインタであることがわかります。ここからが、IP アドレス構造体の中身についての細かい話になります。
通常、これらの構造体に書き込む必要はありません。多くの場合、addrinfo
構造体を埋めるために getaddrinfo()
を呼び出すだけでよいでしょう。しかし、これらの構造体の内部を覗いて値を取得する必要があるため、ここでそれらを紹介します。
(また、構造体 addrinfo
が発明される前に書かれたコードはすべて、これらのものをすべて手作業で梱包していたので、まさにそのような IPv4 コードを多く見かけることができます。このガイドの古いバージョンなどでもそうです。)
ある構造体は IPv4 で、ある構造体は IPv6 で、ある構造体はその両方です。どれが何なのか、メモしておきます。
とにかく、構造体 sockaddr
は、多くの種類のソケットのためのソケットアドレス情報を保持します。
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
sa_family
には様々なものを指定できますが、この文書ではすべて AF_INET
(IPv4)または AF_INET6
(IPv6)とします。sa_data
にはソケットの宛先アドレスとポート番号を指定します。sa_data
にアドレスを手で詰め込むのは面倒なので、これはかなり扱いにくいです。
構造体 sockaddr
を扱うために、プログラマは IPv4 で使用する構造体 sockaddr_in
("in" は "Internet" の意)を並列に作成しました。
sockaddr_in
構造体へのポインタは sockaddr
構造体へのポインタにキャストすることができ、その逆も可能です。つまり、connect()
が struct sockaddr*
を要求しても、struct sockaddr_in
を使用して、最後の最後でキャストすることができるのです!
// (IPv4 only--see struct sockaddr_in6 for IPv6)
struct sockaddr_in {
short int sin_family; // Address family, AF_INET
unsigned short int sin_port; // Port number
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; // Same size as struct sockaddr
};
この構造体により、ソケットアドレスの要素を簡単に参照することができます。sin_zero
(構造体を struct sockaddr
の長さに合わせるために含まれます)は、関数 memset()
ですべて 0
に設定する必要があることに注意してください。また、sin_family
は struct sockaddr
の sa_family
に相当し、"AF_INET
" に設定されることに注意します。最後に、sin_port
はネットワークバイトオーダーでなければなりません(htons()
を使用することで!)。
もっと掘り下げましょう!sin_addr
フィールドは in_addr
構造体であることがわかりますね。あれは何なんだ?まあ、大げさではなく、史上最も恐ろしい組合せの1つです。
// (IPv4 only--see struct in6_addr for IPv6)
// Internet address (a structure for historical reasons)
struct in_addr {
uint32_t s_addr; // that's a 32-bit int (4 bytes)
};
うおぉ まあ、昔はユニオンだったんだけど、今はもうそういう時代じゃないみたいだね。おつかれさまでした。つまり、ina
を struct sockaddr_in
型と宣言した場合、ina.sin_addr.s_addr
は4バイトの IP アドレス(ネットワークバイトオーダー)を参照することになります。あなたのシステムがまだ struct in_addr
のための神々しいユニオンを使用している場合でも、あなたはまだ私が上記のように全く同じ方法で4バイトの IP アドレスを参照することができます(これは #defines
によるものです)ことに注意してください。
IPv6 ではどうでしょうか。これについても同様の構造体が存在します。
// (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4)
struct sockaddr_in6 {
u_int16_t sin6_family; // address family, AF_INET6
u_int16_t sin6_port; // port number, Network Byte Order
u_int32_t sin6_flowinfo; // IPv6 flow information
struct in6_addr sin6_addr; // IPv6 address
u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 address
};
IPv4 が IPv4 アドレスとポート番号を持つように、IPv6 も IPv6 アドレスとポート番号を持つことに注意してください。
また、IPv6 フロー情報やスコープ ID のフィールドについては、今のところ触れないことに注意してください。:-)
最後になりますが、こちらもシンプルな構造体である struct sockaddr_storage
は、IPv4 と IPv6 の両方の構造体を保持できるように十分な大きさに設計されています。コールによっては、struct sockaddr
に IPv4 と IPv6 のどちらのアドレスが記入されるのか事前にわからないことがありますよね。そこで、この並列構造体を渡しますが、サイズが大きい以外は struct sockaddr
とよく似ており、必要な型にキャストします。
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
重要なのは、ss_family
フィールドでアドレスファミリーを確認できることで、これが AF_INET
か AF_INET6
(IPv4 か IPv6 か)かを確認することです。それから、必要なら struct sockaddr_in
や struct sockaddr_in6
にキャストすることができます。
3.4 IP アドレス、パート2
幸いなことに、IP アドレスを操作するための関数がたくさんあります。手書きで把握して <<
演算子で long
に詰め込む必要はありません。
まず、struct sockaddr_in ina
があり、そこに格納したい IP アドレスが 10.12.110.57
または 2001:db8:63b3:1::3490
だとしましょう。inet_pton()
という関数は、数字とドットで表記された IP アドレスを、AF_INET
か AF_INET6
の指定によって、struct in_addr
か sturct in6_addr
に変換する関数です。("pton
" は "presentation to network" の略で、覚えやすければ "printable to network" と呼んでも構いません。)変換は次のように行うことができます。
struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6
(クイックメモ:古い方法では、inet_addr()
という関数や inet_aton()
という別の関数を使っていましたが、これらはもう時代遅れで IPv6 では動きません。)
さて、上記のコードスニペットは、エラーチェックがないため、あまり堅牢ではありません。inet_pton()
はエラー時に -1
を返し、アドレスがめちゃくちゃになった場合は 0
を返します。ですから、使用する前に結果が 0
よりも大きいことを確認してください!
さて、これで文字列の IP アドレスをバイナリ表現に変換することができるようになりました。では、その逆はどうでしょうか?in_addr
構造体を持っていて、それを数字とドットの表記で印刷したい場合はどうでしょうか。この場合、関数 inet_ntop()
("ntop
" は "network to presentation" という意味です。覚えやすければ "network to printable" と呼んでも構いません)を次のように使用します。
// IPv4:
char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
呼び出す際には、アドレスの種類(IPv4 または IPv6)、アドレス、結果を格納する文字列へのポインタ、その文字列の最大長を渡すことになります。(2つのマクロは、最大の IPv4 または IPv6 アドレスを保持するために必要な文字列のサイズを都合よく保持します。INET_ADDRSTRLEN
と INET6_ADDRSTRLEN
です。)
(古いやり方についてもう一度簡単に触れておくと、この変換を行う歴史的な関数は inet_ntoa()
と呼ばれるものでした。これも時代遅れで、IPv6 では動きません。)
最後に、これらの関数は数値の IP アドレスに対してのみ動作します。"www.example.com
" のようなホスト名に対してネームサーバの DNS ルックアップは行いません。後ほど説明するように、そのためには getaddrinfo()
を使用します。
3.4.1 プライベート(またはディスコネクト)ネットワーク
多くの場所では、自分たちを守るために、ネットワークを他の地域から隠すファイアウォールがあります。そして多くの場合、ファイアウォールは、ネットワークアドレス変換(NAT)と呼ばれるプロセスを使って、"内部" IP アドレスを "外部"(世界中の誰もが知っている)IP アドレスに変換しています。
もう緊張してきましたか? "こんな変なことしてどこへ行くんだろう?"
まあ、ノンアルコール飲料でも買ってリラックスしてください。初心者の場合、NAT は透過的に行われるので、心配する必要もありませんから。しかし、あなたが見ているネットワーク番号に混乱し始めた場合に備えて、ファイアウォールの背後にあるネットワークについて話したいと思います。
例えば、私の自宅にはファイアウォールがあります。DSL 会社から割り当てられた2つの固定 IPv4 アドレスを持っていますが、ネットワーク上に7台のコンピューターがあります。どうしてこんなことが可能なのでしょうか?2台のコンピュータが同じ IP アドレスを共有することはできませんし、そうでなければデータはどちらに行けばいいのかわからなくなってしまいます。
答えは、"同じ IP アドレスを共有していない" です。2400万個の IP アドレスが割り当てられたプライベートネットワーク上にあるのです。それらはすべて私のためだけのものです。まあ、他の人たちから見れば、すべて私のためのものなのですが。ここで、何が起こっているのかを説明します。
リモートコンピューターにログインすると、ISP から提供されたパブリック IP アドレスである 192.0.2.33
からログインしていると表示されるのです。しかし、ローカルコンピューターにその IP アドレスを尋ねると、10.0.0.5
と答えるのです。誰が IP アドレスを変換しているのでしょうか?そうです、ファイアウォールです。ファイアウォールが NAT しているのです。
10.x.x.x
は、完全に切断されたネットワークか、ファイアウォールの内側にあるネットワークでのみ使用される、数少ない予約ネットワークの1つです。どのプライベート・ネットワーク番号が使用できるかの詳細は、RFC 1918 に概説されていますが、一般的によく目にするのは、10.x.x.x
と 192.168.x.x
で、x
は通常 0 ~ 255 です。一般的ではないのは、172.y.x.x
で、y
は16から31の間です。
NAT するファイアウォールの内側のネットワークは、これらの予約されたネットワークのいずれかにある必要はありませんが、一般的にはそうなっています。
(楽しい事実!私の外部 IP アドレスは、本当は 192.0.2.33
ではないのです。192.0.2.x
ネットワークは、このガイドのように、ドキュメントで使用するための架空の "本当の" IP アドレスのために予約されているのです!わーい、すごい!)
IPv6 にも、ある意味プライベートネットワークがあります。RFC 4193 にあるように、fdXX:
(将来的には fcXX:
)で始まります。しかし、NAT と IPv6 は一般的に混ざりません(このドキュメントの範囲外である IPv6 から IPv4 へのゲートウェイを行う場合を除きます)。理論的には、自由に使えるアドレスが非常に多くなるため、NAT を使用する必要はなくなるはずです。しかし、外部にルーティングしないネットワーク上で自分のためにアドレスを割り当てたい場合は、このようにします。
4 IPv4 から IPv6 へのジャンプ
しかし、IPv6 で動作させるためには、私のコードのどこを変えればいいのか知りたいのです!今すぐ教えてください!
Ok! Ok!
ここに書かれていることはほとんどすべて、私が上で説明したことですが、せっかちな人のためのショートバージョンです。(もちろん、これ以外にもありますが、このガイドに該当するのはこれです。)
-
まず、構造体を手動で詰めるのではなく、
getaddrinfo()
を使ってすべてのstruct sockaddr
の情報を取得するようにしてください。こうすることで、IP のバージョンに左右されず、また、その後の多くのステップを省くことができます。 -
IP バージョンに関連する何かをハードコーディングしていることが分かったら、ヘルパー関数でラップするようにします。
-
AF_INET
をAF_INET6
に変更します。 -
PF_INET
をPF_INET6
に変更します。 -
INADDR_ANY
の割り当てをin6addr_any
の割り当てに変更し、若干の差異が生じます。struct sockaddr_in sa; struct sockaddr_in6 sa6; sa.sin_addr.s_addr = INADDR_ANY; // use my IPv4 address sa6.sin6_addr = in6addr_any; // use my IPv6 address
また、
IN6ADDR_ANY_INIT
という値は、struct in6_addr
を宣言する際にイニシャライザーとして以下のように使用することができます。struct in6_addr ia6 = IN6ADDR_ANY_INIT;
-
struct sockaddr_in
の代わりにstruct sockaddr_in6
を使用し、必要に応じてフィールドに "6" を追加してください(上記の 構造体の章を参照)。sin6_zero
フィールドはありません。 -
struct in_addr
の代わりにstruct in6_addr
を使用し、必要に応じてフィールドに "6" を追加してください(上記の 構造体の章を参照)。 -
inet_aton()
やinet_addr()
の代わりに、inet_apton()
を使用してください。 -
inet_ntoa()
の代わりにinet_ntop()
を使用してください。 -
gethostbyname()
の代わりに、優れたgetaddrinfo()
を使用してください。 -
gethostbyaddr()
の代わりに、優れたgetnameinfo()
を使用してください(gethostbyaddr()
は IPv6 でも動作可能です)。 -
INADDR_BROADCAST
は動作しなくなりました。代わりに IPv6 マルチキャストを使用してください。
出来上がり!
5 システムコールかバスト
この章では、Unix マシンのネットワーク機能にアクセスするためのシステムコールやその他のライブラリコールに触れることができますし、ソケット API をサポートしているあらゆるマシン(BSD, Windows, Linux, Mac, など)も同様です。これらの関数を呼び出すと、カーネルが引き継ぎ、すべての作業を自動で行ってくれます。
このあたりで多くの人がつまづくのは、これらのものをどのような順序で呼び出すかということです。これについては、皆さんもお分かりのように、man
ページが役に立ちません。そこで、この恐ろしい状況を改善するために、以下の章のシステムコールを、あなたがプログラムの中で呼び出す必要があるのと全く(おおよそ)同じ順序で並べることにしました。
これに、あちこちにあるサンプルコード、ミルクとクッキー(自分で用意しなければならないのが怖い)、そして生粋のガッツと勇気があれば、ジョン・ポステルの息子のようにインターネット上でデータを発信することができるのです!
(なお、以下の多くのコードでは、簡潔にするため、必要なエラーチェックは行っていません。また、getaddrinfo()
の呼び出しが成功し、リンクリストの有効なエントリを返すと仮定することが非常に一般的です。これらの状況はいずれもスタンドアロン・プログラムで適切に対処されているので、それらをモデルとして使用してください。)
5.1 getaddrinfo()
---起動の準備をしよう!
この関数は多くのオプションを持つ真の主力関数ですが、使い方はいたってシンプルです。後で必要な構造体をセットアップするのに役立ちます。
昔は、gethostbyname()
という関数を使って DNS のルックアップを行っていました。そして、その情報を sockaddr_in
構造体に手作業でロードし、それを呼び出しに使用するのです。
これは、ありがたいことに、もう必要ありません。(IPv4 と IPv6 の両方で動作するコードを書きたいのであれば、望ましいことではありません!)現代では、DNS やサービス名のルックアップなど、あらゆる種類の良いことをやってくれる getaddrinfo()
という関数があり、さらに必要な struct
も埋めてくれます!
それでは、ご覧ください!
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct addrinfo **res);
この関数に3つの入力パラメータを与えると、結果のリンクリストである res
へのポインタが得られます。
node
パラメータには、接続先のホスト名、または IP アドレスを指定します。
次にパラメータ service
ですが、これは "80" のようなポート番号か、"http", "ftp", "telnet", "smtp" などの特定のサービスの名前(IANAポートリストや Unix マシンの /etc/services
ファイルで見つけることができます)であることができます。
最後に、hints
パラメータは、関連情報をすでに記入した struct addrinfo
を指します。
以下は、自分のホストの IP アドレス、ポート 3490 をリッスンしたいサーバの場合の呼び出し例です。これは実際にはリスニングやネットワークの設定を行っていないことに注意してください。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo now points to a linked list of 1 or more struct addrinfos
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // free the linked-list
ai_family
を AF_UNSPEC
に設定することで、IPv4 や IPv6 を使うかどうかを気にしないことを表明していることに注意してください。もし、どちらか一方だけを使いたい場合は、AF_INET
または AF_INET6
に設定することができます。
また、AI_PASSIVE
フラグがあるのがわかると思いますが、これは getaddrinfo()
にローカルホストのアドレスをソケット構造体に割り当てるように指示しています。これは、ハードコードする必要がないのがいいところです。(あるいは、getaddrinfo()
の最初のパラメータとして特定のアドレスを入れることもできます。私は現在 NULL
を持っています。)
そして、呼び出しを行います。エラー(getaddrinfo()
が 0
以外を返します)があれば、ご覧のように関数 gai_strerror()
を使ってそれを表示することができます。しかし、すべてがうまくいけば、servinfo
は struct addrinfos
のリンクリストを指し、それぞれのリストには後で使用できる何らかの sturct sockaddr
が含まれています!素晴らしい!
最後に、getaddrinfo()
が快く割り当ててくれたリンクリストをすべて使い終わったら、freeaddrinfo()
を呼び出してすべてを解放することができます(そうすべきです)。
ここでは、クライアントが特定のサーバ、例えば "www.example.net" ポート 3490 に接続したい場合のサンプルコールを紹介します。繰り返しますが、これは実際には接続しませんが、後で使用する構造をセットアップしています。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
status = getaddrinfo("www.example.net", "3490", &hints, &servinfo);
// servinfo now points to a linked list of 1 or more struct addrinfos
// etc.
servinfo
は、あらゆるアドレス情報を持つリンクリストだと言い続けています。この情報を披露するために、簡単なデモプログラムを書いてみよう。この短いプログラムは、コマンドラインで指定された任意のホストの IP アドレスを表示します。
/*
** showip.c -- show IP addresses for a host given on the command line
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct addrinfo hints, *res, *p;
int status;
char ipstr[INET6_ADDRSTRLEN];
if (argc != 2) {
fprintf(stderr,"usage: showip hostname\n");
return 1;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
hints.ai_socktype = SOCK_STREAM;
if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 2;
}
printf("IP addresses for %s:\n\n", argv[1]);
for(p = res; p != NULL; p = p->ai_next) {
void *addr;
char *ipver;
// get the pointer to the address itself,
// different fields in IPv4 and IPv6:
if (p->ai_family == AF_INET) { // IPv4
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else { // IPv6
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
}
// convert the IP to a string and print it:
inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
printf(" %s: %s\n", ipver, ipstr);
}
freeaddrinfo(res); // free the linked list
return 0;
}
ご覧のように、このコードはコマンドラインで渡されたものに対して getaddrinfo()
を呼び出し、res
が指すリンクリストを埋めて、そのリストを繰り返し表示して何かを出力したりすることができます。
(そこには、IP バージョンによって異なるタイプの struct sockaddrs
を掘り下げなければならない、ちょっとした醜さがあります。申し訳ありません。他にいい方法はないかなぁ...)
サンプル走行!みんな大好きスクリーンショット。
$ showip www.example.net
IP addresses for www.example.net:
IPv4: 192.0.2.88
$ showip ipv6.example.com
IP addresses for ipv6.example.com:
IPv4: 192.0.2.101
IPv6: 2001:db8:8c00:22::171
これで、getaddrinfo()
の結果を他のソケット関数に渡して、ついにネットワーク接続を確立することができます。引き続きお読みください。
5.2 socket()
---ファイル記述子を取得しよう!
もう先延ばしにはできません。socket()
システムコールの話をしなければならないのです。以下はその書式です。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
しかし、これらの引数は何なのでしょうか?これらは、どのようなソケットが欲しいか(IPv4 か IPv6 か、ストリームかデータグラムか、TCP か UDP か)を指定することができます。
以前は、これらの値をハードコードする人がいましたが、今でも絶対にそうすることができます。(ドメインは PF_INET
または PF_INET6
、タイプは SOCK_STREAM
または SOCK_DGRAM
、プロトコルは 0
に設定すると、与えられたタイプに適したプロトコルを選択することができます。あるいは getprotobyname()
を呼んで、"tcp" や "udp" などの欲しいプロトコルを調べることもできます。)
(この PF_INET
は、struct sockaddr_in
の sin_family
フィールドを初期化するときに使用できる AF_INET
の近縁種です。実際、両者は非常に密接な関係にあり、実際に同じ値を持っているので、多くのプログラマは socket()
を呼び出して PF_INET
の代わりに AF_INET
を第一引数に渡しています。さて、ミルクとクッキーを用意して、お話の時間です。昔々、あるアドレスファミリ(AF_INET
の AF)が、プロトコルファミリ(PF_INET
の PF)で参照される複数のプロトコルをサポートするかもしれないと考えられたことがあります。しかし、そうはなりませんでした。そして、みんな幸せに暮らした、ザ・エンド。というわけで、最も正しいのは struct sockaddr_in
で AF_INET
を使い、socket()
の呼び出しで PF_INET
を使うことです。)
とにかく、もう十分です。本当にやりたいことは、getaddrinfo()
の呼び出しの結果の値を使い、以下のように直接 socket()
に送り込むことです。
int s;
struct addrinfo hints, *res;
// do the lookup
// [pretend we already filled out the "hints" struct]
getaddrinfo("www.example.com", "http", &hints, &res);
// again, you should do error-checking on getaddrinfo(), and walk
// the "res" linked list looking for valid entries instead of just
// assuming the first one is good (like many of these examples do).
// See the section on client/server for real examples.
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
socket()
は単に、後のシステムコールで使用できるソケット記述子を返すか、エラーの場合は -1
を返します。グローバル変数 errno
にはエラーの値が設定されます(詳細については errno
のマニュアルページを参照してください。また、マルチスレッドプログラムで errno
を使用する際の簡単な注意も参照してください。)
でも、このソケットは何の役に立つのでしょうか?答えは、これだけでは本当に意味がなく、もっと読み進めてシステムコールを作らないと意味がないのです。
5.3 bind()
---私はどのポートにいるのでしょうか?
ソケットを取得したら、そのソケットをローカルマシンのポートに関連付ける必要があるかもしれません。(これは、特定のポートへの接続を listen()
する場合によく行われます。多人数参加型ネットワークゲームで "192.168.5.10 ポート 3490 に接続" と指示されたときに行います。)ポート番号はカーネルが受信パケットを特定のプロセスのソケット記述子にマッチさせるために使用されます。もしあなたが connect()
を行うだけなら(あなたはクライアントであり、サーバではないので)、これはおそらく不要でしょう。とにかく読んでみてください。
bind()
システムコールの書式は以下のとおりです。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd
は socket()
が返すソケットファイル記述子です。my_addr
は自分のアドレスに関する情報、すなわちポートおよび IP アドレスを含む struct sockaddr
へのポインタです。
ふぅー。一度に吸収するのはちょっと無理がありますね。ソケットをプログラムが実行されているホスト、ポート 3490 にバインドする例を見てみましょう。
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);
AI_PASSIVE
フラグを使うことで、プログラムが動作しているホストの IP にバインドするように指示しているのです。もし、特定のローカル IP アドレスにバインドしたい場合は、AI_PASSIVE
を削除して、getaddrinfo()
の最初の引数に IP アドレスを入れてください。
bind()
もエラー時には -1
を返し、errno
にエラーの値を設定します。
多くの古いコードでは、bind()
を呼び出す前に、struct sockaddr_in
を手動でパックしています。これは明らかに IPv4 特有のものですが、IPv6 で同じことをするのを止めるものは何もありません。ただし、一般的には getaddrinfo()
を使う方が簡単になりそうです。とにかく、古いコードは次のようなものです。
// !!! THIS IS THE OLD WAY !!!
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
上記のコードでは、ローカルの IP アドレスにバインドしたい場合、s_addr
フィールドに INADDR_ANY
を代入することもできます(上記の AI_PASSIVE
フラグのようなものです)。INADDR_ANY
の IPv6 バージョンはグローバル変数 in6addr_any
で、struct sockaddr_in6
の sin6_addr
フィールドに代入されます。(変数の初期化で使用できるマクロ IN6ADDR_ANY_INIT
も存在します。)また、IN6ADDR_ANY_INIT
を使用することで、IPv6 の IP アドレスにバインドできます。
bind()
を呼ぶときにもうひとつ気をつけなければならないのは、ポート番号で下手を打たないことです。1024 以下のポートはすべて予約済みです(あなたがスーパーユーザでない限り)!それ以上のポート番号は、(他のプログラムによってすでに使われていなければ) 65535 までの任意のポート番号を使用することができます。
時々、サーバを再実行しようとすると、bind()
が "Address already in use" と言って失敗することに気がつくかもしれません。これはどういうことでしょう?それは、接続されたソケットの一部がまだカーネル内に残っていて、ポートを占有しているのです。それが消えるのを待つか(1分くらい)、次のようにポートが再利用できるようなコードをプログラムに追加します。
int yes=1;
//char yes='1'; // Solaris people use this
// lose the pesky "Address already in use" error message
if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes) == -1) {
perror("setsockopt");
exit(1);
}
bind()
について、最後にちょっとした注意点があります。bind()
を絶対に呼び出す必要がない場合があります。リモートマシンに connect()
する際に、ローカルポートを気にしない場合(telnet のようにリモートポートだけを気にする場合)は、単に connect()
をコールすれば、ソケットが未 bind かどうかをチェックし、必要なら未使用のローカルポートに bind()
してくれます。
5.4 connect()
---やあ、こんにちは!
ちょっとだけ、あなたが telnet アプリケーションであることを仮定してみましょう。ユーザが(映画 TRON のように)ソケットファイル記述子を取得するように命令します。あなたはそれに応じ、socket()
を呼び出します。次に、ユーザはポート "23
"(標準的な telnet ポート)で "10.12.110.57
" に接続するように指示します。やったー!どうするんだ?
幸運なことに、あなたは今、connect()
の章---リモートホストに接続する方法を読んでいるところです。だから、猛烈に読み進めましょう!時間がない!
connect()
の書式は以下の通りです。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd
は socket()
コールで返される、我々の身近なソケットファイル記述子、serv_addr
は宛先ポートと IP アドレスを含む struct sockaddr
、addrlen
はサーバアドレス構造体のバイト長です。
これらの情報はすべて、getaddrinfo()
の呼び出しの結果から得ることができ、これはロックします。
だんだん分かってきたかな?ここからは聞こえないので、そうであることを祈るしかないですね。ポート 3490
の "www.example.com
" にソケット接続する例を見てみましょう。
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// connect!
connect(sockfd, res->ai_addr, res->ai_addrlen);
繰り返しになりますが、古いタイプのプログラムでは、独自の struct sockaddr_ins
を作成して connect()
に渡していました。必要であれば、そうすることができます。上の bind()
の章 で同様のことを書いています。
connect()
の戻り値を必ず確認してください。エラー時に -1
が返され、errno
という変数がセットされます。
また、bind()
を呼んでいないことに注意してください。基本的に、私たちはローカルのポート番号には関心がありません。カーネルは私たちのためにローカルポートを選択し、接続先のサイトは自動的にこの情報を取得します。心配はいりません。
5.5 listen()
---誰かコールしてくれない?
よし、気分転換の時間です。リモートホストに接続したくない場合はどうすればいいのでしょう。例えば、接続が来るのを待ち、何らかの方法でそれを処理したいとします。この処理は2段階です。まず listen()
を行い、次に accept()
を行います(後述)。
listen()
の書式はかなり単純ですが、少し説明が必要です。
int listen(int sockfd, int backlog);
sockfd
は socket()
システムコールから得られる通常のソケットファイル記述子です。backlog
は、受信キューで許可される接続の数です。これはどういう意味でしょうか?着信した接続は、accept()
(後述)するまでこのキューで待機することになりますが、このキューに入れることができる数の上限を表しているのです。ほとんどのシステムでは、この数を黙って約 20 に制限しています。おそらく、5
や 10
に設定しても大丈夫でしょう。
ここでも、いつものように listen()
はエラー時に -1
を返し、errno
をセットします。
さて、想像がつくと思いますが、サーバが特定のポートで動作するように listen()
を呼び出す前に bind()
を呼び出す必要があります。(どのポートに接続するかを仲間に伝えることができなければなりません!)ですから、もし接続を待ち受けるのであれば、一連のシステムコールは次のようになります。
getaddrinfo();
socket();
bind();
listen();
/* accept() goes here */
かなり自明なので、サンプルコードの代わりに置いておきます。(以下の accept()
の章のコードはより完全なものです。)この全体の中で本当に厄介なのは、accept()
の呼び出しです。
5.6 accept()
---"3490番ポートにコールいただきありがとうございます。"
accept()
の呼び出しはちょっと変です。これから起こることはこうです。遠く離れた誰かが、あなたが listen()
しているポートであなたのマシンに connect()
しようとするでしょう。その接続は、accept()
されるのを待つためにキューに入れられることになります。あなたは accept()
をコールし、保留中の接続を取得するように指示します。すると、この接続に使用する新しいソケットファイル記述子が返されます!そうです、1つの値段で2つのソケットファイル記述子を手に入れたことになります。元のソケットファイル記述子はまだ新しい接続を待ち続けており、新しく作成されたソケットファイル記述子はようやく send()
と recv()
を行う準備が整いました。着いたぞ!
書式は以下の通りです。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
は listen()
するソケット記述子です。簡単ですね。addr
は通常、ローカルの struct sockaddr_storage
へのポインタになります。この構造体には、着信接続に関する情報が格納されます(これにより、どのホストがどのポートからコールをかけてきたかを判断することができます)。addrlen
はローカルの整数型変数で、そのアドレスが accept()
に渡される前に sizeof(struct sockaddr_storage)
に設定されなければなりません。accept()
は、addr
に addrlen
以上のバイト数を入れることはありません。もし、それ以下のバイト数であれば、それを反映するように addrlen
の値を変更します。
何だと思いますか?accept()
はエラーが発生した場合は -1
を返し、errno
をセットします。そうだったんですか。
前回と同様、一度に吸収するのは大変なので、サンプルコードの一部をご覧ください。
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
int main(void)
{
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// !! don't forget your error checking for these calls !!
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, MYPORT, &hints, &res);
// make a socket, bind it, and listen on it:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// now accept an incoming connection:
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
// ready to communicate on socket descriptor new_fd!
.
.
.
ここでも、すべての send()
と recv()
の呼び出しに、ソケット記述子 new_fd
を使用することに注意してください。もし、一度しか接続がないのであれば、同じポートからの接続を防ぐために、listen
している sockfd
を close()
することができます。
5.7 send()
と recv()
---話せよ、ベイビー!
この2つの関数は、ストリームソケットまたは接続されたデータグラムソケットで通信を行うためのものです。通常の非接続型データグラムソケットを使いたい場合は、以下の sendto()
と recvfrom()
の章を参照する必要があります。
send()
の書式。
int send(int sockfd, const void *msg, int len, int flags);
sockfd
はデータを送信したいソケット記述子(socket()
で返されたものでも accept()
で取得したものでも可)、msg
は送信したいデータへのポインタ、len
はそのデータの長さ(バイト数)です。flags
を 0
に設定するだけです(フラグに関する詳しい情報は send()
の man ページを参照してください)。
サンプルコードとしては、以下のようなものがあります。
char *msg = "Beej was here!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.
send()
は実際に送信されたバイト数を返しますが、これは送信するように指示した数よりも少ないかもしれません!つまり、大量のデータを送信するように指示しても、それが処理しきれないことがあるのです。その場合、できる限りのデータを送信し、残りは後で送信するように指示します。send()
が返す値が len
の値と一致しない場合、残りの文字列を送信するかどうかはあなた次第だということを覚えておいてください。良いニュースはこれです。パケットが小さければ(1K以下とか)、おそらく全部を一度に送信することができるでしょう。ここでも、エラー時には -1
が返され、 errno
にはエラー番号がセットされます。
recv()
の書式は、多くの点で類似しています。
int recv(int sockfd, void *buf, int len, int flags);
sockfd
は読み込むソケット記述子、buf
は情報を読み込むバッファ、len
はバッファの最大長、flags
は再び 0
に設定できます(フラグについては recv()
の man ページを参照してください)。
recv()
は、実際にバッファに読み込まれたバイト数を返し、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
待ってください!recv()
は 0
を返すことがあります。これは、リモート側が接続を切断したことを意味します!0
という返り値は、recv()
がこのような事態が発生したことをあなたに知らせるためのものです。
ほら、簡単だったでしょう?これでストリームソケットでデータのやり取りができるようになったぞ。やったー!あなたは Unix ネットワークプログラマーです!
5.8 sendto()
と recvfrom()
---DGRAM スタイルで話して。
"これはすべて素晴らしく、ダンディーです"、"しかし、データグラムソケットを接続しないままにしておくのはどうなんだ?"、という声が聞こえてきそうです。大丈夫だ、アミーゴ。ちょうどいいものがありますよ。
データグラムソケットはリモートホストに接続されていないので、パケットを送信する前にどのような情報を与える必要があるか分かりますか?そうです!宛先アドレスです!これがそのスコープです。
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, socklen_t tolen);
見ての通り、この呼び出しは基本的に send()
の呼び出しと同じで、他に2つの情報が追加されています。to
は struct sockaddr
へのポインタで(おそらく直前にキャストした別の struct sockaddr_in
や struct sockaddr_in6
、struct sockaddr_storage
になるでしょう)、送信先の IP アドレスとポートが含まれています。tolen
は int
型ですが、単純に sizeof *to
または sizeof(struct sockaddr_storage)
に設定することができます。
宛先アドレスの構造体を手に入れるには、getaddrinfo()
や以下の recvfrom()
から取得するか、手で記入することになると思います。
send()
と同様、sendto()
は実際に送信したバイト数 (これも、送信するように指示したバイト数よりも少ないかもしれません!) を返し、エラーの場合は -1
を返します。
同様に、recv()
と recvfrom()
も類似しています。recvfrom()
の書式は以下の通りです。
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
これも recv()
と同様であるが、いくつかのフィールドが追加されています。from
はローカルの struct sockaddr_storage
へのポインタで、送信元のマシンの IP アドレスとポートが格納されます。fromlen
はローカルの int
へのポインタであり、sizeof *from
または sizeof(struct sockaddr_storage)
に初期化する必要があります。この関数が戻ったとき、fromlen
は実際に from
に格納されたアドレスの長さを含みます。
recvfrom()
は受信したバイト数を返し、エラーの場合は -1
を返します(errno
はそれに応じて設定されます)。
そこで質問ですが、なぜソケットの型として struct sockaddr_storage
を使うのでしょうか?なぜ、struct sockaddr_in
ではないのでしょうか?なぜなら、私たちは IPv4 や IPv6 に縛られたくないからです。そこで、汎用的な構造体である sockaddr_storage
を使用するのですが、これはどちらにも十分な大きさであることが分かっています。
(そこで...ここでまた疑問なのですが、なぜ struct sockaddr
自体はどんなアドレスに対しても十分な大きさがないのでしょうか?汎用 struct sockaddr_storage
を汎用 struct sockaddr
にキャストしているくらいなのに!?余計なことをしたような気がしますね。答えは、十分な大きさがなく、この時点で変更するのは問題がある、ということでしょう。だから新しいのを作ったんだ。)
データグラムソケットを connect()
すれば、すべてのトランザクションに send()
と recv()
を使用できることを覚えておいてください。ソケット自体はデータグラムソケットであり、パケットは UDP を使用しますが、ソケットインターフェイスが自動的に宛先と送信元の情報を追加してくれるのです。
5.9 close()
と shutdown()
---私の前から失せな!
ふぅー、一日中データの送受信(send()
ing と recv()
ing)をしていて、もう限界です。ソケット記述子の接続を閉じる準備ができました。これは簡単です。通常の Unix ファイル記述子の close()
関数を使えばいいのです。
close(sockfd);
これにより、それ以上のソケットへの読み書きができなくなります。リモート側でソケットの読み書きをしようとすると、エラーが発生します。
ソケットの閉じ方をもう少し制御したい場合は、shutdown()
関数を使用します。この関数では、特定の方向、あるいは両方の通信を遮断することができます(ちょうど close()
がそうであるように)。書式:
int shutdown(int sockfd, int how);
sockfd
はシャットダウンしたいソケットファイル記述子、how
は以下のいずれかです。
how | Effect |
---|---|
0 | それ以上の受信は不可 |
1 | それ以上の送信は禁止 |
2 | それ以上の送受信は禁止(close() のように) |
shutdown()
は成功すると 0
を、エラーが発生すると -1
を返します(errno
は適宜設定されます)。
データグラムソケットが接続されていない状態で shutdown()
を使用すると、それ以降の send()
および recv()
呼び出しに使用できなくなります(データグラムソケットを connect()
した場合、これらを使用できることを忘れないでください)。
shutdown()
は実際にはファイル記述子を閉じないことに注意することが重要です。ソケット記述子を解放するには、close()
を使用する必要があります。
何もないんだけどね。
(ただし、Windows と Winsock を使用している場合は、close()
ではなく closesocket()
を呼び出すべきであることを忘れないでください。)
5.10 getpeername()
---あなたは誰ですか?
この関数はとても簡単です。
あまりに簡単なので、ほとんど独自の章を設けなかったほどです。でも、とりあえずここに書いておきます。
getpeername()
関数は、接続されたストリームソケットのもう一方の端にいるのが誰であるかを教えてくれます。その書式は
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd
は接続したストリームソケットの記述子、addr
は接続の相手側の情報を保持する struct sockaddr
(または struct sockaddr_in
)へのポインタ、addrlen
は int
へのポインタであり、 sizeof *addr
または sizeof(struct sockaddr)
で初期化される必要があります。
この関数は,エラーが発生すると -1
を返し,それに応じて errno
を設定します。
アドレスがわかれば、inet_ntop()
、getnameinfo()
、gethostbyaddr()
を使って、より詳しい情報を表示したり取得したりすることができます。いいえ、ログイン名を取得することはできません。(OK、OK。相手のコンピュータで ident デーモンが動いていれば、可能です。しかし、これはこのドキュメントの範囲外です。詳しくは RFC 1413 をチェックしてください。)
5.11 gethostname()
---私は誰なのか?
getpeername()
よりもさらに簡単なのは、gethostname()
という関数です。これは、あなたのプログラムが動作しているコンピュータの名前を返します。この名前は、後述の gethostbyname()
でローカルマシンの IP アドレスを決定するために使用されます。
これ以上楽しいことはないでしょう?いくつか思いつきましたが、ソケットプログラミングには関係ないですね。とにかく、書式はこんな感じです。
#include <unistd.h>
int gethostname(char *hostname, size_t size);
引数は単純で、hostname
はこの関数が戻ったときにホスト名を格納する文字列の配列へのポインタ、size
はホスト名配列のバイト長です。
この関数は、正常に終了した場合は 0
を、エラーの場合は -1
を返し、通常通り errno
を設定します。
6 クライアント-サーバの背景
クライアント-サーバの世界なのです。ネットワーク上のあらゆることが、クライアント・プロセスとサーバ・プロセスとの対話、またはその逆を扱っています。たとえば、telnet
を考えてみましょう。ポート 23 のリモートホストに telnet
で接続すると(クライアント)、そのホスト上のプログラム(telnetd と呼ばれるサーバ)が起動します。このプログラムは、送られてきた telnet
接続を処理し、ログインプロンプトを表示するなどの設定を行います。
クライアントとサーバ間の情報のやりとりは、上の図のようにまとめられます。
クライアントとサーバのペアは、SOCK_STREAM
、SOCK_DGRAM
、その他(同じことを話している限り)何でも話すことができることに注意してください。クライアントとサーバのペアの良い例としては、telnet
/telnetd
、ftp
/ftpd
、Firefox
/Apache
などがあります。ftp
を使うときはいつも、リモートプログラム ftpd
があなたにサービスを提供します。
多くの場合、1つのマシンには1つのサーバしかなく、そのサーバは fork()
を使用して複数のクライアントを処理します。基本的なルーチンは、サーバが接続を待ち、それを accept()
し、それを処理するために子プロセスを fork()
する、というものです。これが、次の章で紹介するサンプルサーバが行っていることです。
6.1 シンプルなストリームサーバ
このサーバがすることは、ストリーム接続で Hello, world!
という文字列を送り出すだけです。このサーバをテストするために必要なことは、あるウィンドウでこのサーバを実行し、別のウィンドウからこのサーバに telnet でアクセスすることだけです。
$ telnet remotehostname 3490
ここで、remotehostname
は実行するマシンの名前です。
/*
** server.c -- a stream socket server demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
void sigchld_handler(int s)
{
// waitpid() might overwrite errno, so we save and restore it:
int saved_errno = errno;
while(waitpid(-1, NULL, WNOHANG) > 0);
errno = saved_errno;
}
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
int sockfd, new_fd; // listen on sock_fd, new connection on new_fd
struct addrinfo hints, *servinfo, *p;
struct sockaddr_storage their_addr; // connector's address information
socklen_t sin_size;
struct sigaction sa;
int yes=1;
char s[INET6_ADDRSTRLEN];
int rv;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // use my IP
if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
// loop through all the results and bind to the first we can
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("server: socket");
continue;
}
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("server: bind");
continue;
}
break;
}
freeaddrinfo(servinfo); // all done with this structure
if (p == NULL) {
fprintf(stderr, "server: failed to bind\n");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
sa.sa_handler = sigchld_handler; // reap all dead processes
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
printf("server: waiting for connections...\n");
while(1) { // main accept() loop
sin_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1) {
perror("accept");
continue;
}
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
s, sizeof s);
printf("server: got connection from %s\n", s);
if (!fork()) { // this is the child process
close(sockfd); // child doesn't need the listener
if (send(new_fd, "Hello, world!", 13, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); // parent doesn't need this
}
return 0;
}
一応、構文的にわかりやすいように、1つの大きな main()
関数にまとめてあります。もし、その方が良いと思われるなら、自由に小さな関数に分割してください。
(また、この sigaction()
全体は、あなたにとって新しいものかもしれません---それは大丈夫です。このコードは、fork()
された子プロセスが終了するときに現れるゾンビプロセスを刈り取る役割を担っているのです。ゾンビをたくさん作ってそれを刈り取らないと、システム管理者が怒りますよ。)
このサーバからデータを取得するには、次の節に記載されているクライアントを使用します。
6.2 シンプルなストリームクライアント
こいつはサーバよりもっと簡単です。このクライアントがすることはコマンドラインで指定したホスト、ポート 3490 に接続するだけです。サーバが送信する文字列を取得します。
/*
** client.c -- a stream socket client demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT "3490" // the port client will be connecting to
#define MAXDATASIZE 100 // max number of bytes we can get at once
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct addrinfo hints, *servinfo, *p;
int rv;
char s[INET6_ADDRSTRLEN];
if (argc != 2) {
fprintf(stderr,"usage: client hostname\n");
exit(1);
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
// loop through all the results and connect to the first we can
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("client: socket");
continue;
}
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("client: connect");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "client: failed to connect\n");
return 2;
}
inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
s, sizeof s);
printf("client: connecting to %s\n", s);
freeaddrinfo(servinfo); // all done with this structure
if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
perror("recv");
exit(1);
}
buf[numbytes] = '\0';
printf("client: received '%s'\n",buf);
close(sockfd);
return 0;
}
クライアントを実行する前にサーバを実行しない場合、connect()
は "Connection refused" を返すことに注意してください。非常に便利です。
6.3 データグラムソケット
UDP データグラムソケットの基本は、上記の 5.8 sendto() と recvfrom() ですでに説明したので、ここでは talker.c
と listener.c
という2つのサンプルプログラムのみを紹介します。
listener
は、ポート 4950 で入ってくるパケットを待つマシンに座っています。talker
は、指定されたマシンのそのポートに、ユーザがコマンドラインに入力したものを含むパケットを送信します。
データグラムソケットはコネクションレス型であり、パケットを無慈悲に発射するだけなので、クライアントとサーバには IPv6 を使用するように指示することにしています。こうすることで、サーバが IPv6 でリッスンしていて、クライアントが IPv4 で送信するような状況を避けることができます。(接続された TCP ストリームソケットの世界では、まだ不一致があるかもしれませんが、一方のアドレスファミリーの connect()
でエラーが発生すると、他方のアドレスファミリーの再試行が行われます。)
/*
** listener.c -- a datagram sockets "server" demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MYPORT "4950" // the port users will be connecting to
#define MAXBUFLEN 100
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
int sockfd;
struct addrinfo hints, *servinfo, *p;
int rv;
int numbytes;
struct sockaddr_storage their_addr;
char buf[MAXBUFLEN];
socklen_t addr_len;
char s[INET6_ADDRSTRLEN];
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET6; // set to AF_INET to use IPv4
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE; // use my IP
if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
// loop through all the results and bind to the first we can
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("listener: socket");
continue;
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("listener: bind");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "listener: failed to bind socket\n");
return 2;
}
freeaddrinfo(servinfo);
printf("listener: waiting to recvfrom...\n");
addr_len = sizeof their_addr;
if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1 , 0,
(struct sockaddr *)&their_addr, &addr_len)) == -1) {
perror("recvfrom");
exit(1);
}
printf("listener: got packet from %s\n",
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
s, sizeof s));
printf("listener: packet is %d bytes long\n", numbytes);
buf[numbytes] = '\0';
printf("listener: packet contains \"%s\"\n", buf);
close(sockfd);
return 0;
}
getaddrinfo()
の呼び出しで、最終的に SOCK_DGRAM
を使用していることに注意してください。また、listen()
や accept()
は必要ないことに注意してください。これは非接続型データグラムソケットを使用する利点の1つです!
/*
** talker.c -- a datagram "client" demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define SERVERPORT "4950" // the port users will be connecting to
int main(int argc, char *argv[])
{
int sockfd;
struct addrinfo hints, *servinfo, *p;
int rv;
int numbytes;
if (argc != 3) {
fprintf(stderr,"usage: talker hostname message\n");
exit(1);
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET6; // set to AF_INET to use IPv4
hints.ai_socktype = SOCK_DGRAM;
if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
// loop through all the results and make a socket
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("talker: socket");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "talker: failed to create socket\n");
return 2;
}
if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
p->ai_addr, p->ai_addrlen)) == -1) {
perror("talker: sendto");
exit(1);
}
freeaddrinfo(servinfo);
printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
close(sockfd);
return 0;
}
と、これだけです!listener
をあるマシンで実行し、次に takler
を別のマシンで実行します。それらのコミュニケーションをご覧ください!核家族で楽しめる G 級興奮体験です!
今回はサーバを動かす必要もありません。talker
はただ楽しくパケットをエーテルに発射し、相手側に recvfrom()
の準備が出来ていなければ消えてしまうのです。UDP データグラムソケットを使用して送信されたデータは、到着が保証されていないことを思い出してください!
過去に何度も述べた、もうひとつの小さなディテールを除いては、コネクテッド・データグラム・ソケットです。このドキュメントのデータグラムの章にいるので、ここでこれについて話す必要があります。例えば、talker
が connect()
を呼び出して listener
のアドレスを指定したとします。それ以降、talker
は connect()
で指定されたアドレスにのみ送信と受信ができます。このため、sendto()
と recvfrom()
を使う必要はなく、単に send()
と recv()
を使えばいいのです。
7 少し高度なテクニック
これらは本当に高度なものではありませんが、私たちがすでにカバーしたより基本的なレベルから抜け出したものです。実際、ここまでくれば、Unix ネットワークプログラミングの基本をかなり習得したと考えてよいでしょう!おめでとうございます!
さて、ここからは、より難解な事柄の勇敢な新世界に突入します。ソケットについて学ぶことができます。どうぞお楽しみに!
7.1 ブロッキング
ブロッキング。聞いたことがあると思います---さて、一体何でしょう?一言で言えば、"ブロック" は技術用語で "スリープ" のことです。上で listener
を実行したとき、パケットが到着するまでただそこに座っていることに気付いたと思います。何が起こったかというと、recvfrom()
を呼び出したのですが、データがなかったので、recvfrom()
はデータが到着するまで "block"(つまり、そこで眠る)と言われているのです。
多くの関数がブロックします。accept()
がブロックします。すべての recv()
関数がブロックします。このようなことができるのは、ブロックすることが許されているからです。最初に socket()
でソケット記述子を作成するとき、カーネルはそれをブロッキングに設定します。もし、ソケットをブロッキングさせたくなければ、fcntl()
を呼び出す必要があります。
#include <unistd.h>
#include <fcntl.h>
.
.
.
sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
.
ソケットをノンブロッキングに設定することで、効果的にソケットの情報を "ポーリング" することができます。ノンブロッキングソケットから読み込もうとしたときに、そこにデータがない場合、ブロックすることは許されません---その際には -1
が返り、errno
には EAGAIN
または EWOULDBLOCK
がセットされます。
(待てよ--EAGAIN
や EWOULDBLOCK
を返すこともあるのか?どちらをチェックする?仕様では実際にあなたのシステムがどちらを返すかは指定されていないので、移植性のために両方チェックしましょう。)
しかし、一般的に言って、この種のポーリングは悪い考えです。ソケットのデータを探すためにプログラムをビジーウェイト状態にすると、流行遅れのように CPU 時間を吸い取られてしまうからです。読み込み待ちのデータがあるかどうかを確認するための、よりエレガントなソリューションが、次の poll()
の章で紹介されています。
7.2 poll()
---同期式 I/O 多重化
本当にやりたいことは、一度にたくさんのソケットを監視して、データの準備ができたものを処理することです。そうすれば、すべてのソケットを継続的にポーリングして、どれが読み込み可能な状態にあるかを確認する必要がなくなります。
警告:
poll()
は膨大な数のコネクションを持つ場合、恐ろしく遅くなります。そのような状況では、システムで利用可能な最も高速なメソッドを使用しようとする libevent のようなイベントライブラリの方が良いパフォーマンスを得ることができるでしょう。
では、どうすればポーリングを回避できるのでしょうか。少し皮肉なことに、poll()
システムコールを使えばポーリングを避けることができます。簡単に言うと、オペレーティングシステムにすべての汚い仕事を代行してもらい、どのソケットでデータが読めるようになったかだけを知らせてもらうのです。その間、我々のプロセスはスリープして、システムリソースを節約することができます。
一般的なゲームプランは、どのソケット記述子を監視したいか、どのような種類のイベントを監視したいかという情報を struct pollfd
の配列として保持することです。OS は、これらのイベントのいずれかが発生するか(例えば "socket ready to read!")またはユーザが指定したタイムアウトが発生するまで poll()
呼び出しでブロックします。
便利なことに、listen()
しているソケットは、新しい接続が accept()
される準備ができたときに "ready to read" を返します。
雑談はこのくらいにして。これをどう使うかです?
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds
は情報の配列(どのソケットの何を監視するか)、nfds
は配列の要素数、そして timeout
はミリ秒単位のタイムアウトです。poll
はイベントが発生した配列の要素数を返します。
struct pollfd
を見てみましょう。
struct pollfd {
int fd; // the socket descriptor
short events; // bitmap of events we're interested in
short revents; // when poll() returns, bitmap of events that occurred
};
そして、その配列を用意するんだ。各要素の fd
フィールドに、監視したいソケット記述子を指定します。そして、events
フィールドには、監視するイベントの種類を指定します。
events
フィールドは、以下のビット単位の OR です。
Macro | 説明 |
---|---|
POLLIN | このソケットで recv() のためのデータが準備できたときに警告します。 |
POLLOUT | このソケットにブロックせずにデータを send() できるようになったら警告します。 |
一旦 struct pollfd
の配列を整えたら、それを poll()
に渡すことができます。配列のサイズと、ミリ秒単位のタイムアウト値も一緒に渡してください。(タイムアウトに負の値を指定すると、永遠に待つことができます。)
poll()
が返った後、revents
フィールドをチェックして、POLLIN
または POLLOUT
がセットされているかどうかを確認し、イベントが発生したことを示すことができます。
(実際には poll()
の呼び出しでできることはもっとたくさんあります。詳細は以下の poll()
man ページを参照してください。)
ここでは、標準入力からデータを読み込めるようになるまで、つまり RETURN
を押したときに 2.5 秒間待つ例を示します。
#include <stdio.h>
#include <poll.h>
int main(void)
{
struct pollfd pfds[1]; // More if you want to monitor more
pfds[0].fd = 0; // Standard input
pfds[0].events = POLLIN; // Tell me when ready to read
// If you needed to monitor other things, as well:
//pfds[1].fd = some_socket; // Some socket descriptor
//pfds[1].events = POLLIN; // Tell me when ready to read
printf("Hit RETURN or wait 2.5 seconds for timeout\n");
int num_events = poll(pfds, 1, 2500); // 2.5 second timeout
if (num_events == 0) {
printf("Poll timed out!\n");
} else {
int pollin_happened = pfds[0].revents & POLLIN;
if (pollin_happened) {
printf("File descriptor %d is ready to read\n", pfds[0].fd);
} else {
printf("Unexpected event occurred: %d\n", pfds[0].revents);
}
}
return 0;
}
poll()
が pfds
配列の中でイベントが発生した要素の数を返していることに再度注目してください。これは配列のどの要素かを教えてくれるわけではありませんが(そのためにはまだスキャンしなければなりません)、revents
フィールドが 0
以外のエントリがいくつあるかを教えてくれます(したがって、その数がわかったらスキャンをやめることができます)。
ここで、いくつかの疑問が出てくるかもしれません。poll()
に渡したセットに新しいファイル記述子を追加するにはどうしたらいいのでしょうか?これについては、単に配列に必要なだけのスペースがあることを確認するか、必要に応じて realloc()
でスペースを追加してください。
セットから項目を削除する場合はどうすればよいのでしょうか。この場合は、配列の最後の要素をコピーして、削除する要素の上に置くことができます。そして、その数をひとつ減らして poll()
に渡します。もうひとつの方法として、fd
フィールドに負の数を設定すると、poll()
はそれを無視します。
どうすれば、telnet
できるチャットサーバにまとめることができるのでしょうか?
これから行うのは、リスナーソケットを起動し、それをファイル記述子のセットに追加して poll()
に送ることです。(これは、接続があったときに読み込み可能な状態を表示します。)
そして、新しい接続を struct pollfd
配列に追加していきます。そして、容量が足りなくなったら、動的にそれを増やしていきます。
接続が終了したら、その接続を配列から削除します。
そして、ある接続が読み取り可能になったら、そこからデータを読み取り、そのデータを他のすべての接続に送ることで、他のユーザが入力した内容を見ることができるようにします。
そこで、このポール・サーバを試してみてください。あるウィンドウで実行し、他の多くのターミナルウィンドウから telnet localhost 9034
を実行してみてください。一つのウィンドウで入力したものが他のウィンドウでも(RETURNを押した後で)見られるようになるはずです。
それだけでなく、CTRL-]
を押して quit
とタイプして telnet
を終了すると、サーバは切断を検出し、ファイル記述子の配列からあなたを削除するはずです。
/*
** pollserver.c -- a cheezy multiperson chat server
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#define PORT "9034" // Port we're listening on
// Get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
// Return a listening socket
int get_listener_socket(void)
{
int listener; // Listening socket descriptor
int yes=1; // For setsockopt() SO_REUSEADDR, below
int rv;
struct addrinfo hints, *ai, *p;
// Get us a socket and bind it
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
exit(1);
}
for(p = ai; p != NULL; p = p->ai_next) {
listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (listener < 0) {
continue;
}
// Lose the pesky "address already in use" error message
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
close(listener);
continue;
}
break;
}
freeaddrinfo(ai); // All done with this
// If we got here, it means we didn't get bound
if (p == NULL) {
return -1;
}
// Listen
if (listen(listener, 10) == -1) {
return -1;
}
return listener;
}
// Add a new file descriptor to the set
void add_to_pfds(struct pollfd *pfds[], int newfd, int *fd_count, int *fd_size)
{
// If we don't have room, add more space in the pfds array
if (*fd_count == *fd_size) {
*fd_size *= 2; // Double it
*pfds = realloc(*pfds, sizeof(**pfds) * (*fd_size));
}
(*pfds)[*fd_count].fd = newfd;
(*pfds)[*fd_count].events = POLLIN; // Check ready-to-read
(*fd_count)++;
}
// Remove an index from the set
void del_from_pfds(struct pollfd pfds[], int i, int *fd_count)
{
// Copy the one from the end over this one
pfds[i] = pfds[*fd_count-1];
(*fd_count)--;
}
// Main
int main(void)
{
int listener; // Listening socket descriptor
int newfd; // Newly accept()ed socket descriptor
struct sockaddr_storage remoteaddr; // Client address
socklen_t addrlen;
char buf[256]; // Buffer for client data
char remoteIP[INET6_ADDRSTRLEN];
// Start off with room for 5 connections
// (We'll realloc as necessary)
int fd_count = 0;
int fd_size = 5;
struct pollfd *pfds = malloc(sizeof *pfds * fd_size);
// Set up and get a listening socket
listener = get_listener_socket();
if (listener == -1) {
fprintf(stderr, "error getting listening socket\n");
exit(1);
}
// Add the listener to set
pfds[0].fd = listener;
pfds[0].events = POLLIN; // Report ready to read on incoming connection
fd_count = 1; // For the listener
// Main loop
for(;;) {
int poll_count = poll(pfds, fd_count, -1);
if (poll_count == -1) {
perror("poll");
exit(1);
}
// Run through the existing connections looking for data to read
for(int i = 0; i < fd_count; i++) {
// Check if someone's ready to read
if (pfds[i].revents & POLLIN) { // We got one!!
if (pfds[i].fd == listener) {
// If listener is ready to read, handle new connection
addrlen = sizeof remoteaddr;
newfd = accept(listener,
(struct sockaddr *)&remoteaddr,
&addrlen);
if (newfd == -1) {
perror("accept");
} else {
add_to_pfds(&pfds, newfd, &fd_count, &fd_size);
printf("pollserver: new connection from %s on "
"socket %d\n",
inet_ntop(remoteaddr.ss_family,
get_in_addr((struct sockaddr*)&remoteaddr),
remoteIP, INET6_ADDRSTRLEN),
newfd);
}
} else {
// If not the listener, we're just a regular client
int nbytes = recv(pfds[i].fd, buf, sizeof buf, 0);
int sender_fd = pfds[i].fd;
if (nbytes <= 0) {
// Got error or connection closed by client
if (nbytes == 0) {
// Connection closed
printf("pollserver: socket %d hung up\n", sender_fd);
} else {
perror("recv");
}
close(pfds[i].fd); // Bye!
del_from_pfds(pfds, i, &fd_count);
} else {
// We got some good data from a client
for(int j = 0; j < fd_count; j++) {
// Send to everyone!
int dest_fd = pfds[j].fd;
// Except the listener and ourselves
if (dest_fd != listener && dest_fd != sender_fd) {
if (send(dest_fd, buf, nbytes, 0) == -1) {
perror("send");
}
}
}
}
} // END handle data from client
} // END got ready-to-read from poll()
} // END looping through file descriptors
} // END for(;;)--and you thought it would never end!
return 0;
}
次の章では、似たような古い関数である select()
について見ていきます。select()
と poll()
はどちらも似たような機能とパフォーマンスを持っており、どのように使うかが違うだけです。select()
の方が若干移植性が高いかもしれませんが、使い勝手は少し悪いかもしれません。あなたのシステムでサポートされている限り、一番好きなものを選んでください。
7.3 select()
---同期式 I/O 多重化、旧式
この関数、ちょっと不思議なんですが、とても便利なんです。次のような状況を考えてみましょう。あなたはサーバで、入ってくるコネクションをリッスンするだけでなく、すでに持っているコネクションを読み続けたいのです。
問題ありません。accept()
と recv()
を数回実行するだけです。そうはいかないよ、バスター!もし accept()
の呼び出しがブロックされていたらどうでしょう?どうやって recv()
を同時に行うんだ?"ノンブロッキングソケットを使いましょう!" まさか!CPU を占有するようなことはしない方がいい。じゃあ、何?
select()
は同時に複数のソケットを監視する力を与えてくれます。どのソケットが読み込み可能で、どのソケットが書き込み可能か、そしてどのソケットが例外を発生させたか、本当に知りたければ教えてくれるでしょう。
警告:
select()
は非常にポータブルですが、巨大な数の接続が発生した場合には恐ろしく遅くなります。そのような状況では、libevent のようなイベントライブラリの方が、あなたのシステムで利用可能な最も高速なメソッドを使用しようとするため、より良いパフォーマンスを得ることができることでしょう。
さっそくですが、select()
の書式を説明します。
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
この関数は、ファイル記述子の "セット"、特に readfds
、writefds
、exceptfds
を監視します。標準入力とソケット記述子 sockfd
から読み込めるかどうかを確認したい場合、ファイル記述子 0
と sockfd
を readfds
の集合に追加するだけでよいです。パラメータ numfds
には、最も大きいファイル記述子の値に 1
を足した値を設定する必要があります。この例では、標準入力(0
)よりも確実に大きいので、sockfd+1
に設定する必要があります。
select()
が戻ると、readfds
は、選択したファイル記述子のうち、どれが読み込める状態にあるかを反映するように変更されます。以下のマクロ FD_ISSET()
を用いて、それらをテストすることができます。
この先に進む前に、これらのセットを操作する方法について説明します。各セットは fd_set
型です。以下のマクロはこの型を操作します。
Function | 説明 |
---|---|
FD_SET(int fd, fd_set *set); | set に fd を追加します。 |
FD_CLR(int fd, fd_set *set); | set から fd を削除します。 |
FD_ISSET(int fd, fd_set *set); | fd が set に含まれる場合は true を返します。 |
FD_ZERO(fd_set *set); | set からすべてのエントリをクリアします。 |
最後に、この奇妙な struct timeval
とは何でしょうか?まあ、誰かがデータを送ってくるのをいつまでも待っていたくない場合もあるでしょう。例えば、96 秒ごとに "Still Going..." とターミナルに表示させたい、でも何も起きていない。この time 構造体では、タイムアウト時間を指定することができます。タイムアウト時間を超えても select()
がまだ準備のできたファイル記述子を見つけられなければ、処理を続行できるように返されます。
struct timeval
は以下のフィールドを持ちます。
struct timeval {
int tv_sec; // seconds
int tv_usec; // microseconds
};
tv_sec
に待ち時間の秒数を、tv_usec
に待ち時間のマイクロ秒数を設定するだけです。そう、これはミリ秒ではなくマイクロ秒なのです。ミリ秒の中には 1000 マイクロ秒があり、1 秒の中には 1000 ミリ秒があります。したがって、1 秒の中には 1,000,000 マイクロ秒があることになります。なぜ "usec" なのか?"u" は、私たちが "マイクロ" に使っているギリシャ文字の \(\mu\)(ミュー)に似ていると思われるからです。また、関数が戻ってきたとき、timeout
はまだ残っている時間を表示するように更新されるかもしれません。これは、あなたが使っている Unix のフレーバーに依存します。
やったー!マイクロ秒の分解能のタイマーを手に入れたぞ!まあ、当てにしない方がいいです。どんなに小さな struct timeval
を設定しても、おそらく標準的な Unix のタイムスライスの一部を待つ必要があります。
その他、気になること。もし struct timeval
のフィールドを 0
に設定すると、select()
は直ちにタイムアウトし、セット内のすべてのファイル記述子を効率よくポーリングします。パラメータ timeout
を NULL
に設定すると、決してタイムアウトせず、最初のファイル記述子が準備できるまで待ちます。最後に、特定のセットを待つことを気にしないのであれば、select()
の呼び出しの際に NULL
を設定すればよいでしょう。
次のコードでは、標準入力に何か表示されるまで 2.5 秒待ちます。
/*
** select.c -- a select() demo
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 // file descriptor for standard input
int main(void)
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
// don't care about writefds and exceptfds:
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");
else
printf("Timed out.\n");
return 0;
}
ラインバッファ端末の場合、押すキーは RETURN でないと、とにかくタイムアウトしてしまいます。
さて、この方法はデータグラムソケットでデータを待つのに最適な方法だと思う人もいるかもしれませんね。Unix の中にはこの方法で select を使えるものもあれば、使えないものもあります。試してみたいなら、ローカルの man ページに何が書いてあるか見てみるといいです。
Unix の中には、タイムアウトまでの残り時間を反映して、struct timeval
の時間を更新するものがあります。しかし、そうでないものもあります。ポータブルにしたいのであれば、そのようなことが起こることを当てにしないでください。(経過時間を追跡する必要がある場合は、gettimeofday()
を使ってください。残念なことですが、それが現実なのです。)
リードセット内のソケットがコネクションをクローズした場合はどうなるのでしょうか?その場合、select()
はそのソケット記述子を "ready to read" に設定して返します。実際にそこから recv()
を実行すると、recv()
は 0
を返します。これが、クライアントが接続を閉じたことを知るための方法です。
もうひとつ select()
について書いておくと、listen()
しているソケットがある場合、そのソケットのファイル記述子を readfds
セットに入れておけば、新しい接続があるかどうかチェックすることができます。
以上、全能の関数 select()
の概要を簡単に説明しました。
しかし、ご要望の多かった、より詳細な例をご紹介します。残念ながら、上記のごく簡単な例と、こちらの例では、大きな違いがあります。しかし、ご覧になってから、その後に続く説明をお読みください。
このプログラムは、簡単なマルチユーザチャットサーバのように動作します。一つのウィンドウで起動し、他の複数のウィンドウから telnet
("telnet hostname 9034
")で接続してください。ある telnet
セッションで何かを入力すると、他のすべてのウィンドウに表示されるはずです。
/*
** selectserver.c -- a cheezy multiperson chat server
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT "9034" // port we're listening on
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
fd_set master; // master file descriptor list
fd_set read_fds; // temp file descriptor list for select()
int fdmax; // maximum file descriptor number
int listener; // listening socket descriptor
int newfd; // newly accept()ed socket descriptor
struct sockaddr_storage remoteaddr; // client address
socklen_t addrlen;
char buf[256]; // buffer for client data
int nbytes;
char remoteIP[INET6_ADDRSTRLEN];
int yes=1; // for setsockopt() SO_REUSEADDR, below
int i, j, rv;
struct addrinfo hints, *ai, *p;
FD_ZERO(&master); // clear the master and temp sets
FD_ZERO(&read_fds);
// get us a socket and bind it
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
exit(1);
}
for(p = ai; p != NULL; p = p->ai_next) {
listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (listener < 0) {
continue;
}
// lose the pesky "address already in use" error message
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
close(listener);
continue;
}
break;
}
// if we got here, it means we didn't get bound
if (p == NULL) {
fprintf(stderr, "selectserver: failed to bind\n");
exit(2);
}
freeaddrinfo(ai); // all done with this
// listen
if (listen(listener, 10) == -1) {
perror("listen");
exit(3);
}
// add the listener to the master set
FD_SET(listener, &master);
// keep track of the biggest file descriptor
fdmax = listener; // so far, it's this one
// main loop
for(;;) {
read_fds = master; // copy it
if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
perror("select");
exit(4);
}
// run through the existing connections looking for data to read
for(i = 0; i <= fdmax; i++) {
if (FD_ISSET(i, &read_fds)) { // we got one!!
if (i == listener) {
// handle new connections
addrlen = sizeof remoteaddr;
newfd = accept(listener,
(struct sockaddr *)&remoteaddr,
&addrlen);
if (newfd == -1) {
perror("accept");
} else {
FD_SET(newfd, &master); // add to master set
if (newfd > fdmax) { // keep track of the max
fdmax = newfd;
}
printf("selectserver: new connection from %s on "
"socket %d\n",
inet_ntop(remoteaddr.ss_family,
get_in_addr((struct sockaddr*)&remoteaddr),
remoteIP, INET6_ADDRSTRLEN),
newfd);
}
} else {
// handle data from a client
if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) {
// got error or connection closed by client
if (nbytes == 0) {
// connection closed
printf("selectserver: socket %d hung up\n", i);
} else {
perror("recv");
}
close(i); // bye!
FD_CLR(i, &master); // remove from master set
} else {
// we got some data from a client
for(j = 0; j <= fdmax; j++) {
// send to everyone!
if (FD_ISSET(j, &master)) {
// except the listener and ourselves
if (j != listener && j != i) {
if (send(j, buf, nbytes, 0) == -1) {
perror("send");
}
}
}
}
}
} // END handle data from client
} // END got new incoming connection
} // END looping through file descriptors
} // END for(;;)--and you thought it would never end!
return 0;
}
このコードでは、2つのファイル記述子セットを持っていることに注意してください。master
と read_fds
です。最初の master
は、現在接続されているすべてのソケット記述子と、新しい接続を待ち受けているソケット記述子を保持します。
master
のセットを持っている理由は、select()
が実際に渡すセットを変更して、どのソケットが読み込み可能な状態にあるかを反映させるためです。ある select()
から次の select()
への呼び出しまでの接続を追跡する必要があるので、これらをどこかに安全に保存しておかなければなりません。最後の最後で、master
を read_fds
にコピーしてから select()
を呼び出します。
しかし、これでは新しい接続を得るたびに、それを master
セットに追加しなければならないのではありませんか?そうです。そして接続が終了するたびに、それを master
セットから削除しなければならないのですか?はい、その通りです。
注目すべきは、listener
ソケットが読み込み可能な状態になったかどうかをチェックしていることです。このとき、新しい接続が保留されていることを意味するので、それを accept()
して master
セットに追加します。同様に、クライアントの接続が読み込み可能な状態になったときに、recv()
が 0
を返したら、クライアントが接続を閉じたことがわかるので、master
セットからそれを削除しなければなりません。
しかし、クライアントの recv()
がゼロ以外を返した場合、何らかのデータを受信したことが分かります。そこで私はそれを取得し、master
リストを経由して、接続されている残りのすべてのクライアントにそのデータを送信します。
以上が、全能の関数 select()
の簡単でない概要です。
Linux ファンの皆さんへ:まれに、Linux の select()
が "ready-to-read" を返した後、実際には読み込む準備ができていないことがあります!これは、Linux の select()
が "ready-to-read" を返した後、実際には読み込む準備ができていないことを意味します。これはつまり、select()
が読まないと言っているのに、read()
でブロックしてしまうということです!なぜだ、この野郎---!とにかく、回避策は受信側のソケットで O_NONBLOCK
フラグをセットして、 EWOULDBLOCK
でエラーにすることです(これは発生しても無視しても大丈夫です)。ソケットをノンブロッキングに設定する方法については、fcntl()
のリファレンスページを参照してください。
さらに、ここでボーナス的な余談ですが、poll()
という別の関数があります。これは select()
とほぼ同じ動作をしますが、ファイル記述子集合を管理するシステムが異なります。チェックしてみてください!
7.4 部分的な send()
s の処理
上の send()
の章で、send()
はあなたが頼んだバイトをすべて送らないかもしれない、と言ったのを覚えていますか?つまり、512 バイト送って欲しいのに、412 バイトが返ってきたとします。残りの 100 バイトはどうなったのでしょうか?
さて、それらはまだあなたの小さなバッファの中で送信されるのを待っています。あなたがコントロールできない状況のために、カーネルはすべてのデータを1つのチャンクで送信しないことを決定しました、そして今、私の友人は、データを外に出すのはあなた次第です。
このような関数を書いてもいいんじゃないでしょうか。
#include <sys/types.h>
#include <sys/socket.h>
int sendall(int s, char *buf, int *len)
{
int total = 0; // how many bytes we've sent
int bytesleft = *len; // how many we have left to send
int n;
while(total < *len) {
n = send(s, buf+total, bytesleft, 0);
if (n == -1) { break; }
total += n;
bytesleft -= n;
}
*len = total; // return number actually sent here
return n==-1?-1:0; // return -1 on failure, 0 on success
}
この例では、s
がデータを送信するソケット、buf
がデータを格納するバッファ、len
がバッファのバイト数を格納する int
へのポインタです。
この関数は、エラーが発生すると -1
を返します(また、send()
の呼び出しによって errno
が設定されたままです)。また、実際に送信されたバイト数は len
として返されます。これは、エラーがない限り、あなたが送信するように頼んだバイト数と同じになります。sendall()
はデータを送信するために最善を尽くし、ハァハァ言っていますが、もしエラーがあればすぐに返されます。
念のため、この関数の呼び出しのサンプルを示します。
char buf[10] = "Beej!";
int len;
len = strlen(buf);
if (sendall(s, buf, &len) == -1) {
perror("sendall");
printf("We only sent %d bytes because of the error!\n", len);
}
パケットの一部が到着した場合、受信側ではどのようなことが起こるのでしょうか?パケットの長さがまちまちな場合、あるパケットが終わり、別のパケットが始まるのを受信側はどうやって知るのでしょうか?そう、現実のシナリオはロバの王道なのです。あなたはおそらくカプセル化しなければならないでしょう(冒頭のデータのカプセル化の章を覚えていますか?)詳細はこちらをご覧ください。
7.5 シリアライゼーション---データの詰め方
ネットワーク上でテキストデータを送るのは簡単ですが、int
や float
のような "バイナリ" データを送りたい場合はどうしたらよいでしょうか?その結果、いくつかの選択肢があることがわかりました。
-
数字を
sprintf()
などの関数でテキストに変換し、送信します。受信側はstrtol()
などの関数を使ってテキストを解析し、数値に戻します。 -
send()
にデータへのポインタを渡して、データをそのまま送信します。 -
数値を携帯可能な2進数にエンコードします。受信側はそれをデコードします。
スニークプレビュー!今夜だけ!
[カーテン上昇]
Beejは、"私は、上の方法3を好みます!" と言っています。
[終]
(この章を本格的に始める前に、これを行うためのライブラリは世の中に存在し、自分でローリングして移植性とエラーのない状態を維持することはかなり困難であることをお伝えしておきます。ですから、自分で実装することを決める前に、いろいろと調べて下調べをしてください。私は、このようなことがどのように機能するのかに興味がある人のために、ここに情報を記載します。)
実は、上記の方法はどれも欠点と利点があるのですが、一般的には、先ほど言ったように、私は3番目の方法を好みます。しかし、まず、他の2つの方法の欠点と利点について説明しましょう。
最初の方法は、数字をテキストとしてエンコードしてから送信するもので、電線を伝わってくるデータを簡単に印刷して読むことができるという利点があります。インターネットリレーチャット(IRC)のように、帯域幅を必要としない状況で使用するには、人間が読めるプロトコルが優れている場合もあります。しかし、変換に時間がかかるという欠点があり、その結果はほとんど常に元の数値よりも多くのスペースを取ってしまいます。
方法2:生データを渡します。これは非常に簡単です(しかし危険です!)。送信するデータへのポインタを取り、それを使って send()
を呼び出すだけです。
double d = 3490.15926535;
send(s, &d, sizeof d, 0); /* DANGER--non-portable! */
受け手はこのように受け取ります。
double d;
recv(s, &d, sizeof d, 0); /* DANGER--non-portable! */
速くて、シンプルで、いいことずくめじゃないですか。しかし、すべてのアーキテクチャが double
(あるいは int
)を同じビット表現で、あるいは同じバイト順序で表現しているわけではないことがわかりました!このコードは明らかに非移植的です。(おいおい---もしかしたら移植性は必要ないかもしれない。その場合は、これはいいし、速い。)
整数型をパッキングするとき、htons()
クラスの関数が、数値をネットワークバイトオーダーに変換することによって、いかに移植性を保つのに役立つか、そして、それがいかに正しい行為であるかをすでに見てきました。残念ながら、float
型に対する同様の関数はありません。希望は失われてしまったのでしょうか?
恐るべし!(一瞬、怖くなったか?いいえ?少しも?)私たちにできることがあります。データを既知のバイナリ形式にパックし(または "マーシャル"、"シリアライズ"、あるいは他の1億の名前のうちの1つ)、受信者がリモート側で解凍できるようにすることができるのです。
"既知のバイナリ形式" とはどういう意味でしょうか?さて、htons()
の例はもう見ましたね?これは、ホスト側のフォーマットが何であれ、数値をネットワークバイトオーダーに変更(あるいは "エンコード")します。数字を逆変換(アンエンコード)するために、受信側は ntohs()
を呼び出します。
でも、他の非整数型にはそんな関数はないって、さっき言ったばかりじゃないですか。そうです。そうなんだ。そして、C 言語にはこれを行う標準的な方法がないので、ちょっと困ったことになります(Python ファンにとってはありがたいダジャレですね)。
そのためには、データを既知の形式にパックし、それを電送してデコードする必要があります。例えば、float
をパックするために、以下は迅速で汚い方法ですが、改善の余地はたくさんあります。
#include <stdint.h>
uint32_t htonf(float f)
{
uint32_t p;
uint32_t sign;
if (f < 0) { sign = 1; f = -f; }
else { sign = 0; }
p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction
return p;
}
float ntohf(uint32_t p)
{
float f = ((p>>16)&0x7fff); // whole part
f += (p&0xffff) / 65536.0f; // fraction
if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set
return f;
}
上記のコードは、32 ビットの数値に float
を格納する素朴な実装のようなものです。上位ビット(31)は数値の符号("1" は負を意味します)を格納するために使用され、次の7ビット(30-16)は float
の整数部を格納するために使用されます。最後に残りのビット(15-0)は、数値の小数部分を格納するために使用されます。
使い方はいたって簡単です。
#include <stdio.h>
int main(void)
{
float f = 3.1415926, f2;
uint32_t netf;
netf = htonf(f); // convert to "network" form
f2 = ntohf(netf); // convert back to test
printf("Original: %f\n", f); // 3.141593
printf(" Network: 0x%08X\n", netf); // 0x0003243F
printf("Unpacked: %f\n", f2); // 3.141586
return 0;
}
プラス面は、小さくてシンプル、そして速いことです。32767 より大きい数を格納しようとすると、とても満足できるものではありません!マイナス面は、スペースを有効活用できないことと、範囲が大きく制限されることです。上の例では、小数点以下の桁数が正しく保存されていないこともおわかりいただけると思います。
代わりに何ができるのでしょうか?浮動小数点数を保存するための標準規格は IEEE-754 として知られています。ほとんどのコンピュータは内部でこのフォーマットを使って浮動小数点演算を行っているので、厳密に言えば変換する必要はないのです。しかし、ソースコードの移植性を重視するのであれば、必ずしもそのような前提は成り立ちません。(一方、高速に動作させたいのであれば、変換を行う必要のないプラットフォームでは最適化すべきです!それが htons()
やその類いの処理です。)
以下は、浮動小数点と倍数を IEEE-754 フォーマットにエンコードするコードです。(ほとんど--- NaN や Infinity はエンコードしませんが、そのように修正することができます。)
#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
#define unpack754_32(i) (unpack754((i), 32, 8))
#define unpack754_64(i) (unpack754((i), 64, 11))
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// check sign and begin normalization
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// get the normalized form of f and track the exponent
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// calculate the binary form (non-float) of the significand data
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// return the final answer
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}
long double unpack754(uint64_t i, unsigned bits, unsigned expbits)
{
long double result;
long long shift;
unsigned bias;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (i == 0) return 0.0;
// pull the significand
result = (i&((1LL<<significandbits)-1)); // mask
result /= (1LL<<significandbits); // convert back to float
result += 1.0f; // add the one back on
// deal with the exponent
bias = (1<<(expbits-1)) - 1;
shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias;
while(shift > 0) { result *= 2.0; shift--; }
while(shift < 0) { result /= 2.0; shift++; }
// sign it
result *= (i>>(bits-1))&1? -1.0: 1.0;
return result;
}
32 ビット(おそらく float
)と 64 ビット(おそらく double
)の数値のパッキングとアンパッキングのための便利なマクロをトップに置きましたが、pack754()
関数を直接呼んで bits
分のデータ(expbits
は正規化した数値の指数用に予約されています)をエンコードするように指示することができます。
以下は使用例です。
#include <stdio.h>
#include <stdint.h> // defines uintN_t types
#include <inttypes.h> // defines PRIx macros
int main(void)
{
float f = 3.1415926, f2;
double d = 3.14159265358979323, d2;
uint32_t fi;
uint64_t di;
fi = pack754_32(f);
f2 = unpack754_32(fi);
di = pack754_64(d);
d2 = unpack754_64(di);
printf("float before : %.7f\n", f);
printf("float encoded: 0x%08" PRIx32 "\n", fi);
printf("float after : %.7f\n\n", f2);
printf("double before : %.20lf\n", d);
printf("double encoded: 0x%016" PRIx64 "\n", di);
printf("double after : %.20lf\n", d2);
return 0;
}
上記のコードでは、このように出力されます。
float before : 3.1415925
float encoded: 0x40490FDA
float after : 3.1415925
double before : 3.14159265358979311600
double encoded: 0x400921FB54442D18
double after : 3.14159265358979311600
もう一つの疑問は、struct
をどのようにパックするかということです。残念ながら、コンパイラは struct
の中に自由にパディングを入れることができるので、全体を1つのチャンクでポータブルに送信することはできません。("これができない"、"あれができない" というのはもう聞き飽きた?すみません。友人の言葉を借りれば、"何か問題が起きると、いつもマイクロソフトのせいにする" ということです。これは確かにマイクロソフトのせいではないかもしれませんが、友人の発言は完全に事実です。)
話を戻すと、struct
を電線で送るには、各フィールドを独立してパックし、相手側に到着したらそれらを struct
にアンパックするのが一番良い方法です。
それは大変なことだ、とお考えでしょう。そうなんです。ひとつは、データをパックするのを手伝ってくれるヘルパー関数を書くことです。これは楽しいぞ。本当に!?
Kernighan と Pike の The Practice of Programming という本の中で、彼らは printf()
に似た関数である pack()
と unpack()
を実装し、まさにこのようなことをやっています。リンクしたいのですが、どうやらこれらの関数はこの本の他のソースと一緒にオンラインにないようです。
(The Practice of Programming は素晴らしい読み物です。ゼウスは私が勧めるたびに子猫を救ってくれます。)
この時点で、私は使ったことはありませんが、完全に立派に見える Protocol Buffers implementation in C へのポインタを落とすつもりです。Python や Perl のプログラマは、同じことを実現するために、それぞれの言語の pack()
と unpack()
関数をチェックアウトしたいと思うでしょう。また、Java には大きな Serializable インターフェースがあり、同じような方法で使用することができます。
しかし、C言語で独自のパッキングユーティリティを書きたい場合、K&P のトリックは、変数の引数リストを使って printf()
風の関数を作り、パケットを構築することです。 以下は、それを元に私が自作したバージョンですが、うまくいけば、このようなものがどのように動作するかのアイデアを与えるのに十分なものです。
(このコードは、上記の pack754()
関数を参照しています。packi*()
関数はおなじみの htons()
ファミリーと同じように動作しますが、別の整数の代わりに char
配列にパックする点が異なります。)
#include <stdio.h>
#include <ctype.h>
#include <stdarg.h>
#include <string.h>
/*
** packi16() -- store a 16-bit int into a char buffer (like htons())
*/
void packi16(unsigned char *buf, unsigned int i)
{
*buf++ = i>>8; *buf++ = i;
}
/*
** packi32() -- store a 32-bit int into a char buffer (like htonl())
*/
void packi32(unsigned char *buf, unsigned long int i)
{
*buf++ = i>>24; *buf++ = i>>16;
*buf++ = i>>8; *buf++ = i;
}
/*
** packi64() -- store a 64-bit int into a char buffer (like htonl())
*/
void packi64(unsigned char *buf, unsigned long long int i)
{
*buf++ = i>>56; *buf++ = i>>48;
*buf++ = i>>40; *buf++ = i>>32;
*buf++ = i>>24; *buf++ = i>>16;
*buf++ = i>>8; *buf++ = i;
}
/*
** unpacki16() -- unpack a 16-bit int from a char buffer (like ntohs())
*/
int unpacki16(unsigned char *buf)
{
unsigned int i2 = ((unsigned int)buf[0]<<8) | buf[1];
int i;
// change unsigned numbers to signed
if (i2 <= 0x7fffu) { i = i2; }
else { i = -1 - (unsigned int)(0xffffu - i2); }
return i;
}
/*
** unpacku16() -- unpack a 16-bit unsigned from a char buffer (like ntohs())
*/
unsigned int unpacku16(unsigned char *buf)
{
return ((unsigned int)buf[0]<<8) | buf[1];
}
/*
** unpacki32() -- unpack a 32-bit int from a char buffer (like ntohl())
*/
long int unpacki32(unsigned char *buf)
{
unsigned long int i2 = ((unsigned long int)buf[0]<<24) |
((unsigned long int)buf[1]<<16) |
((unsigned long int)buf[2]<<8) |
buf[3];
long int i;
// change unsigned numbers to signed
if (i2 <= 0x7fffffffu) { i = i2; }
else { i = -1 - (long int)(0xffffffffu - i2); }
return i;
}
/*
** unpacku32() -- unpack a 32-bit unsigned from a char buffer (like ntohl())
*/
unsigned long int unpacku32(unsigned char *buf)
{
return ((unsigned long int)buf[0]<<24) |
((unsigned long int)buf[1]<<16) |
((unsigned long int)buf[2]<<8) |
buf[3];
}
/*
** unpacki64() -- unpack a 64-bit int from a char buffer (like ntohl())
*/
long long int unpacki64(unsigned char *buf)
{
unsigned long long int i2 = ((unsigned long long int)buf[0]<<56) |
((unsigned long long int)buf[1]<<48) |
((unsigned long long int)buf[2]<<40) |
((unsigned long long int)buf[3]<<32) |
((unsigned long long int)buf[4]<<24) |
((unsigned long long int)buf[5]<<16) |
((unsigned long long int)buf[6]<<8) |
buf[7];
long long int i;
// change unsigned numbers to signed
if (i2 <= 0x7fffffffffffffffu) { i = i2; }
else { i = -1 -(long long int)(0xffffffffffffffffu - i2); }
return i;
}
/*
** unpacku64() -- unpack a 64-bit unsigned from a char buffer (like ntohl())
*/
unsigned long long int unpacku64(unsigned char *buf)
{
return ((unsigned long long int)buf[0]<<56) |
((unsigned long long int)buf[1]<<48) |
((unsigned long long int)buf[2]<<40) |
((unsigned long long int)buf[3]<<32) |
((unsigned long long int)buf[4]<<24) |
((unsigned long long int)buf[5]<<16) |
((unsigned long long int)buf[6]<<8) |
buf[7];
}
/*
** pack() -- store data dictated by the format string in the buffer
**
** bits |signed unsigned float string
** -----+----------------------------------
** 8 | c C
** 16 | h H f
** 32 | l L d
** 64 | q Q g
** - | s
**
** (16-bit unsigned length is automatically prepended to strings)
*/
unsigned int pack(unsigned char *buf, char *format, ...)
{
va_list ap;
signed char c; // 8-bit
unsigned char C;
int h; // 16-bit
unsigned int H;
long int l; // 32-bit
unsigned long int L;
long long int q; // 64-bit
unsigned long long int Q;
float f; // floats
double d;
long double g;
unsigned long long int fhold;
char *s; // strings
unsigned int len;
unsigned int size = 0;
va_start(ap, format);
for(; *format != '\0'; format++) {
switch(*format) {
case 'c': // 8-bit
size += 1;
c = (signed char)va_arg(ap, int); // promoted
*buf++ = c;
break;
case 'C': // 8-bit unsigned
size += 1;
C = (unsigned char)va_arg(ap, unsigned int); // promoted
*buf++ = C;
break;
case 'h': // 16-bit
size += 2;
h = va_arg(ap, int);
packi16(buf, h);
buf += 2;
break;
case 'H': // 16-bit unsigned
size += 2;
H = va_arg(ap, unsigned int);
packi16(buf, H);
buf += 2;
break;
case 'l': // 32-bit
size += 4;
l = va_arg(ap, long int);
packi32(buf, l);
buf += 4;
break;
case 'L': // 32-bit unsigned
size += 4;
L = va_arg(ap, unsigned long int);
packi32(buf, L);
buf += 4;
break;
case 'q': // 64-bit
size += 8;
q = va_arg(ap, long long int);
packi64(buf, q);
buf += 8;
break;
case 'Q': // 64-bit unsigned
size += 8;
Q = va_arg(ap, unsigned long long int);
packi64(buf, Q);
buf += 8;
break;
case 'f': // float-16
size += 2;
f = (float)va_arg(ap, double); // promoted
fhold = pack754_16(f); // convert to IEEE 754
packi16(buf, fhold);
buf += 2;
break;
case 'd': // float-32
size += 4;
d = va_arg(ap, double);
fhold = pack754_32(d); // convert to IEEE 754
packi32(buf, fhold);
buf += 4;
break;
case 'g': // float-64
size += 8;
g = va_arg(ap, long double);
fhold = pack754_64(g); // convert to IEEE 754
packi64(buf, fhold);
buf += 8;
break;
case 's': // string
s = va_arg(ap, char*);
len = strlen(s);
size += len + 2;
packi16(buf, len);
buf += 2;
memcpy(buf, s, len);
buf += len;
break;
}
}
va_end(ap);
return size;
}
/*
** unpack() -- unpack data dictated by the format string into the buffer
**
** bits |signed unsigned float string
** -----+----------------------------------
** 8 | c C
** 16 | h H f
** 32 | l L d
** 64 | q Q g
** - | s
**
** (string is extracted based on its stored length, but 's' can be
** prepended with a max length)
*/
void unpack(unsigned char *buf, char *format, ...)
{
va_list ap;
signed char *c; // 8-bit
unsigned char *C;
int *h; // 16-bit
unsigned int *H;
long int *l; // 32-bit
unsigned long int *L;
long long int *q; // 64-bit
unsigned long long int *Q;
float *f; // floats
double *d;
long double *g;
unsigned long long int fhold;
char *s;
unsigned int len, maxstrlen=0, count;
va_start(ap, format);
for(; *format != '\0'; format++) {
switch(*format) {
case 'c': // 8-bit
c = va_arg(ap, signed char*);
if (*buf <= 0x7f) { *c = *buf;} // re-sign
else { *c = -1 - (unsigned char)(0xffu - *buf); }
buf++;
break;
case 'C': // 8-bit unsigned
C = va_arg(ap, unsigned char*);
*C = *buf++;
break;
case 'h': // 16-bit
h = va_arg(ap, int*);
*h = unpacki16(buf);
buf += 2;
break;
case 'H': // 16-bit unsigned
H = va_arg(ap, unsigned int*);
*H = unpacku16(buf);
buf += 2;
break;
case 'l': // 32-bit
l = va_arg(ap, long int*);
*l = unpacki32(buf);
buf += 4;
break;
case 'L': // 32-bit unsigned
L = va_arg(ap, unsigned long int*);
*L = unpacku32(buf);
buf += 4;
break;
case 'q': // 64-bit
q = va_arg(ap, long long int*);
*q = unpacki64(buf);
buf += 8;
break;
case 'Q': // 64-bit unsigned
Q = va_arg(ap, unsigned long long int*);
*Q = unpacku64(buf);
buf += 8;
break;
case 'f': // float
f = va_arg(ap, float*);
fhold = unpacku16(buf);
*f = unpack754_16(fhold);
buf += 2;
break;
case 'd': // float-32
d = va_arg(ap, double*);
fhold = unpacku32(buf);
*d = unpack754_32(fhold);
buf += 4;
break;
case 'g': // float-64
g = va_arg(ap, long double*);
fhold = unpacku64(buf);
*g = unpack754_64(fhold);
buf += 8;
break;
case 's': // string
s = va_arg(ap, char*);
len = unpacku16(buf);
buf += 2;
if (maxstrlen > 0 && len >= maxstrlen) count = maxstrlen - 1;
else count = len;
memcpy(s, buf, count);
s[count] = '\0';
buf += len;
break;
default:
if (isdigit(*format)) { // track max str len
maxstrlen = maxstrlen * 10 + (*format-'0');
}
}
if (!isdigit(*format)) maxstrlen = 0;
}
va_end(ap);
}
そして、上記のコードで、あるデータを buf
にパックし、それを変数に展開するデモプログラムを以下に示します。なお、文字列の引数(フォーマット指定子 "s
")を指定して unpack()
を呼び出す場合は、バッファオーバーランを防ぐために最大長を "96s
" のように前に置くことが賢明です。ネットワーク経由で受け取ったデータを解凍するときには注意が必要です。悪意のあるユーザが、あなたのシステムを攻撃するために、うまく構成されたパケットを送るかもしれません!
#include <stdio.h>
// various bits for floating point types--
// varies for different architectures
typedef float float32_t;
typedef double float64_t;
int main(void)
{
unsigned char buf[1024];
int8_t magic;
int16_t monkeycount;
int32_t altitude;
float32_t absurdityfactor;
char *s = "Great unmitigated Zot! You've found the Runestaff!";
char s2[96];
int16_t packetsize, ps2;
packetsize = pack(buf, "chhlsf", (int8_t)'B', (int16_t)0, (int16_t)37,
(int32_t)-5, s, (float32_t)-3490.6677);
packi16(buf+1, packetsize); // store packet size in packet for kicks
printf("packet is %" PRId32 " bytes\n", packetsize);
unpack(buf, "chhl96sf", &magic, &ps2, &monkeycount, &altitude, s2,
&absurdityfactor);
printf("'%c' %" PRId32" %" PRId16 " %" PRId32
" \"%s\" %f\n", magic, ps2, monkeycount,
altitude, s2, absurdityfactor);
return 0;
}
自分でコードをロールアップするにしても、他人のコードを使うにしても、毎回手作業で各ビットをパッキングするのではなく、バグを抑えるために一般的なデータパッキングルーチンのセットを用意するのは良いアイデアだと思います。
データをパッキングする場合、どのような形式が良いのでしょうか?素晴らしい質問です。幸いなことに、RFC4506(外部データ表現規格)では、浮動小数点型、整数型、配列、生データなど、さまざまな型のバイナリ形式をすでに定義しているんです。もし自分でデータをロールバックするのであれば、それに準拠することをお勧めします。でも、そうする義務はありません。パケット警察は、あなたのドアのすぐ外にいるわけではありません。少なくとも、私は彼らがそうだとは思いません。
いずれにせよ、データを送信する前に何らかの方法でエンコードするのが正しい方法なのです!
7.6 データカプセル化の子
ところで、データをカプセル化するとはどういうことでしょうか。最も単純なケースでは、識別情報かパケット長のどちらか、あるいは両方を含むヘッダーをそこに貼り付けるということです。
ヘッダーはどのようなものにすべきでしょうか?まあ、プロジェクトを完成させるために必要だと思うものを表すバイナリデータに過ぎません。
うわー。漠然としてますね。
なるほど。例えば、SOCK_STREAM
を使用したマルチユーザチャットプログラムがあるとします。ユーザが何かをタイプする("発言する")と、2つの情報がサーバに送信される必要があります: 何を発言したか、誰が発言したかです。
ここまでは良いですか?"では何が問題なのか?" とあなたは聞いているのでしょう。
問題は、メッセージの長さがまちまちであることです。ある人は "tom" と名乗り、ある人は "Benjamin" と名乗り、"Hey guys what is up?" と言うかもしれません。
そこで、これらのものが入ってきたときに send()
でクライアントに送るわけです。送信するデータストリームは次のようになります。
t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?
といった具合に。あるメッセージが始まり、別のメッセージが止まったとき、クライアントはどうやって知るのでしょうか?もし望むなら、すべてのメッセージを同じ長さにして、上で実装した sendall()
を呼び出すだけでよいでしょう。しかし、それはバンド幅を浪費します!"tom" が "Hi" と言うために 1024 バイトも send()
したくはないでしょう。
そこで、データを小さなヘッダーとパケット構造でカプセル化します。クライアントもサーバも、このデータをどのようにパックしたりアンパックしたりするか("マーシャル"、"アンマーシャル" と呼ばれることもあります)知っています。今は見ないでください、私たちは、クライアントとサーバがどのように通信するかを記述するプロトコルを定義し始めているのです
この場合、ユーザ名は固定長で8文字、パディングは '\0'
とします。そして、データは最大 128 文字の可変長とします。このような場合に使用するパケット構造の例を見てみましょう。
-
len
(1 byte, unsigned)---8 バイトのユーザ名とチャットデータをカウントしたパケットの総長です。 -
name
(8 bytes)---ユーザの名前。必要なら NUL パッドされます。 -
chatdata
(n-bytes) --- データ自体で、128 バイト以下です。パケットの長さは、このデータの長さに 8(上記の名前フィールドの長さ)を加えたものとします。
なぜ、8 バイトと 128 バイトというフィールドの制限を選んだのか?十分な長さがあると思い、思いつきで選んだのです。しかし、8 バイトでは制約が多すぎる、30 バイトの名前フィールドがあってもいい、などということもあるかもしれません。選択はあなた次第です。
上記のパケット定義を用いると、最初のパケットは以下の情報(16 進数、ASCII)で構成されることになります。
0A 74 6F 6D 00 00 00 00 00 48 69
(length) T o m (padding) H i
そして、2つ目も同様です。
18 42 65 6E 6A 61 6D 69 6E 48 65 79 20 67 75 79 73 20 77 ...
(length) B e n j a m i n H e y g u y s w ...
(もちろん、長さはネットワークバイトオーダーで格納されます。この場合は 1 バイトだけなので問題ないですが、一般的にはパケット内の 2 進整数はすべてネットワークバイトオーダーで格納されるようにしたいです。)
このデータを送信するときには、安全策をとって上記の sendall()
のようなコマンドを使うべきです。そうすれば、たとえすべてのデータを送信するために send()
を複数回呼び出す必要があったとしても、すべてのデータが送信されたことを確認できます。
同様に、このデータを受信するときにも、少し余分な作業をする必要があります。念のため、部分的なパケットを受け取るかもしれないことを想定しておく必要があります(例えば、上の Benjamin から "18 42 65 6E 6A
" を受け取るかもしれませんが、この recv()
のコールではそれがすべてです。)。パケットが完全に受信されるまで、何度も何度も recv()
を呼び出す必要があります。
でも、どうやって?さて、私たちはパケットが完成するために必要な合計バイト数を知っています。その数はパケットの前面に貼られているからです。 また、パケットの最大サイズは 1+8+128、つまり 137 バイトであることも知っています(パケットの定義がそうなっているからです)。
実は、ここでできることがいくつかあるんです。すべてのパケットが長さで始まることを知っているので、パケットの長さを取得するためだけに recv()
を呼び出すことができます。そして、一度それを手に入れたら、パケットの残りの長さを正確に指定して(おそらくすべてのデータを得るために繰り返し)、完全なパケットを手に入れるまで再びそれを呼び出すことができます。この方法の利点は、1つのパケットに対して十分な大きさのバッファが必要なことで、欠点は、すべてのデータを取得するために少なくとも2回 recv()
を呼び出す必要があることです。
もう一つの方法は、単に recv()
を呼び出して、受信してもよい量をパケットの最大バイト数として言うことです。そして、受け取ったものはすべてバッファの後ろに貼り付け、最後にパケットが完了したかどうかを確認します。もちろん、次のパケットの一部を受け取るかもしれないので、そのためのスペースが必要です。
そこで、2つのパケットに十分な大きさの配列を宣言します。これは作業用の配列で、到着したパケットを再構築します。
データを recv()
するたびに、ワークバッファにデータを追加し、パケットが完成したかどうかチェックします。すなわち、バッファ内のバイト数がヘッダで指定された長さ以上であること(ヘッダの長さには長さ自体のバイト数は含まれないので、+1)です。バッファ内のバイト数が1より小さい場合、明らかにパケットは完全ではありません。この場合、特別なケースを作る必要があります。しかし、最初の 1 バイトはゴミなので、正しいパケット長を知るためにそれを当てにすることはできないからです。
パケットを完成させたら、あとは好きなように使ってください。使って、ワークバッファから削除してください。
ふぅ〜。もう頭の中でグルグルしてますか?さて、ここでワンツーパンチの2つ目です。1回の recv()
呼び出しで、あるパケットの終わりを越えて次のパケットを読んでしまったかもしれません。つまり、一つの完全なパケットと、次のパケットの不完全な部分を持つワークバッファを持っているのです!なんてこったい。(しかし、このような事態を想定して、ワークバッファを2つのパケットを保持できるような大きさにしたのです!)
最初のパケットの長さはヘッダからわかっているし、ワークバッファのバイト数も記録しているので、ワークバッファのバイト数のうち何バイトが2番目の(不完全な)パケットに属しているかを差し引いて計算することができるのです。最初のパケットを処理したら、それをワークバッファから取り除き、部分的な2番目のパケットをバッファの先頭に移動させ、次の recv()
のためにすべての準備をすることができます。
(読者の中には、実際に部分的な2番目のパケットをワークバッファの先頭に移動させるのに時間がかかることを指摘する人もいるでしょう。プログラムは、循環バッファを使用することによって、これを必要としないようにコード化することができます。残念ながら、円形バッファに関する議論は、この記事の範囲外です。それでも気になるなら、データ構造の本を読んでみてください。)
簡単だとは言っていません。なるほど、簡単だとは言いました。練習すれば、すぐに自然にできるようになりますよ。エクスカリバーに誓って!
7.7 ブロードキャストパケット---Hello, World!
これまで、このガイドでは、1つのホストから他の1つのホストにデータを送信することについて話してきました。しかし、適切な権限があれば、同時に複数のホストにデータを送信することが可能です、と私は主張します!
UDP(TCP ではなく UDP のみ)と標準的な IPv4 では、これはブロードキャストと呼ばれるメカニズムによって行われます。IPv6 ではブロードキャストはサポートされていないため、マルチキャストという優れた技術を利用する必要があります。しかし、未来に目を向けるのはもう十分です。私たちは 32 ビットの現在に留まっています。
しかし、ちょっと待ってください!いきなりブロードキャストを始めることはできません。ブロードキャストパケットをネットワークに送信する前に、ソケットオプション SO_BROADCAST
をセットしなければなりません。まるでミサイルの発射スイッチの上に被せる小さなプラスチックのカバーのようなものです。そのくらいのパワーを秘めているのです。
しかし、真面目な話、ブロードキャストパケットを使うには危険性があるのです。ブロードキャストパケットを受信したすべてのシステムは、そのデータの行き先がどのポートか分かるまで、何層にもわたるデータのカプセル化を解かなければならないのです。そして、そのデータを渡すか、廃棄するかしなければなりません。どちらの場合でも、ブロードキャストパケットを受信した各マシンは、ローカルネットワーク上のすべてのマシンで、多くのマシンが不必要な作業をすることになりかねません。ゲーム Doom が発売された当初、これはそのネットワークコードに対する不満でした。
さて、猫の皮を剥ぐ方法は一つではない......ちょっと待った。本当に猫の皮を剥ぐ方法は1つではないのですか?どんな表現なんだ?それと同じように、ブロードキャストパケットを送る方法も1つではありません。では、本題のブロードキャストメッセージの宛先アドレスはどのように指定するのでしょうか?一般的には2つの方法があります。
-
特定のサブネットのブロードキャストアドレスにデータを送信します。これは、サブネットのネットワーク番号に、アドレスのホスト部分のすべての 1 ビットを設定したものです。例えば、私の家のネットワークは
192.168.1.0
、ネットマスクは255.255.255.0
で、アドレスの最後のバイトは私のホスト番号です(ネットマスクによると、最初の 3 バイトはネットワーク番号だからです)。つまり、私のブロードキャストアドレスは192.168.1.255
です。Unix では、ifconfig
コマンドが実際にこのすべてのデータを与えてくれます。(気になる方は、ブロードキャストアドレスを得るためのビット論理はnetwork_number
OR (NOTnetmask
) です。)このタイプのブロードキャストパケットは、自分のローカルネットワークだけでなく、遠隔地のネットワークにも送ることができますが、宛先のルータによってパケットがドロップされる危険性があります。(もしドロップされなかったら、どこかのランダムなスマーフが彼らの LAN にブロードキャストトラフィックを流し始めるかもしれません。) -
"グローバル" ブロードキャストアドレスにデータを送信します。これは
255.255.255.255
で、別名INADDR_BROADCAST
と呼ばれるものです。多くのマシンはこれをネットワーク番号と自動的にビット演算して、ネットワークブロードキャストアドレスに変換しますが、そうでないものもあります。ルータは、皮肉なことに、この種のブロードキャストパケットをローカルネットワークから転送しません。
では、ソケットオプション SO_BROADCAST
を設定せずに、ブロードキャストアドレスでデータを送信しようとするとどうなるでしょうか。それでは、古き良き時代の talker
と listener
を起動し、何が起こるか見てみましょう。
$ talker 192.168.1.2 foo
sent 3 bytes to 192.168.1.2
$ talker 192.168.1.255 foo
sendto: Permission denied
$ talker 255.255.255.255 foo
sendto: Permission denied
そう、全然嬉しくないんです...ソケットオプションの SO_BROADCAST
を設定していなかったからです。そうすれば、どこでも sendto()
ができるようになります!
実際、これがブロードキャストできる UDP アプリケーションとできない UDP アプリケーションの唯一の違いなのです。そこで、古い talker
アプリケーションに SO_BROADCAST
ソケットオプションを設定する章を一つ追加してみましょう。このプログラムを broadcaster.c
と呼ぶことにします。
/*
** broadcaster.c -- a datagram "client" like talker.c, except
** this one can broadcast
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define SERVERPORT 4950 // the port users will be connecting to
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in their_addr; // connector's address information
struct hostent *he;
int numbytes;
int broadcast = 1;
//char broadcast = '1'; // if that doesn't work, try this
if (argc != 3) {
fprintf(stderr,"usage: broadcaster hostname message\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { // get the host info
perror("gethostbyname");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
// this call is what allows broadcast packets to be sent:
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast,
sizeof broadcast) == -1) {
perror("setsockopt (SO_BROADCAST)");
exit(1);
}
their_addr.sin_family = AF_INET; // host byte order
their_addr.sin_port = htons(SERVERPORT); // short, network byte order
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero);
if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
(struct sockaddr *)&their_addr, sizeof their_addr)) == -1) {
perror("sendto");
exit(1);
}
printf("sent %d bytes to %s\n", numbytes,
inet_ntoa(their_addr.sin_addr));
close(sockfd);
return 0;
}
"通常の" UDP クライアント/サーバの状況と何が違うのでしょうか?何もありません。(この場合、クライアントがブロードキャストパケットの送信を許可されていることを除けば。)このように、古い UDP listener
プログラムを1つのウィンドウで実行し、broadcaster
を別のウィンドウで実行してみてください。これで、上で失敗した送信をすべて行うことができるはずです。
$ broadcaster 192.168.1.2 foo
sent 3 bytes to 192.168.1.2
$ broadcaster 192.168.1.255 foo
sent 3 bytes to 192.168.1.255
$ broadcaster 255.255.255.255 foo
sent 3 bytes to 255.255.255.255
そして、listener
がパケットを受け取ったと応答するのが見えるはずです。(もし listener
が応答しないなら、それは IPv6 アドレスにバインドされているからかもしれません。listener.c
の AF_UNSPEC
を AF_INET
に変更して、強制的に IPv4 にすることを試してみてください。)
なるほど、それはちょっと楽しみですね。しかし、今度は同じネットワーク上にある隣のマシンで listener
を起動して、それぞれのマシンに2つのコピーを作成し、ブロードキャストアドレスを指定して broadcaster
を再度実行してみてください... おい!両方の listener
がパケットを受け取るぞ!sendto()
を一度しか呼んでいないのにな。かっこいい!
もし listener
が、あなたが直接送ったデータを受け取って、ブロードキャストアドレスのデータを受け取らないなら、あなたのローカルマシンのファイアウォールがパケットをブロックしている可能性があります。(そうそう、Pat とBapper、これが私のサンプルコードが動作しない理由だと、私より先に気づいてくれてありがとうございます。ガイドの中で紹介すると言っておいたのに、こうして紹介してくれて。というわけで、にゃー。)
繰り返しになりますが、ブロードキャストパケットには注意が必要です。LAN 上のすべてのマシンが recvfrom()
したかどうかに関わらず、そのパケットを処理しなければならないので、計算機ネットワーク全体にかなりの負荷を与える可能性があります。ブロードキャストパケットは控えめに、そして適切に使用されるべきものであることは間違いありません。
8 よくある質問
8.1 ヘッダーファイルはどこで入手できますか?
もし、あなたのシステムに既にないのであれば、おそらく必要ないでしょう。あなたのプラットフォームのマニュアルを参照してください。Windows 用にビルドする場合は、#include <winsock.h>
だけが必要です。
8.2 bind()
が "Address already in use" と報告した場合、どうすればよいのでしょうか?
リスニングしているソケットで SO_REUSEADDR
オプションを指定して setsockopt()
を使用する必要があります。例として、bind()
の章と select()
の章をチェックしてみてください。
8.3 システム上のオープンソケットのリストを取得するにはどうすればよいですか?
netstat
を使用します。詳しくは man
ページをチェックしてください。しかし、タイプするだけで良い出力が得られるはずです。
$ netstat
唯一のコツは、どのソケットがどのプログラムに関連付けられているかを判断することです。:-)
8.4 ルーティングテーブルを見るにはどうしたらいいですか?
route
コマンド(ほとんどの Linux では /sbin
にあります)または netstat -r
コマンドを実行します。
8.5 クライアントとサーバのプログラムが1台しかない場合、どのように実行すればよいのでしょうか?ネットワークプログラムを書くのにネットワークは必要ないのですか?
幸いなことに、事実上すべてのマシンは、カーネル内に配置され、ネットワークカードのふりをするループバックネットワーク "デバイス" を実装しています。(これはルーティングテーブルで "lo
" としてリストされるインターフェースです。)
あなたが "goat
" という名前のマシンにログインしていると仮定します。あるウィンドウでクライアントを、別のウィンドウでサーバを実行します。あるいは、サーバをバックグラウンドで起動し("server &
")、同じウィンドウでクライアントを実行します。ループバックデバイスの利点は、client goat
か client localhost
(localhost
は /etc/hosts
ファイルで定義されていると思われるので)を使えば、ネットワークなしでクライアントがサーバと通信できるようになることです!
つまり、ネットワークに接続されていない1台のマシンで動作させるために、コードの変更は一切必要ないのです!ハッザー!
8.6 リモート側が接続を閉じたかどうかを判断するにはどうすればよいですか?
recv()
が 0
を返すのでわかります。
8.7 "ping" ユーティリティを実装するには?ICMP とは何ですか?生ソケットと SOCK_RAW
についてもっと詳しく知りたいのですが、どこに行けばよいですか?
生ソケットに関するすべての疑問は、W. Richard Stevens の UNIX Network Programming books で解決されるでしょう。また、オンラインの Stevens の UNIX Network Programming のソースコードの ping/
サブディレクトリも見てみてください。
8.8 connect()
の呼び出しのタイムアウトを変更または短縮するにはどうすればよいですか?
W. Richard Stevens と全く同じ答えを出すのではなく、UNIX Network Programming のソースコードにある lib/connect_nonb.c
を参照することにしましょう。
その要点は、socket()
でソケット記述子を作成し、それをノンブロッキングに設定して connect()
を呼び、すべてがうまくいけば connect()
は直ちに -1
を返して errno
は EINPROGRESS
に設定されるということです。その後、好きなタイムアウトで select()
を呼び出し、ソケット記述子をリードとライトの両方で渡します。タイムアウトしなければ、connect()
の呼び出しが完了したことになります。このとき、getsockopt()
に SO_ERROR
オプションをつけて connect()
呼び出しからの戻り値を取得する必要があります(エラーがなければ 0
になるはずです)。
最後に、データ転送を開始する前に、ソケットを再びブロッキングに設定する必要があります。
この方法には、接続中に何か他のことをすることができるという利点もあることに注意してください。例えば、タイムアウトを 500ms のような低い値に設定し、タイムアウトするたびに画面上のインジケータを更新し、それから再び select()
を呼び出すことができます。例えば、select()
を20回ほど呼び出してタイムアウトしたら、そろそろ接続をあきらめる時期だとわかるでしょう。
私が言ったように、スティーブンスのソースに完璧に優れた例があるので、チェックしてみてください。
8.9 Windows 用にビルドするにはどうしたらいいですか?
まず、Windows を削除し、Linux または BSD をインストールします。};-)
。いや、実際には、導入部の Windows 用のビルドの章を見ればいいんですけどね。
8.10 Solaris/SunOS 用にビルドするにはどうしたらいいですか?コンパイルしようとするとリンカエラーが出ます!
リンカーエラーは、Sun ボックスがソケットライブラリを自動的にコンパイルしないために起こります。この方法の例については、導入部の Solaris/SunOS 用のビルドに関する章 を参照してください。
8.11 select()
がシグナルで落ち続けるのはなぜか?
シグナルはブロックされたシステムコールに -1
を返し、errno
に EINTR
をセットする傾向があります。sigaction()
でシグナルハンドラをセットアップするときに、フラグ SA_RESTART
を設定することができます。これは、システムコールが中断された後に再開することを想定したものです。
当然ながら、いつもうまくいくわけではありません。
私のお気に入りの解決策は、goto
ステートメントを使うことです。これは教授をイライラさせるだけなので、ぜひやってみてください。
select_restart:
if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
if (errno == EINTR) {
// some signal just interrupted us, so restart
goto select_restart;
}
// handle the real error here:
perror("select");
}
もちろん、この場合は goto
を使う必要はありません。他の構造体を使用して制御することができます。しかし、私は goto
文の方が実際きれいだと思うんです。
8.12 recv()
の呼び出しにタイムアウトを実装するにはどうすればよいですか?
select()
を使いましょう!これは、読み込みたいソケット記述子に対して、タイムアウトパラメータを指定することができます。あるいは、このように関数全体を一つの関数で包むこともできます。
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
int recvtimeout(int s, char *buf, int len, int timeout)
{
fd_set fds;
int n;
struct timeval tv;
// set up the file descriptor set
FD_ZERO(&fds);
FD_SET(s, &fds);
// set up the struct timeval for the timeout
tv.tv_sec = timeout;
tv.tv_usec = 0;
// wait until timeout or data received
n = select(s+1, &fds, NULL, NULL, &tv);
if (n == 0) return -2; // timeout!
if (n == -1) return -1; // error
// data must be here, so do a normal recv()
return recv(s, buf, len, 0);
}
.
.
.
// Sample call to recvtimeout():
n = recvtimeout(s, buf, sizeof buf, 10); // 10 second timeout
if (n == -1) {
// error occurred
perror("recvtimeout");
}
else if (n == -2) {
// timeout occurred
} else {
// got some data in buf
}
.
.
.
recvtimeout()
はタイムアウトした場合、-2
を返すことに注意してください。なぜ 0
を返さないのでしょうか?そうですね、思い出してみてください、recv()
を呼び出したときに 0
という値が返ってきたら、リモート側が接続を閉じたことを意味します。そのため、この戻り値はすでに話されており、-1
は "error" を意味します。そこで、タイムアウトのインジケータとして -2
を選択しました。
8.13 ソケットで送信する前にデータを暗号化または圧縮するにはどうすればよいですか?
暗号化を行う簡単な方法として、SSL(セキュア・ソケット・レイヤー)を使用する方法がありますが、このガイドの範囲を超えています。(詳しくは OpenSSL プロジェクトを見てください。)
しかし、独自のコンプレッサーや暗号化システムを導入する場合は、データが両端間で一連のステップを通過すると考えればよいのです。各ステップは、何らかの形でデータを変更します。
- サーバがファイルやどこからでもデータを読み取ります。
- サーバがデータを暗号化・圧縮します(この部分を追加します)。
- サーバが暗号化されたデータを
send()
します。
今度はその逆です。
- クライアントが暗号化されたデータを
recv()
します。 - クライアントがデータを復号・伸張します(この部分はあなたが追加します)。
- クライアントがデータをファイル(または任意の場所)に書き込みます。
圧縮して暗号化するなら、最初に圧縮することだけは忘れないでください。:-)
ただ、サーバがやったことをクライアントがきちんと元に戻してくれれば、いくら中間ステップを追加しても、最終的にはデータは大丈夫です。
つまり、私のコードを使うために必要なことは、データが読み込まれる場所と、ネットワーク上で(send()
を使って)データが送信される場所の間にある場所を見つけ、そこに暗号化を行うコードを貼り付けるだけです。
8.14 "PF_INET
" をずっと見ているのですが、これは何ですか?AF_INET
と関係があるのでしょうか?
はい、そうです。詳しくは socket()
の章を参照してください。
8.15 クライアントからシェルコマンドを受け取って実行するサーバを書くにはどうしたらいいでしょうか?
簡単のために、クライアントが connect()
s, send()
s, close()
s で接続したとしましょう(つまり、クライアントが再び接続しない限り、その後のシステムコールはありません)。
クライアントがたどるプロセスはこうです。
- サーバに
connect()
します。 send("/sbin/ls > /tmp/client.out")
- 接続を
close()
します。
一方、サーバはデータを処理し、実行します。
- クライアントからの接続を
accept()
します。 - コマンド文字列を
recv(str)
します。 - 接続を
close()
します。 system(str)
を使ってコマンドを実行します。
ご注意ください!サーバがクライアントの言うことを実行することは、リモートシェルアクセスを与えるようなもので、サーバに接続すると、あなたのアカウントに何かすることができます。例えば、上記の例で、クライアントが "rm -rf ~
" と送ったらどうでしょう?それはあなたのアカウント内のすべてを削除してしまいます。
そこで、賢明にも、foobar
ユーティリティのような、安全だとわかっているいくつかのユーティリティを除いて、クライアントが何も使えないようにするのです。
if (!strncmp(str, "foobar", 6)) {
sprintf(sysstr, "%s > /tmp/server.out", str);
system(sysstr);
}
しかし、残念ながらまだ安全ではありません。クライアントが "foobar; rm -rf ~
" と入力したらどうでしょうか。最も安全なのは、コマンドの引数に含まれるすべての非英数字(適切であればスペースを含む)の前にエスケープ文字("\
")を置く小さなルーチンを書くことです。
このように、クライアントが送信したものをサーバが実行し始めると、セキュリティはかなり大きな問題になります。
8.16 slew でデータを送っているのですが、recv()
すると、一度に 536 バイトか 1460 バイトしか受信できません。しかし、私のローカルマシンで実行すると、すべてのデータを同時に受信することができます。どうなっているのでしょうか?
物理媒体が処理できる最大サイズであるMTUに当たっています。ローカルマシンでは、8K 以上を問題なく処理できるループバックデバイスを使用しています。しかし、Ethernet では、ヘッダーを含めて 1500 バイトしか扱えないので、その限界にぶつかってしまうのです。モデムでは、576 MTU(これもヘッダ付き)で、さらに低い制限にぶつかります。
まず、すべてのデータが送信されていることを確認する必要があります。(詳しくは sendall()
関数の実装を参照してください。)それが確認できたら、すべてのデータが読み込まれるまで、ループで recv()
を呼び出す必要があります。
recv()
を複数回呼び出して完全なデータパケットを受信する方法については、7.6 データカプセル化の子 の章を読んでください。
8.17 私は Windows マシンを使っていますが、fork()
システムコールも struct sigaction
のようなものも持っていません。どうしたらいいでしょうか?
もし、どこかにあるとすれば、コンパイラに同梱されているであろう POSIX ライブラリでしょう。私は Windows を持っていないので、答えようがないのですが、Microsoft には POSIX 互換性レイヤーがあり、そこに fork()
があるように記憶しています。(そして多分、sigaction
も。)
VC++ 付属のヘルプで "fork" または "POSIX" を検索して、何か手がかりがないか見てみてください。
もし、それでうまくいかない場合は、fork()
/sigaction
を捨てて、Win32 の CreateProcess()
に置き換えてください。私は CreateProcess()
の使い方を知りません。引数をたくさん取りますが、VC++ に付属するドキュメントに記載されているはずです。
8.18 ファイアウォールの内側にいるのですが、ファイアウォールの外側にいる人に私の IP アドレスを知らせて、私のマシンに接続できるようにするにはどうしたらよいでしょうか?
残念ながら、ファイアウォールの目的は、ファイアウォール外の人がファイアウォール内のマシンに接続するのを防ぐことなので、それを許可することは基本的にセキュリティ違反とみなされます。
これは、すべてが失われたということではありません。ひとつには、ファイアウォールがマスカレードやNATのようなことを行っている場合、しばしば connect()
を通して接続することができます。自分が常に接続を開始するようにプログラムを設計すれば、うまくいくでしょう。
もしそれで満足できないなら、システム管理者に頼んでファイアウォールに穴を開けてもらい、人々があなたに接続できるようにすることができます。ファイアウォールは NAT ソフトウェアやプロキシなどを通して、あなたに転送することができます。
ファイアウォールの穴は、決して軽視できないものであることを認識しておいてください。悪い人が内部ネットワークにアクセスできないようにしなければなりません。初心者の場合、ソフトウェアを安全にするのは想像以上に難しいのです。
シスアドを怒らせないようにね。;-)
8.19 パケットスニファーの書き方を教えてください。イーサネットインタフェースをプロミスキャスモードにするにはどうしたらよいですか?
ネットワークカードが "プロミスカスモード" の場合、このマシン宛のパケットだけでなく、すべてのパケットをオペレーティングシステムに転送することをご存じない方のために説明します。(しかし、イーサネットは IP より下位の層なので、すべての IP アドレスは効果的に転送されます。より詳しくは、2.2 低レベルのナンセンスとネットワーク理論 の章をご覧ください。)
これは、パケットスニファーの仕組みの基本です。インターフェイスをプロミスカスモードにし、OSはワイヤー上を通過するすべてのパケットを取得します。 このデータを読むために、ある種のソケットを持っているはずです。
残念ながら、その答えはプラットフォームによって異なりますが、例えば "windows promiscuous ioctl" でググれば、おそらくどこかにたどり着けるはずです。Linux の場合は、Stack Overflow のスレッドにも有用そうなものがあります。
8.20 TCP/UDP ソケットにカスタムタイムアウト値を設定するにはどうすればよいですか?
お使いのシステムに依存します。ネットで SO_RCVTIMEO
や SO_SNDTIMEO
(setsockopt()
で使用)を検索して、あなたのシステムがその機能をサポートしているかどうかを確認するとよいでしょう。
Linux のマニュアルでは,alarm()
や setitimer()
で代用することを勧めています.
8.21 どのポートが使用可能か、どうすれば分かりますか?"公式" なポート番号の一覧はありますか?
通常、これは問題ではありません。もしあなたが、例えばウェブサーバを書いているのであれば、よく知られているポート 80 をあなたのソフトウェアに使用するのは良い考えです。もし、自分専用のサーバを書くのであれば、適当にポートを選んで(ただし 1023 より大きい)試してみてください。
そのポートがすでに使われている場合、bind()
しようとすると "Address already in use" というエラーが表示されます。他のポートを選んでください。(ソフトウェアのユーザが、設定ファイルやコマンドラインスイッチで別のポートを指定できるようにしておくとよいでしょう。)
IANA(Internet Assigned Numbers Authority)が管理している公式ポート番号一覧があります。そのリストにあるもの(1023 以上)があるからといって、そのポートを使ってはいけないというわけではありません。例えば、Id Software 社の DOOM は、それが何であれ、"mdqs" と同じポートを使っています。重要なのは、あなたがそのポートを使いたいときに、同じマシンの他の誰もそのポートを使っていないことです。
9 Man ページ
Unix の世界では、マニュアルがたくさんあります。それらのマニュアルには、自由に使える個々の機能を説明する小さな章があります。
もちろん、manual
は入力するのが面倒でしょう。つまり、私を含め、Unix の世界では誰もそんなにタイプするのが好きではないのです。実際、私はどれだけ簡潔であることを好むかについて長々と語ることもできますが、その代わりに、事実上すべての状況において私がどれだけ全く驚くほど簡潔であることを好むかについて、長ったらしい放談であなたを退屈させないように簡潔に語ることにします。
[拍手喝采]
ありがとうございます。私が言いたいのは、このようなページは Unix の世界では "man ページ"と呼ばれ、私はここに私なりに切り詰めた man ページを載せて、皆さんに読んで楽しんでもらおうということなのです。これらの関数の多くは、私が言っているよりもずっと汎用的なものですが、ここではインターネットソケットプログラミングに関連する部分のみを紹介するつもりです。
しかし、お待ちを!私の man ページの悪いところはそれだけではありません。
- ガイドに記載されている基本的なことしか書かれていない不完全なものです。
- 実際の世界には、これ以外にも多くのマニュアルページが存在します。
- ご使用のシステムとは異なります。
- ヘッダーファイルは、お使いのシステムの関数によって異なる場合があります。
- お使いのシステムによっては、関数パラメータが異なる場合があります。
もし本当の情報が欲しいなら、ローカルの Unix man ページで man whatever
と入力してチェックしてください。ここで、"whatever" は "accept
" のような、信じられないほど興味のあるものです。(Microsoft Visual Studio のヘルプセクションにも同じようなものがあると思います。しかし、"man" の方が "help" よりも1バイト簡潔で良いのです。またもや Unix の勝利!)
では、もしこれらに欠陥があるのなら、なぜガイドに掲載するのでしょうか?理由はいくつかありますが、一番の理由は、(a) これらのバージョンはネットワーク・プログラミングに特化しており、実際のものよりも消化しやすいこと、(b) これらのバージョンには例が含まれていることです。
あ!あと、例題といえば、エラーチェックは、コードが長くなってしまうので、あまり入れないようにしています。でも、100% 失敗しないという確信がない限り、システムコールをするときは必ずエラーチェックをした方がいいです。それでもしたほうがいいです。
9.1 accept()
リスニング中のソケットで着信接続を承認します。
9.1.1 書式
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
9.1.2 解説
一旦、 SOCK_STREAM
ソケットを取得して、 listen()
で着信接続のための設定を行うという手間をかけると、そのソケットを使用することができます。次に accept()
を呼び出して、新しく接続したクライアントとの通信に使用する新しいソケット記述子を実際に取得します。
リスニングに使用している古いソケットはまだ残っています。そして、さらに accept()
を呼び出す際に使用されます。
パラメータ | 説明 |
---|---|
s | listen() 中のソケット記述子。 |
addr | ここには、接続先のサイトのアドレスが記入されています。 |
addrlen | これは addr パラメータで返された構造体の sizeof() で埋められています。これは addr に渡された型なので、 struct sockaddr_in が返ってきたと仮定すれば、安全に無視することができます。 |
accept()
は通常ブロックされますが、select()
を使用して、リスニング中のソケット記述子が "ready to read" になっているかどうかを前もって確認することができます。もしそうなら、新しい接続が accept()
を待っていることになります!やったー!あるいは、fcntl()
を使って、リスニング中のソケットに O_NONBLOCK
フラグを設定することもできます。そうすると、決してブロックせず、代わりに errno
を EWOULDBLOCK
に設定して -1
を返します。
accept()
が返すソケット記述子は、正真正銘のソケット記述子であり、リモートホストに開いて接続されています。それを使い終わったら close()
しなければなりません。
9.1.3 返り値
accept()
は新しく接続されたソケット記述子を返すが、エラーの場合は -1
を返し、 errno
は適切に設定されます。
9.1.4 例
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, MYPORT, &hints, &res);
// make a socket, bind it, and listen on it:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// now accept an incoming connection:
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
// ready to communicate on socket descriptor new_fd!
9.1.5 参照
socket()
,
getaddrinfo()
,
listen()
,
struct sockaddr_in
9.2 bind()
ソケットをIPアドレスとポート番号に関連付けます。
9.2.1 書式
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
9.2.2 解説
リモートマシンがあなたのサーバプログラムに接続しようとするとき、IP アドレスとポート番号という2つの情報が必要です。bind()
の呼び出しによって、まさにそれが可能になります。
まず、getaddrinfo()
を呼び出して、宛先アドレスとポート情報を持つ struct sockaddr
をロードします。それから socket()
を呼んでソケット記述子を取得し、ソケットとアドレスを bind()
に渡すと、IP アドレスとポートが魔法のように(実際の魔法を使って)ソケットにバインドされるのです!
IP アドレスを知らない場合、あるいはマシンに IP アドレスが 1 つしかないことが分かっている場合、あるいはマシンの IP アドレスがどれでも構わない場合は、getaddrinfo()
の hints
パラメータに AI_PASSIVE
フラグを渡すだけでよいでしょう。これは struct sockaddr
の IP アドレス部分を特別な値で埋めるもので、bind()
に対して、このホストの IP アドレスを自動的に埋めるように指示するものです。
何、何?現在のホストのアドレスを自動入力するために、struct sockaddr
の IP アドレスにどんな特別な値が読み込まれているのでしょうか?もしそうでなければ、上記のように getaddrinfo()
からの結果を使用してください。IPv4 では、struct sockaddr_in
構造体の sin_addr.s_addr
フィールドは INADDR_ANY
に設定されます。IPv6 では、struct sockaddr_in6
構造体の sin6_addr
フィールドは、グローバル変数 in6addr_any
から代入されます。また、新しい struct in6_addr
を宣言する場合は、IN6ADDR_ANY_INIT
で初期化することができます。
最後に、addrlen
パラメータに sizeof my_addr
を設定します。
9.2.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
9.2.4 例
// modern way of doing things with getaddrinfo()
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
// (you should actually walk the "res" linked list and error-check!)
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);
// example of packing a struct by hand, IPv4
struct sockaddr_in myaddr;
int s;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(3490);
// you can specify an IP address:
inet_pton(AF_INET, "63.161.169.137", &(myaddr.sin_addr));
// or you can let it automatically select one:
myaddr.sin_addr.s_addr = INADDR_ANY;
s = socket(PF_INET, SOCK_STREAM, 0);
bind(s, (struct sockaddr*)&myaddr, sizeof myaddr);
9.2.5 参照
getaddrinfo()
,
socket()
,
struct sockaddr_in
,
struct in_addr
9.3 connect()
サーバにソケットを接続します。
9.3.1 書式
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr,
socklen_t addrlen);
9.3.2 解説
socket()
コールでソケット記述子を構築したら、connect()
システムコールを使って、そのソケットをリモートサーバに接続することができます。必要なことは、ソケット記述子と、もっとよく知りたいと思うサーバのアドレスを渡すだけです。(それと、このような関数によく渡されるアドレスの長さも。)
通常、この情報は getaddrinfo()
を呼び出した結果として得られますが、必要であれば自分自身で struct sockaddr
を埋めることもできます。
ソケット記述子の bind()
をまだ呼んでいない場合、ソケットは自動的にあなたの IP アドレスとランダムなローカルポートに束縛されます。サーバでない場合は、ローカルポートを気にする必要はなく、リモートポートを気にして serv_addr
パラメータに設定するだけなので、通常はこれで十分です。クライアントソケットを特定の IP アドレスとポートにしたい場合は、bind()
を呼び出すことができますが、これはかなりまれなケースです。
ソケットを connect()
したら、あとは自由に send()
や recv()
をして、好きなだけデータを取り込めます。
特記事項:SOCK_DGRAM
UDP ソケットをリモートホストに connect()
した場合、send()
と recv()
だけでなく、sendto()
と recvfrom()
も使用できるようになります。もし必要なら。
9.3.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返す(それに応じて errno
が設定されます)。
9.3.4 例
// connect to www.example.com port 80 (http)
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
// we could put "80" instead on "http" on the next line:
getaddrinfo("www.example.com", "http", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// connect it to the address and port we passed in to getaddrinfo():
connect(sockfd, res->ai_addr, res->ai_addrlen);
9.3.5 参照
9.4 close()
ソケット記述子を閉じます。
9.4.1 書式
#include <unistd.h>
int close(int s);
9.4.2 解説
ソケットを使い終えて、send()
や recv()
、あるいはそれ以外のことを一切したくなくなったら、 close()
をすれば、ソケットは解放され、二度と使われることはありません。
リモート側は、このような事態が発生したかどうかを、2つの方法のいずれかで判断することができます。1つ目は、リモート側が recv()
を呼び出した場合、0
を返します。2つ目は、リモート側が send()
を呼び出した場合、シグナル SIGPIPE
を受け取り、send()
は -1
を返し、errno
は EPIPE
にセットされます。
Windowsユーザ:あなたが使うべき関数は closesocket()
であって、close()
ではありません。もし、ソケット記述子上で close()
を使おうとすると、Windows が怒る可能性があります...。そして、怒られると嫌なものです。
9.4.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
9.4.4 例
s = socket(PF_INET, SOCK_DGRAM, 0);
.
.
.
// a whole lotta stuff...*BRRRONNNN!*
.
.
.
close(s); // not much to it, really.
9.4.5 参照
9.5 getaddrinfo()
, freeaddrinfo()
, gai_strerror()
ホスト名やサービスに関する情報を取得し、その結果を struct sockaddr
にロードします。
9.5.1 書式
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *nodename, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *ai);
const char *gai_strerror(int ecode);
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, ...
int ai_family; // AF_xxx
int ai_socktype; // SOCK_xxx
int ai_protocol; // 0 (auto) or IPPROTO_TCP, IPPROTO_UDP
socklen_t ai_addrlen; // length of ai_addr
char *ai_canonname; // canonical name for nodename
struct sockaddr *ai_addr; // binary address
struct addrinfo *ai_next; // next structure in linked list
};
9.5.2 解説
getaddrinfo()
は特定のホスト名に関する情報(IPアドレスなど)を返し、struct sockaddr
を読み込んで、細かい部分(IPv4 か IPv6 か)を処理してくれる優れた関数です。これは、古い関数である gethostbyname()
と getservbyname()
を置き換えるものです。以下の説明には、少し難しく感じるかもしれない多くの情報が含まれていますが、実際の使い方はとてもシンプルです。まずはサンプルを見てみるのもいいかもしれません。
興味のあるホスト名は nodename
パラメータに入れます。アドレスは "www.example.com" のようなホスト名か、IPv4 または IPv6 のアドレス(文字列として渡される)のいずれかになります。このパラメータは、AI_PASSIVE
フラグを使用している場合は NULL
にすることもできます(下記参照)。
servname
パラメータは、基本的にポート番号です。ポート番号("80" のような文字列として渡される)、または "http"、"tftp"、"smtp"、"pop" などのサービス名であることができます。よく知られたサービス名は、IANA ポートリストや、/etc/services
ファイルで見つけることができます。
最後に、入力パラメータとして hints
があります。これは getaddrinfo()
関数が何をしようとしているかを定義する場所です。memset()
で使用する前に、構造体全体をゼロにします。それでは、使用する前に設定する必要があるフィールドを見てみましょう。
ai_flags
には様々なものを設定することができますが、ここでは重要なものをいくつか紹介します。(複数のフラグを指定するには、|
演算子でビット単位の OR を指定します。)フラグの完全なリストについては、man ページをチェックしてください。
AI_CANONNAME
は、結果の ai_canonname
をホストの標準的な(実際の)名前で埋め尽くすようにします。AI_PASSIVE
は、結果の IP アドレスを INADDR_ANY
(IPv4)または in6addr_any
(IPv6)で埋めます。これにより、その後の bind()
への呼び出しで、struct sockaddr
の IP アドレスが現在のホストのアドレスで自動的に埋められるようになります。これは、アドレスをハードコードしたくない場合に、サーバをセットアップするのに優れています。
もし、AI_PASSIVE
フラグを使用する場合は、nodename
に NULL
を渡すことができます(bind()
が後でそれを埋めてくれるからです)。
続けて、入力パラメータについてですが、おそらく ai_family
を AF_UNSPEC
に設定すると、getaddrinfo()
に IPv4 と IPv6 の両方のアドレスを検索させることができるようになります。また、AF_INET
や AF_INET6
でどちらか一方に限定することもできます。
次に、socktype
フィールドを SOCK_STREAM
または SOCK_DGRAM
のどちらかに設定する必要があります。
最後に、ai_protocol
を 0
にしておくと、自動的にプロトコルの種類が選択されます。
さて、このようなものを全部入れたら、いよいよ getaddrinfo()
を呼び出すことができます!
もちろん、ここからが楽しいところです。res
は struct addrinfo
のリンクリストを指すようになり、このリストを通して、hints で渡したものと一致するすべてのアドレスを取得することができるようになります。
さて、何らかの理由で動作しないアドレスを取得することは可能です。Linux の man ページでは、socket()
と connect()
(または AI_PASSIVE
フラグでサーバをセットアップしている場合は bind()
)を成功するまで呼び出すリストをループしています。
最後に、リンクリストを使い終わったら、freeaddrinfo()
を呼んでメモリを解放する必要があります (さもないと、メモリがリークしてしまい、Some People が怒ることになります)。
9.5.3 返り値
成功した場合は 0
を、エラーが発生した場合は 0
以外を返します。非ゼロを返した場合、関数 gai_strerror()
を用いて、エラーコードのプリント可能なバージョンを戻り値に含めることができる。
9.5.4 例
// code for a client connecting to a server
// namely a stream socket to www.example.com on port 80 (http)
// either IPv4 or IPv6
int sockfd;
struct addrinfo hints, *servinfo, *p;
int rv;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use AF_INET6 to force IPv6
hints.ai_socktype = SOCK_STREAM;
if ((rv = getaddrinfo("www.example.com", "http", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
exit(1);
}
// loop through all the results and connect to the first we can
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("socket");
continue;
}
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
perror("connect");
close(sockfd);
continue;
}
break; // if we get here, we must have connected successfully
}
if (p == NULL) {
// looped off the end of the list with no connection
fprintf(stderr, "failed to connect\n");
exit(2);
}
freeaddrinfo(servinfo); // all done with this structure
// code for a server waiting for connections
// namely a stream socket on port 3490, on this host's IP
// either IPv4 or IPv6.
int sockfd;
struct addrinfo hints, *servinfo, *p;
int rv;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use AF_INET6 to force IPv6
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // use my IP address
if ((rv = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
exit(1);
}
// loop through all the results and bind to the first we can
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("socket");
continue;
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("bind");
continue;
}
break; // if we get here, we must have connected successfully
}
if (p == NULL) {
// looped off the end of the list with no successful bind
fprintf(stderr, "failed to bind socket\n");
exit(2);
}
freeaddrinfo(servinfo); // all done with this structure
9.5.5 参照
gethostbyname()
,
getnameinfo()
9.6 gethostname()
システムの名称を返します。
9.6.1 書式
#include <sys/unistd.h>
int gethostname(char *name, size_t len);
9.6.2 解説
あなたのシステムには名前がついています。みんなそうです。これは、今まで話してきた他のネットワーク的なものよりも、若干 Unix 的なものですが、それでも使い道はあります。
例えば、ホスト名を取得してから gethostbyname()
を呼び出すと、IP アドレスを知ることができます。
パラメータ name
はホスト名を格納するバッファを指し、len
はそのバッファのサイズ(バイト)を表します。gethostname()
はバッファの終端を上書きせず(エラーを返すかもしれませんし、単に書き込みを止めるかもしれません)、バッファに文字列のスペースがある場合は NUL
-ターミネイトを行います。
9.6.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返す(それに応じて errno
が設定されます)。
9.6.4 例
char hostname[128];
gethostname(hostname, sizeof hostname);
printf("My hostname: %s\n", hostname);
9.6.5 参照
9.7 gethostbyname()
, gethostbyaddr()
ホスト名に対する IP アドレスの取得、またはその逆を行います。
9.7.1 書式
#include <sys/socket.h>
#include <netdb.h>
struct hostent *gethostbyname(const char *name); // DEPRECATED!
struct hostent *gethostbyaddr(const char *addr, int len, int type);
9.7.2 解説
注意:この2つの関数は getaddrinfo()
と getnameinfo()
に取って代わられています!特に、gethostbyname()
は IPv6 ではうまく動きません。
これらの関数は、ホスト名と IP アドレスの間を行き来します。例えば、"www.example.com" があれば、 gethostbyname()
を使ってその IP アドレスを取得し、struct in_addr
に格納することができます。
逆に、struct in_addr
や struct in6_addr
があれば、gethostbyaddr()
を使用してホスト名を取得することができます。gethostbyaddr()
は IPv6 互換ですが、より新しいピカピカな getnameinfo()
を代わりに使用すべきです。
(ホスト名を調べたい IP アドレスをドットアンドナンバー形式で表した文字列がある場合、AI_CANONNAME
フラグを指定して getaddrinfo()
を使った方がよいでしょう。)
gethostbyname()
は "www.yahoo.com" のような文字列を受け取り、IP アドレスを含む膨大な情報を含む struct hostent
を返します。(他の情報は、正式なホスト名、エイリアスのリスト、アドレスの種類、アドレスの長さ、そしてアドレスのリストです---これは汎用の構造体で、一度方法を見れば我々の特定の目的のために使うのはかなり簡単です。)
gethostbyaddr()
は struct in_addr
または struct in6_addr
を受け取って、対応するホスト名を表示します(もし存在すれば)。したがって、gethostbyname()
の逆バージョンということになります。パラメータについては、 addr
は char*
、実際には struct in_addr
へのポインタを渡したいのです。len
は sizeof(struct in_addr)
で、type
は AF_INET
であるべきです。
では、返される struct hostent
とは何なのでしょうか?これは、問題のホストに関する情報を含むいくつかのフィールドを持っています。
Field | 解説 |
---|---|
char *h_name | 実際の正規のホスト名。 |
char **h_aliases | 配列でアクセスできるエイリアスのリスト--- 最後の要素は NULL です。 |
int h_addrtype | 結果のアドレスの型。この目的のためには、本当は AF_INET でなければなりません。 |
int length | アドレスの長さをバイト数で表したもので、IP(バージョン4)アドレスの場合は4となります。 |
char **h_addr_list | このホストの IP アドレスのリスト。これは char** ですが、実際には struct in_addr* の配列で、偽装されています。配列の最後の要素は NULL です。 |
h_addr | h_addr_list[0] の共通定義エイリアスです。もし、このホストの古い IP アドレスが欲しいだけなら(ええ、複数持つこともできます)、このフィールドを使えばいいのです。 |
9.7.3 返り値
成功すれば struct hostent
へのポインタを、エラーなら NULL
を返します。
通常の perror()
やエラー報告に使うようなものの代わりに、これらの関数は h_errno
という変数に並列の結果を持ち、herror()
や hstrerror()
という関数を使って表示することができるようになりました。 これらの関数は、通常の errno
、perror()
、strerror()
関数と同じように動作します。
9.7.4 例
// THIS IS A DEPRECATED METHOD OF GETTING HOST NAMES
// use getaddrinfo() instead!
#include <stdio.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int i;
struct hostent *he;
struct in_addr **addr_list;
if (argc != 2) {
fprintf(stderr,"usage: ghbn hostname\n");
return 1;
}
if ((he = gethostbyname(argv[1])) == NULL) { // get the host info
herror("gethostbyname");
return 2;
}
// print information about this host:
printf("Official name is: %s\n", he->h_name);
printf(" IP addresses: ");
addr_list = (struct in_addr **)he->h_addr_list;
for(i = 0; addr_list[i] != NULL; i++) {
printf("%s ", inet_ntoa(*addr_list[i]));
}
printf("\n");
return 0;
}
// THIS HAS BEEN SUPERCEDED
// use getnameinfo() instead!
struct hostent *he;
struct in_addr ipv4addr;
struct in6_addr ipv6addr;
inet_pton(AF_INET, "192.0.2.34", &ipv4addr);
he = gethostbyaddr(&ipv4addr, sizeof ipv4addr, AF_INET);
printf("Host name: %s\n", he->h_name);
inet_pton(AF_INET6, "2001:db8:63b3:1::beef", &ipv6addr);
he = gethostbyaddr(&ipv6addr, sizeof ipv6addr, AF_INET6);
printf("Host name: %s\n", he->h_name);
9.7.5 参照
getaddrinfo()
,
getnameinfo()
,
gethostname()
,
errno
,
perror()
,
strerror()
,
struct in_addr
9.8 getnameinfo()
与えられた struct sockaddr
のホスト名とサービス名の情報を検索します。
9.8.1 書式
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);
9.8.2 解説
この関数は getaddrinfo()
の逆で、すでにロードされている struct sockaddr
を受け取り、その名前とサービス名のルックアップを行う関数です。これは、古い gethostbyaddr()
と getservbyport()
関数を置き換えるものです。
sa
パラメータに struct sockaddr
(実際には struct sockaddr_in
または struct sockaddr_in6
をキャストしたもの)へのポインタを渡し、salen
にその struct
の長さを渡す必要があります。
結果として得られるホスト名とサービス名は、host
と serv
パラメータで指定された領域に書き込まれます。もちろん、これらのバッファの最大長を hostlen
と servlen
で指定する必要があります。
最後に、渡すことのできるフラグがいくつかありますが、ここでは良いものを2つ紹介します。NI_NOFQDN
は host
にドメイン名全体ではなく、ホスト名のみを含めるようにする。NI_NAMEREQD
は、DNS ルックアップで名前が見つからない場合に、関数を失敗させます(このフラグを指定せず、名前が見つからない場合は、getnameinfo()
が代わりに IP アドレスの文字列を host
に格納します。)
いつも通り、ローカルの man ページで全容を確認してください。
9.8.3 返り値
成功した場合は 0
を、エラーが発生した場合は 0
以外を返します。戻り値が 0
でない場合、それを gai_strerror()
に渡すと、人間が読める文字列を得ることができます。詳しくは getaddrinfo
を参照してください。
9.8.4 例
struct sockaddr_in6 sa; // could be IPv4 if you want
char host[1024];
char service[20];
// pretend sa is full of good information about the host and port...
getnameinfo(&sa, sizeof sa, host, sizeof host, service, sizeof service, 0);
printf(" host: %s\n", host); // e.g. "www.example.com"
printf("service: %s\n", service); // e.g. "http"
9.8.5 参照
getaddrinfo()
,
gethostbyaddr()
9.9 getpeername()
リモート側の接続に関するアドレス情報を返す。
9.9.1 書式
#include <sys/socket.h>
int getpeername(int s, struct sockaddr *addr, socklen_t *len);
9.9.2 解説
リモート接続を accept()
したり、サーバに connect()
したら、今度はピアとして知られるものを手に入れます。ピアとは、単に接続先のコンピュータのことで、IP アドレスとポートで識別されます。つまり...
getpeername()
は単に、接続中のマシンに関する情報が詰まった struct sockaddr_in
を返します。
なぜ "名前" なのか?このガイドで使っているようなインターネットソケットだけでなく、さまざまな種類のソケットがあるので、"名前"はすべてのケースをカバーする良い総称だったのです。この場合、相手の"名前"は相手の IP アドレスとポートです。
この関数は結果のアドレスのサイズを len
で返しますが、len
には addr
のサイズをあらかじめ代入しておく必要があります。
9.9.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます。)
9.9.4 例
// assume s is a connected socket
socklen_t len;
struct sockaddr_storage addr;
char ipstr[INET6_ADDRSTRLEN];
int port;
len = sizeof addr;
getpeername(s, (struct sockaddr*)&addr, &len);
// deal with both IPv4 and IPv6:
if (addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&addr;
port = ntohs(s->sin_port);
inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr);
} else { // AF_INET6
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr;
port = ntohs(s->sin6_port);
inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr);
}
printf("Peer IP address: %s\n", ipstr);
printf("Peer port : %d\n", port);
9.9.5 参照
gethostname()
,
gethostbyname()
,
gethostbyaddr()
9.10 errno
最後のシステムコールのエラーコードを保持します。
9.10.1 書式
#include <errno.h>
int errno;
9.10.2 解説
これは、多くのシステムコールのエラー情報を保持する変数です。思い起こせば、socket()
や listen()
などはエラーが発生すると -1
を返し、どのエラーが発生したかを具体的に知らせるために errno
という正確な値を設定します。
ヘッダーファイル errno.h
には、EADDRINUSE
, EPIPE
, ECONNREFUSED
などのような、エラーに対する定数シンボル名の束がリストアップされています。ローカルの man ページには、どのようなコードがエラーとして返されるのかが書かれています。そして、実行時にこれらを使用して、異なるエラーを異なる方法で処理することができます。
あるいは、より一般的には perror()
や strerror()
を呼び出して、人間が読める形式のエラーを取得することができます。
マルチスレッドの愛好家のために一つ注意しておきたいのは、ほとんどのシステムで errno
はスレッドセーフな方法で定義されているということです。(つまり、実際にはグローバル変数ではないのですが、シングルスレッド環境ではグローバル変数と同じように振る舞います。)
9.10.3 返り値
この変数の値は、最後に発生したエラーであり、最後のアクションが成功した場合は "success" のコードになるかもしれません。
9.10.4 例
s = socket(PF_INET, SOCK_STREAM, 0);
if (s == -1) {
perror("socket"); // or use strerror()
}
tryagain:
if (select(n, &readfds, NULL, NULL) == -1) {
// an error has occurred!!
// if we were only interrupted, just restart the select() call:
if (errno == EINTR) goto tryagain; // AAAA! goto!!!
// otherwise it's a more serious error:
perror("select");
exit(1);
}
9.10.5 参照
9.11 fcntl()
ソケット記述子を制御します。
9.11.1 書式
#include <sys/unistd.h>
#include <sys/fcntl.h>
int fcntl(int s, int cmd, long arg);
9.11.2 解説
この関数は通常、ファイルロックやその他のファイル指向の処理を行うために使用されますが、時々見たり使ったりするようなソケット関連の関数もいくつか持っています。
パラメータ s
には操作したいソケット記述子、cmd
には F_SETFL
を設定し、arg
には以下のコマンドのいずれかを指定します。(ここで説明した以外にも fcntl()
には様々な機能がありますが、ここではソケットに特化して説明します。)
cmd | 解説 |
---|---|
O_NONBLOCK | ソケットをノンブロッキングに設定します。詳しくは 7.1 ブロッキングの章を参照してください。 |
O_ASYNC | 非同期 I/O を行うようにソケットを設定します。ソケットでデータを recv() する準備ができたら、シグナル SIGIO を発生させます。これはめったに見られないことであり、ガイドの範囲を超えています。また、特定のシステムでのみ利用可能だと思います。 |
9.11.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
fcntl()
システムコールの使い方によって、実際には異なる戻り値がありますが、ここではソケットに関係しないので取り上げていません。詳しくはローカルの fcntl()
man ページを参照してください。
9.11.4 例
int s = socket(PF_INET, SOCK_STREAM, 0);
fcntl(s, F_SETFL, O_NONBLOCK); // set to non-blocking
fcntl(s, F_SETFL, O_ASYNC); // set to asynchronous I/O
9.11.5 参照
9.12 htons()
, htonl()
, ntohs()
, ntohl()
マルチバイト整数型をホストバイトオーダーからネットワークバイトオーダーに変換します。
9.12.1 書式
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
9.12.2 解説
不幸なことに、コンピュータによってマルチバイト整数(つまり char
より大きな整数)の内部でのバイト順序が異なっています。この結果、2 バイトの short int
を Intel のコンピュータから Mac(Intel のコンピュータになる前)に send()
すると、一方のコンピュータが 1
という数字だと思っていても、他方は 256
という数字だと思い、またその逆もしかりということになるのです。
この問題を回避する方法は、皆がそれぞれの違いを脇に置き、モトローラと IBM は正しく、インテルは奇妙なやり方をしたのだから、送信する前にバイトオーダーを "ビッグエンディアン" に変換することに同意することです。Intel は "リトルエンディアン" マシンなので、私たちが好むバイト順序を "ネットワークバ イトオーダー" と呼ぶ方が、はるかに政治的に正しいのです。そこで、これらの関数は、ネイティブのバイトオーダーからネットワークバイトオーダーに変換し、また元に戻します。
(つまり、Intel ではこれらの関数はすべてのバイトを入れ替えますが、PowerPC ではバイトがすでにネットワークバイトオーダーになっているため、何もしません。しかし、Intel マシンでビルドしても正常に動作するようにしたい人がいるかもしれないので、いずれにせよコードでは常にこれらを使用する必要があります。)
32 ビット(4バイト、おそらく int
)と 16 ビット(2バイト、おそらく short
)の数値が含まれることに注意してください。64 ビットマシンには 64 ビットの int
用の htonll()
があるかもしれませんが、私は見たことがありません。自分で書くしかないでしょう。
とにかく、これらの関数の動作は、まず、ホスト(あなたのマシン)のバイトオーダーから変換するのか、ネットワークのバイトオーダーから変換するのかを決定することです。もし "ホスト" なら、呼び出す関数の最初の文字は "h" です。そうでなければ、"network" の場合は "n" です。関数名の真ん中は、ある "もの" から別の "もの" に変換するため、常に "to" であり、最後の1文字は何に変換するかを示しています。最後の文字はデータの大きさを表し、sort
の場合は "s"、long
の場合は "l" です。このように
関数 | 解説 |
---|---|
htons() | h ost to n etwork s hort |
htonl() | h ost to n etwork l ong |
ntohs() | n etwork to h ost s hort |
ntohl() | n etwork to h ost l ong |
9.12.3 返り値
各関数は変換後の値を返します。
9.12.4 例
uint32_t some_long = 10;
uint16_t some_short = 20;
uint32_t network_byte_order;
// convert and send
network_byte_order = htonl(some_long);
send(s, &network_byte_order, sizeof(uint32_t), 0);
some_short == ntohs(htons(some_short)); // this expression is true
9.13 inet_ntoa()
, inet_aton()
, inet_addr
IP アドレスをドットアンドナンバーの文字列から struct in_addr
に変換し、元に戻します。
9.13.1 書式
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ALL THESE ARE DEPRECATED! Use inet_pton() or inet_ntop() instead!!
char *inet_ntoa(struct in_addr in);
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
9.13.2 解説
これらの関数は IPv6 を扱えないため、非推奨です!代わりに inet_ntop()
または inet_pton()
を使ってください!これらの関数がここに含まれているのは、まだ野放しになっていることがあるからです。
これらの関数はすべて struct in_addr
(おそらく struct sockaddr_in
の一部)からドットアンドナンバー形式の文字列(例:"192.168.5.10")に変換し、その逆もまた可能です。コマンドラインなどで IP アドレスが渡された場合、connect()
の相手となる struct in_addr
を得るには、これが一番簡単な方法だと思います。もっと強力なものが必要なら、gethostbyname()
のような DNS 関数を試してみたり、自分の住んでいる国でクーデターを起こしてみたりしてください。
関数 inet_ntoa()
は struct in_addr
に格納されたネットワークアドレスをドットアンドナンバー形式の文字列に変換します。"ntoa" の "n" はネットワークを表し、"a" は歴史的な理由から ASCII を表しています(つまり "Network To ASCII" です。"toa" というサフィックスは C ライブラリに atoi()
という ASCII 文字列に変換する類似の友人があります)。
関数 inet_aton()
はその逆で、ドットや数字を含む文字列から in_addr_t
(これは struct in_addr
のフィールド s_addr
の型)に変換します。
最後に、関数 inet_addr()
は古い関数で、基本的に inet_aton()
と同じことをします。理論的には非推奨ですが、よく目にしますし、使っても警察には捕まりません。
9.13.3 返り値
inet_aton()
はアドレスが有効な場合は 0
以外を返し、アドレスが無効な場合は 0
を返します。
inet_ntoa()
はドットアンドナンバーズの文字列を静的バッファに格納し、この関数を呼び出すたびに上書きされるように返します。
net_addr()
はアドレスを in_addr_t
として返し、エラーがある場合は -1
を返します。(これは、有効な IP アドレスである "255.255.255.255
" という文字列を変換しようとした場合と同じ結果です。これが inet_aton()
の方が良い理由です。)
9.13.4 例
struct sockaddr_in antelope;
char *some_addr;
inet_aton("10.0.0.1", &antelope.sin_addr); // store IP in antelope
some_addr = inet_ntoa(antelope.sin_addr); // return the IP
printf("%s\n", some_addr); // prints "10.0.0.1"
// and this call is the same as the inet_aton() call, above:
antelope.sin_addr.s_addr = inet_addr("10.0.0.1");
9.13.5 参照
inet_ntop()
,
inet_pton()
,
gethostbyname()
,
gethostbyaddr()
9.14 inet_ntop()
, inet_pton()
IPアドレスを人間が読める形に変換して戻します。
9.14.1 書式
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);
9.14.2 解説
これらの関数は、人間が読める IP アドレスを扱い、様々な関数やシステムコールで使用するためにバイナリ表現に変換するためのものです。"n" は "network"、"p" は "presentation" を表します。あるいは "text presentation" 。しかし、"printable" と考えてもよいです。"ntop" は "network to printable " です。ほらね?
IP アドレスを見るときに、2 進数の山を見たくないことがあります。192.0.2.180
や 2001:db8:8714:3a90::12
のような、プリント可能な形式でありたいと思うことでしょう。そのような場合には、inet_ntop()
を使用します。
inet_ntop()
は af
パラメータにアドレスファミリ(AF_INET
または AF_INET6
)を受け取ります。src
パラメータには、文字列に変換したいアドレスを含む struct in_addr
または struct in6_addr
へのポインタを指定する必要があります。最後に dst
と size
は、変換先の文字列へのポインタと、その文字列の最大長を表します。
dst
の文字列の最大長はどうすればよいのでしょうか?IPv4 アドレスと IPv6 アドレスの最大長はどのくらいですか?幸いなことに、あなたを助けてくれるマクロがいくつかあります。最大長は以下の通りです。INET_ADDRSTRLEN
と INET6_ADDRSTRLEN
です。
また、IP アドレスを可読形式で含む文字列を持っていて、それを struct sockaddr_in
や struct sockaddr_in6
に格納したい場合もあります。このような場合は、反対の関数 inet_pton()
を使用します。
また、inet_pton()
は af
パラメータにアドレスファミリ(AF_INET
または AF_INET6
)を受け取ります。src
パラメータには、IP アドレスをプリント可能な形式で格納した文字列へのポインタを指定します。最後に dst
パラメータは、結果を格納する場所を指定します。これはおそらく struct in_addr
または struct in6_addr
となります。
これらの関数は DNS のルックアップを行いません---そのためには getaddrinfo()
が必要です。
9.14.3 返り値
inet_ntop()
は成功すると dst
パラメータを返し、失敗すると NULL
を返します(そして errno
が設定されます)。
inet_pton()
は成功すると 1
を返します。エラーがあった場合(errno
が設定されます)は -1
を返し、入力が有効な IP アドレスでなかった場合は 0
を返します。
9.14.4 例
// IPv4 demo of inet_ntop() and inet_pton()
struct sockaddr_in sa;
char str[INET_ADDRSTRLEN];
// store this IP address in sa:
inet_pton(AF_INET, "192.0.2.33", &(sa.sin_addr));
// now get it back and print it
inet_ntop(AF_INET, &(sa.sin_addr), str, INET_ADDRSTRLEN);
printf("%s\n", str); // prints "192.0.2.33"
// IPv6 demo of inet_ntop() and inet_pton()
// (basically the same except with a bunch of 6s thrown around)
struct sockaddr_in6 sa;
char str[INET6_ADDRSTRLEN];
// store this IP address in sa:
inet_pton(AF_INET6, "2001:db8:8714:3a90::12", &(sa.sin6_addr));
// now get it back and print it
inet_ntop(AF_INET6, &(sa.sin6_addr), str, INET6_ADDRSTRLEN);
printf("%s\n", str); // prints "2001:db8:8714:3a90::12"
// Helper function you can use:
//Convert a struct sockaddr address to a string, IPv4 and IPv6:
char *get_ip_str(const struct sockaddr *sa, char *s, size_t maxlen)
{
switch(sa->sa_family) {
case AF_INET:
inet_ntop(AF_INET, &(((struct sockaddr_in *)sa)->sin_addr),
s, maxlen);
break;
case AF_INET6:
inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)sa)->sin6_addr),
s, maxlen);
break;
default:
strncpy(s, "Unknown AF", maxlen);
return NULL;
}
return s;
}
9.14.5 参照
9.15 listen()
ソケットに着信を待つように指示します。
9.15.1 書式
#include <sys/socket.h>
int listen(int s, int backlog);
9.15.2 解説
ソケット記述子(socket()
システムコールで作成)を受け取り、着信接続をリッスンするように指示することができます。これがサーバとクライアントを区別するポイントです、みんな。
backlog
パラメータはシステムによって異なる意味を持ちますが、大まかに言うと、カーネルが新しい接続を拒否し始めるまでに保留できる接続の数です。つまり、新しい接続が来たら、バックログがいっぱいにならないように、素早く accept()
する必要があります。10 程度に設定してみて、もしクライアントが高負荷で "Connection refused" を受け始めたら、もっと高く設定してみてください。
listen()
を呼び出す前に、サーバは bind()
を呼び出して特定のポート番号に自分自身をアタッチする必要があります。そのポート番号(サーバの IP アドレス上)は、クライアントが接続するポート番号になります。
9.15.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
9.15.4 例
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, 10); // set s up to be a server (listening) socket
// then have an accept() loop down here somewhere
9.15.5 参照
9.16 perror()
, strerror()
エラーを人間が読める文字列で表示します。
9.16.1 書式
#include <stdio.h>
#include <string.h> // for strerror()
void perror(const char *s);
char *strerror(int errnum);
9.16.2 解説
多くの関数がエラー時に -1
を返し、変数 errno
の値を何らかの数値に設定するので、それを自分にとって意味のある形で簡単に表示できたら確かに良いでしょう。
幸いなことに、perror()
がそれをやってくれます。もし、エラーの前にもっと説明を表示させたい場合は、パラメータ s
を指定します(あるいは s
を NULL
にしておけば、何も追加で表示されません)。
簡単に言うと、この関数は ECONNRESET
のような errno
値を受け取り、それらを "Connection reset by peer" のようにうまく表示します。
関数 strerror()
は perror()
に非常に似ていますが、与えられた値(通常は変数 errno
を渡します)に対するエラーメッセージの文字列へのポインタを返す点が異なります。
9.16.3 返り値
strerror()
はエラーメッセージの文字列へのポインタを返します。
9.16.4 例
int s;
s = socket(PF_INET, SOCK_STREAM, 0);
if (s == -1) { // some error has occurred
// prints "socket error: " + the error message:
perror("socket error");
}
// similarly:
if (listen(s, 10) == -1) {
// this prints "an error: " + the error message from errno:
printf("an error: %s\n", strerror(errno));
}
9.16.5 参照
9.17 poll()
複数のソケットで同時にイベントをテストします。
9.17.1 書式
#include <sys/poll.h>
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
9.17.2 解説
この関数は select()
と非常によく似ており、どちらもファイル記述子の集合を監視して、recv()
の準備ができた受信データ、send()
へのソケットの準備、recv()
への帯域外データの準備、エラーなどのイベントを検出します。
基本的な考え方は、 ufds
に nfds
struct pollfd
の配列と、ミリ秒単位のタイムアウト(1秒は1000ミリ秒)を渡すというものです。永遠に待ちたい場合は、タイムアウトを負数にすることでできます。タイムアウトまでにどのソケット記述子でもイベントが発生しなかった場合、poll()
が返ります。
配列 struct pollfd
の各要素は、1つのソケット記述子を表し、以下のフィールドを含みます。
struct pollfd {
int fd; // the socket descriptor
short events; // bitmap of events we're interested in
short revents; // when poll() returns, bitmap of events that occurred
};
poll()
を呼び出す前に、ソケット記述子 fd
をロードし(fd
に負の数を設定すると、この struct pollfd
は無視され、その revents
フィールドはゼロになります)、次に以下のマクロのビット単位の OR により event
フィールドを構築します。
Macro | 解説 |
---|---|
POLLIN | このソケットで recv() のためのデータが準備できたときに警告を出します。 |
POLLOUT | このソケットにブロックせずにデータを send() できるようになったら警告を出します。 |
POLLPRI | このソケットで帯域外データが recv() できるようになったら警告を出します。 |
poll()
コールが戻ってくると、revents
フィールドは上記のフィールドのビット単位の OR となり、どの記述子で実際にそのイベントが発生したかがわかります。さらに、これらの他のフィールドが存在する可能性があります。
Macro | 解説 |
---|---|
POLLERR | このソケットでエラーが発生しました。 |
POLLHUP | リモート側の接続がハングアップしました。 |
POLLNVAL | ソケット記述子 fd に何か問題があったようだ---たぶん初期化されていないのだろう? |
9.17.3 返り値
配列 ufds
のうち、イベントが発生した要素の数を返します。タイムアウトが発生した場合は 0
になります。また、エラーが発生した場合は -1
を返します(それに応じて errno
が設定されます。)
9.17.4 例
int s1, s2;
int rv;
char buf1[256], buf2[256];
struct pollfd ufds[2];
s1 = socket(PF_INET, SOCK_STREAM, 0);
s2 = socket(PF_INET, SOCK_STREAM, 0);
// pretend we've connected both to a server at this point
//connect(s1, ...)...
//connect(s2, ...)...
// set up the array of file descriptors.
//
// in this example, we want to know when there's normal or out-of-band
// data ready to be recv()'d...
ufds[0].fd = s1;
ufds[0].events = POLLIN | POLLPRI; // check for normal or out-of-band
ufds[1].fd = s2;
ufds[1].events = POLLIN; // check for just normal data
// wait for events on the sockets, 3.5 second timeout
rv = poll(ufds, 2, 3500);
if (rv == -1) {
perror("poll"); // error occurred in poll()
} else if (rv == 0) {
printf("Timeout occurred! No data after 3.5 seconds.\n");
} else {
// check for events on s1:
if (ufds[0].revents & POLLIN) {
recv(s1, buf1, sizeof buf1, 0); // receive normal data
}
if (ufds[0].revents & POLLPRI) {
recv(s1, buf1, sizeof buf1, MSG_OOB); // out-of-band data
}
// check for events on s2:
if (ufds[1].revents & POLLIN) {
recv(s1, buf2, sizeof buf2, 0);
}
}
9.17.5 参照
9.18 recv()
, recvfrom()
ソケットでデータを受信します。
9.18.1 書式
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int s, void *buf, size_t len, int flags);
ssize_t recvfrom(int s, void *buf, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
9.18.2 解説
ソケットを立ち上げて接続したら、recv()
(TCP SOCK_STREAM
ソケットの場合)と recvfrom()
(UDP SOCK_DGRAM
ソケットの場合)を使ってリモート側から受信したデータを読み込むことができるようになります。
どちらの関数も、ソケット記述子 s
、バッファへのポインタ buf
、バッファのサイズ(バイト数)len
、そして関数の動作を制御するための flags
を受け取ります。
さらに、recvfrom()
は struct sockaddr*
を受け取り、from
はデータがどこから来たかを示し、 fromlen
には struct sockaddr
のサイズを記入することになります。(また、fromlen
を from
または struct sockaddr
のサイズに初期化する必要があります。)
では、この関数にどんな不思議なフラグを渡すことができるのでしょうか?以下にそのいくつかを紹介しますが、より詳細な情報やあなたのシステムで実際にサポートされているものについては、ローカルのマニュアルページをチェックする必要があります。また、通常のバニラの recv()
にしたい場合は、flags
を 0
に設定します。
Macro | 解説 |
---|---|
MSG_OOB | Out of Band データを受信します。これは、send() で MSG_OOB フラグを指定して送信されたデータを取得する方法です。受信側としては、緊急のデータがあることを伝えるシグナル SIGURG が発生しているはずです。このシグナルに対するハンドラで、この MSG_OOB フラグを指定して recv() を呼び出すことができます。 |
MSG_PEEK | もし recv() を "見せかけだけ" に呼び出したい場合は、このフラグを付けて呼び出すことができます。これは、recv() を "本当の意味で"(つまり MSG_PEEK フラグなしで)呼び出したときに、バッファの中で何が待っているかを教えてくれるものです。これは、次の recv() 呼び出しに対するスニークプレビューのようなものです。 |
MSG_WAITALL | recv() に、len パラメータで指定したデータがすべて揃うまで戻らないように指示します。しかし、シグナルによって呼び出しが中断された場合、何らかのエラーが発生した場合、リモート側が接続を閉じた場合など、極端な状況下ではあなたの希望を無視することができます。怒らないでください。 |
recv()
を呼び出すと、読み込むべきデータがあるまでブロックされます。ブロックしたくない場合は、ソケットをノンブロッキングに設定するか、select()
や poll()
で受信データがあるかどうかを確認してから recv()
や recvfrom()
を呼び出してください。
9.18.3 返り値
実際に受け取ったバイト数(len
パラメータで指定した値よりも少ないかもしれません)を返します。エラーの場合は -1
を返します(それに応じて errno
がセットされます)。
リモート側が接続を閉じていた場合、recv()
は 0
を返します。これは、リモート側が接続を閉じたかどうかを判断するための通常の方法です。正常なのは良いことだ、反乱軍!
9.18.4 例
// stream sockets and recv()
struct addrinfo hints, *res;
int sockfd;
char buf[512];
int byte_count;
// get host info, make socket, and connect it
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sockfd, res->ai_addr, res->ai_addrlen);
// all right! now that we're connected, we can receive some data!
byte_count = recv(sockfd, buf, sizeof buf, 0);
printf("recv()'d %d bytes of data in buf\n", byte_count);
// datagram sockets and recvfrom()
struct addrinfo hints, *res;
int sockfd;
int byte_count;
socklen_t fromlen;
struct sockaddr_storage addr;
char buf[512];
char ipstr[INET6_ADDRSTRLEN];
// get host info, make socket, bind it to port 4950
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
getaddrinfo(NULL, "4950", &hints, &res);
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
// no need to accept(), just recvfrom():
fromlen = sizeof addr;
byte_count = recvfrom(sockfd, buf, sizeof buf, 0, &addr, &fromlen);
printf("recv()'d %d bytes of data in buf\n", byte_count);
printf("from IP address %s\n",
inet_ntop(addr.ss_family,
addr.ss_family == AF_INET?
((struct sockadd_in *)&addr)->sin_addr:
((struct sockadd_in6 *)&addr)->sin6_addr,
ipstr, sizeof ipstr);
9.18.5 参照
send()
,
sendto()
,
select()
,
poll()
,
Blocking
9.19 select()
ソケット記述子が読み込み/書き込み可能かどうか確認します。
9.19.1 書式
#include <sys/select.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
FD_SET(int fd, fd_set *set);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
9.19.2 解説
select()
関数は、複数のソケットを同時にチェックして、受信待ちのデータがあるか、ブロッキングせずに send()
できるか、何らかの例外が発生したかを確認する方法を提供します。
上記の FD_SET()
のようなマクロを使用して、ソケット記述子のセットを投入します。セットができたら、それを以下のいずれかのパラメータとして関数に渡します。readfds
(セット内のいずれかのソケットが recv()
データを受信できる状態になったとき)、writefds
(セット内のいずれかのソケットが send()
データを送信できる状態になったとき)、または exceptfds
(いずれかのソケットで例外が発生したときに知る必要がある場合)。もし、これらのタイプのイベントに興味がなければ、これらのパラメータのいずれか、あるいはすべてを NULL
にすることができます。select()
が返された後、セット内の値が変更され、どれが読み書き可能で、どれが例外を発生させたかが表示されます。
最初のパラメータ n
は、最も大きい番号のソケット記述子(これらは単なる int
s, 覚えていますか?)に 1
を加えたものです。
最後に、最後に struct timeval
, timeout
を記述します。これは select()
に、これらのセットをどれくらいの時間チェックするかを指示します。タイムアウト後、あるいはイベントが発生したときのどちらか早いほうに戻ります。timeval
構造体は2つのフィールドを持っています。tv_sec
は秒数で、これに tv_usec
というマイクロ秒(1,000,000 マイクロ秒が 1 秒)を加えたものです。
ヘルパーマクロは次のような働きをします。
Macro | 解説 |
---|---|
FD_SET(int fd, fd_set *set); | set に fd を追加します。 |
FD_CLR(int fd, fd_set *set); | set から fd を削除します。 |
FD_ISSET(int fd, fd_set *set); | fd が set に含まれる場合は true を返す。 |
FD_ZERO(fd_set *set); | set からすべてのエントリをクリアします。 |
Linux ユーザーへの注意事項:Linux の select()
は "ready-to-read" を返すことがありますが、実際には読み込む準備ができていないため、その後の read()
呼び出しがブロックされることがあります。このバグを回避するには、受信側のソケットに O_NONBLOCK
フラグを設定し、EWOULDBLOCK
でエラーになるようにし、このエラーが発生しても無視します。ソケットをノンブロッキングに設定する方法については、fcntl()
の man ページ を参照してください。
9.19.3 返り値
成功した場合はセットに含まれる記述子の数を、タイムアウトに達した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。また、どのソケットが準備できているかを示すために、セットも変更されます。
9.19.4 例
int s1, s2, n;
fd_set readfds;
struct timeval tv;
char buf1[256], buf2[256];
// pretend we've connected both to a server at this point
//s1 = socket(...);
//s2 = socket(...);
//connect(s1, ...)...
//connect(s2, ...)...
// clear the set ahead of time
FD_ZERO(&readfds);
// add our descriptors to the set
FD_SET(s1, &readfds);
FD_SET(s2, &readfds);
// since we got s2 second, it's the "greater", so we use that for
// the n param in select()
n = s2 + 1;
// wait until either socket has data ready to be recv()d (timeout 10.5 secs)
tv.tv_sec = 10;
tv.tv_usec = 500000;
rv = select(n, &readfds, NULL, NULL, &tv);
if (rv == -1) {
perror("select"); // error occurred in select()
} else if (rv == 0) {
printf("Timeout occurred! No data after 10.5 seconds.\n");
} else {
// one or both of the descriptors have data
if (FD_ISSET(s1, &readfds)) {
recv(s1, buf1, sizeof buf1, 0);
}
if (FD_ISSET(s2, &readfds)) {
recv(s2, buf2, sizeof buf2, 0);
}
}
9.19.5 参照
9.20 setsockopt()
, getsockopt()
ソケットの各種オプションを設定します。
9.20.1 書式
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s, int level, int optname, void *optval,
socklen_t *optlen);
int setsockopt(int s, int level, int optname, const void *optval,
socklen_t optlen);
9.20.2 解説
ソケットはかなり設定しやすい獣です。実際、とても設定しやすいので、ここですべてをカバーするつもりはありません。どうせシステム依存でしょうから。しかし、基本的なことについてはお話します。
当然ながら、これらの関数はソケットの特定のオプションを取得・設定します。Linux では、ソケットの情報はすべて "man page for socket" の 7 章にあります。(これらの情報を得るには "man 7 socket
" とタイプしてください。)
パラメータとしては、s
はあなたが話しているソケットで、level
は SOL_SOCKET
に設定する必要があります。そして、optname
に興味のある名前を設定します。繰り返しになりますが、すべてのオプションについては man ページを参照してください。しかし、ここでは最も楽しいものをいくつか紹介します。
optname | 解説 |
---|---|
SO_BINDTODEVICE | このソケットは bind() を使って IP アドレスにバインドするのではなく、 eth0 のようなシンボリックなデバイス名にバインドしてください。Unix で ifconfig コマンドを入力すると、デバイス名が表示されます。 |
SO_REUSEADDR | このポートにバインドしているアクティブなソケットがない限り、他のソケットが bind() を行えるようにします。これにより、クラッシュ後にサーバを再起動しようとしたときに表示される "Address already in use" エラーメッセージを回避することができます。 |
SOCK_DGRAM | UDP データグラム(SOCK_DGRAM )ソケットがブロードキャストアドレスに送信されたパケットを送受信できるようにします。TCP ストリームソケットには何もしません---NOTHING!!---ははは! |
パラメータ optval
については、通常、問題の値を示す int
へのポインタとなります。ブール値の場合、0
は偽で、0
以外は真です。そしてそれは、あなたのシステムで異なっていない限り、絶対的な事実です。渡すべきパラメータがない場合、optval
は NULL
にすることができます。
最後のパラメータ optlen
には、optval
の長さ、おそらく sizeof(int)
を設定する必要がありますが、オプションによって異なります。getsockopt()
の場合、これは socklen_t
へのポインタであり、optval
に格納される最大サイズのオブジェクトを指定することに注意してください(バッファオーバーフローを防止するためです)。そして getsockopt()
は optlen
の値を、実際に設定されたバイト数を反映するように変更します。
警告:いくつかのシステム(特に Sun と Windows)では、このオプションは int
の代わりに char
とすることができ、例えば int
値 1
の代わりに '1'
という文字値を設定することができます。繰り返しになりますが、より詳しい情報は "man setsockopt
" と "man 7 socket
" で各自の man ページをチェックしてください!
9.20.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
9.20.4 例
int optval;
int optlen;
char *optval2;
// set SO_REUSEADDR on a socket to true (1):
optval = 1;
setsockopt(s1, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);
// bind a socket to a device name (might not work on all systems):
optval2 = "eth1"; // 4 bytes long, so 4, below:
setsockopt(s2, SOL_SOCKET, SO_BINDTODEVICE, optval2, 4);
// see if the SO_BROADCAST flag is set:
getsockopt(s3, SOL_SOCKET, SO_BROADCAST, &optval, &optlen);
if (optval != 0) {
print("SO_BROADCAST enabled on s3!\n");
}
9.20.5 参照
9.21 send()
, sendto()
ソケットでデータを送信します。
9.21.1 書式
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void *buf, size_t len, int flags);
ssize_t sendto(int s, const void *buf, size_t len,
int flags, const struct sockaddr *to,
socklen_t tolen);
9.21.2 解説
これらの関数は、ソケットにデータを送信します。一般に、send()
は TCP の SOCK_STREAM
接続型ソケットに、sendto()
は UDP の SOCK_DGRAM
非接続型データグラムソケットに用います。非接続型ソケットでは、パケットを送信するたびにその宛先を指定する必要があり、そのため sendto()
の最後のパラメータでパケットの行き先を定義しています。
send()
と sendto()
では、パラメータ s
がソケット、buf
が送信するデータへのポインタ、len
が送信するバイト数、flags
がデータの送信方法に関する詳細情報を指定することが可能です。通常の "データ" にしたい場合は、flags
を 0
に設定します。以下は一般的に使用されるフラグですが、詳細はローカルの send()
man ページを確認してください。
Macro | 解説 |
---|---|
MSG_OOB | "帯域外" データとして送信します。TCP はこれをサポートしており、このデータが通常のデータよりも優先順位が高いことを受信側のシステムに伝える方法です。受信側はシグナル SIGURG を受け取り、キューにある残りの通常のデータをすべて最初に受信することなく、このデータを受信することができます。 |
MSG_DONTROUTE | このデータはルータで送らず、ローカルに置いてください。 |
MSG_DONTWAIT | 送信トラフィックが詰まっているために send() がブロックされる場合、EAGAIN を返すようにします。これは "この送信のときだけノンブロックを有効にする" ようなものです。詳しくは blocking の章を参照してください。 |
MSG_NOSIGNAL | もし recv() が終了したリモートホストに send() した場合、通常 SIGPIPE というシグナルを受け取ります。このフラグを追加することで、このシグナルが発生するのを防ぐことができます。 |
9.21.3 返り値
実際に送信されたバイト数を返します。エラーの場合は -1
を返します(それに応じて errno
が設定されます)。実際に送信されたバイト数は、送信を要求したバイト数よりも少なくなる可能性があることに注意してください!これを回避するためのヘルパー関数については、handling partial send()
s の章を参照してください。
また、どちらかの側でソケットが閉じられていた場合、send()
を呼び出したプロセスはシグナル SIGPIPE
を受け取ります。(ただし、send()
が MSG_NOSIGNAL
フラグ付きで呼び出された場合を除きます。)
9.21.4 例
int spatula_count = 3490;
char *secret_message = "The Cheese is in The Toaster";
int stream_socket, dgram_socket;
struct sockaddr_in dest;
int temp;
// first with TCP stream sockets:
// assume sockets are made and connected
//stream_socket = socket(...
//connect(stream_socket, ...
// convert to network byte order
temp = htonl(spatula_count);
// send data normally:
send(stream_socket, &temp, sizeof temp, 0);
// send secret message out of band:
send(stream_socket, secret_message, strlen(secret_message)+1, MSG_OOB);
// now with UDP datagram sockets:
//getaddrinfo(...
//dest = ... // assume "dest" holds the address of the destination
//dgram_socket = socket(...
// send secret message normally:
sendto(dgram_socket, secret_message, strlen(secret_message)+1, 0,
(struct sockaddr*)&dest, sizeof dest);
9.21.5 参照
9.22 shutdown()
ソケットでのさらなる送受信を停止します。
9.22.1 書式
#include <sys/socket.h>
int shutdown(int s, int how);
9.22.2 解説
それだ!もうダメだ!このソケットではもう send()
はできないが、recv()
はまだしたい!その逆もしかり!どうすればいいんだ?
ソケット記述子を close()
すると、読み書きのためのソケットの両側を閉じ、ソケット記述子を解放します。片方だけを閉じたい場合は、この shutdown()
コールを使用することができます。
パラメータとして、s
は当然このアクションを実行したいソケットで、どのようなアクションを実行するかは how
パラメータで指定することができます。How
は SHUT_RD
でそれ以降の recv()
を禁止したり、SHUT_WR
でそれ以降の send()
を禁止したり、SHUT_RDWR
でその両方を禁止したりすることができます。
shutdown()
はソケット記述子を解放しないので、ソケットを完全にシャットダウンしても、最終的には close()
しなければならないことに注意してください。
これは、ほとんど使われないシステムコールです。
9.22.3 返り値
成功した場合は 0
を、エラーの場合は -1
を返します(それに応じて errno
が設定されます)。
9.22.4 例
int s = socket(PF_INET, SOCK_STREAM, 0);
// ...do some send()s and stuff in here...
// and now that we're done, don't allow any more sends()s:
shutdown(s, SHUT_WR);
9.22.5 参照
9.23 socket()
ソケット記述子をアロケートします。
9.23.1 書式
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
9.23.2 解説
ソケット的なことをするのに使える新しいソケット記述子を返します。これは一般に、ソケットプログラムを書くための膨大なプロセスの最初の呼び出しとなり、その後の listen()
, bind()
, accept()
や他の様々な関数の呼び出しでその結果を利用することができるようになります。
通常の使い方では、以下の例のように getaddrinfo()
を呼び出して、これらのパラメータの値を取得します。しかし、本当に必要であれば、手動でこれらの値を埋めることができます。
Macro | 解説 |
---|---|
domain | domain には、どのような種類のソケットに興味があるのかを記述します。これは様々なものがありますが、これはソケットガイドなので、IPv4 では PF_INET 、IPv6 では PF_INET6 となります。 |
type | また、type パラメータには様々なものを指定できますが、信頼性の高い TCP ソケット(send() , recv() )には SOCK_STREAM を、信頼性の低い高速な UDP ソケット(sendto() , recvfrom() )には SOCK_DGRAM を設定することになると思われます。(もうひとつの興味深いソケットタイプは SOCK_RAW で、これはパケットを手動で構築するために使用することができます。これはかなりクールです。) |
protocol | 最後に、protocol パラメータは、特定のソケットタイプでどのプロトコルを使用するかを指示します。既に述べたように、例えば SOCK_STREAM は TCP を使用します。幸いなことに、SOCK_STREAM や SOCK_DGRAM を使用する場合は、プロトコルを 0 に設定すれば、自動的に適切なプロトコルを使用することができます。そうでない場合は、getprotobyname() を使用して適切なプロトコル番号を調べることができます。 |
9.23.3 返り値
それ以降の呼び出しで使用される新しいソケット記述子、またはエラーの場合は -1
(それに応じて errno
が設定されます)。
9.23.4 例
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET, AF_INET6, or AF_UNSPEC
hints.ai_socktype = SOCK_STREAM; // SOCK_STREAM or SOCK_DGRAM
getaddrinfo("www.example.com", "3490", &hints, &res);
// make a socket using the information gleaned from getaddrinfo():
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
9.23.5 参照
accept()
,
bind()
,
getaddrinfo()
,
listen()
9.24 struct sockaddr
and pals
インターネットアドレスを扱うための構造体です。
9.24.1 書式
#include <netinet/in.h>
// All pointers to socket address structures are often cast to pointers
// to this type before use in various functions and system calls:
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
// IPv4 AF_INET sockets:
struct sockaddr_in {
short sin_family; // e.g. AF_INET, AF_INET6
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // zero this if you want to
};
struct in_addr {
unsigned long s_addr; // load with inet_pton()
};
// IPv6 AF_INET6 sockets:
struct sockaddr_in6 {
u_int16_t sin6_family; // address family, AF_INET6
u_int16_t sin6_port; // port number, Network Byte Order
u_int32_t sin6_flowinfo; // IPv6 flow information
struct in6_addr sin6_addr; // IPv6 address
u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
unsigned char s6_addr[16]; // load with inet_pton()
};
// General socket address holding structure, big enough to hold either
// struct sockaddr_in or struct sockaddr_in6 data:
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
9.24.2 解説
これらは、インターネット・アドレスを扱うすべてのシステムコールと関数の基本構造です。多くの場合、getaddrinfo()
を使ってこれらの構造体を埋めておき、必要なときにそれを読み出すことになります。
メモリ上では、struct sockaddr_in
と struct sockaddr_in6
は struct sockaddr
と同じ開始構造体を共有しており、一方の型のポインタを他方に自由にキャストしても、宇宙の終焉の可能性を除けば何の害もないのです。
もし、struct sockaddr_in*
を struct sockaddr*
にキャストしたときに宇宙が終わるとしたら、それは全くの偶然であり、心配する必要はないでしょう。
このことを念頭に置いて、関数が struct sockaddr*
を受け取るときはいつでも、struct sockaddr_in*
, struct sockaddr_in6*
, または struct sockadd_storage*
を簡単に安全にその型にキャストできることを覚えておいてください。
sockaddr_in
構造体は、IPv4 アドレス(例:"192.0.2.10")で使用される構造体です。アドレスファミリ(AF_INET
)、ポート(sin_port
)と IPv4 アドレス(sin_addr
)を保持します。
また、struct sockaddr_in
には sin_zero
というフィールドがあり、ある人はこれを 0
に設定しなければならないと主張しています。他の人はそれについて何も主張していませんし(Linux のドキュメントでは全く言及されていません)、 0
に設定することは実際には必要ないように思われます。ですから、もしあなたがそうしたいと思ったら、memset()
を使って 0
にセットしてください。
さて、この struct in_addr
は、システムによって奇妙な獣のようなものです。時には、あらゆる種類の #define
やその他のナンセンスなものを含むクレイジーな union
になることもあります。しかし、多くのシステムでは s_addr
フィールドしか実装されていないので、この構造体の s_addr
フィールドのみを使用する必要があります。
struct sockadd_in6
と struct in6_addr
は IPv6 で使用されることを除けば、非常によく似ています。
struct sockaddr_storage
は、IP バージョンに依存しないコードを書こうとしているときに、新しいアドレスが IPv4 か IPv6 か分からない場合に accept()
や recvfrom()
に渡すことができる構造体です。struct sockaddr_storage
構造体は、元の小さな struct sockaddr
とは異なり、両方のタイプを保持するのに十分な大きさを持っています。
9.24.3 例
// IPv4:
struct sockaddr_in ip4addr;
int s;
ip4addr.sin_family = AF_INET;
ip4addr.sin_port = htons(3490);
inet_pton(AF_INET, "10.0.0.1", &ip4addr.sin_addr);
s = socket(PF_INET, SOCK_STREAM, 0);
bind(s, (struct sockaddr*)&ip4addr, sizeof ip4addr);
// IPv6:
struct sockaddr_in6 ip6addr;
int s;
ip6addr.sin6_family = AF_INET6;
ip6addr.sin6_port = htons(4950);
inet_pton(AF_INET6, "2001:db8:8714:3a90::12", &ip6addr.sin6_addr);
s = socket(PF_INET6, SOCK_STREAM, 0);
bind(s, (struct sockaddr*)&ip6addr, sizeof ip6addr);
9.24.4 参照
accept()
,
bind()
,
connect()
,
inet_aton()
,
inet_ntoa()
10 その他の参考文献
ここまで来たのに、もっともっとと叫んでいるあなたへ。このようなことをもっと知るには、他にどこへ行けばいいのでしょうか?
10.1 書籍
昔ながらのパルプ紙を使った本をお探しなら、以下のような素晴らしい本があります。これらは、人気のある書店のアフィリエイト・リンクにリダイレクトされ、私に素敵なキックバックを与えてくれます。もしあなたが単に気前がいいと思うなら、beej@beej.us
に寄付をすることができます。:-)
Unix Network Programming, volumes 1-2 by W. Richard Stevens. Published by Addison-Wesley Professional and Prentice Hall. ISBNs for volumes 1-2: 978-0131411555, 978-0130810816.
Internetworking with TCP/IP, volume I by Douglas E. Comer. Published by Pearson. ISBN 978-0136085300.
TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs for volumes 1, 2, and 3 (and a 3-volume set): 978-0201633467, 978-0201633542, 978-0201634952, (978-0201776317).
TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 978-0596002978.
Advanced Programming in the UNIX Environment by W. Richard Stevens. Published by Addison Wesley. ISBN 978-0321637734.
10.2 Web 参考文献
ウェブで:
BSD Sockets: A Quick And Dirty Primer (Unixシステムのプログラミング情報も!)
そして、関連する Wikipedia のページを紹介します。
Transmission Control Protocol (TCP)
Serialization(パッキングとアンパッキングデータ)
10.3 RFCs
RFCs ---本当の汚れ!これは、割り当てられた番号、プログラミング API、インターネット上で使用されるプロトコルについて記述した文書です。ここでは、いくつかの RFC へのリンクを掲載しますので、ポップコーンを片手に考える帽子をかぶって楽しんでください。
RFC 1 ---最初の RFC。これは、"インターネット" が誕生したばかりの頃、どのようなものだったのか、そして、どのように一から設計されていたのかを知ることができます。(この RFC は、明らかに完全に時代遅れです!)
RFC 768 ---ユーザーデータグラムプロトコル(UDP)
RFC 791 ---インターネットプロトコル(IP)
RFC 793 ---伝送制御プロトコル(TCP)
RFC 854 ---テルネットプロトコル
RFC 959 ---ファイル転送プロトコル(FTP)
RFC 1350 ---些細なフィル転送プロトコル(TFTP)
RFC 1459 ---Internet Relay Chat Protocol(IRC)
RFC 1918 ---プライベートインターネットにおけるアドレスの割り当て
RFC 2131 ---Dynamic Host Configuration Protocol(DHCP)
RFC 2616 ---ハイパーテキスト転送プロトコル(HTTP)
RFC 2821 ---簡易メール転送プロトコル(SMTP)
RFC 3330 ---特殊用途のIPv4アドレス
RFC 3493 ---IPv6のための基本的なソケットインタフェース拡張
RFC 3542 --- IPv6用の拡張ソケットアプリケーション・プログラム・インターフェース(API)
RFC 3849 ---ドキュメンテーションのためのIPv6アドレスプレフィックス予約
RFC 3920 ---拡張可能なメッセージングおよびプレゼンスプロトコル(XMPP)
RFC 3977 ---ネットワークニュース転送プロトコル(NNTP)
RFC 4193 ---ユニークローカルIPv6ユニキャストアドレス
RFC 4506 ---外部データ表現標準(XDR)
IETF には、RFC の検索とブラウズのための素晴らしいオンラインツールがあります。