diff --git a/.travis.yml b/.travis.yml index 00dd9c9..ea5faa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ os: - linux - osx julia: - - 0.5 - 0.6 sudo: false notifications: diff --git a/README.md b/README.md index eac28ac..b3b792b 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,184 @@ -WebSockets.jl -============= +# WebSockets.jl + [![Build Status](https://travis-ci.org/JuliaWeb/WebSockets.jl.png)](https://travis-ci.org/JuliaWeb/WebSockets.jl) [![Coverage Status](https://img.shields.io/coveralls/JuliaWeb/WebSockets.jl.svg)](https://coveralls.io/r/JuliaWeb/WebSockets.jl) - -[![WebSockets](http://pkg.julialang.org/badges/WebSockets_0.5.svg)](http://pkg.julialang.org/?pkg=WebSockets&ver=0.5) [![WebSockets](http://pkg.julialang.org/badges/WebSockets_0.6.svg)](http://pkg.julialang.org/?pkg=WebSockets&ver=0.6) -This is a server-side implementation of the WebSockets protocol in Julia. If you want to write a web app in Julia that uses WebSockets, you'll need this package. -This package works with [HttpServer.jl](https://github.com/JuliaWeb/HttpServer.jl), which is what you use to set up a server that accepts HTTP(S) connections. +Server and client side [Websockets](https://tools.ietf.org/html/rfc6455) protocol in Julia. WebSockets is a small overhead message protocol layered over [TCP](https://tools.ietf.org/html/rfc793). It uses HTTP(S) for establishing the connections. + +## Getting started +WebSockets.jl must be used with either HttpServer.jl or HTTP.jl, but neither is a dependency of this package. You will need to first add one or both, i.e.: + +```julia +julia> Pkg.add("HttpServer") +julia> Pkg.add("HTTP") +julia> Pkg.add("WebSockets") +``` +### Open a client side connection +Client side websockets are created by calling `WebSockets.open` (with a server running). Client side websockets require [HTTP.jl](https://github.com/JuliaWeb/HttpServer.jl). + +### Accept server side connections + +Server side websockets are asyncronous [tasks](https://docs.julialang.org/en/stable/stdlib/parallel/#Tasks-1), spawned by either +[HttpServer.jl](https://github.com/JuliaWeb/HttpServer.jl) or HTTP.jl. + +##### Using HttpServer +Call `run`, which is a wrapper for calling `listen`. See inline docs. + +##### Using HTTP +Call `WebSockets.serve`, which is a wrapper for `HTTP.listen`. See inline docs. + +## What does `WebSockets.jl` enable? + +- reading and writing between entities you can program or know about +- low latency messaging +- implement your own 'if X send this, Y do that' subprotocols +- implement registered [websocket subprotocols](https://www.iana.org/assignments/websocket/websocket.xml#version-number) +- heartbeating, relaying +- build a network including browser clients +- convenience functions for gatekeeping with a common interface for HttpServer and HTTP +- writing http handlers and websockets 'handlers' in the same process can be an advantage. Exchanging unique tokens via http(s) + before accepting websockets is recommended for improved security. + +WebSockets are well suited for user interactions via a browser. By calling compiled Javascript functions in browsers and using parallel workers, +user interaction and graphics workload, even development time can be moved off Julia resources. + +The /logutils folder contains some logging functionality that is quite fast and can make working with multiple asyncronous tasks easier. This functionality may be moved out of WebSockets in the future, depending on how other logging capabilities develop. + +You should also have a look at Julia packages [DandelionWebSockets](https://github.com/dandeliondeathray/DandelionWebSockets.jl) or the implementation currently part of HTTP.jl. + +## What are the main downsides to WebSockets (in Julia)? + +- Logging. We need customizable and very fast logging for building networked applications. +- Security. Julia's Http(s) servers are currently not working to our knowledge. +- Non-compliant proxies on the internet, company firewalls. Commercial applications often use competing technologies for this reason, according to some old articles at least. HTTP.jl lets you use such techniques. +- 'Warm-up', i.e. compilation when a method is first used. These are excluded from current benchmarks. +- Garbage collection, which increases message latency at semi-random intervals. See benchmark plots. +- If a connection is closed improperly, the connection task will throw uncaught ECONNRESET and similar messages. +- TCP quirks, including 'warm-up' time with low transmission speed after a pause. Heartbeats can alleviate. +- Neither HTTP.jl or HttpServer.jl are made just for connecting WebSockets. You may need strong points from both. +- The optional dependencies increases load time compared to fixed dependencies. +- Since 'read' is a blocking function, you can easily end up reading indefinetely from both sides. -As a first example, we can create a WebSockets echo server: +## Server side example + +As a first example, we can create a WebSockets echo server. We use named function arguments for more readable stacktraces while debugging. ```julia using HttpServer using WebSockets -wsh = WebSocketHandler() do req,client - while true - msg = read(client) - write(client, msg) +function coroutine(ws) + while isopen(ws) + data, = readguarded(ws) + s = String(data) + if s == "" + break + end + println(s) + if s[1] == "P" + writeguarded(ws, "No, I'm not!") + else + writeguarded(ws, "Why?") + end end - end +end -server = Server(wsh) -run(server,8080) -``` +function gatekeeper(req, ws) + if origin(req) == "http://127.0.0.1:8080" || origin(req) == "http://localhost:8080" + coroutine(ws) + else + println(origin(req)) + end +end -This sets up a server running on localhost, port 8080. -It will accept WebSockets connections. -The function in `wsh` will be called once per connection; it takes over that connection. -In this case, it reads each `msg` from the `client` and then writes the same message back: a basic echo server. +handle(req, res) = Response(200) -The function that you pass to the `WebSocketHandler` constructor takes two arguments: -a `Request` from [HttpCommon.jl](https://github.com/JuliaWeb/HttpCommon.jl/blob/master/src/HttpCommon.jl#L142), -and a `WebSocket` from [here](https://github.com/JuliaWeb/WebSockets.jl/blob/master/src/WebSockets.jl#L17). +server = Server(HttpHandler(handle), + WebSocketHandler(gatekeeper)) -## What can you do with a `WebSocket`? -You can: +run(server, 8080) +``` -* `write` data to it -* `read` data from it -* send `ping` or `pong` messages -* `close` the connection +Now open a browser on http://127.0.0.1:8080/ and press F12. In the console, type the lines following ≫: +```javascript +≫ws = new WebSocket("ws://127.0.0.1:8080") + ← WebSocket { url: "ws://127.0.0.1:8080/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null, onclose: null, extensions: "", protocol: "", onmessage: null, binaryType: "blob" } +≫ws.send("Peer, you're lying!") + ← undefined +≫ws.onmessage = function(e){console.log(e.data)} + ← function onmessage() +≫ws.send("Well, then.") + ← undefined +Why? debugger eval code:1:28 +``` -## Installation/Setup +If you now navigate or close the browser, this happens: +1. the client side of the websocket connection will quickly send a close request and go away. +2. Server side `readguarded(ws)` has been waiting for messages, but instead closes 'ws' and returns ("", false) +3. `coroutine(ws)` is finished and the task's control flow returns to HttpServer +4. HttpServer does nothing other than exit this task. In fact, it often crashes because + somebody else (the browser) has closed the underlying TCP stream. If you had replaced the last Julia line with '@async run(server, 8080', you would see some long error messages. +5. The server, which spawned the task, continues to listen for incoming connections, and you're stuck. Ctrl + C! -~~~julia -julia> Pkg.add("WebSockets") -~~~ +You could replace 'using HttpServer' with 'using HTTP'. Also: + Serve -> ServeWS + HttpHandler -> HTTP.Handler + WebSocketHandler -> WebSockets.WebsocketHandler -At this point, you can use the examples below to test that it all works. -## [Chat client/server example](https://github.com/JuliaWeb/WebSockets.jl/blob/master/examples/chat.jl): +## Client side -1. Move to the `~/.julia//WebSockets` directory -2. Run `julia examples/chat.jl` -3. In a web browser, open `localhost:8000` -4. You should see a basic IRC-like chat application +You need to use [HTTP.jl](https://github.com/JuliaWeb/HttpServer.jl). +What you can't do is use a browser as the server side. The server side can be the example above, running in an asyncronous task. The server can also be running in a separate REPL, or in a a parallel task. The benchmarks puts the `client` side on a parallel task. -## Echo server example: +The following example +- runs server in an asyncronous task, client in the REPL control flow +- uses [Do-Block-Syntax](https://docs.julialang.org/en/v0.6.3/manual/functions/#Do-Block-Syntax-for-Function-Arguments-1), which is a style choice +- the server `ugrade` skips checking origin(req)` +- the server is invoked with `listen(..)` instead of `serve()` +- read(ws) and write(ws, msg) instead of readguarded(ws), writeguarded(ws) -~~~julia -using HttpServer +```julia + +using HTTP using WebSockets -wsh = WebSocketHandler() do req,client - while true - msg = read(client) - write(client, msg) +const PORT = 8080 + +# Server side +@async HTTP.listen("127.0.0.1", PORT) do http + if WebSockets.is_upgrade(http.message) + WebSockets.upgrade(http) do req, ws + while isopen(ws) + msg = String(read(ws)) + write(ws, msg) + end + end end - end +end + +sleep(2) + + +WebSockets.open("ws://127.0.0.1:$PORT") do ws + write(ws, "Peer, about your hunting") + println("echo received:" * String(read(ws))) +end +``` + +The output in a console session is barely readable, which is irritating. To build real-time applications, we need more code. + +Some logging utilties for a running relay server are available in /logutils. + + + -server = Server(wsh) -run(server,8080) -~~~ -To play with a WebSockets echo server, you can: -1. Paste the above code in to the Julia REPL -2. Open `localhost:8080` in Chrome -3. Open the Chrome developers tools console -4. Type `ws = new WebSocket("ws://localhost:8080");` into the console -5. Type `ws.send("hi")` into the console. -6. Switch to the 'Network' tab; click on the request; click on the 'frames' tab. -7. You will see the two frames containing "hi": one sent and one received. ~~~~ :::::::::::::::: diff --git a/REQUIRE b/REQUIRE index 3f44968..c8fe5e1 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,6 +1,3 @@ -julia 0.5 -Compat 0.28.0 -HttpCommon -HttpServer -Codecs +julia 0.6 MbedTLS +Requires \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index aa55302..845944a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,12 @@ environment: matrix: - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe" - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe" # HttpCommon not building yet - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" # HttpCommon not building yet - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" branches: only: + - change_dependencies - master - /release-.*/ @@ -35,7 +34,7 @@ build_script: - IF EXIST .git\shallow (git fetch --unshallow) - ps: | Write-Host "This is PowerShell - STDERR output will be red. Redirect! " - $env:PATH+=";C:\projects\julia\bin\" + $env:PATH+=";C:\projects\julia\bin\" julia -e 'versioninfo();redirect_stderr(STDOUT);println(STDOUT, \"stdout\"); println(STDERR, \"stderr\")' julia -e 'redirect_stderr(STDOUT);Pkg.init()' julia --depwarn=no -e 'redirect_stderr(STDOUT);Pkg.add(\"HttpServer\");using HttpServer;' @@ -44,4 +43,4 @@ build_script: test_script: - ps: | Write-Host "This is PS" - julia -e 'redirect_stderr(STDOUT);info(\"Local websockets directory:\", Pkg.dir(\"WebSockets\"));Pkg.test(\"WebSockets\")' \ No newline at end of file + julia -e 'redirect_stderr(STDOUT);info(\"Local websockets directory:\", Pkg.dir(\"WebSockets\"));Pkg.test(\"WebSockets\")' diff --git a/benchmark/REQUIRE b/benchmark/REQUIRE new file mode 100644 index 0000000..e554899 --- /dev/null +++ b/benchmark/REQUIRE @@ -0,0 +1,5 @@ +# Being placed in the benchmark folder, this file has no effect on Julia. +# To run benchmarks, you may need to Pkg.add("..package below...") +HTTP +IndexedTables +UnicodePlots diff --git a/benchmark/bce.html b/benchmark/bce.html new file mode 100644 index 0000000..ff2e031 --- /dev/null +++ b/benchmark/bce.html @@ -0,0 +1,97 @@ + + + + + + + WS text + + +

BCE echoing websocket. +

+ +

| +Initiates a websocket connection. +For every received websocket message, sends one empty message and then the received message. The websocket is closed by +the server, or when user navigates away.
+

+

+

  This is ...

+

ws1 Websocket

