Skip to content

Commit 43574ee

Browse files
committed
A working Websocket implementation
1 parent e0bcb5e commit 43574ee

File tree

19 files changed

+817
-0
lines changed

19 files changed

+817
-0
lines changed

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,24 @@
11
# chat
22
Simple chat application with utron
3+
4+
This is implementation of a chat application that ports [revel based chat
5+
application](https://revel.github.io/samples/chat.html) to use utron framework.
6+
7+
# Prerequisite
8+
9+
You only need a working Go environment
10+
11+
# Installation
12+
```
13+
go get github.com/utronframework/chat
14+
```
15+
16+
# Running the application
17+
18+
```
19+
cd $GOPATH/github.com/utronframework/chat
20+
go run main.go
21+
```
22+
23+
# screen shot
24+
![chat app with utron](screenshot.png)

chatroom/chatroom.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package chatroom
2+
3+
import (
4+
"container/list"
5+
"time"
6+
)
7+
8+
type Event struct {
9+
Type string // "join", "leave", or "message"
10+
User string
11+
Timestamp int // Unix timestamp (secs)
12+
Text string // What the user said (if Type == "message")
13+
}
14+
15+
type Subscription struct {
16+
Archive []Event // All the events from the archive.
17+
New <-chan Event // New events coming in.
18+
}
19+
20+
// Owner of a subscription must cancel it when they stop listening to events.
21+
func (s Subscription) Cancel() {
22+
unsubscribe <- s.New // Unsubscribe the channel.
23+
drain(s.New) // Drain it, just in case there was a pending publish.
24+
}
25+
26+
func newEvent(typ, user, msg string) Event {
27+
return Event{typ, user, int(time.Now().Unix()), msg}
28+
}
29+
30+
func Subscribe() Subscription {
31+
resp := make(chan Subscription)
32+
subscribe <- resp
33+
return <-resp
34+
}
35+
36+
func Join(user string) {
37+
publish <- newEvent("join", user, "")
38+
}
39+
40+
func Say(user, message string) {
41+
publish <- newEvent("message", user, message)
42+
}
43+
44+
func Leave(user string) {
45+
publish <- newEvent("leave", user, "")
46+
}
47+
48+
const archiveSize = 10
49+
50+
var (
51+
// Send a channel here to get room events back. It will send the entire
52+
// archive initially, and then new messages as they come in.
53+
subscribe = make(chan (chan<- Subscription), 10)
54+
// Send a channel here to unsubscribe.
55+
unsubscribe = make(chan (<-chan Event), 10)
56+
// Send events here to publish them.
57+
publish = make(chan Event, 10)
58+
)
59+
60+
// This function loops forever, handling the chat room pubsub
61+
func chatroom() {
62+
archive := list.New()
63+
subscribers := list.New()
64+
65+
for {
66+
select {
67+
case ch := <-subscribe:
68+
var events []Event
69+
for e := archive.Front(); e != nil; e = e.Next() {
70+
events = append(events, e.Value.(Event))
71+
}
72+
subscriber := make(chan Event, 10)
73+
subscribers.PushBack(subscriber)
74+
ch <- Subscription{events, subscriber}
75+
76+
case event := <-publish:
77+
for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
78+
ch.Value.(chan Event) <- event
79+
}
80+
if archive.Len() >= archiveSize {
81+
archive.Remove(archive.Front())
82+
}
83+
archive.PushBack(event)
84+
85+
case unsub := <-unsubscribe:
86+
for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
87+
if ch.Value.(chan Event) == unsub {
88+
subscribers.Remove(ch)
89+
break
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
func init() {
97+
go chatroom()
98+
}
99+
100+
// Helpers
101+
102+
// Drains a given channel of any messages.
103+
func drain(ch <-chan Event) {
104+
for {
105+
select {
106+
case _, ok := <-ch:
107+
if !ok {
108+
return
109+
}
110+
default:
111+
return
112+
}
113+
}
114+
}

config/app.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
app_name: utron chat application
2+
base_url: http://localhost:8090
3+
port: 8090
4+
verbose: false
5+
static_dir: public
6+
view_dir: views
7+
database: ""
8+
database_conn: ""
9+
automigrate: true
10+
no_model: true

controllers/app.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package controllers
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gernest/utron/controller"
7+
)
8+
9+
type App struct {
10+
controller.BaseController
11+
Routes []string
12+
}
13+
14+
func (a *App) Index() {
15+
a.Ctx.Template = "Application/Index"
16+
a.Ctx.Data["title"] = "Sign in"
17+
a.HTML(http.StatusOK)
18+
}
19+
20+
func (a *App) Demo() {
21+
r := a.Ctx.Request()
22+
r.ParseForm()
23+
user := r.FormValue("user")
24+
demo := r.FormValue("demo")
25+
switch demo {
26+
case "websocket":
27+
a.Ctx.Redirect("/websocket/room?user="+user, http.StatusFound)
28+
}
29+
}
30+
31+
func NewApp() controller.Controller {
32+
return &App{
33+
Routes: []string{
34+
"get;/;Index",
35+
"get;/demo;Demo",
36+
},
37+
}
38+
}

controllers/refresh.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package controllers
2+
3+
import "github.com/gernest/utron/controller"
4+
5+
type Refresh struct {
6+
controller.BaseController
7+
Routes []string
8+
}
9+
10+
func (c Refresh) Index() {
11+
}
12+
13+
func (c Refresh) Room() {
14+
}
15+
16+
func (c Refresh) Say(user, message string) {
17+
}
18+
19+
func (c Refresh) Leave(user string) {
20+
}
21+
22+
func NewRefresh() controller.Controller {
23+
return &Refresh{
24+
Routes: []string{
25+
"get;/refresh;Index",
26+
"get;/refresh/room;Room",
27+
"post;/refresh/room;Say",
28+
"post;/refresh/room/leave;Leave",
29+
},
30+
}
31+
}

controllers/websocket.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package controllers
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
8+
"github.com/gernest/utron/controller"
9+
"github.com/gorilla/websocket"
10+
"github.com/utronframework/chat/chatroom"
11+
)
12+
13+
var upgrader = websocket.Upgrader{
14+
ReadBufferSize: 1024,
15+
WriteBufferSize: 1024,
16+
}
17+
18+
type Websocket struct {
19+
controller.BaseController
20+
Routes []string
21+
}
22+
23+
func (w *Websocket) Room() {
24+
r := w.Ctx.Request()
25+
user := r.URL.Query().Get("user")
26+
w.Ctx.Data["title"] = "Chat Room"
27+
w.Ctx.Data["user"] = user
28+
w.Ctx.Template = "WebSocket/Room"
29+
w.HTML(http.StatusOK)
30+
}
31+
32+
func (w *Websocket) RoomSocket() {
33+
fmt.Println("HOME WS")
34+
r := w.Ctx.Request()
35+
res := w.Ctx.Response()
36+
user := r.URL.Query().Get("user")
37+
conn, err := upgrader.Upgrade(res, r, nil)
38+
if err != nil {
39+
log.Println(err)
40+
return
41+
}
42+
43+
subscription := chatroom.Subscribe()
44+
defer subscription.Cancel()
45+
46+
chatroom.Join(user)
47+
defer chatroom.Leave(user)
48+
49+
// Send down the archive.
50+
for _, event := range subscription.Archive {
51+
if conn.WriteJSON(event) != nil {
52+
return
53+
}
54+
}
55+
56+
// In order to select between websocket messages and subscription events, we
57+
// need to stuff websocket events into a channel.
58+
newMessages := make(chan string)
59+
go func() {
60+
for {
61+
_, b, err := conn.ReadMessage()
62+
if err != nil {
63+
close(newMessages)
64+
return
65+
}
66+
newMessages <- string(b)
67+
}
68+
}()
69+
70+
// Now listen for new events from either the websocket or the chatroom.
71+
for {
72+
select {
73+
case event := <-subscription.New:
74+
if conn.WriteJSON(&event) != nil {
75+
return
76+
}
77+
case msg, ok := <-newMessages:
78+
// If the channel is closed, they disconnected.
79+
if !ok {
80+
return
81+
}
82+
83+
// Otherwise, say something.
84+
chatroom.Say(user, msg)
85+
}
86+
}
87+
return
88+
}
89+
90+
func NewWebsocket() controller.Controller {
91+
return &Websocket{
92+
Routes: []string{
93+
"get;/websocket/room;Room",
94+
"get;/websocket/room/socket;RoomSocket",
95+
},
96+
}
97+
}

main.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
8+
"github.com/gernest/utron"
9+
"github.com/utronframework/chat/controllers"
10+
)
11+
12+
func main() {
13+
14+
// Start the MVC App
15+
app, err := utron.NewMVC()
16+
if err != nil {
17+
log.Fatal(err)
18+
}
19+
20+
// Register Controllers
21+
app.AddController(controllers.NewApp)
22+
app.AddController(controllers.NewWebsocket)
23+
app.AddController(controllers.NewRefresh)
24+
25+
// Start the server
26+
port := fmt.Sprintf(":%d", app.Config.Port)
27+
app.Log.Info("staring server on port", port)
28+
log.Fatal(http.ListenAndServe(port, app))
29+
}

public/images/favicon.png

687 Bytes
Loading

public/javascripts/jquery-1.5.min.js

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/javascripts/jquery.scrollTo-min.js

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/javascripts/templating.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
// Simple JavaScript Templating
3+
// John Resig - http://ejohn.org/ - MIT Licensed
4+
(function(){
5+
var cache = {};
6+
7+
this.tmpl = function tmpl(str, data){
8+
// Figure out if we're getting a template, or if we need to
9+
// load the template - and be sure to cache the result.
10+
var fn = !/\W/.test(str) ?
11+
cache[str] = cache[str] ||
12+
tmpl(document.getElementById(str).innerHTML) :
13+
14+
// Generate a reusable function that will serve as a template
15+
// generator (and which will be cached).
16+
new Function("obj",
17+
"var p=[],print=function(){p.push.apply(p,arguments);};" +
18+
19+
// Introduce the data as local variables using with(){}
20+
"with(obj){p.push('" +
21+
22+
// Convert the template into pure JavaScript
23+
str
24+
.replace(/[\r\t\n]/g, " ")
25+
.split("<%").join("\t")
26+
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
27+
.replace(/\t=(.*?)%>/g, "',$1,'")
28+
.split("\t").join("');")
29+
.split("%>").join("p.push('")
30+
.split("\r").join("\\'")
31+
+ "');}return p.join('');");
32+
33+
// Provide some basic currying to the user
34+
return data ? fn( data ) : fn;
35+
};
36+
})();

0 commit comments

Comments
 (0)