From 01fd719116f84ae22ee570188df6a26464b8268f Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Thu, 29 Aug 2024 16:34:06 +0300 Subject: [PATCH] examples: add examples/veb/websocket, to show how to use http connection upgrade to a websocket, from a `veb` route (#22128) --- examples/veb/websocket/assets/client.js | 44 ++++++++++ examples/veb/websocket/assets/favicon.ico | Bin 0 -> 1350 bytes examples/veb/websocket/assets/style.css | 41 +++++++++ examples/veb/websocket/index.html | 17 ++++ examples/veb/websocket/server.v | 98 ++++++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 examples/veb/websocket/assets/client.js create mode 100644 examples/veb/websocket/assets/favicon.ico create mode 100644 examples/veb/websocket/assets/style.css create mode 100644 examples/veb/websocket/index.html create mode 100644 examples/veb/websocket/server.v diff --git a/examples/veb/websocket/assets/client.js b/examples/veb/websocket/assets/client.js new file mode 100644 index 00000000000000..cc2b05877da183 --- /dev/null +++ b/examples/veb/websocket/assets/client.js @@ -0,0 +1,44 @@ +const messageList = document.getElementById('message-list'); +const messageInput = document.getElementById('message-input'); +const messageStatus = document.getElementById('message-status'); +const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; +// +let status = '---'; +function set_status(s) { status = s; } +function show_status() { + messageStatus.innerHTML = `${status}, ${socket.readyState}`; + requestAnimationFrame(show_status); +} +requestAnimationFrame(show_status); +// +var socket = start_reconnecting_socket(1000, `${protocol}://${location.host}/ws`); +function start_reconnecting_socket(timeout, target) { + var nsocket = new WebSocket(target); + nsocket.addEventListener('open', (event) => { console.log('Connected to WS server'); set_status('connected'); }); + nsocket.addEventListener('message', (event) => { + messageList.innerHTML += `
  • received: ${event.data}
  • `; + set_status('received message'); + }); + nsocket.addEventListener('close', (event) => { + console.log('on close'); + set_status('connection closed'); + setTimeout(()=>{ + console.log('Try reconnecting ...') + socket = start_reconnecting_socket(timeout, target); + }, timeout); + }); + nsocket.addEventListener('error', (event) => { console.log('on error'); set_status('error'); }); + return nsocket; +} + +// +function send(message) { + socket.send(message); + set_status('sending message'); +} +messageInput.addEventListener('keyup', ({key}) => { + if (key === 'Enter') { + send(messageInput.value); + messageInput.value = ''; + } +}); diff --git a/examples/veb/websocket/assets/favicon.ico b/examples/veb/websocket/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e4f68ade5ac71145ad431f3642496e80d3b8a7fc GIT binary patch literal 1350 zcmeH`Jx;_h5QU%Dh0CO(bxO)qw97Rpkhn>*+G4N63BY?3SJW<3L>b=1i8dPr1VV@g zCr@8yJpR#(4H#II<&HFX25?7JRgP-|yfLopDYn4P4!C8m)H+JXebio0k7Jhz(D{o} z0nCaHPzBC?K4u*b!10`=V}Ckc_ghi-)YtYaxg{qOCj~1qt2H93ytR=7(9G(AeQ(-P zooRh%gHKsSapr3>WTWyub0yUE7SySTMUy)E3=ly>9-oU zMKx0M3wn(^;ED+-G`~To`3PJw09NyBwuT#Jeofc_%}*TQA{t=LUh{jPN0FB7H1<6I n@O&?15leX&CUP*R;C;}eG*V<_BQmgVv{}BxfB*GA{I>t^iMs=d literal 0 HcmV?d00001 diff --git a/examples/veb/websocket/assets/style.css b/examples/veb/websocket/assets/style.css new file mode 100644 index 00000000000000..177863b2f884f5 --- /dev/null +++ b/examples/veb/websocket/assets/style.css @@ -0,0 +1,41 @@ +html { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} +body { + padding: 0px; + margin: 0px; + margin-left: 30px; + margin-right: 30px; +} + +#message-list { + width: 100%; + height: 250px; + padding: 5px; + box-sizing: border-box; + margin: 0px; + margin-bottom: 5px; + border: 1px solid lightgray; + overflow-y: scroll; + overscroll-behavior-y: contain; + scroll-snap-type: y proximity; +} +#message-list > li { + list-style-type: none; +} +#message-list > li:last-child { + scroll-snap-align: end; + border-top: 1px solid #eeeeff !important; +} + +#message-input { + width: 100%; + margin: 0px; + padding: 0px; + font-size: 24px; + line-height: 1; + font-weight: 600; +} diff --git a/examples/veb/websocket/index.html b/examples/veb/websocket/index.html new file mode 100644 index 00000000000000..a24b4c6aa939da --- /dev/null +++ b/examples/veb/websocket/index.html @@ -0,0 +1,17 @@ + + + + + veb websocket example + + + +

    This is an example of using V's `veb` and `net.websocket` modules in + the same app, where the websocket connection happens on the /ws route of + your veb app.

    + + +
    Client status: ---
    + + + diff --git a/examples/veb/websocket/server.v b/examples/veb/websocket/server.v new file mode 100644 index 00000000000000..f6eafefe776544 --- /dev/null +++ b/examples/veb/websocket/server.v @@ -0,0 +1,98 @@ +module main + +import os +import veb +import log +import time +import term +import net +import net.http +import net.websocket + +const app_port = 8990 + +fn main() { + mut app := &App{ + wss: new_websocker_server()! + } + app.mount_static_folder_at(os.resource_abs_path('assets'), '/assets')! + app.serve_static('/favicon.ico', os.resource_abs_path('assets/favicon.ico'))! + veb.run[App, Context](mut app, app_port) +} + +pub struct Context { + veb.Context +} + +pub struct App { + veb.StaticHandler +mut: + wss &websocket.Server +} + +pub fn (mut app App) index(mut ctx Context) veb.Result { + return $veb.html() +} + +pub fn (mut app App) ws(mut ctx Context) veb.Result { + key := ctx.get_header(http.CommonHeader.sec_websocket_key) or { '' } + if key == '' { + ctx.error('Invalid websocket handshake. Key is missing.') + return ctx.redirect('/') + } + dump(ctx.req.cookie('token') or { http.Cookie{} }.value) + wlog('> transferring connection with key: ${key}, to the websocket server ${voidptr(app.wss)} ...') + ctx.takeover_conn() + ctx.conn.set_write_timeout(time.infinite) + ctx.conn.set_read_timeout(time.infinite) + spawn fn (mut wss websocket.Server, mut connection net.TcpConn, key string) { + wss.handle_handshake(mut connection, key) or { wlog('handle_handshake error: ${err}') } + wlog('>> wss.handle_handshake finished, key: ${key}') + }(mut app.wss, mut ctx.conn, key) + wlog('> done transferring connection') + return veb.no_result() +} + +fn slog(message string) { + eprintln(term.colorize(term.bright_yellow, message)) +} + +fn wlog(message string) { + eprintln(term.colorize(term.bright_blue, message)) +} + +fn new_websocker_server() !&websocket.Server { + mut logger := &log.Log{} + logger.set_level(.info) + mut wss := websocket.new_server(.ip, app_port, '', logger: logger) + wss.set_ping_interval(100) + wss.on_connect(fn [mut logger] (mut server_client websocket.ServerClient) !bool { + server_client.client.logger = logger + slog('wss.on_connect client.id: ${server_client.client.id} | server_client.client_key: ${server_client.client_key}') + return true + })! + wss.on_close(fn (mut client websocket.Client, code int, reason string) ! { + slog('wss.on_close client.id: ${client.id} | code: ${code}, reason: ${reason}') + }) + wss.on_message(fn [mut wss] (mut client websocket.Client, msg &websocket.Message) ! { + slog('wss.on_message client.id: ${client.id} | msg.opcode: ${msg.opcode} | msg.payload: `${msg.payload.bytestr()}`') + text := '${client.id} says: "${msg.payload.bytestr()}"' + // client.write_string(text) or { slog('client.write err: ${err}') return err } + for i, _ in rlock wss.server_state { + wss.server_state.clients + } { + mut c := rlock wss.server_state { + wss.server_state.clients[i] or { continue } + } + if c.client.get_state() == .open { + c.client.write_string(text) or { + slog('error while broadcasting to i: ${i}, ${voidptr(c)}, err: ${err}') + continue + } + } + } + }) + + slog('Websocket Server initialized, wss: ${voidptr(wss)}') + return wss +}