
C言語でインターネットの Web 上からデータを取得する場合を考えます。 TCP/IPを使って、http://nodachisoft.com (このブログが動いているサーバ)からデータを取得してみましょう。
なお、インターネットへのアクセスをする時に使用するネットワーク通信の関数は Windows 環境と Unix / Linux / MacOS 環境では異なります。
今回は Windows 環境を中心にコードを確認していきます。
Windows 環境でネットワークへの操作を行う場合は、どんな通信をするのかの種類や ネットワークで接続する先を決めておく必要があります。
それぞれの項目詳細説明はここでは割愛しますが、今回はアプリがWeb接続するときに利用する、標準的な接続をしてみます。
設定項目 | 使う仕組み |
---|---|
インターネット層 | IP |
IPアドレス種類 | IPv4 |
トランスポート層 | TCP |
通信プロトコル | HTTP |
接続先サーバホスト | google.co.jp |
接続先サーバポート | 80 |
ではでは、プログラムからネットワーク上の Webサーバ(nodachisoft.com)に 接続して、Web画面の生データを取得するプログラムを確認していきます。
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <Winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>
#pragma comment(lib,"ws2_32")
int main() {
int const addressType = AF_INET; // IPv4 を使う
int const sockType = SOCK_STREAM; // TCP+IPv4/v6 用のソケットタイプ
int const useIpProtocol = IPPROTO_TCP; // TCP を使う
char const* destServerName = "nodachisoft.com"; // 接続先サーバのホスト名
int const destServerPort = 80; // 接続先サーバのポート番号
char const get_http[256] = // サーバに送信する文字列
"GET / HTTP/1.1" "\r\n" // GET メソッド
"HOST: nodachisoft.com" "\r\n\r\n"; // HTTPリクエスト先ホスト名
// 接続に使用する変数
WSADATA wsaData;
SOCKET sock;
struct sockaddr_in server;
struct hostent* host;
char ipaddr[64];
int errorcheck;
WSAStartup(MAKEWORD(2, 2), &wsaData);
sock = socket(addressType, sockType, useIpProtocol);
printf("ソケット作成成功。\n");
host = gethostbyname(destServerName);
unsigned char* ipv4_p = host->h_addr_list[0]; // IPv4 データ格納場所
if (host->h_addr_list[0]) {
sprintf(ipaddr, "%d.%d.%d.%d", *(ipv4_p), *(ipv4_p + 1), *(ipv4_p + 2), *(ipv4_p + 3));
}
printf("IPアドレス参照成功。IP は %s\n", ipaddr);
server.sin_addr.s_addr = inet_addr(ipaddr);
server.sin_family = AF_INET;
server.sin_port = htons(destServerPort);
// サーバ接続
connect(sock, (struct sockaddr*) & server, sizeof(server));
printf("サーバ接続成功。\n");
// サーバへデータ送信
send(sock, get_http, strlen(get_http), 0);
printf("サーバへリクエスト送信成功。\n");
// サーバからデータ受信
int recv_size;
char server_reply[20000];
recv_size = recv(sock, server_reply, 20000, 0);
server_reply[recv_size] = '\0';
// データ内容表示
printf("サーバからデータ受信。サイズは %d バイト。\n", recv_size);
printf("受信したデータを表示します。\n====\n");
printf(server_reply);
closesocket(sock); // サーバとの接続終了
WSACleanup(); // WinSock の使用終了
}
これを実行すると、プログラムは自動的に nodachisoft.com (筆者の管理するサーバ)に アクセスし、「http://nodachisoft.com/」のアドレスにアクセスします。
gccでのコンパイル
gcc + cygwin 環境でこのプログラムをコンパイルする場合、下の用に「-lws2_32」とoptionをつけてコンパイルします。
gcc winhttpclient_ne.c -lws2_32
また、gcc でコンパイルする時はプログラムの最初の 2 行の define 文は不要です。
Microsoft Visual Studio Community などの環境では、警告メッセージを抑制するために記載しています。
実行結果は下のようになります。
ソケット作成成功。
IPアドレス参照成功。IP は 52.69.219.14
サーバ接続成功。
サーバへリクエスト送信成功。
サーバからデータ受信。サイズは 354 バイト。
受信したデータを表示します。
====
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sun, 14 Mar 2021 07:17:56 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://nodachisoft.com/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
上記の実行結果で、8行目以降がインターネットを通じて 「http://nodachisoft.com/」からダウンロードしてきたデータです。 データの見方については、本題からそれるため割愛しますが、 概要のみ記載すると、「http://nodachisoft.com/」ではなく「https:/nodachisoft.jp/」のアドレスにアクセスしてね、というサーバからのメッセージデータが返ってきています。
ネットワークを通じて、インターネット上のサーバからデータを取得してくる手順は下のようになっています。
プログラムから Windows 環境でネットワーク通信をする際 まず、最初にWSAStartup関数を呼び出して Windows Socket API 関連を使用できるように初期化が必要です。
WSAStartup(MAKEWORD(2,2), &wsaData);
sock = socket(addressType, sockType, useIpProtocol);
printf("ソケット作成成功。\n");
この後に登場する、通信ソケット作成、通信のやり取り(send, recv 関数)も すべて、Windows Socket API の一種で、WSAStartup 関数で初期化していないとエラーとなります。
WSAStartup 関数は下のように呼出します。
WSAStartup( WORD 通信に使うWinSockバージョン , &WSADATA 使用するソケット情報入力先 );
WSAStartup の実行が成功なら 0 、失敗ならエラー理由に応じたエラーコードを返します。
今回、24行目では、 通信に使うWinSockバージョンとして、MAKEWORD(2,2)
と入力しています。
MAKEWORD は windows.h ヘッダをインクルードすると使えるマクロ関数で、指定した WORD 型の値を作ることができます。
WORD型は Microsoft OS の Windows API を呼び出すときに、関数の引数としてよく使われる型で、
正の整数が代入できます。実際には、unsigned short
と一緒です。
WORD型の中身は short 型ですので、 2 バイトで構成されています。 MAKEWORD マクロ関数は下のように使うと、2バイトの下位バイト、上位バイトを直接指定して WORD 型を作ることができます。
MAKEWORD( 下位バイト, 上位バイト )
今回は MAKEWORD(2,2)
と指定しており、WSAStartup はこれをそのままバージョン "2.2" として解釈してくれます。
この WSAStartup に指定するバージョンについては 2021年3月現在、「2.2」と指定しておけば最新の安定したバージョンとして問題なく使用できます。(Windows XP 以降であれば、問題なく利用可能です)
Windows でネットワーク通信をする場合、このWSAStartup 関数で最初に初期化を行い、 Windows のライブラリ(Ws2_32 と呼ばれるライブラリ)を利用可能となります。
WSAという略称 Windows API の関数名や型で WSA と大文字で記載がありますが、 これは Windows Socket API の頭文字の略称です。
ネットワーク通信を開始するにあたっての初期化が完了したので、次のステップです。 25行目で、具体的に通信をするための方式を指定して通信の出入口となるソケットと呼ばれる オブジェクトを取得します。
WSAStartup(MAKEWORD(2,2), &wsaData);
sock = socket(addressType, sockType, useIpProtocol);
printf("ソケット作成成功。\n");
この後、実際にサーバと接続したりデータを送受信するときはこのソケットを通じて接続先とやり取りします。
socket 関数は下のように使います。
SOCKET socket(int af,int type,int protocol);
第1引数の AF ではアドレスファミリ(Address Family)の種類を指定します。アドレスファミリとは、ネット上の場所を表すアドレスの種類のことです。例えば最も一般的にインターネットの通信で利用される IPv4 であれば「AF_INET」、IPv6 であれば「AF_INET6」という定数を指定してあげます。
第2引数の type では、ソケットでデータを送受信する種類を指定します。大きなデータ列を送信するのに優れている際は通常、「SOCK_STREAM」です。FPSゲームなどのように速度重視で小さなパケット(UDPパケットと呼ばれます)を送信する場合は「SOCK_DGRAM」を選択し、送信するデータを小さく区切って送信することもあります。
第3引数はネットワーク通信の規約(プロトコル)を指定します。IPv4、IPv6 であれば、「IPPROTO_TCP」を指定します。
ネットワークのどこに接続するかを指定して、実際に接続するまでの 流れを見ていきます。
host = gethostbyname(destServerName);
unsigned char* ipv4_p = host->h_addr_list[0]; // IPv4 データ格納場所
27 行目で gethostbyname 関数を使って、サーバのドメイン名に対応するIP アドレス(ネットワーク上の場所を表す住所のようなもの) の情報を確認します。
gethostbyname 関数は hostent 型に、接続先のサーバの情報(IPアドレスやサーバ名など)を取得して 接続に必要な情報を取得できます。
続いて、28 行目で接続先サーバのIPアドレスを 1 つ取得します。 IPアドレスには IPv4 と IPv6 と呼ばれる種類があり、「aaa.bbb.ccc.ddd」のような形式で表現されます。
例えば nodachisoft.com (筆者の管理するサーバ)であれば「52.69.219.14」ですし、 yahoo.co.jp であれば「182.22.59.229」です。
IPv4 は 0~255 の数字を 4 つ組み合わせて表現できます。 C言語では unsigned char 型 4 つぶんで 1 つの IPv4 のデータを持つことが出来ます。
変数 host の中の h_addr_list 配列には、接続したいサーバのIP アドレスの一覧が格納されており、 その中から、1 つめの IPアドレスのデータを変数「ipv4_p」に格納しています。
続いて 29 行目からは IPアドレスを、ipaddr に文字列の「aaa.bbb.ccc.ddd」形式に変換して格納します。
if (host->h_addr_list[0]) {
sprintf(ipaddr, "%d.%d.%d.%d", *(ipv4_p), *(ipv4_p + 1), *(ipv4_p + 2), *(ipv4_p + 3));
}
printf("IPアドレス参照成功。IP は %s\n", ipaddr);
次に、接続するサーバの詳細な設定を行います。
server.sin_addr.s_addr = inet_addr(ipaddr);
server.sin_family = addressType;
server.sin_port = htons(destServerPort);
接続する先のサーバに、どのように接続するかのデータを 構造体 sockaddr_in の変数「server」に詰め込んでいきます。
htons (接続先ポート番号)
で実行した結果を代入するようにします。45 行目で、今まで用意した接続のための情報を使って、実際にインターネット上へのサーバへの接続を行います。
// サーバ接続
connect(sock, (struct sockaddr*) & server, sizeof(server));
printf("サーバ接続成功。\n");
connect 関数を使って、実際にインターネット上のサーバへ接続を行うための関数です。
使い方は下の通りです。
int connect(
SOCKET s // 接続に使うソケットデータ
,const struct sockaddr *server // 接続したいサーバ情報
,int length ); // 接続したいサーバ情報のデータのサイズ
接続がうまくいかずにエラーとなったときは、戻り値はマイナスの数値が返ります。
無事にサーバとの接続が成功したら、いよいよデータをサーバとやり取りできます。
まずはサーバへのデータ送信部分です。
// サーバへデータ送信
send(sock, get_http, strlen(get_http), 0);
printf("サーバへリクエスト送信成功。\n");
サーバへのデータ送信に send 関数を使用します。 send 関数の使用方法は下の通りです。
int send(
SOCKET s, // 接続に使うソケットデータ
const char *data, // 送信するデータへのポインタ
int len, // 送信するデータのサイズ
int flags); // 特殊なデータ送信方法を ON/OFF するフラグ
今回は予め文字列で用意しておいた、変数 get_http のデータを全て送信します。
続いてサーバからの応答データを受け付けます。
// サーバからデータ受信
int recv_size;
char server_reply[20000];
recv_size = recv(sock, server_reply, 20000, 0);
server_reply[recv_size] = '\0';
サーバからデータを受信(ダウンロード)する先の領域を server_reply として 20000 バイトほど 用意しました。
もっと大きなデータをダウンロードする時は、malloc 関数などを使って動的にダウンロード先の領域を 用意する方が良いですが、今回はシンプルに char 型の配列に代入します。
サーバからデータを受信するために recv 関数を使います。
recv 関数の使用方法は下の通りです。
int recv(
SOCKET s, // 接続に使うソケットデータ
const char *data, // 受信するデータへのポインタ
int len, // 受信するデータのサイズ
int flags); // 特殊なデータ受信方法を ON/OFF するフラグ
server_reply 変数の中に、20,000バイトまでのデータを受信して格納します。 受信するデータが 20,000 バイト以内の場合は、受信できるデータサイズのみが格納されます。
実際に受信したデータサイズは、recv 関数の戻り値で返ってきます。 今回は変数 recv_size に、実際に受信したデータのサイズが代入されます。
データ受信した際、データの終わりに終端文字('\0')は自動的には入らないため、 最後に '\0' を手動で設定して、データの終わりを明示的に設定してあげます。
サーバから受信したデータのサイズと内容は下で出力しています。
// データ内容表示
printf("サーバからデータ受信。サイズは %d バイト。\n", recv_size);
printf("受信したデータを表示します。\n====\n");
printf(server_reply);
サーバとのやりとりを終了するため、 サーバとの接続に使用していたソケットを終了します。
closesocket(sock); // サーバとの接続終了
ネットワークの利用が終了したので、 必要なネットワーク関連(WinSock)を開放します。
WSACleanup(); // WinSock の使用終了
先ほどのサーバに接続してデータを送受信する処理に、 エラーが発生していないかのエラーハンドリング処理を加えてみましょう。
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <Winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32")
int main(){
int const addressType = AF_INET; // IPv4 を使う
int const sockType = SOCK_STREAM; // TCP+IPv4/v6 用のソケットタイプ
int const useIpProtocol = IPPROTO_TCP; // TCP を使う
char const* destServerName = "nodachisoft.com"; // 接続先サーバのホスト名
int const destServerPort = 80; // 接続先サーバのポート番号
char const get_http[256] = // サーバに送信する文字列
"GET / HTTP/1.1" "\r\n" // GET メソッド
"HOST: nodachisoft.com" "\r\n\r\n"; // HTTPリクエスト先ホスト名
// 接続に使用する変数
WSADATA wsaData;
SOCKET sock;
struct sockaddr_in server;
struct hostent *host;
char ipaddr[64];
int errorcheck;
errorcheck = WSAStartup(MAKEWORD(2,2), &wsaData);
if ( errorcheck != 0 ) {
printf("WinSock の初期化失敗。エラーコードは %d\n", WSAGetLastError());
return 1;
}
printf("WinSock の初期化成功。\n");
if (( sock = socket(addressType, sockType, useIpProtocol) ) == INVALID_SOCKET ) {
printf("ソケット作成失敗。エラーコードは %d\n" , WSAGetLastError());
}
printf("ソケット作成成功。\n");
host = gethostbyname( destServerName );
if ( host == NULL ) {
printf("IPアドレス参照失敗。エラーコードは %d\n", WSAGetLastError());
return 1;
}
unsigned char *ipv4_p = host->h_addr_list[0]; // IPv4 データ格納場所
if ( host->h_addr_list[0] ) {
sprintf(ipaddr,"%d.%d.%d.%d",*(ipv4_p),*(ipv4_p + 1),*(ipv4_p + 2),*(ipv4_p + 3));
}
printf("IPアドレス参照成功。IP は %s\n", ipaddr );
server.sin_addr.s_addr = inet_addr(ipaddr);
server.sin_family = AF_INET;
server.sin_port = htons( destServerPort );
// サーバ接続
if (connect(sock , (struct sockaddr *)&server , sizeof(server)) < 0) {
printf("サーバ接続失敗。エラーコードは %d\n" , WSAGetLastError());
return 1;
}
printf("サーバ接続成功。\n");
// サーバへデータ送信
if( send(sock , get_http , strlen(get_http) , 0) < 0) {
printf("サーバへデータ送信失敗。エラーコードは %d\n" , WSAGetLastError());
return 1;
}
printf("サーバへデータ送信成功。\n");
// サーバからデータ受信
int recv_size;
char server_reply[20000];
if((recv_size = recv(sock , server_reply , 20000 , 0)) == SOCKET_ERROR) {
printf("サーバからのデータ受信失敗。エラーコードは %d\n" , WSAGetLastError());
}
server_reply[recv_size] = '\0';
// データ内容表示
printf("サーバからデータ受信。サイズは %d バイト。\n", recv_size);
printf("受信したデータを表示します。\n====\n");
printf(server_reply);
closesocket(sock); // サーバとの接続終了
WSACleanup(); // WinSock の使用終了
}
WSAStartup 関数、socket 関数、gethostbyname関数、connect 関数、send 関数、 recv 関数に対して、 それぞれうまく動作しなかった時のエラーの条件分岐を追加しています。
WSAGetLastError 関数は直前で発生した、Windows のネット通信関係(WinSock関係)のエラーデータを 取得します。
具体的にどのような種類のエラーが発生したのかは、Microsoft の公式ドキュメントで 仕様 が公開されています。
代表的な2つを参考として下に記載しておきます。
エラーコード | 定数 | エラーの発生する事例 |
---|---|---|
11001 | WSAHOST_NOT_FOUND | 実行している環境がネットワーク(インターネットなど)に繋がっていない場合。ドメイン名が間違っている場合。 |
10060 | WSAETIMEDOUT | サーバに接続しようとしたけれど、うまく接続出来なかった場合。指定したポート番号をサーバが公開していない場合など。 |
定数は Winerror.h の中で定義されており、Windows.h をインクルードしていれば自動的に Winerror.h もインクルードされて使えるようになります。
使用するライブラリは -l で決めます。ソースコードの指定は先にし、使用するライブラリの -l オプションの指定は後ろに付けましょう。
gcc winhttpclient.c -L"C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\x86_64-w64-mingw32\lib" -lws2_32 -lwsock32
> gcc -lws2_32 -lwsock32 winhttpclient.c
c:/programdata/chocolatey/lib/mingw/tools/install/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/10.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\amaji\Temp\ccPy65Tw.o:winhttpclient.c:(.text+0x172): undefined reference to `__imp_WSAStartup'
c:/programdata/chocolatey/lib/mingw/tools/install/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/10.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\amaji\AppData\Local\Temp\ccPy65Tw.o:winhttpclient.c:(.text+0x18b): undefined reference to `__imp_socket'
c:/programdata/chocolatey/lib/mingw/tools/install/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/10.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\amaji\AppData\Local\Temp\ccPy65Tw.o:winhttpclient.c:(.text+0x1a2): undefined reference to `__imp_gethostbyname'
collect2.exe: error: ld returned 1 exit status
なお、Cygwin ( MinGW ) の gcc でライブラリを指定する必要があります。 pragma で lib 指定は MSVC のみに有効です。
gcc ではコードの中の pramga は無視されますので、自分で使用したいライブラリをコンパイル時で引数で指定して事項してあげる必要があります。
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: