Created
February 26, 2014 12:01
-
-
Save taichi/9228318 to your computer and use it in GitHub Desktop.
simple chat server on top of websocket.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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