The introduction of RFC 854, the Telnet protocol specification, states: "The purpose of the TELNET Protocol is to provide a general, bi-directional, eight-bit byte oriented communications facility. Its primary goal is to facilitate the interfacing of terminal devices and terminal-oriented processes."
A Telnet session consists of two main parts:
The following Silk Performer Telnet script comes from a project in which a script was recorded and customized from a Telnet session with VT220 terminal emulation. The client (the terminal emulation software) is NT based and the server application resides on a Unix box.
Here is the first part of the Telnet session recording:
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);
There is only one readable string in the inline comments: VT220, the terminal type. This is a first hint at the semantics of this Telnet traffic; server and client are negotiating terminal type and various other session settings.
In terms of script customization, nothing needs to be changed here. Silk Performer virtual users should negotiate session settings exactly as the terminal emulation software has done here.
Nevertheless, an analysis of the record.log file, along with the information from RFC 854 (the Telnet protocol specification), reveals how the conversation was achieved. The first part of the log is translated into TELNET codes in the following table:
Server-to-Client | Client-to-Server |
---|---|
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) | |
… | … |
Each command begins with the "Interpret as Command" (IAC) escape character 0xFF. Server and client agree on a terminal type (VT220) and a number of other session settings. Telnet session settings that can be negotiated between client and server include terminal speed, echo, "suppress go ahead," window size, remote flow control, and more.
The second part of the script contains the user interaction. The script generated from the recording session looks like this:
// … // 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); // …
As you can see from the inline comments, this part of the script contains a login process (account name and password), followed by the selection of an item from a main menu (by hitting the 1 and <RETURN> keys).
If you leave a recorded script unchanged, it will typically play back without problems. However changes do have to be applied to scripts for data parameterization, and response verification.
Each keystroke is sent to the server as a single byte (without header or footer). The log file reveals that the server sends back the same byte as an echo:
WebTcpipSend(hWeb0, "s"); WebTcpipRecvExact(hWeb0, NULL, 1); TcpipServerToClient(#432, 1 bytes) { s }
Looking back at the recorded script, you'll find that the sending of the password, as opposed to the sending of the user login name, doesn't trigger a sequence of echoes. This is because the password isn't supposed to appear on the terminal screen.
Note that the communication is full duplex. This means that both server and client can send simultaneously; they don't have to wait for each other. In the above example, you can see the result of this in the lines:
WebTcpipSend(hWeb0, "se"); WebTcpipRecvExact(hWeb0, NULL, 2);
Here, because of rapid typing during the recording session, the echo s came back only after the e keystroke had been sent to the server.
Once the password is sent, a number of WebTcpipRecvExact statements follow in the above code. In the recording's log file, one of these statements looks like this:
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 }
The server response contains plain text as well as meta information, such as text position and format (note that in German, "Hinweis: Kein Kunde gefunden" means "Message: No customer found").
In this Telnet example, determining when the server response is complete is challenging. First, the packet length is not included in the response data (Case I). Second, there is no termination byte sequence (Case II). Therefore this example represents Case III: No information on response packet size from section “Dynamically Receiving Server Responses”.
To solve this problem generically, a TelnetReceive-Response function that accepts incoming server responses of unspecified length in a loop is written. The loop is terminated when the client waits for a new response packet for more than a specified number of seconds. The corresponding function code is included later in this section.
Looking at the same part of the script after customization, the structure is more visible, and the server responses are handled by the new function. Note that complete strings can be sent to the server, as opposed to sending each key stroke as a single packet. You consequently don't have to wait for each echo character individually, because they can be read asynchronously (due to the full-duplex nature of the Telnet protocol) after sending the complete request.
// 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");
The TelnetReceiveResponse function eliminates the need to wait for incoming server data for appropriate periods of time. It takes three parameters:
hWeb0: Is the handle of the open TCP connection
fTimeout: Defines how to decide when the server response is complete: If after fTimeout seconds, no further server response is available, the function returns.
sAction: This string is used for appropriate naming of the custom timer, and can be used for logging and debugging purposes.
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;
The function works as follows: It waits until a server response is ready to be read in under fTimeout seconds (WebTcpipSelect). As soon as a server response is available, it is received and appended to the global string variable gsResponse. The loop terminates when the server response is empty, when the timeout is exceeded, or if WebTcpipRecv fails for any reason.
This loop structure is necessary because often a Telnet server will send a line of characters, nothing will happen for a couple of seconds, and then suddenly more lines come in.
Some care must be taken when looking at these time measurements. Because of the nature of the timeout, the time measurements usually include a final timeout period of fTimeout seconds. This has to be subtracted from the measured time to get the true roundtrip time measurement. This corrected time measurement is made available as a custom measurement with the name "RespTime: " + sAction and the dimension seconds.