WSAGetLastErrorとWSAStartupでWinsockを初期化する
WindowsでTCP/IP通信やUDP通信を実装するには、ソケット関数を呼び出す前にWinsockの利用準備を整える必要があります。中心となるのが、Winsock DLLを初期化するWSAStartup関数と、直前の関数呼び出しで発生した通信エラーを取得するWSAGetLastError関数です。
この2つは役割が異なります。WSAStartupは通信機能を使える状態にする初期化処理、WSAGetLastErrorは失敗原因を調べるためのエラーコード取得処理です。初期化の戻り値を確認せずにソケットを作成すると、原因が分かりにくいWSANOTINITIALISEDが発生することがあります。
Windows APIの呼び出し規約やエラー処理を整理するときは、関連するAPIの説明をまとめたWindows API資料も参照できます。Winsock固有の規則を理解し、初期化、通信、終了処理を一組として設計することが重要です。
WSAStartupが担う初期化処理
WSAStartupは、アプリケーションが使用するWinsockのバージョンを指定して、Winsock DLLを初期化します。一般的なWinsock 2.2を利用する場合は、次のように呼び出します。
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
// resultにはWSAStartup固有のエラーコードが入る
return 1;
}
第1引数のMAKEWORD(2, 2)は、メジャーバージョン2、マイナーバージョン2を要求する指定です。第2引数のWSADATA構造体には、実際に利用可能なバージョンや実装情報が格納されます。通常は戻り値が0であれば初期化成功と判断します。
重要なのは、WSAStartupの失敗時にWSAGetLastErrorを呼び出してはいけない点です。WSAStartupは失敗理由を戻り値として直接返すため、まずresultの値を確認します。WSAGetLastErrorは、初期化後にsocket、bind、connect、recvなどのWinsock関数が失敗した場合に利用します。
WSAGetLastErrorで失敗原因を取得する
ソケット関数の戻り値がエラーを示したときは、できるだけ直後にWSAGetLastErrorを呼び出します。例えばsocket関数は失敗時にINVALID_SOCKETを返します。
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
int errorCode = WSAGetLastError();
// errorCodeをログへ記録する
}
connectやbindなど、関数ごとに定められた失敗値を先に確認することも欠かせません。戻り値を確認せずにWSAGetLastErrorだけを呼ぶと、以前の処理で設定された古いエラーコードを扱う可能性があります。また、成功した直後のWSAGetLastErrorの値には意味がないため、成功時の値を診断情報として利用してはいけません。
WSAGetLastErrorが返す値は、通常のWin32 APIで使われるGetLastErrorのコードとは別体系です。例えば、初期化前にWinsock関数を呼んだ場合はWSANOTINITIALISED、接続先が応答しない場合はWSAETIMEDOUTが候補になります。エラーコードを表示する際は、数値だけでなく発生したAPI名や対象のアドレスも記録すると調査しやすくなります。
初期化と終了処理を対にする
WSAStartupを呼び出したアプリケーションは、通信処理が終わった段階でWSACleanupを呼び出します。Winsockは内部的に初期化回数を管理しているため、WSAStartupを複数回呼んだ場合は、対応する回数だけWSACleanupを実行する必要があります。
bool winsockReady = false;
WSADATA data;
if (WSAStartup(MAKEWORD(2, 2), &data) == 0) {
winsockReady = true;
// socket、connect、送受信などの処理
WSACleanup();
winsockReady = false;
}
実際のプログラムでは、早期returnや例外によってWSACleanupが抜けない設計にします。C++ならRAIIを利用した管理クラスを作成し、デストラクタで終了処理を行う方法が安全です。C言語では終了ラベルを一つに集約し、生成済みのソケット、接続、Winsock環境を順番に解放します。
Winsockの初期化はプロセス全体の通信基盤を用意する処理であり、ソケット単位の作成とは異なります。CreateMutexなどのハンドル管理でも、生成した資源を適切な順序で解放する設計が必要です。Windows APIの資源管理に関するMutexの実装例も、終了処理を考える際の参考になります。
よくある失敗と診断方法
WSANOTINITIALISEDが発生する代表例は、WSAStartupより前にsocket、getaddrinfo、inet_ptonなどのWinsock関連APIを呼び出しているケースです。初期化関数をmainの途中に置くより、通信機能を管理する開始処理の先頭で一度だけ呼び出すと、呼び出し順序が明確になります。
WSAStartupそのものが失敗した場合は、戻り値をWinsockのエラーコードとして確認します。要求したバージョンをサポートしていない場合、引数が不正な場合、DLLやシステム環境に問題がある場合などが考えられます。WSADATAのwVersionとwHighVersionを調べれば、要求したバージョンと実際に利用可能なバージョンの差も確認できます。
診断時には、エラーを取得する呼び出しを別のAPIで挟まないことが大切です。例えばsocketが失敗した直後にログ出力用のWinsock関数や別の処理を実行すると、確認対象が変わる可能性があります。エラーコードを直ちにローカル変数へ保存し、その後に文字列化やログ出力を行うと安全です。
実装全体での使い分け
WSAStartupの戻り値、ソケット関数の戻り値、WSAGetLastErrorの取得タイミングを分けて考えると、初期化処理と通信処理の境界がはっきりします。次のように、各APIが返す情報の意味を整理できます。
| 処理 | 成功時の確認 | 失敗時の確認 | 主な注意点 |
|---|---|---|---|
| WSAStartup | 戻り値が0 | 戻り値を直接確認 | WSAGetLastErrorを使わない |
| socket | INVALID_SOCKETではない | 直後にWSAGetLastError | 作成失敗後に無効なソケットを使わない |
| bind | 0 | 直後にWSAGetLastError | ポート競合やアドレス設定を確認 |
| connect | 0 | 直後にWSAGetLastError | タイムアウトや接続拒否を区別する |
| send・recv | 送受信バイト数 | 直後にWSAGetLastError | 0は切断を示す場合がある |
| WSACleanup | 戻り値を必要に応じて確認 | 終了順序を点検 | WSAStartupと回数を対応させる |
例えばconnectの失敗を調べるコードでは、エラーコードを保存してからメッセージを出力します。
int result = connect(sock, reinterpret_cast<sockaddr*>(&server), sizeof(server));
if (result == SOCKET_ERROR) {
const int errorCode = WSAGetLastError();
if (errorCode == WSAECONNREFUSED) {
// 接続先で待ち受けていない
} else if (errorCode == WSAETIMEDOUT) {
// 応答が一定時間内に届かない
}
}
実用上の要点は、プログラム開始時にWSAStartupを一度実行し、各Winsock関数の失敗直後にWSAGetLastErrorでコードを保存し、すべてのソケットを閉じた後にWSACleanupを呼ぶことです。この順序を守れば、Winsockの初期化不足、通信エラー、終了処理の漏れを切り分けやすくなります。