タイトルの通り、C言語で「簡易HTTPサーバ 兼 Webアプリ」作ってみました。
(※遊びで作ったので、ちゃんとした製品を作る場合は考慮すべきことがもっともっとあります。)
10年以上ぶりにC触りましたが、なんかもう、完全に忘れてますね…。
CでWebアプリ作るのって、普段超高級言語(PHPとか)ばかり触っている人間からするとなんかロマンがあります。
せっかくなのでメモがてら記事にしておきます。
作ったもの
**LaaS(Lip as a Service)を実現するための世界初のソフトウェア、「AwesomeLaaS」**を開発しました。
以下にアクセスすると「リップ」をjsonで返してくれます。
【AwesomeLaaS】
http://laas.hoku.in/
具体的には、以下のようなリップを返してくれます。
- すごい品があるね!!
- いやほんと柔軟な発想力がある!
- 超活き活きとしてるじゃん!
- とっても意識が高いね!
- etc...
ソースコードと使い方は以下で公開しています。
https://github.com/hoku/awesomelaas
LaaS(Lip as a Service)とは?
そんな用語はこの世に存在していないのですが、勝手に作りました。
「リップとサービスの間に as a を入れればXaaSになるじゃん!」という、うまくもなんとも無い発想により考え出されました。
仕組み
今回開発したAwesomeLaaSは、大まかには以下の要素で出来上がっています。
Apache HTTP Server や Nginx 等は使用せず、全てC言語で記述しています。
- HTTPでリクエストを待ち受ける。
- リップを自動生成する。
- jsonでリップを返却する。
極めてシンプルですが、それぞれについて細かく書いていきたいと思います。
HTTPでリクエストを待ち受ける
※詳細はソースコードを見ていただければと思います。(マジで適当なコードですが。)
※「厳密には//はC++だろゴラァ!!」とかは許して。。
やっていることは単純な話で、「80番ポートで要求を待ち受ける実装をするだけ」になります。
int sock;
struct sockaddr_in addr;
// ソケット作成
sock = socket(AF_INET, SOCK_STREAM, 0);
// 待受用のソケットアドレス設定
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
addr.sin_addr.s_addr = INADDR_ANY;
// ソケットにバインド
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
// リッスン
listen(sock, 10);
while (1) {
// 接続要求を待ち受ける
client_sock = accept(sock, (struct sockaddr *)&client_addr, &addr_length);
// 要求が来たら、なんか色々処理する
...
}
// ↑実際は適宜エラー処理が必要です。
上記のaccept関数で待ち受け、80番ポートに要求が来たら以降の処理に入ります。
while(1) {} で囲っていますが、これにより「要求受ける → 処理してレスポンス返す → また要求待ち受ける → (繰り返し)」を実現しています。
つまり、いわばHTTPサーバ的に動き続けるようになります。
リップを自動生成する
注意!このコードはクソコードです!
// リップの構成要素1
char msg1[13][60] = {
{"いやほんと"},
…
{"とっても"},
{"とても"}};
// リップの構成要素2
char msg2[56][60] = {
{"格好いい"},
…
{"本質を見抜く力がある"},
{"才能ある"}};
// リップの構成要素3(「ね」と「よね」の出現率を上げておく)
char msg3[11][30] = {
{"ね"},
…
{"じゃん"},
{""}};
// リップの構成要素4
char msg4[10][30] = {
{"!"},
…
{"!!"},
{"!!!"}};
// 返却するリップ
char result_message[100];
int main(void) {
…
while (1) {
// 接続要求を待ち受ける
client_sock = accept(sock, (struct sockaddr *)&client_addr, &addr_length);
// 今回のリップを生成
sprintf(result_message, "{\"lip\":\"%s%s%s%s\"}",
msg1[rand() % (sizeof(msg1) / sizeof(msg1[0]))],
msg2[rand() % (sizeof(msg2) / sizeof(msg2[0]))],
msg3[rand() % (sizeof(msg3) / sizeof(msg3[0]))],
msg4[rand() % (sizeof(msg4) / sizeof(msg4[0]))]);
…
}
}
考え方としては、変数宣言でmsg1, msg2, msg3, msg4, result_message、のメモリを確保しておきます。
その上で、sprintf(result_message, ...) をして、自動生成したリップの文字列をresult_messageのメモリ領域に格納しています。
C言語なので事前に領域を確保しておく必要があります。
普段高級言語ばかり触っていると、ここらへん「どうやるんだっけな?」と訳分からなくなる部分ですよね。(なりました)
しかも今回のプログラムはプロセスがずっと生きている類のものなので、「まぁプロセス終わったら開放されるだろう」的な逃げも出来ません…。
ちなみに、リップの内容は人力で頑張ってリストアップします。
それをランダムに繋げるだけです。
AIとか令和とか関係ありません、気合と根性です。
jsonでリップを返却する
返却するリップを生成したら、あとはそれをhttpレスポンスとしてクライアント側に返します。
// 返却するhttpレスポンス
char result_http[1000];
int main(void) {
…
while (1) {
…
// httpレスポンスを生成
sprintf(result_http, "%s%s%s%s%d%s%s%s%s%s",
"HTTP/1.1 200 OK\r\n",
"Content-Type: application/json; charset=UTF-8\r\n",
"Server: HokuServer\r\n",
"Content-Length: ",
strlen(result_message),
"\r\n",
"Connection: close\r\n",
"Access-Control-Allow-Origin: *\r\n",
"\r\n",
result_message);
// httpレスポンスを出力
write(client_sock, result_http, strlen(result_http));
// クローズ
close(client_sock);
}
close(sock);
return 0;
}
sprintf(result_http, ...) で、result_httpのメモリ領域にhttpレスポンス全体の文字列を格納します。
そして、write関数でクライアント側にhttpレスポンス全体の文字列を出力しています。
「HTTP」はざっくり言うと「TCPの80番ポートでテキストをやり取りしているだけ」なので、HTTPのルールに則ってテキストを作って、それをクライアント側に返してあげれば通信が成立します。
(正確に言うと、80番ポートじゃなくても成立します)
ビルド&実行
上記を実装したら、ビルドして実行します。
# ビルド
gcc -o laas laas.c
# 実行
sudo nohup ./laas &
ちなみに、実行はsudoでやる必要があります。
80番ポートはOS的に特別扱いされるポートなので、root権限無しで実行しようとすると「させねーよ?」的なエラーが出ます。
※ソースを修正して8080番ポートとかで待ち受けるのであれば、root権限無しで実行できます。
まとめ
やっぱ普通のWebアプリ作るなら超高級言語(PHPとか)に限るわ!!1
-
個人の見解です。 ↩