URLパーサをつくってみた

URLを「スキーム、ホスト、ポート、パス、クエリー」に分解するツールをつくってみました。

ソース

/**
 * url_parser.c
 **/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define URL_MAX_LEN 2083 /* IEの制限と同じ数値に設定 */
#define SCHEME_MAX_LEN 32
#define SCHEME_DELIMITER "://"
#define DOMAIN_MAX_LEN URL_MAX_LEN

int main(int argc, char *argv[])
{

    char _url[URL_MAX_LEN+1], _scheme[SCHEME_MAX_LEN+1], _domain[DOMAIN_MAX_LEN+1], _path[URL_MAX_LEN+1];
    char *url = _url, *scheme = _scheme, *domain = _domain, *path = _path;
    char *host, *port, *query;
    char *scheme_pointer;
    char *str1, *token;
    char *saveptr1, *saveptr2;
    size_t scheme_length;
    
    if (argc != 2) {
	fprintf(stderr, "Usage: %s url\n", argv[0]);
	exit(EXIT_FAILURE);
    }
    
    if (strlen(argv[1]) > URL_MAX_LEN) {
	fprintf(stderr, "url too long\n");
	exit(EXIT_FAILURE);
    }
    
    /* 変数初期化 */
    str1 = argv[1];
    strcpy(url, argv[1]);
    strcpy(scheme, "");
    strcpy(domain, "");
    strcpy(path, "/");
    host = "";
    query = "";
    port = "";
    
    /* schemeが指定されているか確認 */
    scheme_pointer = strstr(url, SCHEME_DELIMITER);
    scheme_length = (scheme_pointer == NULL)? 0 : scheme_pointer-url;
        
    /* schemeの設定 */
    if(scheme_length > 0) { 
	
	if (scheme_length > SCHEME_MAX_LEN) {
	    fprintf(stderr, "scheme too long\n");
	    exit(EXIT_FAILURE);
	}
	
	strncpy(scheme, url, scheme_length);
	scheme[scheme_length] = '\0'; /* NULL文字を付け加える */
	
    }
    
    str1 = (scheme_length == 0)? str1 : &str1[scheme_length+strlen(SCHEME_DELIMITER)];
    
    /* domainの設定 */
    token = strtok_r(str1, "/", &saveptr1);
    if (token == NULL) {
	fprintf(stderr, "domain is not listed \n");
	exit(EXIT_FAILURE);
    }
    strcpy(domain, token);
    
    /* host */
    token = strtok_r(token, ":", &saveptr2);
    host = token;
    
    /* port */
    token = strtok_r(NULL, ":", &saveptr2);
    if (token != NULL) {
	port = token;
    }
    
    /* path */
    token = strtok_r(NULL, "?", &saveptr1);
    if (token != NULL) {
        strcat(path, token);
    }
    
    /* query */
    token = strtok_r(NULL, "", &saveptr1);
    if (token != NULL) {
        query = token;
    }
    
    printf("url    : %s\n", url);
    printf("scheme : %s\n", scheme);
    printf("host   : %s\n", host);
    printf("port   : %s\n", port);
    printf("path   : %s\n", path);
    printf("query  : %s\n", query);
    
    exit(EXIT_SUCCESS);
}

利用方法

$ ./url_parser "http://google.com"
url    : http://google.com
scheme : http
host   : google.com
port   : 
path   : /
query  :
$ ./url parser  "http://linuxjm.sourceforge.jp:80/cgi-bin/man.cgi?Pagename=test"
url    : http://linuxjm.sourceforge.jp:80/cgi-bin/man.cgi?Pagename=test
scheme : http
host   : linuxjm.sourceforge.jp
port   : 80
path   : /cgi-bin/man.cgi
query  : Pagename=test

HTTP Client の動作フロー その6 : getaddrinfo()

名前解決には、gethostbyname()ではなく、getaddrinfo()を使えとのことだったので、getaddrinfo()について調べてみた。

getaddrinfo(3)

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