+ + + diff --git a/benchmark/benchmark.jl b/benchmark/benchmark.jl new file mode 100644 index 0000000..afc7b68 --- /dev/null +++ b/benchmark/benchmark.jl @@ -0,0 +1,83 @@ +#= +TERMINOLOGY + +Client The side of a connection which initiates. +Server The side of a connection which accepts. +Origin Sender +Destination Receiver +HTS HTTP server +JCE Julia Client Echoing (only compatible with HTS) +BCE Browser Client Echoing (compatible with HTS and HSS) + +Note we use 'inverse speed' below. In lack of better words, we call this speed. +This is more compatible with BenchmarkTools and more directly useful. +For some fun reasoning, refer https://www.lesswrong.com/posts/qNiH3RRTiXXyvMJRg/inverse-speed + +Note 'latency' can also be defined in other ways. Ad hoc: + +speed [ns/b] time / amount +messagesize [b] Datasize for an individual message (one or more frames) +clientlatency [ns] Time from start sending from client to having received on destination +serverlatency [ns] Time from start sending from server to having received on destination +clientspeed [ns/b] = [s/GB] + = upspeed = clientlatency / messagesize + For JCE: Client sends the message, server records its having received time + For BCE: Server sends the message, client sends back two messages of different lengths. + Clientspeed is calculated from server time records. +serverspeed [ns/b] = [s/GB] + = Downspeed = serverlatency / messagesize + For JCE: Server sends the message, client measures its having received time + For BCE: Server sends the message, client sends back two messages of different lengths. + Serverspeed is calculated from server time records. +clientbandwidth [ns/b] = [s/GB] + Average clientspeed, time per datasize +serverbandwidth [ns/b] = [s/GB] + Average serverspeed, time per datasize + +=# + +#= +----- TODO ---- + +Not used: +** HSS HttpServer server +** delay [ns] Time usefully spent between received message and start of reply +** clientRTT [ns] Round-trip-time for a message initiated by client +** serverRTT [ns] Round-trip-time for a message initiated by server +** Up2down ClientBandwidth / ServerBandwidth +** throughput datasize / (RTT - Delay) +** serversize [b] Messagesize from server +** clientsize [b] Messagesize from client + + + +Outlined benchmarks for optimizing WebSockets: + +- Clientlatency() @ Bandwidth ≈ 0, Clientmessage = 1b + Requires some tweaking of BenchMarkTools.Trial, postponed. +- Serverlatency() @ Bandwidth ≈ 0, Servermessage = 1b + Requires some tweaking of BenchMarkTools.Trial, postponed. +- Maximized bandwidth(VSIZE) @ Up2Down ≈ 1 + Not using BenchmarkTools directly +- ClientRTT(VDELAY) @ Bandwidth ≈ 0, Clientmessage = 1b +- ServerRTT(VDELAY) @ Bandwidth ≈ 0, Servermessage = 1b + The last two checks vs. possibly related issue #1608 on ZMQ + + + +Outlined benchmarks for developing an application using WebSockets (postponed): +- ClientRTT(VSIZE) @ Bandwidth[0%, 50%, 100%] + ClientRTT determines user responsiveness, e.g. mouse clicks in a browser + This requires small messages or threaded read / write (async has to wait for + the server finishing sending its message) +- Bandwidth(VUP2DOWN, VSIZE) + This depends on the network in use and operating system + [(up2down, msize) for up2down in VUP2DOWN for msize in VSIZE] +- Serverlatency(VSIZE) @ Bandwidth[0%, 50%, 100%] + The outliers (due to e.g. semi-random garbage collection) determines + choice of message size and buffers for media streams +- Clientlatency() @ 100% bandwidth @ Up2Down + The outliers (due to e.g. semi-random garbage collection) determines + choice of message size and buffers for long-running calculations which + connects to a server for distribution of results +=# diff --git a/benchmark/benchmark_prepare.jl b/benchmark/benchmark_prepare.jl new file mode 100644 index 0000000..01dc129 --- /dev/null +++ b/benchmark/benchmark_prepare.jl @@ -0,0 +1,316 @@ +# See benchmark.jl for definitions + +# This file collects data and tests functions. +# The intention is preparing input for a benchmark suite, +# but the output actually is sufficient for most purposes. +# +# Both log file, results plots and tables are printed to the same file. +# Viewing the plots in a text editor is probably possible, see UnicodePlots.jl. +# Running this file on a Windows laptop with all browsers takes 5-10 minutes + +"A vector of message sizes" +const VSIZE = reverse([i^3 * 1020 for i = 1:12]) +const SRCPATH = Base.source_dir() == nothing ? Pkg.dir("WebSockets", "benchmark") :Base.source_dir() +const LOGGINGPATH = realpath(joinpath(SRCPATH, "../logutils/")) +SRCPATH ∉ LOAD_PATH && push!(LOAD_PATH, SRCPATH) +LOGGINGPATH ∉ LOAD_PATH && push!(LOAD_PATH, LOGGINGPATH) +include(joinpath(SRCPATH, "functions_open_browsers.jl")) +include(joinpath(SRCPATH, "functions_benchmark.jl")) +# +prepareworker() +# Load modules on both processes +import HTTP +using WebSockets +using ws_jce +using UnicodePlots +import IndexedTables.table +import ws_hts: listen_hts, getws_hts +# +remotecall_fetch(ws_jce.clog, 2, "ws_jce ", :green, " is ready") +# Start async HTS server on this process and check that it is up and running +const TIMEOUT = Base.Dates.Second(20) +hts_task = start_hts(TIMEOUT) + +""" +Prepare logging for this process + + logs are written to console as well as to a file, which + is different for the other process. + Logs in other processes appear in console with a delay, hence the timestamp + is interesting. + To write log file buffer to disk immediately, call zflush(). + To drop duplicate console log, use zlog(..) instead of clog(..) +""" +const ID = "Benchmark" +const LOGFILE = "benchmark_prepare.log" +import logutils_ws: logto, clog, zlog, zflush, clog_notime +fbm = open(joinpath(SRCPATH, "logs", LOGFILE), "w") +logto(fbm) +clog(ID, "Started async HTS and prepared parallel worker") +zflush() + + +# +# Do an initial test run, HTS-JCE, brief text output +# + +const INITSIZE = 130560 +const INITN = 200 + +# Measured time interval vectors [ns] +# Time measurements are taken directly both at server and client +(testid, serverlatencies, clientlatencies) = HTS_JCE(INITN, INITSIZE) +# Calculate speeds [ns/b] and averaged speeds (bandwidths) +(serverspeeds, clientspeeds, + serverbandwidth, clientbandwidth) = serverandclientspeeds(INITSIZE, serverlatencies, clientlatencies) +# Store plots and a table in dictionaries +vars= [:serverspeeds, :clientspeeds]; +init_plots = Dict(testid => lp(vars, testid)); +init_tables = Dict(testid => table(eval.(vars)..., names = vars)); +init_serverbandwidths = Dict(testid => serverbandwidth); +init_clientbandwidths = Dict(testid => clientbandwidth); +# Sleep to avoid interspersing with worker output to REPL +sleep(2) +# Brief output to file and console +clog(testid, " Initial test run with messagesize ", INITSIZE, " bytes \n\t", + "serverbandwidth = ", :yellow, round(serverbandwidth, 4), :normal, " [ns/b] = [s/GB]\n\t", + :normal, "clientbandwidth = ", :yellow, round(clientbandwidth, 4), :normal, " [ns/b] = [s/GB]") + +# +# Continue initial test run with HTS-BCE, brief text output for each browser +# + +COUNTBROWSER.value = 0 +serverbandwidth = 0. +clientbandwidth = 0. +serverbandwidths = Vector{Float64}() +clientbandwidths = Vector{Float64}() +t1 = Vector{Int}() +t2 = Vector{Int}() +t3 = Vector{Int}() +t4 = Vector{Int}() +browser = "" +success = true +while success + # Measured time interval vectors [ns] for the next browser in line + # Time measurements are taken only at server; a message pattern + # is used to distinguish server and client performance + (browser, t1, t2, t3, t4) = HTS_BCE(INITN, INITSIZE) + if browser != "" + # Calculate speeds [ns/b] and averaged speeds (bandwidths) + (serverspeeds, clientspeeds, + serverbandwidth, clientbandwidth) = + serverandclientspeeds_indirect(INITSIZE, t1, t2, t3, t4) + # Store plots and a table in dictionaries + testid = "HTS_BCE " * browser + push!(init_plots, testid => lp(vars, testid)); + push!(init_tables, testid => table(eval.(vars)..., names = vars)); + push!(init_serverbandwidths, testid => serverbandwidth); + push!(init_clientbandwidths, testid => clientbandwidth); + # Brief output to file and console + clog(testid, " Initial test run with messagesize ", INITSIZE, " bytes \n\t", + "serverbandwidth = ", :yellow, round(serverbandwidth, 4), :normal, " [ns/b] = [s/GB]\n\t", + :normal, "clientbandwidth = ", :yellow, round(clientbandwidth, 4), :normal, " [ns/b] = [s/GB]") + else + success = false + end +end + +# +# Collect HTS_JCE bandwidths vs message size +# + +const SAMPLES = 200 +serverbandwidths = Dict{String, Vector{Float64}}() +clientbandwidths = Dict{String, Vector{Float64}}() +for msgsiz in VSIZE + # Measured time interval vectors [ns] + # Time measurements are taken directly both at server and client + (testid, serverlatencies, clientlatencies) = HTS_JCE(SAMPLES, msgsiz) + # Find averaged speed (bandwidth) scalars + (_, _, + sbw, cbw) = serverandclientspeeds(msgsiz, serverlatencies, clientlatencies) + # Assign storage + !haskey(serverbandwidths, testid) && push!(serverbandwidths, testid => Vector{Float64}()) + !haskey(clientbandwidths, testid) && push!(clientbandwidths, testid => Vector{Float64}()) + # Store bandwidths from this size + push!(serverbandwidths[testid], sbw) + push!(clientbandwidths[testid], cbw) +end + +# +# Collect HTS_BCE bandwidths +# For individual and available browsers +# This opens a lot of browser tabs; there may +# be no easy way of closing them except +# by closing Julia. internet explorer even +# opens a separate window every time. +# +for msgsiz in VSIZE + COUNTBROWSER.value = 0 + t1 = Vector{Int}() + t2 = Vector{Int}() + t3 = Vector{Int}() + t4 = Vector{Int}() + browser = "" + success = true + while success + # Measured time interval vectors [ns] for the next browser in line + # Time measurements are taken only at server; a message pattern + # is used to distinguish server and client performance + (browser, t1, t2, t3, t4) = HTS_BCE(SAMPLES, msgsiz) + testid = "HTS_BCE " * browser + if browser != "" + # Find averaged speed (bandwidth) scalars + (_, _, + sbw, cbw) = + serverandclientspeeds_indirect(msgsiz, t1, t2, t3, t4) + # Assign storage + !haskey(serverbandwidths, testid) && push!(serverbandwidths, testid => Vector{Float64}()) + !haskey(clientbandwidths, testid) && push!(clientbandwidths, testid => Vector{Float64}()) + # Store bandwidths from this size + push!(serverbandwidths[testid], sbw) + push!(clientbandwidths[testid], cbw) + else + success = false + end + end +end + + + +# +# Measurements are done. Close server and log file, open results log file. +# +ws_hts.close_hts() +clog(ID, "Closing HTS server") +const RESULTFILE = "benchmark_results.log" +clog(ID, "Results are summarized in ", joinpath(SRCPATH, "logs", RESULTFILE)) +fbmr = open(joinpath(SRCPATH, "logs", RESULTFILE), "w") +logto(fbmr) +close(fbm) + + + +# +# Find optimum message size and nominal 100% bandwidths +# Make and store plots and tables +# +test_bestserverbandwidths = Dict{String, Float64}() +test_bestclientbandwidths = Dict{String, Float64}() +test_bestserverlatencies = Dict{String, Float64}() +test_bestclientlatencies = Dict{String, Float64}() +test_plots = Dict() +test_tables = Dict() +test_latency_plots = Dict() +test_latency_tables = Dict() +serverbandwidth = Vector{Float64}() +clientbandwidth = Vector{Float64}() +serverlatency = Vector{Float64}() +clientlatency = Vector{Float64}() +bestserverbandwidth = 0. +bestclientbandwidth = 0. +bestserverlatency = 0. +bestclientlatency = 0. + +for testid in keys(serverbandwidths) + serverbandwidth = serverbandwidths[testid] + clientbandwidth = clientbandwidths[testid] + # Store the optimal bandwidths in a dictionary + bestserverbandwidth = minimum(serverbandwidth) + bestclientbandwidth = minimum(clientbandwidth) + push!(test_bestserverbandwidths, testid => bestserverbandwidth); + push!(test_bestclientbandwidths, testid => bestclientbandwidth); + # Store msgsiz-bandwidth line plots and tables in dictionaries + vars = [:serverbandwidth, :clientbandwidth] + tvars = vcat([:VSIZE], vars) + push!(test_plots, testid => lp([:VSIZE, :VSIZE], vars, testid)); + push!(test_tables, testid => table(eval.(tvars)..., names = tvars)); + # Store msgsiz-latency line plots and tables in dictionaries + serverlatency = serverbandwidth .* VSIZE + clientlatency = clientbandwidth .* VSIZE + bestserverlatency = minimum(serverlatency) + bestclientlatency = minimum(clientlatency) + push!(test_bestserverlatencies, testid => bestserverlatency); + push!(test_bestclientlatencies, testid => bestclientlatency); + vars = [:serverlatency, :clientlatency] + tvars = vcat([:VSIZE], vars) + push!(test_latency_plots, testid => lp([:VSIZE, :VSIZE], vars, testid)); + push!(test_latency_tables, testid => table(eval.(tvars)..., names = tvars)); + + # Brief output to file and console + clog_notime(testid, :normal, " Varying message size: \n\t", + "bestserverbandwidth = ", :yellow, round(bestserverbandwidth, 4), :normal, " [ns/b] = [s/GB]", + " @ size = ", VSIZE[findfirst(serverbandwidth, bestserverbandwidth)], " b\n\t", + :normal, "bestclientbandwidth = ", :yellow, round(bestclientbandwidth, 4), :normal, " [ns/b] = [s/GB]", + " @ size = ", VSIZE[findfirst(clientbandwidth, bestclientbandwidth)], " b\n\t", + "bestserverlatency = ", :yellow, Int(round(bestserverlatency)), :normal, " [ns] ", + " @ size = ", VSIZE[findfirst(serverlatency, bestserverlatency)], " b\n\t", + :normal, "bestclientlatency = ", :yellow, Int(round(bestclientlatency)), :normal, " [ns]", + " @ size = ", VSIZE[findfirst(clientlatency, bestclientlatency)], " b\n\t" + ) +end + + +# +# Full text output +# The plots are not currently readably encoded in the text file +# + +clog_notime(ID, :bold, :yellow, " Plots of all samples :init_plots [ns/b], message size ", INITSIZE, " b ", SAMPLES, " samples" ) +foreach(values(init_plots)) do pls + foreach(values(pls)) do pl + clog_notime(pl) + end +end +clog_notime(ID, :bold, :yellow, " Tables of all samples, :init_tables, message size ", INITSIZE, " b ", SAMPLES, " samples" ) +for (ke, ta) in init_tables + clog_notime(ke, "\n=> ", ta, "\n") +end + + +clog_notime(ID, :bold, :yellow, " Plots of varying size messages :test_plots [ns/b],\n\t VSIZE = ", VSIZE) +foreach(values(test_plots)) do pls + foreach(values(pls)) do pl + clog_notime(pl) + end +end + +clog_notime(ID, :bold, :yellow, " Tables of varying size messages :test_tables [ns/b]") +for (ke, ta) in test_tables + clog_notime(ke, "\n=> ", ta, "\n") +end + +clog_notime(ID, :bold, :yellow, " Plots of varying size messages :test_latency_plots [ns],\n\t VSIZE = ", VSIZE) +foreach(values(test_latency_plots)) do pls + foreach(values(pls)) do pl + clog_notime(pl) + end +end + +clog_notime(ID, :bold, :yellow, " Tables of varying size messages :test_latency_tables [ns]") +for (ke, ta) in test_latency_tables + clog_notime(ke, "\n=> ", ta, "\n") +end + +clog_notime(ID, :bold, :yellow, " Dictionary :test_bestserverlatencies [ns]") +for (ke, va) in test_bestserverlatencies + clog_notime(ke, " => \t", Int(round(va))) +end + +clog_notime(ID, :bold, :yellow, " Dictionary :test_bestclientlatencies [ns]") +for (ke, va) in test_bestclientlatencies + clog_notime(ke, " => \t", Int(round(va))) +end + +clog_notime(ID, :bold, :yellow, " Dictionary :test_bestserverbandwidths [ns/b]") +for (ke, va) in test_bestserverbandwidths + clog_notime(ke, " => \t", round(va, 4)) +end + +clog_notime(ID, :bold, :yellow, " Dictionary :test_bestclientbandwidths [ns/b]") +for (ke, va) in test_bestclientbandwidths + clog_notime(ke, " => \t", round(va, 4)) +end + +zflush() diff --git a/benchmark/favicon.ico b/benchmark/favicon.ico new file mode 100644 index 0000000..6d1163d Binary files /dev/null and b/benchmark/favicon.ico differ diff --git a/benchmark/functions_benchmark.jl b/benchmark/functions_benchmark.jl new file mode 100644 index 0000000..1354b50 --- /dev/null +++ b/benchmark/functions_benchmark.jl @@ -0,0 +1,302 @@ +# Included in benchmark.jl +"Adds process 2, same LOAD_PATH as process 1" +function prepareworker() + # Prepare worker + FULLLOADPATH = LOAD_PATH + if nworkers() < 2 + addprocs(1) + end + futur = @spawnat 2 for p in FULLLOADPATH + p ∉ LOAD_PATH && push!(LOAD_PATH, p) + end + # waits for process 2 to get ready, trigger errors if any + fetch(futur) +end + +"Start and wait for async hts server" +function start_hts(timeout) + hts_task = @schedule ws_hts.listen_hts() + t1 = now() + timeout + while now() < t1 + sleep(0.5) + isdefined(ws_hts.TCPREF, :x) && break + end + if now()>= t1 + msg = " did not establish server before timeout " + clog("start_hts", :red, msg, timeout) + error(msg) + end + hts_task +end + + + +""" +Make connection and get a reference to the HTS side of HTS-JCE connection. +Assumes a server is already running. +""" +function get_hts_jce() + id = "get_hts_jce" + # Make the parallel worker initiate a websocket connection to this process. + # It will finish execution when either peer closes the connection. + @spawnat 2 ws_jce.echowithdelay_jce() + # async HTS will not accept the connection before we yield to it + zlog(id, "JCE will be connecting to HTS. We ask HTS for a reference...") + zflush() + hts = Union{WebSocket, String}("") + t1 = now() + TIMEOUT + while now() < t1 + yield() + hts = ws_hts.getws_hts() + isa(hts, WebSocket) && break + sleep(0.5) + end + return hts +end +""" +Make and get a reference to the server side of HTS-BCE websocket connection. +Will launch a different web browser every time. Returns a string +if all available browsers have been launched one time. + +Assumes a server is already running. +""" +function get_hts_bce() + id = "get_hts_jce" + hts = Union{WebSocket, String}("") + browser ="" + opened, browser = open_a_browser() + # Launch the next browser in line. + if opened + zlog(id, "BCE in, ", browser, " will be connecting to HTS. Getting reference...") + zflush() + hts = Union{WebSocket, String}("") + t1 = now() + TIMEOUT + while now() < t1 + yield() + hts = ws_hts.getws_hts() + isa(hts, WebSocket) && break + sleep(0.5) + end + return hts, browser + end + return hts, browser +end +""" +Send n messages of length messagesize HTS-JCE and back. + -> id, serverlatencies, clientlatencies +Starts and closes a new HTS-JCE connection every function call. +In the terminology of BenchmarkTools, a call is a sample +consisting of n evaluations. +""" +function HTS_JCE(n, messagesize) + id = "HTS_JCE" + zlog(id, "Warming up, compiling") + zflush() + hts = get_hts_jce() + if !isa(hts, WebSocket) + msg = " could not get websocket reference" + clog(id, msg) + error(id * msg) + end + clog(id, hts) + # Random seeding, same for all samples + srand(1) + clog(id, "Sending ", n, " messages of ", messagesize , " random bytes") + sendtime = Int64(0) + receivereplytime = Int64(0) + sendtimes = Vector{Int64}() + receivereplytimes = Vector{Int64}() + for i = 1:n + msg = rand(0x20:0x7f, messagesize) + sendtime = time_ns() + write(hts, msg) + # throw away replies + readguarded(hts) + receivereplytime = time_ns() + push!(receivereplytimes, Int64(receivereplytime < typemax(Int64) ? receivereplytime : 0 )) + push!(sendtimes, Int64(sendtime < typemax(Int64) ? sendtime : 0 )) + end + # + zlog(id, "Sending 'exit', JCE will send its time log and then exit.") + zflush() + write(hts, "exit") + # We deserialize JCE's time records from this sample + bs = BufferStream() + write(bs, read(hts)) + close(bs) + if nb_available(bs) > 0 + receivetimes, replytimes = deserialize(bs) + else + error("Did not receive receivetimes, replytimes") + end + # We must read from the websocket in order for it to respond to + # a closing message from JCE. It's also nice to yield to the async server + # so it can exit from it's handler and release the websocket reference. + isopen(hts) && readguarded(hts) + yield() + serverlatencies = receivetimes - sendtimes + clientlatencies = receivereplytimes - replytimes + return id, serverlatencies, clientlatencies +end +""" +Use the next browser in line, start a new HTS-BCE connection and collect n evaluations. +This is one sample. +Returns browser name and vectors with n rows: +# t1 Send 0, receive 0, measure time interval +# t2 Send 0, receive 0 twice, measure time interval +# t3 Send x, receive 0, measure time interval +# t4 Send x, receive 0, receive x, measure time interval +""" +function HTS_BCE(n, x) + id = "HTS_BCE" + zlog(id) + zflush() + msg = "" + (hts, browser) = get_hts_bce() + if browser == "" + msg = "Could not find and open more browser types" + elseif !isa(hts, WebSocket) + msg = " could not get ws reference from " * browser * " via HTS" + end + if msg != "" + clog(id, msg) + return "", Vector{Int}(), Vector{Int}(), Vector{Int}(), Vector{Int}() + end + clog(id, hts) + # Random seeding, same for all samples + srand(1) + clog(id, "Sending ", n, " messages of ", x , " random bytes") + st1 = UInt64(0) + st2 = UInt64(0) + rt1 = UInt64(0) + rt2 = UInt64(0) + rt3 = UInt64(0) + rt4 = UInt64(0) + + t1 = Vector{Int}() + t2 = Vector{Int}() + t3 = Vector{Int}() + t4 = Vector{Int}() + + for i = 1:n + # Send empty message, measure time after two empty replies + msg = Vector{UInt8}() + st1 = time_ns() + write(hts, msg) + read(hts) + rt1 = time_ns() + read(hts) + rt2 = time_ns() + + # Send message, measure time after one empty and one echoed replies + msg = rand(0x20:0x7f, x) + st2 = time_ns() + write(hts, msg) + # throw away replies. We expect a sequence per sent message: + # two empty messages, then the original content. + read(hts) + rt3 = time_ns() + read(hts) + rt4 = time_ns() + # Store time intervals + push!(t1, Int(rt1 - st1)) + push!(t2, Int(rt2 - st1)) + push!(t3, Int(rt3 - st2)) + push!(t4, Int(rt4 - st2)) + end + # + zlog(id, "Close websocket") + close(hts) + # Also yield to the server task so it can release it's reference. + sleep(1) + # Return browser name and measured time interval vectors [ns] + browser, t1, t2, t3, t4 +end + +" +Constant message size [b], measured time interval vectors [ns] + -> server and client speeds, server and client bandwidth [ns/b] +" + +function serverandclientspeeds(messagesize, serverlatencies, clientlatencies) + serverspeeds = serverlatencies / messagesize + clientspeeds = clientlatencies / messagesize + n = length(serverspeeds) + serverbandwidth = sum(serverlatencies) / (n * messagesize) + clientbandwidth = sum(clientlatencies) / (n * messagesize) + serverspeeds, clientspeeds, serverbandwidth, clientbandwidth +end + + +" +Measured time interval vectors [ns] -> client and server speeds, bandwidth [ns/b] +x is message size [b]. +" +function serverandclientspeeds_indirect(x, t1, t2, t3, t4) + # assuming a measured time interval consists of + # t(x) = t0s + t0c + a*x + b*x + # where + # t(x) measured time at the server + # t0s initial serverlatency, for a "zero length" message + # t0c initial clientlatency, for a "zero length" message + # x message length [b] + # a marginal server speed [ns/b] + # b marginal client speed [ns/b] + # + # + # t1 = t0s + t0c Send 0, receive 0, measure t1 + # t2 = t0s + 2t0c Send 0, receive 0 twice, measure t2 + # t3 = t0s + t0c + a*x Send x, receive 0, measure t3 + # t4 = t0s + 2t0c + a*x + b*x Send x, receive 0, receive x, measure t4 + # + # hence, + t0s = 2t1 - t2 + t0c = -t1 + t2 + a = (- t1 + t3) / x + b = (t1 - t2 - t3 + t4) / x + # Drop the marginal speed info, return effective speeds + # serverspeeds = (t0s + a*x) / x + # clientspeeds = (t0s + a*x) / x + serverspeeds = t0s / x + a + clientspeeds = t0c / x + b + n = length(serverspeeds) + serverbandwidth = sum(serverspeeds) / n + clientbandwidth = sum(clientspeeds) / n + return serverspeeds, clientspeeds, serverbandwidth, clientbandwidth +end + + +## Note that the shorthand plot functions below require input symbols +## that are defined at module-level (not in a local scope) + +"Generate a time series lineplot" +lp(sy::Symbol) = lineplot(collect(1:length(eval(sy))), eval(sy), title = String(sy), width = displaysize(STDOUT)[2]-20, canvas = AsciiCanvas) + + +"Generate a vector of time series lineplots with a common title prefix" +function lp(symbs::Vector{Symbol}, titleprefix) + map(symbs) do sy + pl = lp(sy) + title!(pl, titleprefix * " " * title(pl)) + end +end + +"Generate an x-y lineplot in REPL" +function lp(syx::Symbol, syy::Symbol) + lpl = lineplot(eval(syx), eval(syy), title = String(syy), width = displaysize(STDOUT)[2]-20, canvas = AsciiCanvas) + xlabel!(lpl, String(syx)) +end + +"Generate a vector of x-y lineplots with a common title prefix" +function lp(syxs::Vector{Symbol}, syys::Vector{Symbol}, titleprefix) + map(zip(syxs, syys)) do pair + pl = lp(pair[1], pair[2]) + title!(pl, titleprefix * " " * title(pl)) + end +end + +"Generate an x-y scatterplot" +function sp(syx::Symbol, syy::Symbol) + spl = scatterplot(eval(syx), eval(syy), title = String(syy), width = displaysize(STDOUT)[2]-15, canvas = DotCanvas) + xlabel!(spl, String(syx)) +end \ No newline at end of file diff --git a/benchmark/functions_open_browsers.jl b/benchmark/functions_open_browsers.jl new file mode 100644 index 0000000..d22492d --- /dev/null +++ b/benchmark/functions_open_browsers.jl @@ -0,0 +1,174 @@ +# Included in benchmark_2.jl + +"A list of potentially available browsers, to be tried in succession if present" +const BROWSERS = ["chrome", "firefox", "iexplore", "safari", "phantomjs"] +"An complicated browser counter." +mutable struct Countbrowser;value::Int;end +(c::Countbrowser)() =COUNTBROWSER.value += 1 +"For next value: COUNTBROWSER(). For current value: COUNTBROWSER.value" +const COUNTBROWSER = Countbrowser(0) +const PORT = 8000 +const URL = "http://127.0.0.1:$PORT/bce.html" + +"Get application path for developer applications" +function fwhich(s) + fi = "" + if Sys.is_windows() + try + fi = split(readstring(`where.exe $s`), "\r\n")[1] + if !isfile(fi) + fi = "" + end + catch + fi ="" + end + else + try + fi = readchomp(`which $s`) + catch + fi ="" + end + end + fi +end +function browser_path_unix_apple(shortname) + trypath = "" + if shortname == "chrome" + if Base.Sys.is_apple() + return "Google Chrome" + else + return "google-chrome" + end + end + if shortname == "firefox" + return "firefox" + end + if shortname == "safari" + if Base.Sys.is_apple() + return "safari" + else + return "" + end + end + if shortname == "phantomjs" + return fwhich(shortname) + end + return "" +end +function browser_path_windows(shortname) + # windows accepts English paths anywhere. + # Forward slash is acceptable too. + # Searches C and D drives.... + trypath = "" + homdr = ENV["HOMEDRIVE"] + path32 = homdr * "/Program Files (x86)/" + path64 = homdr * "/Program Files/" + if shortname == "chrome" + trypath = path64 * "Chrome/Application/chrome.exe" + isfile(trypath) && return trypath + trypath = path32 * "Google/Chrome/Application/chrome.exe" + isfile(trypath) && return trypath + end + if shortname == "firefox" + trypath = path64 * "Mozilla Firefox/firefox.exe" + isfile(trypath) && return trypath + trypath = path32 * "Mozilla Firefox/firefox.exe" + isfile(trypath) && return trypath + end + if shortname == "safari" + trypath = path64 * "Safari/Safari.exe" + isfile(trypath) && return trypath + trypath = path32 * "Safari/Safari.exe" + isfile(trypath) && return trypath + end + if shortname == "iexplore" + trypath = path64 * "Internet Explorer/iexplore.exe" + isfile(trypath) && return trypath + trypath = path32 * "Internet Explorer/iexplore.exe" + isfile(trypath) && return trypath + end + if shortname == "phantomjs" + return fwhich(shortname) + end + return "" +end +"Constructs launch command" +function launch_command(shortbrowsername) + if Sys.is_windows() + pt = browser_path_windows(shortbrowsername) + else + pt = browser_path_unix_apple(shortbrowsername) + end + pt == "" && return `` + if shortbrowsername == "iexplore" + prsw = "-private" + else + prsw = "--incognito" + end + if shortbrowsername == "phantomjs" + if isdefined(:SRCPATH) + script = joinpath(SRCPATH, "phantom.js") + else + script = "phantom.js" + end + return Cmd(`$pt $script $URL`) + else + if Sys.is_windows() + return Cmd( [ pt, URL, prsw]) + else + if Sys.is_apple() + return Cmd(`open --fresh -n $URL -a $pt --args $prsw`) + elseif Sys.is_linux() || Sys.is_bsd() + return Cmd(`xdg-open $(URL) $pt`) + end + end + end +end + + +function open_testpage(shortbrowsername) + id = "open_testpage" + dmc = launch_command(shortbrowsername) + if dmc == `` + clog(id, "Could not find " * shortbrowsername) + return false + else + try + if shortbrowsername == "phantomjs" + # Run enables text output of phantom messages in the REPL. In Windows + # standalone REPL, run will freeze the main thread if not run async. + @async run(dmc) + else + spawn(dmc) + end + catch + clog(id, :red, "Failed to spawn " * shortbrowsername) + return false + end + end + return true +end + +"Try to open one browser from BROWSERS. +In some cases we expect an immediate indication +of failure, for example when the corresponding file +is not found on the system. In other cases, we will +just wait in vain for a request. In those cases, +call this function again. It remembers which browsers +were tried before. +" +function open_a_browser() + id = "open_next_browser" + if COUNTBROWSER.value > length(BROWSERS) + return false + end + success = false + b = "" + while COUNTBROWSER.value < length(BROWSERS) && !success + b = BROWSERS[COUNTBROWSER()] + clog(id, "Trying to launch browser no. ", COUNTBROWSER.value, ", ", :yellow, b) + success = open_testpage(b) + end + success && clog(id, "seems to work:", :yellow, b, :normal, " on ", URL) + success, b +end diff --git a/benchmark/logs/benchmark_results_readable.log b/benchmark/logs/benchmark_results_readable.log new file mode 100644 index 0000000..5fcf32d --- /dev/null +++ b/benchmark/logs/benchmark_results_readable.log @@ -0,0 +1,968 @@ +HTS_BCE iexplore Varying message size: + bestserverbandwidth = 2.9396 [ns/b] = [s/GB] @ size = 1762560 b + bestclientbandwidth = 2.9335 [ns/b] = [s/GB] @ size = 349860 b + bestserverlatency = 111936 [ns] @ size = 8160 b + bestclientlatency = 21268 [ns] @ size = 1020 b + +HTS_BCE chrome Varying message size: + bestserverbandwidth = 6.1808 [ns/b] = [s/GB] @ size = 1357620 b + bestclientbandwidth = 4.4656 [ns/b] = [s/GB] @ size = 27540 b + bestserverlatency = 84456 [ns] @ size = 1020 b + bestclientlatency = 21596 [ns] @ size = 1020 b + +HTS_BCE phantomjs Varying message size: + bestserverbandwidth = 8.5377 [ns/b] = [s/GB] @ size = 1357620 b + bestclientbandwidth = 1.759 [ns/b] = [s/GB] @ size = 8160 b + bestserverlatency = 135242 [ns] @ size = 1020 b + bestclientlatency = 2516 [ns] @ size = 1020 b + +HTS_BCE firefox Varying message size: + bestserverbandwidth = 6.5741 [ns/b] = [s/GB] @ size = 127500 b + bestclientbandwidth = 5.0461 [ns/b] = [s/GB] @ size = 65280 b + bestserverlatency = 370618 [ns] @ size = 8160 b + bestclientlatency = 34604 [ns] @ size = 1020 b + +HTS_JCE Varying message size: + bestserverbandwidth = 1.7232 [ns/b] = [s/GB] @ size = 220320 b + bestclientbandwidth = 4.0568 [ns/b] = [s/GB] @ size = 220320 b + bestserverlatency = 86726 [ns] @ size = 8160 b + bestclientlatency = 66992 [ns] @ size = 1020 b + +Benchmark Plots of all samples :init_plots [ns/b], message size 130560 b 200 samples + HTS_BCE iexplore serverspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │ . │ + │ | │ + │ | │ + │ [ │ + │ . | [ │ + │ | l [ │ + │ ] ] [ │ + │ ] ] [ │ + │ ||| O N │ + │ ||/. N N │ + │. \_/|||.N.., N . . .. .. .. .│ + │"`` `/^f"Vl|^l./uN/./-`/..._.\____/Lr.^_\_.`-^..r.v-JK.u`"\._,/"-_/.-\r//-.__./│ + 0 │ ' │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE iexplore clientspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │ │ + │ || │ + │ , || │ + │ | || │ + │ | || │ + │ | || │ + │ [ || │ + │ || || │ + │ || || │ + │ || || │ + │ . || |\ . │ + │.vv,/uA. n _._..__.__\_.,.._..u__._/_../.,..dl/....._/_.../.\._u//..u_.|l_M.,.__│ + 0 │ ' "" " ` `` \ `''` " ` "" "'`` '"`"`'` """`'"' `' '` `"` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE chrome serverspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 50 │ │ + │ │ + │ || │ + │ || │ + │ || │ + │ || │ + │ . || │ + │ ,, | || │ + │ || ] || │ + │ |] ] || │ + │ |||| || || │ + │. |||| ||..\ │ + │|.. |\`U| ... ...,. . . _ . . |__ . . . .\. ||A|| │ + │ ""`| ''/\f"""'"T'\-/'"^`---v/-//`\^/`--/"/`/`/-\--^-`-V` F\`/\v""""/`^//``"/\Y│ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE chrome clientspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ ,, │ + │ || || │ + │ || || │ + │ || || │ + │ || || │ + │ || || │ + │ || || │ + │ || || │ + │ || || . │ + │ , || |\ | │ + │ .|] /. .| /, │ + │| A.|l|| \ || || .,.. || .│ + │|/`'`' /"`^-/"/\-----/\----`-^---/\------/\v-r---------/|/`-/"\\f\/r--^"`-`""\/`│ + 0 │ "` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE phantomjs serverspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 50 │ │ + │ . │ + │ | │ + │ l │ + │ ] │ + │ ] │ + │ ] │ + │ | | ] │ + │ | | ] │ + │ ,\ ,\ N │ + │ .||, || N . │ + │.\ J/|/,.| . ,,N. . , , .,\ \ │ + │""\``\|-`\-/^--^-/`"^-\vv__.__v`-._../._r/--/L___...//\v--n--r^v-r-v---/"'-O_/_u│ + │ '" `"` │ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE phantomjs clientspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 30 │ . │ + │ | │ + │ | │ + │ ] │ + │ ] │ + │ ] │ + │ ] │ + │ ] │ + │ ] │ + │ J │ + │ N │ + │ N │ + │ .. N │ + │.. .L.......... .._ . . ,__....... . . .__.... ...N. .._..._.. │ + 0 │ '"` '""`"`""''"`'"T""""""""""` '``'``'"""""""""'""`' '"`""`""``""""""`"'" """│ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE firefox serverspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 50 │ . │ + │ | │ + │ [ │ + │ | [ │ + │ |. . [ │ + │||d l . . , N │ + │||/| ,.N.../| . | , .. . . /. . N .., │ + │/||\/'"|l|l|\.M ,.M /Nk\ .. ..._.Ar., .\ /| /,||, J. . . N|l.l _ │ + │ '` ' "/"`|| /`K|-`'''"`'"-/""` `'"""\r-`""`"`"``/"^^/-//`"`/-/`\-/``"`""`"│ + │------------d+----------------------------A------------------------------------*│ + │ || " │ + │ [ │ + │ F │ + │ | │ + -30 │ | │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE firefox clientspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 40 │ . │ + │ | │ + │ | │ + │ ] │ + │ , ] │ + │ | ] │ + │ ] . ] │ + │ ] | ] │ + │ ], [ ] │ + │ J[ [ N │ + │ NM [ | | l N │ + │ . NN N.. [ /. . . . /, , . N │ + │/.,J/V",N.|\..^ ||. ..,l___. _.ADAk._rl. .. |\.k._.A .. .. ....... .\_N,.-.│ + │ ''"" /"`'"``"\``'"\"` "" `"`'" ""''\/ ' ` "`"/"`'/"`""'"`'"`"""` "` '│ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_JCE serverspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 500 │ │ + │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │l │ + │| │ + │| │ + │| ,| |│ + 0 │|________JL___________________________________________________________________Jl│ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_JCE clientspeeds + ┌────────────────────────────────────────────────────────────────────────────────┐ + 800 │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + │| │ + │| │ + │| │ + 0 │|________________/________________________________n________________.____________│ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 200 + +Benchmark Tables of all samples, :init_tables, message size 130560 b 200 samples +HTS_BCE iexplore +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +5.42427 3.1758 +4.97737 3.8266 +4.83492 3.51935 +4.18971 3.15345 +4.37963 4.13943 +6.50241 2.52779 +5.34885 4.24557 +5.6142 2.78196 +5.55555 3.60594 +⋮ +3.24562 2.60879 +3.28752 2.5669 +3.19256 2.78755 +3.4998 3.37132 +3.56962 3.32104 +3.47466 3.18696 +5.73431 3.22328 +3.36014 3.3741 + +HTS_BCE chrome +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +10.2927 7.4046 +8.92965 4.02491 +5.71755 4.22602 +6.19517 4.39919 +5.86558 4.78186 +6.90184 7.54985 +6.31528 5.58627 +5.9382 5.78738 +5.46337 4.96061 +⋮ +5.08071 4.6394 +4.97178 4.24835 +42.9277 1.59208 +5.74828 4.85168 +5.12261 5.06117 +6.17283 4.8405 +4.41316 5.84882 +6.86831 5.42707 + +HTS_BCE phantomjs +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +10.7955 2.6032 +9.97428 2.201 +12.7674 2.0753 +10.762 1.91608 +9.67542 1.9133 +9.40448 1.92727 +8.25651 1.73454 +10.4715 1.99429 +13.4378 2.37138 +⋮ +7.25935 1.98592 +8.3403 1.97475 +7.49118 1.96637 +7.17835 2.16747 +6.93816 1.96357 +7.88503 1.9943 +7.36271 1.96357 +7.45209 2.00547 + +HTS_BCE firefox +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +10.7843 6.35159 +11.0524 7.2845 +33.297 5.45499 +6.34042 5.06396 +7.3236 6.49683 +19.7167 5.31812 +24.2975 5.25388 +11.1697 6.02479 +11.2871 8.4632 +⋮ +6.18401 5.99407 +6.11976 5.91586 +6.83201 6.96329 +10.7564 6.69794 +7.45767 6.33204 +6.58063 5.51923 +6.61694 4.86844 +6.37115 6.48845 + +HTS_JCE +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +444.176 751.409 +2.48869 4.01094 +2.41886 4.08915 +1.70661 3.97742 +3.14507 4.03329 +1.65912 3.8657 +1.54181 3.8238 +1.52505 3.83218 +1.63119 3.85173 +⋮ +1.57253 3.77911 +1.56974 3.7847 +1.56415 3.79029 +1.59209 3.9886 +1.57533 3.80146 +57.2649 4.32936 +1.82392 3.84615 +1.74292 3.84335 + +Benchmark Plots of varying size messages :test_plots [ns/b], + VSIZE = [1762560, 1357620, 1020000, 743580, 522240, 349860, 220320, 127500, 65280, 27540, 8160, 1020] + HTS_BCE iexplore serverbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 200 │ │ + │ │ + │ │ + │ │ + │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + 0 │'---__________________________________________________________________. │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE iexplore clientbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 30 │ │ + │ │ + │ │ + │ │ + │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │"""\----/-------------------------------------------------------------* │ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome serverbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 90 │ │ + │| │ + │| │ + │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │", │ + │ -.___________________________________________________________________. │ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome clientbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 30 │ │ + │ │ + │ │ + │ │ + │. │ + │| │ + │| │ + │| │ + │. │ + │| │ + │\ │ + │| ________----------/"""""""""` │ + │"u-----/"""""""""""----------""""""""""""" │ + │ │ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs serverbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 200 │ │ + │ │ + │ │ + │ │ + │ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\. │ + 0 │ '""\-----/""""""""""""""""""""""`------------------------------------* │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs clientbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 2.7 │ │ + │ ._.--/"` │ + │ __r--""` │ + │. __r-/"" │ + │| ,\. ._.--"" │ + │| / '.. ._--------"""""` │ + │| | '-._. ._-/` │ + │| . """\------"` │ + │| | │ + │|,,` │ + │N"\ │ + │N │ + │[ │ + │[ │ + 1.7 │` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox serverbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 500 │ │ + │ │ + │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │. │ + 0 │".____________________________________________________________________. │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox clientbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │r..._.--------------------.____.-----------------------------------/""` │ + │ '` │ + 0 │ │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE serverbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 90 │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + 0 │ """-__________________________.------------__________________________. │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE clientbandwidth + ┌────────────────────────────────────────────────────────────────────────────────┐ + 70 │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │L │ + │ `.__ │ + 0 │ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + +Benchmark Tables of varying size messages :test_tables [ns/b] +HTS_BCE iexplore +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 2.93964 3.00496 +1357620 3.03359 3.05538 +1020000 3.18315 3.09781 +743580 3.269 3.21041 +522240 3.16076 3.25224 +349860 3.95211 2.93349 +220320 3.41565 3.35517 +127500 3.63724 3.05993 +65280 5.3764 3.56742 +27540 7.03191 3.41558 +8160 13.7176 3.85587 +1020 141.73 20.8507 + +HTS_BCE chrome +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 6.58524 7.82265 +1357620 6.18083 7.01997 +1020000 7.55952 5.901 +743580 7.82293 5.34699 +522240 7.23085 5.21598 +349860 7.32768 5.6382 +220320 6.54803 5.56201 +127500 7.16353 4.80599 +65280 7.9837 4.82014 +27540 9.56091 4.46564 +8160 68.4336 13.2017 +1020 82.8002 21.1724 + +HTS_BCE phantomjs +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 8.76095 2.63199 +1357620 8.53769 2.48405 +1020000 8.65545 2.34874 +743580 8.99798 2.33596 +522240 9.89303 2.19347 +349860 9.0338 2.21736 +220320 8.81227 2.28016 +127500 8.71783 2.41444 +65280 9.74343 2.00539 +27540 13.8662 2.06535 +8160 26.9265 1.75901 +1020 132.59 2.46688 + +HTS_BCE firefox +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 7.50156 7.21121 +1357620 7.36962 6.64654 +1020000 7.76317 6.77497 +743580 8.18686 6.10902 +522240 9.11455 6.4394 +349860 10.6628 7.03435 +220320 8.41041 6.64883 +127500 6.5741 6.13918 +65280 7.69537 5.04611 +27540 14.0782 7.07422 +8160 45.4188 6.10177 +1020 422.115 33.9253 + +HTS_JCE +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 1.92309 4.34566 +1357620 1.90076 4.3161 +1020000 2.0336 4.28842 +743580 1.99315 4.26777 +522240 1.89237 4.32442 +349860 1.75345 4.2868 +220320 1.72322 4.05675 +127500 1.98503 4.5993 +65280 5.84204 5.90265 +27540 5.11944 8.51887 +8160 10.6282 10.8941 +1020 85.0579 65.6785 + +Benchmark Plots of varying size messages :test_latency_plots [ns], + VSIZE = [1762560, 1357620, 1020000, 743580, 522240, 349860, 220320, 127500, 65280, 27540, 8160, 1020] + HTS_BCE iexplore serverlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 6000000 │ │ + │ │ + │ ._.-/"` │ + │ ._.-/"` │ + │ ._.-/"` │ + │ __r-/"` │ + │ ._--"" │ + │ ._.-/"` │ + │ ._--"` │ + │ _r-"` │ + │ .__-/" │ + │ .r--"""` │ + │ _-/` │ + │ .__-/" │ + 0 │./"` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + +PhantomJS saved render, exits after 30s + HTS_BCE iexplore clientlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 6000000 │ │ + │ _. │ + │ ._r-/" │ + │ _.-/"` │ + │ __--"" │ + │ ._r-/" │ + │ ._--"` │ + │ ._.-/"` │ + │ __-/"` │ + │ _.-/" │ + │ ._r-"" │ + │ ._-/` │ + │ .__--/` │ + │ ..-"` │ + 0 │_r-/"` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome serverlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ .__--/ │ + │ __r-/"` │ + │ ._______.-/"" │ + │ .__r--""""""` │ + │ ._.--""` │ + │ __-/"` │ + │ .__--/"" │ + │ __r-/"` │ + 0 │n_---""" │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome clientlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ ._/ │ + │ ._r-"` │ + │ ._r-"` │ + │ ._r-"` │ + │ ._-/"` │ + │ __-/"` │ + │ __.-/" │ + │ __r-/"" │ + │ .__.---""" │ + │ .__r---/"""` │ + 0 │___.--/""` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs serverlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ ._-* │ + │ ._.-/"` │ + │ __-/"` │ + │ ._.--"" │ + │ __r-/"` │ + │ .__--"" │ + │ .__--/"` │ + │ __.--""` │ + │ ._r-"" │ + │ ._.-/` │ + │ __--"` │ + 0 │_r--"" │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs clientlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 5000000 │ │ + │ ._-/` │ + │ ..-/` │ + │ _r-"` │ + │ ._r/" │ + │ _.-"` │ + │ ._-/" │ + │ ._r-"` │ + │ ._--"` │ + │ ._r-/"` │ + │ ._r-"` │ + │ __r-"` │ + │ .__--"" │ + │ ._.-/"` │ + 0 │__--"` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox serverlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ │ + │ ._.-/"` │ + │ .__--""` │ + │ .__.--/"` │ + │ .__---""` │ + │ .__--/""` │ + │ .__.--/""` │ + │ .__.---""` │ + │ _-/"` │ + │ _./" │ + 0 │_.---/" │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox clientlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ │ + │ ._r* │ + │ ._r-/"` │ + │ ._.-/"` │ + │ .__r--/"` │ + │ .__--/""` │ + │ ._.-/"` │ + │ .___--/"` │ + │ .___---/""` │ + │ __r-/""` │ + 0 │___r-/"" │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE serverlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 4000000 │ │ + │ │ + │ ._r-/ │ + │ ._--"` │ + │ _.-/"` │ + │ ._.--"" │ + │ ._.--""` │ + │ _.-/"` │ + │ _.-/" │ + │ _.-/" │ + │ ._-/" │ + │ ._-/"` │ + │ __-/"` │ + │ .r._.__--"" │ + 0 │r/ ` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE clientlatency + ┌────────────────────────────────────────────────────────────────────────────────┐ + 8000000 │ ._/ │ + │ _.-"` │ + │ _.-/" │ + │ ._-/" │ + │ ._-/"` │ + │ ._-/"` │ + │ _r-"` │ + │ _.-/" │ + │ _.-/" │ + │ _.-/" │ + │ ._r-/" │ + │ _r-"` │ + │ _.-/" │ + │ ._.-/" │ + 0 │_-/"` │ + └────────────────────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + +Benchmark Tables of varying size messages :test_latency_tables [ns] +HTS_BCE iexplore +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 5.1813e6 5.29642e6 +1357620 4.11847e6 4.14804e6 +1020000 3.24682e6 3.15977e6 +743580 2.43077e6 2.3872e6 +522240 1.65068e6 1.69845e6 +349860 1.38268e6 1.02631e6 +220320 7.52537e5 7.39212e5 +127500 4.63748e5 3.9014e5 +65280 3.50971e5 2.32881e5 +27540 1.93659e5 94065.2 +8160 111936.0 31463.9 +1020 144565.0 21267.7 + +HTS_BCE chrome +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.16069e7 1.37879e7 +1357620 8.39122e6 9.53045e6 +1020000 7.71071e6 6.01902e6 +743580 5.81698e6 3.97591e6 +522240 3.77624e6 2.72399e6 +349860 2.56366e6 1.97258e6 +220320 1.44266e6 1.22542e6 +127500 9.1335e5 6.12763e5 +65280 5.21176e5 3.14659e5 +27540 2.63308e5 1.22984e5 +8160 5.58418e5 1.07726e5 +1020 84456.2 21595.9 + +HTS_BCE phantomjs +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.54417e7 4.63904e6 +1357620 1.15909e7 3.37239e6 +1020000 8.82856e6 2.39572e6 +743580 6.69072e6 1.73697e6 +522240 5.16653e6 1.14552e6 +349860 3.16056e6 7.75764e5 +220320 1.94152e6 5.02365e5 +127500 1.11152e6 3.07841e5 +65280 6.36051e5 1.30912e5 +27540 3.81875e5 56879.7 +8160 2.1972e5 14353.5 +1020 1.35242e5 2516.22 + +HTS_BCE firefox +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.32219e7 1.27102e7 +1357620 1.00051e7 9.02348e6 +1020000 7.91843e6 6.91047e6 +743580 6.08759e6 4.54255e6 +522240 4.75998e6 3.36291e6 +349860 3.73048e6 2.46104e6 +220320 1.85298e6 1.46487e6 +127500 8.38198e5 782746.0 +65280 5.02354e5 3.2941e5 +27540 3.87714e5 194824.0 +8160 3.70618e5 49790.4 +1020 430557.0 34603.8 + +HTS_JCE +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 3.38956e6 7.65948e6 +1357620 2.58051e6 5.85962e6 +1020000 2.07427e6 4.37419e6 +743580 1.48207e6 3.17343e6 +522240 9.88273e5 2.25838e6 +349860 6.13464e5 1.49978e6 +220320 3.7966e5 8.93783e5 +127500 2.53091e5 5.8641e5 +65280 3.81368e5 385325.0 +27540 1.40989e5 2.3461e5 +8160 86726.2 88896.0 +1020 86759.1 66992.1 + +Benchmark Dictionary :test_bestserverlatencies [ns] +HTS_BCE iexplore => 111936 +HTS_BCE chrome => 84456 +HTS_BCE phantomjs => 135242 +HTS_BCE firefox => 370618 +HTS_JCE => 86726 +Benchmark Dictionary :test_bestclientlatencies [ns] +HTS_BCE iexplore => 21268 +HTS_BCE chrome => 21596 +HTS_BCE phantomjs => 2516 +HTS_BCE firefox => 34604 +HTS_JCE => 66992 +Benchmark Dictionary :test_bestserverbandwidths [ns/b] +HTS_BCE iexplore => 2.9396 +HTS_BCE chrome => 6.1808 +HTS_BCE phantomjs => 8.5377 +HTS_BCE firefox => 6.5741 +HTS_JCE => 1.7232 +Benchmark Dictionary :test_bestclientbandwidths [ns/b] +HTS_BCE iexplore => 2.9335 +HTS_BCE chrome => 4.4656 +HTS_BCE phantomjs => 1.759 +HTS_BCE firefox => 5.0461 +HTS_JCE => 4.0568 diff --git a/benchmark/logs/benchmark_results_readable_previous.log b/benchmark/logs/benchmark_results_readable_previous.log new file mode 100644 index 0000000..513417f --- /dev/null +++ b/benchmark/logs/benchmark_results_readable_previous.log @@ -0,0 +1,968 @@ +HTS_BCE iexplore Varying message size: + bestserverbandwidth = 2.843 [ns/b] = [s/GB] @ size = 1357620 b + bestclientbandwidth = 3.0215 [ns/b] = [s/GB] @ size = 220320 b + bestserverlatency = 142514 [ns] @ size = 1020 b + bestclientlatency = 19971 [ns] @ size = 1020 b + +HTS_BCE chrome Varying message size: + bestserverbandwidth = 5.2447 [ns/b] = [s/GB] @ size = 220320 b + bestclientbandwidth = 4.4889 [ns/b] = [s/GB] @ size = 27540 b + bestserverlatency = 192733 [ns] @ size = 1020 b + bestclientlatency = 32490 [ns] @ size = 1020 b + +HTS_BCE phantomjs Varying message size: + bestserverbandwidth = 7.994 [ns/b] = [s/GB] @ size = 220320 b + bestclientbandwidth = 1.6196 [ns/b] = [s/GB] @ size = 27540 b + bestserverlatency = 266542 [ns] @ size = 8160 b + bestclientlatency = 2883 [ns] @ size = 1020 b + +HTS_BCE firefox Varying message size: + bestserverbandwidth = 6.1446 [ns/b] = [s/GB] @ size = 127500 b + bestclientbandwidth = 4.8754 [ns/b] = [s/GB] @ size = 127500 b + bestserverlatency = 435462 [ns] @ size = 1020 b + bestclientlatency = 35413 [ns] @ size = 1020 b + +HTS_JCE Varying message size: + bestserverbandwidth = 1.6133 [ns/b] = [s/GB] @ size = 220320 b + bestclientbandwidth = 4.5496 [ns/b] = [s/GB] @ size = 127500 b + bestserverlatency = 61228 [ns] @ size = 1020 b + bestclientlatency = 56592 [ns] @ size = 1020 b + +Benchmark Plots of all samples :init_plots [ns/b], message size 130560 b 200 samples + HTS_BCE iexplore serverspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 20 │ │ + │ │ + │ , │ + │ | | │ + │ | .|| │ + │ | ||| │ + │ ] ||| │ + │ O ||| │ + │ N [|| │ + │ .Nl N|\ , │ + │ ||^.N||, . . 1 , .. , |, . │ + │\ ./"`]|/l[.N [ ..."^k| ||.[ |M| |.,| .._.|\ .v J\ │ + │lKv` \M"A^.|K" `\\ |KG^./"/_kV||/. ,... ,\''\/|.//'P`"\k||│ + │ ` "` """ " ` `" "`"`"/"/""`" """ '''│ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE iexplore clientspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │ | │ + │ | | │ + │ | | │ + │ [ | │ + │ [ | │ + │ [ ] │ + │ [ ] │ + │ [ /. │ + │ [ || │ + │ , [ || │ + │ || N || │ + │._..^/.^/l__,..._.___/_...._.___.__.N... \... .L_runl._.v.|||...│ + 0 │ "`"` ' ''`" " ' "`"`""""``""" `" ` '"'"'│ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE chrome serverspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 30 │ │ + │ │ + │ │ + │ | │ + │ | │ + │ | . │ + │ | | │ + │ k | │ + │ .N [ │ + │ |N M │ + │G, ||N . N │ + │]L ./| \. l / , N. _ .. |. _ │ + │//F"/ \/| .. O .`"../, rK\.\r_|L`\,r"V, .v/.. ._,.\"^\Y\r.\./`W│ + │ `'""`""""" '`""` '` \ '` '\" ""``'` "" ' │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE chrome clientspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ | | , │ + │ | | | │ + │ | || | │ + │ | | || | │ + │ | |. || l │ + │ | || || ] │ + │ .| || || ] │ + │ || || || ] │ + │ , |. || || N │ + │ l || || || N │ + │. ] || || |. N │ + │|.O.u,LN| || , . . ||. . , N. ,,│ + │'"""'`''"`^^^/--/``"--^`-/""-""""`//f\/^---\-/--^/`fK"/"""/--`/""│ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE phantomjs serverspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 30 │ │ + │ │ + │ . │ + │ | │ + │ | │ + │ l │ + │|, ] │ + │|| ] , │ + │/| N, | . l , │ + │/|\ NA J /. . ] , \ , [d│ + │ /l N| N./\,, ,. .. \../.\ .vJ| , l. /, l. /, ||]│ + │ r`"u|v` H\^/\\.v\^/`^`"K`'^`v"v^'"'`/\//\-``JJ/v\/^"YK`ln\^fl/│ + │ │ + │ │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE phantomjs clientspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │ │ + │ | │ + │ | │ + │ | │ + │ | │ + │ | │ + │ | │ + │ || │ + │ || │ + │ || │ + │ || │ + │. ..\, . . . || . .. . │ + 0 │"""`"""""'""""/"""""""\/"""\`//"'""""""""""""'`^--/`"""""`"""""\-│ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE firefox serverspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 50 │ │ + │ │ + │ , | │ + │ , , | | | │ + │.|, | | |. | │ + │|N| | ] , || |, │ + │|N| l ] .] || ||, │ + │|N| O /, d],||,.|| | │ + │|N| N || ]][|||||] [ || │ + │|N\.N || , ]][/|O||/. @, @| │ + │|/||| |[ ].]][||N|||| . @| M\ │ + │|F\/| |^,N|@/\|MN|.|| | |l|| , .,|| │ + │ |`|] |'``'|l`||`\`^\_kf./n./__^^.,u../|1\ . 1. A .\J lKd,│ + │ ' "'''` "'``^-\-`f ---"-`"'' | ''│ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_BCE firefox clientspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 80 │ │ + │ | │ + │ [ │ + │ [ │ + │ [ │ + │ [ | │ + │ [ , | │ + │ [ | ] │ + │ [ [ ] │ + │ [ [ , ] │ + │ [ . [ | ] │ + │ . . . [ l . [ | /, │ + │,. l .| d. M ] l, .\ d |, || ,[| │ + │vM\V.\/N./J.Nu_N.^\_\_._.__Jl..._. ./..Nk. || . _./\|,.│ + 0 │ '' " `" ` ' ' ` " ` ' ' """`"```""""'""""""""'""" ' '"''│ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_JCE serverspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 600 │ │ + │. │ + │\ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| , , │ + 0 │l__O_______________________________________________________/u____│ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + + HTS_JCE clientspeeds + ┌─────────────────────────────────────────────────────────────────┐ + 800 │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + 0 │l_____________________/____________uL____________u____________a__│ + └─────────────────────────────────────────────────────────────────┘ + 0 200 + +Benchmark Tables of all samples, :init_tables, message size 130560 b 200 samples +HTS_BCE iexplore +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +5.15613 3.65342 +2.77637 2.99144 +2.85458 2.76242 +3.77073 3.49141 +3.15904 3.011 +3.74279 2.93838 +2.48589 2.86296 +2.80711 3.19255 +4.31818 3.96347 +⋮ +2.49427 2.92161 +3.73722 2.58085 +2.43282 2.98865 +2.53058 2.6032 +3.10876 2.91324 +3.93274 2.57527 +2.51104 2.8881 +2.50265 2.69817 + +HTS_BCE chrome +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +8.9548 8.54699 +5.32372 5.04999 +9.70335 4.56677 +7.00239 4.83212 +5.11423 5.63935 +6.103 5.27623 +4.64777 5.7371 +5.75386 14.2366 +5.86559 4.99133 +⋮ +5.80693 4.60867 +5.55275 4.56399 +6.57504 5.30975 +6.1058 4.62544 +5.98848 6.70352 +5.06116 4.28746 +4.34892 4.23439 +6.10859 6.24265 + +HTS_BCE phantomjs +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +10.9435 2.60879 +13.0188 2.89369 +17.6163 1.75409 +9.12797 2.10603 +9.91004 2.30712 +10.5469 1.91608 +11.5273 2.71493 +8.68665 3.25959 +8.15316 1.99709 +⋮ +6.71749 2.5222 +10.2117 2.02223 +13.2311 2.87413 +6.2287 1.60885 +6.61415 1.55857 +6.14769 1.55857 +11.5245 1.63958 +7.47722 1.89095 + +HTS_BCE firefox +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +10.9658 7.75375 +11.8038 5.96335 +34.255 13.826 +10.8066 6.15048 +13.1501 9.20897 +38.4643 6.82642 +20.3927 9.6028 +12.7758 5.6952 +8.42969 6.44935 +⋮ +9.13076 5.16171 +8.11127 5.36281 +6.43259 5.26785 +9.39052 7.19511 +8.01071 5.59745 +6.63649 5.2064 +5.79576 4.54164 +5.54157 4.46064 + +HTS_JCE +=> Table with 200 rows, 2 columns: +serverspeeds clientspeeds +────────────────────────── +522.696 789.896 +2.57247 4.56398 +2.92721 4.22881 +2.36858 4.18691 +2.32109 9.30672 +2.12837 4.04726 +2.05854 3.89922 +1.75688 4.2316 +1.72616 5.80413 +⋮ +1.56695 30.5988 +1.94122 5.25947 +1.61163 4.03049 +1.75688 3.88246 +1.4273 4.30143 +1.76526 4.05005 +1.80158 3.96346 +1.76806 3.86291 + +Benchmark Plots of varying size messages :test_plots [ns/b], + VSIZE = [1762560, 1357620, 1020000, 743580, 522240, 349860, 220320, 127500, 65280, 27540, 8160, 1020] + HTS_BCE iexplore serverbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 200 │ │ + │ │ + │ │ + │ │ + │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + 0 │"---.____________________________________________________. │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE iexplore clientbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 20 │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + │|--"\-.__r-----------------------------------------------/ │ + │ │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome serverbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 200 │. │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + 0 │|--------------------------------------------------------/ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome clientbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │l . _____________________.------------""""""""` │ + │""""""""""""""" │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs serverbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 300 │ │ + │ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + 0 │"\-------------------------------------------------------/ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs clientbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 3 │ │ + │, .__-/ │ + │| __--/"` │ + │| _/-._. ______r-----/"" │ + │| _-" '"-._.--/""" │ + │| , .r/ │ + │||"\___./` │ + │|/ │ + │|| │ + │l` │ + │| │ + │ │ + │ │ + │ │ + 1 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox serverbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 500 │ │ + │ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\ │ + 0 │|-_______________________________________________________. │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox clientbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 40 │ │ + │ │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │. │ + │| │ + │\ .__----------------------------------/""""""""""""--/ │ + │"""""` │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE serverbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 70 │ │ + │ │ + │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │\. │ + 0 │ '\------------------------------------------------------/ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE clientbandwidth + ┌─────────────────────────────────────────────────────────────────┐ + 60 │ │ + │, │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │| │ + │l │ + │""`._____________________________________________________. │ + 0 │ │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + +Benchmark Tables of varying size messages :test_tables [ns/b] +HTS_BCE iexplore +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 2.85556 3.31603 +1357620 2.84298 3.20976 +1020000 2.97696 3.21081 +743580 3.61289 3.16786 +522240 3.23825 3.12889 +349860 3.7709 3.19613 +220320 3.65095 3.02147 +127500 4.54435 3.65872 +65280 5.62479 3.46019 +27540 9.29125 3.46253 +8160 21.3134 5.14942 +1020 139.719 19.5797 + +HTS_BCE chrome +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 5.77379 7.83374 +1357620 5.55071 6.69094 +1020000 5.6383 5.99683 +743580 6.51132 5.74968 +522240 6.16186 5.44588 +349860 5.64196 5.07182 +220320 5.24472 5.19075 +127500 8.33349 5.36475 +65280 7.65596 4.94229 +27540 8.5631 4.48888 +8160 31.1207 8.589 +1020 188.954 31.8534 + +HTS_BCE phantomjs +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 9.21371 2.80276 +1357620 8.29873 2.54377 +1020000 8.4713 2.48141 +743580 8.8489 2.35881 +522240 9.99551 2.55679 +349860 9.1925 2.28953 +220320 7.99395 2.10036 +127500 8.62231 2.07285 +65280 11.2885 2.24716 +27540 16.7748 1.61957 +8160 32.6645 1.9753 +1020 264.541 2.82625 + +HTS_BCE firefox +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 7.93723 7.08603 +1357620 8.04689 7.23327 +1020000 8.59622 6.29865 +743580 9.22209 6.33995 +522240 9.5801 6.44354 +349860 9.96628 7.03896 +220320 10.5642 6.05941 +127500 6.14458 4.87543 +65280 9.71296 5.16403 +27540 20.6851 4.95048 +8160 67.5271 12.0259 +1020 426.923 34.7188 + +HTS_JCE +=> Table with 12 rows, 3 columns: +VSIZE serverbandwidth clientbandwidth +───────────────────────────────────────── +1762560 2.6018 5.29989 +1357620 2.03094 4.64923 +1020000 1.96231 5.01191 +743580 1.99671 4.64635 +522240 2.11014 4.69373 +349860 1.85716 4.72792 +220320 1.61326 4.60257 +127500 1.96424 4.54956 +65280 3.57493 7.10317 +27540 4.91413 7.4573 +8160 7.55778 13.2848 +1020 60.0278 55.4819 + +Benchmark Plots of varying size messages :test_latency_plots [ns], + VSIZE = [1762560, 1357620, 1020000, 743580, 522240, 349860, 220320, 127500, 65280, 27540, 8160, 1020] + HTS_BCE iexplore serverlatency + ┌─────────────────────────────────────────────────────────────────┐ + 6000000 │ │ + │ │ + │ ._-/ │ + │ _.-"` │ + │ ._-/" │ + │ ._r-/` │ + │ ._--"` │ + │ ___.--/"` │ + │ _-""" │ + │ .r" │ + │ ._r/` │ + │ .,-/"` │ + │ _r/` │ + │ ..-/" │ + 0 │-"` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE iexplore clientlatency + ┌─────────────────────────────────────────────────────────────────┐ + 6000000 │ _r/ │ + │ ../" │ + │ _-/` │ + │ ../" │ + │ ._-/` │ + │ ._-/` │ + │ ..-"` │ + │ ..-"` │ + │ ._-/` │ + │ ._-/` │ + │ ._-/` │ + │ _.-"` │ + │ ..-" │ + │ ._--/` │ + 0 │_r/` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome serverlatency + ┌─────────────────────────────────────────────────────────────────┐ + 11000000 │ │ + │ _r/` │ + │ ../" │ + │ ._-"` │ + │ _r/` │ + │ ._-/" │ + │ _r-"` │ + │ __r--"" │ + │ .r/"" │ + │ _-"` │ + │ ..-" │ + │ .r/` │ + │ _-/` │ + │ _---/" │ + 0 │.-" │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE chrome clientlatency + ┌─────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ _/ │ + │ _r/" │ + │ ..-" │ + │ ._-/` │ + │ _.-/` │ + │ ._-/" │ + │ ._.-/"` │ + │ .__--/"` │ + │ __r-/"` │ + │ .__,-/"" │ + 0 │__.--/""` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs serverlatency + ┌─────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ .. │ + │ _r/` │ + │ ../" │ + │ _-/` │ + │ ._./" │ + │ __-/"` │ + │ ._r-/" │ + │ ._r-/"` │ + │ .__--/"` │ + │ ..-"` │ + │ _r/` │ + │ ._r/" │ + 0 │.--""` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE phantomjs clientlatency + ┌─────────────────────────────────────────────────────────────────┐ + 5000000 │ ../` │ + │ ../` │ + │ ./` │ + │ _-" │ + │ ._-" │ + │ ._-/` │ + │ ._-/` │ + │ _-/` │ + │ _r/" │ + │ ._r/" │ + │ .__-/"` │ + │ .r/` │ + │ ._r"` │ + │ ._-/` │ + 0 │_.-/"` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox serverlatency + ┌─────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ ._r/ │ + │ ._.-/"` │ + │ _.-/"` │ + │ __r-/"" │ + │ ._.-/"" │ + │ ._r-/"` │ + │ ._-/"` │ + │ _.-/"` │ + │ ._-/" │ + │ .-"` │ + 0 │----/` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_BCE firefox clientlatency + ┌─────────────────────────────────────────────────────────────────┐ + 20000000 │ │ + │ │ + │ │ + │ │ + │ │ + │ .__/ │ + │ ._r-/"` │ + │ ._--""` │ + │ ..-"` │ + │ _r-"` │ + │ ._.--"" │ + │ ._.--""` │ + │ .__r--""` │ + │ __-/"` │ + 0 │___--/" │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE serverlatency + ┌─────────────────────────────────────────────────────────────────┐ + 5000000 │ │ + │ .-` │ + │ ./` │ + │ .r" │ + │ _/` │ + │ ../ │ + │ _r` │ + │ _.-/" │ + │ _.-/" │ + │ ._r-/" │ + │ ._r-/"` │ + │ ._--/"` │ + │ ..-/` │ + │ ._.-"` │ + 0 │.-""""` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + + HTS_JCE clientlatency + ┌─────────────────────────────────────────────────────────────────┐ + 10000000 │ . │ + │ .r" │ + │ .r/` │ + │ .r/` │ + │ ../` │ + │ ._r/` │ + │ ._r-/"` │ + │ ._-""` │ + │ _r/` │ + │ ..-" │ + │ ._-/"` │ + │ ._r/"` │ + │ ._-"` │ + │ ._r-"` │ + 0 │_-""` │ + └─────────────────────────────────────────────────────────────────┘ + 0 2000000 + VSIZE + +Benchmark Tables of varying size messages :test_latency_tables [ns] +PhantomJS saved render, exits after 30s +HTS_BCE iexplore +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 5.0331e6 5.84469e6 +1357620 3.85969e6 4.35764e6 +1020000 3.0365e6 3.27503e6 +743580 2.68647e6 2.35556e6 +522240 1.69114e6 1.63403e6 +349860 1.31929e6 1.1182e6 +220320 8.04377e5 6.6569e5 +127500 5.79405e5 4.66486e5 +65280 3.67186e5 2.25881e5 +27540 2.55881e5 95358.0 +8160 1.73917e5 42019.3 +1020 1.42514e5 19971.3 + +HTS_BCE chrome +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.01767e7 1.38074e7 +1357620 7.53576e6 9.08375e6 +1020000 5.75107e6 6.11676e6 +743580 4.84169e6 4.27534e6 +522240 3.21797e6 2.84406e6 +349860 1.9739e6 1.77443e6 +220320 1.15552e6 1.14363e6 +127500 1.06252e6 6.84006e5 +65280 4.99781e5 3.22632e5 +27540 2.35828e5 1.23624e5 +8160 2.53945e5 70086.3 +1020 1.92733e5 32490.5 + +HTS_BCE phantomjs +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.62397e7 4.94004e6 +1357620 1.12665e7 3.45348e6 +1020000 8.64073e6 2.53104e6 +743580 6.57986e6 1.75396e6 +522240 5.22005e6 1.33526e6 +349860 3.21609e6 8.01016e5 +220320 1.76123e6 4.6275e5 +127500 1.09934e6 2.64289e5 +65280 7.3691e5 1.46695e5 +27540 4.61979e5 44603.0 +8160 2.66542e5 16118.5 +1020 2.69832e5 2882.77 + +HTS_BCE firefox +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 1.39898e7 1.24896e7 +1357620 1.09246e7 9.82003e6 +1020000 8.76814e6 6.42462e6 +743580 6.85736e6 4.71426e6 +522240 5.00311e6 3.36507e6 +349860 3.4868e6 2.46265e6 +220320 2.3275e6 1.33501e6 +127500 7.83433e5 6.21618e5 +65280 6.34062e5 337108.0 +27540 5.69668e5 1.36336e5 +8160 5.51021e5 98131.3 +1020 4.35462e5 35413.2 + +HTS_JCE +=> Table with 12 rows, 3 columns: +VSIZE serverlatency clientlatency +───────────────────────────────────── +1762560 4.58583e6 9.34138e6 +1357620 2.75724e6 6.31189e6 +1020000 2.00155e6 5.11215e6 +743580 1.48471e6 3.45493e6 +522240 1.102e6 2.45125e6 +349860 6.49745e5 1.65411e6 +220320 3.55433e5 1.01404e6 +127500 2.5044e5 5.80069e5 +65280 2.33372e5 4.63695e5 +27540 1.35335e5 205374.0 +8160 61671.5 1.08404e5 +1020 61228.4 56591.6 + +Benchmark Dictionary :test_bestserverlatencies [ns] +HTS_BCE iexplore => 142514 +HTS_BCE chrome => 192733 +HTS_BCE phantomjs => 266542 +HTS_BCE firefox => 435462 +HTS_JCE => 61228 +Benchmark Dictionary :test_bestclientlatencies [ns] +HTS_BCE iexplore => 19971 +HTS_BCE chrome => 32490 +HTS_BCE phantomjs => 2883 +HTS_BCE firefox => 35413 +HTS_JCE => 56592 +Benchmark Dictionary :test_bestserverbandwidths [ns/b] +HTS_BCE iexplore => 2.843 +HTS_BCE chrome => 5.2447 +HTS_BCE phantomjs => 7.994 +HTS_BCE firefox => 6.1446 +HTS_JCE => 1.6133 +Benchmark Dictionary :test_bestclientbandwidths [ns/b] +HTS_BCE iexplore => 3.0215 +HTS_BCE chrome => 4.4889 +HTS_BCE phantomjs => 1.6196 +HTS_BCE firefox => 4.8754 +HTS_JCE => 4.5496 diff --git a/benchmark/phantom.js b/benchmark/phantom.js new file mode 100644 index 0000000..09aaf58 --- /dev/null +++ b/benchmark/phantom.js @@ -0,0 +1,34 @@ +// Note that the console log output does not appear in Julia REPL when +// this is called using spawn. For debugging, use "run". These functions +// are run in a shell outside the web page. +var page = require('webpage').create(), + system = require('system'), + t, address; + +if (system.args.length === 1) { + console.log('Phantomjs.js: too few arguments. Need phantom.js '); + phantom.exit(); +} +console.log('PhantomJS: The default user agent is ' + page.settings.userAgent); +page.settings.userAgent = 'PhantomJS'; +console.log('PhantomJS: User agent set to ' + page.settings.userAgent); + + +t = Date.now(); +address = system.args[1]; +page.open(address, function(status) { + if (status !== 'success') { + console.log('FAIL to load the address'); + } else { + t = Date.now() - t; + console.log('PhantomJS loading ' + system.args[1]); + console.log('PhantomJS loading time ' + t + ' msec'); + window.setTimeout( (function() { + page.render("phantomjs.png"); + console.log("PhantomJS saved render, exits after 30s") + phantom.exit() + }), + 30000) + } + } +); diff --git a/benchmark/ws_hts.jl b/benchmark/ws_hts.jl new file mode 100644 index 0000000..087a223 --- /dev/null +++ b/benchmark/ws_hts.jl @@ -0,0 +1,166 @@ +# Submodule Julia HTTP Server +# HTTP and WebSockets need to be loaded in the calling context. +# LOAD_PATH must include logutils +# Intended for accepting echoing clients, such as ws_jce.jl, and +# running echo tests with that client. +# The server stays open until close_hts or the websocket is closed. +module ws_hts +using ..HTTP +import HTTP.Header +using ..WebSockets +# We want to log to a separate file, so +# we use our own instance of logutils_ws here. +import logutils_ws: logto, clog, zlog, zflush, clog_notime +const SRCPATH = Base.source_dir() == nothing ? Pkg.dir("WebSockets", "benchmark") :Base.source_dir() +const SERVEFILE = "bce.html" +const PORT = 8000 +const SERVER = "127.0.0.1" +const WSMAXTIME = Base.Dates.Second(600) +const WEBSOCKET = Vector{WebSockets.WebSocket}() +const TCPREF = Ref{HTTP.Sockets.TCPServer}() +"Run asyncronously or in separate process" +function listen_hts() + id = "listen_hts" + try + clog(id,"listen_hts starts on ", SERVER, ":", PORT) + zflush() + HTTP.listen(SERVER, UInt16(PORT), tcpref = TCPREF) do http + if WebSockets.is_upgrade(http.message) + acceptholdws(http) + clog(id, "Websocket closed, server stays open until ws_hts.close_hts()") + else + HTTP.Servers.handle_request(handlerequest, http) + end + end + catch err + clog(id, :red, err) + clog_notime.(catch_stacktrace()[1:4]) + zflush() + end +end + +"Accepts an incoming connection, upgrades to websocket, +and waits for timeout or a closed socket. +Also stops the server from accepting more connections on exit." +function acceptholdws(http) + id = "ws_hts.acceptholdws" + zlog(id);zflush() + # If the ugrade is successful, just hold the reference and thread + # of execution. Other tasks may do useful things with it. + WebSockets.upgrade(http) do ws + if length(WEBSOCKET) > 0 + # unexpected behaviour. + if !isopen(WEBSOCKET[1]) + pop!(WEBSOCKET) + else + msg = " A websocket is already open. Not accepting the attempt at opening more." + clog(id, :red, msg);zflush() + return + end + end + push!(WEBSOCKET, ws) + zlog(id, ws);zflush() + t1 = now() + WSMAXTIME + while isopen(ws) && now() < t1 + yield() + end + length(WEBSOCKET) > 0 && pop!(WEBSOCKET) + zlog(id, " exiting");zflush() + end +end +"Returns a websocket or a string" +function getws_hts() + id = "getws_hts" + if length(WEBSOCKET) > 0 + if isopen(WEBSOCKET[1]) + zlog(id, " return reference") + return WEBSOCKET[1] + else + msg = " Websocket is referred but not open. Acceptholdws might not have been scheduled after is was closed." + clog(id, msg) + zflush() + return msg + end + else + if !isdefined(TCPREF, :x) + msg = " No server running yet, run ws_hts.listen_hts() or wait" + else + msg = " No websocket has connected yet, run ws_hce.echowithdelay_jce() or wait" + end + zlog(id, msg) + return id * msg + end +end + + + + +"HTTP request -> HTTP response." +function handlerequest(request::HTTP.Request) + id = "handlerequest" + zlog(id, request) + response = responseskeleton(request) + try + if request.method == "GET" + response = resp_HTTP(request.target, response) + elseif request.method == "HEAD" + response = resp_HTTP(request.target, response) + response.body = Array{UInt8,1}() + else + response = HTTP.Response(501, "Not implemented method: $(request.method), fix $id") + end + end + zlog(id, response) + response +end + + +""" +Tell browser about the methods this server supports. +""" +function responseskeleton(request::HTTP.Request) + r = HTTP.Response() + HTTP.Messages.appendheader(r, Header("Allow" => "GET,HEAD")) + HTTP.Messages.appendheader(r, Header("Connection" => "close")) + r # +end + +"request.target -> HTTP.Response , building on a skeleton response" +function resp_HTTP(resource::String, resp::HTTP.Response) + id = "resp_HTTP" + if resource == "/favicon.ico" + s = read(joinpath(SRCPATH, "favicon.ico")) + push!(resp.headers, "Content-Type" => "image/x-icon") + else + s = read(joinpath(SRCPATH, SERVEFILE)) + push!(resp.headers, "Content-Type" => "text/html") + end + resp.body = s + push!(resp.headers, "Content-Length" => string(length(s))) + resp.status = 200 + resp +end + +"Close the websocket, stop the server. TODO improve" +function close_hts() + clog("ws_hts.close_hts") + length(WEBSOCKET) >0 && isopen(WEBSOCKET[1]) && close(WEBSOCKET[1]) && sleep(0.5) + isassigned(TCPREF) && close(TCPREF.x) +end + +end # module +""" +For debugging: + +import HTTP +using WebSockets +const SRCPATH = Base.source_dir() == nothing ? Pkg.dir("WebSockets", "benchmark") :Base.source_dir() +const LOGGINGPATH = realpath(joinpath(SRCPATH, "../logutils/")) +# for finding local modules +SRCPATH ∉ LOAD_PATH && push!(LOAD_PATH, SRCPATH) +LOGGINGPATH ∉ LOAD_PATH && push!(LOAD_PATH, LOGGINGPATH) +import ws_hts.listen_hts +tas = @schedule ws_hts.listen_hts() +sleep(7) +hts = ws_hts.getws_hts() +""" \ No newline at end of file diff --git a/benchmark/ws_jce.jl b/benchmark/ws_jce.jl new file mode 100644 index 0000000..ad26d09 --- /dev/null +++ b/benchmark/ws_jce.jl @@ -0,0 +1,135 @@ +__precompile__(false) +""" +Submodule Julia Client Echo +Intended for running in its own worker process. +HTTP and WebSockets need to be loaded in the calling context. +LOAD_PATH must include the directory logutils_ws. +See comment at the end of this file for debugging code. +""" +module ws_jce +using ..HTTP +using ..WebSockets +# We want to log to a separate file, and so use our own +# instance of logutils_ws in this process +import logutils_ws: logto, clog, zlog, zflush, clog_notime +const SRCPATH = Base.source_dir() == nothing ? Pkg.dir("WebSockets", "benchmark") :Base.source_dir() +const LOGFILE = "ws_jce.log" + +const PORT = 8000 +const SERVER = "ws://127.0.0.1:$(PORT)" +const CLOSEAFTER = Base.Dates.Second(30) + +""" +Opens a client, echoes with an optional delay, an integer in milliseconds. +Stores time records for received messages and before sending messages. +Specify the delay in milliseconds by sending a message on the websocket: + send(ws_jce, "delay|15") +Echoes any message except "exit" and "delay". + +Delays to reading, in the websocket use situation, would be caused by usefully spent +calculation time between reads. However, they may be interpreted by the underlying protocol +as transmission problems and cause large slowdowns. Hence the interest in testing +with delays. A countermeasure for optimizing speed might be to run a websocket +reading function in a parallel, not asyncronous process, putting messages on an internal queue. + +At exit or after CLOSEAFTER, this function sends one message containing two vectors of +timestamps [ns]. +""" +function echowithdelay_jce() + # This will be run in a worker process. Even so, individual console log + # output will be redirected to process 1 and prefixed + # with a "From worker 2". + # It will also be interspersed with process 1 output, + # sometimes before a line is finished printing. + # We use :green to distinguish more easily. + id = "echowithdelay_jce" + f = open(joinpath(SRCPATH, "logs", LOGFILE), "w") + try + logto(f) + clog(id, :green, "Open client on ", SERVER, "\nclient side handler ", _jce) + zflush() + WebSockets.open(_jce, SERVER) + zlog(id, :green, " Websocket closed, control returned.") + catch err + clog(id, :red, err) + clog_notime.(catch_stacktrace()[1:4]) + zflush() + finally + clog(id, :green, " Closing log ", LOGFILE) + zflush() + logto(Base.DevNullStream()) + close(f) + end +end +" +Handler for client websocket, defined by echowithdelay_jce +" +function _jce(ws) + id = "_jce" + clog(id, :green, ws) + zflush() + receivetimes = Vector{Int64}() + replytime = Int64(0) + replytimes = Vector{Int64}() + msg = Vector{UInt8}() + delay = 0 # Integer milliseconds + t1 = now() + CLOSEAFTER + while isopen(ws) && now() < t1 + # read, record receive time + msg = read(ws) + ti = time_ns() + push!(receivetimes, Int64(ti < typemax(Int64) ? ti : 0 )) + # break out when receiving 'exit' + length(msg) == 4 && msg == Vector{UInt8}("exit") && break + # react to delay instruction + if length(msg) < 16 && msg[1:6] == Vector{UInt8}("delay=") + delay = parse(Int, String(msg[7:end])) + clog(id, :green, " Changing delay to ", delay, " ms") + zflush() + end + sleep(delay / 1000) + # record send time, echo + replytime = time_ns() + write(ws, msg) + # clean record of instruction message + if length(msg) > 16 && msg[1:6] != Vector{UInt8}("delay=") + push!(replytimes, Int64(replytime < typemax(Int64) ? replytime : 0 )) + elseif msg[1:6] != Vector{UInt8}("delay=") + push!(replytimes, Int64(replytime < typemax(Int64) ? replytime : 0 )) + end + end + if length(receivetimes) > 1 + # Don't include receive time for the "exit" message. Reply time was not recorded + pop!(receivetimes) + if length(receivetimes) > 1 + # Send one message with serialized receive and reply times. + buf = IOBuffer() + serialize(buf, (receivetimes, replytimes)) + if isopen(ws) + write(ws, take!(buf)) + zlog(id, :green, " Sent serialized receive and reply times.") + zflush() + end + end + end + clog(id, :green, " Exit, close websocket.") + zflush() + # Exiting this function starts a closing handshake + nothing +end +end # module + +#= +For debugging in a separate terminal: + +using HTTP +using WebSockets +const SRCPATH = Base.source_dir() == nothing ? Pkg.dir("WebSockets", "benchmark") :Base.source_dir() +const LOGGINGPATH = realpath(joinpath(SRCPATH, "../logutils/")) +# for finding local modules +SRCPATH ∉ LOAD_PATH && push!(LOAD_PATH, SRCPATH) +LOGGINGPATH ∉ LOAD_PATH && push!(LOAD_PATH, LOGGINGPATH) + +import ws_jce.echowithdelay_jce +ws_jce.echowithdelay_jce() +=# \ No newline at end of file diff --git a/examples/chat-client.html b/examples/chat-client.html index 5ae1c61..c4a4607 100644 --- a/examples/chat-client.html +++ b/examples/chat-client.html @@ -1,6 +1,9 @@ - + + + + Websockets client @@ -65,6 +67,14 @@

