[C++] 160 HTTPプロトコル 受信データをファイルに保存

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

『C言語によるTCP/IPネットワークプログラミング』サンプルコード読解の続きです。ここら辺になってくると土台にある知見が膨大なので端折りながら理解に努めます。

受信データのヘッダ部は標準出力し、データ部は同名のファイルとして保存します。

それぞれのデータサイズ算出のため下図にポインタの関係をまとめました。

int SOCrecvDataToFile(int soc, const char *filename)
{
	int	width;
	struct timeval	timeout; // 時刻
	fd_set	readOK;  // FD(ファイルディスクリプタ)
	// fd_set	mask;
	char	tmpbuf[8193]; // 8192 = 2^13
	char	*ptr;
	int	size;
	int end;
	int head;
	FILE	*fp;

	if((fp = fopen(filename, "w")) == NULL){
		perror("fopen");
		return(-1);
	}

	FD_ZERO(&readOK); // FD初期化
	FD_SET(soc,&readOK); // FDセット
	width = soc + 1;

	head =0 ;
	end = 0;
	while(1){
		timeout.tv_sec = 1;
		timeout.tv_usec = 0;
		// readOK = mask;
		// FDを監視
		switch(select(width, &readOK, NULL, NULL ,&timeout)){
			case	-1:
				if(errno!=EINTR){
					perror("select");
					end=1;
				}
				break;
			case	0:
				break;
			default:
				if(FD_ISSET(soc, &readOK)){ // FDを判別
					size = recv(soc, tmpbuf, 8192, 0); // データ受信
					if(size <= 0){
						end = 1;
					}
					else{
						if(head == 0){
							ptr = memstr(tmpbuf, size, "\r\n\r\n", 4); // バイナリ検索
							if(ptr != NULL){
								// ヘッダ部
								fwrite(tmpbuf, ptr-tmpbuf + 4, 1, stdout);
								head=1;
								// データ部 
								fwrite(ptr + 4,size-(ptr-tmpbuf)-4,1,fp);
							}
							else{
								// ヘッダ部
								fwrite(tmpbuf, size, 1, stdout);
							}
						}
						else{
							// データ部 
							fwrite(tmpbuf, size, 1, fp);
						}
					}
				}
				break;
		}
		if(end==1){
			break;
		}
	}

	fclose(fp);

	return(0);
}

[C++] 159 HTTPプロトコル ソケット接続

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

『C言語によるTCP/IPネットワークプログラミング』のサンプルコード読解の続きです。前回のリクエスト送信と順序が逆になってしまいました。

ドメイン名からIPアドレスを取得しソケット接続するConnectHost関数について調べました。コード的に特にいじるところはありませんでした。

// ソケット接続 
int ConnectHost(const char *host,const char *port,int portno)
{
	struct hostent	*servhost;    // ホスト名
	struct servent	*service;     // サービス名
	struct sockaddr_in	sockaddr; // IPアドレス・ポート番号保管
	int	soc;
	int p;

	// ホスト名取得
	if((servhost = gethostbyname(host)) == NULL){ // ホスト名がない場合
		u_long addr;
		addr = inet_addr(host);

		servhost = gethostbyaddr((char *) &addr, sizeof(addr), AF_INET);
		if (servhost == NULL) {
			perror("Error:gethostbyaddr");
			return(-1);
		}
	}

	// サービス名取得
	if((service = getservbyname(port,"tcp")) == NULL){
	}

	// ソケット作成
	if((soc = socket(AF_INET,SOCK_STREAM,0)) < 0){
		perror("socket");
		return(-1);
	}

	// sockaddr作成
	memset((char *) &sockaddr, 0, sizeof(sockaddr));
	sockaddr.sin_family = AF_INET; // アドレスファミリー

	cout << "sockaddrのサイズ(unsigned long) " << sizeof(sockaddr) << endl;

	if(service == NULL){
		if((p = atoi(port)) == 0){
			p = portno;
		}
		sockaddr.sin_port = p;
	} else {
		sockaddr.sin_port = service -> s_port; // ポート番号取得
	}

	// IPアドレスをコピー
	memcpy((char *)&sockaddr.sin_addr,servhost->h_addr,servhost->h_length);

	// servhostアドレスリスト・メモリ番地(このコードでは不要)
	cout << "servhost->h_addr_list " << servhost->h_addr_list << endl;

	// sockaddr・メモリ番地
	cout << "&sockaddr " << &sockaddr << endl;

	// IPアドレス・メモリ番地
	cout << "&sockaddr.sin_addr " << &sockaddr.sin_addr << endl;

	// ソケット接続(ソケット記述子・IPアドレス・ポート番号が必要)
	if(connect(soc,(struct sockaddr *)&sockaddr,sizeof(sockaddr))==-1){
		perror("connect");
		SocketClose(soc);
		return(-1);
	}
	return(soc);
}
host = localhost,path = /index.html
sockaddrのサイズ(unsigned long) 16
servhost->h_addr_list 0x11f8040da
&sockaddr 0x16b681fb8
&sockaddr.sin_addr 0x16b681fbc
request GET /index.html HTTP/1.0