getaddrinfo() は、(インターネットのホストとサービスを識別する) node と service を渡すと、一つ以上の addrinfo 構造体を返す。それぞれの addrinfo 構造体には、 bind(2) や connect(2) を呼び出す際に指定できるインターネットアドレスが格納されている。 getaddrinfo() 関数は、 getservbyname(3) と getservbyport(3) の機能をまとめて一つのインターフェースにしたものであるが、 これらの関数と違い、 getaddrinfo() はリエントラントであり、 getaddrinfo() を使うことでプログラムは IPv4IPv6 の違いに関する依存関係を なくすことができる。

getaddrinfo() は成功すると 0 を返し、失敗すると以下の非 0 のエラーコードのいずれかを返す。

getaddrinfo() 関数は、 addrinfo 構造体のメモリ確保を行い、 addrinfo 構造体のリンクリストを初期化し、 res にリストの先頭へのポインタを入れて返す。 このとき、各構造体のネットワークアドレスは node と service に一致し、 hints で課されたすべての制限を満たすものとなる。 リンクリストの要素は ai_next フィールドにより連結される。

getaddrinfo() が用いる addrinfo 構造体は以下のフィールドを含む。

struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};

node

node には、数値形式のネットワークアドレス (IPv4 の場合は inet_aton(3) でサポートされているドット区切りの数字による表記、 IPv6 の場合は inet_pton(3) でサポートされている 16 進数の文字列形式) もしくは ネットワークホスト名を指定する。 ネットワークホスト名を指定した場合には、そのネットワークアドレスが検索され、 名前解決が行なわれる。

service

service により、返される各アドレス構造体のポート番号が決まる。 この引き数がサービス名 (services(5) 参照) の場合、対応するポート番号に翻訳される。 この引き数には 10 進数も指定することができ、 この場合にはバイナリへの変換だけが行われる。 service が NULL の場合、返されるソケットアドレスのポート番号は 初期化されないままとなる。

実例

