Winsock と BSD ソケットとの互換性
Warren Young 著
つまり、君はずっと長い間 UNIX ハッカーで、Windows プログラミ
ングについては良く知らないっていうわけだね? そして君の愛しの BSD
ソケットと互換性のある Winsock っていうすごい API のことを聞きつ
けた。だけど、いくらがんばってみても、君は readv()
呼出しを見つけることさえできなかった、ってわけかい? ちょうどいい
ぜ、相棒、この記事は君のためのものだ。
はじめに
始めは混沌がありました。この混沌は Windows TCP/IP API の非互
換性という形を取っていました。つまり、例えば FTP Software TCP/IP
スタック用に書かれたプログラムは、JSB のスタック上では動かなかっ
たわけです。
そして 1990 年のいつ頃か、何人もの人々が一緒になって、一つの、
かっこいい、大きな、互換性のある、Windowsソケットと呼ばれる API
を作って、一つのプログラムをどのベンダのプロトコルスタック上でも
動かすことができるようにしよう、と決めました。彼らはまたこのAPI
を、すでに普及しているネットワークプログラミングの BSD ソケット
モデルを元にすることも決めました。しかしさまざまな理由によって、
Winsock と BSD ソケットの間には未だに多くの相違点があります。こ
の記事では、Winsock が BSD ソケットとどのように異なっているかを
示し、BSD ソケットのプログラムを Winsock の同等の機能をどのよう
に使って変換できるかを示します。
この記事はまだ発展途上です。以下の各節には、「追加」の日付と
「更新」日付がついているものがあるので、定期的にこの文書に戻って
きて、それまで Winsock API でカバーされていなかったもので、何か
新しくBSD 風の動作ができるものがないかチェックすることができます。
この文書への変更点は、Winsock プログラマー FAQ のメインページの、
What's New セクションでアナウンスされます。
公式のお言葉 (2000.06.12 追加)
Winsock 仕様書には「
バークレーソケットからの逸脱」というセクションがあり、この記
事と同様な議論やそれ以外にもいくつかの議論を取り上げています。
#include の相違 (2000.06.12 追加)
BSD ソケットでは、どのソケット呼び出しを使っているかによって、
いくつか異なるヘッダファイルがあります。典型的な BSD ソケットプ
ログラムでは、最初のほうに以下のような #include の塊があるでしょ
う。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
Winsock ではこれらは全く必要ありません。代わりに、単に
winsock.h を include するだけで良いです。(Winsock 2 特有の機能が
必要な場合は winsock2.h です。)
errno
対 WSAGetLastError() (1998.12.24 追加、2000.06.12 更新)
WSAGetLastError() は、本質的には Unix の
errno グローバル変数と同じ物です。エラー定数のマク
ロ名と値はもちろん違っていますが。Winsock 仕様書の中に、全てのエ
ラー定数に対して Winsock エラー定数毎に対応する BSDエラー定数を
一覧に示した表があります。その違いのほとんどは、単に Winsock の
方の定数マクロ名のはじめに "WSA" が追加されているだけです。(すな
わち、 BSD のエラー定数 EINTR の Winsock 版は WSAEINTR です。)
他に覚えておくべきこととしては、perror() 呼び出し
は、ほとんどの Windows 用コンパイラのランタイムライブラリに存在
しているものの、Winsock 呼び出しに対しては動作しない、ということ
があります。(これは Winsock は errno 変数を使っていないことによ
ります。) この FAQ の basic Winsock examples
のところには WSAGetLastErrorMessage() という関数が
あり、これは perror() 風関数として使えるように作ら
れています。これは ws-util.cpp モジュールに入っています。
EAGAIN (1998.12.24
追加)
多くの Unix 上のプログラム、特に System V 起源のものでは、非
ブロック型の呼び出しが失敗したときは、グローバル変数
errno の値が EAGAIN であるかどうかをチェックするよ
うになっています。これは BSD の EWOULDBLOCK や Winsock の
WSAEWOULDBLOCK エラーと同じ物です。あなたのシステムでどうなって
いるかはヘッダファイルをチェックしてもらうしかありませんが、これ
に関して私がチェックした限りの全ての Unix では、#define EAGAIN
と EWOULDBLOCK とは同じ値でした。ですので、Winsock 間との移行が
お互いに容易になるように、Unix 上では EAGAIN の代わりに
EWOULDBLOCK を使う習慣をつけたほうが良いかもしれません。
ファイルハンドルとソケットハンドルの同一性
(1998.08.01 追加、2000.06.12 更新)
Unix 上では、I/O システムコールはファイルハンドルとソケットハ
ンドルに対して全く同等に動作します。例えば BSD ソケットプログラ
ムにおいては、ソケットからデータを読み込むときに、
recv() の代わりに read() を使うのはよ
く行われることです。Windows 3.1 と Windows 9x では、ソケットハン
ドルとファイルハンドルは完全に別物です。多くの Windows コンパイ
ラが提供するランタイムライブラリ(RTL)には POSIX エミュレーション
関数が含まれていますが、これらはファイルに対してのみ動作するよう
に設計されていて、ソケットに対してではありません。
Windows NT/2000 では、ファイル I/O 関数はソケットに対しても動
作するようになっているという点でより Unix 的です。例えば
ReadFile() は Winsock の recv() 関数と
ほぼ同等です。
Visual C++ のランタイムライブラリは POSIX 関数をエミュレート
していますが、その名前として頭にアンダースコアが付いているという
点だけが異なっています。例えば read() ではなく、
_read() になります。_read() 関数は内部
的に ReadFile() を使っているので、この関数はソケッ
トに対しても動作するだろう、と思うかもしれません。しかし問題は、
この関数の最初の引数はランタイムライブラリ特有のハンドルであって、
オペレーティングシステムのファイルハンドルではない、ということに
あります。もしソケットハンドルを _read() や
_write() に渡したとすると、ランタイムライブラリにとっ
てはそれが自分が扱えるハンドルではないと気付き、呼び出しは失敗し
てしまうのです。
しかし幸運なことに、Visual C++ のランタイムライブラリには橋渡
しのための関数 _open_osfhandle() があります。(もし
Visual C++ 以外を使っているのであれば、そのランタイムライブラリ
の提供元に同様の関数があるかを確認しなければならないでしょう。)
私はこれを試したことはないのですが、これはオペレーティングシステ
ムのファイルハンドル(ソケットハンドルも含む)を取り、ランタイムラ
イブラリの POSIX エミュレーション関数に対して使えるハンドルを返
却するもののようです。これは Microsoft 以外の Winsock スタックで
も、それがきちんと作られていれば動作するはずだと私は聞きましたが、
なにしろ私は試したことがないので、もしあなたが他のプロトコルスタッ
クもサポートしたいのであれば、ご自分で確認してください。:)
なお、_open_osfhandle() を使った手法は Windows
NT/2000 でしか動作しません。なぜなら Win9x のファイル入出力関数
はソケットに対しては動作しないからで、ランタイムライブラリをだま
して受け取らせる方法では解決できないのです。
この制限があなたにとって厳しすぎるのであれば、Unix エミュレー
ションシステムを試してみるのも良いかもしれません。私個人的には、
この目的のために Cygwin を使っ
ており、見事に動いています。他に同様なものとして UWin、Interix、
MainWin、Wind/U などがありますが、私はこれらを使ったことはありま
せん。
まあこれらを脇に置いとくとしても、普通はプログラムを書き換え
て、read() のようないかにも Unix 風のものを使って
Unix の流儀で無理矢理 Windows 上で動作するように移植するよりも、
recv() などの移植性の良い関数を使うようにする方がは
るかに簡単です。
poll() (2000.07.04 追加)
Winsock 用の poll() は、Jarle Aasa 氏による adns ライブラリの Win32 版 とい
う実装があります。この実装には二つの制限があります。1) この実装
は GPL でライ
センスされます。つまりあなたのプログラムも GPL でライセンスする
のでなければ、このコードを使うことはできません。2) この実装は
Win32 のイベントオブジェクト機構を使って構築されているため、同時
に 64 個以上ソケットを扱うことができない、という意外な 制限があります。詳しくはそ
の FAQ 項目を参照して下さい。
Winsock の closesocket() 対 BSD
の close() (2000.06.12 追加)
Winsock における closesocket() は、ソケットディ
スクリプタを close するものです。BSD の close() シ
ステムコールは、ある種のハンドルを close するもので、そのハンド
ルがソケットディスクリプタであることもあります。この違い
によって関数名の違いがでてくるのです。ファイルハンドル・ソケット
ハンドルの違いの問題については、二つ前の項目での議論を参照してく
ださい。
Winsock の ioctlsocket() 対 BSD
の ioctl() (1998.08.01 追加)
BSD Unix (とその他の Unix の仲間)は ioctl() 呼び
出しを提供することによって、ソケットやファイルハンドルに対するさ
まざまな情報を取得、設定できるようにしています。Winsock はこのほ
とんどを ioctlsocket() 呼び出しによって模倣していま
すが、全てのものがあるわけではありません。以下に同等なものを示し
ます。(他にもあったら歓迎します!)
SIOCGIFCONF
この ioctl() オプションは利用可能なネットワークイン
ターフェースに関する情報が得ることができるようにします。Winsock
2 では ioctlsocket() の
SIO_GET_INTERFACE_LIST オプションによって、同様の機
能を提供しています。
fcntl() (2000.06.12 追加、2001.01.23 更新)
Unix の fcntl() 呼び出しに直接対応するものは
Winsock にはありません。それが必要なところでは、Winsock の
ioctlsocket() 呼び出しに同様の機能が存在しています。
例えば、Unix の fcntl() を使ってソケットの
O_NONBLOCK フラグを設定するのと同等のことは、Winsock の
ioctlsocket() で FIONBIO フラグを設定する
ことによってできます。
コネクションが切れたことの検出 (1999.09.25 追加、2000.06.12 更新)
BSD Unix では、プログラムが recv() 中でブロック
していて、接続相手がコネクションを閉じたとすると、
recv() からは 0 が返されます。Winsock でも同じよう
に振舞いますが、例外として -1 が返されることもあります。このとき
WSAGetLastError() は WSAECONNRESET 、
WSAECONNABORTED 、WSAESHUTDOWN のいずれ
かを返却し、コネクションの異常切断の種類が検出できたことを通知し
ます。
Unix 上では、プログラムが send() 中でブロックし
ていて、SIGPIPE シグナルを無視するようになっていれ
ば、接続相手がコネクションを切ったときに -1 が返却されて、
errno の値は EPIPE になります。シグナ
ルを無視するようにしていなければ SIGPIPE シグナルが発生し、シグ
ナルハンドラで何もしないとプログラムは終了します。Winsock におい
ては SIGPIPE/EPIPE の機能は全く存在しません。send()
は、正常切断に対しては 0 、異常切断に対しては -1 のいずれかを返
却します。このとき WSAGetLastError() の返却値は、上
記の recv() のときと同様のエラーが返されます。
readv() と
writev() (1998.08.01 追加)
BSD ソケットの分解・集積機構は、Winsock 2 のオーバーラップ
I/O 機構の一部と非常によく似ています。オーバーラップ I/O は、い
くつかのバッファをネットワークに送る前に、
WSASend() に渡して「集積」させ、また入ってきたデー
タを WSARecv() を使っていくつかの入力バッファに「分
解」させることを可能にします。
dup() (1998.12.24 追加、2001.01.23 更新)
Unix の dup() 関数はファイルハンドルを複製するも
ので、もちろんソケットに対しても動作します。Winsock 2 においては
WSADuplicateSocket() を使って同じことができます。実
際にはもうちょっと複雑になりますが、MSDN の
WSADuplicateSocket() のドキュメントには、この機能の
使い方を一つ一つ説明した良い例があります。
dup2() (1998.12.24 追加、1999.06.27 更新)
Winsock では、dup2() 機能とは異なってはいますが、
この機能の一部をサポートするものがあります。Unix においては
dup2() はハンドルを受け取り、dup() と
同じようにハンドルを複製しますが、このとき、新しいファイルハンド
ルをあなたの指定した値を割り当てる、というちょっと特別なことをし
ます。これは通常、ソケットを C 言語の標準入力、標準出力のファイ
ルハンドルに割り当て、printf() や
fgets() のような標準入出力関数をソケットに対しても
使えるようにするために使われます。
Microsoft Knowledge Base の Q190351
の項目では、子プロセスの標準ハンドルをソケットにリダイレクトする
方法を示しています。この方法における制限事項は、自分自身のプロセ
スのハンドルに対してはできないということと、任意のハンド
ルに対してソケットにリダイレクトすることはできない、ということ
(つまり、標準入力、標準出力、標準エラーのみに対してできるという
こと)、また全てのプロセスでこの API の機能が完全に動作するわけで
はない、ということです。しかし少なくとも、Win32 上で inetd 風の
プログラムを作る程度のことは可能です。
UDP の振舞い (2001.03.26 追加)
Ilpo Ruotsalainen 氏は以下のように述べています。「... 多くの
BSD ソケットの実装では、遅延 UDP エラー(少なくとも ICMP port
unreachable、おそらくは他のエラーも)はrecvfrom() に
は渡されませんが、Winsock 2 では行われます(Windows 2000 上ではそ
うですが、Windows 98 は違います)。Linux も同様(Windows 2000 のよ
うな振舞い)ですが、BSD 風と互換の動作をさせるために
setsockopt() に SO_BSDCOMPAT が用意されています。」
別の言い方をすれば、移植性の高いプログラムにするには、
recvfrom() から非即時の問題に対するエラーが返される
ことに対応するべきであり、またそれに依存してもならないということ
になります。
更新の嘆願
この記事はさらなる作業が常に必要です。このままでも Winsock に
おける BSD 風振舞いについて、いくらかは良い情報を提供しています
が、しかし埋められるべき穴はまだたくさん、たくさん残っています。
もし助けていただけるのなら、どうか私にご一報を! 特に、他の BSD
のみの ioctl() オプションのいくつかは、Win32 機構の
他の方法で模倣できるということは間違いないと思います。
Copyright © 1998-2001 by Warren Young. All rights
reserved.
|