Websocketを使用したGoでのWeb端末エミュレーターの作成

何を書きますか



前回の記事では、PHPで簡単なターミナルエミュレーターを作成しました。 今こそ、Webソケットについてもっと深刻なことを書くときだと思います。 Webソケットの操作に使用する言語は何ですか? Python ..? ルビー..? JavaScript ..? いや! Go 1が登場したので、書きましょう;)。 私はここで自分自身を繰り返さないで、コード全体を書かないようにします。 私の意見では、興味深い断片のみを提供します。



デモ



ターミナルエミュレータの動作見る機会を与えてくれたAleks_jaに感謝します(最新バージョンのWebソケット(Firefox 11や最新のChromeなど)が必要です)。 デーモンが100ミリ秒で起動する時間がない場合、初めて接続できない場合があります。最初にページを更新してみてください。







Webターミナルのソースコードはgithubで入手できます。 ( go build



)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 ...



はありません。



私が話さなかった瞬間



次の実装の詳細については話しませんでした。





Githubプロジェクト



興味がある人のために、githubへのリンクを繰り返します:

github.com/YuriyNasretdinov/WebTerm-記事で説明したターミナルエミュレーター

github.com/YuriyNasretdinov/pyte-JavaScriptでのselectel / pyteライブラリの実装(残念ながら開発者には受け入れられません)



All Articles