Windows Network Programming 入門 1. はじめに  「Windows Network Programming 入門」ということで、今回はWindows版ソ ケットのWinsock(Windows Sockets)を使ってネットワークプログミングを体験 してみます。もともとソケットという概念はUNIXで利用されているものですが、 もちろんWindowsでもWinSockという形で実現されています。コンパイラは VC++.NET(VisualC++.NET)を使っていますが、VC++ならどれでもOKでしょう。 しかし私はVC++6.0以上しか使ったことがないのでそれ未満のバージョンは分 かりません。あとボーランド社のBCCも分かりません。Windowsプログラミング に関する基礎的なことは 「猫でもわかるプログラミング」(http://www.kumei.ne.jp/c_lang/) がとても有意義なサイトですので参照してください。 2. WinSock(Windows Sockets)とは  WindowsでTCP/IPの機能を利用したソフトウェアを開発するためのAPI。BSD OS向けに開発されたBSD Socketは、ネットワーク対応ソフトウェアの開発を容 易にする優れた「ソケットインターフェース」を備えていた。これをWindows アプリケーションの開発でも利用できるよう用意されたのがWinsockである。 Winsockの登場により、Windows上でのTCP/IPアプリケーションの開発が容易に なり、また、UNIX用のネットワークアプリケーションの移植が容易になった。 Winsockでは、UNIX系OSのソケット機能に加えて、Windows独自の機能が追加さ れており、また、UNIX系にあってWindows版にはない機能もある。このため、 WindowsとUNIXで完全に同じソースコードが利用できるわけではなく、移植の 際には修正作業が必要となる。Winsock2ではTCP/IP以外のプロトコルにも対応 している。(IT用語辞書 e-Words より) 3. HTTPクライアント  まずはクライアントプログラム書きます。ということでHTTPを利用してホー ムページを取得するプログラムを書いてみます。HTTPってなに? という方は Studying HTTP(http://www.studyinghttp.net/)をお読みください。あとRFC なども参照してください。 http://ruffnex.oc.to/kenji/src/windows/HTTP/cHTTP.cpp ----- cHTTP.cpp #define WIN32_LEAN_AND_MEAN -----  まずWIN32_LEAN_AND_MEANの定義ですが、これを定義しておくとヘッダファ イルから使用頻度の低いAPIを排除してくれます。よってコンパイルが速くな るらしいです。VC++が自動で生成してくれるスケルトンのstdafx.hに書き込ま れてます。 ----- #include #include #include #pragma comment(lib, "WSock32.lib") -----  winsock2.hをインクルードしなければなりません。これにネットワークに関 することが定義されてます。ちなみにWinSockにはバージョンがありWinSock2 を利用するばあいはwinsock2.hをインクルードしなければなりません。が、 WinSock1.1を利用するばあいwinsock.hですがwinsock2.hでもよいので結局こ れをインクルードしてればOKということになります。#pragma...は WSock32.lib(ws2_32.libでも可)をプロジェクトに追加してればコメントア ウトしてください。 ----- //Winsockの初期化をします WSADATA wsaData; if(WSAStartup(MAKEWORD(2, 1), &wsaData) != 0){ perror("WSAStartup"); exit(1); } -----  ここがUNIXとの大きな違いですね。一応最低限の説明を入れておくと、二つ の数字MAKEWORD(2, 1)はWinSockのバージョンをあらわします。MAKEWORD(2, 1)ならバージョン2.1でMAKEWORD(1, 1)ならバージョン1.1ですね。それだけで す。 ----- //ソケット作成 SOCKET sock; if((sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET){ perror("socket"); exit(1); } -----  ソケットを作成してます。WindowsにはなんとSOCKET型というものが存在し ます。といっても別にsocket関数の戻り値をint型で受け取ってもよいのです けどね。まぁあるのでSOCKET型を使いましょう。INVALID_SOCKETとはなんでしょ うか? これはwinsock2.h(C:\Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include\WinSock2.h)に定義されているので、興味が あれば見てみてください。 ----- //接続先を設定 LPHOSTENT lpHost; if((lpHost = gethostbyname(servName)) == NULL){ perror("gethostbyaddr"); exit(1); } -----  おなじみのgethostbyname関数です。 ----- SOCKADDR_IN Serv; memset(&Serv, 0, sizeof(Serv)); Serv.sin_family = AF_INET; Serv.sin_port = htons(80); Serv.sin_addr = *((LPIN_ADDR)*lpHost->h_addr_list); //接続 if(connect(sock, (PSOCKADDR)&Serv, sizeof(Serv)) != 0){ perror("connect"); exit(1); } -----  定石ですね。Serv.sin_portが接続先のポート、Serv.sin_addrにアドレスを いれてそのままconnect関数で接続します。 ----- //データ送信 char Data[256]; sprintf(Data, "GET /%s HTTP/1.0\r\n\r\n", Path); send(sock, Data, (int)strlen(Data), 0); printf("%s", Data); -----  send関数を使ってデータをサーバに送ります。 ----- //データ受信 memset(Data, '\0', sizeof(Data)); while(recv(sock, Data, sizeof(Data) - 1, 0) > 0){ printf("%s", Data); memset(Data, '\0', sizeof(Data)); } -----  recv関数で受信します。recvの戻り値は受信したデータの長さなので0以下 になったらwhileを抜けます。 ----- //送受信停止 if(shutdown(sock, SD_BOTH) != 0) perror("shutdown"); -----  shutdown関数は送受信を制御します。第二引数のSD_SENDは送信を、 SD_RECEIVEは受信を、SD_BOTHは送受信をそれぞれ停止させます。 ----- closesocket(sock); WSACleanup(); -----  最後にあとかたづけをします。Windowsのばあいclosesocket関数を使います。 closesoketはソケットを閉じる、WSACleanupはWinSockのリソースを開放しま す。まぁ細かいことは気にせずソケットを使ったあとはこれを実行する。とい うことだけ覚えておけばよいかと。  ではこのプログラムを実行しましょう。といってもCUIなのでコマンドプロ ンプトから以下のように打ってください。 ----- C:\...\cHTTP\Debug>cHTTP http://www.microsoft.com/ > test.txt C:\...\cHTTP\Debug> -----  これでtest.txtにマイクロソフトのホームページが取得できたとおもいます。 簡単ですね。しかしせっかくWindowsで実験してるので(しかもVC++でやって んだから)GUIでやりたいですよね。ということで以上のことをふまえた上で GUIのHTTPクライアントを作ってみます。 http://ruffnex.oc.to/kenji/src/windows/HTTP/gHTTP.cpp http://ruffnex.oc.to/kenji/src/windows/HTTP/resource.h http://ruffnex.oc.to/kenji/src/windows/HTTP/gHTTP.rc  「ファイル->新規作成->プロジェクト->Win32プロジェクト->空のプロジェ クト」でプロジェクトを作成します。上記のファイルはすべて新しく作成した プロジェクトフォルダ以下に置けばいいかと。そして「ファイル->存続の項目 の追加」として上の3つのファイルを追加してください。VS.NETでの説明をし ていますが、VC++6.0でも同じような項目がありますので適当に読み替えてく ださい。さてソースは長いですが半分以上は窓ですので気にせずいきましょう。 ----- gHTTP.cpp case WM_CREATE: if((MainMemory = (char *)VirtualAlloc( NULL, TEXT_SIZE, MEM_COMMIT, PAGE_READWRITE)) == NULL){ MessageBox(hWnd, "必要な領域を確保できませんでした", "Error", MB_OK); DestroyWindow(hWnd); } GetClientRect(hWnd, &rect); hEditWindow = CreateWindow("EDIT", NULL, WS_CHILD | WS_VISIBLE | ES_WANTRETURN | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL | ES_AUTOHSCROLL | WS_HSCROLL, 0, 0, rect.right, rect.bottom, hWnd, NULL, hInst, NULL); SendMessage(hEditWindow, EM_SETLIMITTEXT, (WPARAM)TEXT_SIZE, 0); break; -----  最初にVirtualAlloc関数を利用してメモリ空間を確保します。確保されてい るメモリサイズはTEXT_SIZEとなっていますが、これは次のCreateWindow関数 により作成されるテキスト領域の最大サイズです。ソースの最初に定義されて いますが、TEXT_SIZEは(1024 * 64)となっていますので65536バイト確保しま す。この程度ならいまのPCに搭載されてるメモリならどうってことはないで しょう。 ----- case IDM_CONNECT: MainProg(hEditWindow, hWnd, MainMemory); break; case IDM_SKELETON: setString(hEditWindow); break; -----  メニューバーの「ファイル->接続」が選択されたらMainProg関数を呼びます。 これはのちほど説明するということでその下の「ファイル->スケルトン」が選 択された場合はsetString関数が呼ばれます。これはソースの一番したにあり ます。 ----- void setString(HWND hEditWindow) { char *string = "www.microsoft.com\r\n" "GET / HTTP/1.0\r\n" "\r\n"; Edit_SetText(hEditWindow, string); } ----- というように一行目がサーバ、二行目がHTTPリクエストというようなテキスト を出力するだけです。もし「ファイル->接続」が選択されたら書かれてある文 字列を読み取ってサーバに接続し、レスポンスを受け取って再びWindowに出力 ということをするのですが、それがのちのち出てくるMainProg関数の処理とな ります。 ----- case WM_DESTROY: VirtualFree(MainMemory, TEXT_SIZE, MEM_DECOMMIT); PostQuitMessage(0); break; -----  もちろん終了時にはメモリを開放しなければなりません。 ----- BOOL MainProg(HWND hEditWindow, HWND hWnd, char *MainMemory) { if(GetData(hEditWindow, MainMemory)){ MessageBox(hWnd, "テキストを取得できません", "Error", MB_OK); return TRUE; } -----  ここが「ファイル->接続」が選択されたときに実行される場所です。最初に GetData関数を呼び出しています。これはWindowのテキスト領域に書かれてあ る文字列を、プログラムの最初に確保したメモリ領域MainMemoryに格納してい ます。 ----- char HostAddr[64]; char *Header = GetLine(HostAddr, (UINT)sizeof(HostAddr), MainMemory); -----  GetLine関数は最初の一行(改行まで)を取得します。よってMainMemory (テキスト領域に書かれてあるすべての文字列)から最初の一行(サーバ)を 取得しHostAddrに格納します。 ----- SOCKET sock; if((sock = ServConnect(HostAddr)) == -1){ MessageBox(hWnd, "サーバに接続できません", "Error", MB_OK); WSACleanup(); return TRUE; } -----  ServConnect関数は接続処理を担当します。 ----- send(sock, Header, (int)strlen(Header), 0); -----  ここでHTTPヘッダを送信します(ヘッダといっても一行しかないですけ どね)。 ----- UINT L = 0, rLen = 0, DataLen = (UINT)strlen(MainMemory); while((L = recv(sock, MainMemory + DataLen + rLen, TEXT_SIZE - DataLen - rLen - 1, 0)) > 0){ rLen += L; } Edit_SetText(hEditWindow, MainMemory); -----  受信してそれをWindowのテキスト領域に書き込みます。あとは接続の終了処 理です。さて、とりあえずプログラムの説明は終わったので、実行してみてく ださい。まずは「ファイル->スケルトン」としてマイクロソフトのHPアドレ スとリクエストヘッダを出力させます。そして、「ファイル->接続」として、 マイクロソフトのサーバに接続してください。見事 http://www.microsoft.com のページが取得できたと思います(図1)。 http://ruffnex.oc.to/kenji/src/windows/HTTP/http.png(図1)  図1のように表示されたら成功です。  さてさて、これまではWinSockというライブラリを利用していました。これ を使えばソケットを利用することができましたが、実はネットワーク関連には WinInetという便利なライブラリが存在します。このライブラリはHTTPと FTPに限り有効ですが、なんさま便利です。便利すぎてHTTPに関する知 識を必要とせずにHTTPを利用できてしまいます(FTPに関しても同じで す)。「だったらそっちを最初に教えろよ!」って話ですが、WinInetは便利 であるがために応用がききません。ソケットプログラミングを理解していれば、 HTTPだろうとFTPだろうとSMTPだろうとPOP3だろうと、もちろ んOSがWindowsじゃなくともその知識は容易に応用できますが、WinInetを利 用したネットワークプログラムしか知らなかったら、Windowsでしか、しかも HTTPとFTPしか利用できないのです。そういう意味でソケットレベルで のプログラムを先に説明しました。ではWinInetを利用したHTTPクライア ントを作ってみます。 http://ruffnex.oc.to/kenji/src/windows/HTTPWinInet/gHTTP2.cpp http://ruffnex.oc.to/kenji/src/windows/HTTPWinInet/resource.h http://ruffnex.oc.to/kenji/src/windows/HTTPWinInet/gHTTP2.rc ----- HINTERNET hInet; hInet = InternetOpen("server", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if(hInet == NULL){ MessageBox(hWnd, "InternetOpen", "Error", MB_OK); return TRUE; } -----  InternetOpen関数はWinInetの初期化を行い戻り値としてハンドルを返しま す。失敗したばあいはNULLが返ります。第一引数はHTTPによりインターネット にアクセスする際のエージェント名(要するに何でも良い)。第二引数はアク セス方法を指定するフラグ。 INTERNET_OPEN_TYPE_DIRECT 全てのホスト名をローカルで解決します。 INTERNET_OPEN_TYPE_PROXY プロキシを利用する。 INTERNET_OPEN_TYPE_PRECONFIG レジストリに保持されている設定を利用する。 通常はINTERNET_OPEN_TYPE_PRECONFIGで問題ないでしょう。第三引数はプロキ シによるアクセスが指定されている場合にプロキシサーバ名を指定する。プロ キシを使用しないばあいはNULL。他はMSDNを参照。 ----- HINTERNET hUrl; hUrl = InternetOpenUrl(hInet, (LPCTSTR)URL, NULL, 0, 0, 0); if(hUrl == NULL){ MessageBox(hWnd, "InternetOpenUrl", "Error", MB_OK); InternetCloseHandle(hInet); return TRUE; } -----  InternetOpenUrl関数は第一引数にInternetOpenの戻り値であるハンドル。 第二引数にHPアドレスの文字列。第三引数にはサーバに送るヘッダ文字列 (NULLでもよい)。第四引数に第三引数の文字列の長さ(第五、第六引数は MSDNなどを参照してください)。戻り値は、オープンした結果のハンドルであ り失敗時はNULL。 ----- HttpQueryInfo(hUrl, HTTP_QUERY_RAW_HEADERS_CRLF, MainMemory + DataLen, &rLen, NULL); if(GetLastError()==ERROR_HTTP_HEADER_NOT_FOUND){ MessageBox(hWnd, "InternetOpenUrl", "Error", MB_OK); InternetCloseHandle(hInet); InternetCloseHandle(hUrl); return TRUE; } -----  HttpQueryInfo関数はレスポンスヘッダを取得します。詳細はMSDNで調べて ください。 ----- DataLen = (UINT)strlen(MainMemory); InternetReadFile(hUrl, MainMemory + DataLen, TEXT_SIZE - DataLen - 1, &rLen); InternetCloseHandle(hUrl); InternetCloseHandle(hInet); -----  InternetReadFile関数にてレスポンスデータを読み込みます。最後に InternetCloseHandleにて後始末をします。ではプログラムの実行結果を示し ます(図2)。 http://ruffnex.oc.to/kenji/src/windows/HTTPWinInet/http2.png(図2)  ほぼWinSock版と同じですがWinInetを利用するとsocketやらconnectやらと いった接続処理を省くことができます。HPアドレスさえ渡せばページが取得 できるので有難いのですが、それゆえ応用力にかけます。まぁ便利であること に違いないわけですから状況によって使い分ければよいかと。 4. ECHOサーバ  今度はサーバを書きます。もっともシンプルなサーバである(と思う)ECHO サーバを作ってみます。ECHOサーバとは、クライアントが文字列を送信したな ら、その文字列をそっくりクライアントに返信するサーバです。そんなサーバ になんの意味があるのかと問われれば正直、意味はないと答えるしかなさそう ですが、ネットワークを学ぶためにはなかなかよい題材ですのでWindows用の ECHOサーバを作ってみます。 http://ruffnex.oc.to/kenji/src/windows/ECHO/cECHO.cpp  まずはCUI版です。プログラムの第一引数に開くべきポート番号を指定して やります。ソケット周りはクライアントとだいたい同じですが、やはりサーバ なので微妙な部分は違います。大きな違いとしてbind関数とlisten関数、そし てaccept関数があります。クライアントと違い、サーバは常に起動してクライ アントを待っていなければなりません。そこでクライアントの接続を待つ関数 がacceptです。bindとlistenもそれぞれ意味はあるのですが、それは最初はし らなくともいいでしょう。ネットワークプログラムをいくつか組むうちにわか ってきます(多分)。 ----- struct servent *serv; my.sin_family = AF_INET; if((serv = getservbyname(argv[1], "tcp")) == NULL) { int port; if((port = atoi(argv[1])) == 0) { perror("port"); exit(1); } my.sin_port = htons(port); }else{ my.sin_port = serv->s_port; } -----  最初は構造体生成の処理です。getservbynameにポートと"tcp" or "udp"を 渡してネットワークバイトオーダーのポートを取得したいのですが、戻り値が NULLでできなかったばあいはhtonsで直接ポートを決定します。 ----- struct sockaddr_in from; int len = sizeof(from); SOCKET acc; if((acc = accept(sock, (struct sockaddr *)&from, &len)) < 0) { perror("accept"); exit(1); } -----  accept関数はクライアントを待ちます。よってクライアントがくるまではこ こで処理がストップします(これはGUIのばあいに問題となります)。クラ イアントがくるとaccにクライアントと繋がっているソケットが返されるので それを利用して以後、会話を行います。 ----- while(1) { char buf[512]; if((len = recv(acc, buf, (sizeof(buf)-1), 0)) < 0) { perror("recv"); exit(1); } if(buf[0] == 'Z') { break; } buf[len] = '\0'; len = (int)strlen(buf); if((len = send(acc, buf, len, 0)) < 0) { perror("send"); exit(1); } } -----  まぁ読めば分かるでしょう。クライアントとさほど変わりはありませんね。 クライアントがデータを送信し続けるかぎりはwhileで無限に処理させます。' Z'を受け取ったら、whileを抜けてプログラム終了となります。通常はサーバ はクライアントが切断しても再度次のクライアントを待ち続けなければいけな いのでacceptに戻らなければならないのですがこのプログラムでは終了してい ます(^^; まぁwhileをいれるだけですので気に入らないなら適当に変更して ください。では実行してみます。 ----- C:\....\Debug>cECHO 5555 port=5555 ready for accept accept:127.0.0.1:1104 -----  サーバ側はこのようになります。クライアントは例えばtelnetなどを利用す ればECHOサーバの動作を確認できます。 ----- C:\>telnet localhost 5555 dddd kkggffkk .... -----  では次はこれをGUIにしてみます。 http://ruffnex.oc.to/kenji/src/windows/ECHO/gECHO.cpp ----- case WM_ASYNC_SELECT: switch(WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: AcceptConnect(sListen, &Data); break; case FD_READ: char c; recv(Data.sock, &c, 1, 0); send(Data.sock, &c, 1, 0); break; case FD_CLOSE: shutdown(Data.sock, SD_BOTH); closesocket(Data.sock); Data.acceptFlag = TRUE; break; } break; -----  ポイントはここでしょう。GUIの場合はacceptで処理をストップさせるこ とはできません。なぜなら、WindowsGUIプログラムは常にwhileでメッセージ を待ち続けなければならないからです。Windowの大きさが変更されたり、最大 化、最小化ボタンがおされたり、終了ボタンが押されたり、マウスがクリック されたりとさまざまなメッセージが送られてきます。だのにこっちの都合でプ ログラム自体をacceptでストップさせてしまったら、それはメッセージが受け 取れない、メッセージを送っても返答がこないプログラムになってしまいます。 これはOSからしてみれば「応答無し」と判断されるかもしれません。この問 題は「非同期処理 ソケット」などで検索したら結構な数がヒットします。こ の問題を解決するために「接続要求」や「受信バッファにデータが存在する」 や「切断要求」などをメッセージとして処理することにします。つまりOSか ら「クライアントが接続要求してきたよ」(FD_ACCEPT)というメッセージが きたらaccept関数を実行するのです。そしてOSが「クライアントが切断要求 してきたよ」(FD_CLOSE)というとclosesocketを、「受信バッファにデータ があるよ」(FD_READ)というとrecvを呼ぶということです。  つまりクライアントの状態などをOSに教えてもらうのです。OSからのメ ッセージに対応して我々が書いたプログラムを実行させるようにするというこ とです。これはGUI特有の問題でありGUI特有の解決法ですね。まさにG UIプログラムはOSの力を借りて実行されるということでしょうか(笑)C UI(Linux)になれてる私にとっては悩みどころでした(^^;  これ以外は、CUIとほとんど同じなので(もちろん窓を描画してる分ソー スは長いですが)特に説明はしません。HIDEをdefineするとWindowが描画され なくなるので、サーバっぽいです。 5. さいごに  ここまでを理解したならば、一応ネットワークプログラムの世界に足を踏み 入れたということになるでしょう(もちろんWindows以外のOSでネットワー クプログラムを組んでる方は別ですよ)。あとはFTPなりSMTPなり、さらには rawsocketなりで自分好みのサーバあるいはクライアントプログラムを作って 楽しんでください。お勧めとしては、匿名メール送信ツールや、BBS連続投 稿ツール、トロイの木馬、IP偽装パケット生成ツールなどが面白いかと、っ てこんなんばっかやな(笑) まぁネットワークは本当に面白いのでぜひ挑戦 してみてください。 6. 参考文献  猫でもわかるネットワークプログラミング 粂井康孝 著 End. written by kenji aiko 2004/02/23 Copyright (C) 2004 kenji aiko All Rights Reserved