Skip to content

Commit 130f7f8

Browse files
author
Jan Stamer
committed
Initial commit.
0 parents  commit 130f7f8

File tree

7 files changed

+448
-0
lines changed

7 files changed

+448
-0
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Chat Example
2+
3+
This application shows how to use the
4+
[websocket](https://github.com/gorilla/websocket) package to implement a simple
5+
web chat application.
6+
7+
## Running the example
8+
9+
The example requires a working Go development environment. The [Getting
10+
Started](http://golang.org/doc/install) page describes how to install the
11+
development environment.
12+
13+
Once you have Go up and running, you can download, build and run the example
14+
using the following commands.
15+
16+
$ go get github.com/gorilla/websocket
17+
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
18+
$ go run *.go
19+
20+
To use the chat example, open http://localhost:8080/ in your browser.
21+
22+
## Server
23+
24+
The server application defines two types, `Client` and `Hub`. The server
25+
creates an instance of the `Client` type for each websocket connection. A
26+
`Client` acts as an intermediary between the websocket connection and a single
27+
instance of the `Hub` type. The `Hub` maintains a set of registered clients and
28+
broadcasts messages to the clients.
29+
30+
The application runs one goroutine for the `Hub` and two goroutines for each
31+
`Client`. The goroutines communicate with each other using channels. The `Hub`
32+
has channels for registering clients, unregistering clients and broadcasting
33+
messages. A `Client` has a buffered channel of outbound messages. One of the
34+
client's goroutines reads messages from this channel and writes the messages to
35+
the websocket. The other client goroutine reads messages from the websocket and
36+
sends them to the hub.
37+
38+
### Hub
39+
40+
The code for the `Hub` type is in
41+
[hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go).
42+
The application's `main` function starts the hub's `run` method as a goroutine.
43+
Clients send requests to the hub using the `register`, `unregister` and
44+
`broadcast` channels.
45+
46+
The hub registers clients by adding the client pointer as a key in the
47+
`clients` map. The map value is always true.
48+
49+
The unregister code is a little more complicated. In addition to deleting the
50+
client pointer from the `clients` map, the hub closes the clients's `send`
51+
channel to signal the client that no more messages will be sent to the client.
52+
53+
The hub handles messages by looping over the registered clients and sending the
54+
message to the client's `send` channel. If the client's `send` buffer is full,
55+
then the hub assumes that the client is dead or stuck. In this case, the hub
56+
unregisters the client and closes the websocket.
57+
58+
### Client
59+
60+
The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go).
61+
62+
The `serveWs` function is registered by the application's `main` function as
63+
an HTTP handler. The handler upgrades the HTTP connection to the WebSocket
64+
protocol, creates a client, registers the client with the hub and schedules the
65+
client to be unregistered using a defer statement.
66+
67+
Next, the HTTP handler starts the client's `writePump` method as a goroutine.
68+
This method transfers messages from the client's send channel to the websocket
69+
connection. The writer method exits when the channel is closed by the hub or
70+
there's an error writing to the websocket connection.
71+
72+
Finally, the HTTP handler calls the client's `readPump` method. This method
73+
transfers inbound messages from the websocket to the hub.
74+
75+
WebSocket connections [support one concurrent reader and one concurrent
76+
writer](https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency). The
77+
application ensures that these concurrency requirements are met by executing
78+
all reads from the `readPump` goroutine and all writes from the `writePump`
79+
goroutine.
80+
81+
To improve efficiency under high load, the `writePump` function coalesces
82+
pending chat messages in the `send` channel to a single WebSocket message. This
83+
reduces the number of system calls and the amount of data sent over the
84+
network.
85+
86+
## Frontend
87+
88+
The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html).
89+
90+
On document load, the script checks for websocket functionality in the browser.
91+
If websocket functionality is available, then the script opens a connection to
92+
the server and registers a callback to handle messages from the server. The
93+
callback appends the message to the chat log using the appendLog function.
94+
95+
To allow the user to manually scroll through the chat log without interruption
96+
from new messages, the `appendLog` function checks the scroll position before
97+
adding new content. If the chat log is scrolled to the bottom, then the
98+
function scrolls new content into view after adding the content. Otherwise, the
99+
scroll position is not changed.
100+
101+
The form handler writes the user input to the websocket and clears the input
102+
field.

