何を書きますか
前回の記事では、PHPで簡単なターミナルエミュレーターを作成しました。 今こそ、Webソケットについてもっと深刻なことを書くときだと思います。 Webソケットの操作に使用する言語は何ですか? Python ..? ルビー..? JavaScript ..? いや! Go 1が登場したので、書きましょう;)。 私はここで自分自身を繰り返さないで、コード全体を書かないようにします。 私の意見では、興味深い断片のみを提供します。
デモ
ターミナルエミュレータの動作を見る機会を与えてくれたAleks_jaに感謝します(最新バージョンのWebソケット(Firefox 11や最新のChromeなど)が必要です)。 デーモンが100ミリ秒で起動する時間がない場合、初めて接続できない場合があります。最初にページを更新してみてください。
Webターミナルのソースコードはgithubで入手できます。 (
go build
)Webソケットデーモンを自分でコンパイルする必要があります-これは、ホストを「クラック」するのを好む人に対する小さな保護策です。
成分
そのため、次のものが必要です。
- Goコンパイラ1がインストールされている
- Websocketライブラリ(code.google.com/p/go.net/websocketを
go get code.google.com/p/go.net/websocket
) - 最新のWebsocket仕様をサポートするブラウザー(最新のFirefoxおよびChromeなど)
- PHPを備えたWebサーバー(デーモンを自動的に起動するため)
Webデーモンソケットの作成
デーモンでは、Webソケットサーバーと擬似端末エミュレーターが組み合わされます。 Goで擬似端末を使用するためのネイティブサポートがないという事実にもかかわらず、この言語はCと簡単に統合されるため、Cの対応する呼び出しを使用して擬似端末を直接操作します。
Webソケットを操作する
package main import ( "code.google.com/p/go.net/websocket" "http" "log" ) // - func PtyServer(ws *websocket.Conn) { // ws — *websocket.Conn, // Read() Write() / } func main() { http.Handle("/ws", websocket.Handler(PtyServer)) // "/ws" log.Fatal(http.ListenAndServe(":12345", nil)) // 12345 }
擬似端末で作業する
forkpty()とioctl()のバインディングを記述します(ioctl()で端末の「ウィンドウ」のサイズを変更します):Goは、Cとうまく統合できますが、pid_tとintが同じものであることを理解しませんC関数で可変数のパラメーターを操作する方法がわかりません。
package main /* #cgo LDFLAGS: -lutil #include <stdlib.h> #include <sys/ioctl.h> #... ... int goForkpty(int *amaster, struct winsize *winp) { return forkpty(amaster, NULL, NULL, winp); } int goChangeWinsz(int fd, struct winsize *winp) { return ioctl(fd, TIOCSWINSZ, winp); } */ import "C"
ハンドラーでは、これを使用します。
func PtyServer(ws *websocket.Conn) { cols, rows := 80, 24 // - int var winsz = new(C.struct_winsize) // "var name = ..." — winsz.ws_row = C.ushort(rows); // winsz.ws_col = C.ushort(cols); winsz.ws_xpixel = C.ushort(cols * 9); winsz.ws_ypixel = C.ushort(rows * 16); cpttyno := C.int(-1) pid := int(C.goForkpty(&cpttyno, winsz)) pttyno := int(cpttyno) // ... }
Webソケットと擬似端末間の通信
次に、たとえばbashを実行して、対応する記述子(pttyno)からの出力をWebソケットに送信する必要があります。逆も同様です。Webソケットから入力pttynoに入力を送信するのは簡単です。 この問題は、不完全なUTF-8シーケンスが疑似端末から届いたときに発生します。 擬似端末からはブロック(たとえば2 Kb)でのみ読み取ることができ、ブロックの終わりはUTF-8文字を2つの部分に「カット」できます。この「トリム」はブラウザーに送信しないでください。 この状況を正しく処理する小さなコードを次に示します。
for end = buflen - 1; end >= 0; end-- { // Go if utf8.RuneStart(buf[end]) { // ... ch, width := utf8.DecodeRune(buf[end:buflen]) if ch != utf8.RuneError { end += width } break } }
UTF-8文字(Goの用語-ルーン語)の先頭として機能するバッファー(buf)の最後のバイトを見つけて、この文字がそのままかどうかを確認する必要があります。 すべてが最後の文字で問題ない場合は、バッファーの「終了」を返します。それ以外の場合は、バッファーのサイズを縮小して、文字全体のみがそこに残るようにします。
疑似端末からの出力をブラウザーに表示します
最初は、JSLinuxを使用して出力を表示しましたが、その作成者はライブラリのコードの変更と配布を許可していません。そのため、 Selectelの仲間が書いたselectel / pyteライブラリを使用しましょう。 Javascriptで書き換えます :)! pythonからの移植は完璧ではありません。また、私はpythonの特別な専門家ではありませんが、その仕事はします-Midnight Commanderが起動して問題なく動作します。
ユーザー入力を受け入れる
ユーザー入力を受け入れるために、JSLinuxの作成者から一定量のコードを借りました。基本的な原理はここで説明されています 。 また、下の入力フィールドにテキスト(パスワードなど)を挿入する機能を追加し、F1-F12キーとAlt +(左/右矢印)のマッピングを追加しました。 判明したように、Fキーの入力文字の値は環境変数$ TERMに依存し、VT100のキーボード上になかったため、vt100にはまったく定義されていません:)。 出力にはpyteが使用されるため、$ TERM環境変数はlinuxと等しくなければなりません。したがって、この端末ではこれらのキーのマッピングを使用します。
「オンデマンド」でデーモンを起動します
最後の接続の1分後に終了するようにWebソケットデーモンを実装したため、端末でページを開くときにスクリプト自体がWebソケットデーモンを起動すると便利です。 このためのPHPコードは非常に簡単です。
<?php $PORT = 13923; // port terminal daemon will be run at system('exec nohup ./ws '.$PORT.' </dev/null >>ws.log 2>&1 &');
exec
わからない場合は説明します。これはUNIXシェルの特別な組み込みコマンドであり、シェルを強制的に呼び出されたプロセスに置き換えます。 つまり、「余分な」プロセス
sh -c ./ws ...
はありません。
私が話さなかった瞬間
次の実装の詳細については話しませんでした。
- クライアント通信プロトコルはウィンドウのサイズ変更をサポートするために少し複雑でした
が、ロシア文字の入力にバグが現れました - Webデーモンはパスワードで保護されており、起動時に自動的に生成され、接続時に使用されます
- bashrcを使用して、目的の端末設定を設定します
- サーバーにレンダリングがないため、バイトのみが送信されるため、デーモンはsshdに匹敵するサーバーをロードします(つまり、CPU負荷はゼロに近い)
- javascriptのpyte実装は非常に高速です。MidnightCommanderの起動時に目に見える遅延はありません。スループットは毎秒数千行のテキストです
- ブラウザウィンドウを閉じると、セッションは正しく終了します
- 1つのデーモンで多数のクライアントを問題なく同時に処理できます。
- <audio>タグとubuntuからの音の使用により、端末は「ビープ音」を鳴らすことができます:)
Githubプロジェクト
興味がある人のために、githubへのリンクを繰り返します:
github.com/YuriyNasretdinov/WebTerm-記事で説明したターミナルエミュレーター
github.com/YuriyNasretdinov/pyte-JavaScriptでのselectel / pyteライブラリの実装(残念ながら開発者には受け入れられません)