Select a userinput

var $chatForm = document.querySelector("#say_message"); var $chatInput = $chatForm.querySelector("input[name=say]"); + + function uuid4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ) + } + var id = uuid4(); + function addContent(html) { var $content = document.querySelector("#content"); var div = document.createElement("div"); @@ -86,7 +96,11 @@

Select a userinput

if( !uname.replace(/\s/gi,'').length ) { alert("Please select a valid userinput"); } else { - connection.send('setusername:'+ uname); + var msg = { + "id": id, + "userName": uname + }; + connection.send(JSON.stringify(msg)); $userName.innerHTML = uname; $welcome.style.display = "none"; $chat.style.display = "block"; @@ -94,12 +108,16 @@

Select a userinput

} function whenChatMessage() { - var msg = $chatInput.value; - if(!msg.replace(/\s/gi,'').length) { + var content = $chatInput.value; + if( !content.replace(/\s/gi,'').length) { /* nothing to do */ } else { - connection.send('say:'+ msg); - addContent(`

${you}: ${msg}

`); + var msg = { + "id": id, + "say": content + }; + connection.send(JSON.stringify(msg)); + addContent(`

${you}: ${content}

`); $chatInput.focus(); } } @@ -110,19 +128,14 @@

Select a userinput

whenChatMessage(); return false; }) - - $chatInput.addEventListener("keypress", (e) => { - if( e.keyCode === 13 ) { whenChatMessage(); } ; - return false; - }, false) - + $userForm.addEventListener("submit", function(e){ e.preventDefault(); e.stopImmediatePropagation(); whenUserName(); return false; }); - + const connection = new SocketConnection(onMessageReceived); connection.start(); diff --git a/examples/chat.jl b/examples/chat.jl index 8983e4d..dfaa307 100644 --- a/examples/chat.jl +++ b/examples/chat.jl @@ -1,38 +1,44 @@ using HttpServer using WebSockets +using JSON +struct User + name::String + client::WebSocket +end #global Dict to store open connections in -global connections = Dict{Int,WebSocket}() -global usernames = Dict{Int,String}() +global connections = Dict{String,User}() function decodeMessage( msg ) - String(copy(msg)) + JSON.parse(String(copy(msg))) end wsh = WebSocketHandler() do req, client global connections - @show connections[client.id] = client while true msg = read(client) msg = decodeMessage(msg) - if startswith(msg, "setusername:") - println("SETTING USERNAME: $msg") - usernames[client.id] = msg[13:end] + id = msg["id"] + if haskey(msg,"userName") && !haskey(connections,id) + uname = msg["userName"] + println("SETTING USERNAME: $(uname)") + connections[id] = User(uname,client) end - if startswith(msg, "say:") - println("EMITTING MESSAGE: $msg") + if haskey(msg,"say") + content = msg["say"] + println("EMITTING MESSAGE: $(content)") for (k,v) in connections - if k != client.id - write(v, (usernames[client.id] * ": " * msg[5:end])) + if k != id + write(v.client, (v.name * ": " * content)) end end end end end -onepage = readstring(Pkg.dir("WebSockets","examples","chat-client.html")) httph = HttpHandler() do req::Request, res::Response - Response(onepage) + onepage = readstring(Pkg.dir("WebSockets","examples","chat-client.html")) + Response(onepage) end server = Server(httph, wsh) diff --git a/examples/chat_explore.html b/examples/chat_explore.html new file mode 100644 index 0000000..c148d2e --- /dev/null +++ b/examples/chat_explore.html @@ -0,0 +1,155 @@ + + + + + + + Websockets client + + + + +
+

