Telnet プロトコル

RFC 854 の前書きで、Telnet プロトコル仕様は次のように定義されています。"TELNET プロトコルの目的は、汎用的で、双方向、8 ビットバイト志向の通信機器を提供することです。.主な目的は、ターミナル デバイスのインターフェイスやターミナル志向プロセスを利用することです。"

Telnet セッションは、2 つの主要部分から構成されます。

次の Silk Performer Telnet スクリプトは、VT220 ターミナル エミュレーションとの Telnet セッションから記録、カスタマイズされたスクリプトを含むプロジェクトからの抜粋です。クライアント (ターミナル エミュレーション ソフトウェア) は NT ベースで、サーバー アプリケーションは、Unix ボックスにあります。

パート I:接続を確立する

Telnet セッションの記録の最初の部分を示します。

WebTcpipConnect(hWeb0, "myserver", 23);

WebTcpipRecvExact(hWeb0, NULL, 3);
WebTcpipSendBin(hWeb0, "\hFFFC24", 3); // ·ü$

WebTcpipRecvExact(hWeb0, NULL, 3);
WebTcpipSendBin(hWeb0, "\hFFFB18", 3); // ·û·

WebTcpipRecvExact(hWeb0, NULL, 6);
WebTcpipSendBin(hWeb0, "\hFFFA18005654323230FFF0", 11);
                                   // ·ú··VT220·ð

WebTcpipRecvExact(hWeb0, NULL, 3);
WebTcpipSendBin(hWeb0, "\hFFFC20", 3); // ·ü

WebTcpipRecvExact(hWeb0, NULL, 60);

インライン コメントに 1 つだけ読み取れる文字列があります: VT220、これはターミナルの種類です。これは Telnet トラフィックのセマンティクスを理解する最初の手がかりとなります。つまり、サーバーとクライアントはターミナルの種類とさまざまなその他のセッションの設定をネゴシエートします。

スクリプト カスタマイズの観点では、変更する必要はここではありません。Silk Performer 仮想ユーザーは、ターミナル エミュレーション ソフトウェアがここで行っていることと完全に同じようにセッション設定をネゴシエートすべきです。

それでもなお、RFC 854 (Telnet プロトコル仕様) の情報と共に record.log ファイルを分析すると、この対話がどのように行われたかが明らかになります。ログの最初の部分を TELNET コードに解釈したものを次の表に示します。

サーバーからクライアント クライアントからサーバー
FF FD 24(IAC DO ENVIRONMENT VARIABLES)
FF FC 24(IAC WON'T ENVIRONMENT VARIABLES)
FF FD 18(IAC DO TERMINAL-TYPE)
FF FB 18(IAC WILL TERMINAL-TYPE )
FF FA 18 01 FF F0(IAC SB TERMINAL-TYPE SEND IAC SE)
FF FA 18 00 56 54 32 32 30 FF F0(IAC SB TERMINAL-TYPE IS "VT220" IAC SE)
FF FD 20(IAC DO TERMINAL-SPEED)
FF FC 20(IAC WON'T TERMINAL-SPEED)

各コマンドは IAC (Interpret as Command: コマンドとして解釈) エスケープ文字 0xFF で始まります。サーバーとクライアントは、ターミナルの種類 (VT220) といくつかのほかのセッション設定に同意します。クライアントとサーバー間でネゴシエートできる Telnet セッション設定には、ターミナル速度、エコー、Go Ahead 抑止、ウィンドウ サイズ、リモート フロー制御などがあります。

パート II:ユーザー操作

スクリプトの 2 番目の部分は、ユーザー操作が含まれます。記録したセッションから生成されたスクリプトは次のようになります。

// …
    // login: send username  
    WebTcpipSend(hWeb0, "t");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "e");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "s");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "t");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "u");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "se");
    WebTcpipRecvExact(hWeb0, NULL, 2);
    WebTcpipSend(hWeb0, "r");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "\r");
    WebTcpipRecvExact(hWeb0, NULL, 2);
    WebTcpipRecvExact(hWeb0, NULL, 10);
    // login: send password
    WebTcpipSend(hWeb0, "password\r");
    WebTcpipRecvExact(hWeb0, NULL, 2);
    WebTcpipRecvExact(hWeb0, NULL, 230);
    WebTcpipRecvExact(hWeb0, NULL, 47);
    WebTcpipRecvExact(hWeb0, NULL, 58);
    WebTcpipRecvExact(hWeb0, NULL, 40);
    WebTcpipRecvExact(hWeb0, NULL, 594);
    WebTcpipRecvExact(hWeb0, NULL, 340);
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipRecvExact(hWeb0, NULL, 188);
    WebTcpipRecvExact(hWeb0, NULL, 147);
    // Choose "1" from main menu and hit RETURN
    ThinkTime(7.0);
    WebTcpipSend(hWeb0, "1");
    WebTcpipRecvExact(hWeb0, NULL, 1);
    WebTcpipSend(hWeb0, "\r");
    WebTcpipRecvExact(hWeb0, NULL, 2);
    WebTcpipRecvExact(hWeb0, NULL, 7);
    WebTcpipRecvExact(hWeb0, NULL, 21);
    WebTcpipRecvExact(hWeb0, NULL, 691);
    // …