HTTP/1.1 200 OK
Date: Wed, 24 Aug 2022 13:36:26 GMT
Server: Apache/2.4.46 (Unix) OpenSSL/1.0.2u PHP/7.4.21 mod_wsgi/3.5 Python/2.7.18 mod_fastcgi/mod_fastcgi-SNAP-0910052141 mod_perl/2.0.11 Perl/v5.30.1
Last-Modified: Tue, 23 Aug 2022 16:04:38 GMT
ETag: "78-5e6eab8a35180"
Accept-Ranges: bytes
Content-Length: 120
Connection: close
Content-Type: text/html

[C++] 158 HTTPプロトコル リクエスト送信

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

引き続き『C言語によるTCP/IPネットワークプログラミング』のサンプルコード読解です。

サンプルコードは手持ちのコードをつないで動いたものを載せている感が強いので、こちらで勝手にリファクタリングしていきます。

2001年に最前線の技術について著していることに価値があるのであって、単にコードを洗練させるというのは誰にでもできます。

前回記事にしたSOCprintf関数は廃止して、DoHttpGet関数内に移しました。

サンプルコードのような例外処理はあまり考えずに書きました。

// HTTP取得
int DoHttpGet(const char *host, const char *path)
{
	int	soc;
	const char *ptr;
	char request_char[4096];

	fprintf(stderr,"host = %s,path = %s\n", host, path);

	// ホストのHTTPに接続 
	if((soc = ConnectHost(host,"http",80)) == -1){
		fprintf(stderr,"Cannot connect to %s http.\n",host);
		return -1;
	}

	// リクエスト送信(旧SOCprintf関数)
	string request = "GET ";
	request += string(path);
	request += " HTTP/1.0\r\n\r\n";
	
	cout << "request " << request << endl;
	request.copy(request_char, 4095);

	send(soc, request_char, strlen(request_char), 0);
	
	// データ受信 
	if((ptr = strrchr(path,'/'))!=NULL){
		if(strlen(ptr+1)==0){
			SOCrecvDataToFile(soc,"noname");
		}
		else{
			SOCrecvDataToFile(soc,ptr+1);
		}
	}
	else{
		SOCrecvDataToFile(soc,path);
	}

	// ソケットクローズ 
	SocketClose(soc);

	return(0);
}

[C++] 157 va_listの機能調査

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

“[C++] 155 TCP/IP HTTPプロトコル”にあるSOCprintf関数の挙動を調べ、va_listの機能を把握しました。かなり特徴的な文法に思えます。

vsprintfとvfprintfは初見でした。