ホスト名を引数に渡すとアドレス解決して表示
/* getaddrinfo_test.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{

        struct addrinfo hints;
        struct addrinfo *result, *rp;
	struct sockaddr_in *addr_in;
	struct sockaddr_in6 *addr_in6;
        int s;
        const char *errormess;
	char address[INET6_ADDRSTRLEN];

        if (argc < 2) {
                fprintf(stderr, "Usage: %s host\n", argv[0]);
		return 1;
        }

        //hintsを初期化
        memset(&hints, 0, sizeof(struct addrinfo));
        hints.ai_family = AF_UNSPEC; //IPv4とIPv6どちらでもOK
        hints.ai_socktype = SOCK_STREAM; //TCP
        hints.ai_flags = 0;
        hints.ai_protocol = 0; //Any protocol

        //名前解決
        s = getaddrinfo(argv[1], "http", &hints, &result);

        //エラー処理
        if (s != 0) {
                errormess = gai_strerror(s);
                fprintf(stderr, "getaddrinfo: %s\n", errormess);
                return 1;
        }

        //取得した情報を表示
        for (rp = result; rp != NULL; rp = rp->ai_next) {

                printf("{\n"); 
                printf("\tai_family:\t%d\n", rp->ai_family);
                printf("\tai_socktype:\t%d\n", rp->ai_socktype);
                printf("\tai_protocol:\t%d\n", rp->ai_protocol);
                printf("\tai_addrlen:\t%zd\n", rp->ai_addrlen);
                printf("\tai_canonname:\t%s\n", rp->ai_canonname);
		

		if (rp->ai_family == AF_INET) { //IPv4

			addr_in = (struct sockaddr_in *) rp->ai_addr;
			if (inet_ntop(rp->ai_family, &addr_in->sin_addr.s_addr, address, rp->ai_addrlen) == NULL) {
				fprintf(stderr, "inet_ntop: error\n");
				return 1;
			}
			printf("\tai_addr: {\n");
			printf("\t\tsin_port:\t%d\n", ntohs(addr_in->sin_port));
			printf("\t\tsin_addr: {\n");
			printf("\t\t\ts_addr:\t%s\n", address);
			printf("\t\t}\n");
			printf("\t}\n");

		} else if (rp->ai_family == AF_INET6) { //IPv6

			addr_in6 = (struct sockaddr_in6 *) rp->ai_addr;
			if (inet_ntop(rp->ai_family, &addr_in6->sin6_addr.s6_addr, address, rp->ai_addrlen) == NULL) {
				fprintf(stderr, "inet_ntop: error\n");
				return 1;
			}
			printf("\tai_addr: {\n");
			printf("\t\tsin6_port:\t%d\n", ntohs(addr_in6->sin6_port));
			printf("\t\tsin6_addr: {\n");
			printf("\t\t\ts6_addr:\t%s\n", address);
			printf("\t\t}\n");
			printf("\t}\n");
		}

                printf("}\n");

        }

        return 0;

}

実行例

$ ./getaddrinfo_test yahoo.co.jp
{
	ai_family:	2
	ai_socktype:	1
	ai_protocol:	6
	ai_addrlen:	16
	ai_canonname:	(null)
	ai_addr: {
		sin_port:	80
		sin_addr: {
			s_addr:	124.83.187.140
		}
	}
}
{
	ai_family:	2
	ai_socktype:	1
	ai_protocol:	6
	ai_addrlen:	16
	ai_canonname:	(null)
	ai_addr: {
		sin_port:	80
		sin_addr: {
			s_addr:	203.216.243.240
		}
	}
}
$
$ ./getaddrinfo_test localhost
{
	ai_family:	10
	ai_socktype:	1
	ai_protocol:	6
	ai_addrlen:	28
	ai_canonname:	(null)
	ai_addr: {
		sin6_port:	80
		sin6_addr: {
			s6_addr:	::1
		}
	}
}
{
	ai_family:	2
	ai_socktype:	1
	ai_protocol:	6
	ai_addrlen:	16
	ai_canonname:	(null)
	ai_addr: {
		sin_port:	80
		sin_addr: {
			s_addr:	127.0.0.1
		}
	}
}
ホスト名を解決してソケットを開きコネクトする例
/*  getaddrinfo_socket_connect.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char *argv[])
{

        struct addrinfo hints;
        struct addrinfo *result, *rp;
        int s, sfd;
        const char *errormess;

        if (argc < 2) {
                fprintf(stderr, "Usage: %s host\n", argv[0]);
		return 1;
        }

        //hintsを初期化
        memset(&hints, 0, sizeof(struct addrinfo));
        hints.ai_family = AF_UNSPEC; //IPv4とIPv6どちらでもOK
        hints.ai_socktype = SOCK_STREAM; //TCP
        hints.ai_flags = 0;
        hints.ai_protocol = 0; //Any protocol

        //名前解決
        s = getaddrinfo(argv[1], "http", &hints, &result);

        //エラー処理
        if (s != 0) {
                errormess = gai_strerror(s);
                fprintf(stderr, "getaddrinfo: %s\n", errormess);
                return 1;
        }

        //ホストに接続
        for (rp = result; rp != NULL; rp = rp->ai_next) {

		//ソケットを生成
		sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);

		//ソケットエラー処理
		if (sfd == -1) {
			//ソケットが生成できなかった場合,次のアドレスでリトライ
			continue;
		}

		//ホストに接続
		if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1) {
			//接続できた場合はループを抜ける
			break;	
		}

		//接続できかなった場合はソケットを閉じて次のアドレスでリトライ
		close(sfd);

        }

	//すべてのアドレスで接続に成功しなかった場合は終了
	if (rp == NULL) {
		fprintf(stderr, "Could not connect\n");
		return 1;
	}
	
	//resultのメモリ領域を開放
	freeaddrinfo(result);

	//接続後30秒待機して接続を切る
	sleep(30);
	close(sfd);	

        return 0;

}

実行例

$ ./ getaddrinfo_socket_connect localhost & netstat -anptl | grep  getaddrinfo_socket_connect
[2] 7886
(一部のプロセスが識別されますが, 所有していないプロセスの情報は
表示されません。それら全てを見るにはルートになる必要があります.)
tcp        0      0 127.0.0.1:44619         127.0.0.1:80            ESTABLISHED 7886/getaddrinfo_socket_connect

ESTABLISHED!!

HTTP Client の動作フロー その7 : write() でリクエストを送る

connect()で接続ができたら、write()関数でリクエストを送ります。

char buf[1024];
char *host; //ホスト名

host = "localhost";

/* 中略 */

