mirror of https://github.com/gorilla/websocket.git
Improve chat example
- Remove jQuery. - Rename Conn to Client to avoid confusion with underlying ws connection. - Remove global variables.
This commit is contained in:
parent
a836c37014
commit
5e2e56d5df
|
@ -1,8 +1,8 @@
|
||||||
# Chat Example
|
# Chat Example
|
||||||
|
|
||||||
This application shows how to use use the
|
This application shows how to use use the
|
||||||
[websocket](https://github.com/gorilla/websocket) package and
|
[websocket](https://github.com/gorilla/websocket) package to implement a simple
|
||||||
[jQuery](http://jquery.com) to implement a simple web chat application.
|
web chat application.
|
||||||
|
|
||||||
## Running the example
|
## Running the example
|
||||||
|
|
||||||
|
@ -21,42 +21,71 @@ To use the chat example, open http://localhost:8080/ in your browser.
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
|
|
||||||
The server application is implemented with Go's [http](https://golang.org/pkg/net/http/) and the Gorilla [websocket](https://godoc.org/github.com/gorilla/websocket) package.
|
The server application defines two types, `Client` and `Hub`. The server
|
||||||
|
creates an instance of the `Client` type for each webscocket connection. A
|
||||||
|
`Client` acts as an intermediary between the websocket connection and a single
|
||||||
|
instance of the `Hub` type. The `Hub` maintains a set of registered clients and
|
||||||
|
broadcasts messages to the clients.
|
||||||
|
|
||||||
The application defines two types, `Conn` and `Hub`. The server creates an instance of the `Conn` type for each webscocket connection. A `Conn` acts as an intermediary between the websocket and a single instance of the `Hub` type. The `Hub` maintains a set of registered connections and broadcasts messages to the connections.
|
The application runs one goroutine for the `Hub` and two goroutines for each
|
||||||
|
`Client`. The goroutines communicate with each other using channels. The `Hub`
|
||||||
The application runs one goroutine for the `Hub` and two goroutines for each `Conn`. The goroutines communicate with each other using channels. The `Hub` has channels for registering connections, unregistering connections and broadcasting messages. A `Conn` has a buffered channel of outbound messages. One of the connection's goroutines reads messages from this channel and writes the messages to the webscoket. The other connection goroutine reads messages from the websocket and sends them to the hub.
|
has channels for registering clients, unregistering clients and broadcasting
|
||||||
|
messages. A `Client` has a buffered channel of outbound messages. One of the
|
||||||
|
client's goroutines reads messages from this channel and writes the messages to
|
||||||
|
the webscoket. The other client goroutine reads messages from the websocket and
|
||||||
|
sends them to the hub.
|
||||||
|
|
||||||
### Hub
|
### Hub
|
||||||
|
|
||||||
The code for the `Hub` type is in [hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go).
|
The code for the `Hub` type is in
|
||||||
|
[hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go).
|
||||||
|
The application's `main` function starts the hub's `run` method as a goroutine.
|
||||||
|
Clients send requests to the hub using the `register`, `unregister` and
|
||||||
|
`broadcast` channels.
|
||||||
|
|
||||||
The application's `main` function starts the hub's `run` method as a goroutine. Connections send requests to the hub using the `register`, `unregister` and `broadcast` channels.
|
The hub registers clients by adding the client pointer as a key in the
|
||||||
|
`clients` map. The map value is always true.
|
||||||
|
|
||||||
The hub registers connections by adding the connection pointer as a key in the `connections` map. The map value is always true.
|
The unregister code is a little more complicated. In addition to deleting the
|
||||||
|
client pointer from the `clients` map, the hub closes the clients's `send`
|
||||||
|
channel to signal the client that no more messages will be sent to the client.
|
||||||
|
|
||||||
The unregister code is a little more complicated. In addition to deleting the connection pointer from the connections map, the hub closes the connection's `send` channel to signal the connection that no more messages will be sent to the connection.
|
The hub handles messages by looping over the registered clients and sending the
|
||||||
|
message to the client's `send` channel. If the client's `send` buffer is full,
|
||||||
|
then the hub assumes that the client is dead or stuck. In this case, the hub
|
||||||
|
unregisters the client and closes the websocket.
|
||||||
|
|
||||||
The hub handles messages by looping over the registered connections and sending the message to the connection's `send` channel. If the connection's `send` buffer is full, then the hub assumes that the client is dead or stuck. In this case, the hub unregisters the connection and closes the websocket.
|
### Client
|
||||||
|
|
||||||
### Conn
|
The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go).
|
||||||
|
|
||||||
The code for the `Conn` type is in [conn.go](https://github.com/gorilla/websocket/blob/master/examples/chat/conn.go).
|
The `wsHandler` function is registered by the application's `main` function as
|
||||||
|
an HTTP handler. The handler upgrades the HTTP connection to the WebSocket
|
||||||
|
protocol, creates a client, registers the client with the hub and schedules the
|
||||||
|
client to be unregistered using a defer statement.
|
||||||
|
|
||||||
An instance of the `wsHandler` type is registered by the application's `main` function as an HTTP handler. The handler upgrades the HTTP connection to the WebSocket protocol, creates a connection object, registers the connection with the hub and schedules the connection to be unregistered using a defer statement.
|
Next, the HTTP handler starts the client's `writePump` method as a goroutine.
|
||||||
|
This method transfers messages from the client's send channel to the websocket
|
||||||
|
connection. The writer method exits when the channel is closed by the hub or
|
||||||
|
there's an error writing to the websocket connection.
|
||||||
|
|
||||||
Next, the HTTP handler starts the connection's `writePump` method as a goroutine. This method transfers messages from the connection's send channel to the websocket. The writer method exits when the channel is closed by the hub or there's an error writing to the websocket.
|
Finally, the HTTP handler calls the client's `readPump` method. This method
|
||||||
|
transfers inbound messages from the websocket to the hub.
|
||||||
|
|
||||||
Finally, the HTTP handler calls the connection's `readPump` method. This method transfers inbound messages from the websocket to the hub.
|
## Frontend
|
||||||
|
|
||||||
## Client
|
The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html).
|
||||||
|
|
||||||
The code for the client is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html).
|
On document load, the script checks for websocket functionality in the browser.
|
||||||
|
If websocket functionality is available, then the script opens a connection to
|
||||||
|
the server and registers a callback to handle messages from the server. The
|
||||||
|
callback appends the message to the chat log using the appendLog function.
|
||||||
|
|
||||||
The client uses [jQuery](http://jquery.com/) to manipulate objects in the browser.
|
To allow the user to manually scroll through the chat log without interruption
|
||||||
|
from new messages, the `appendLog` function checks the scroll position before
|
||||||
|
adding new content. If the chat log is scrolled to the bottom, then the
|
||||||
|
function scrolls new content into view after adding the content. Otherwise, the
|
||||||
|
scroll position is not changed.
|
||||||
|
|
||||||
On document load, the script checks for websocket functionality in the browser. If websocket functionality is available, then the script opens a connection to the server and registers a callback to handle messages from the server. The callback appends the message to the chat log using the appendLog function.
|
The form handler writes the user input to the websocket and clears the input
|
||||||
|
field.
|
||||||
To allow the user to manually scroll through the chat log without interruption from new messages, the `appendLog` function checks the scroll position before adding new content. If the chat log is scrolled to the bottom, then the function scrolls new content into view after adding the content. Otherwise, the scroll position is not changed.
|
|
||||||
|
|
||||||
The form handler writes the user input to the websocket and clears the input field.
|
|
||||||
|
|
|
@ -37,26 +37,28 @@ var upgrader = websocket.Upgrader{
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conn is an middleman between the websocket connection and the hub.
|
// Client is an middleman between the websocket connection and the hub.
|
||||||
type Conn struct {
|
type Client struct {
|
||||||
|
hub *Hub
|
||||||
|
|
||||||
// The websocket connection.
|
// The websocket connection.
|
||||||
ws *websocket.Conn
|
conn *websocket.Conn
|
||||||
|
|
||||||
// Buffered channel of outbound messages.
|
// Buffered channel of outbound messages.
|
||||||
send chan []byte
|
send chan []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPump pumps messages from the websocket connection to the hub.
|
// readPump pumps messages from the websocket connection to the hub.
|
||||||
func (c *Conn) readPump() {
|
func (c *Client) readPump() {
|
||||||
defer func() {
|
defer func() {
|
||||||
hub.unregister <- c
|
c.hub.unregister <- c
|
||||||
c.ws.Close()
|
c.conn.Close()
|
||||||
}()
|
}()
|
||||||
c.ws.SetReadLimit(maxMessageSize)
|
c.conn.SetReadLimit(maxMessageSize)
|
||||||
c.ws.SetReadDeadline(time.Now().Add(pongWait))
|
c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||||
for {
|
for {
|
||||||
_, message, err := c.ws.ReadMessage()
|
_, message, err := c.conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||||
log.Printf("error: %v", err)
|
log.Printf("error: %v", err)
|
||||||
|
@ -64,22 +66,22 @@ func (c *Conn) readPump() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
||||||
hub.broadcast <- message
|
c.hub.broadcast <- message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write writes a message with the given message type and payload.
|
// write writes a message with the given message type and payload.
|
||||||
func (c *Conn) write(mt int, payload []byte) error {
|
func (c *Client) write(mt int, payload []byte) error {
|
||||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
return c.ws.WriteMessage(mt, payload)
|
return c.conn.WriteMessage(mt, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePump pumps messages from the hub to the websocket connection.
|
// writePump pumps messages from the hub to the websocket connection.
|
||||||
func (c *Conn) writePump() {
|
func (c *Client) writePump() {
|
||||||
ticker := time.NewTicker(pingPeriod)
|
ticker := time.NewTicker(pingPeriod)
|
||||||
defer func() {
|
defer func() {
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
c.ws.Close()
|
c.conn.Close()
|
||||||
}()
|
}()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -90,8 +92,8 @@ func (c *Conn) writePump() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
w, err := c.ws.NextWriter(websocket.TextMessage)
|
w, err := c.conn.NextWriter(websocket.TextMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -116,14 +118,14 @@ func (c *Conn) writePump() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveWs handles websocket requests from the peer.
|
// serveWs handles websocket requests from the peer.
|
||||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn := &Conn{send: make(chan []byte, 256), ws: ws}
|
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
||||||
hub.register <- conn
|
client.hub.register <- client
|
||||||
go conn.writePump()
|
go client.writePump()
|
||||||
conn.readPump()
|
client.readPump()
|
||||||
}
|
}
|
|
@ -2,49 +2,53 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Chat Example</title>
|
<title>Chat Example</title>
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function () {
|
window.onload = function () {
|
||||||
var conn;
|
var conn;
|
||||||
var msg = $("#msg");
|
var msg = document.getElementById("msg");
|
||||||
var log = $("#log");
|
var log = document.getElementById("log");
|
||||||
|
|
||||||
function appendLog(msg) {
|
function appendLog(item) {
|
||||||
var d = log[0];
|
var doScroll = log.scrollTop === log.scrollHeight - log.clientHeight;
|
||||||
var doScroll = d.scrollTop === d.scrollHeight - d.clientHeight;
|
log.appendChild(item);
|
||||||
msg.appendTo(log);
|
|
||||||
if (doScroll) {
|
if (doScroll) {
|
||||||
d.scrollTop = d.scrollHeight - d.clientHeight;
|
log.scrollTop = log.scrollHeight - log.clientHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#form").submit(function () {
|
document.getElementById("form").onsubmit = function () {
|
||||||
if (!conn) {
|
if (!conn) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!msg.val()) {
|
if (!msg.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
conn.send(msg.val());
|
conn.send(msg.value);
|
||||||
msg.val("");
|
msg.value = "";
|
||||||
return false;
|
return false;
|
||||||
});
|
};
|
||||||
|
|
||||||
if (window["WebSocket"]) {
|
if (window["WebSocket"]) {
|
||||||
conn = new WebSocket("ws://{{$}}/ws");
|
conn = new WebSocket("ws://{{$}}/ws");
|
||||||
conn.onclose = function (evt) {
|
conn.onclose = function (evt) {
|
||||||
appendLog($("<div><b>Connection closed.</b></div>"));
|
var item = document.createElement("div");
|
||||||
|
item.innerHTML = "<b>Connection closed.</b>";
|
||||||
|
appendLog(item);
|
||||||
};
|
};
|
||||||
conn.onmessage = function (evt) {
|
conn.onmessage = function (evt) {
|
||||||
var messages = evt.data.split('\n')
|
var messages = evt.data.split('\n');
|
||||||
for (var i = 0; i < messages.length; i++) {
|
for (var i = 0; i < messages.length; i++) {
|
||||||
appendLog($("<div/>").text(messages[i]));
|
var item = document.createElement("div");
|
||||||
|
item.innerText = messages[i];
|
||||||
|
appendLog(item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"));
|
var item = document.createElement("div");
|
||||||
|
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
|
||||||
|
appendLog(item);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -4,46 +4,48 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// hub maintains the set of active connections and broadcasts messages to the
|
// hub maintains the set of active clients and broadcasts messages to the
|
||||||
// connections.
|
// clients.
|
||||||
type Hub struct {
|
type Hub struct {
|
||||||
// Registered connections.
|
// Registered clients.
|
||||||
connections map[*Conn]bool
|
clients map[*Client]bool
|
||||||
|
|
||||||
// Inbound messages from the connections.
|
// Inbound messages from the clients.
|
||||||
broadcast chan []byte
|
broadcast chan []byte
|
||||||
|
|
||||||
// Register requests from the connections.
|
// Register requests from the clients.
|
||||||
register chan *Conn
|
register chan *Client
|
||||||
|
|
||||||
// Unregister requests from connections.
|
// Unregister requests from clients.
|
||||||
unregister chan *Conn
|
unregister chan *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var hub = Hub{
|
func newHub() *Hub {
|
||||||
|
return &Hub{
|
||||||
broadcast: make(chan []byte),
|
broadcast: make(chan []byte),
|
||||||
register: make(chan *Conn),
|
register: make(chan *Client),
|
||||||
unregister: make(chan *Conn),
|
unregister: make(chan *Client),
|
||||||
connections: make(map[*Conn]bool),
|
clients: make(map[*Client]bool),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hub) run() {
|
func (h *Hub) run() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case conn := <-h.register:
|
case client := <-h.register:
|
||||||
h.connections[conn] = true
|
h.clients[client] = true
|
||||||
case conn := <-h.unregister:
|
case client := <-h.unregister:
|
||||||
if _, ok := h.connections[conn]; ok {
|
if _, ok := h.clients[client]; ok {
|
||||||
delete(h.connections, conn)
|
delete(h.clients, client)
|
||||||
close(conn.send)
|
close(client.send)
|
||||||
}
|
}
|
||||||
case message := <-h.broadcast:
|
case message := <-h.broadcast:
|
||||||
for conn := range h.connections {
|
for client := range h.clients {
|
||||||
select {
|
select {
|
||||||
case conn.send <- message:
|
case client.send <- message:
|
||||||
default:
|
default:
|
||||||
close(conn.send)
|
close(client.send)
|
||||||
delete(h.connections, conn)
|
delete(h.clients, client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ var addr = flag.String("addr", ":8080", "http service address")
|
||||||
var homeTemplate = template.Must(template.ParseFiles("home.html"))
|
var homeTemplate = template.Must(template.ParseFiles("home.html"))
|
||||||
|
|
||||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println(r.URL)
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.Error(w, "Not found", 404)
|
http.Error(w, "Not found", 404)
|
||||||
return
|
return
|
||||||
|
@ -29,9 +30,12 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
hub := newHub()
|
||||||
go hub.run()
|
go hub.run()
|
||||||
http.HandleFunc("/", serveHome)
|
http.HandleFunc("/", serveHome)
|
||||||
http.HandleFunc("/ws", serveWs)
|
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serveWs(hub, w, r)
|
||||||
|
})
|
||||||
err := http.ListenAndServe(*addr, nil)
|
err := http.ListenAndServe(*addr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("ListenAndServe: ", err)
|
log.Fatal("ListenAndServe: ", err)
|
||||||
|
|
Loading…
Reference in New Issue