int SOCprintf(int num, ...)
{
	va_list args;
	int	soc;
	char *fmt;
	char buf[4096];

	// numは...内の個数
	va_start(args, num);

	// int型の引数を取得
	soc = va_arg(args, int);
	// char*型の引数を取得
	fmt = va_arg(args, char *);

	cout << "soc " << soc << endl;
	cout << "fmt " << fmt << endl;

	// 引数をformat変換しbufに格納
	vsprintf(buf, fmt, args);
	cout << "buf " << buf << endl;
	cout << "vsprintf pass" << endl;

	// format変換した引数を標準出力
	vfprintf(stdout, fmt, args);
	// vfprintf(stderr,fmt,args); // 旧コード
	cout << "vfprintf pass" << endl;

	// 引数のリストをクリア
	va_end(args);

	// リクエスト送信(socはソケット記述子の整数)
	send(soc, buf, strlen(buf), 0);

	return 0;
}

[C++] 156 strtok関数の書き換え

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

HTTPプロトコルのサンプルコードで初めてstrtok関数を目にしました。文字列を区切り文字で分割したプレフィックスのポインタを返す関数です。

この関数を使った箇所を私なりに類推しながら理解しようとしましたがさっぱり分からず、ネットで意味を調べました。この関数を使うメリットは多々あるようですが、正直使いづらいです。C++でsplitする関数が一向に創出されないのは、使いづらいけど性能は相当高いstrtok関数のせいではないかと思いました。

そのような事情はさておき、私が使いやすいように自製クラスを用いて大幅に書き換えました。

個人的にはif文の条件内で処理をしTrue/Falseで条件分岐するコードはあまり好きではありません。まさか、このような書き方ができればよりハイスキルとか言ってなければいいのですが。コードサイズを切り詰める時代でもないですし、とにかく読みにくいです。

strtok関数については今のC/C++ユーザーはどのように対処しているのか気になります。

for(i = 1; i < argc; i++){
		// ホスト:パス 
		strcpy(buf, argv[i]);
		if((ptr = strtok(buf, ":")) == NULL){
			strcpy(host, "localhost");
			strcpy(path, argv[1]);

		} else {
			strcpy(host, ptr);
			if((ptr = strtok(NULL, "")) == NULL){
				fprintf(stderr, "host:path error\n");
				return -1;
			}
			strcpy(path,ptr);
		}

		// HTTP取得実行 
		DoHttpGet(host,path);
}
#include "Split.h"

Split spt;

for(i = 1; i < argc; i++){
		// ホスト:パス 
		vector<string> arg_list = spt.splits(argv[i], ":");
		const char* arg0 = (arg_list[0]).c_str();
		const char* arg1 = (arg_list[1]).c_str();

		// HTTP取得実行
		DoHttpGet(arg0, arg1);
}

[C++] 155 TCP/IP HTTPプロトコル

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

昨年2021年7月に購入した『C言語によるTCP/IPネットワークプログラミング』(初版 2001年)をようやく読み始めました。

まずはHTTPプロトコルのサンプルコードを走らせようとしましたが、何せ21年前に書かれたC99のコードなので案の定エラーの嵐でした。

色々修正し、さらにC言語をC++に変えて完成させました。

修正手順
1.ヘッダファイルを別途作成
2.”exit(-1)”を”return -1″に書き換え他
3.va_listまわりの修正
4.Makefileの書き換え(ccコンパイラをclang++へ変更他)

一番苦労したのは変数引数リストva_listを作成するところです。元ソースにあったvarargs.hがすでに廃止されていたため、stdarg.hに切り替えました。引数の内容が変わっており、修正に結構時間を費やしました。

Mac mini内にMAMPで仮想Webサーバーを立ち上げ、その中にあるindex.htmlにアクセスしてみました。

とりあえず動きましたが、コードの内容についてはこれから理解を深めていきます。

# コマンド
httpget localhost:/index.html
--------------------------------------------------
出力
--------------------------------------------------
host=localhost,path=/index.html
GET /index.html HTTP/1.0