client.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"bytes"
9+
"log"
10+
"net/http"
11+
"strings"
12+
"time"
13+
14+
"github.com/gorilla/websocket"
15+
)
16+
17+
const (
18+
// Time allowed to write a message to the peer.
19+
writeWait = 10 * time.Second
20+
21+
// Time allowed to read the next pong message from the peer.
22+
pongWait = 60 * time.Second
23+
24+
// Send pings to peer with this period. Must be less than pongWait.
25+
pingPeriod = (pongWait * 9) / 10
26+
27+
// Maximum message size allowed from peer.
28+
maxMessageSize = 512
29+
)
30+
31+
var (
32+
newline = []byte{'\n'}
33+
space = []byte{' '}
34+
)
35+
36+
var upgrader = websocket.Upgrader{
37+
ReadBufferSize: 1024,
38+
WriteBufferSize: 1024,
39+
}
40+
41+
// Client is a middleman between the websocket connection and the hub.
42+
type Client struct {
43+
hub *Hub
44+
45+
// The websocket connection.
46+
conn *websocket.Conn
47+
48+
// Buffered channel of outbound messages.
49+
send chan []byte
50+
}
51+
52+
// readPump pumps messages from the websocket connection to the hub.
53+
//
54+
// The application runs readPump in a per-connection goroutine. The application
55+
// ensures that there is at most one reader on a connection by executing all
56+
// reads from this goroutine.
57+
func (c *Client) readPump() {
58+
defer func() {
59+
c.hub.unregister <- c
60+
c.conn.Close()
61+
}()
62+
c.conn.SetReadLimit(maxMessageSize)
63+
c.conn.SetReadDeadline(time.Now().Add(pongWait))
64+
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
65+
for {
66+
_, message, err := c.conn.ReadMessage()
67+
if err != nil {
68+
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
69+
log.Printf("error: %v", err)
70+
}
71+
break
72+
}
73+
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
74+
c.hub.broadcast <- message
75+
}
76+
}
77+
78+
// writePump pumps messages from the hub to the websocket connection.
79+
//
80+
// A goroutine running writePump is started for each connection. The
81+
// application ensures that there is at most one writer to a connection by
82+
// executing all writes from this goroutine.
83+
func (c *Client) writePump() {
84+
ticker := time.NewTicker(pingPeriod)
85+
defer func() {
86+
ticker.Stop()
87+
c.conn.Close()
88+
}()
89+
for {
90+
select {
91+
case message, ok := <-c.send:
92+
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
93+
if !ok {
94+
// The hub closed the channel.
95+
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
96+
return
97+
}
98+
99+
w, err := c.conn.NextWriter(websocket.TextMessage)
100+
if err != nil {
101+
return
102+
}
103+
msg := `{
104+
"identifier":
105+
"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"**mysignature**\"}",
106+
"message":
107+
"<turbo-stream action="append" target="board">
108+
<template>
109+
<p>$$MESSAGE$$</p>
110+
</template>
111+
</turbo-stream>"
112+
}`
113+
114+
msg = strings.Replace(msg, "$$MESSAGE$$", string(message), -1)
115+
w.Write([]byte(msg))
116+
117+
// Add queued chat messages to the current websocket message.
118+
n := len(c.send)
119+
for i := 0; i < n; i++ {
120+
w.Write(newline)
121+
w.Write(<-c.send)
122+
}
123+
124+
if err := w.Close(); err != nil {
125+
return
126+
}
127+
case <-ticker.C:
128+
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
129+
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
130+
return
131+
}
132+
}
133+
}
134+
}
135+
136+
// serveWs handles websocket requests from the peer.
137+
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
138+
conn, err := upgrader.Upgrade(w, r, nil)
139+
if err != nil {
140+
log.Println(err)
141+
return
142+
}
143+
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
144+
client.hub.register <- client
145+
146+
// Allow collection of memory referenced by the caller by doing all work in
147+
// new goroutines.
148+
go client.writePump()
149+
go client.readPump()
150+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module go_websocket_turbo
2+
3+
go 1.15
4+
5+
require github.com/gorilla/websocket v1.4.2 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
2+
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

home.html

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Chat Example</title>
5+
<script src="https://unpkg.com/@hotwired/[email protected]/dist/turbo.es5-umd.js" ></script>
6+
<script type="text/javascript">
7+
window.onload = function () {
8+
var conn;
9+
var msg = document.getElementById("msg");
10+
var log = document.getElementById("log");
11+
12+
function appendLog(item) {
13+
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
14+
log.appendChild(item);
15+
if (doScroll) {
16+
log.scrollTop = log.scrollHeight - log.clientHeight;
17+
}
18+
}
19+
20+
document.getElementById("form").onsubmit = function () {
21+
if (!conn) {
22+
return false;
23+
}
24+
if (!msg.value) {
25+
return false;
26+
}
27+
conn.send(msg.value);
28+
msg.value = "";
29+
return false;
30+
};
31+
32+
if (window["WebSocket"]) {
33+
Turbo.connectStreamSource(new WebSocket("ws://" + document.location.host + "/ws"));
34+
conn = new WebSocket("ws://" + document.location.host + "/ws");
35+
} else {
36+
var item = document.createElement("div");
37+
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
38+
appendLog(item);
39+
}
40+
};
41+
</script>
42+
<style type="text/css">
43+
html {
44+
overflow: hidden;
45+
}
46+
47+
body {
48+
overflow: hidden;
49+
padding: 0;
50+
margin: 0;
51+
width: 100%;
52+
height: 100%;
53+
background: gray;
54+
}
55+
56+
#board {
57+
background: white;
58+
margin: 0;
59+
padding: 0.5em 0.5em 0.5em 0.5em;
60+
position: absolute;
61+
top: 0.5em;
62+
left: 0.5em;
63+
right: 0.5em;
64+
bottom: 3em;
65+
overflow: auto;
66+
}
67+
68+
#form {
69+
padding: 0 0.5em 0 0.5em;
70+
margin: 0;
71+
position: absolute;
72+
bottom: 1em;
73+
left: 0px;
74+
width: 100%;
75+
overflow: hidden;
76+
}
77+
78+
</style>
79+
</head>
80+
<body>
81+
<!--
82+
<turbo-cable-stream-source
83+
channel="Turbo::StreamsChannel"
84+
signed-stream-name="**mysignature**"
85+
>
86+
</turbo-cable-stream-source>
87+
-->
88+
89+
<div id="board"></div>
90+
<form id="form">
91+
<input type="submit" value="Send" />
92+
<input type="text" id="msg" size="64" autofocus />
93+
</form>
94+
95+
</body>
96+
</html>

0 commit comments

Comments
 (0)