Select a username

+
+ + +
+
+
+
+ + +
+ + + diff --git a/examples/chat_explore.jl b/examples/chat_explore.jl new file mode 100644 index 0000000..ea91935 --- /dev/null +++ b/examples/chat_explore.jl @@ -0,0 +1,169 @@ +#= + +A chat application using both HttpServer and HTTP to do +the same thing: Start a new task for each browser (tab) that connects. + +To use: + - include("chat_explore.jl") in REPL + - start a browser on address 127.0.0.1:8000, and another on 127.0.0.1:8080 + - inspect global variables starting with 'last' while the chat is running asyncronously + +To call in from other devices, figure out your IP address on the network and change the 'gatekeeper' code. + +Note that type of 'lastreq' changes depending on whether the last call was made through HttpServer or HTTP. + +Functions used as arguments are explicitly defined with names instead of anonymous functions (do..end constructs). +This may improve debugging readability at the cost of increased verbosity. + +=# +global lastreq = 0 +global lastws= 0 +global lastmsg= 0 +global lastws= 0 + +using HttpServer +using HTTP +using WebSockets +const CLOSEAFTER = Base.Dates.Second(1800) +const HTTPPORT = 8080 +const HTTPSERVERPORT = 8000 +const URL = "127.0.0.1" +const USERNAMES = Dict{String, WebSocket}() +const HTMLSTRING = readstring(Pkg.dir("WebSockets","examples","chat_explore.html")); + +# If we are to access a websocket from outside +# it's websocket handler function, we need some kind of +# mutable container for storing references: +const WEBSOCKETS = Dict{WebSocket, Int}() + +""" +Called by 'gatekeeper', this function stays active while the +particular websocket is open. The argument is an open websocket. +Other instances of the function run in other tasks. The tasks +are generated by either HTTP or HttpServer. +""" +function usews(thisws) + global lastws = thisws + push!(WEBSOCKETS, thisws => length(WEBSOCKETS) +1 ) + t1 = now() + CLOSEAFTER + username = "" + while now() < t1 + # This next call waits for a message to + # appear on the socket. If there is none, + # this task yields to other tasks. + data, success = readguarded(thisws) + !success && break + global lastmsg = msg = String(data) + print("Received: $msg ") + if username == "" + username = approvedusername(msg, thisws) + if username != "" + println("from new user $username ") + !writeguarded(thisws, username) && break + println("Tell everybody about $username") + foreach(keys(WEBSOCKETS)) do ws + writeguarded(ws, username * " enters chat") + end + else + println(", username taken!") + !writeguarded(thisws, "Username taken!") && break + end + else + println("from $username ") + distributemsg(msg, thisws) + startswith(msg, "exit") && break + end + end + exitmsg = username == "" ? "unknown" : username * " has left" + distributemsg(exitmsg, thisws) + println(exitmsg) + # No need to close the websocket. Just clean up external references: + removereferences(thisws) + nothing +end + +function removereferences(ws) + haskey(WEBSOCKETS, ws) && pop!(WEBSOCKETS, ws) + for (discardname, wsref) in USERNAMES + if wsref === ws + pop!(USERNAMES, discardname) + break + end + end + nothing +end + + +function approvedusername(msg, ws) + !startswith(msg, "userName:") && return "" + newname = msg[length("userName:") + 1:end] + newname =="" && return "" + haskey(USERNAMES, newname) && return "" + push!(USERNAMES, newname => ws) + newname +end + + +function distributemsg(msgout, not_to_ws) + foreach(keys(WEBSOCKETS)) do ws + if ws !== not_to_ws + writeguarded(ws, msgout) + end + end + nothing +end + + +""" +`Server => gatekeeper(Request, WebSocket) => usews(WebSocket)` + +The gatekeeper makes it a little harder to connect with +malicious code. It inspects the request that was upgraded +to a a websocket. +""" +function gatekeeper(req, ws) + global lastreq = req + global lastws = ws + orig = WebSockets.origin(req) + if startswith(orig, "http://localhost") || startswith(orig, "http://127.0.0.1") + usews(ws) + else + warn("Unauthorized websocket connection, $orig not approved by gatekeeper") + end + nothing +end + +"Request to response. Response is the predefined HTML page with some javascript" +req2resp(req::HttpServer.Request, resp) = HTMLSTRING |> Response +req2resp(req::HTTP.Request) = HTMLSTRING |> HTTP.Response + +# Both server definitions need two function wrappers; one handler function for page requests, +# one for opening websockets (which the javascript in the HTML page will try to do) +server_httpserver = Server(HttpHandler(req2resp), WebSocketHandler(gatekeeper)) +server_HTTP = WebSockets.ServerWS(HTTP.HandlerFunction(req2resp), WebSockets.WebsocketHandler(gatekeeper)) + +# Start the HTTP server asyncronously, and stop it later +litas_HTTP = @schedule WebSockets.serve(server_HTTP, URL, HTTPPORT, false) +@schedule begin + println("HTTP server listening on $URL:$HTTPPORT for $CLOSEAFTER") + sleep(CLOSEAFTER.value) + println("Time out, closing down $HTTPPORT") + Base.throwto(litas_HTTP, InterruptException()) +end + +# Start the HttpServer asyncronously, stop it later +litas_httpserver = @schedule run(server_httpserver, HTTPSERVERPORT) +@schedule begin + println("HttpServer listening on $URL:$HTTPSERVERPORT for $CLOSEAFTER") + sleep(CLOSEAFTER.value + 2) + println("Time out, closing down $HTTPSERVERPORT") + Base.throwto(litas_httpserver, InterruptException()) +end + +# Note that stopping the HttpServer in a while will send an error messages to the +# console. We could get rid of the messages by binding the task to a Channel. +# However, we can't get rid of ECONNRESET messages in that way. This is +# because the errors are triggered in tasks generated by litas_httpserver again, +# and those aren't channeled anywhere. + +nothing \ No newline at end of file diff --git a/examples/repl-client.html b/examples/repl-client.html index aad5b83..91d4373 100644 --- a/examples/repl-client.html +++ b/examples/repl-client.html @@ -1,6 +1,9 @@ - + + + + Websockets client + + + +PlotlyEChartsHeadlessChromiumDataVoyagerD3TreesEscherPagesAtomPlotlyJSMuxLibExpatGSLHTTPClientLibCURLBlinkWebSocketsHttpParserCodecsGumboNullablesBufferedStreamsAbstractTreesPrimesSHAHttpServerLibzHttpCommonMbedTLSURIParserCairoOffsetArraysBaseTestNextWinRPMFixedPointNumbersRequestsHomebrewFactCheckJSONBinDepsDataStructures \ No newline at end of file diff --git a/examples/server.jl b/examples/server.jl index 0cf883b..78ac7da 100644 --- a/examples/server.jl +++ b/examples/server.jl @@ -1,10 +1,6 @@ using HttpServer using WebSockets -#global Dict to store open connections in -global connections = Dict{Int,WebSocket}() -global usernames = Dict{Int,String}() - function decodeMessage( msg ) String(copy(msg)) end @@ -19,8 +15,6 @@ function eval_or_describe_error(strmsg) end wsh = WebSocketHandler() do req, client - global connections - connections[client.id] = client while true val = client |> read |> decodeMessage |> eval_or_describe_error output = String(take!(Base.mystreamvar)) diff --git a/logutils/log_http.jl b/logutils/log_http.jl new file mode 100644 index 0000000..c8b750f --- /dev/null +++ b/logutils/log_http.jl @@ -0,0 +1,42 @@ +#= +Included in logutils xxx.jl if HTTP is loaded +=# + +"HTTP.Response already has a show method, we're not overwriting that. +This metod is called only when logging to an Abstractdevice. The default +show method does not print binary data well as per now." +function _show(d::AbstractDevice, response::HTTP.Messages.Response) + _log(d, :green, "Response status: ", :bold, response.status," ") + response.status > 0 && _log(d, HTTP.Messages.STATUS_MESSAGES[response.status], " ") + if !isempty(response.headers) + _log(d, :green, " Headers: ", :bold, length(response.headers)) + _log(d, :green, "\n", response.headers) + end + if isdefined(response, :cookies) + if !isempty(response.cookies) + _log(d, " Cookies: ", :bold, length(response.cookies)) + _log(d, "\n", response.cookies) + end + end + if !isempty(response.body) + _log(d, "\t", DataDispatch(response.body, HTTP.header(response, "content-type", ""))) + end + nothing +end + + +"HTTP.Request already has a show method, we're not overwriting that. +This metod is called only when logging to an Abstractdevice" +function _show(d::AbstractDevice, request::HTTP.Messages.Request) + _log(d, :normal, :light_yellow, "Request ", :normal) + _log(d, :bold, request.method, " ", :cyan, request.target, "\n", :normal) + if !isempty(request.body) + _log(d, "\t", DataDispatch(request.body, HTTP.header(request, "content-type", ""))) + end + if !isempty(request.headers) + _log(d, "\t", :cyan, " Headers: ", length(request.headers)) + _log(d, :cyan, "\n", request.headers) + end + nothing +end + diff --git a/logutils/log_httpserver.jl b/logutils/log_httpserver.jl new file mode 100644 index 0000000..9166ba6 --- /dev/null +++ b/logutils/log_httpserver.jl @@ -0,0 +1,87 @@ +import HttpServer.Client +import HttpServer.HttpHandler +import HttpServer.Server +import HttpServer.ClientParser +import HttpServer.HttpParser +import HttpServer.Request # Not sure if necessary, but otherwise dispatching on HttpCommon.Request can fail. +import HttpServer.Response +import HttpCommon.Cookie +import HttpCommon.STATUS_CODES +import URIParser.URI +export printstartinfo +show(io::IO, client::Client) = directto_abstractdevice(io, client) +function _show(d::AbstractDevice, client::Client) + _log(d, typeof(client), " id ", :bold, client.id, :normal) + _log(d, " ", client.sock, "\n") + _log(d, "\t\t\t", client.parser) + nothing +end + +# TODO is this good enough? +function show(io::IO, p::ClientParser) + print(io, "HttpServer.ClientParser.HttpParser.libhttp-parser: v", HttpParser.version()) + if p.parser.http_major > 0 + print(io, " HTTP/", p.parser.http_major, ".", p.parser.http_minor) + end + nothing +end + +"Response already has a decent show method, we're not overwriting that. +This metod is called only when logging to an Abstractdevice" +function _show(d::AbstractDevice, response::HttpServer.Response) + _log(d, :green, "Response status: ", :bold, response.status," ") + _log(d, get(STATUS_CODES, response.status, "--"), " ") + if !isempty(response.headers) + _log(d, :green, " Headers: ", :bold, response.headers.count) + _log(d, :green, "\n", response.headers) + end + if !isempty(response.cookies) + _log(d, " Cookies: ", :bold, length(response.cookies)) + _log(d, "\n", response.cookies) + end + if !isempty(response.data) + _log(d, "\t", DataDispatch(response.data, get(response.headers, "Content-Type","---"))) + end + nothing +end + +"Request already has a decent show method, we're not overwriting that. +This metod is called only when logging to an Abstractdevice" +function _show(d::AbstractDevice, request::Request) + _log(d, :normal, :light_yellow, "Request ", :normal) + _log(d, :bold, request.method, " ", :cyan, request.resource, "\n", :normal) + if !isempty(request.data) + _log(d, "\t", DataDispatch(request.data, get(request.headers, "Content-Type", "text/html; charset=utf-8"))) + end + if request.uri != URI("") + _log(d, :cyan, "\tUri:", :bold, _string(request.uri), :normal, "\n\t") + end + if !isempty(request.headers) + _log(d, "\t", :cyan, " .headers: ", request.headers.count) + _log(d, :cyan, "\n", request.headers) + end + nothing +end + +"HttpServer does not define a show method for its server type. Defining this is not piracy." +show(io::IO, server::Server) = directto_abstractdevice(io, server) +function _show(d::AbstractDevice, server::Server) + _log(d, :bold , :green, typeof(server), "(\n", :normal) + server.http != nothing && _log(d, "\t", server.http) + server.websock != nothing && _log(d, "\t", server.websock) + _log(d, :bold, :green, ")") + nothing +end + +"HttpServer does not define a show method for its HttpHandler type. Defining this is not piracy." +show(io::IO, httphandler::HttpHandler) = directto_abstractdevice(io, httphandler) +function _show(d::AbstractDevice, httphandler::HttpHandler) + _log(d, :bold, Base.info_color(), typeof(httphandler), "( " , :normal) + _log(d, ".handle: ", :blue, :bold, httphandler.handle, :normal, "\n") + _log(d, "\t\t\t.events:\n") + _log(d, httphandler.events) + _log(d, "\t\t\t", ".socket:\t", httphandler.sock, :bold, Base.info_color(), ")\n") + nothing +end + +nothing \ No newline at end of file diff --git a/logutils/log_ws.jl b/logutils/log_ws.jl new file mode 100644 index 0000000..a4649dd --- /dev/null +++ b/logutils/log_ws.jl @@ -0,0 +1,22 @@ +#= +Included in logutils.jl +=# + +import WebSockets.WebSocket # todo remove when including in WebSockets itself. +show(io::IO, ws::WebSocket) = directto_abstractdevice(io, ws) +function _show(d::AbstractDevice, ws::WebSocket{T}) where T + _log(d, "WebSocket{", T, "}(") + _log(d, ws.server ? "server, " : "client, ") + _log(d, ws.socket, " ") + showcompact(d.s, ws.state) + _log(d, ")") + nothing +end +@require HttpServer import WebSockets.WebSocketHandler +@require HttpServer show(io::IO, wsh::WebSocketHandler) = directto_abstractdevice(io, wsh) +@require HttpServer function _show(d::AbstractDevice, wsh::WebSocketHandler) + _log(d, typeof(wsh), "( " , :blue, :bold, wsh.handle, :normal, ")") + nothing + end + +nothing \ No newline at end of file diff --git a/logutils/logutils_ws.jl b/logutils/logutils_ws.jl new file mode 100644 index 0000000..7e3437a --- /dev/null +++ b/logutils/logutils_ws.jl @@ -0,0 +1,398 @@ +__precompile__() +""" +Specialized logging for testing and examples in WebSockets. + +To avoid type piracy, defines _show for types where specialized show methods exist, +and falls back to 'show' where they don't. + +When logging to a file, it's beneficial to keep a file open during the whole logging sesssion, +since opening and closing files is so slow it may affect the sequence of things. + +This module dispatches on an ad-hoc type AbstractDevice, instead of putting +additional data on an IOContext method like 'show' does. When AbstracDevice points to +Base.NullDevice(), the input arguments are processed before the call is made, but +that is all. + +In Julia 0.7, current logging functionality is replaced with macros, which +can be even faster. Macros can also retrieve the current function's name without using stacktraces. +With this module, each function defines id = "thisfunction". + +Methods in this file have no external dependencies. Methods with dependencies are +loaded from separate files with @require. This adds to loading time. +""" +module logutils_ws +using Requires +import Base.text_colors +import Base.color_normal +import Base.text_colors +import Base.show +@require HttpServer include("log_httpserver.jl") +@require HTTP include("log_http.jl") +@require WebSockets include("log_ws.jl") +export clog +export clog_notime +export zlog +export zlog_notime +export logto +export loggingto +export directto_abstractdevice +export AbstractDevice +export DataDispatch +export zflush + +const ColorDevices = Union{Base.TTY, IOBuffer, IOContext, Base.PipeEndpoint} +const BlackWDevices = Union{IOStream, IOBuffer} # and endpoint... +const LogDevices = Union{ColorDevices, BlackWDevices, Base.DevNullStream} +abstract type AbstractDevice{T} end +struct NullDevice <: AbstractDevice{NullDevice} + s::Base.DevNullStream + end +struct ColorDevice{S<:ColorDevices}<:AbstractDevice{ColorDevice} + s::S + end +struct BlackWDevice{S<:Union{IOStream, IOBuffer}}<:AbstractDevice{BlackWDevice} + s::S + end +mutable struct LogDevice + s::AbstractDevice + end +_devicecategory(::AbstractDevice{S}) where S = S +const CURDEVICE = LogDevice(NullDevice(Base.DevNullStream())) +struct DataDispatch + data::Array{UInt8,1} + contenttype::String +end +""" +Redirect coming zlog calls to a stream. Default is no logging. +clog calls will duplicate to STDOUT. +""" +logto(io::ColorDevices) = CURDEVICE.s = ColorDevice(io) +logto(io::BlackWDevices) = CURDEVICE.s = BlackWDevice(io) +logto(io::Base.DevNullStream) = CURDEVICE.s = NullDevice(io) + +""" +Returns the current logging stream +""" +loggingto() = CURDEVICE.s.s + +""" +Log to (default) nothing, or streams as given in CURDEVICE. +First argument is expected to be function id. +""" +function zlog(vars::Vararg) + _zlog(CURDEVICE.s, vars...) + nothing +end +""" +Log to (default) NullDevice, or the device given in CURDEVICE. +Falls back to Base.showdefault when no special methods are defined. +""" +function zlog_notime(vars::Vararg) + _zlog_notime(CURDEVICE.s, vars...) + nothing +end +""" +Log to the given device, but also to STDOUT if that's not the given device. +""" +function clog(vars::Vararg) + _zlog(CURDEVICE.s, vars...) + _devicecategory(CURDEVICE.s) != ColorDevice && _zlog(ColorDevice(STDOUT), vars...) + nothing +end +""" +Log to the given device, but also to STDOUT if that's not the given device. +""" +function clog_notime(vars::Vararg) + _zlog_notime(CURDEVICE.s, vars...) + _devicecategory(CURDEVICE.s) != ColorDevice && _zlog_notime(ColorDevice(STDOUT), vars...) + nothing +end +""" +Flushing file write buffers, only has an effect on streams. +We do not flush automatically after every log, because that +has a relatively dramatic effect on logging speeds. +""" +zflush() = isa(CURDEVICE.s.s, IOStream) && flush(CURDEVICE.s.s) + + +## Below are internal functions starting with _ +_zlog(::NullDevice, ::Vararg) = nothing +_zlog_notime(::NullDevice, ::Vararg) = nothing +"First argument padded and emphasized. +End the original argument list with a :normal and a new line argument." +function _zlog(d::AbstractDevice, vars::Vararg{Any,N}) where N + if N == 1 + _log(d, :normal, _tg(), " ", :bold, :cyan, vars[1], :normal, "\n") + else + lid = 26 + pads = repeat(" ", max(0, lid - length(_string(vars[1])))) #rpad don't work with color codes + _log(d, :normal, _tg(), " ", :bold, :cyan, vars[1], :normal, pads, vars[2:end]..., :normal, "\n") + end + nothing +end +"zlog, but no time stamp and no different format first argument." +function _zlog_notime(d::AbstractDevice, vars::Vararg{Any,N}) where N + # End the original argument list with a :normal and a new line argument. + _log(d, :bold, :cyan, vars[1:end]..., :normal, "\n") + nothing +end +"Write to blackwdevices, inapplicable formatting neglegted. No linefeed." +function _log(bwd::BlackWDevice, vars::Vararg) + _show.(bwd, vars) + nothing +end + +"Write colordevices. Does not reset color codes after, no linefeed." +function _log(cd::ColorDevice, vars::Vararg) + buf = ColorDevice(IOBuffer()) + _show.(buf, vars) + write(cd.s, take!(buf.s)) + nothing +end +"Write directly to colordevice/ IOBuffers" +function _log(cd::ColorDevice{Base.AbstractIOBuffer{Array{UInt8,1}}}, vars::Vararg) + _show.(cd, vars) + nothing +end + +"Return a string by emulating a bwdevice, where color codes are not output" +function _string(vars::Vararg) + buf = BlackWDevice(IOBuffer()) + _show.(buf, vars) + buf.s |> take! |> String +end + + + +"_show takes just device and one other argument. +It falls back to normal show for nondefined _show methods." +_show(d::AbstractDevice, var) = show(d.s, var) +_show(d::AbstractDevice, s::AbstractString) = write(d.s, s);nothing + +function _show(d::ColorDevice, sy::Symbol) + co = get(text_colors, sy, :NA) + if co != :NA + write(d.s, co) + else + write(d.s, sy) + end + nothing +end +function _show(d::ColorDevices, sy::Symbol) + co = get(text_colors, sy, :NA) + if co != :NA + write(d.s, co) + else + write(d.s, sy) + end + nothing +end + +function _show(d::BlackWDevice, sy::Symbol) + co = get(text_colors, sy, :NA) + if co == :NA + write(d.s, ":", sy) + end + nothing +end + +function _show(d::AbstractDevice, ex::Exception) + _log(d, Base.warn_color(), typeof(ex), "\n") + showerror(d.s, ex, []) + _log(d, :normal, "\n") +end +function _show(d::AbstractDevice, err::ErrorException) + _log(d, Base.error_color(), typeof(err), "\n") + showerror(d.s, err, []) + _log(d, :normal, "\n") +end +function _show(d::AbstractDevice, err::Base.UVError) + _log(d, Base.error_color(), typeof(err), "\n") + showerror(d.s, err, []) + _log(d, :normal, "\n") +end + + + +"Print dict, no heading, three pairs per line, truncate end to fit" +function _show(d::AbstractDevice, di::Dict) + linelength = displaysize(STDOUT)[2] + indent = 8 + npa = 3 + plen = div(linelength - indent, npa) + pairs = collect(di) + lpa = length(pairs) + _log(d, :bold, :blue) + for i = 1:npa:lpa + write(d.s, " "^indent) + write(d.s, _pairpad(pairs[i], plen)) + i+1 <= lpa && write(d.s, _pairpad(pairs[i + 1], plen)) + i+2 <= lpa && write(d.s, _pairpad(pairs[i + 2], plen)) + write(d.s, "\n") + end + _log(d, :normal) + nothing +end +"Print array of pairs, no heading, three pairs per line, truncate end to fit" +function _show(d::AbstractDevice, pairs::Vector{Pair{SubString{String},SubString{String}}}) + linelength = 95 + indent = 8 + npa = 3 + plen = div(linelength - indent, npa) + lpa = length(pairs) + _log(d, :bold, :blue) + for i = 1:npa:lpa + write(d.s, " "^indent) + write(d.s, _pairpad(pairs[i], plen)) + i+1 <= lpa && write(d.s, _pairpad(pairs[i + 1], plen)) + i+2 <= lpa && write(d.s, _pairpad(pairs[i + 2], plen)) + write(d.s, "\n") + end + _log(d, :normal) + nothing +end +"Print dict, no heading, two pairs per line, truncate end to fit" +function _show(d::AbstractDevice, di::Dict{String, Function}) + linelength = 95 + indent = 8 + npa = 2 + plen = div(linelength - indent, npa) + pairs = collect(di) + lpa = length(pairs) + _log(d, :bold, :blue) + for i = 1:npa:lpa + write(d.s, " "^indent) + write(d.s, _pairpad(pairs[i], plen)) + i+1 <= lpa && write(d.s, _pairpad(pairs[i + 1], plen)) + write(d.s, "\n") + end + _log(d, :normal) + nothing +end + + +_pairpad(pa::Pair, plen::Int) = Base.cpad(_limlen(_string(pa), plen) , plen ) +_string(pa::Pair) = _string(pa[1]) * " => " * _string(pa[2]) +function _show(d::AbstractDevice, f::Function) + mt = typeof(f).name.mt + fnam = splitdir(string(mt.defs.func.file))[2] + write(d.s, string(f) * " at " * fnam * ":" + * string(mt.defs.func.line)) + nothing +end + +"Type info not printed here as it is assumed the type is given by the context." +function _show(d::ColorDevice, stream::Base.LibuvStream) + _log(d, "(", :bold, _uv_status(stream)..., :normal) + nba = Base.nb_available(stream.buffer) + nba > 0 && print(d.s, ", ", Base.nb_available(stream.buffer)," bytes waiting") + print(d.s, ")") + nothing +end +function _uv_status(x) + s = x.status + if x.handle == Base.C_NULL + if s == Base.StatusClosed + return :red, "✘" #"closed" + elseif s == Base.StatusUninit + return :red, "null" + end + return :red, "invalid status" + elseif s == Base.StatusUninit + return :yellow, "uninit" + elseif s == Base.StatusInit + return :yellow, "init" + elseif s == Base.StatusConnecting + return :yellow, "connecting" + elseif s == Base.StatusOpen + return :green, "✓" # "open" + elseif s == Base.StatusActive + return :green, "active" + elseif s == Base.StatusPaused + return :red, "paused" + elseif s == Base.StatusClosing + return :red, "closing" + elseif s == Base.StatusClosed + return :red, "✘" #"closed" + elseif s == Base.StatusEOF + return :yellow, "eof" + end + return :red, "invalid status" +end + + +function _show(d::AbstractDevice, serv::Base.LibuvServer) + _log(d, typeof(serv), "(", :bold, _uv_status(serv)..., :normal, ")") + nothing +end + + +"Data as a truncated string" +function _show(d::AbstractDevice, datadispatch::DataDispatch) + _showdata(d, datadispatch.data, datadispatch.contenttype) + write(d.s, "\n") + nothing +end + + +function _showdata(d::AbstractDevice, data::Array{UInt8,1}, contenttype::String) + if ismatch(r"(text|script|html|xml|julia|java)", lowercase(contenttype)) + _log(d, :green, "\Data length: ", length(data), " ", :bold, :blue) + s = data |> String |> _limlen + write(d.s, replace(s, r"\s+", " ")) + else + _log(d, :green, "\Data length: ", length(data), " ", :blue) + write(d.s, data |> _limlen) + end + nothing +end + +"Truncates for logging" +_limlen(data::AbstractString) = _limlen(data, 74) +function _limlen(data::AbstractString, linelength::Int) + le = length(data) + if le < linelength + return normalize_string(string(data), stripcc = true) + else + adds = " … " + addlen = length(adds) + truncat = 2 * div(linelength, 3) + tail = linelength - truncat - addlen - 1 + truncstring = String(data)[1:truncat] * adds * String(data)[end-tail:end] + return normalize_string(truncstring, stripcc = true) + end +end +function _limlen(data::Union{Vector{UInt8}, Vector{Float64}}) + le = length(data) + maxlen = 12 # elements, not characters + if le < maxlen + return string(data) + else + adds = " ..... " + addlen = 2 + truncat = 2 * div(maxlen, 3) + tail = maxlen - truncat - addlen - 1 + return string(data[1:truncat])[1:end-1] * adds * string(data[end-tail:end])[7:end] + end +end + + +"Time group. show() converts to string only when necessary." +_tg() = Dates.Time(now()) + + + +"For use in show(io::IO, obj) methods. Hook into this logger's dispatch mechanism." +function directto_abstractdevice(io::IO, obj) + if isa(io, ColorDevices) + buf = ColorDevice(IOBuffer()) + else + buf = BlackWDevice(IOBuffer()) + end + _show(buf, obj) + write(io, take!(buf.s)) + nothing +end + +nothing +end # module \ No newline at end of file diff --git a/src/HTTP.jl b/src/HTTP.jl new file mode 100644 index 0000000..4e95a1d --- /dev/null +++ b/src/HTTP.jl @@ -0,0 +1,317 @@ +info("Loading HTTP methods...") + +""" +Initiate a websocket|client connection to server defined by url. If the server accepts +the connection and the upgrade to websocket, f is called with an open websocket|client + +e.g. say hello, close and leave +```julia +import HTTP +using WebSockets +WebSockets.open("ws://127.0.0.1:8000") do ws + write(ws, "Hello") + println("that's it") +end; +``` +If a server is listening and accepts, "Hello" is sent (as a Vector{UInt8}). + +On exit, a closing handshake is started. If the server is not currently reading +(which is a blocking function), this side will reset the underlying connection (ECONNRESET) +after a reasonable amount of time and continue execution. +""" +function open(f::Function, url; verbose=false, subprotocol = "", kw...) + + key = base64encode(rand(UInt8, 16)) + headers = [ + "Upgrade" => "websocket", + "Connection" => "Upgrade", + "Sec-WebSocket-Key" => key, + "Sec-WebSocket-Version" => "13" + ] + if subprotocol != "" + push!(headers, "Sec-WebSocket-Protocol" => subprotocol ) + end + + try + HTTP.open("GET", url, headers; + reuse_limit=0, verbose=verbose ? 2 : 0, kw...) do http + _openstream(f, http, key) + end + catch err + # TODO don't pass on WebSocketClosedError and other known ones. + if typeof(err) <: Base.UVError + throw(WebSocketClosedError(" while open ws|client: $(string(err))")) + elseif typeof(err) <: HTTP.ExceptionRequest.StatusError + return err.response + else + rethrow(err) + end + end +end +"Called by open with a stream connected to a server, after handshake is initiated" +function _openstream(f::Function, http::HTTP.Streams.Stream, key::String) + + HTTP.startread(http) + + status = http.message.status + if status != 101 + return + end + + check_upgrade(http) + + if HTTP.header(http, "Sec-WebSocket-Accept") != generate_websocket_key(key) + throw(WebSocketError(0, "Invalid Sec-WebSocket-Accept\n" * + "$(http.message)")) + end + + io = HTTP.ConnectionPool.getrawstream(http) + ws = WebSocket(io,false) + try + f(ws) + finally + close(ws) + end + +end + + +""" +Used as part of a server definition. Call this if +is_upgrade(http.message) returns true. + +Responds to a WebSocket handshake request. +If the connection is acceptable, sends status code 101 +and headers according to RFC 6455, then calls +user's handler function f with the connection wrapped in +a WebSocket instance. + +f(ws) is called with the websocket and no client info +f(headers, ws) also receives a dictionary of request headers for added security measures + +On exit from f, a closing handshake is started. If the client is not currently reading +(which is a blocking function), this side will reset the underlying connection (ECONNRESET) +after a reasonable amount of time and continue execution. + +If the upgrade is not accepted, responds to client with '400'. + + +e.g. server with local error handling. Combine with WebSocket.open example. +```julia +import HTTP +using WebSockets + +badgatekeeper(reqdict, ws) = sqrt(-2) +handlerequest(req) = HTTP.Response(501) + +try + HTTP.listen("127.0.0.1", UInt16(8000)) do http + if WebSockets.is_upgrade(http.message) + WebSockets.upgrade(badgatekeeper, http) + else + HTTP.Servers.handle_request(handlerequest, http) + end + end +catch err + showerror(STDERR, err) + println.(catch_stacktrace()[1:4]) +end +``` +""" +function upgrade(f::Function, http::HTTP.Stream) + # Double check the request. is_upgrade should already have been called by user. + check_upgrade(http) + if !HTTP.hasheader(http, "Sec-WebSocket-Version", "13") + HTTP.setheader(http, "Sec-WebSocket-Version" => "13") + HTTP.setstatus(http, 400) + HTTP.startwrite(http) + return + end + if HTTP.hasheader(http, "Sec-WebSocket-Protocol") + requestedprotocol = HTTP.header(http, "Sec-WebSocket-Protocol") + if !hasprotocol(requestedprotocol) + HTTP.setheader(http, "Sec-WebSocket-Protocol" => requestedprotocol) + HTTP.setstatus(http, 400) + HTTP.startwrite(http) + return + else + HTTP.setheader(http, "Sec-WebSocket-Protocol" => requestedprotocol) + end + end + key = HTTP.header(http, "Sec-WebSocket-Key") + if length(base64decode(key)) != 16 # Key must be 16 bytes + HTTP.setstatus(http, 400) + HTTP.startwrite(http) + return + end + # This upgrade is acceptable. Send the response. + HTTP.setheader(http, "Sec-WebSocket-Accept" => generate_websocket_key(key)) + HTTP.setheader(http, "Upgrade" => "websocket") + HTTP.setheader(http, "Connection" => "Upgrade") + HTTP.setstatus(http, 101) + HTTP.startwrite(http) + # Pass the connection on as a WebSocket. + io = HTTP.ConnectionPool.getrawstream(http) + ws = WebSocket(io, true) + # If the callback function f has two methods, + # prefer the more secure one which takes (request, websocket) + try + if applicable(f, http.message, ws) + f(http.message, ws) + else + f(ws) + end +# catch err +# warn("WebSockets.HTTP.upgrade: Caught unhandled error while calling argument function f, the handler / gatekeeper:\n\t") +# mt = typeof(f).name.mt +# fnam = splitdir(string(mt.defs.func.file))[2] +# print_with_color(:yellow, STDERR, "f = ", string(f) * " at " * fnam * ":" * string(mt.defs.func.line) * "\nERROR:\t") +# showerror(STDERR, err, catch_stacktrace()) + finally + close(ws) + end +end + +"It is up to the user to call 'is_upgrade' on received messages. +This provides double checking from within the 'upgrade' function." +function check_upgrade(http) + if !HTTP.hasheader(http, "Upgrade", "websocket") + throw(WebSocketError(0, "Check upgrade: Expected \"Upgrade => websocket\"!\n$(http.message)")) + end + if !(HTTP.hasheader(http, "Connection", "upgrade") || HTTP.hasheader(http, "Connection", "keep-alive, upgrade")) + throw(WebSocketError(0, "Check upgrade: Expected \"Connection => upgrade or Connection => keep alive, upgrade\"!\n$(http.message)")) + end +end + +""" +Fast checking for websockets vs http requests, performed on all new HTTP requests. +Similar to HttpServer.is_websocket_handshake +""" +function is_upgrade(r::HTTP.Message) + if (r isa HTTP.Request && r.method == "GET") || (r isa HTTP.Response && r.status == 101) + if HTTP.header(r, "Connection", "") != "keep-alive" + # "Connection => upgrade" for most and "Connection => keep-alive, upgrade" for Firefox. + if HTTP.hasheader(r, "Connection", "upgrade") || HTTP.hasheader(r, "Connection", "keep-alive, upgrade") + if lowercase(HTTP.header(r, "Upgrade", "")) == "websocket" + return true + end + end + end + end + return false +end +# Inline docs in 'WebSockets.jl' +target(req::HTTP.Messages.Request) = req.target +subprotocol(req::HTTP.Messages.Request) = HTTP.header(req, "Sec-WebSocket-Protocol") +origin(req::HTTP.Messages.Request) = HTTP.header(req, "Origin") + +""" +WebsocketHandler(f::Function) <: HTTP.Handler + +A simple Function-wrapper for HTTP. +The provided argument should be one of the forms + `f(WebSocket) => nothing` + `f(HTTP.Request, WebSocket) => nothing` +The latter form is intended for gatekeeping, ref. RFC 6455 section 10.1 + +f accepts a `WebSocket` and does interesting things with it, like reading, writing and exiting when finished. + +Take note of the very similar WebSocketHandler (capital 'S'), which is a subtype of HttpServer, an alternative +to HTTP. +""" +struct WebsocketHandler{F <: Function} <: HTTP.Handler + func::F # func(ws) or func(request, ws) +end + + +""" + WebSockets.ServerWS(::HTTP.HandlerFunction, ::WebSockets.WebsocketHandler(gatekeeper)) + +WebSockets.ServerWS is an argument type for WebSockets.serve. Instances +include .in and .out channels, see WebSockets.serve. +""" +mutable struct ServerWS{T <: HTTP.Servers.Scheme, H <: HTTP.Handler, W <: WebsocketHandler} + handler::H + wshandler::W + logger::IO + in::Channel{Any} + out::Channel{Any} + options::HTTP.ServerOptions + + ServerWS{T, H, W}(handler::H, wshandler::W, logger::IO = HTTP.compat_stdout(), ch=Channel(1), ch2=Channel(2), + options=HTTP.ServerOptions()) where {T, H, W} = + new{T, H, W}(handler, wshandler, logger, ch, ch2, options) +end + +ServerWS(h::Function, w::Function, l::IO=HTTP.compat_stdout(); + cert::String="", key::String="", args...) = ServerWS(HTTP.HandlerFunction(h), WebsocketHandler(w), l; + cert=cert, key=key, args...) +function ServerWS(handler::H, + wshandler::W, + logger::IO = HTTP.compat_stdout(); + cert::String = "", + key::String = "", + args...) where {H <: HTTP.Handler, W <: WebsocketHandler} + if cert != "" && key != "" + serverws = ServerWS{HTTP.Servers.https, H, W}(handler, wshandler, logger, Channel(1), Channel(2), HTTP.ServerOptions(; sslconfig=HTTP.MbedTLS.SSLConfig(cert, key), args...)) + else + serverws = ServerWS{HTTP.Servers.http, H, W}(handler, wshandler, logger, Channel(1), Channel(2), HTTP.ServerOptions(; args...)) + end + return serverws +end + +""" + WebSockets.serve(server::ServerWS{T, H, W}, host, port, verbose) + +A wrapper for HTTP.listen. +Puts any caught error and stacktrace on the server.out channel. +To stop a running server, put HTTP.Servers.KILL on the .in channel. +```julia + @shedule WebSockets.serve(server, "127.0.0.1", 8080, false) +``` +After a suspected connection task failure: +```julia + if isready(server.out) + err = take!(myserver_WS.out) + end + if isready(myserver_WS.out) + stack_trace = take!(server_WS.out) + end +``` +""" +function serve(server::ServerWS{T, H, W}, host, port, verbose) where {T, H, W} + + tcpserver = Ref{HTTP.Sockets.TCPServer}() + + @async begin + while !isassigned(tcpserver) + sleep(1) + end + while true + val = take!(server.in) + val == HTTP.Servers.KILL && close(tcpserver[]) + end + end + + HTTP.listen(host, port; + tcpref=tcpserver, + ssl=(T == HTTP.Servers.https), + sslconfig = server.options.sslconfig, + verbose = verbose, + tcpisvalid = server.options.ratelimit > 0 ? HTTP.Servers.check_rate_limit : + (tcp; kw...) -> true, + ratelimits = Dict{IPAddr, HTTP.Servers.RateLimit}(), + ratelimit = server.options.ratelimit) do stream::HTTP.Stream + try + if is_upgrade(stream.message) + upgrade(server.wshandler.func, stream) + else + HTTP.Servers.handle_request(server.handler.func, stream) + end + catch err + put!(server.out, err) + put!(server.out, catch_stacktrace()) + end + end + return +end \ No newline at end of file diff --git a/src/HttpServer.jl b/src/HttpServer.jl new file mode 100644 index 0000000..954aa5a --- /dev/null +++ b/src/HttpServer.jl @@ -0,0 +1,135 @@ +info("Loading HttpServer methods...") + +export WebSocketHandler + +""" +Called by HttpServer. Responds to a WebSocket handshake request. +If the connection is acceptable, sends status code 101 +and headers according to RFC 6455. Function returns +a WebSocket instance with the open socket as one of the fields. + +Otherwise responds with '400' and returns false. + +Any other response means 'decline', so a reason can be given. +The function returns 'true' to HttpServer, which then calls the +user's websocket handler. + +It is recommended to do further checks of the upgrade request in the +user handler function. + - "Origin" header: Included by clients in browsers, e.g: => "http://localhost:8000" + - "Sec-WebSocket-Protocol" header: If included, e.g.: => "myOwnProtocol" + - "Sec-WebSocket-Extensions" => "permessage-deflate" + +A WebSocketHandler may include: + .function + .acceptURI + .acceptsubprotocol + . acceptsource + . acceptOrin. +Typical headers: + - + "Connection" => "keep-alive, Upgrade" + "Sec-WebSocket-Version" => "13" + "http_minor" => "1" + "Keep-Alive" => "1" + "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Fi… + "Accept-Encoding" => "gzip, deflate" + "Cache-Control" => "no-cache" + "Origin" => "http://localhost:8000" + "Sec-WebSocket-Key" => "R9b6CHWxy9cg3H+1WuCFCA==" + "Sec-WebSocket-Protocol" => "relay_frontend" + "Sec-WebSocket-Extensions" => "permessage-deflate" + "Host" => "localhost:8000" + "Upgrade" => "websocket" + "Pragma" => "no-cache" + "http_major" => "1" + "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + "Accept-Language" => "en-US,en;q=0.5" + """ +function websocket_handshake(request, client) + response = HttpServer.Response(101) + if get(request.headers, "Sec-WebSocket-Version", "13") != "13" + response = HttpServer.Response(400) + response.headers["Sec-WebSocket-Version"] = "13" + Base.write(client.sock, response) + return false + end + if haskey(request.headers, "Sec-WebSocket-Protocol") + if hasprotocol(request.headers["Sec-WebSocket-Protocol"]) + response.headers["Sec-WebSocket-Protocol"] = request.headers["Sec-WebSocket-Protocol"] + else + Base.write(client.sock, HttpServer.Response(400)) + return false + end + end + + if !haskey(request.headers, "Sec-WebSocket-Key") + Base.write(client.sock, HttpServer.Response(400)) + return false + end + key = request.headers["Sec-WebSocket-Key"] + if length(base64decode(key)) != 16 # Key must be 16 bytes + Base.write(client.sock, HttpServer.Response(400)) + return false + end + resp_key = generate_websocket_key(key) + + + response.headers["Upgrade"] = "websocket" + response.headers["Connection"] = "Upgrade" + response.headers["Sec-WebSocket-Accept"] = resp_key + + Base.write(client.sock, response) + return true +end + +""" +WebSocketHandler(f::Function) <: HttpServer.WebSocketInterface + +A simple Function-wrapper for HttpServer. + +The provided argument should be of the form + `f(Request, WebSocket) => nothing` + +Request is intended for gatekeeping, ref. RFC 6455 section 10.1. +WebSocket is for reading, writing and exiting when finished. + +Take note of the very similar WebsocketHandler (no capital 'S'), which is a subtype of HTTP. +""" +struct WebSocketHandler <: HttpServer.WebSocketInterface + handle::Function +end + +""" +Performs handshake. If successfull, establishes WebSocket type and calls +handler with the WebSocket and the original request. On exit from handler, closes websocket. No return value. +""" +function HttpServer.handle(handler::WebSocketHandler, req::HttpServer.Request, client::HttpServer.Client) + websocket_handshake(req, client) || return + sock = WebSocket(client.sock,true) + handler.handle(req, sock) + if isopen(sock) + try + close(sock) + end + end +end + +""" +Fast checking for websockets vs http requests, performed on all new HttpServer requests. +Similar to is_upgrade(r::HTTP.Message) +""" +function HttpServer.is_websocket_handshake(handler::WebSocketHandler, req::HttpServer.Request) + if req.method == "GET" + if ismatch(r"upgrade"i, get(req.headers, "Connection", "")) + if lowercase(get(req.headers, "Upgrade", "")) == "websocket" + return true + end + end + end + return false +end +# Inline docs in WebSockets.jl +target(req::HttpServer.Request) = req.resource +subprotocol(req::HttpServer.Request) = get(req.headers, "Sec-WebSocket-Protocol", "") +origin(req::HttpServer.Request) = get(req.headers, "Origin", "") \ No newline at end of file diff --git a/src/WebSockets.jl b/src/WebSockets.jl index f7ccc08..2c8122f 100644 --- a/src/WebSockets.jl +++ b/src/WebSockets.jl @@ -1,64 +1,88 @@ __precompile__() """ WebSockets -This module implements the server side of the WebSockets protocol. Some -things would need to be added to implement a WebSockets client, such as -masking of sent frames. +This module implements the WebSockets protocol. -WebSockets expects to be used with HttpServer to provide the HttpServer -for accepting the HTTP request that begins the opening handshake. WebSockets -implements a subtype of the WebSocketInterface from HttpServer; this means -that you can create a WebSocketsHandler and pass it into the constructor for -an http server. +WebSockets relies on either packages HttpServer, HTTP or both. + +Websocket|server relies on a client initiating the connection. +Websocket|client initiate the connection, but requires HTTP. + +The other side of the connection, the peer, is typically a browser with +scripts enabled. Browsers are always the initiating, client side. But the +peer can be any program, in any language, that follows the protocol. That +includes another Julia session, parallel process or task. Future improvements: 1. Logging of refused requests and closures due to bad behavior of client. -2. Better error handling (should we always be using "error"?) -3. Unit tests with an actual client -- to automatically test the examples. -4. Send close messages with status codes. -5. Allow users to receive control messages if they want to. +2. Allow users to receive control messages or metadata if they want to. + For example RSV1 (compressed message) would be interesting. +3. Check rsv1 to rsv3 values. This will reduce bandwidth. +4. Optimize maskswitch!, possibly threaded above a certain limit. +5. Split messages over several frames. +6. Allow customizable console output (e.g. 'ping'). See HttpServer listen. """ module WebSockets - -using HttpCommon -using HttpServer -using Codecs -using MbedTLS -using Compat; import Compat.String - +import MbedTLS: digest, MD_SHA1 +using Requires export WebSocket, - WebSocketHandler, + readguarded, + writeguarded, write, read, close, + subprotocol, + target, + origin, send_ping, - send_pong + send_pong, + WebSocketClosedError const TCPSock = Base.TCPSocket +"A reasonable amount of time" +const TIMEOUT_CLOSEHANDSHAKE = 10.0 @enum ReadyState CONNECTED=0x1 CLOSING=0x2 CLOSED=0x3 """ Buffer writes to socket till flush (sock)""" -init_socket(sock) = Base.buffer_writes(sock) +init_socket(sock) = Base.buffer_writes(sock) -type WebSocketClosedError <: Exception end -Base.showerror(io::IO, e::WebSocketClosedError) = print(io, "Error: client disconnected") +struct WebSocketClosedError <: Exception + message::String +end + +struct WebSocketError <: Exception + status::Int16 + message::String +end + +"Status codes according to RFC 6455 7.4.1" +const codeDesc = Dict{Int, String}(1000=>"Normal", 1001=>"Going Away", + 1002=>"Protocol Error", 1003=>"Unsupported Data", + 1004=>"Reserved", 1005=>"No Status Recvd- reserved", + 1006=>"Abnormal Closure- reserved", 1007=>"Invalid frame payload data", + 1008=>"Policy Violation", 1009=>"Message too big", + 1010=>"Missing Extension", 1011=>"Internal Error", + 1012=>"Service Restart", 1013=>"Try Again Later", + 1014=>"Bad Gateway", 1015=>"TLS Handshake") + """ A WebSocket is a wrapper over a TcpSocket. It takes care of wrapping outgoing data in a frame and unwrapping (and concatenating) incoming data. """ -type WebSocket - id::Int - socket::TCPSock +mutable struct WebSocket{T <: IO} <: IO + socket::T + server::Bool state::ReadyState - function WebSocket(id::Int,socket::TCPSock) + function WebSocket{T}(socket::T,server::Bool) where T init_socket(socket) - new(id, socket, CONNECTED) + new(socket, server, CONNECTED) end end +WebSocket(socket,server) = WebSocket{typeof(socket)}(socket,server) # WebSocket Frames # @@ -103,78 +127,53 @@ const OPCODE_PONG = 0xA """ Handshakes with subprotocols are rejected by default. -Add to supported SUBProtocols through e.g. -# Examples -``` - WebSockets.addsubproto("special-protocol") +Add to acceptable SUBProtocols through e.g. +```julia WebSockets.addsubproto("json") -``` -In the general websocket handler function, specialize -further by checking -# Example -``` -if get(wsrequest.headers, "Sec-WebSocket-Protocol", "") = "special-protocol" - specialhandler(websocket) -else - generalhandler(websocket) -end ``` +Also see function subprotocol """ -const SUBProtocols= Array{String,1}() +const SUBProtocols= Array{String,1}() -"Used in handshake. See SUBProtocols" -hasprotocol(s::String) = in(s,SUBProtocols) - -"Used to specify handshake response. See SUBProtocols" -function addsubproto(name) - push!(SUBProtocols, string(name)) - return true -end -""" - write_fragment(io, islast, data::Array{UInt8}, opcode) -Write the raw frame to a bufffer """ -function write_fragment(io::IO, islast::Bool, data::Array{UInt8}, opcode) + write_fragment(io, islast, opcode, hasmask, data::Array{UInt8}) +Write the raw frame to a bufffer. Websocket|client must set 'hasmask'. +""" +function write_fragment(io::IO, islast::Bool, opcode, hasmask::Bool, data::Vector{UInt8}) l = length(data) b1::UInt8 = (islast ? 0b1000_0000 : 0b0000_0000) | opcode - # TODO: Do the mask xor thing?? - # 1. set bit 8 to 1, - # 2. set a mask - # 3. xor data with mask + mask::UInt8 = hasmask ? 0b1000_0000 : 0b0000_0000 + write(io, b1) if l <= 125 - write(io, b1) - write(io, @compat UInt8(l)) - write(io, data) + write(io, mask | UInt8(l)) elseif l <= typemax(UInt16) - write(io, b1) - write(io, @compat UInt8(126)) - write(io, hton(@compat UInt16(l))) - write(io, data) + write(io, mask | UInt8(126)) + write(io, hton(UInt16(l))) elseif l <= typemax(UInt64) - write(io, b1) - write(io, @compat UInt8(127)) - write(io, hton(@compat UInt64(l))) - write(io, data) + write(io, mask | UInt8(127)) + write(io, hton(UInt64(l))) else error("Attempted to send too much data for one websocket fragment\n") end -end - -""" - write_fragment(io, islast, data::String, opcode) -A version of send_fragment for text data. -""" -function write_fragment(io::IO, islast::Bool, data::String, opcode) - write_fragment(io, islast, data.data, opcode) + if hasmask + if opcode == OPCODE_TEXT + # Avoid masking Strings bytes in place. + # This makes client websockets slower than server websockets. + data = copy(data) + end + # Write the random masking key to io, also mask the data in-place + write(io, maskswitch!(data)) + end + write(io, data) end """ Write without interruptions""" -function locked_write(io::IO, islast::Bool, data, opcode) +function locked_write(io::IO, islast::Bool, opcode, hasmask::Bool, data::Vector{UInt8}) isa(io, TCPSock) && lock(io.lock) try - write_fragment(io, islast, Vector{UInt8}(data), opcode) + write_fragment(io, islast, opcode, hasmask, data) finally if isa(io, TCPSock) flush(io) @@ -185,86 +184,110 @@ end """ Write text data; will be sent as one frame.""" function Base.write(ws::WebSocket,data::String) - if !isopen(ws) - @show ws - error("Attempted write to closed WebSocket\n") - end - locked_write(ws.socket, true, data, OPCODE_TEXT) + locked_write(ws.socket, true, OPCODE_TEXT, !ws.server, Vector{UInt8}(data)) # Vector{UInt8}(String) will give a warning in v0.7. end """ Write binary data; will be sent as one frame.""" function Base.write(ws::WebSocket, data::Array{UInt8}) - if !isopen(ws) - @show ws - error("attempt to write to closed WebSocket\n") - end - locked_write(ws.socket, true, data, OPCODE_BINARY) + locked_write(ws.socket, true, OPCODE_BINARY, !ws.server, data) end -function write_ping(io::IO, data = "") - locked_write(io, true, data, OPCODE_PING) +function write_ping(io::IO, hasmask, data = UInt8[]) + locked_write(io, true, OPCODE_PING, hasmask, data) end """ Send a ping message, optionally with data.""" -send_ping(ws, data...) = write_ping(ws.socket, data...) +send_ping(ws, data...) = write_ping(ws.socket, !ws.server, data...) -function write_pong(io::IO, data = "") - locked_write(io, true, data, OPCODE_PONG) +function write_pong(io::IO, hasmask, data = UInt8[]) + locked_write(io, true, OPCODE_PONG, hasmask, data) end """ Send a pong message, optionally with data.""" -send_pong(ws, data...) = write_pong(ws.socket, data...) +send_pong(ws, data...) = write_pong(ws.socket, !ws.server, data...) -""" +""" close(ws::WebSocket) -Send a close message. + close(ws::WebSocket, statusnumber = n) + close(ws::WebSocket, statusnumber = n, freereason = "my reason") +Send an OPCODE_CLOSE frame, and wait for the same response or until +a reasonable amount of time, $(round(TIMEOUT_CLOSEHANDSHAKE, 1)) s, has passed. +Data received while closing is dropped. +Status number n according to RFC 6455 7.4.1 can be included, see WebSockets.codeDesc """ -function Base.close(ws::WebSocket) - if !isopen(ws) - error("Attempt to close closed WebSocket") - end +function Base.close(ws::WebSocket; statusnumber = 0, freereason = "") + if isopen(ws) + ws.state = CLOSING + if statusnumber == 0 + locked_write(ws.socket, true, OPCODE_CLOSE, !ws.server, UInt8[]) + elseif freereason == "" + statuscode = reinterpret(UInt8, [hton(UInt16(statusnumber))]) + locked_write(ws.socket, true, OPCODE_CLOSE, !ws.server, copy(statuscode)) + else + statuscode = vcat(reinterpret(UInt8, [hton(UInt16(statusnumber))]), + Vector{UInt8}(freereason)) + locked_write(ws.socket, true, OPCODE_CLOSE, !ws.server, copy(statuscode)) + end - # Ask client to acknowledge closing the connection - locked_write(ws.socket, true, "", OPCODE_CLOSE) - ws.state = CLOSING - - # Wait till the client responds with an OPCODE_CLOSE. This process is - # complicated by potential blocking reads on the WebSocket in other Tasks - # which may receive the response control frame. Synchronization of who is - # responsible for closing the underlying socket is done using the - # WebSocket's state. When this side initiates closing the connection it is - # responsible for cleaning up, when the other side initiates the close the - # read method is - # - # The exception handling is necessary as read_frame will error when the - # OPCODE_CLOSE control frame is received by a potentially blocking read in - # another Task - try - while ws.state === CLOSING - wsf = read_frame(ws.socket) - # ALERT: stuff might get lost in ether here - if is_control_frame(wsf) && (wsf.opcode == OPCODE_CLOSE) - ws.state = CLOSED + # Wait till the peer responds with an OPCODE_CLOSE while discarding any + # trailing bytes received. + # + # We have no guarantee that the peer is actually reading our OPCODE_CLOSE + # frame. If not, the peer's state will not change, and we will not receive + # an aknowledgment of closing. We use a nonblocking read and give up + # after TIMEOUT_CLOSEHANDSHAKE + # + # This process is + # complicated by potential blocking reads on the WebSocket in other Tasks + # which may receive the response control frame. Synchronization of who is + # responsible for closing the underlying socket is done using the + # WebSocket's state. When this side initiates closing the connection it is + # responsible for cleaning up, when the other side initiates the close the + # read method is. + # + # The exception handling is necessary as read_frame will error when the + # OPCODE_CLOSE control frame is received by a potentially blocking read in + # another Task + # + try + t1 = time() + TIMEOUT_CLOSEHANDSHAKE + while isopen(ws) && time() < t1 + wsf = readframe_nonblocking(ws) + if is_control_frame(wsf) && (wsf.opcode == OPCODE_CLOSE) + ws.state = CLOSED + end end + if isopen(ws.socket) + close(ws.socket) + end + catch err + # Typical 'errors' received while closing down are neglected. + errtyp = typeof(err) + errtyp != InterruptException && + errtyp != Base.UVError && + errtyp != Base.BoundsError && + errtyp != Base.EOFError && + errtyp != Base.ArgumentError && + rethrow(err) end - - close(ws.socket) - catch exception - !isa(exception, EOFError) && rethrow(exception) + else + ws.state = CLOSED end end + """ - isopen(WebSocket)-> Bool + isopen(::WebSocket)-> Bool A WebSocket is closed if the underlying TCP socket closes, or if we send or receive a close message. """ -Base.isopen(ws::WebSocket) = (ws.state === CONNECTED) && isopen(ws.socket) +Base.isopen(ws::WebSocket) = (ws.state != CLOSED) && isopen(ws.socket) +Base.eof(ws::WebSocket) = (ws.state == CLOSED) || eof(ws.socket) """ Represents one (received) message frame.""" -type WebSocketFragment +mutable struct WebSocketFragment is_last::Bool - rsv1::Bool + rsv1::Bool # Set for compressed messages. rsv2::Bool rsv3::Bool opcode::UInt8 # This is actually a UInt4 value. @@ -302,114 +325,172 @@ end is_control_frame(msg::WebSocketFragment) = (msg.opcode & 0b0000_1000) > 0 """ Respond to pings, ignore pongs, respond to close.""" -function handle_control_frame(ws::WebSocket,wsf::WebSocketFragment) +function handle_control_frame(ws::WebSocket, wsf::WebSocketFragment) if wsf.opcode == OPCODE_CLOSE - # A close OPCODE can be received for two reasons. Either the other side - # is initiating a disconnection, or the this side is (through a call to - # close on the WebSocket) and the client has replied that it is okay - # with closing the connection. This can be derived from the current - # state of the WebSocket - if ws.state !== CLOSING - # The other side initiated the disconnect, so the action must be - # acknowledged by replying with an empty CLOSE frame and cleaning - # up - try - locked_write(ws.socket, true, "", OPCODE_CLOSE) - catch exception - # On sudden disconnects, the other side may be gone before the - # close acknowledgement can be sent. This will cause an - # ArgumentError to be thrown due to the underlying stream being - # closed. These are swallowed here and will be replaced by a - # WebSocketClosedError below - !isa(exception, ArgumentError) && rethrow(exception) - end - - close(ws.socket) - end - - # In the other case the close method is expected to clean-up, which can - # be triggered by changing the state of the WebSocket ws.state = CLOSED - - throw(WebSocketClosedError()) + try + locked_write(ws.socket, true, OPCODE_CLOSE, !ws.server, UInt8[]) + end + # Find out why the other side wanted to close. + # RFC 6455 5.5.1. If there is a status code, it's a two-byte number in network order. + if wsf.payload_len == 0 + reason = " No reason " + elseif wsf.payload_len == 2 + scode = Int(reinterpret(UInt16, reverse(wsf.data))[1]) + reason = string(scode) * ":" * get(codeDesc, scode, "") + else + scode = Int(reinterpret(UInt16, reverse(wsf.data[1:2]))[1]) + reason = string(scode) * ":" * String(wsf.data[3:end]) + end + throw(WebSocketClosedError("ws|$(ws.server ? "server" : "client") respond to OPCODE_CLOSE " * reason)) elseif wsf.opcode == OPCODE_PING - write_pong(ws.socket,wsf.data) + info("ws|$(ws.server ? "server" : "client") received OPCODE_PING") + send_pong(ws, wsf.data) elseif wsf.opcode == OPCODE_PONG + info("ws|$(ws.server ? "server" : "client") received OPCODE_PONG") # Nothing to do here; no reply is needed for a pong message. else # %xB-F are reserved for further control frames - error("Unknown opcode $(wsf.opcode)") + error(" while handle_control_frame(ws|$(ws.server ? "server" : "client"), wsf): Unknown opcode $(wsf.opcode)") end end """ Read a frame: turn bytes from the websocket into a WebSocketFragment.""" -function read_frame(io::IO) - a = read(io,UInt8) +function read_frame(ws::WebSocket) + # Try to read two bytes. There is no guarantee that two bytes are actually allocated. + ab = read(ws.socket, 2) + #= + Browsers will seldom close in the middle of writing to a socket, + but other clients often do, and the stacktraces can be very long. + ab can be assigned, but of length 1. Use an enclosing try..catch in the calling function + =# + a = ab[1] fin = a & 0b1000_0000 >>> 7 # If fin, then is final fragment rsv1 = a & 0b0100_0000 # If not 0, fail. rsv2 = a & 0b0010_0000 # If not 0, fail. rsv3 = a & 0b0001_0000 # If not 0, fail. opcode = a & 0b0000_1111 # If not known code, fail. - # TODO: add validation somewhere to ensure rsv, opcode, mask, etc are valid. - b = read(io,UInt8) - mask = b & 0b1000_0000 >>> 7 # If not 1, fail. + b = ab[2] + mask = b & 0b1000_0000 >>> 7 + hasmask = mask != 0 - if mask != 1 - error("WebSocket reader cannot handle incoming messages without mask. " * - "See http://tools.ietf.org/html/rfc6455#section-5.3") + if hasmask != ws.server + if ws.server + msg = "WebSocket|server cannot handle incoming messages without mask. Ref. rcf6455 5.3" + else + msg = "WebSocket|client cannot handle incoming messages with mask. Ref. rcf6455 5.3" + end + throw(WebSocketError(1002, msg)) end payload_len::UInt64 = b & 0b0111_1111 if payload_len == 126 - payload_len = ntoh(read(io,UInt16)) # 2 bytes + payload_len = ntoh(read(ws.socket,UInt16)) # 2 bytes elseif payload_len == 127 - payload_len = ntoh(read(io,UInt64)) # 8 bytes + payload_len = ntoh(read(ws.socket,UInt64)) # 8 bytes end - maskkey = Array{UInt8,1}(4) - for i in 1:4 - maskkey[i] = read(io,UInt8) - end + maskkey = hasmask ? read(ws.socket,4) : UInt8[] - data = Array{UInt8,1}(payload_len) - for i in 1:payload_len - d = read(io, UInt8) - d = xor(d , maskkey[mod(i - 1, 4) + 1]) - data[i] = d - end + data = read(ws.socket,Int(payload_len)) + hasmask && maskswitch!(data,maskkey) return WebSocketFragment(fin,rsv1,rsv2,rsv3,opcode,mask,payload_len,maskkey,data) end + """ read(ws::WebSocket) +Typical use: + msg = String(read(ws)) Read one non-control message from a WebSocket. Any control messages that are -read will be handled by the handle_control_frame function. This function will -not return until a full non-control message has been read. If the other side -doesn't ever complete its message, this function will never return. Only the -data (contents/body/payload) of the message will be returned from this -function. +read will be handled by the handle_control_frame function. +Only the data (contents/body/payload) of the message will be returned as a +Vector{UInt8}. + +This function will not return until a full non-control message has been read. """ function Base.read(ws::WebSocket) if !isopen(ws) - error("Attempt to read from closed WebSocket") + error("Attempt to read from closed WebSocket|$(ws.server ? "server" : "client"). First isopen(ws), or use readguarded(ws)!") end - frame = read_frame(ws.socket) - - # Handle control (non-data) messages. - if is_control_frame(frame) - # Don't return control frames; they're not interesting to users. - handle_control_frame(ws,frame) + try + frame = read_frame(ws) + # Handle control (non-data) messages. + if is_control_frame(frame) + # Don't return control frames; they're not interesting to users. + handle_control_frame(ws, frame) + # Recurse to return the next data frame. +- return read(ws) + end - # Recurse to return the next data frame. - return read(ws) + # Handle data message that uses multiple fragments. + if !frame.is_last + return vcat(frame.data, read(ws)) + end + return frame.data + catch err + try + errtyp = typeof(err) + if errtyp <: InterruptException + msg = " while read(ws|$(ws.server ? "server" : "client") received InterruptException." + # This exception originates from this side. Follow close protocol so as not to irritate the other side. + close(ws, statusnumber = 1006, freereason = msg) + throw(WebSocketClosedError(msg * " Performed closing handshake.")) + elseif errtyp <: WebSocketError + # This exception originates on the other side. Follow close protocol with reason. + close(ws, statusnumber = err.status) + throw(WebSocketClosedError(" while read(ws|$(ws.server ? "server" : "client")) $(err.message) - Performed closing handshake.")) + elseif errtyp <: Base.UVError || + errtyp <: Base.BoundsError || + errtyp <: Base.EOFError || + errtyp <: Base.ArgumentError + throw(WebSocketClosedError(" while read(ws|$(ws.server ? "server" : "client")) $(string(err))")) + else + # Unknown cause, give up continued execution. + # If this happens in a multiple fragment message, the accumulated + # stacktrace could be very long since read(ws) is iterative. + rethrow(err) + end + finally + if isopen(ws.socket) + close(ws.socket) + end + ws.state = CLOSED + end end + return Vector{UInt8}() +end - # Handle data message that uses multiple fragments. - if !frame.is_last - return vcat(frame.data, read(ws)) +""" +For the closing handshake, we won't wait indefinitely for non-responsive clients. +Returns a throwaway frame if the socket happens to be empty +""" +function readframe_nonblocking(ws) + chnl= Channel{WebSocketFragment}(1) + # Read, output put to Channel for type stability + function _readinterruptable(c::Channel{WebSocketFragment}) + try + put!(chnl, read_frame(ws)) + catch + # Output a dummy frame that is not a control frame. + put!(chnl, WebSocketFragment(false, false, false, false, + UInt8(0), false, UInt64(0), + Vector{UInt8}([0x0,0x0,0x0,0x0]), + Vector{UInt8}())) + end end - - return frame.data + # Start reading as a task. Will not return if there is nothing to read + rt = @schedule _readinterruptable(chnl) + bind(chnl, rt) + yield() + # Define a task for throwing interrupt exception to the (possibly blocked) read task. + # We don't start this task because it would never return + killta = @task try;Base.throwto(rt, InterruptException());end + # We start the killing task. When it is scheduled the second time, + # we pass an InterruptException through the scheduler. + try;schedule(killta, InterruptException(), error = false);end + # We now have content on chnl, and no additional tasks. + take!(chnl) end """ @@ -422,78 +503,177 @@ value. This is done in three steps: This function then returns the string of the base64-encoded value. """ function generate_websocket_key(key) - hashed_key = digest(MD_SHA1, key*"258EAFA5-E914-47DA-95CA-C5AB0DC85B11") - String(encode(Base64, hashed_key)) + hashkey = "$(key)258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + return base64encode(digest(MD_SHA1, hashkey)) end """ -Responds to a WebSocket handshake request. -Checks for required headers and subprotocols; sends Response(400) if they're missing or bad. Otherwise, transforms client key into accept value, and sends Reponse(101). -Function returns true for accepted handshakes. + maskswitch!(data) + maskswitch!(data, key:: 4-element Vector{UInt8}) + +Masks or unmasks data in-place, returns the key used. +Calling twice with the same key restores data. +Ref. RFC 6455 5-3. """ -function websocket_handshake(request,client) - if !haskey(request.headers, "Sec-WebSocket-Key") - Base.write(client.sock, Response(400)) - return false - end - if get(request.headers, "Sec-WebSocket-Version", "13") != "13" - response = Response(400) - response.headers["Sec-WebSocket-Version"] = "13" - Base.write(client.sock, response) - return false +function maskswitch!(data, mask = rand(UInt8, 4)) + for i in 1:length(data) + data[i] = data[i] ⊻ mask[((i-1) % 4)+1] end + return mask +end - key = request.headers["Sec-WebSocket-Key"] - if length(decode(Base64,key)) != 16 # Key must be 16 bytes - Base.write(client.sock, Response(400)) - return false +"Used in handshake. See SUBProtocols" +hasprotocol(s::AbstractString) = in(s, SUBProtocols) + +"Used to specify acceptable subprotocols. See SUBProtocols" +function addsubproto(name) + push!(SUBProtocols, string(name)) + return true +end + + + + + + +""" +`target(request) => String` + +Convenience function for reading upgrade request target. + E.g. +```julia + function gatekeeper(req, ws) + if target(req) == "/gamepad" + @spawnat 2 gamepad(ws) + elseif target(req) == "/console" + @spawnat 3 console(ws) + ... + end end - resp_key = generate_websocket_key(key) - - response = Response(101) - response.headers["Upgrade"] = "websocket" - response.headers["Connection"] = "Upgrade" - response.headers["Sec-WebSocket-Accept"] = resp_key - - if haskey(request.headers, "Sec-WebSocket-Protocol") - if hasprotocol(request.headers["Sec-WebSocket-Protocol"]) - response.headers["Sec-WebSocket-Protocol"] = request.headers["Sec-WebSocket-Protocol"] - else - Base.write(client.sock, Response(400)) - return false - end - end - - Base.write(client.sock, response) - return true +``` +Then, in browser javascript (or equivalent with Julia WebSockets.open( , )) +```javascript +function load(){ + var wsuri = document.URL.replace("http:", "ws:"); + ws1 = new WebSocket(wsuri + "/gamepad"); + ws2 = new WebSocket(wsuri + "/console"); + ws3 = new WebSocket(wsuri + "/graphics"); + ws4 = new WebSocket(wsuri + "/audiochat"); + ws1.onmessage = function(e){vibrate(e.data)} + } // load + +``` +""" +function target # Methods added in include files end -""" Implement the WebSocketInterface, for compatilibility with HttpServer.""" -immutable WebSocketHandler <: HttpServer.WebSocketInterface - handle::Function +""" +`subprotocol(request) => String` + +Convenience function for reading upgrade request subprotocol. +Acceptable subprotocols need to be predefined using +addsubproto(myprotocol). No other subprotocols will pass the handshake. +E.g. +```julia +WebSockets.addsubproto("instructions") +WebSockets.addsubproto("relay_backend") +function gatekeeper(req, ws) + subpr = WebSockets.subprotocol(req) + if subpr == "instructions" + instructions(ws) + elseif subpr == "relay_backend" + relay_backend(ws) + end end +``` -import HttpServer: handle, is_websocket_handshake +Then, in browser javascript (or equivalent with Julia WebSockets.open( , )) +```javascript +function load(){ + var wsuri = document.URL.replace("http:", "ws:"); + ws1 = new WebSocket(wsuri, "instructions"); + ws2 = new WebSocket(wsuri, "relay_backend"); + ws1.onmessage = function(e){doinstructions(e.data)}; + ... + } // load +``` """ -Performs handshake. If successfull, establishes WebSocket type and calls -handler with the WebSocket and the original request. On exit from handler, closes websocket. No return value. +function subprotocol # Methods added in include files +end + + """ -function handle(handler::WebSocketHandler, req::Request, client::HttpServer.Client) - websocket_handshake(req, client) || return - sock = WebSocket(client.id, client.sock) - handler.handle(req, sock) - if isopen(sock) - try - close(sock) +`origin(request) => String` +Convenience function for checking which server / port address +the client claims its code was downloaded from. +The resource path can be found with target(req). +E.g. +```julia +function gatekeeper(req, ws) + orig = WebSockets.origin(req) + if startswith(orig, "http://localhost") || startswith(orig, "http://127.0.0.1") + handlewebsocket(ws) end end end -function is_websocket_handshake(handler::WebSocketHandler, req::Request) - is_get = req.method == "GET" - # "upgrade" for Chrome and "keep-alive, upgrade" for Firefox. - is_upgrade = contains(lowercase(get(req.headers, "Connection", "")),"upgrade") - is_websockets = lowercase(get(req.headers, "Upgrade", "")) == "websocket" - return is_get && is_upgrade && is_websockets +``` +""" +function origin # Methods added in include files end + +""" +`writeguarded(websocket, message) => Bool` + +Return true if write is successful, false if not. +The peer can potentially disconnect at any time, but no matter the +cause you will usually just want to exit your websocket handling function +when you can't write to it. + +""" +function writeguarded(ws, msg) + try + write(ws, msg) + catch + return false + end + true +end + +""" +`readguarded(websocket) => (Vector, Bool)` + +Return (data::Vector, true) + or + (Vector{UInt8}(), false) + +The peer can potentially disconnect at any time, but no matter the +cause you will usually just want to exit your websocket handling function +when you can't write to it. + +E.g. +```julia +while true + data, success = readguarded(websocket) + !success && break + println(String(data)) +end +``` +""" +function readguarded(ws) + data = Vector{UInt8}() + success = true + try + data = read(ws) + catch err + data = Vector{UInt8}() + success = false + finally + return data, success + end +end + + +@require HTTP include("HTTP.jl") +@require HttpServer include("HttpServer.jl") end # module WebSockets diff --git a/test/HttpServer.jl b/test/HttpServer.jl new file mode 100644 index 0000000..d08060c --- /dev/null +++ b/test/HttpServer.jl @@ -0,0 +1,211 @@ +@testset "HttpServer" begin + +using HttpServer +using WebSockets +import WebSockets: generate_websocket_key, + write_fragment, + read_frame, + websocket_handshake + # is_websocket_handshake, + # handle +import HttpCommon: Request, Response + + +@sync yield() # avoid mixing of output with possible deprecation warnings from .juliarc +info("Starting test WebSockets...") +#is_control_frame is one line, checking one bit. +#get_websocket_key grabs a header. +#is_websocket_handshake grabs a header. +#generate_websocket_key makes a call to a library. +info("Test generate_websocket_key") +@test generate_websocket_key("dGhlIHNhbXBsZSBub25jZQ==") == "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" + +# Test writing + +function xor_payload(maskkey, data) + out = Array{UInt8,1}(length(data)) + for i in 1:length(data) + d = data[i] + d = xor(d , maskkey[mod(i - 1, 4) + 1]) + out[i] = d + end + out +end + +const io = IOBuffer() + +info("Test length less than 126") +for len = [8, 125], op = (rand(UInt8) & 0b1111), fin=[true, false] + + test_str = randstring(len) + write_fragment(io, fin, op, false, Vector{UInt8}(test_str)) + + frame = take!(io) + + @test bits(frame[1]) == (fin ? "1" : "0") * "000" * bits(op)[end-3:end] + @test frame[2] == UInt8(len) + @test String(frame[3:end]) == test_str + + # The new WebSockets can handle both client and server so can handle no masks + # =========================================================================== + # # Check to see if reading message without a mask fails + # in_buf = IOBuffer(String(frame)) + # @test_throws ErrorException read_frame(in_buf) + # close(in_buf) + + # add a mask + maskkey = rand(UInt8, 4) + data = vcat( + frame[1], + frame[2] | 0b1000_0000, + maskkey, + xor_payload(maskkey, frame[3:end]) + ) + frame_back = read_frame(WebSocket(IOBuffer(data),true)) + + @test frame_back.is_last == fin + @test frame_back.rsv1 == false + @test frame_back.rsv2 == false + @test frame_back.rsv3 == false + @test frame_back.opcode == op + @test frame_back.is_masked == true + @test frame_back.payload_len == len + @test all(map(==, frame_back.maskkey, maskkey)) + @test test_str == String(frame_back.data) +end + +info("Test length 126 or more") +for len = 126:129, op = 0b1111, fin=[true, false] + + test_str = randstring(len) + write_fragment(io, fin, op, false, Vector{UInt8}(test_str)) + + frame = take!(io) + + @test bits(frame[1]) == (fin ? "1" : "0") * "000" * bits(op)[end-3:end] + @test frame[2] == 126 + + @test bits(frame[4])*bits(frame[3]) == bits(hton(UInt16(len))) + + # add a mask + maskkey = rand(UInt8, 4) + data = vcat( + frame[1], + frame[2] | 0b1000_0000, + frame[3], + frame[4], + maskkey, + xor_payload(maskkey, frame[5:end]) + ) + frame_back = read_frame(WebSocket(IOBuffer(data),true)) + + @test frame_back.is_last == fin + @test frame_back.rsv1 == false + @test frame_back.rsv2 == false + @test frame_back.rsv3 == false + @test frame_back.opcode == op + @test frame_back.is_masked == true + @test frame_back.payload_len == len + @test all(map(==, frame_back.maskkey, maskkey)) + @test test_str == String(frame_back.data) +end + +# TODO: test for length > typemax(Uint32) + +info("Tests for is_websocket_handshake") +chromeheaders = Dict{String, String}( + "Connection"=>"Upgrade", + "Upgrade"=>"websocket" + ) +chromerequest = HttpCommon.Request( + "GET", + "", + chromeheaders, + "" + ) + +firefoxheaders = Dict{String, String}( + "Connection"=>"keep-alive, Upgrade", + "Upgrade"=>"websocket" + ) + +firefoxrequest= Request( + "GET", + "", + firefoxheaders, + "" + ) + +wshandler = WebSocketHandler((x,y)->nothing);#Dummy wshandler + +for request in [chromerequest, firefoxrequest] + @test HttpServer.is_websocket_handshake(wshandler,request) == true +end + +info("Test of handshake response") +takefirstline(buf) = split(buf |> take! |> String, "\r\n")[1] + +take!(io) +Base.write(io, "test") +@test takefirstline(io) == "test" + +info("Test reject / switch format") +const SWITCH = "HTTP/1.1 101 Switching Protocols " +const REJECT = "HTTP/1.1 400 Bad Request " +Base.write(io, Response(400)) +@test takefirstline(io) == REJECT +Base.write(io, Response(101)) +@test takefirstline(io) == SWITCH + +function handshakeresponse(request) + cli = HttpServer.Client(2, IOBuffer()) + websocket_handshake(request, cli) + takefirstline(cli.sock) +end + +info("Test simple handshakes that are unacceptable") +for request in [chromerequest, firefoxrequest] + @test handshakeresponse(request) == REJECT + push!(request.headers, "Sec-WebSocket-Version" => "13") + @test handshakeresponse(request) == REJECT + push!(request.headers, "Sec-WebSocket-Key" => "mumbojumbobo") + @test handshakeresponse(request) == REJECT + push!(request.headers, "Sec-WebSocket-Version" => "11") + push!(request.headers, "Sec-WebSocket-Key" => "zkG1WqHM8BJdQMXytFqiUw==") + @test handshakeresponse(request) == REJECT +end + +info("Test simple handshakes, acceptable") +for request in [chromerequest, firefoxrequest] + push!(request.headers, "Sec-WebSocket-Version" => "13") + push!(request.headers, "Sec-WebSocket-Key" => "zkG1WqHM8BJdQMXytFqiUw==") + @test handshakeresponse(request) == SWITCH +end + +info("Test unacceptable subprotocol handshake subprotocol") +for request in [chromerequest, firefoxrequest] + push!(request.headers, "Sec-WebSocket-Version" => "13") + push!(request.headers, "Sec-WebSocket-Key" => "zkG1WqHM8BJdQMXytFqiUw==") + push!(request.headers, "Sec-WebSocket-Protocol" => "my.server/json-zmq") + @test handshakeresponse(request) == REJECT +end + +info("add simple subprotocol to acceptable list") +@test true == WebSockets.addsubproto("xml") + +info("add subprotocol with difficult name") +@test true == WebSockets.addsubproto("my.server/json-zmq") + +info("Test handshake subprotocol now acceptable") +for request in [chromerequest, firefoxrequest] + push!(request.headers, "Sec-WebSocket-Version" => "13") + push!(request.headers, "Sec-WebSocket-Key" => "zkG1WqHM8BJdQMXytFqiUw==") + push!(request.headers, "Sec-WebSocket-Protocol" => "xml") + @test handshakeresponse(request) == SWITCH + push!(request.headers, "Sec-WebSocket-Protocol" => "my.server/json-zmq") + @test handshakeresponse(request) == SWITCH +end +close(io) +include("browsertest.jl") + +end \ No newline at end of file diff --git a/test/REQUIRE b/test/REQUIRE new file mode 100644 index 0000000..f3d7be5 --- /dev/null +++ b/test/REQUIRE @@ -0,0 +1,3 @@ +HTTP +HttpServer +JSON \ No newline at end of file diff --git a/test/browsertest.html b/test/browsertest.html index 63216ff..09f82c3 100644 --- a/test/browsertest.html +++ b/test/browsertest.html @@ -1,9 +1,11 @@ - + + + + WS text - - +

Test tries to open three different websockets.

@@ -24,6 +26,14 @@

ws3 Websocket: server_denies_protocol