HTTP/1.1 200 OK
Date: Tue, 23 Aug 2022 16:06:37 GMT
Server: Apache/2.4.46 (Unix) OpenSSL/1.0.2u PHP/7.4.21 mod_wsgi/3.5 Python/2.7.18 mod_fastcgi/mod_fastcgi-SNAP-0910052141 mod_perl/2.0.11 Perl/v5.30.1
Last-Modified: Tue, 23 Aug 2022 16:04:38 GMT
ETag: "78-5e6eab8a35180"
Accept-Ranges: bytes
Content-Length: 120
Connection: close
Content-Type: text/html
int main(int argc,char *argv[])
{
char buf[512];
char host[MAXHOSTNAMELEN],path[512],*ptr;
int	i;

	if(argc<=1){
		fprintf(stderr,"httpget host:path ...\n");
		// exit(-1);
		return -1;
	}

	// 引数のループ 
	for(i=1;i<argc;i++){
		// ホスト:パス 
		strcpy(buf,argv[i]);
		if((ptr=strtok(buf,":"))==NULL){
			strcpy(host,"localhost");
			strcpy(path,argv[1]);
		}
		else{
			strcpy(host,ptr);
			if((ptr=strtok(NULL,""))==NULL){
				fprintf(stderr,"host:path error\n");
				// exit(-1);
				return -1;
			}
			strcpy(path,ptr);
		}
		// HTTP取得実行 
		DoHttpGet(host,path);
	}
}

int SOCprintf(int num, ...)
// va_dcl
{
	va_list args;
	int	soc;
	char *fmt;
	char buf[4096];

	va_start(args,num);
	soc = va_arg(args,int);
	fmt = va_arg(args,char *);

	vsprintf(buf,fmt,args);
	vfprintf(stderr,fmt,args);
	va_end(args);

	send(soc,buf,strlen(buf),0);

	return 0;
}

[C++] 154 マルチバイト文字有無の判定 コード改良の試み mbstowcs関数

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, FLTK 1.3.8, NO IDE]

前回の続きです。

どうしても気になったので、さらに調べてみました。

どうやらappファイルではmbstowcs関数がまともに動かないようです。マルチバイト文字を1文字としてカウントすることができません。

下図の右ウインドウに表示されているように、#FFFFFF(#は3バイト文字)がstring、wstringともに10文字としてカウントされています。mbstowcs関数が機能していればwstringは8文字になるはずです。同時にビルドした実行ファイルでは実際そうなっています。

これはライブラリを提供しているApple側の問題に思えます。これで一応の結論にたどり着きました。

MacOSでC++を扱っていると細かいところで非対応や不具合に遭遇します。やはりWindowsのVisual C++が至高だと思います。まあいざとなればObjective-C++へ鞍替えします。

int narrowToWide(string str) {
	wchar_t *wcs = new wchar_t[str.length() + 1];
	int num = mbstowcs(wcs, str.c_str(), str.length() + 1);
    
	return num;
}

int multibyteDetect(string str){
    int length = str.length();
    cout << "length " << length << endl;
    output_line2->insert("length ");
    output_line2->insert((to_string(length)).c_str());
    output_line2->insert("\n");

    int length_w = narrowToWide(str);
    cout << "length_w " << length_w << endl;
    output_line2->insert("length_w ");
    output_line2->insert((to_string(length_w)).c_str());
    output_line2->insert("\n");

    if (length != length_w){
        return -1;
    }
    
    return 0;
}

[C++] 153 マルチバイト文字有無の判定 コード改良の試み

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, FLTK 1.3.8, NO IDE]

前回の続きです。

そう言えば、ちょうど1ヶ月前にマルチバイト文字の抽出方法について記事を書いていました。その内容を流用します。

stringをwstringに変換して文字数を取得すれば、マルチバイト文字を1文字扱いにした全文字数+αが分かります。

これで文字列が可変長でも対応できるはずですが、自製カラーアプリのappファイルが落ちるようになってしまったため、前回のコードのままにしておきます。なおコンソール付きの実行ファイルでは問題なく動作します。

マルチバイト文字にあまり関わると深みにはまってしまうので、これ以上追究するのはやめておきます。

wstring narrowToWide(const string src) {
    wchar_t *wcs = new wchar_t[src.length() + 1];
    mbstowcs(wcs, src.c_str(), src.length() + 1);
    return wcs;
}