//送信内容を作成
snprintf(buf, sizeof(buf), "GET / HTTP/1.0\r\nHost: %s\r\r\n", host);
	
//リクエストを送信
write(sfd, buf, strlen(buf)); //sfdはsocket()で取得したファイルディスクリプション

ここではsnprintf()関数を使って下記のようなHTTPリクエストを作成して、write()関数でサーバに送信しています。

GET / HTTP/1.0
Host: localhost

追記 20110529

HTTP/1.1だと「Transfer-Encoding: chunked」に対応しないといけなくなるため、プロトコルをHTTP/1.0に変更。

HTTP Client の動作フロー その8 : read() でレスポンスを読み込む

write()でHTTPリクエストをサーバに送信したあとは、read()関数でサーバからのHTTPレスポンスを読み込みます。

//バッファを空に
memset(buf, 0, sizeof(buf));

//レスポンスを受信して出力
do {
        
        //バッファ分読み込む
        n = read(sfd, buf, sizeof(buf));		

        
        //標準出力に表示
        if(write(STDOUT_FILENO, buf, n) < 0) {
                perror("write STDOUT");
                return 1;
        }
        
} while(n > 0);

//エラー処理
if(n == -1) {
        perror("");
        return 1;
}

//ソケットを閉じる
close(sfd);

ここでは、read()関数で読み込んだHTTPレスポンスをwrite()関数でそのまま標準出力に出力しています。

追記 20090529

読み込んだレスポンスを表示する処理でprintf()を使用していましたが、バッファーサイズより大きなページを読み込んで表示する時に問題が発生したため、write()を使用するように変更。

printf()を使う際の問題点
  • バッファーサイズより大きなファイルを読み込んだ場合、末尾が'\0'になっていない文字列を出力することになる
  • bufを空にしていなかった為、前に読み込んだデータが残ってしまっていた。
  • '%'という文字列が入っていると、printf()の特性上置換されてしまう。(しかも、置換対象を指定していない為なにが起こるかはメモリ次第...)

HTTP Client の動作フロー その5 : connect() でサーバに接続する [2]

connect()の実例

127.0.0.1(localhost)の80番ポートに接続する。

//connect_test.c

#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{

	int sockfd;
	struct sockaddr_in sockaddr;
	char *addr_str = "127.0.0.1"; 

	//ソケットの生成
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1) {
		printf("socket err\n");
		return 1;
	}

	//addrの初期化
	int result = inet_pton(AF_INET, addr_str, &sockaddr.sin_addr);
	if (result != 1) {
		printf("inet_pton err");
		return 1;
	}


	//sockaddrの初期化
	sockaddr.sin_family = AF_INET;
	sockaddr.sin_port = htons(80); //80番ポート

	//connect
	result = connect(sockfd, (struct sockaddr*) &sockaddr, sizeof(sockaddr));
	if (result == -1) {
		printf("connect err");
		return 1;
	}

	//30秒後に切断
	sleep(30);	
	close(sockfd);

	return 0;
}

実行

$ ./connect_test & netstat -anptl |grep 127.0.0.1:80 
[1] 2222
(一部のプロセスが識別されますが, 所有していないプロセスの情報は
表示されません。それら全てを見るにはルートになる必要があります.)
tcp        0      0 127.0.0.1:80            127.0.0.1:43796         SYN_RECV    -               
tcp        0      0 127.0.0.1:43796         127.0.0.1:80            ESTABLISHED 2222/connect_test      

ESTABLISHED!!

HTTP Client の動作フロー その4 : connect() でサーバに接続する

connect()の書式 | connect(2)

#include <sys/types.h> /* 移植性が必要な場合 */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

connect() システムコールは、ファイルディスクリプタ sockfd が参照しているソケットを addr で指定されたアドレスに接続する。 addrlen 引き数は addr の大きさを示す。 addr のアドレスのフォーマットはソケット sockfd のアドレス空間により異なる。 さらなる詳細は socket(2) を参照のこと。

接続または対応づけに成功するとゼロを返す。 失敗すると -1 を返し、 errno に適切な値を設定する。

TCP/IPの場合の addr フォーマット | ip(7)

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
  • sin_familiy には常に AF_INET をセットする。
  • sin_port にはポート番号をネットワークバイトオーダーで指定する。
  • sin_addr は IP ホストアドレスである
    • struct in_addr の s_addr メンバには、ホストのインターフェースアドレスを ネットワークバイトオーダーで指定する
    • in_addrは下記のいずれかで設定する
      • INADDR_* の一つ (例えば INADDR_ANY)
      • ライブラリ関数 inet_aton(3), inet_addr(3), inet_makeaddr(3) を用いる
      • 名前解決機構 (name resolver) を直接用いる(getaddrinfo(3)等)

ネットワークバイトオーダーって??

i386 ではホストバイトオーダは Least Significant Byte (LSB) first (リトルエンディアン) だが、 インターネットで使われるネットワークバイトオーダは Most Significant Byte (MSB) first (ビッグエンディアン) である

http://archive.linux.or.jp/JM/html/LDP_man-pages/man3/inet.3.html

ポート番号をネットワークバイトオーダーに変換する方法

htons()をつかって変換。

htons() 関数は unsigned short integer hostshort を ホストバイトオーダーからネットワークバイトオーダーに変換する。

ポート番号の範囲はunsigned short integer(16ビット符号無し整数)と定められているので、htons()をつかって変換する。

書式
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);

IPアドレスをネットワークバイトオーダーに変換する方法

inet_pton()関数を使う。(IPv4/IPv6対応)

書式
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);

この関数は文字列 src を、アドレスファミリー af のネットワークアドレス構造体に変換し、 dst にコピーする。 af 引き数は AF_INET か AF_INET6 のどちらかでなければならない。

ソケットってそもそもなんだ?

ソケット(英: Socket)とは、BSDUNIXを起源とするAPIであり、C言語によるアプリケーション開発でのプロセス間通信、特にコンピュータネットワークに関するライブラリを構成する。BSDソケット、バークレーソケットなどとも呼ばれる。

1983年にリリースされたUNIXオペレーティングシステム(OS) 4.2BSD で初めて API として実装された。ネットワークの抽象化インタフェースとしてのデファクトスタンダードとなっている。本来のソケットAPIC言語を対象とするが、他のプログラミング言語でも類似のインタフェースを採用しているものが多い。

http://ja.wikipedia.org/wiki/%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88_(BSD)

BSDソケットは、ホスト間の通信や1つのコンピュータ上のプロセス間の通信を可能とする。通信媒体としては様々な入出力機器やデバイスドライバを利用可能だが、その部分はオペレーティングシステムの実装に依存する。このインタフェース実装はTCP/IPを利用する場合にはほぼ必ず必要とされ、インターネットを支える基盤技術の一つとなっている。当初、カリフォルニア大学バークレー校でUNIX向けに開発された。最近の全てのオペレーティングシステムには間違いなくBSDソケットが何らかの形で実装されており、インターネットへの接続の標準インタフェースとなっている。

http://ja.wikipedia.org/wiki/%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88_(BSD)

ということみたいです。


TCP/IPでのクライアント接続は下記のように行う。

  1. socket()でソケットを作成
  2. connect() でサーバに接続
  3. send()とrecv()、またはwrite()とread()で通信を行う
  4. close()で接続を切断

socket()の扱いかたはすでに勉強したので、次はconnect()について勉強したい。