[ BBS (Bulletin Board System) - Perl to C - ] 現在(2003/09/17) BBS は Perl で書くのが主流のようだ。 ならば C で書いてみよう!というあまのじゃく的な考え方のもとこの文章を書き上げた。 動作確認は Linux, Apache 1.3 で行っています。 1. 時間取得 まずは時間を取得する関数を作る。 sub GetTime { ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); return(sprintf("%d/%02d/%02d %02d:%02d:%02d", $year+1900, ++$mon, $mday, $hour, $min, $sec)); } これは Perl で書いたものだ。(みれば分かるが) ちょっと見にくいが時間を取得して return でその値(文字列)を返している。 では C で同じものを書いてみると int GetTime(char *date) { char *str, *p[8]; int i = 0; time_t t = time(NULL); /* 改行までをstrに格納(つまり改行を取り除く) */ str = strtok(ctime(&t), "\x0A"); /* 空白をはさんで配列に入れていく */ p[i++] = strtok(str, "\x20"); while((p[i] = strtok(NULL, "\x20")) != NULL) i++; /* p[0]week, p[1]month, p[2]day, p[3]time, p[4]year */ if((strncmp(p[1], "January" , 3)) == 0) strcpy(p[1], "01"); else if((strncmp(p[1], "February" , 3)) == 0) strcpy(p[1], "02"); else if((strncmp(p[1], "March" , 3)) == 0) strcpy(p[1], "03"); else if((strncmp(p[1], "April" , 3)) == 0) strcpy(p[1], "04"); else if((strncmp(p[1], "May" , 3)) == 0) strcpy(p[1], "05"); else if((strncmp(p[1], "June" , 3)) == 0) strcpy(p[1], "06"); else if((strncmp(p[1], "July" , 2)) == 0) strcpy(p[1], "07"); else if((strncmp(p[1], "August" , 3)) == 0) strcpy(p[1], "08"); else if((strncmp(p[1], "September", 3)) == 0) strcpy(p[1], "09"); else if((strncmp(p[1], "October" , 3)) == 0) strcpy(p[1], "10"); else if((strncmp(p[1], "November" , 3)) == 0) strcpy(p[1], "11"); else if((strncmp(p[1], "December" , 3)) == 0) strcpy(p[1], "12"); else return(-1); if((sprintf(date, "%s/%s/%s %s",p[4], p[1], p[2], p[3])) < 0) return(-2); return 0; } 長い!(^^ 時間取得関数の実現には他にもいろいろな方法がありますが この関数は引数に時間を格納してもらう配列のポインタを渡しています。 そして戻り値をエラー処理用に使用しています。戻り値が負ならエラー発生。 #include #include これらをincludeする必要があります。 time.c ------------------------------------------------------------------------------ #include #include #include /* 時間取得の関数 [説明] 引数に "2000/00/00 00:00:00"以上の 長さを持つ文字列のポインタを渡すと時間をいれてくれる。 [引数] "2000/00/00 00:00:00 (MON)"以上の長さを持つ配列のポインタ [戻り値] 正常:0 エラー:負の値 */ int GetTime(char *date) { char *str, *p[8]; int i = 0; time_t t = time(NULL); /* 改行までをstrに格納(つまり改行を取り除く) */ str = strtok(ctime(&t), "\x0A"); /* 空白をはさんで配列に入れていく */ p[i++] = strtok(str, "\x20"); while((p[i] = strtok(NULL, "\x20")) != NULL) i++; /* p[0]week, p[1]month, p[2]day, p[3]time, p[4]year */ if((strncmp(p[1], "January" , 3)) == 0) strcpy(p[1], "01"); else if((strncmp(p[1], "February" , 3)) == 0) strcpy(p[1], "02"); else if((strncmp(p[1], "March" , 3)) == 0) strcpy(p[1], "03"); else if((strncmp(p[1], "April" , 3)) == 0) strcpy(p[1], "04"); else if((strncmp(p[1], "May" , 3)) == 0) strcpy(p[1], "05"); else if((strncmp(p[1], "June" , 3)) == 0) strcpy(p[1], "06"); else if((strncmp(p[1], "July" , 2)) == 0) strcpy(p[1], "07"); else if((strncmp(p[1], "August" , 3)) == 0) strcpy(p[1], "08"); else if((strncmp(p[1], "September", 3)) == 0) strcpy(p[1], "09"); else if((strncmp(p[1], "October" , 3)) == 0) strcpy(p[1], "10"); else if((strncmp(p[1], "November" , 3)) == 0) strcpy(p[1], "11"); else if((strncmp(p[1], "December" , 3)) == 0) strcpy(p[1], "12"); else return(-1); if((sprintf(date, "%s/%s/%s %s",p[4], p[1], p[2], p[3])) < 0) return(-2); return 0; } int main(void) { char date[32]; GetTime(date); printf("%s\n", date); return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall time.c bbs]$ ./a.out 2003/09/17 17:15:59 bbs]$ 注意すべきことは この関数は引数からデータを格納する配列のポインタをもらっているので char date[32]; のようにある程度の大きさを持った配列のポインタを渡さないと セグメンテーション違反がおきるということだ。 試しに char date[8]; として実行してみればいい。 まぁ 32bytes 確保すれば十分なのだがもし気にくわないなら 変数を Static で確保してそのポインタを渡してやればいい。 2. フォームデータの加工 BBSを作るなら必要不可欠な要素だ。 aaaa=1111&bbbb=2222&name=love&mail=test.com というような文字列を扱いやすいように分解してやる処理だ。 if( $ENV{'REQUEST_METHOD'} eq "POST" ) { read( STDIN, $buf, $ENV{'CONTENT_LENGTH'} ); } @data = split(/&/, $buf); foreach $data (@data) { ($key, $val) = split(/=/, $data); $val =~ tr/+/ /; $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $val =~ s/&/&/g; $val =~ s/"/"/g; $val =~ s//>/g; $decode_data{$key} = $val; } Perl ならこんな風に実現されてるかもしれない。 C ではまず key, val に分ける処理をする関数を作る。 そのあと $val =~ tr/+/ /; $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; この2行の処理を行う関数を作り、 ファイル書き込み時に $val =~ s/&/&/g; $val =~ s/"/"/g; $val =~ s//>/g; これらの処理を行わせる。 ではまずフォームデータを切り分ける関数を書く。 cut.c ------------------------------------------------------------------------------ #include /* フォームデータの文字列(1行)を切り分ける関数 [説明] フォームから送られてくるデータ"111=aaa&222=bbb&333=ccc"を それぞれの名前と値にわけていく。 [引数] 1行の文字列, その文字列の長さ, nameを格納, valueを格納, 要素数 [戻り値] 正常:0 エラー:負の値 */ int parse_form(char *data, int len, char *name[], char *value[], int *element) { int i = 0; int cur_field = 0; *element = 0; if(data[0] == '\0') return(-1); name[0] = data; do{ if(data[i] == '='){ data[i] = '\0'; value[cur_field] = data + i + 1; }else if(data[i] == '&'){ data[i] = '\0'; cur_field++; name[cur_field] = data + i + 1; } }while((data[++i] != '\0') && (i < len)); *element = cur_field + 1; return 0; } int main(void) { char data[] = "1111=aaaa&2222=bbbb&3333=cccc"; char *name[20],*val[20]; int element; parse_form(data, strlen(data), name, val, &element); printf("%s %s\n",name[0],val[0]); printf("%s %s\n",name[1],val[1]); printf("%s %s\n",name[2],val[2]); return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall cut.c bbs]$ ./a.out 1111 aaaa 2222 bbbb 3333 cccc bbs]$ まず最初に name[0] に data のポインタをいれて '=' or '&' が発見されたら その場所を '\0' にして name, value 配列にポインタをいれていく。 3. DECODE処理 つぎは $val =~ tr/+/ /; $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; の部分を処理する関数を作る。 この部分の処理は実際にCGIを置いて確かめた方が良いので、 ・name, mail, comment の3つの書き込み場所がある。 ・投稿すると書き込んだデータが整理されてHTMLで出力される。 ・ファイル保存は行わない。 ・投稿した時間も出力させる。 というような仕様のCGIを作ってみます。 test.c ------------------------------------------------------------------------------ #include #include #include #include int GetTime(char *date) { char *str, *p[8]; int i = 0; time_t t = time(NULL); /* 改行までをstrに格納(つまり改行を取り除く) */ str = strtok(ctime(&t), "\x0A"); /* 空白をはさんで配列に入れていく */ p[i++] = strtok(str, "\x20"); while((p[i] = strtok(NULL, "\x20")) != NULL) i++; /* p[0]week, p[1]month, p[2]day, p[3]time, p[4]year */ if((strncmp(p[1], "January" , 3)) == 0) strcpy(p[1], "01"); else if((strncmp(p[1], "February" , 3)) == 0) strcpy(p[1], "02"); else if((strncmp(p[1], "March" , 3)) == 0) strcpy(p[1], "03"); else if((strncmp(p[1], "April" , 3)) == 0) strcpy(p[1], "04"); else if((strncmp(p[1], "May" , 3)) == 0) strcpy(p[1], "05"); else if((strncmp(p[1], "June" , 3)) == 0) strcpy(p[1], "06"); else if((strncmp(p[1], "July" , 2)) == 0) strcpy(p[1], "07"); else if((strncmp(p[1], "August" , 3)) == 0) strcpy(p[1], "08"); else if((strncmp(p[1], "September", 3)) == 0) strcpy(p[1], "09"); else if((strncmp(p[1], "October" , 3)) == 0) strcpy(p[1], "10"); else if((strncmp(p[1], "November" , 3)) == 0) strcpy(p[1], "11"); else if((strncmp(p[1], "December" , 3)) == 0) strcpy(p[1], "12"); else return(-1); if((sprintf(date, "%s/%s/%s %s",p[4], p[1], p[2], p[3])) < 0) return(-2); return 0; } int parse_form(char *data, int len, char *name[], char *value[], int *element) { int i = 0; int cur_field = 0; *element = 0; if(data[0] == '\0') return(-1); name[0] = data; do{ if(data[i] == '='){ data[i] = '\0'; value[cur_field] = data + i + 1; }else if(data[i] == '&'){ data[i] = '\0'; cur_field++; name[cur_field] = data + i + 1; } }while((data[++i] != '\0') && (i < len)); *element = cur_field + 1; return 0; } /*------------------------------------------------ decode_form関数 [引数] デコードする文字列,その文字列の長さ [戻り値] 正常:0 エラー:負の値 ------------------------------------------------*/ int decode_form(char *s,int len) { char buf, *str; int i, j; if(len == 0) return(-1); if((str=(char*)malloc((sizeof(char))*(len))) == NULL) return(-1); for(i=0,j=0;i='A') ? s[i]-'A'+10 : s[i]-'0'); buf *= 16; buf += ((s[++i]>='A') ? s[i]-'A'+10 : s[i]-'0'); str[j] = buf; } for(i=0;i\n" "\n" "\n" "%s\n" "\n" ,str); return 0; } /*------------------------ エラー出力 ------------------------*/ void Error(char *str) { Head("Error"); printf( "\n" "

Error

\n" "

%s

\n" "\n" "\n" ,str); exit(0); } #define MAX_LEN (1024 * 2) int main(void) { char *buf; unsigned int length; char *name[20], *value[20]; int nField, i; char date[32] = "2000/00/00 00:00:00"; /* hash */ short int eName = -1; short int eComment = -1; short int eMail = -1; /* getenv */ char *cLength = ENV("CONTENT_LENGTH"); char *Method = ENV("REQUEST_METHOD"); char *Addr = ENV("REMOTE_ADDR"); /* POST ? or GET ? */ if(cLength && !strncmp( Method, "POST", 4 ) && (length=(unsigned int)atoi(cLength)) > 0 && length < MAX_LEN){ /* データを格納する領域を確保してそこにデータを読み込む */ if((buf = (char*)malloc((sizeof(char))*(length+1))) == NULL) Error("Malloc:buf"); i = (int)fread(buf, 1, length, stdin); buf[i] = '\0'; /* データを切り分ける */ if((parse_form(buf, (int)strlen(buf), name, value, &nField)) < 0) Error("parse_form"); /* デコード処理 */ for(i=0;i\n"); printf("

%s: %s

\n", name[eName], value[eName]); printf("

%s: %s

\n", name[eMail], value[eMail]); printf("

%s: %s

\n", name[eComment], value[eComment]); printf("

TIME:%s, IP:%s

\n", date, Addr); printf(""); free(buf); }else{ Head("input"); printf("\n"); /* 注意! action を cgiファイル名にしなければならない */ printf( "
\n" "NAME:
\n" "MAIL:
\n" "
\n" "\n" "\n" "
\n" ); printf(""); } return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall test.c -o test.cgi bbs]$ いままでの復習も兼ねて書きました。 ポイントは decode_form関数 です。これが Perl の $val =~ tr/+/ /; $val =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; と同じことをやっています。 C で書くと Perl に比べ 複雑で分かりにくいです。(ソース長いし) やはりCGIはよほど負荷を軽くしなければならない理由がない限り Perl で書いた方が良いです。 さてここまで来ると、あとはファイル操作だけです。 整形したフォームデータをファイルに保存しファイル内容を出力する。 実用的なBBSを作る場合は他にもロック処理や二重投稿防止など いろいろしなければならないこともありますが、 ここでは最小限の機能をそろえたBBSを作るだけなので他のことはやりません。 そもそも私がそういう所にあまり興味が持てない人間なのです(^^; 4. ファイル操作 まずファイル書き込みを担う関数を書きます。 file.c ------------------------------------------------------------------------------ #include #include /* ファイル書き込み関数 [説明] 引数に渡されたデータを"<>"をはさんでファイルに書き込む。 引数に渡された順番にファイルへの書き込みが行われる。 [引数] logfileにファイル名、 nに実際に書き込むデータの数、其の後に不確定数の文字列(char *)を続ける。 [戻り値] 正常:0 エラー:負の値 */ int file_write(char *logfile, int n, ...) { FILE *fp; char *str; va_list ap; int i; if((fp = fopen( logfile, "a" )) == NULL) return(-1); va_start(ap, n); for(i=0;i' : fputs(">", fp); break; case '&' : fputs("&", fp); break; case '"' : fputs(""", fp); break; case '\r' : if(*(str+1) != '\n') fputs("
", fp); break; case '\n' : fputs("
", fp); break; default : fputc(*str, fp); break; } } fputs("<>", fp); } va_end(ap); fputc('\n', fp); fclose(fp); return 0; } int main(void) { file_write("log.txt", 5, "111", "2<<2", "33>>3", "4&&44", "5\"55"); return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall file.c bbs]$ ./a.out bbs]$ cat log.txt 111<>2<<2<>33>>3<>4&&44<>5"55<> bbs]$ Perl でいう $val =~ s/&/&/g; $val =~ s/"/"/g; $val =~ s//>/g; という処理をやっています。 Perl の場合文字列置換が得意なのでそのままやってますが C は文字列置換をやるのが結構大変なので ファイル書き込み時にそれを実現しています。 次は読み込みの処理を書きます。 といっても読み込みの場合は一行単位で読み込んで処理するのか? それとも一気に全部メモリに読み込んだあと処理するのか? というように人によって書き方に違いがでてくるので その部分はmain関数にまかせて、ここでは文字列の整形を担当する関数を書こうと思う。 file2.c ------------------------------------------------------------------------------ #include /* ファイルから読み込んだ文字列(1行)を切り分ける関数 [説明] "<>"で分けられた1行の文字列を要素ごとに切り分ける [引数] 1行の文字列,その文字列の長さ,データを格納するポインタ,要素数 [戻り値] 正常:0 エラー:負の値 */ int parse_fData(char *data, int len, char *line[], int *element) { int i = 0; int cur_field = 0; *element = 0; if(data[0] == '\0') return(-1); line[0] = data; do{ if(data[i] == '<' && data[i+1] == '>'){ data[i] = '\0'; cur_field++; line[cur_field] = data + i + 2; } }while((data[++i] != '\0') && (i < len)); *element = cur_field + 1; return 0; } int main(void) { char data[] = "111<>222<>333<>444<>555<>"; char *line[20]; int element, i; parse_fData(data, strlen(data), line, &element); for(i=0;i<5;i++) printf("%s\n",line[i]); return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall file2.c bbs]$ ./a.out 111 222 333 444 555 bbs]$ 5. 簡易BBSの作成 ピースを全部集めてこよう。 ・まず時間を取得する関数。 ・フォームデータの整形関数とデコード関数 ・ファイル操作系の関数 他には test.c で使用した ENV や Head などのCGI特有の関数などを持って来る。 これらを組み合わせるだけだ。 なお漢字コード変換の処理は行わない。 bbs.c ------------------------------------------------------------------------------ #include #include #include #include #include int GetTime(char *date) { char *str, *p[8]; int i = 0; time_t t = time(NULL); /* 改行までをstrに格納(つまり改行を取り除く) */ str = strtok(ctime(&t), "\x0A"); /* 空白をはさんで配列に入れていく */ p[i++] = strtok(str, "\x20"); while((p[i] = strtok(NULL, "\x20")) != NULL) i++; /* p[0]week, p[1]month, p[2]day, p[3]time, p[4]year */ if((strncmp(p[1], "January" , 3)) == 0) strcpy(p[1], "01"); else if((strncmp(p[1], "February" , 3)) == 0) strcpy(p[1], "02"); else if((strncmp(p[1], "March" , 3)) == 0) strcpy(p[1], "03"); else if((strncmp(p[1], "April" , 3)) == 0) strcpy(p[1], "04"); else if((strncmp(p[1], "May" , 3)) == 0) strcpy(p[1], "05"); else if((strncmp(p[1], "June" , 3)) == 0) strcpy(p[1], "06"); else if((strncmp(p[1], "July" , 2)) == 0) strcpy(p[1], "07"); else if((strncmp(p[1], "August" , 3)) == 0) strcpy(p[1], "08"); else if((strncmp(p[1], "September", 3)) == 0) strcpy(p[1], "09"); else if((strncmp(p[1], "October" , 3)) == 0) strcpy(p[1], "10"); else if((strncmp(p[1], "November" , 3)) == 0) strcpy(p[1], "11"); else if((strncmp(p[1], "December" , 3)) == 0) strcpy(p[1], "12"); else return(-1); if((sprintf(date, "%s/%s/%s %s",p[4], p[1], p[2], p[3])) < 0) return(-2); return 0; } int parse_form(char *data, int len, char *name[], char *value[], int *element) { int i = 0; int cur_field = 0; *element = 0; if(data[0] == '\0') return(-1); name[0] = data; do{ if(data[i] == '='){ data[i] = '\0'; value[cur_field] = data + i + 1; }else if(data[i] == '&'){ data[i] = '\0'; cur_field++; name[cur_field] = data + i + 1; } }while((data[++i] != '\0') && (i < len)); *element = cur_field + 1; return 0; } int decode_form(char *s,int len) { char buf, *str; int i, j; if(len == 0) return(-1); if((str=(char*)malloc((sizeof(char))*(len))) == NULL) return(-1); for(i=0,j=0;i='A') ? s[i]-'A'+10 : s[i]-'0'); buf *= 16; buf += ((s[++i]>='A') ? s[i]-'A'+10 : s[i]-'0'); str[j] = buf; } for(i=0;i\n" "\n" "\n" "%s\n" "\n" ,str); return 0; } void Error(char *str) { Head("Error"); printf( "\n" "

Error

\n" "

%s

\n" "\n" "\n" ,str); exit(0); } int file_write(char *logfile, int n, ...) { FILE *fp; char *str; va_list ap; int i; if((fp = fopen( logfile, "a" )) == NULL) return(-1); va_start(ap, n); for(i=0;i' : fputs(">", fp); break; case '&' : fputs("&", fp); break; case '"' : fputs(""", fp); break; case '\r' : if(*(str+1) != '\n') fputs("
", fp); break; case '\n' : fputs("
", fp); break; default : fputc(*str, fp); break; } } fputs("<>", fp); } va_end(ap); fputc('\n', fp); fclose(fp); return 0; } int parse_fData(char *data, int len, char *line[], int *element) { int i = 0; int cur_field = 0; *element = 0; if(data[0] == '\0') return(-1); line[0] = data; do{ if(data[i] == '<' && data[i+1] == '>'){ data[i] = '\0'; cur_field++; line[cur_field] = data + i + 2; } }while((data[++i] != '\0') && (i < len)); *element = cur_field + 1; return 0; } #define MAX_LEN (1024 * 2) int main(void) { FILE *fp; char *buf; unsigned int length; char *name[20], *value[20], *line[20]; int nField, i; char date[32] = "2000/00/00 00:00:00"; /* hash */ short int eName = -1; short int eComment = -1; short int eMail = -1; /* getenv */ char *cLength = ENV("CONTENT_LENGTH"); char *Method = ENV("REQUEST_METHOD"); char *Addr = ENV("REMOTE_ADDR"); /* POST ? or GET ? */ if(cLength && !strncmp( Method, "POST", 4 ) && (length=(unsigned int)atoi(cLength)) > 0 && length < MAX_LEN){ /* データを格納する領域を確保してそこにデータを読み込む */ if((buf = (char*)malloc((sizeof(char))*(length+1))) == NULL) Error("Malloc:buf"); i = (int)fread(buf, 1, length, stdin); buf[i] = '\0'; /* データを切り分ける */ if((parse_form(buf, (int)strlen(buf), name, value, &nField)) < 0) Error("parse_form"); /* デコード処理 */ for(i=0;i\n"); printf("

以下の書き込みは完了しました

\n"); printf("

%s: %s

\n", name[eName], value[eName]); printf("

%s: %s

\n", name[eMail], value[eMail]); printf("

%s: %s

\n", name[eComment], value[eComment]); printf("

TIME:%s, IP:%s

\n", date, Addr); /* 注意! href を cgiファイル名にしなければならない */ printf("back"); printf(""); free(buf); }else{ Head("input"); printf("\n"); /* file read */ if((fp = fopen( "log.txt", "r" )) == NULL) Error("read"); if((buf = (char *)malloc(MAX_LEN)) == NULL) Error("malloc"); while((fgets( buf, MAX_LEN, fp )) != NULL){ /* HTML */ parse_fData(buf, (int)strlen(buf), line, &nField); printf("
 %s\n %s\n %s
\n", line[0], line[2], line[1]); } free(buf); fclose(fp); /* 注意! action を cgiファイル名にしなければならない */ printf( "
\n" "NAME:
\n" "MAIL:
\n" "
\n" "\n" "\n" "
\n" ); printf(""); } return 0; } ------------------------------------------------------------------------------ bbs]$ gcc -Wall bbs.c -o bbs.cgi bbs]$ chmod 666 log.txt 最後に 最小限度の機能しかつけてませんが C で無事掲示板を作ることができました。 しかしさすがに辛かったです。せめて C++ なら String型 があるのだが なんさま C は文字列の扱いがめんどうです。全部配列でやんなきゃなんない(T_T それに行き当たりばったりで作って来たのでどことなくソースが汚いかも... 結論! CGI は、よほど重大な理由がない限りは Perl で書いた方が無難です。 というか Perl で書くべきでしょう。(最近は PHP などもありますが) End. written by kenji aiko 2003/09/18 Copyright (C) 2003 kenji aiko All Rights Reserved