From f4b757e47d04e98359727df4859d4705d5c4b3c9 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 3 Feb 2021 15:40:06 +0200 Subject: [PATCH] examples: add examples/vweb/server_sent_events; implement vweb.sse --- .../vweb/server_sent_events/assets/site.css | 19 +++++ .../vweb/server_sent_events/assets/v-logo.svg | 1 + examples/vweb/server_sent_events/favicon.ico | Bin 0 -> 15406 bytes examples/vweb/server_sent_events/index.html | 38 +++++++++ examples/vweb/server_sent_events/server.v | 38 +++++++++ vlib/vweb/sse/sse.v | 77 ++++++++++++++++++ vlib/vweb/vweb.v | 14 +++- 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 examples/vweb/server_sent_events/assets/site.css create mode 100644 examples/vweb/server_sent_events/assets/v-logo.svg create mode 100644 examples/vweb/server_sent_events/favicon.ico create mode 100644 examples/vweb/server_sent_events/index.html create mode 100644 examples/vweb/server_sent_events/server.v create mode 100644 vlib/vweb/sse/sse.v diff --git a/examples/vweb/server_sent_events/assets/site.css b/examples/vweb/server_sent_events/assets/site.css new file mode 100644 index 0000000000..4ad9eb895f --- /dev/null +++ b/examples/vweb/server_sent_events/assets/site.css @@ -0,0 +1,19 @@ +body { + font-family: Arial, Helvetica, sans-serif; + color: #eee; + background-color: #333; + background-image: url("v-logo.svg"); + background-repeat: no-repeat; + background-size: 10em; + margin: 0; + padding-left: 11em; +} + +h1 { + color: #6699CC; +} + +img.logo { + float: left; + width: 10em; +} diff --git a/examples/vweb/server_sent_events/assets/v-logo.svg b/examples/vweb/server_sent_events/assets/v-logo.svg new file mode 100644 index 0000000000..9a4ec604c0 --- /dev/null +++ b/examples/vweb/server_sent_events/assets/v-logo.svg @@ -0,0 +1 @@ + diff --git a/examples/vweb/server_sent_events/favicon.ico b/examples/vweb/server_sent_events/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fa834c378eefbad8b5ff7211e4158d5ac5e62816 GIT binary patch literal 15406 zcmeHOYiu0V72Xg6DJ3F6A#H`gfYd~^6{JD5s1k}QH81MLJL5@WduHQMXYG9@BtNPo zMOnx8&dl!GA+d>2lBx(0Q85T8c|acCwT+<&g({7rv{h*YD&$d;v}!`g)6?(Rd)Ifz zyYpDPF;YF!=+1qdbH01#%)R%Vd&lFM;F;)|I@N=;%+qka$McBC<0&f}%+I*hcA{X0CK8E69%Pz;tMxn@^Jt(uWm<-Uw2|AFqk%{3o11s43!DS&QWZp&zj z!S3I!U1;cGUz-#9&Q57!Mpce6T1q#s5R61fJk}{m6YY3(X*F|r+Py6`hSpGFB*UVS z3{}ptqwm&~7kK)YS4&2MXmKaP!i!e)2O0x6f&M={efx4@Q1$@5cs8ZX`DQU5>A1g| zr^P!skq8OL2C!!_q#{CfF?z_}l~1p!o*vJ;3emrpQtrgNzRc5itNuq*Ve!ZL z^upy-gYQl=kF=^R=V|c{j~ZmUxbg1(&hvmimz(J!`$L{S9amJ=Mh^;co<128K4f*2 zrkmyI-MZ4x)9=%3a`pec0E$BNLL)(!_8%~XJXn~b%s?+PeY)oG;oWsaUx>e<6EBSA(TWmBhdm=j@iQ7je zEuXqC;hh56=d9#!dCF(#q1>EwF7c#yNcAs05F$~bb_e;e9yH?HeO!@O#eZ@H@t5BK>SLW6`Zg9k6vFk9$d}5N9 zTYC?btMaSKh`7CwT(0bsSj=;s8`;#hM_=-orEGlj7?XumNG#`M4bwl>wT6Yq+{y>< ztyc1}w);2cw-Q|E)cGBV?bkl8?IQ8P8-`*k&akb!TF=vnR9m zb}8xAMz%rz5aW}I_t~b+} z^N`(ZDSycSGLue#9e&Kq8}=L$d(A|AeowPRP;vhlhR#p?dr-{g&FNLdiM0R4{*+lK6wyhK}XAK*7^448xJ4<@jim2P}Q^zSRIPeWvPAfJ$mipzlOOQ(Gxdut7eD@OxA1_L@@bI6I# zxiN3F0Hs5^CF zOJG<` zbPxO=WRkVtE{1>OimGYA^PW@u*>*>04lKsASULQ25%C?%^I5#@13USXt%tdB$ILq> zNjKmdNCMYY7k#)~N{kEmck=jbpLOM5IHOQu#%#{FMFcO-WSntx9-^Y!*{vK4Ss5ib2^Po z2Uh98T2oV8#ioG2zc(rW$cist*VZs_wjJ?v=Y9eDe9+07l=k?6=ad`$<1FMyF4yb$ zM?pB?20z-KqO$_L9?Z{}Hzxs09N4co#*e*=9$g8WdD-QGCrsnJi#}XQ;=hV5fxx6< zya)aXQEko{nd~0@Jl>a(bN^Np<83ZatdOPy`*9ceX+6|i=wB4ZyDQ;;1bFOXQ~j8e zadt)d66qIMFC=#~y59q$ITiZHTu}K1<^=K$=uC^<`#7Iykmz@?f){K*f}7ULv={mr z?!(CAzbs`3QmjMe4*Ji4&i-~M}9 z=MGwTqAxpe_n~ix(a+D&&vWqOXgv`r#=2LP?L&!qp!_R+p*qUJ7&chdQwOia3& z=2qJ8Ek-}ma^g*~6Y@Ffyb;Y6STo&)_{)82M4Un6mKv4tx83+Pu7Pn4jB8-*HSk{~ CV;+D2 literal 0 HcmV?d00001 diff --git a/examples/vweb/server_sent_events/index.html b/examples/vweb/server_sent_events/index.html new file mode 100644 index 0000000000..7e500c9295 --- /dev/null +++ b/examples/vweb/server_sent_events/index.html @@ -0,0 +1,38 @@ + +
+ @title + + @css 'assets/site.css' +
+ +

@title

+ +
    + + + diff --git a/examples/vweb/server_sent_events/server.v b/examples/vweb/server_sent_events/server.v new file mode 100644 index 0000000000..e5216a837d --- /dev/null +++ b/examples/vweb/server_sent_events/server.v @@ -0,0 +1,38 @@ +module main + +import rand +import time +import vweb +import vweb.sse + +struct App { + vweb.Context +} + +fn main() { + vweb.run(8081) +} + +pub fn (mut app App) init_once() { + app.serve_static('/favicon.ico', 'favicon.ico', 'img/x-icon') + app.handle_static('.') +} + +pub fn (mut app App) index() vweb.Result { + title := 'SSE Example' + return $vweb.html() +} + +fn (mut app App) sse() vweb.Result { + mut session := sse.new_connection(app.conn) + // NB: you can setup session.write_timeout and session.headers here + session.start() or { return app.server_error(501) } + session.send_message(data: 'ok') or { return app.server_error(501) } + for { + data := '{"time": "$time.now().str()", "random_id": "$rand.ulid()"}' + session.send_message(event: 'ping', data: data) or { return app.server_error(501) } + println('> sent event: $data') + time.sleep_ms(1000) + } + return app.server_error(501) +} diff --git a/vlib/vweb/sse/sse.v b/vlib/vweb/sse/sse.v new file mode 100644 index 0000000000..82cd50a04d --- /dev/null +++ b/vlib/vweb/sse/sse.v @@ -0,0 +1,77 @@ +module sse + +import net +import time +import strings + +// This module implements the server side of `Server Sent Events`. +// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format +// as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events +// for detailed description of the protocol, and a simple web browser client example. +// +// > Event stream format +// > The event stream is a simple stream of text data which must be encoded using UTF-8. +// > Messages in the event stream are separated by a pair of newline characters. +// > A colon as the first character of a line is in essence a comment, and is ignored. +// > Note: The comment line can be used to prevent connections from timing out; +// > a server can send a comment periodically to keep the connection alive. +// > +// > Each message consists of one or more lines of text listing the fields for that message. +// > Each field is represented by the field name, followed by a colon, followed by the text +// > data for that field's value. + +[ref_only] +pub struct SSEConnection { +pub mut: + headers map[string]string + conn &net.TcpConn + write_timeout time.Duration = 600 * time.second +} + +pub struct SSEMessage { + id string + event string + data string + retry int +} + +pub fn new_connection(conn &net.TcpConn) SSEConnection { + return SSEConnection{ + conn: conn + } +} + +// sse_start is used to send the start of a Server Side Event response. +pub fn (mut sse SSEConnection) start() ? { + sse.conn.set_write_timeout(sse.write_timeout) + mut start_sb := strings.new_builder(512) + start_sb.write('HTTP/1.1 200') + start_sb.write('\r\nConnection: keep-alive') + start_sb.write('\r\nCache-Control: no-cache') + start_sb.write('\r\nContent-Type: text/event-stream') + for k, v in sse.headers { + start_sb.write('\r\n$k: $v') + } + start_sb.write('\r\n') + sse.conn.write(start_sb.buf) or { return error('could not start sse response') } +} + +// send_message sends a single message to the http client that listens for SSE. +// It does not close the connection, so you can use it many times in a loop. +pub fn (mut sse SSEConnection) send_message(message SSEMessage) ? { + mut sb := strings.new_builder(512) + if message.id != '' { + sb.write('id: $message.id\n') + } + if message.event != '' { + sb.write('event: $message.event\n') + } + if message.data != '' { + sb.write('data: $message.data\n') + } + if message.retry != 0 { + sb.write('retry: $message.retry\n') + } + sb.write('\n') + sse.conn.write(sb.buf) ? +} diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index e84dcd1b56..f25451194b 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -158,6 +158,17 @@ pub fn (mut ctx Context) ok(s string) Result { return Result{} } +pub fn (mut ctx Context) server_error(ecode int) Result { + $if debug { + eprintln('> ctx.server_error ecode: $ecode') + } + if ctx.done { + return Result{} + } + send_string(mut ctx.conn, vweb.http_500) or { } + return Result{} +} + pub fn (mut ctx Context) redirect(url string) Result { if ctx.done { return Result{} @@ -304,7 +315,7 @@ fn handle_conn(mut conn net.TcpConn, mut app T) { page_gen_start := time.ticks() first_line := reader.read_line() or { $if debug { - eprintln('Failed first_line') // show this only in debug mode, because it always would be shown after a chromium user visits the site + eprintln('Failed first_line') // show this only in debug mode, because it always would be shown after a chromium user visits the site } return } @@ -592,6 +603,7 @@ fn handle_conn(mut conn net.TcpConn, mut app T) { // call action method if method.args.len == vars.len { app.$method(vars) + return } else { eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the vweb route `$method.attrs` ($vars.len)') }