int multibyteDetect(string str){
    int length = str.length();
    cout << "length " << length << endl;

    wstring str_w = narrowToWide(str);
    int length_w = str_w.length();
    cout << "length_w " << length_w << endl;

    // stringの内容確認
    int i = 0;
    for (int b: str){
        cout << "string " << i << " " << b << endl;
        i++;
    }

    // stringの内容確認2
    for (int i = 0 ; i < str.length() ; ++i){
        cout << "string2 " << i << " " << str[i] << endl;
    }

    // wstringの内容確認
    int i2 = 0;
    for (int b: str_w){
        cout << "wstring " << i2 << " " << b << endl;
        i2++;
    }

    // wstringの内容確認2
    for (int i = 0 ; i < str_w.length() ; ++i){
        cout << "wstring2 " << i << " " << str_w[i] << endl;
    }

    if (length != length_w){
        return -1;
    }
    
    return 0;
}
--------------------------------------------------
出力例 #FFFFFF 数学記号の#(ここでの表記は1バイト文字の#)
--------------------------------------------------
length 10
length_w 8

string 0 35
string 1 -30
string 2 -128
string 3 -83
string 4 70
string 5 70
string 6 70
string 7 70
string 8 70
string 9 70

string2 0 #
string2 1 ?
string2 2 ?
string2 3 ?
string2 4 F
string2 5 F
string2 6 F
string2 7 F
string2 8 F
string2 9 F

wstring 0 35
wstring 1 8237
wstring 2 70
wstring 3 70
wstring 4 70
wstring 5 70
wstring 6 70
wstring 7 70

wstring2 0 35
wstring2 1 8237
wstring2 2 70
wstring2 3 70
wstring2 4 70
wstring2 5 70
wstring2 6 70
wstring2 7 70

[C++] 152 マルチバイト文字有無の判定

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

HTMLやCSSではカラーコードは#を先頭に付けます。

ただ、この#が曲者で1バイト文字、3バイト文字(数学記号)、3バイト文字(全角)の3種類あります。

ネット初心者以外ではまずないとは思いますが、後2者が混ざり込んでいるのに気が付かずネットに貼ってしまう方がいたりします。全角の場合は見れば分かるものの、数学記号の方は見た目が全く同じで厄介です。

そのようなカラーコードをアプリにコピペした際、不具合が生じないようにするため合計バイト数が7である場合のみ処理を進めるようにしました。

文字列が可変長の場合については調査中です。

ネットから#を含む文字列をコピペする場合はそのようなリスクがあることを知っておくべきでしょうね。

int multibyteDetect(string str){
    int length = str.length();
    cout << "length " << length << endl;

    if (length != 7){
        return -1;
    }
    return 0;
}

[C++] 151 FLTK : Fl_Multiline_OutputとFl_Text_Displayの比較

[M1 Mac, Big Sur 11.6.8, clang 13.0.0, FLTK 1.3.8, NO IDE]

Fl_MultiLine_OutputにFl_Scrollbarを内包させられるかどうか公式ドキュメントを調べたところ、そもそもの性質が合わないために難しいことが判明しました。

調べた結果を以下の図にまとめました。違和感を覚えていたFl_Groupの命名の意味がようやく理解できました。言っても仕方ないですが、Fl_Containerとでも命名してくれていたら理解は早かったと思います。

Fl_Input_群は行数を把握できない低機能なウィジェットであることが分かりました。行数が分からないのではFl_Scrollbarを内包できないです。

ただFl_ScrollをコンテナとしてFl_Multiline_Outputを中に配置した場合に適切に動作するよう調整が可能なのか、確認しておく必要があります。

FLTKのクラス名をJava的な感覚で解釈すると痛い目にあいますね。Swingがいかに小慣れた巧みな命名をしているか、Swingの方がむしろ特殊で突出して優れていると見るべきでしょうか。だからこそ一時代を築くことが出来たのだと思います。

A