インライン コメントで確認できるように、スクリプトのこの部分には、ログイン処理 (アカウント名とパスワード)、続いて、メイン メニューからの項目の選択 (1<RETURN> キーを押すことによる)が含まれます。

記録したスクリプトをそのまま変更しない場合、ほとんど問題なく再生することができます。しかし、データのパラメータ化やレスポンスの検証などをスクリプトに適用して変更する必要があります。

各キーストロークは、単一バイト (ヘッダーやフッターを持たない) としてサーバーに送信されます。ログ ファイルを見ると、サーバーが同じバイトをエコーして送り返していることがわかります。

WebTcpipSend(hWeb0, "s");
WebTcpipRecvExact(hWeb0, NULL, 1);
TcpipServerToClient(#432, 1 bytes)
{
  s
}

記録したスクリプトを遡って見ると、ユーザー ログイン名の送信とは異なり、パスワードの送信では、一連のエコーが行われていないことがわかります。これは、パスワードはターミナル画面に表示しないためです。

通信は全二重であることに注意してください。これは、サーバーとクライアントの両方が同時に送信でき、お互いに待機する必要がないことを意味します。上記の例では、次の行でこの結果を確認できます:

WebTcpipSend(hWeb0, "se");
WebTcpipRecvExact(hWeb0, NULL, 2);

ここで、セッションの記録中にすばやく入力したため、エコー s は、e キーストロークがサーバーに送信された後に返ります。

一旦パスワードが送信されると、いくつかの WebTcpipRecvExact が上のコードに続きます。記録のログ ファイルでは、これらのステートメントの 1 つは次のようになります。

TcpipServerToClientBin(#432, 59 bytes)
{
  00000000 [8;19H·[;7m+---  1B 5B 38 3B 31 39 48 1B 5B 3B 37 6D 2B 2D 2D 2D
  00000010 ---Hinweis: Kein 2D 2D 2D 48 69 6E 77 65 69 73 3A 20 4B 65 69 6E
  00000020 Kunde gefunden-  20 4B 75 6E 64 65 20 67 65 66 75 6E 64 65 6E 2D
  00000030 ------+·[m·      2D 2D 2D 2D 2D 2D 2B 1B 5B 6D 0A 
}

サーバー レスポンスには、テキスト位置や書式などのメタ情報とプレーン テキストが含まれます (ドイツ語の "Hinweis:Kein Kunde gefunden" は、"メッセージ:顧客が見つかりません" を意味します)。

この Telnet の例では、サーバー レスポンスが完了したことを特定することは困難です。まず、パケット長がレスポンス データに含まれていません (ケース I)。次に、終了バイト シーケンスがありません (ケース II)。よって、この例はケース III を表します:「サーバー レスポンスを動的に受信する」セクションからレスポンスのパケット サイズについての情報がありません。

注: 他の Telnet ベースのプロジェクトでは、サーバー レスポンス データに終端文字列があることもあります (たとえば、コマンド プロンプト文字など)。これはテスト対象アプリケーションに完全に依存します。

この問題を一般的に解決するには、長さ未指定の受信サーバー レスポンスを受け付ける TelnetReceive-Response 関数を書くことです。うーぷは、クライアントが新しいレスポンス パケットを指定した秒数以上待機するときに終了します。対応する関数コードは、このセクションの後で示します。

カスタマイズ後のスクリプトの同じ部分を見て見ると、構造がより明らかで、サーバー レスポンスが新しい関数によって処理されています。各キー ストロークが単一のパケットとして送信されるのとは対照的に、完全な文字列をサーバーに送信できることに注意してください。結果的に、各エコー文字を個々に待機する必要はなくなります。これは、Telnet プロトコルが全二重であるため、完全なリクエストを送信した後に、非同期に読み込むことができるためです。

// Login: Send Username
WebTcpipSend(hWeb0, "testuser\r");
TelnetReceiveResponse(hWeb0, 1.0, "Login: Username");

// Login: Send Password
WebTcpipSend(hWeb0, "password\r");
TelnetReceiveResponse(hWeb0, 5.0, "Login: Passwort");
    
// Choose "1" from main menu and hit RETURN
WebTcpipSend(hWeb0, "1\r");
TelnetReceiveResponse(hWeb0, 5.0, "Choose 1 from menu"); 

TelnetReceiveResponse 関数によって、適当な期間、受信サーバー データを待機する必要がなくなります。この関数は 3 つのパラメータを取ります。

hWeb0:開かれている TCP/IP 接続のハンドル

fTimeout:サーバー レスポンスが完了したことを特定する方法の定義fTimeout 秒後に、サーバー レスポンスがこれ以上利用可能でない場合、関数は戻ります。

sAction:この文字列は、カスタム タイマーの名前に使用され、ログやデバッグ目的にも使用されます。

function TelnetReceiveResponse(hWeb0: number;
                               fTimeout: float; 
                               sAction: string): number
var
  sData:      string(4096);
  nRecv:      number;   
  nRecvSum:   number; 
  fTime:      float;

begin   
  gsResponse := "";
  nRecvSum   := 0;
                                                      
  MeasureStart(sAction);
  
  while WebTcpipSelect(hWeb0, fTimeout) do
    if NOT WebTcpipRecv(hWeb0, sData, sizeof(sData), nRecv)
    then 
      exit; 
    end;
    if nRecv = 0 then 
      exit; 
    end;  
    SetMem(gsResponse, nRecvSum + 1, sData, nRecv);
    nRecvSum   := nRecvSum + nRecv;        
  end;    

  MeasureStop(sAction);    
  MeasureGet(sAction, MEASURE_TIMER_RESPONSETIME, 
             MEASURE_KIND_LAST, fTime);

  if fTime > fTimeout then
    fTime := fTime - fTimeout;
  end;
  MeasureIncFloat("RespTime: " + sAction, fTime, "sec", 
                  MEASURE_USAGE_TIMER);
    
  TelnetReceiveResponse := nRecvSum;
    
end TelnetReceiveResponse;

関数は次のように機能します。サーバー レスポンスが fTimeout 秒内で読み込み可能になるまで待機します (WebTcpipSelect)。サーバー レスポンスが利用可能になるとすぐに受信し、グローバル文字列変数 gsResponse に追加されます。サーバー レスポンスが空になったとき、タイムアウト値を超えたとき、またはWebTcpipRecv が何かの理由で失敗した場合に、ループは終了します。

Telnet サーバーがよく文字の行を送信し、数秒間何もせずに、突然さらに数行送ってくることがあるため、このループ構造は必要です。

これらの時間の測定値を見る際に、いくつかの注意が必要です。タイムアウトの本質として、時間の測定値には、fTimeout 秒のうちの最終的なタイムアウト時間が通常含まれるためです。これは、真の時間を測定するためには、測定時間から減算する必要があることを意味します。この訂正した時間の測定値は、"RespTime: " + sAction という名前のカスタム測定値として利用可能です。