HTTPサーバを作る

きっかけ

最近、HTTPサーバを作っている。
と言っても、商用利用を考えているわけではなくて、学習目的だ。
何事も理解したいと思ったら自分で作ってみるのが一番だし、学習にはなるべく低レイヤーの方が良い。
明確にゴールを決めているわけではないけど、ひとまずGETメソッドでファイルコンテンツを返すところまで出来たので、区切りのエントリ。
f:id:nao_bamboo:20151005013024p:plain

プログラム

ソースはGitHubに置いてある。github.com

ので、細かい解説はしないが、ポイントだけ。

HTTPサーバと言っても、大きく2つのパートに分けられる。
1つはTCPコネクション周りで、ソース上はserver.cにあたる。
もう1つはHTTP周りの処理で、ソース上はhttp.cにあたる。

TCPパートは、コネクションに関する、socket -> bind -> listen -> acceptという定型的な処理があって、コネクションが成立した時のfork(並行サーバ)、コネクションが切断した時のシグナル処理なども含まれる。
この辺りの処理は、HTTPサーバに限らず、TCPによるコネクションを行うサーバであれば、ある程度共通になるだろう。

逆にHTTPパートは何をするかというと、TCPコネクション成立後のデータストリームの処理を行う。
HTTPだからと言って何か特別な処理をするわけではなくて、ソケットディスクリプタからreadしたデータ(HTTPリクエスト)に対して、HTTPプロトコルに則った処理を施して、ソケットディスクリプタにwrite (HTTPレスポンス) する処理を行う。
その意味では、HTTPとは正にプロトコル (規約) なんだなということを実感できる。

TCPについては以下の本が非常にためになった。
ピアソンエデュケーションでもう絶版かもしれないが。

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

  • 作者: W.リチャードスティーヴンス,W.Richard Stevens,篠田陽一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 1999/07
  • メディア: 単行本
  • 購入: 8人 クリック: 151回
  • この商品を含むブログ (37件) を見る

HTTPについては以下の本が参考になると思う。
WebAPI開発の時に利用していたが、HTTPサーバの開発にも役立つと思う。

Web API: The Good Parts

Web API: The Good Parts

HTTPクライアントには以下のアプリを利用している。

まとめ

まだ道半ばではあるものの、今回の開発を通じて得られた知識は非常に多い。
特にソケットを中心としたTCPに関する知識や、シグナルなどのLinuxカーネルに関する知識は、これまで曖昧だった部分も含めて理解を深めることができた。
また、Apacheやnginxが「きっとこう処理してるんだろうな」と想像できるようになったことは、当初の目的に適っている。
HTTP部分の処理についてはまだまだやるべきことがあるので、引き続き取り組んでいきたい。

Homebrewが壊れたら

はじめに

OS X のパッケージマネージャーといえば、もちろん Homebrew だ。
Homebrew — OS X用パッケージマネージャー

開発ツールの管理を非常に便利に行うことができる。

ところが、この Homebrew、ふとした拍子によく壊れる (気がする) 。
特に、OS のアップデートなどで動かなくなるといったケースが散見されるようだ。

今回も次のようなエラーが表示され動かなくなってしまった。

MacBookAir:homebrew admin$ brew update
/usr/local/bin/brew: /usr/local/src/homebrew/Library/brew.rb: /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby: bad interpreter: No such file or directory
/usr/local/bin/brew: line 21: /usr/local/src/homebrew/Library/brew.rb: Undefined error: 0

もちろん、brew.rbなんて弄った覚えはないのだが。

Homebrew の構成

Homebrew は通常、

/usr/local/src/homebrew

に必要なファイルがインストールされる。
実行ファイルである /usr/local/bin/brew も、この src/homebrew 以下のファイルへのシンボリックリンクとなっている。
ということは、この src/homebrew 以下のファイルを正しい状態へ戻してやれば直るはずである。

そして、src/homebrew は Git によってバージョン管理されている。

つまり、個別にファイルを修正する必要はなく、ファイルが壊れたなら git コマンドで HEAD に戻す、あるいはアップデートが必要なら remote から最新バージョンにしてやれば良いということになる。

直す

何はともあれ、まずは念のため git reset をかけておく。

MacBookAir:homebrew admin$ git reset --hard HEAD

次にリポジトリの情報を確認する。

