/*****************************************************************************/ /* wsb.c WebSocket test-bench and gremlin inducing client. This command-line program is designed to test and exercise the WASD WebSocket environment during development. It also reports on data transfered per second and so can be used to metric the raw throughput of a given WASD WebSocket environment and platform. This utility uses the client-specific functionality of the WSLIBCL.C library as well as the standard WebSockets support of WSLIB.C The complementary WebSocket server (script, application) is WS_BENCH.C. This utility is not intended to be a paradigm of WebSocket programming virtue. It is a pragmatic test-bench and while it might be preferable to have some independent, reference test-bench, back here in mid-2011 as the RFC begins to gel (again) there is precious little available. At least WASD will have consistent implementation quirks :-) The three primary functions implementing test behaviour are; ClientToServer() performing the client-end writes, ServerToClient() supplying the processing of reads from the server responses, and PushToServer() proving random-interval writes to the server. The WSB (and wsLIB) can only handle messages sizes up to 4GB (not the full 63 bit size permitted by WebSocket protocol). It is also better to send multiple smaller messages rather than one humungous one because the WSB allocates message memory before filling it appropriately (and allocating 4GB *might* take a relative while). ECHO TEST --------- The client utility simply generates frames of the specified length and content and type and sends them to the server. The server echoes these frames back to the client (first UTF-8 to ASCII decoding then ASCII to UTF-8 encoding text) where it is compared to what has been sent. Errors are reported. PUSH TEST --------- Both the client and send send unsolicited frames to each other at random intervals (from 50mS to 1600mS) containing the specified length and content type. The receiving entity then echos these back ina frame for comparison checking by the sender. PING/PONG TEST -------------- The /PING and /PONG qualifiers cause server and client agents to ping each other at random intervals during other test sequences. USAGE EXAMPLES -------------- $ WSB == "$WASD_EXE:WSB" $ WSB /DO=ECHO /NUMBER=1000 /CONCURRENT=50 /QBF /REPEAT=100 Sends one thousand requests, fifty at a time, sending "The quick brown fox ..." one hundred times to the server (UTF-8 encoded, though is really only 7 bit ASCII) each of which is decoded and then reencoded and sent it back where the client compares that echo. $ WSB /DO=ECHO /NUMBER=1000 /CONCURRENT=10 /BINARY /REPEAT=500 /BREAK Sends one thousand requests, ten at a time, sending randomly generated binary content to the server five hundred times, which echos it back and the client reporting any discrepancy. Connections are broken randomly during each transaction to test resiliancy of WASD and server script/library. $ WSB /DO=PUSH /NUMBER=100 /CONCURRENT=10 /REPEAT=500 /SIZE=64K /TEXT /PING Sends one hundred requests, ten at a time, each sending fifty mssages of sixty-four kilobytes, both the client and server each asynchronously sending randomly generated UTF-8 text content, at random intervals, which the other echos back and the server reporting any discrepancy. It generates asynchronous traffic in both directions. In addition ping frames are sent from client to server at random intervals and any non-pong response notified. PLATFORM THROUGHPUT METRIC -------------------------- Measure of raw message and byte throughput for a series of messages of various sizes can provide useful information on the underlying maximum messaging characteristics of a given WASD/VMS platform. Data traverses the full WSB -> TCP/IP -> WASD -> script/application -> WASD -> TCP/IP -> WSB communication and IPC pathways. Of course this may also be performed between autonomous VMS platforms in which case it also includes the physical network. $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=0 $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=16 $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=64 $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=256 $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=1024 $ WSB /DO=ECHO /THROUGHPUT /REPEAT=1000 /SIZE=65K The /THROUGHPUT qualifier sends WebSocket binary messages but DOES NOT fill those messages with any specific data (it remains as zeroes). This saves the overhead of buffer filling and allows more of the system's resources to be devoted to the transferring data. Of course the same test can be undertaken using /BINARY, /QBF and /TEXT data (with expected and observed reduced throughput metrics). QUALIFIERS ---------- /BINARY send random data as binary frames /BREAK[=] randomly break connection at one in (default 5%) /BRIEF brief statistics /CONCURRENT= number of concurrent runs (default 1) /COUNT= total number of individual requests (synonym for /NUMBER) /DYNAMIC dynamically allocated buffers /DO=ECHO echo various contents /DO=PING send a standalone PING to the server /DO=PONG send a standalone, unsolicited PONG to the server /DO=PUSH asynchronous push content in both directions /HEARTBEAT= add periodic ping to the mix at the specified seconds /MRS= allows the maximum record size of the socket to be specified /NUMBER= total number of individual requests (default 1) /PING add PING frames randomly to the test /PONG add unsolicited PONG and server PING/PONGs into the mix /PROXY= make a proxied WebSocket request /RANDOM= random events occur at one in (default 5%) /REPEAT= number of messages within the request (default 1) /SIZE= message size in bytes (can use 'G', 'M', or 'k') /QBF send 'quick brown fox' as text frames (7 bit ASCII) /[NO]STAGGER initial connections staggered one after the other (default) /TEXT send strings of the characters 0..255 as text frames (UTF-8) /THROUGHPUT send whatever's in the buffer as binary data COPYRIGHT --------- Copyright (C) 2011-2021 Mark G.Daniel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. VERSION HISTORY (update SOFTWAREVN as well!) --------------- 05-FEB-2012 MGD v1.1.0, guess what a proxied request might look like 25-JUN-2011 MGD v1.0.0, initial development (happy birthday Sis) */ /*****************************************************************************/ #define SOFTWAREVN "1.1.0" #define SOFTWARENM "WSB" #ifdef __ALPHA # define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN #endif #ifdef __ia64 # define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN #endif #ifdef __VAX # define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN #endif #ifdef __x86_64 # define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN #endif #define COPYRIGHT_DATES "2011-2020" #define COPYRIGHT_FULL \ "Copyright (C) 2011,2012 Mark G.Daniel\n\ This program, comes with ABSOLUTELY NO WARRANTY.\n\ This is free software, and you are welcome to redistribute it under the\n\ conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or later version.\n\ http://www.gnu.org/licenses/gpl.txt\n" /* standard C header files */ #include #include #include #include #include #include #include #include /* VMS related header files */ #include #include #include #include #include #include #include #include #include #include #include "wslib.h" #define BOOL int #define TRUE 1 #define FALSE 0 /* this definition is not available in older VMS versions */ #ifndef EFN$C_ENF # define EFN$C_ENF 128 /* Event No Flag (no stored state) */ #endif #ifndef SS$_FISH # define SS$_FISH 2928 /* for earlier VAXen */ #endif #define VMSok(x) ((x) & STS$M_SUCCESS) #define VMSnok(x) !(((x) & STS$M_SUCCESS)) #define DEFAULT_BUFFER_SIZE 80 #define DEFAULT_BREAK 20 /* one in twenty (~5%) */ #define DEFAULT_RANDOM 20 /* one in twenty (~5%) */ #define DEFAULT_READ_BUFFER_SIZE 16384 #define DEFAULT_SERVER_PORT 80 #define DEFAULT_ECHO_CHUNK 1024 #define MAX_CONNECTION_ERROR_COUNT 30 #define HEARTBEAT_AT 100 /* lib$cvtf_from_internal_time() complains about about a zero elapsed time! */ #define LIB$_DELTIMREQ 1410052 /* mainly to allow easy use of the __unaligned directive */ #define ULONGPTR __unaligned unsigned long* #define USHORTPTR __unaligned unsigned short* #define INT64PTR __unaligned __int64* #define EXIT_LINE(status) { printf ("[WSB:%d]", __LINE__); exit(status); } #define QUAD_TO_FLOAT(lptr) \ ((float)(((unsigned long*)lptr)[0]) + \ (float)(((unsigned long*)lptr)[1]) * 4294967295.0) struct ConnectionStruct { int ContentCount, ContentLength, ConnectNumber, CountDown, FrameHeaderLength, PayloadLength, PayloadCount, PingBufferCount, PushCount, PushTimerId, ReadBufferCount, ReadBufferSize, WebCloseHandshake, WebSocketVersion, WriteBufferCount, WriteBufferSize; unsigned short Channel; unsigned char FrameHeader [2+2+6]; char HttpHeader [256], PingBuffer [256], WebSocketUrl, WebSocketHost; char *ReadBufferPtr, *WriteBufferPtr; struct _iosb SocketIOsb, ReadIOsb, WriteIOsb; struct WsLibStruct *WsLibPtr; }; char Utility [] = "WSB"; unsigned long CvtTimeFloatSeconds = LIB$K_DELTA_SECONDS_F; BOOL Debug, CliBinaryFrame, CliBreak, CliBrief, CliDynamic, CliQbfFrame, CliTextFrame, CliPing, CliPong, CliStagger = TRUE, CliThroughPut, DoEcho, DoPing, DoPong, DoPush, DoShowHelp, DoShowVersion; int BreakRequestCount, BreakResponseCount, BreakRequestLast, BreakResponseLast, CliBufferSize = DEFAULT_BUFFER_SIZE, CliConcurrency = 1, CliCount = 1, CliHeartBeat = 0, CliSocketMrs = 0, CliNumber = 1, CliRandom = DEFAULT_RANDOM, ClientPingCount, ClientPongCount, ConcurrentCount, ConnectionCount, ConnectionErrorCount, EfnEnf = EFN$C_ENF, FailedRequestCount, HeartBeatAt = HEARTBEAT_AT, HttpProtocol = 11, HttpReadErrorCount, PongOkCount, ReadBufferSize, RequestCount, ResponseErrorCount, ServerPort, ServerPingCount, ServerPongCount, ServerPongFailCount, TotalBytesSent, WebSocketErrorCount, WebSocketProtocolVersion = 8; unsigned long ReadTotal [2], ReadMsgTotal [2], WriteTotal [2], WriteMsgTotal [2]; char *CliProxyServer, *RequestUriPtr; char CloseAdieu [128], CommandLine [256], ServerHost [256], ServerSoftware [256], RequestUri [256]; extern int WsLibClBreakCount, WsLibClBreakEvery; /* function prototypes */ void BinaryFill (char*, int); char* BytesPerSecond (float); char* BytesOf (float); void ClientToServer (struct WsLibStruct*); void CloseConnection (struct WsLibStruct*); void ConnectionAst (struct WsLibStruct*); void ConnectToServer (); float DurationSeconds (unsigned long*); void GetParameters (int, char**); void MessageCallback (struct WsLibStruct*); void ParseURL (); void PongCallback (struct WsLibStruct*); void PushEvent (struct ConnectionStruct*); void PushServer (struct ConnectionStruct*); void QbfFill (char*, int); BOOL RandomEvent (); void ServerReadAst (struct WsLibStruct*); void ServerToClient (struct WsLibStruct*); void ServerWriteAst (struct WsLibStruct*); void ShowHelp (); int strsame (char*, char*, int); void UpdateTotals (struct WsLibStruct*); void Utf8Fill (char*, int); /*****************************************************************************/ /* */ int main ( int argc, char *argv[] ) { static char SyiVersion [8]; static struct { short buf_len; short item; char *buf_addr; short *short_ret_len; } SyiItem [] = { { sizeof(SyiVersion), SYI$_VERSION, (char*)&SyiVersion, 0 }, { 0,0,0,0 } }; int cnt, status, VmsVersion; float BytesFloat = 0.0, ReadFloat, ReadMsgFloat, SecsFloat, WriteFloat, WriteMsgFloat; char *cptr, *sptr, *uptr, *zptr; /*********/ /* begin */ /*********/ if (getenv("WSB$DBUG")) Debug = TRUE; GetParameters (argc, argv); if (Debug) setenv ("WATCH_SCRIPT", "*", 1); if (DoShowVersion) { fprintf (stdout, "%%%s-I-VERSION, %s\n%s", Utility, SOFTWAREID, COPYRIGHT_FULL); exit (SS$_NORMAL); } if (DoShowHelp) ShowHelp (); sprintf (CloseAdieu, "%s bids adieu!", SOFTWAREID); if (VMSnok (status = sys$getsyi (0, 0, 0, &SyiItem, 0, 0, 0))) exit (status); VmsVersion = ((SyiVersion[1]-48) * 10) + (SyiVersion[3]-48); if (Debug) fprintf (stdout, "VmsVersion: %d\n", VmsVersion); if (VmsVersion < 70) EfnEnf = 0; /* at each count there is an opportunity so ... */ if (CliBreak) { CliBreak *= CliCount; WsLibClBreakEvery = CliBreak; } ParseURL (); uptr = RequestUri; if (cptr = CliProxyServer) { /* make it a proxied request */ uptr += sprintf (RequestUri, "http://%s:%d", ServerHost, ServerPort); /* change the connected-to host and port */ sptr = ServerHost; while (*cptr && *cptr != ':' && *cptr != '/') *sptr++ = *cptr++; *sptr = '\0'; if (*cptr++ == ':') if (isdigit(*cptr)) ServerPort = atoi(cptr); } if (DoEcho) { sprintf (uptr, "/cgiplus-bin/ws_bench?do=echo&size=%d%s%s", CliDynamic ? 0 : CliBufferSize, CliPing ? "&ping=20" : "", CliPong ? "&pong=20" : ""); RequestUriPtr = RequestUri; } else if (DoPush) { sprintf (uptr, "/cgiplus-bin/ws_bench?do=push&size=%d%s%s", CliDynamic ? 0 : CliBufferSize, CliPing ? "&ping=20" : "", CliPong ? "&pong=20" : ""); RequestUriPtr = RequestUri; } else if (DoPing || DoPong) { sprintf (uptr, "/cgiplus-bin/ws_bench?do=%s&size=%d", DoPing ? "ping" : "pong", CliDynamic ? 0 : CliBufferSize); RequestUriPtr = RequestUri; } else exit (SS$_NORMAL); /******/ /* go */ /******/ DurationSeconds (NULL); if (CliStagger) { /* connections to the server will be one after the other */ ConnectToServer (); } else { /* this throws the concurrent total at the server at once! */ sys$setast (0); for (cnt = 0; cnt < CliConcurrency; cnt++) { status = sys$dclast (&ConnectToServer, 0, 0); if (VMSnok (status)) EXIT_LINE (status); } sys$setast (1); } sys$hiber (); SecsFloat = DurationSeconds (NULL); /*********/ /* stats */ /*********/ fprintf (stdout, "%%%s-I-STATS, %d total connections\n", Utility, ConnectionCount); if (CliBreak) fprintf (stdout, "%d broken connections\n", WsLibClBreakCount); if (DoPing || CliPing) fprintf (stdout, "%d client pings, %d server pongs, %d server pings\n", ClientPingCount, ServerPongCount, ServerPingCount); if (CliBrief) exit (SS$_NORMAL); ReadFloat = QUAD_TO_FLOAT(&ReadTotal); ReadMsgFloat = QUAD_TO_FLOAT(&ReadMsgTotal); WriteFloat = QUAD_TO_FLOAT(&WriteTotal); WriteMsgFloat = QUAD_TO_FLOAT(&WriteMsgTotal); fprintf (stdout, "Duration: %.3f seconds\n", SecsFloat); fprintf (stdout, "Tx: %.0f msgs at %.0f/S, %s at %s\n", WriteMsgFloat, WriteMsgFloat/SecsFloat, BytesOf(WriteFloat), BytesPerSecond(WriteFloat/SecsFloat)); fprintf (stdout, "Rx: %.0f msgs at %.0f/S, %s at %s\n", ReadMsgFloat, ReadMsgFloat/SecsFloat, BytesOf(ReadFloat), BytesPerSecond(ReadFloat/SecsFloat)); fprintf (stdout, "Total: %.0f msgs at %.0f/S, %s at %s\n", WriteMsgFloat+ReadMsgFloat, (WriteMsgFloat+ReadMsgFloat)/SecsFloat, BytesOf(WriteFloat+ReadFloat), BytesPerSecond((WriteFloat+ReadFloat)/SecsFloat)); exit (SS$_NORMAL); } /*****************************************************************************/ /* Loop through the specified number of connections (or attempts thereof). Maintain, as best it can be, the specified number of simultaneous connections. If a connection attempt fails just quit there for that particular loop, leave a bit of time (as reads occur) to allow the server to recover. */ void ConnectToServer () { int status; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "ConnectToServer()\n"); if (ConnectionCount >= CliNumber) { /* reached the total number of requests */ if (!ConcurrentCount) sys$wake (0, 0); return; } ConnectionCount++; ConcurrentCount++; if (HeartBeatAt && !(ConnectionCount % HeartBeatAt)) fprintf (stdout, "Completed %d requests\n", ConnectionCount); /* init connection structure */ cxptr = calloc (1, sizeof(struct ConnectionStruct)); if (!cxptr) EXIT_LINE (vaxc$errno); cxptr->ConnectNumber = ConnectionCount; cxptr->CountDown = CliCount; cxptr->WriteBufferSize = CliBufferSize; /* allocate some buffer size even for a zero-sized payload */ cxptr->WriteBufferPtr = calloc (1, cxptr->WriteBufferSize+256); if (!cxptr->WriteBufferPtr) EXIT_LINE (vaxc$errno); cxptr->ReadBufferSize = cxptr->WriteBufferSize; cxptr->ReadBufferPtr = calloc (1, cxptr->ReadBufferSize); if (!cxptr->ReadBufferPtr) EXIT_LINE (vaxc$errno); cxptr->WsLibPtr = WsLibCreate (cxptr, &CloseConnection); if (CliSocketMrs) WsLibClSetSocketMrs (cxptr->WsLibPtr, CliSocketMrs); WsLibSetRoleClient (cxptr->WsLibPtr); WsLibSetMsgCallback (cxptr->WsLibPtr, &MessageCallback); /* maxiumum of five seconds without received data */ WsLibSetReadSecs (cxptr->WsLibPtr, 5); WsLibSetPongCallback (cxptr->WsLibPtr, &PongCallback); if (CliHeartBeat) WsLibSetPingSecs (cxptr->WsLibPtr, CliHeartBeat); status = WsLibClConnect (cxptr->WsLibPtr, ServerHost, ServerPort, RequestUri, &ConnectionAst); if (Debug) fprintf (stdout, "WsLibClConnect() %%X%08.08X\n", status); } /****************************************************************************/ /* Connection to server has been established (or not). */ void ConnectionAst (struct WsLibStruct *wsptr) { int status; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "ConnectionAst()\n"); cxptr = WsLibGetUserData (wsptr); if (VMSnok (wsptr->SocketIOsb.iosb$w_status)) { /* connect failed */ EXIT_LINE (wsptr->SocketIOsb.iosb$w_status); } if (CliStagger) { if (ConcurrentCount < CliConcurrency) { /* initiate the next serial connection */ status = sys$dclast (&ConnectToServer, 0, 0); if (VMSnok (status)) EXIT_LINE (status); } } /* begin to interact with the server (WebSocket) application */ ClientToServer (wsptr); } /****************************************************************************/ /* Request is concluded. */ void CloseConnection (struct WsLibStruct *wsptr) { int status; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "CloseConnection()\n"); cxptr = WsLibGetUserData (wsptr); if (cxptr->PushTimerId) sys$cantim (cxptr->PushTimerId, 0); UpdateTotals (wsptr); WsLibDestroy (wsptr); if (cxptr->WriteBufferPtr) free (cxptr->WriteBufferPtr); if (cxptr->ReadBufferPtr) free (cxptr->ReadBufferPtr); free (cxptr); if (ConcurrentCount) ConcurrentCount--; /* replace the just-closed connection (unless limits reached) */ ConnectToServer (); } /****************************************************************************/ /* Write a frame from the client to the server (WS_BENCH.C) which does the complementary processing and writes the result(s) back, this being processed in ServerToClient() below. */ void ClientToServer (struct WsLibStruct *wsptr) { int status; struct ConnectionStruct *cxptr; struct WsLibFrmStruct *frmptr; struct WsLibMsgStruct *msgptr; /*********/ /* begin */ /*********/ cxptr = WsLibGetUserData (wsptr); if (Debug) fprintf (stdout, "ClientToServer() %d\n", cxptr->CountDown); if (WsLibIsClosed (wsptr)) return; if (VMSnok (status = WsLibClSocketStatus (wsptr))) { cxptr->CountDown = 0; fprintf (stdout, "%%%s-W-CONNECT, [%d] #%d failed (%%X%08.08X)\n", Utility, __LINE__, cxptr->ConnectNumber, status); } if (WsLibClBreakNow (wsptr)) return; if (!cxptr->CountDown) { WsLibClose (wsptr, WSLIB_CLOSE_NORMAL, CloseAdieu); return; } cxptr->WriteBufferCount = CliBufferSize; if ((DoPing || DoPong) || ((CliPing || CliPong) && RandomEvent())) { if (cxptr->WriteBufferSize > sizeof(cxptr->PingBuffer)) cxptr->PingBufferCount = sizeof(cxptr->PingBuffer); else cxptr->PingBufferCount = cxptr->WriteBufferSize; if (CliBinaryFrame) BinaryFill (cxptr->PingBuffer, cxptr->PingBufferCount); else if (CliTextFrame) Utf8Fill (cxptr->PingBuffer, cxptr->PingBufferCount); else if (CliQbfFrame) QbfFill (cxptr->PingBuffer, cxptr->PingBufferCount); /* else /throughput is in the buffer! */ if (DoPing || CliPing) WsLibPing (wsptr, cxptr->PingBuffer, cxptr->PingBufferCount); /* expect to see this at server end but without any response to client */ if (DoPong || CliPong) WsLibPong (wsptr, cxptr->PingBuffer, cxptr->PingBufferCount); if (DoPing || CliPing) ClientPingCount++; else ClientPongCount++; } else if (DoEcho) { if (CliBinaryFrame) { WsLibSetBinary (wsptr); BinaryFill (cxptr->WriteBufferPtr, cxptr->WriteBufferCount); } else if (CliTextFrame) { /* at server UTF-8 will be converted to ASCII and then back again */ WsLibSetUtf8 (wsptr); Utf8Fill (cxptr->WriteBufferPtr, cxptr->WriteBufferCount); } else if (CliQbfFrame) { /* brown foxes are only 7 bit ASCII */ WsLibSetUtf8 (wsptr); QbfFill (cxptr->WriteBufferPtr, cxptr->WriteBufferCount); } /* else /throughput is in the buffer! */ WsLibWrite (wsptr, cxptr->WriteBufferPtr, cxptr->WriteBufferCount, ServerWriteAst); } else if (DoPush) { /* if the first call then initiate server push */ if (!cxptr->PushTimerId) PushServer (cxptr); } /* read next frame */ WsLibRead (wsptr, cxptr->ReadBufferPtr, cxptr->ReadBufferSize, ServerReadAst); WsLibClBreakNow (wsptr); } /****************************************************************************/ /* The server (WS_BENCH.C) has processed what was sent by ClientToServer() and sent it back to the client for checking. When processed sent next to server. */ void ServerToClient (struct WsLibStruct *wsptr) { int ClientCount, ServerCount; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ cxptr = WsLibGetUserData (wsptr); if (Debug) fprintf (stdout, "ServerToClient() %d\n", cxptr->CountDown); if (WsLibIsClosed (wsptr)) return; if (!cxptr->CountDown--) { WsLibClose (wsptr, WSLIB_CLOSE_NORMAL, CloseAdieu); return; } if (WsLibClBreakNow (wsptr)) return; cxptr->ReadBufferCount = WsLibReadCount (wsptr); if (DoEcho) { if ((CliBinaryFrame && !WsLibReadIsBinary(wsptr)) || (CliTextFrame && !WsLibReadIsText(wsptr)) || (CliQbfFrame && !WsLibReadIsText(wsptr)) || (CliThroughPut && !WsLibReadIsBinary(wsptr)) || cxptr->WriteBufferCount != WsLibReadCount(wsptr) || memcmp (cxptr->ReadBufferPtr, cxptr->WriteBufferPtr, cxptr->WriteBufferCount)) { fprintf (stdout, "%%%s-W-ECHO, [%d] #%d data mismatch\n", Utility, __LINE__, cxptr->ConnectNumber); goto ServerToClientShutdown; } } else if (DoPush) { if ((CliBinaryFrame && !WsLibReadIsBinary(wsptr)) || (CliTextFrame && !WsLibReadIsText(wsptr)) || (CliQbfFrame && !WsLibReadIsText(wsptr)) || (CliThroughPut && !WsLibReadIsBinary(wsptr)) || cxptr->ReadBufferCount != cxptr->WriteBufferCount) { fprintf (stdout, "%%%s-W-PUSH, [%d] #%d data mismatch (%s)\n", Utility, __LINE__, cxptr->ConnectNumber, cxptr->WriteBufferPtr); goto ServerToClientShutdown; } if (memcmp (cxptr->ReadBufferPtr+24, cxptr->WriteBufferPtr+24, cxptr->WriteBufferCount-24)) { /* but ignore slightly stale data */ sscanf (cxptr->ReadBufferPtr, "C:%d S:%d", &ClientCount, &ServerCount); if (ClientCount+1 != cxptr->PushCount) { fprintf (stdout, "%%%s-W-PUSH, [%d] #%d data mismatch |%s|%s|\n", Utility, __LINE__, cxptr->ConnectNumber, cxptr->WriteBufferPtr, cxptr->ReadBufferPtr); goto ServerToClientShutdown; } } } WsLibClBreakNow (wsptr); ClientToServer (wsptr); return; ServerToClientShutdown: FailedRequestCount++; WsLibClose (wsptr, WSLIB_CLOSE_NORMAL, CloseAdieu); return; } /*****************************************************************************/ /* Push content to the server at pseudo-random intervals between approximately 100mS and 3200mS. Client can change content at any time. */ void PushServer (struct ConnectionStruct *cxptr) { static int RandomNumber, RandomFiller; int len, status; unsigned long DeltaTime [2]; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "PushServer() %d\n", cxptr); if (WsLibIsClosed (cxptr->WsLibPtr)) return; cxptr->PushTimerId = (int)cxptr; if (DoPush) { /* change the pushed content (first 24 bytes are reserved) */ memset (cxptr->WriteBufferPtr, '_', 24); /* update number from client */ len = sprintf (cxptr->WriteBufferPtr, "C:%d", ++cxptr->PushCount); cxptr->WriteBufferPtr[len] = '_'; /* fill the remainder with "data" */ if (CliQbfFrame) { QbfFill (cxptr->WriteBufferPtr+24, cxptr->WriteBufferCount-24); WsLibSetAscii (cxptr->WsLibPtr); } else if (CliTextFrame) { Utf8Fill (cxptr->WriteBufferPtr+24, cxptr->WriteBufferCount-24); WsLibSetUtf8 (cxptr->WsLibPtr); } else { BinaryFill (cxptr->WriteBufferPtr+24, cxptr->WriteBufferCount-24); WsLibSetBinary (cxptr->WsLibPtr); } WsLibWrite (cxptr->WsLibPtr, cxptr->WriteBufferPtr, cxptr->WriteBufferCount, NULL); } /* 100mS to 3200ms */ if (!RandomNumber) sys$gettim (&RandomNumber); RandomNumber = RandomNumber * 69069 + 1; DeltaTime[0] = -1000000 * ((RandomNumber & 31) + 1); DeltaTime[1] = -1; status = sys$setimr (0, &DeltaTime, PushServer, cxptr, 0); if (VMSnok(status)) EXIT_LINE (status); } /****************************************************************************/ /* */ void ServerReadAst (struct WsLibStruct *wsptr) { int status; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "ServerReadAst()\n"); if (VMSnok (status = WsLibReadStatus(wsptr))) { /* WEBSOCKET_INPUT read error (can be EOF) */ if (Debug) fprintf (stdout, "%%X%08.08X\n", status); WsLibClose (wsptr, WSLIB_CLOSE_BANG, NULL); return; } cxptr = WsLibGetUserData (wsptr); ServerToClient (wsptr); } /****************************************************************************/ /* */ void ServerWriteAst (struct WsLibStruct *wsptr) { /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "ServerWriteAst()\n"); } /****************************************************************************/ /* Using PRNG data to trigger "random" events at the specified proportion. */ int RandomEvent () { static int RandomNumber, RandomFiller; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "RandomEvent()\n"); if (!RandomNumber) sys$gettim (&RandomNumber); RandomNumber = RandomNumber * 69069 + 1; if (RandomNumber % CliRandom) return (FALSE); return (TRUE); } /*****************************************************************************/ /* Parse the CLI specified URL into it's host-name (and resolved address), port and path. */ void ParseURL () { int status; char *cptr, *sptr; /*********/ /* begin */ /*********/ cptr = RequestUriPtr; if (strsame (cptr, "ws://", 5)) { cptr += 5; sptr = ServerHost; while (*cptr && *cptr != ':' && *cptr != '/') *sptr++ = *cptr++; *sptr = '\0'; if (*cptr == ':') { cptr++; if (isdigit(*cptr)) ServerPort = atoi(cptr); while (*cptr && isdigit(*cptr)) cptr++; } RequestUriPtr = cptr; } if (!ServerHost[0]) { /* assume the local host is the server */ gethostname (ServerHost, sizeof(ServerHost)); if (Debug) fprintf (stdout, "ServerHost |%s|\n", ServerHost); } if (!ServerPort) ServerPort = 80; } /****************************************************************************/ /* Fill the specified buffer with PRNG octets. */ void BinaryFill ( char *BufferPtr, int BufferSize ) { static unsigned long RandomNumber, GetTimFiller; char *cptr, *sptr, *zptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "BinaryFill()\n"); if (!RandomNumber) sys$gettim (&RandomNumber); zptr = (sptr = BufferPtr) + BufferSize; while (sptr < zptr) { RandomNumber = RandomNumber * 69069 + 1; cptr = (char*)&RandomNumber; *sptr++ = *cptr++; if (sptr < zptr) *sptr++ = *cptr++; if (sptr < zptr) *sptr++ = *cptr++; if (sptr < zptr) *sptr++ = *cptr; } } /****************************************************************************/ /* Fill the buffer with UTF-8 strings of the 8 bit characters 0..255. */ void Utf8Fill ( char *BufferPtr, int BufferSize ) { unsigned char ch = 0; unsigned char *sptr, *zptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "Utf8Fill()\n"); zptr = (sptr = (unsigned char*)BufferPtr) + BufferSize; while (sptr < zptr) { if (ch & 0x80) { if (sptr+1 >= zptr) break; *sptr++ = ((ch & 0xc0) >> 6) | 0xc0; *sptr++ = (ch & 0x3f) | 0x80; } else *sptr++ = ch; if (++ch > 255) ch = 0; } while (sptr < zptr) *sptr++ = '\0'; } /****************************************************************************/ /* Fill the specified buffer with brown foxes. */ void QbfFill ( char *BufferPtr, int BufferSize ) { char *cptr, *sptr, *zptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "QbfFill()\n"); cptr = ""; zptr = (sptr = BufferPtr) + BufferSize; while (sptr < zptr) { if (!*cptr) cptr = "The quick brown fox jumps over the lazy dog.\n"; while (*cptr && sptr < zptr) *sptr++ = *cptr++; } } /****************************************************************************/ /* Called each time wsLIB receives a pong from the server application. */ void PongCallback (struct WsLibStruct *wsptr) { struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "PongCallback()\n"); cxptr = WsLibGetUserData (wsptr); ServerPongCount++; if (DoPing || DoPong) { if (cxptr->CountDown-- <= 1) { WsLibClose (wsptr, WSLIB_CLOSE_NORMAL, CloseAdieu); return; } } } /****************************************************************************/ /* Display non-informational messages output by the wsLIB library. */ void MessageCallback (struct WsLibStruct *wsptr) { int status = SS$_FISH; char *cptr; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "MessageCallback()\n"); cxptr = WsLibGetUserData (wsptr); cptr = WsLibMsgString (wsptr); if (*(USHORTPTR)cptr == '%X') status = strtol(cptr+2,NULL,16); /* if informational */ if (status & 1) return; if (status == SS$_VCCLOSED) return; fprintf (stdout, "%%%s-W-WSLIB, [#%d] msg callback (WSLIB line %d)\n \\%s\\\n", Utility, cxptr->ConnectNumber, WsLibMsgLineNumber(wsptr), cptr); } /****************************************************************************/ /* Return a floating-point representation of the elapsed time. First call starts the duration. Second call ends it, resets the duration, and returns the floating point seconds. If no pointer to a duration quadword is supplied it uses internal static storage. Alpha and IA64 expect CC/FLOAT=IEEE. */ float DurationSeconds (unsigned long *StartTimePtr) { static int LibDeltaSecs = LIB$K_DELTA_SECONDS_F; static unsigned long StartTime [2]; int status; unsigned long DeltaBinTime [2], EndBinTime [2]; float SecsFloat; /*********/ /* begin */ /*********/ if (!StartTimePtr) StartTimePtr = StartTime; if (!(StartTimePtr[0] || StartTimePtr[1])) { sys$gettim (StartTimePtr); return (0.0); } sys$gettim (EndBinTime); status = lib$sub_times (&EndBinTime, StartTimePtr, &DeltaBinTime); if (VMSnok (status)) exit (status); StartTimePtr[0] = StartTimePtr[1] = 0; #ifdef __ia64 status = lib$cvts_from_internal_time (&LibDeltaSecs, &SecsFloat, &DeltaBinTime); #else status = lib$cvtf_from_internal_time (&LibDeltaSecs, &SecsFloat, &DeltaBinTime); # ifdef __ALPHA /* lib$cvts_from_internal_time() does not exist with V7.3-2 or earlier */ # include # include if (VMSok(status)) status = cvt$convert_float (&SecsFloat, CVT$K_VAX_F, &SecsFloat, CVT$K_IEEE_S, 0); # endif #endif if (VMSnok (status)) exit (status); return (SecsFloat); } /****************************************************************************/ /* Return a string with a suitable abbreviated bytes. */ char* BytesOf (float bytes) { static char BytesString [32]; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "BytesOf() %f\n", bytes); if (bytes >= 1000000000.0) sprintf (BytesString, "%.1f Gbytes", bytes/1000000000.0); else if (bytes >= 1000000.0) sprintf (BytesString, "%.1f Mbytes", bytes/1000000.0); else if (bytes >= 1000.0) sprintf (BytesString, "%.1f kbytes", bytes/1000.0); else sprintf (BytesString, "%.0f bytes", bytes); return (BytesString); } /****************************************************************************/ /* Return a string with a suitable abbreviated bytes/second. */ char* BytesPerSecond (float bps) { static char BytesPerString [32]; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "BytesPerSecond() %f\n", bps); if (bps >= 1000000000.0) sprintf (BytesPerString, "%.1f GB/S", bps/1000000000.0); else if (bps >= 1000000.0) sprintf (BytesPerString, "%.1f MB/S", bps/1000000.0); else if (bps >= 1000.0) sprintf (BytesPerString, "%.1f kB/S", bps/1000.0); else sprintf (BytesPerString, "%.0f B/S", bps); return (BytesPerString); } /****************************************************************************/ /* Do just as the function name says! */ void UpdateTotals (struct WsLibStruct *wsptr) { int status; unsigned long *ulptr; struct ConnectionStruct *cxptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "UpdateTotals()\n"); cxptr = WsLibGetUserData (wsptr); ulptr = WsLibReadTotal (wsptr); #ifdef __VAX lib$addx (ulptr, &ReadTotal, &ReadTotal, 0); #else *(__int64*)&ReadTotal = *(__int64*)&ReadTotal + *ulptr; #endif ulptr = WsLibReadMsgTotal (wsptr); #ifdef __VAX lib$addx (ulptr, &ReadMsgTotal, &ReadMsgTotal, 0); #else *(__int64*)&ReadMsgTotal = *(__int64*)&ReadMsgTotal + *ulptr; #endif ulptr = WsLibWriteTotal (wsptr); #ifdef __VAX lib$addx (ulptr, &WriteTotal, &WriteTotal, 0); #else *(__int64*)&WriteTotal = *(__int64*)&WriteTotal + *ulptr; #endif ulptr = WsLibWriteMsgTotal (wsptr); #ifdef __VAX lib$addx (ulptr, &WriteMsgTotal, &WriteMsgTotal, 0); #else *(__int64*)&WriteMsgTotal = *(__int64*)&WriteMsgTotal + *ulptr; #endif } /*****************************************************************************/ /* Get "command-line" parameters, whether from the command-line or from a configuration symbol or logical containing the equivalent. This function has been customized for this utility to allow it to be called multiple times during the one use. */ void GetParameters ( int argc, char *argv[] ) { static char CommandLine [256]; static unsigned long Flags = 0; int acnt, status; unsigned short Length; char ch; char *aptr, *cptr, *clptr, *sptr; $DESCRIPTOR (CommandLineDsc, CommandLine); /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "GetParameters() %d\n", argc); /**************/ /* initialize */ /**************/ ReadBufferSize = DEFAULT_READ_BUFFER_SIZE; ServerPort = DEFAULT_SERVER_PORT; RequestUriPtr = ""; if (argc <= 1) return; /* get the entire command line following the verb */ if (VMSnok (status = lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags))) exit (status); (clptr = CommandLine)[Length] = '\0'; if (Debug) fprintf (stdout, "clptr |%s|\n", clptr); aptr = NULL; ch = *clptr; for (;;) { if (aptr != NULL && *aptr == '/') *aptr = '\0'; if (!ch) break; *clptr = ch; if (Debug) fprintf (stdout, "clptr |%s|\n", clptr); while (*clptr && isspace(*clptr)) *clptr++ = '\0'; aptr = clptr; if (*clptr == '/') clptr++; while (*clptr && !isspace (*clptr) && *clptr != '/') { if (*clptr != '\"') { clptr++; continue; } cptr = clptr; clptr++; while (*clptr) { if (*clptr == '\"') if (*(clptr+1) == '\"') clptr++; else break; *cptr++ = *clptr++; } *cptr = '\0'; if (*clptr) clptr++; } ch = *clptr; if (*clptr) *clptr = '\0'; if (Debug) fprintf (stdout, "aptr |%s|\n", aptr); if (!*aptr) continue; /**************/ /* parameters */ /**************/ if (strsame (aptr, "/BINARY", 4)) { CliBinaryFrame = TRUE; CliQbfFrame = CliTextFrame = CliThroughPut = FALSE; continue; } if (strsame (aptr, "/BREAK=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliBreak = atoi(cptr); if (!CliBreak) CliBreak = DEFAULT_BREAK; if (CliBreak < 1) CliBreak = 1; if (CliBreak > 100) CliBreak = 100; continue; } if (strsame (aptr, "/BRIEF", -1)) { CliBrief = TRUE; continue; } if (strsame (aptr, "/CONCURRENCY=", 5)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliConcurrency = atoi(cptr); continue; } if (strsame (aptr, "/COUNT=", 4) || strsame (aptr, "/NUMBER=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliNumber = atoi(cptr); continue; } if (strsame (aptr, "/DBUG", -1)) { Debug = TRUE; continue; } if (strsame (aptr, "/DO=ECHO", 8)) { DoEcho = TRUE; continue; } if (strsame (aptr, "/DO=PING", 8)) { DoPing = TRUE; continue; } if (strsame (aptr, "/DO=PONG", 8)) { DoPong = TRUE; continue; } if (strsame (aptr, "/DO=PUSH", 8)) { DoPush = TRUE; continue; } if (strsame (aptr, "/DYNAMIC", 4)) { CliDynamic = TRUE; continue; } if (strsame (aptr, "/HEARTBEAT=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliHeartBeat = atoi(cptr); if (!CliHeartBeat) CliHeartBeat = 10; continue; } if (strsame (aptr, "/HELP", 4)) { DoShowHelp = TRUE; continue; } if (strsame (aptr, "/MRS=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliSocketMrs = atoi(cptr); continue; } if (strsame (aptr, "/PING", 4)) { CliPing = TRUE; continue; } if (strsame (aptr, "/PONG", 4)) { CliPong = TRUE; continue; } if (strsame (aptr, "/PROXY=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliProxyServer = cptr; continue; } if (strsame (aptr, "/QBF", -1)) { CliQbfFrame = TRUE; CliBinaryFrame = CliTextFrame = CliThroughPut = FALSE; continue; } if (strsame (aptr, "/RANDOM=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliRandom = atoi(cptr); if (!CliRandom) CliRandom = DEFAULT_RANDOM; if (CliRandom < 1) CliRandom = 1; if (CliRandom > 100) CliRandom = 100; continue; } if (strsame (aptr, "/REPEAT=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliCount = atoi(cptr); continue; } if (strsame (aptr, "/SIZE=", 4)) { for (cptr = aptr; *cptr && *cptr != '='; cptr++); if (*cptr) cptr++; CliBufferSize = atoi(cptr); while (*cptr && isdigit(*cptr)) cptr++; if (toupper(*cptr) == 'G') CliBufferSize *= 1000000000; else if (toupper(*cptr) == 'M') CliBufferSize *= 1000000; else if (tolower(*cptr) == 'k') CliBufferSize *= 1000; continue; } if (strsame (aptr, "/STAGGER", 4)) { CliStagger = TRUE; continue; } if (strsame (aptr, "/NOSTAGGER", 4)) { CliStagger = FALSE; continue; } if (strsame (aptr, "/TEXT", 4)) { CliTextFrame = TRUE; CliBinaryFrame = CliQbfFrame = CliThroughPut = FALSE; continue; } if (strsame (aptr, "/VERSION", 4)) { DoShowVersion = TRUE; continue; } if (strsame (aptr, "/THROUGHPUT", 4)) { CliThroughPut = TRUE; CliBinaryFrame = CliQbfFrame = CliTextFrame = FALSE; continue; } if (*aptr == '/') { fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n", Utility, aptr+1); exit (STS$K_ERROR | STS$M_INHIB_MSG); } if (!RequestUriPtr[0]) { RequestUriPtr = aptr; continue; } fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n", Utility, aptr); exit (STS$K_ERROR | STS$M_INHIB_MSG); } } /*****************************************************************************/ /* */ void ShowHelp () { /*********/ /* begin */ /*********/ fprintf (stdout, "%%%s-I-HELP, usage for the WASD WebSocket test-bench :^) utility (%s)\n\ \n\ WSB - CLI test and exercise the WASD WebSocket implementation.\n\ WS_BENCH - the complementary WebSocket server script/application.\n\ \n\ $ WB [qualifiers ...]\n\ \n\ /BINARY /BREAK[=] /COUNT= /CONCURRENT=\n\ /DO=[ECHO|PING|PONG|PUSH] /NUMBER= /PING /PONG /PROXY= /QBF\n\ /RANDOM= /REPEAT= /SIZE= /[NO]STAGGER /TEXT\n\ \n\ Usage examples:\n\ \n\ $ WSB /NUM=1000 /DO=PUSH\n\ $ WSB /CONC=50 /NUM=1000 /DO=ECHO /PING /BREAK\n\ \n", Utility, SOFTWAREID); exit (SS$_NORMAL); } /****************************************************************************/ /* Does a case-insensitive, character-by-character string compare and returns TRUE if two strings are the same, or FALSE if not. If a maximum number of characters are specified only those will be compared, if the entire strings should be compared then specify the number of characters as 0. */ BOOL strsame ( char *sptr1, char *sptr2, int count ) { while (*sptr1 && *sptr2) { if (toupper (*sptr1++) != toupper (*sptr2++)) return (FALSE); if (count) if (!--count) return (TRUE); } if (*sptr1 || *sptr2) return (FALSE); else return (TRUE); } /****************************************************************************/