2023/09/19
最近のことだが、或る問題で HTTP の POST method を使う場面があったのであるが、動作に疑問を感じた。僕は HTTP サーバー Pegasus のコードを書き上げた当時は、 POST については、よく解っていたはずだが、最近は年のせいもあり、自信がない。そこでもう一度復習することとした。
HTTP POST と言うのはサーバーに対してデータを送るためのプロトコルである。通信プロトコルには Web で使われている HTTP が使われている。送るデータは、簡単なテキストから、画像ファイルのようなバイナリデータのファイルまで想定されている。以下では理解を確かなものにするために実験を行う。従ってクライアント側は HTTP 通信あるいは TCP 通信を行うソフトウェアを持つ必要がある。Web で使われるブラウザはそのようなソフトウェアの一つではあるが、行なっていることはブラックボックスに近いので、何だか変だなと感じたときに、追求しにくい。もっとシンプルなツールが欲しい。unix の世界には、TCP の通信ソフトとしては curl と nc (netcat) が名高い。これについては後で取り上げる。僕は Plan9 上に connect と名付けた通信ツールを作って持っている。シンプルの極みで、こういうのが一倍扱い易い。unix 上に Russ Cox の P9P (Plan9 Port) をインストールしていれば、そこに含まれている dial も良いであろう。
HTTP のバージョンは web 誕生以来、進化しているが、現在においても基本的なところは
この記事の unix とは Linux, FreeBSD, MacOS に代表される unix 系 OS のことである。Plan9 は 9front で動作が確認されている。
以下の実験に関係するコンピュータは次の3つである。
name | ip | OS | command prompt |
---|---|---|---|
hebe | 192.168.0.6 |
Plan9(9front) | hebe% |
maia | 192.168.0.3 |
Linux(Ubuntu) | ubu$ |
mbook | 192.168.0.249 |
MacOS | mbook$ |
実験で確認しているのは
Browser を使った実験は MacBook から行っている。
name | version |
---|---|
Safari | 12.1.2 |
Chrome | 103.0.5060.134 |
Firefox | 115.0.3 |
/rc/bin/service
の中に置かれた実行プログラム tcp8083 が処理する仕組みになっている。
unix では Listener として xinetd がよく使われる。しかし幾分複雑である。
xinetd が動いていることは次のように確認できる:
ubu$ ps ax|grep xinet 1147587 ? Ss 0:00 /usr/sbin/xinetd -pidfile /run/xinetd.pid -stayalive -inetd_compat -inetd_ipv6 1158776 pts/3 S+ 0:00 grep xinet ubu$
xinetd を使ったサービスの提供は次の手順で行う:
1. port 8083 のサービス名称を xxx
とせよ。xxx
とポート番号との関係は /etc/services
に書いておく:
xxx 8083/tcp # for experiment
2. ファイル /etc/xinetd.d/xxx
を作成して、その内容を
service xxx { disable = no socket_type = stream protocol = tcp wait = no user = root server = path_xxx }とする。ここに
path_xxx
と書いたのはサービスを行うプログラムの path である。xxx
は tcp8083
に固定して(つまり Plan9 のルールに合わせている)service tcp8083 { disable = no socket_type = stream protocol = tcp wait = no user = root server = /usr/local/sbin/tcp8083 }としている。過度な柔軟性は管理を面倒にするだけだから。
Linstner によって起動されるサービスの書き方は(unix の場合も Plan9 の場合も)非常に簡単である。
サービス・プログラムへのリクエストは標準入力からやってくる。標準入力を読み取り、リクエストを吟味して回答を標準出力に書き出す。例えば Lua で書かれた次のプログラムはリクエストされた文字列に関する情報(文字列の長さと文字列自体)をクライアントにエコーバックする。そして終了したら "done" を書き出している。
#!/usr/bin/lua g = io.stdin print(os.date()) s = g:read("l") while s and #s > 0 do print(#s,s) s = g:read("l") end print("done")
譜1: unix の tcp8083
chmod 775 tcp8083
/usr/bin/lua
は Lua の置き場所を表しており、OSに依存する。unix 系の OS では、この場所はwhich lua
譜1の
s = g:read("l")
g
から1行を読み出し、結果を s
に格納する。"l"
(エル) は line を意味する。行末のコード('\n'
) は含まれない。"#s
" は s
のサイズである。空行では "#s
" は 0 である。ファイルの末尾で読み取ると "s
" は nil
であり、その場合 #s
はエラーとなる。Bio
に1行を読み取る関数 Brdline()
があるが、読み取るべき行に、行末記号 ('\n'
) を含めてしまったがために、バグの原因となっており、現在では「使うな」扱いとなっている。
Lua に不慣れな読者のために、Lua の基本的な入出力を含むプログラムを載せておく。
http:a7.lua
使い方と動作はプログラム中にコメントされている。
a7.lua
は Plain text なので、ブラウザにテキストとして表示されると期待しているのだが、
どうした訳か Safari だとバイナリデータと同じ扱いになり、ダウンロードされてしまう。
どうして Apple はこんな簡単な所を間違うのだろうね。Chrome や Firefox は期待通りの振る舞いをする。
以上のコメントを頭に入れて、まずは譜1の tcp8083
の動作を確認する。tcp8083
の置き場所に移動して
ubu$ tcp8083 Sat Sep 16 07:22:13 2023 abc 3 abc defg 4 defg done ubu$
図1: tcp8083
の実行例
この場合、実行の終了は、普通のプログラムと同様に、 EOF
(ctl-D
) でプログラム側に伝えられる。
"ctl-D
" が働いていることと "done
" が表示されたことを確認する。
譜1の tcp8083
の設計目標は、通信ができていることを確認することにある。類似のサービスとして tcp7
があり、これは echo サービスとして /etc/service
に登録されているはずである。この内容は Plan9 の場合には
#!/rc /bin/cat
#!/bin/sh
に置き換わっているだけのはずである。tcp7
の echo
はあまりにも素っ気ない。 tcp7
と違って tcp8083
はアクセスするとまず時刻を表示している。それから出力に余分な情報が含まれている。
telnet を使ってアクセスすると次のようになる:
ubu$ telnet 192.168.0.6 8083 Trying 192.168.0.6... Connected to 192.168.0.6. Escape character is '^]'. Fri Sep 15 08:59:13 2023 abc 4 abc defg 5 defg ...
図2: telnet による tcp8083
の実行例
図1と図2を比較してみると、後者の方が #s
の値が1つ大きい。これは unix における行概念と tcp 通信おける行概念が異なることから発生している。unx ではテキストの行は "\n
" で終わる。他方 tcp 通信では行は "\r\n
" で終わる。unix あるいは unix like な OS の Lua は、行は"\n
" で終わると考えている。そこへ行が "\r\n
" で終わると考えるデータがやってきた結果を見ているのである。ここで述べたことは Plan9 で見ればよく判る:
Plan9 は制御コードを表示するフォントを持っているのである。
つまり譜1のプログラムは図1のように直接実行するのと、図2のように tcp 通信を通すのとでは動作が異なることが解る。直接実行では空行を入力すれば終わる。tcp 通信ではクライアントが空行を入力しても終わらない。この問題は重要なので後で改めて採り上げる。当面はサービス・プログラムの仕事に必要な能力をはっきりさせる。
サービス・プログラムが Listener によって起動されない場合にはプログラム作りは非常に難しくなる。httpd のように非常に頻繁にアクセスされるサービス・プログラムは Listener を介さないで動くようにできている。従って Listener が暗黙に行っていた仕事を自分で実行しなければならない。アクセス頻度の小さいサービスは Listener を通じた方が利口である。
Plan9 の場合には、サービスのために開かれている窓口は netstat コマンドで簡単に判る。unix でも昔は同様に netstat コマンドで判定できたのではないかと思える。しかし現在の unix の netstat コマンドでは、実行状態にあるサービスは判るが、潜在的な窓口は簡単には判らない。システム管理者にとっては憂慮すべき状態である。(何を売っているのか判断しにくい経営者のようなもの!)
tcp8083
にアクセス中のクライアントが存在していなければ、netstat コマンドは tcp8083 を表示しない。しかしこのポートはサービスされているのである。例えば mbook からアクセスすると
ubu$ netstat -t | grep 8083 tcp 0 0 maia:tcp8083 mbook.local:64930 ESTABLISHED ubu$と表示される。Plan9 の netstat は接客中の port だけではなく、開かれている port をすべて表示する。
xinetd
のサービスに変更が発生したらsudo kill -HUP PID
PID
は xinetd
のプロセス ID である。xinetd
は変更を読みに行かない。Plan9 はこの必要はない。
HTTP の通信プロトコルにおいて、クライアントがサーバーに送る最初のメッセージの内容を実験的に知ることがここでの目標である。以下で活躍するサービス・プログラム tcp8084
を次に示す。できるだけ簡単な方法でクライアントから送られたメッセージをファイルに記録している。
#!/bin/lua -- look rfc1945 for HTTP/1.0 -- look rfc2616 for HTTP/1.1 f = io.open("/sys/log/tcp8084","a") f:write("=== "..os.date().." ==\n") -- the simplest one s = io.stdin:read("a") if s then f:write(s) print("HTTP/1.1 200 OK\r") -- fake response print("Connection: close\r") -- maybe required end
譜2: tcp8084
(Plan9 版)
これは Plan9 のプログラムであるが、譜1の tcp8083
を参考にすれば unix に移植するのはもはや容易なはずである。
("/bin/lua
" を "/usr/bin/lua
" に、 "/sys/log/tcp8084
" を "/var/log/tcp8040
" に変更すればよい。)
譜1との大きな違いは、input をエコーバックしないで、そのままファイルに記録している点である。
HTTP 通信において、エコーバックはクライアントを混乱させる。
stdin
をすべて読み終えたら何かやることになっているが、アクセスしたクライアントが動いていめ間は読み終えたことにはならない。人手によるクライアントの終了が要求される。このことは譜2の重大な欠陥である。サービス・プログラムが終了すればクライアントも自動終了するのだが、しかしどうやって...
GET method は簡単なのでここから出発する。この中にも重要な論点が存在する。
mbook の Safari から http://192.168.0.6:8084/
にアクセス。
log/tcp8084
は
=== Fri Sep 15 14:33:34 2023 == GET / HTTP/1.1 Host: 192.168.0.6:8084 Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15 Accept-Language: ja-jp Accept-Encoding: gzip, deflate Connection: keep-alive
図3 HTTP GET
もしも url を
http://192.168.0.6:8084/path?query
GET /path?query HTTP/1.1
他のブラウザでも大差はない。アクセスまでの手順を telnet で表せば、次のようなものである。
telnet 192.168.0.6 8084 GET / HTTP/1.1 Host: 192.168.0.6:8084 ...
最後に空行が送られている。
GET
に続く空行までの行は request header と言い、各行は
request ":" value
POST を行ってくれるコマンドツールとして curl
があるが後回しにする。
HTTP の世界で何と言ってもよく使われている、従ってまたバグが少ないのはブラウザーである。
以下では我が MacBook で動いている3つのブラウザー(Safari, Chrome, Firefox)を POST の実験ツールとして利用する。
以前に僕のサーバの中に、HTTP POST の実験ページを公開していた。このページは現在も残っている:
http://ar.nyx.link/test/post.html
post.html のコードはブラウザの「ページのソース」を表示させれば手に入る。
ソースコードから飾りに関する部分を削除して、さらに "form action=
" の値を
"http://192.168.0.6:8084/post/"
post2.html
とする。
<!DOCTYPE html> <html lang="ja"><head> <meta charset="utf-8"> <title>POST Method Test</title> </head> <body> <h1>POST Method Test</h1> <h2 id="id.1.0.0">POST textarea</h2> <code>enctype</code> の指定は無いが<br> <div style="margin-left:2em;"> <pre>enctype="application/x-www-form-urlencoded" </pre> </div> と結果は同じ<br> <p> Push the SUBMIT button<br> <form action="http://192.168.0.6:8084/post/" method=POST target="other"> <input size=10 type="text" name="name" value="alice"> <input size=10 type="text" name="age" value="18"> <textarea name="DATA" rows="4" cols="40"> Please input some data </textarea> <input type=submit value="SUBMIT"> </form> <p> <h2 id="id.2.0.0">POST textarea</h2> <div style="margin-left:2em;"> <pre>enctype="application/x-www-form-urlencoded" </pre> </div> Push the SUBMIT button<br> <form action="http://192.168.0.6:8084/post/" method=POST enctype="application/x-www-form-urlencoded" target="other"> <input size=10 type="text" name="name" value="alice"> <input size=10 type="text" name="age" value="18"> <textarea name="DATA" rows="4" cols="40"> www-form-urlencoded Please input some data </textarea> <input type=submit value="SUBMIT"> </form> <p> <h2 id="id.3.0.0">POST file</h2> <div style="margin-left:2em;"> <pre>enctype="multipart/form-data" </pre> </div> <p> Push the SUBMIT button<br> <p> <form enctype="multipart/form-data" method="POST" action="http://192.168.0.6:8084/post/" target="result"> File to send. Please brows. <input type="file" name="file" size="60"><br> <input type=submit value="SUBMIT"> </form> </body> </html>
譜3 pot2.html
僕の場合には post2.html
の置き場所を
/Users/arisawa/tmp/
file:///Users/arisawa/tmp/post2.html
post2.html
を表示してくれる。
このまま submit
すると 192.168.0.6:8084
に POST してくれる。(POST を完了させるには人手の介入が必要で、止めてやらなくてはならない)
Safari の場合、結果は次のようになる。
=== Fri Sep 15 17:58:25 2023 == POST /post/ HTTP/1.1 Host: 192.168.0.6:8084 Content-Type: application/x-www-form-urlencoded Origin: null Connection: keep-alive Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15 Content-Length: 56 Accept-Language: ja-jp Accept-Encoding: gzip, deflate name=alice&age=18&DATA=Please+input%0D%0Asome+data%0D%0A
Content-Length: 56
となっている。この 56
は「name=alice&age=18&DATA=Please+input%0D%0Asome+data%0D%0A」
ファイルを POST する場合
を使う。送信ファイルを a3.txt
として、その内容を "abc\ndefg\n" とする。結果は次のとおりになる。
=== Sat Sep 16 15:12:17 2023 == POST /post/ HTTP/1.1 Host: 192.168.0.6:8084 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJgBjrLio8OOUkR8B Origin: null Connection: keep-alive Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15 Content-Length: 189 Accept-Language: ja-jp Accept-Encoding: gzip, deflate ------WebKitFormBoundaryJgBjrLio8OOUkR8B Content-Disposition: form-data; name="file"; filename="a3.txt" Content-Type: text/plain abc defg ------WebKitFormBoundaryJgBjrLio8OOUkR8B--
図4: ファイルの POST
Content-Length
の 189 は------WebKitFormBoundaryuqGiQCuwdnRuHGDm ... ------WebKitFormBoundaryuqGiQCuwdnRuHGDm--
\r\n
" も含む(ただしフアイルの内容の行末はそのまま)。(もともと binary ファイルの送信用に設計されているのだから当然)
HTTP POST では空行("\r\n
")が重要な役割を果たしていることが解る。
ここでは curl
を使ったファイルの POST 実験を行う。
この実験は図4に対応している。
ubu$ curl -F file=@a3.txt http://192.168.0.6:8084 === Sat Sep 16 18:43:12 2023 == POST / HTTP/1.1 Host: 192.168.0.6:8084 User-Agent: curl/7.81.0 Accept: */* Content-Length: 193 Content-Type: multipart/form-data; boundary=------------------------00bdd4ee245bdf30 --------------------------00bdd4ee245bdf30 Content-Disposition: form-data; name="file"; filename="a3.txt" Content-Type: text/plain abc defg --------------------------00bdd4ee245bdf30--
通信に関係するソフトウェアを作るときに、正しく作られているかを確認できる環境が必要になる。診断ツールは可能な限りシンプルで、行われていることが簡単に理解されなくてはならない。
curl はうんざりする。オプションの山でやりたいことを見つけるにも苦労する。このようなソフトが好きな人もいるのだから不思議である。gnu のツールはこのような傾向がある。
譜2の tcp8084
は、POST したクライアントが自動停止しない問題点があった。次の譜4は、この問題点を克服する HTTP POST サーバーである。
#!/bin/lua -- fake http server for post -- look rfc1945 for HTTP/1.0 -- look rfc2616 for HTTP/1.1 substr = string.sub match = string.match find = string.find function puts(f,s) f:write(s.."\n"); if f == io.stdout then f:flush() end end function gets(g) s = g:read("l") if s == nil then return nil end r = find(s,"\r") if r then s = substr(s,1,r-1) end return s end conlen = nil f = io.stdout f = io.open("/sys/log/tcp8080","a") puts(f,"=== "..os.date()) g = io.stdin s = gets(g) while s and #s > 0 do --print(s) -- DBG puts(f,s) --m = match(s,"^([%a-]+): *(%d+)") m = match(s,"^Content%-Length: *(%d+)") -- match returns only the first match (capture). -- if capture is absent, look manual. -- print("DBG m",type(m),m) if m then conlen = tonumber(m,10) end s = gets(g) end --print(conlen) -- DBG if conlen then s = g:read(conlen) --print(s) -- DBG f:write(s) print("HTTP/1.1 200 OK\n") -- need reasponse end
譜4: tcp8080
(Plan9 版)
HTTP の規則では、ヘッダ部の終わりを示す空行と、空行以降の有効なデータのサイズを表す
Content-Length
tcp8080
ではこのことが利用されている。なお tcp8080
にはもう一つの工夫がある。ヘッダ部(次に続く空行も含む)の解析では行末は "\r\n
" でも "\n
" だけでも、どちらでも構わないようになっていることである。これによって unix や Plan9 クライアントから利用しやすくなっている。
この方法は HTTP/1.1(rfc2616) 推奨でもある:
The line terminator for message-header fields is the sequence CRLF.
However, we recommend that applications, when parsing such headers,
recognize a single LF as a line terminator and ignore the leading CR.
nc (netcat) は unix の通信ツールである。マニュアルを見る限り tcp や udp 通信の診断に使いやすい設計になっている。なかなか良さよう。
簡単な使い方の例
一つの window (A) で リスナーとして nc を起動する:
mbook$ nc -l 12345
-l
の l
は listener を意味している。また 12345
はポート番号である。(適当に)
他の window (B) から 12345
にアクセスし何か適当にインプットする:
mbook$ nc localhost 12345 abc
abc
ここで行われたことは1つのコンピュータの中での実験であるが、もちろん相手が他のコンピュータでもよい。例えば maia
の 8083
では譜1 の tcp8083
が動いているので
mbook$ nc -4 maia 8083 Sun Sep 17 14:51:02 2023 abc 3 abc defg 4 defg done mbook$ここに
-4
は IPv4 接続の指定である。
同様に
mbook$ echo abc | nc -4 maia 8083 Sun Sep 17 19:31:44 2023 3 abc done mbook$あるいは
mbook$ nc -4 maia 8083 <<EOF > abc > defg > EOF Sun Sep 17 19:35:19 2023 3 abc 4 defg done mbook$
残念なことにリスナーとして使えるようにしながら、プログラムを起動できる仕様にはなっていない。アクセスに応じてプログラムが起動されれば、使い方の幅が大きく広がったろうに...
nc は標準入力からのデータ入力をサポートしている。サポートしていると言うよりも、unix では普通にプログラムを書いていれば、そのようなる。その結果次のように、ホスト maia に対して curl からの POST であるかのように偽装できる。データ部が長くなるのでシェルスクリプトに書き込むが良い。
nc -4 maia 8080 <<EOF POST / HTTP/1.1 Host: 192.168.0.6:8080 User-Agent: curl/7.81.0 Accept: */* Content-Length: 193 Content-Type: multipart/form-data; boundary=------------------------00bdd4ee245bdf30 --------------------------00bdd4ee245bdf30 Content-Disposition: form-data; name="file"; filename="a3.txt" Content-Type: text/plain abc defg --------------------------00bdd4ee245bdf30-- EOF
nc は表立っては POST をサポートしていないが、実はなんとでもなるのである。nc の開発者は、こうしたこともよく考えて、使いやすいように設計していると思う。
dial
は P9P (Plan 9 Port) に含まれている Russ Cox の通信プログラムである。リスナーとしての機能はない。リスナーは既に P9P に含まれている。
彼は決してあれもこれもサポートしない。本当に必要な機能だけをサポートする。nc と同様な方法で POST もできる。
dial のコマンドシンタックスは
dial [-e] 'net!host!port'
net
は tcp
か udp
を選択する。host は相手方の IP アドレスあるいは名前、port はボート番号あるいは名前である。実行環境が Plan9 の rc であれば引用符('
) は不要である。-e
" フラグの趣旨である。
shell script を作ってその中に POST するデータを書くとよい。
tcp8083
に対してクライアントが自動終了するのは次のケース:
#!/bin/sh dial -e 'tcp!hebe!8083' <<EOF abc defg EOF
実行すると
Sun Sep 17 19:41:47 2023 3 abc 4 defg doneが帰ってくる。これは OK
譜1を見れば判るように、tcp8083
の終了条件は、空行の読み取りか、あるいは read エラーである。後者は読み取るデータが存在しないときに、すなわち通常は EOF 条件で発生する。
どちらの条件で終了しているのかをはっきりさせるために、譜の 譜5:
サーバーがメッセージを送っても、そのときにクライアントが終了していたら、もちろんクライアントはメッセージを受け取らない。プログラムを読んだだけではモヤモヤするだけではっきりしない。
Plan9 には、この問題の解決に使える実験用にデザインされているリスナー
ホスト hebe から
譜6の
回線を切断すれば "
譜7:
次の2つのメッセージはうざいなら削除してよい。
KILL シグナルは乱暴かも知れない。netcat はもう少し上品にやっているのではないかしら。僕はコードを読んでいないけど...
download:
tcp8083
を少し改めて
#!/bin/lua
substr = string.sub
match = string.match
find = string.find
g = io.stdin
f = io.stdout
-- f = io.stderr -- regular case is comment out
function puts(f,s)
f:write(s.."\n")
if f == io.stdout then
f:flush()
end
end
function gets(g)
s = g:read("l")
if s == nil then
return nil
end
r = find(s,"\r")
if r then
s = substr(s,1,r-1)
end
return s
end
print(os.date())
s = gets(g)
while s and #s > 0 do
print(#s,s)
s = gets(g)
end
if s then
puts(f,"empty line")
else
puts(f,"read error")
end
puts(f,"done")
tcp8085
tcp8085
が終了しないことを意味しているのか?
listen1
がある。
aux/listen1 tcp!*!8086 /rc/bin/service/tcp8085
tcp8085
のプログラムが hebe
の 8086
ポートにアクセスすることによって起動される。 tcp!*!8086
の中の *
は、「クライアントを問わないよ」と言っている。
tcp8085
の f
を(コメントを外して)
f = io.stderr -- regular case is comment out
tcp8085
の標準エラー (io.stderr
) への出力はクライアントには渡らないで、aux/listen1
を起動した hebe の window に表示される。従ってクライアントが終了しているか否かに関わらず、"empty line
" で終了したのか、それとも "read error
" で終了したのか決着が着く。
read error
" が発生する。fd
を通信回線のファイル記述子だとすれば close(fd)
で回線は切れる。クライアントプログラムが終了すれば OS によって自動的に close(fd)
は実行される。Russ はこのことをよく知っているので、切断処理を OS に任せたのである。(従って、クライアントからは監察できない)
call
次の call.c
は Russ の dial.c
を少し修正したものである。
Plan9 には通信ツールとして con
があるが、大きすぎる。そのために 僕は connect
を作った。con
を削りながら con
にできないこともやってくれるので重宝している。
Russ も大きすぎる con
を嫌ったのであろう。Russ は綺麗なコードを書く。僕の力任せの connect.c
とは大違いである。比べると恥ずかしくなるくらいである。そこでこの際、Russ のを元に新たに作ったのである。
変更したのは
の2点である。-t
フラグを追加 (connect
の -t
フラグ)
-e
フラグを廃止し、-a time
オプションに変更
-t
フラグは、接続直前に終了するモードである。オープンポートの確認に使う。
通信のクライアントは、普通は2つのプロセスから構成される。一つは、キー入力を読み取り、それをサーバに送るプロセス A。もう一つは、サーバーからのデータを読み取り、それを画面に表示するプロセス B。
プロセス A は ctl-D
で終了する。B も終了させるために、プロセス A は B にシグナルを送る。Russ のは KILL シグナル。これで B は終了する。ところがそれだとサーバーに送られたデータの処理が終わらないうちに B が終了し、困る。
この問題を Russ は -e
フラグで処理した。サーバーの終了処理を待つフラグである。ところがサーバに送ったデータが、サーバーが終了する構造を持っていないと、-e
フラグが機能しない。そのために相手待ちの終了しないプロセスが発生し得る。
call.c
では、待ち時間のリミットを持たせることにした。もっとも(注意深く書かれた)サーバのプログラムは同様に alarm
の下で動いているはずだから1 Russ のでも実際には間に合うけど...
#include <u.h>
#include <libc.h>
/*
* "call" is a Plan9 version of "dial" in P9P by Russ Cox.
* command named "dial" is alreay in 9front.
* so I avoided the name in porting to 9front.
*
* "-t" option is added which was in connect.c
* "-e" flag is discarded. instead "-a time" is introduced.
*
* compile: cc -o call call.c && cp call $home/bin/amd64
* -Kenar
*/
int atime = 0; /* alarm time */
int tflag = 0;
void
usage(void)
{
fprint(2, ("usage: call [-t] [-a time] addr\n"
"-t: test mode\n"
"-a time: alarm time to finish\n")
);
exits("usage");
}
void
killer(void *x, char *msg)
{
USED(x);
if(strcmp(msg, "kill") == 0)
exits(0);
noted(NDFLT);
}
void
main(int argc, char **argv)
{
int fd, pid;
char buf[8192];
int n, waitforeof;
char devdir[NETPATHLEN];
char *lport = nil;
char *dest;
double ftime;
notify(killer);
waitforeof = 0;
ARGBEGIN{
case 'a':
ftime = atof(EARGF(usage()));
if(ftime > 0)
atime = 1000*ftime; /* time in msec */
break;
case 't':
tflag = 1; break;
default:
usage();
}ARGEND
if(argc != 1)
usage();
dest = argv[0];
if((fd = dial(dest, lport, devdir, nil)) < 0)
sysfatal("dial: %r");
if(tflag){
NetConnInfo *cinfo;
fprint(2, "connected to %s on %s\n", dest, devdir);
cinfo = getnetconninfo(nil, fd);
fprint(2, "remote %s\n", cinfo->raddr);
fprint(2, "local %s\n", cinfo->laddr);
exits(nil);
}
switch(pid = fork()){
case -1:
sysfatal("fork: %r");
case 0:
while((n = read(0, buf, sizeof buf)) > 0)
if(write(fd, buf, n) < 0)
break;
if(1) /* for experiment */
close(fd); /* disconnect. so, make the server read error */
if(!atime)
postnote(PNPROC, getppid(), "kill");
fprint(2,"child: exiting\n");
exits(nil);
}
fprint(2,"parent: waiting\n");
alarm(atime);
while((n = read(fd, buf, sizeof buf)) > 0)
if(write(1, buf, n) < 0)
break;
fprint(2,"parent: done\n");
postnote(PNPROC, pid, "kill");
waitpid();
exits(0);
}
call.c
fprint(2,"child: exiting\n");
fprint(2,"parent: waiting\n");
call.c
は P9P の下では次のようにすれば簡単にインストールできる。
9c call.c
9l -o call call.o
cp call $HOME/bin
dial.c
と比較して、エラーの原因を除けばよい。
http://p9.nyx.link/netlib/cmd/call/call-1.0.tgz
注1: インターネットに公開されるサービスでは、アラームタイマーは必須と考えたほうが無難である。aux/listen1
を使ってアラームタイマーの下でサービス prog を起動するには
aux/listen1 tcp!*!port alarm time prog
time
は大抵は 1 (秒)で充分だろう。prog
は絶対パスが要求される。
あるいは prog
の方にアラームタイマーを仕込んであけば
aux/listen1 tcp!*!port prog
prog
は rc
スクリプトの場合には
#!/bin/rc
@{
sleep 1
ppid=`{cat /proc/$pid/ppid}
if(test -e /proc/$ppid)
echo kill >/proc/$ppid/ctl
}&
echo zzz ...
"echo zzz ..." のところにプログラムを書く。この方法は aux/listen
についても使える。
2023/09/23 追加