Skip to content

Instantly share code, notes, and snippets.

@taichi
Created February 26, 2014 12:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taichi/9228318 to your computer and use it in GitHub Desktop.
Save taichi/9228318 to your computer and use it in GitHub Desktop.
simple chat server on top of websocket.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat Example</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
var conn;
function appendLog(msg) {
var log = $("#log");
var d = log[0]
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
msg.appendTo(log)
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
}
}
$("#form").submit(function() {
var msg = $("#msg");
if (!conn) {
return false;
}
if (!msg.val()) {
return false;
}
conn.send(msg.val());
msg.val("");
return false
});
if (window["WebSocket"]) {
conn = new WebSocket("ws://{{.}}/ws");
conn.onclose = function(evt) {
appendLog($("<div><b>Connection closed.</b></div>"))
}
conn.onmessage = function(evt) {
appendLog($("<div/>").text(evt.data))
}
} else {
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>"))
}
});
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 3em;
overflow: auto;
}
#form {
padding: 0 0.5em 0 0.5em;
margin: 0;
position: absolute;
bottom: 1em;
left: 0px;
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="log"></div>
<form id="form">
<input type="submit" value="Send" />
<input type="text" id="msg" size="64"/>
</form>
</body>
</html>
package main
import (
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"html/template"
"log"
"net/http"
)
const (
BUFFER_SIZE int = 8192
)
var homeTemplate = template.Must(template.ParseFiles("home.html"))
func HomeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
homeTemplate.Execute(w, r.Host)
}
func WebsocketHandler(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Origin") != "http://"+r.Host {
http.Error(w, "Origin not allowed", 403)
return
}
ws, err := websocket.Upgrade(w, r, nil, BUFFER_SIZE, BUFFER_SIZE)
if _, ok := err.(websocket.HandshakeError); ok {
http.Error(w, "Not a websocket handshake", 400)
return
} else if err != nil {
http.Error(w, "Server Error", 500)
log.Println(err)
return
}
sock := NewSocket(ws)
sock.Run(room)
}
var room *Room = NewRoom()
func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/ws", WebsocketHandler).Methods("GET")
http.Handle("/", r)
go room.Run()
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalln(err)
}
}
package main
// cf. https://github.com/gorilla/websocket/tree/master/examples/chat
import (
"github.com/gorilla/websocket"
"log"
"sync"
"time"
)
type Room struct {
sockets map[*Socket]bool
ops chan func()
}
func NewRoom() *Room {
return &Room{
sockets: make(map[*Socket]bool),
ops: make(chan func(), 256),
}
}
func (r *Room) Run() {
ticker := time.NewTicker(300)
defer ticker.Stop()
for {
select {
case fn, ok := <-r.ops:
if ok {
fn()
} else {
return
}
case <-ticker.C:
// TODO gracefull shoutdown
}
}
}
func (r *Room) Enter(s *Socket) {
r.queue(func() { r.sockets[s] = true })
}
func (r *Room) Leave(s *Socket) {
r.queue(func() { delete(r.sockets, s) })
}
func (r *Room) Say(msg []byte) {
r.queue(func() {
for s := range r.sockets {
go s.Say(msg, r)
}
})
}
func (r *Room) queue(fn func()) {
select {
case r.ops <- fn:
default:
// TODO retry ?
log.Println("fail to queue operation")
}
}
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
type Socket struct {
ws *websocket.Conn
msgQueue chan []byte
closer sync.Once
}
func NewSocket(ws *websocket.Conn) *Socket {
return &Socket{
ws: ws,
msgQueue: make(chan []byte, 256),
}
}
func (s *Socket) Close() {
s.closer.Do(func() {
if err := s.closeMessage(); err != nil {
log.Println(err)
}
close(s.msgQueue)
if err := s.ws.Close(); err != nil {
log.Println(err)
}
})
}
func (s *Socket) Say(msg []byte, r *Room) {
select {
case s.msgQueue <- msg:
log.Printf("Say %s\n", msg)
default:
log.Printf("Fail to say %s\n", msg)
r.Leave(s)
s.Close()
}
}
func (s *Socket) Run(r *Room) {
r.Enter(s)
go s.readLoop(r)
go s.writeLoop(r)
}
func (s *Socket) readLoop(r *Room) {
defer func() {
r.Leave(s)
s.Close()
}()
s.ws.SetReadLimit(maxMessageSize)
s.extendDeadLine()
s.ws.SetPongHandler(func(str string) error {
return s.extendDeadLine()
})
for {
_, msg, err := s.ws.ReadMessage()
if err != nil {
log.Println(err)
break
}
r.Say(msg)
}
}
func (s *Socket) extendDeadLine() error {
return s.ws.SetReadDeadline(time.Now().Add(pongWait))
}
func (s *Socket) writeMessage(msgType int, payload []byte) error {
if err := s.extendDeadLine(); err != nil {
return err
}
return s.ws.WriteMessage(msgType, payload)
}
func (s *Socket) closeMessage() error {
return s.writeMessage(websocket.CloseMessage, []byte{})
}
func (s *Socket) pingMessage() error {
return s.writeMessage(websocket.PingMessage, []byte{})
}
func (s *Socket) writeLoop(r *Room) {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
s.Close()
}()
for {
select {
case msg, ok := <-s.msgQueue:
if ok {
if err := s.writeMessage(websocket.TextMessage, msg); err != nil {
log.Println(err)
return
}
} else {
return
}
case <-ticker.C:
if err := s.pingMessage(); err != nil {
log.Println(err)
return
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment