Skip to content

Commit

Permalink
Merge pull request #3 from composer22/feature/server
Browse files Browse the repository at this point in the history
Feature/server Cleaned
  • Loading branch information
composer22 committed Apr 24, 2015
2 parents f6c07b3 + ac78f1b commit 0651252
Show file tree
Hide file tree
Showing 22 changed files with 330 additions and 195 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
[![Current Release](https://img.shields.io/badge/release-v0.1.0-brightgreen.svg)](https://github.com/composer22/chattypantz/releases/tag/v0.1.0)
[![Coverage Status](https://coveralls.io/repos/composer22/chattypantz/badge.svg?branch=master)](https://coveralls.io/r/composer22/chattypantz?branch=master)

![chattypantz-logo](assets/img/chattypantz.png)

A demo chat server and client written in [Go.](http://golang.org)

## About
Expand All @@ -15,20 +17,20 @@ Some key objectives in this demonstration:
* Clients connect to the server on ws://{host:port}
* Messages sent by a client are broadcasted to other clients connected to the same chat room.
* The server only supports JSON text messages. Binary websocket frames will be discarded and the clients sending those frames will be disconnected with a message.
* When a client connects to a chat room, the server broadcasts "{nickname} joined the room." to clients that were already connected to the same chat room.
* When a client connects to a chat room, the server broadcasts "{nickname} has joined the room." to clients that were already connected to the same chat room.
* When a client disconnects, the server broadcasts "{nickname} has left the room." to clients connected to the same chat room.
* An unlimited amount of chat rooms can be created on the server (unless it runs out of memory or file descriptors).
* An unlimited amount of clients can join each chat room on the server (unless it runs out of memory or file descriptors).
* Only one connection per unique nickname allowed per chat room.
* The server should provide an optional idle timeout setting. If a user doesn't interact withing n-seconds, the user should be automatically disconnected.
* The server should provide an optional parameter to limit connections to the server.
* The server should provide an optional parameter to limit the number of rooms created.
* Heartbeat (alive) and statistics should be provided via http:// API endpoints.

Additional objectives:

* Only one connection per nickname allowed per chat room.
* The server should provide an idle timeout setting. If a user doesn't interact withing n-seconds, the user should be automatically disconnected.
* The server should provide a parameter to limit connections and number of rooms.
* Alive and statistics should be provided by http:// API endpoints.
* Chat history for each room should be stored in a file for each chat. When the user logs in to a room, the history should be provided to the client. A max history option should be provided.
Future objectives:

For TODOs, please see TODO.md
* Chat history for each room should be stored in a file. When the user logs in to a room, the history should be provided to the client. A max history option should be provided.
* More sophisticated client example code in html and js.

## Usage

Expand All @@ -44,7 +46,6 @@ Server options:
-L, --profiler_port PORT *PORT the profiler is listening on (default: off).
-n, --connections MAX *MAX client connections allowed (default: unlimited).
-r, --rooms MAX *MAX chatrooms allowed (default: unlimited).
-y, --history MAX *MAX num of history records per room (default: 15).
-i, --idle MAX *MAX idle time in seconds allowed (default: unlimited).
-X, --procs MAX *MAX processor cores to use from the machine.
Expand Down Expand Up @@ -94,9 +95,10 @@ Spaces must be encoded in JSON calls.
# ChatReqTypeListRooms = 103
/send {"reqType":103}
# Join a room or join a room with hidden name.
# Join a room
# ChatReqTypeJoin = 104
/send {"roomName":"Your\ Room","reqType":104}
# or join a room with hidden name.
/send {"roomName":"Your\ Room","reqType":104,"content":"hidden"}
# Get a list of nicknames in a room.
Expand Down Expand Up @@ -148,9 +150,7 @@ Date: Fri, 03 Apr 2015 17:29:17 +0000
Server: San Francisco
X-Request-Id: DC8D9C2E-8161-4FC0-937F-4CA7037970D5
Content-Length: 0
```

## Building

This code currently requires version 1.42 or higher of Go.
Expand All @@ -176,10 +176,11 @@ A prebuilt docker image is available at (http://www.docker.com) [chattypantz](ht
If you have docker installed, run:
```
docker pull composer22/chattypantz:latest
or
docker pull composer22/chattypantz:<version>
```
See /docker directory README for more information on how to run it.


## License

(The MIT License)
Expand Down
11 changes: 2 additions & 9 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
# OnDeck
- [ ] Server - stats aggregation + API
- [ ] Server - API for alive
- [ ] Front end code.
- [ ] Enable history in a room and load n records from that log.

# Backlog
- [ ] Token login
- [ ] TLS access

# Done

- [ ] Enable history for each room and load n-ary records from that log.
- [ ] More sophisticated client example code.
Binary file added assets/img/chattypantz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions chattypantz.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ func main() {
flag.IntVar(&opts.MaxConns, "--connections", server.DefaultMaxConns, "Maximum client connections allowed.")
flag.IntVar(&opts.MaxRooms, "r", server.DefaultMaxRooms, "Maximum chat rooms allowed.")
flag.IntVar(&opts.MaxRooms, "--rooms", server.DefaultMaxRooms, "Maximum chat rooms allowed.")
flag.IntVar(&opts.MaxHistory, "y", server.DefaultMaxHistory, "Maximum chat room history allowed.")
flag.IntVar(&opts.MaxHistory, "--history", server.DefaultMaxHistory, "Maximum chat room history allowed.")
flag.IntVar(&opts.MaxIdle, "i", server.DefaultMaxIdle, "Maximum client idle allowed.")
flag.IntVar(&opts.MaxIdle, "--idle", server.DefaultMaxIdle, "Maximum client idle allowed.")
flag.IntVar(&opts.MaxProcs, "X", server.DefaultMaxProcs, "Maximum processor cores to use.")
Expand Down
40 changes: 0 additions & 40 deletions client/EchoTest.html

This file was deleted.

33 changes: 25 additions & 8 deletions server/chat_room.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,33 +129,50 @@ func (r *ChatRoom) message(q *ChatRequest) {
// leave removes the chatter from the room and notifies the group the chatter has left.
func (r *ChatRoom) leave(q *ChatRequest) {
name := q.Who.nickname
r.mu.Lock()
delete(r.chatters, q.Who)
r.mu.Unlock()
r.sendResponse(q.Who, ChatRspTypeLeave, fmt.Sprintf(`You have left room "%s".`, r.name), nil)
r.sendResponseAll(ChatRspTypeLeave, fmt.Sprintf("%s has left the room.", name), nil)
}

// ChatRoomStats is a simple structure for returning statistic information on the room.
type ChatRoomStats struct {
Name string `json:"name"` // The name of the room.
Start time.Time `json:"start"` // The start time of the room.
LastReq time.Time `json:"lastReq"` // The last request time to the room.
LastRsp time.Time `json:"lastRsp"` // The last response time from the room.
ReqCount uint64 `json:"reqcount"` // Total requests received.
RspCount uint64 `json:"rspCount"` // Total responses sent.
Name string `json:"name"` // The name of the room.
Start time.Time `json:"start"` // The start time of the room.
LastReq time.Time `json:"lastReq"` // The last request time to the room.
LastRsp time.Time `json:"lastRsp"` // The last response time from the room.
ReqCount uint64 `json:"reqcount"` // Total requests received.
RspCount uint64 `json:"rspCount"` // Total responses sent.
Chatters []*ChatRoomChatterStat `json:"chatters"` // Stats on chatters in the room
}

type ChatRoomChatterStat struct {
Nickname string `json:"nickname"` // The nickname of the chatter.
RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter.
}

// stats returns status information on the room.
func (r *ChatRoom) stats() *ChatRoomStats {
r.mu.Lock()
defer r.mu.Unlock()
return &ChatRoomStats{
s := &ChatRoomStats{
Name: r.name,
Start: r.start,
LastReq: r.lastReq,
LastRsp: r.lastRsp,
ReqCount: r.reqCount,
RspCount: r.rspCount,
Chatters: []*ChatRoomChatterStat{},
}
for c := range r.chatters {
st := c.stats()
s.Chatters = append(s.Chatters, &ChatRoomChatterStat{
Nickname: st.Nickname,
RemoteAddr: st.RemoteAddr,
})
}
return s
}

// isMember validates if the member exists in the room.
Expand All @@ -177,6 +194,7 @@ func (r *ChatRoom) isMemberName(n string) bool {
// sendResponse sends a message to a single chatter in the room.
func (r *ChatRoom) sendResponse(c *Chatter, rt int, ct string, l []string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.connected {
if l == nil {
l = []string{}
Expand All @@ -189,7 +207,6 @@ func (r *ChatRoom) sendResponse(c *Chatter, rt int, ct string, l []string) {
c.rspq <- rsp
}
}
c.mu.Unlock()
}

// sendResponseAll sends a message to all chatters in the room.
Expand Down
17 changes: 15 additions & 2 deletions server/chat_room_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (
// ChatRoomManager represents a hub of chat rooms for the server.
type ChatRoomManager struct {
rooms map[string]*ChatRoom // A list of rooms on the server.
maxRooms int // Maximum number of rooms allowed to be created
maxRooms int // Maximum number of rooms allowed to be created.
log *ChatLogger // Application log for events.
wg sync.WaitGroup // Synchronizer for manager reqq
wg sync.WaitGroup // Synchronizer for manager reqq.
mu sync.Mutex // Lock for update.
}

// ChatRoomManagerNew is a factory function that returns a new instance of a chat room manager.
Expand All @@ -25,6 +26,8 @@ func ChatRoomManagerNew(n int, cl *ChatLogger) *ChatRoomManager {

// list returns a list of chat room names.
func (m *ChatRoomManager) list() []string {
m.mu.Lock()
defer m.mu.Unlock()
var names []string
for n := range m.rooms {
names = append(names, n)
Expand All @@ -34,7 +37,9 @@ func (m *ChatRoomManager) list() []string {

// find will find a chat room for a given name.
func (m *ChatRoomManager) find(n string) (*ChatRoom, error) {
m.mu.Lock()
r, ok := m.rooms[n]
m.mu.Unlock()
if !ok {
return nil, errors.New(fmt.Sprintf(`Chatroom "%s" not found.`, n))
}
Expand All @@ -49,15 +54,19 @@ func (m *ChatRoomManager) findCreate(n string) (*ChatRoom, error) {
return nil, errors.New("Maximum number of rooms reached. Cannot create new room.")
}
r = ChatRoomNew(n, m.log, &m.wg)
m.mu.Lock()
m.rooms[n] = r
m.wg.Add(1)
m.mu.Unlock()
go r.Run()
}
return r, nil
}

// removeChatterAllRooms releases the chatter from any rooms.
func (m *ChatRoomManager) removeChatterAllRooms(c *Chatter) {
m.mu.Lock()
defer m.mu.Unlock()
for _, r := range m.rooms {
if q, err := ChatRequestNew(c, r.name, ChatReqTypeLeave, ""); err == nil {
r.reqq <- q
Expand All @@ -67,6 +76,8 @@ func (m *ChatRoomManager) removeChatterAllRooms(c *Chatter) {

// getRoomStats returns statistics from each room.
func (m *ChatRoomManager) getRoomStats() []*ChatRoomStats {
m.mu.Lock()
defer m.mu.Unlock()
var s = []*ChatRoomStats{}
for _, r := range m.rooms {
s = append(s, r.stats())
Expand All @@ -76,6 +87,8 @@ func (m *ChatRoomManager) getRoomStats() []*ChatRoomStats {

// shutDownRooms releases all rooms from processing and memory.
func (m *ChatRoomManager) shutDownRooms() {
m.mu.Lock()
defer m.mu.Unlock()
// Close the channel which signals a stop run
for _, r := range m.rooms {
close(r.reqq)
Expand Down
2 changes: 1 addition & 1 deletion server/chatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (c *Chatter) listRooms() {
// ChatterStats is a simple structure for returning statistic information on the chatter.
type ChatterStats struct {
Nickname string `json:"nickname"` // The nickname of the chatter.
RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter
RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter.
Start time.Time `json:"start"` // The start time of the chatter.
LastReq time.Time `json:"lastReq"` // The last request time from the chatter.
LastRsp time.Time `json:"lastRsp"` // The last response time to the chatter.
Expand Down
17 changes: 8 additions & 9 deletions server/const.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package server

const (
version = "0.1.0" // Application and server version.
DefaultHostname = "localhost" // The hostname of the server.
DefaultPort = 6660 // Port to receive requests: see IANA Port Numbers.
DefaultProfPort = 0 // Profiler port to receive requests. *
DefaultMaxConns = 0 // Maximum number of connections allowed. *
DefaultMaxRooms = 0 // Maximum number of chat rooms allowed. *
DefaultMaxHistory = 15 // Maximum number of chat history records per room.
DefaultMaxIdle = 0 // Maximum idle seconds per user connection. *
DefaultMaxProcs = 0 // Maximum number of computer processors to utilize. *
version = "0.1.0" // Application and server version.
DefaultHostname = "localhost" // The hostname of the server.
DefaultPort = 6660 // Port to receive requests: see IANA Port Numbers.
DefaultProfPort = 0 // Profiler port to receive requests. *
DefaultMaxConns = 0 // Maximum number of connections allowed. *
DefaultMaxRooms = 0 // Maximum number of chat rooms allowed. *
DefaultMaxIdle = 0 // Maximum idle seconds per user connection. *
DefaultMaxProcs = 0 // Maximum number of computer processors to utilize. *

// * zeros = no change or no limitation or not enabled.

Expand Down
21 changes: 10 additions & 11 deletions server/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import "encoding/json"

// Info provides basic config information to/about the running server.
type Info struct {
Version string `json:"version"` // Version of the server.
Name string `json:"name"` // The name of the server.
Hostname string `json:"hostname"` // The hostname of the server.
UUID string `json:"UUID"` // Unique ID of the server.
Port int `json:"port"` // Port the server is listening on.
ProfPort int `json:"profPort"` // Profiler port the server is listening on.
MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted.
MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed.
MaxHistory int `json:"maxHistory"` // The maximum number of history recs to retain per room.
MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect.
Debug bool `json:"debugEnabled"` // Is debugging enabled on the server.
Version string `json:"version"` // Version of the server.
Name string `json:"name"` // The name of the server.
Hostname string `json:"hostname"` // The hostname of the server.
UUID string `json:"UUID"` // Unique ID of the server.
Port int `json:"port"` // Port the server is listening on.
ProfPort int `json:"profPort"` // Profiler port the server is listening on.
MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted.
MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed.
MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect.
Debug bool `json:"debugEnabled"` // Is debugging enabled on the server.
}

// InfoNew is a factory function that returns a new instance of Info.
Expand Down
8 changes: 3 additions & 5 deletions server/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
const (
testInfoExpectedJSONResult = `{"version":"9.8.7","name":"Test Server","hostname":"0.0.0.0",` +
`"UUID":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","port":6661,"profPort":6061,"maxConns":999,` +
`"maxRooms":888,"maxHistory":777,"maxIdle":666,"debugEnabled":true}`
`"maxRooms":888,"maxIdle":777,"debugEnabled":true}`
)

func TestInfoNew(t *testing.T) {
Expand All @@ -22,8 +22,7 @@ func TestInfoNew(t *testing.T) {
i.ProfPort = 6061
i.MaxConns = 999
i.MaxRooms = 888
i.MaxHistory = 777
i.MaxIdle = 666
i.MaxIdle = 777
i.Debug = true
})
tp := reflect.TypeOf(info)
Expand Down Expand Up @@ -55,8 +54,7 @@ func TestInfoString(t *testing.T) {
i.ProfPort = 6061
i.MaxConns = 999
i.MaxRooms = 888
i.MaxHistory = 777
i.MaxIdle = 666
i.MaxIdle = 777
i.Debug = true
})
actual := fmt.Sprint(info)
Expand Down
Loading

0 comments on commit 0651252

Please sign in to comment.