MacBookAir:homebrew admin$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
autocrlf = false
[remote "origin"]
url = https://github.com/Homebrew/homebrew.git
fetch = +refs/heads/*:refs/remotes/origin/*

見ての通り、Githubリポジトリが origin として登録されている。
そこで、ここから pull を行う。

MacBookAir:homebrew admin$ git pulll origin master

詳細は割愛するが、大量の更新ファイルがあったようだ。

git log を確認すると、かなりの頻度で更新されているので、現状の OS に合わせたアップデートが必要だったようだ。

MacBookAir:homebrew admin$ tig
2015-09-24 21:04 BrewTestBot o [master] [origin/master] poppler: update 0.36.0 bottle.
2015-09-24 21:04 BrewTestBot o pdf2htmlex: update 0.13.6_3 bottle.
2015-09-24 21:04 BrewTestBot o diff-pdf: update 0.2_3 bottle.
2015-09-21 19:05 Ed Porras o poppler 0.36.0
2015-09-25 12:37 BrewTestBot o nghttp2: update 1.3.4 bottle.
2015-09-25 17:04 Guo Xiao o nghttp2 1.3.4
2015-09-25 13:09 Dominyk Tiller o lynx: update url
2015-09-25 05:08 BrewTestBot o miller: add 2.2.1 bottle.
2015-09-24 20:19 Dominyk Tiller o miller 2.2.1 (new formula)
2015-09-25 03:05 BrewTestBot o librsvg: update 2.40.10_1 bottle.
2015-09-25 03:05 BrewTestBot o gdk-pixbuf: update 2.32.0 bottle.
2015-09-24 20:20 Tom Schoonjans o gdk-pixbuf: relocations now optional, off by default
2015-09-22 14:00 Tom Schoonjans o gdk-pixbuf: disable relocatable library support
2015-09-21 18:46 Tom Schoonjans o gdk-pixbuf 2.32.0

冒頭の brew コマンドをもう一度打って、無事動作を確認することができた。
Homebrew が壊れたら、この方法で直すことができるはずだ。

MacBookAir:homebrew admin$ brew update
Already up-to-date.

nginxのリバースプロキシ設定

はじめに

nginxは、Webサーバであると同時にリバースプロキシとしても使用することができる。
例えば

  • 静的コンテンツはそのままnginxで返し、動的な処理はapacheへ回す
  • フロントにnginxを配置し、LoadBalancerとしてリクエストを各Webサーバへ振り分ける

といった使い方がされている。

今回、以下の方針でセットアップを行なっているので、備忘のためにconfを書き残す。

  1. 特定のサブドメイン宛のhttpリクエストはステータス444を返答
  2. 特定のサブドメイン宛のhttpリクエストはapacheへフォワード
  3. 特定のサブドメイン宛のhttpsリクエストはapacheへフォワード
  4. WebDav over SSLをnginxで運用

事前準備

まず、nginxとapacheがインストールされ、プロセスが起動しているものとする。
注意が必要なのは、nginx/apache共にポート80をデフォルトで使用するため、変更する必要があることだ。

nginxはリバースプロキシとしてポート80で待つ必要があるため、ここではapacheのポートを8080に変更する。
apacheのポート変更は、httpd.conf内の

Listen 80

Listen 8080

に変更する。

同様にSSLについてもポート443が競合するため、

Listen 443 https

Listen 8443 https

に変更しておく。

ただし、nginxからapacheへのフォワードについてはSSLを使う必要はないため、実際の転送はhttpとして8080で行う。

また、フォワードされる側のapache側でも予めバーチャルホストの設定が必要になる。

confの設定

1. httpリクエストに対しステータス444を返答
www宛に来たリクエストへはステータス444を返す設定とする。
また、default_serverに設定しているので、IPで直接アクセスされた場合にも同じレスポンスが返る。

server {
  listen 80 default_server;
  server_name www.hogehoge.net;
  return 444;
}

実際にアクセスされると以下のログが残る。

192.168.0.245 - - [17/Sep/2015:15:46:05 +0900] "GET / HTTP/1.1" 444 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36" "-"

2. httpリクエストをapacheへフォワード
apache.hogehoge.net宛に来たhttpリクエストはapacheへフォワードする。

server {
  listen 80;
  server_name apache.hogehoge.net;
  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_redirect                        off;
    proxy_set_header HOST                 $host;
    proxy_set_header X-Real-IP            $remote_addr;
    proxy_set_header X-Forwarded-Host     $host;
    proxy_set_header X-Forwarded-Server   $host;
    proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
  }
}

ログを見るとステータス200を返している。

/var/log/nginx/access.log

192.168.0.245 - - [17/Sep/2015:15:53:32 +0900] "GET / HTTP/1.1" 200 13296 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36" "-"

/var/log/httpd/access.log

127.0.0.1 - - [17/Sep/2015:15:53:32 +0900] "GET / HTTP/1.0" 200 13268 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36"

3. httpsリクエストをapacheへフォワード
SSLを使ったアクセスについても同様にapacheへフォワードができる。

server {
  listen 443 ssl;
  server_name secure.hogehoge.net;
  ssl on;
  ssl_certificate /etc/pki/tls/certs/secure.crt;
  ssl_certificate_key /etc/pki/tls/certs/secure.key;
  ssl_ciphers RC4-SHA:HIGH:!ADH;
  ssl_prefer_server_ciphers on;
  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_redirect                        off;
    proxy_set_header HOST                 $host;
    proxy_set_header X-Real-IP            $remote_addr;
    proxy_set_header X-Forwarded-Host     $host;
    proxy_set_header X-Forwarded-Server   $host;
    proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
  }
}

SSLに関する設定項目が増えている。
先述の通り、443で受け取ったhttpsリクエストについても8080でapacheへフォワードしている。

/var/log/nginx/access.log

192.168.0.245 - - [17/Sep/2015:16:01:22 +0900] "GET / HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36" "-"

/var/log/httpd/access.log

127.0.0.1 - - [17/Sep/2015:16:01:22 +0900] "GET / HTTP/1.0" 200 21 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36"

4. WebDav over SSLをnginxで運用
WebDavについてはSSL化した上で、nginxで運用を行う。

server {
  listen 443;
  server_name webdav.hogehoge.net;
  ssl on;
  ssl_certificate /etc/pki/tls/certs/webdav.crt;
  ssl_certificate_key /etc/pki/tls/certs/webdav.key;
  ssl_ciphers RC4-SHA:HIGH:!ADH;
  ssl_prefer_server_ciphers on;
  root /var/www/webdav;
  charset utf-8;
  location / {
    dav_methods PUT DELETE MKCOL COPY MOVE;
    dav_ext_methods PROPFIND OPTIONS;
    dav_access user:rw group:rw all:r;
    client_body_temp_path /tmp/nginx/webdav;
    create_full_put_path on;
  }
}

apacheへのフォワードを行わないのでproxy系の設定項目がなくなっている。
代わりに、dav系の設定が増えている。
なお、client_body_temp_pathで一時ファイルの保存先を /tmp以下に設定しているが、一定期間アクセスがないと消えてしまうので、ここは変えたほうが良いかもしれない。

また、通常のSSLリクエストと証明書が異なるのは、無料サービスでワイルドカードSSL証明書が作れなかったため。

PHP5.6へZend APCuをインストールする

はじめに

PHPではオペコードのキャッシュに、ver5.4まではAPC (Alternative PHP Cache)、ver5.5からはOPcacheが採用されている。
OPcacheはAPCに比べパフォーマンス面での改善が図られているが、逆にAPCでは可能だった、コードキャッシュ以外の任意のデータのキャッシュが利用できなくなっている。
代替のキャッシュ機構としてmemcachedなどを使うこともできるが、APCuを使えば、これまでのキャッシュの仕組みをそのまま使うことができる。
名前の通り、APCuはAPCのユーザーキャッシュ部分のみを抜き出したモジュールとなる。

インストール方法

インストール環境

まず、今回の環境は以下の通り。

PECLのアップデート

APCuはPECLを使ってインストールする。
そのため、まずはPECLのアップデートを行う。

$ sudo pecl channel-update pecl.php.net
Updating channel "pecl.php.net"
Update of Channel "pecl.php.net" succeeded

APCuのインストール

PECLでインストールを行うと、以下の通りエラーが出て失敗する。

$ sudo pecl install APCu
Failed to download pecl/APCu within preferred state "stable", latest release is version 4.0.7, stability "beta", use "channel://pecl.php.net/APCu-4.0.7" to install
install failed

どうやら "stable" 版はなく、 "beta" 版でのインストールが必要になるようだ。
そこで、beta版のインストールを行う。

$ sudo pecl install APCu-beta
downloading apcu-4.0.7.tgz ...
Starting to download apcu-4.0.7.tgz (118,670 bytes)
..........................done: 118,670 bytes
43 source files, building

...(中略)

Build process completed successfully
Installing '/usr/lib64/php/modules/apcu.so'
Installing '/usr/include/php/ext/apcu/apc.h'
Installing '/usr/include/php/ext/apcu/apc_api.h'
Installing '/usr/include/php/ext/apcu/apc_cache_api.h'
Installing '/usr/include/php/ext/apcu/apc_lock_api.h'
Installing '/usr/include/php/ext/apcu/apc_pool_api.h'
Installing '/usr/include/php/ext/apcu/apc_sma_api.h'
Installing '/usr/include/php/ext/apcu/apc_bin_api.h'
Installing '/usr/include/php/ext/apcu/apc_serializer.h'
install ok: channel://pecl.php.net/apcu-4.0.7
configuration option "php_ini" is not set to php.ini location
You should add "extension=apcu.so" to php.ini

無事、インストールが行われた。

設定ファイルの追加

インストール時に表示されたメッセージに従い、.iniのスキャンディレクトリに設定ファイル apcu.ini を設置する。
スキャンディレクトリはphpinfoで確認できる。
f:id:nao_bamboo:20150914151054p:plain

apcu.iniの記述内容は以下の通り。

extension=apcu.so
apc.shm_size=32M

確認

phpinfoを見ると、インストールされたことが確認出来る。
f:id:nao_bamboo:20150914151153p:plain

Makefileでワイルドカードを使う方法

はじめに

最近、C言語でコードを書くことが多く、頻繁にmakeコマンドを使っている。
ただ、単純な設定だと、ファイルを追加するごとにMakefileにもファイル名を追記しなければならず、この操作が大変煩わしい。
そこで、Makefileの勉強を兼ねて、これを自動化する設定を考えてみた。

要件

要件は、

  • 実装ファイル (.c) を追加した時に自動でコンパイルの対象になること。
  • ソースファイルがあるディレクトリ (src) とは別の、出力用ディレクトリ (build) に実行ファイルが生成されること。
  • buildディレクトリではsrcのディレクトリ構造が維持された状態で、中間オブジェクトファイルが生成されること。

とする。

サンプルのディレクトリ構成

コンパイル前のソースツリーは以下の通り。

.
├── Makefile
├── include
│   └── common.h
└── src
    ├── lib
    │   ├── error.c
    │   ├── inet_pton.c
    │   ├── readline.c
    │   ├── signal.c.bk
    │   ├── wrapsock.c
    │   └── writen.c
    └── tcpserv.c

インクルードファイルはincludeに、ソースファイルはsrc以下に格納されている。
src直下にはメインファイルであるtcpserv.cがあり、src/libにはライブラリファイルが格納されている。

これをmakeコマンドでコンパイルした時、以下のディレクトリ構成になることを目指す。

.
├── Makefile
├── build
│   ├── src
│   │   ├── lib
│   │   │   ├── error.o
│   │   │   ├── inet_pton.o
│   │   │   ├── readline.o
│   │   │   ├── wrapsock.o
│   │   │   └── writen.o
│   │   └── tcpserv.o
│   └── tcpserv
├── include
│   └── common.h
└── src
    ├── lib
    │   ├── error.c
    │   ├── inet_pton.c
    │   ├── readline.c
    │   ├── signal.c.bk
    │   ├── wrapsock.c
    │   └── writen.c
    └── tcpserv.c

成果物出力用のbuildが作成され、その中にコンパイルされたオブジェクトファイル、実行ファイルが作成される。
build内にはsrcおよびsrc/libが作成され、元のsrcと同じ構成が維持される。

Makefile

最終的なMakefileは次のようになった。

PROGNAME := tcpserv
INCDIR := include
SRCDIR := src
LIBDIR := lib
OUTDIR := build
TARGET := $(OUTDIR)/$(PROGNAME)
SRCS := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/$(LIBDIR)/*.c)
OBJS := $(addprefix $(OUTDIR)/,$(patsubst %.c,%.o,$(SRCS)))
#$(warning $(OBJS))

CC = gcc
CFLAGS = -Wall -O2 -I $(INCDIR)

.PHONY: all clean
all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

$(OUTDIR)/%.o:%.c
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(CC) $(CFLAGS) -o $@ -c $<

clean:
	rm -rf $(OUTDIR)

各所説明

定義
PROGNAME := tcpserv
INCDIR := include
SRCDIR := src
LIBDIR := lib
OUTDIR := build
TARGET := $(OUTDIR)/$(PROGNAME)
SRCS := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/$(LIBDIR)/*.c)
OBJS := $(addprefix $(OUTDIR)/,$(patsubst %.c,%.o,$(SRCS)))

最初にファイルやディレクトリの定義を行っている。
代入演算子 ":=" は、コロンなしの "=" が必要時に代入されるのに対し、":=" は即時代入される。
通常は ":=" を使っておけば良さそうだ。

SRCSには必要なソースファイルを列挙して代入している。
ここで、wildcard関数を使うことによって、今回目的としている、src直下及びsrc/libに追加された ".c" ファイルを自動的にコンパイル対象としている。

OBJSには必要となるオブジェクトファイルを列挙して代入している。
必要なオブジェクトファイルとは、SRCSのソースファイルをコンパイルしてオブジェクト化した ".o" ファイルである。
そのため、patsubst関数を使って、SRCSのファイル名から ".c" を ".o" に置き換えることで定義ができる。
オブジェクトファイルが作成されるのは build ディレクトリ以下となるので、addprefix関数で $(OUTDIR) を接頭辞として連結している。

コメントアウトしているwarning関数は、makeコマンド実行時にログを出力するものである。Makefileデバッグ時には重宝する関数だ。

コンパイラの設定
CC = gcc
CFLAGS = -Wall -O2 -I $(INCDIR)

CCでは使用するコンパイラgccに設定している。
CLAGSはオプションの設定だ。
-I オプションを使用すると、ソースコード内で使用しているインクルードのディレクトリ設定ができる。

ダミーターゲットの設定
.PHONY: all clean
all: $(TARGET)

makeコマンドを引数なしで実行すると、最初に設定されているターゲットをコンパイルする。
今回はダミーターゲットとして all を設定しているため、今後ターゲットが増えた場合にも、all の依存ファイルを増やすことで対応ができる。
.PHONYを設定しているのは、カレントディレクトリに同名のファイルがあった場合にコンパイルできなくなることへの対策である。

実行ファイルのコンパイル
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

allをターゲットとしたコンパイルで、$(TARGET)が依存ファイルとして設定されているので、今度は $(TARGET) をターゲットとしたコンパイルが実行される。

ここでは "$" を使ったマクロを使用してコンパイル方法を指定している。
変数とマクロと展開すると以下のgcc実行コマンドとなる。

gcc -Wall -O2 -I include -o build/tcpserv build/src/tcpserv.o build/src/lib/error.o build/src/lib/inet_pton.o build/src/lib/readline.o build/src/lib/wrapsock.o build/src/lib/writen.o

なお、マクロは色々用意されているが、今回使ったマクロは以下3つである。

マクロ 説明
$@ ターゲット名
$^ 依存ファイルのリスト
$< 最初の依存ファイル
必要なオブジェクトファイルのコンパイル
$(OUTDIR)/%.o:%.c
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(CC) $(CFLAGS) -o $@ -c $<

$(TARGET)をターゲットとしたコンパイルで、$(OBJS)が依存ファイルとして設定されているので、今度はこの$(OBJS)に列挙されたオブジェクトファイルのコンパイルが実行される。

1行目の書き方が分かりづらいが、$(OUTDIR)%.o をターゲットとして、このファイルパスから$(OUTDIR)を取り除き、かつ拡張子を ".c" に変換したファイルを依存ファイルとしてコンパイルが実行されることになる。

2行目については、dirnameコマンドで各ターゲットファイル $@ のディレクトリ名を抽出し、このディレクトリの存在チェックを行った上で、なければ mkdir を実行している。

3行目は実際のコンパイルコマンドで、以下のコンパイルが実行される。

gcc -Wall -O2 -I include -o build/src/tcpserv.o -c src/tcpserv.c
gcc -Wall -O2 -I include -o build/src/lib/error.o -c src/lib/error.c
gcc -Wall -O2 -I include -o build/src/lib/inet_pton.o -c src/lib/inet_pton.c
gcc -Wall -O2 -I include -o build/src/lib/readline.o -c src/lib/readline.c
gcc -Wall -O2 -I include -o build/src/lib/wrapsock.o -c src/lib/wrapsock.c
gcc -Wall -O2 -I include -o build/src/lib/writen.o -c src/lib/writen.c

make cleanの設定
clean:
	rm -rf $(OUTDIR)

最後に make clean 実行時の内容を定義している。
今回成果物は全てbuildに生成されるので、このディレクトリごと消している。

まとめ

ひとまず要件は満たすことができた。
当初は色々戸惑ったものの、ターゲットのコンパイルに必要な依存ファイル、さらにその依存ファイルのコンパイルに必要な依存ファイルが順にコンパイルされていく、という仕組みをイメージすると、わかりやすい。
他にも便利な機能が用意されているので試していきたい。