Skip to content

Commit

Permalink
add socket server
Browse files Browse the repository at this point in the history
  • Loading branch information
cmilhench committed Aug 12, 2024
1 parent 9534e2e commit a4397d7
Show file tree
Hide file tree
Showing 12 changed files with 584 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test: lint ## Run the project tests
start: test ## Start the server
$(call cyan, "Running...")
$(call setenv,)
@go run -ldflags '-w -s ' ./cmd/
@go run -ldflags '-w -s ' ./cmd/chat/
.PHONY: start

watch: ## Run locally and monitor for changes
Expand Down
61 changes: 61 additions & 0 deletions cmd/chat/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"embed"
"fmt"
"log"
"net/http"
"time"

"github.com/cmilhench/x/exp/http/socket"
"github.com/cmilhench/x/exp/http/static"
"github.com/cmilhench/x/exp/irc"
)

//go:embed static
var fs embed.FS

func main() {
server := socket.NewSocketServer()
server.Handle(socketHandler(server))
server.Start()

http.Handle("/", http.FileServer(static.Neutered{Prefix: "static", FileSystem: http.FS(fs)}))
http.HandleFunc("/ws", server.HandleConnections)

log.Println("Socket server started on :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("ListenAndServe: %v", err)
}
}

func socketHandler(server *socket.SocketServer) socket.MessageHandler {
return func(client *socket.Client, messageBytes []byte) {
message := irc.ParseMessage(string(messageBytes))
log.Printf("message ->: %#v", message)
switch message.Command {
case "INFO": // returns information about the <target> server
client.Send([]byte(fmt.Sprintf("INFO %s", "This is an IRC server.")))
case "MOTD": // returns the message of the day
client.Send([]byte(fmt.Sprintf("MOTD %s", "Welcome to the IRC server!")))
case "NICK": // allows a client to change their IRC nickname.
client.Name = message.Params
case "PING": // tests the presence of a connection
client.Send([]byte(fmt.Sprintf("PONG %s", message.Params)))
case "NOTICE", "PRIVMSG": // Sends <message> to <target>, which is usually a user or channel.
if message.Params[0] == '#' {
server.Broadcast([]byte(fmt.Sprintf(":%s PRIVMSG %s", client.Name, message.Trailing)))
} else {
server.Send(message.Params, []byte(fmt.Sprintf(":%s PRIVMSG %s", client.Name, message.Trailing)))
}
case "QUIT": // disconnects the user from the server.
server.Part(client)
case "TIME": // returns the current time on the server
client.Send([]byte(time.Now().Format(time.RFC1123Z)))
case "TOPIC": // sets the topic of <channel> to <topic>
default:
log.Printf("Unknown message type: %#v", message)
}
}
}
176 changes: 176 additions & 0 deletions cmd/chat/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Client</title>
</head>
<body>

<h2>WebSocket Client</h2>

<div>
<label for="message">Message:</label>
<input type="text" id="message" placeholder="message" onchange="enter()">
</div>

<h3>Messages:</h3>
<div id="messages"></div>

<script>
function Client(url) {
if (!(this instanceof Client)) return new Client(url);
this.delay = 0;
this.open(url)
}
Client.prototype.open = function (url) {
var self = this;
var args = Array.prototype.slice.call(arguments);
self._socket = new WebSocket(url);
self._socket.binaryType = "arraybuffer";
self._socket.onerror = function(event) {
console.error("WebSocket error:", event);
};
self._socket.onopen = function (event) {
self.delay = 0;
console.debug('WebSocket connected');
}
self._socket.onclose = function (event) {
console.log('WebSocket disconnected [' + event.code +']!');
if (event.code !== 1000) {
self.delay = Math.min(Math.max(self.delay *= 2, 0.5), 30); // 0.5, 1, 2, 4, 8, 16, 30, 30
window.setTimeout(function () { self.open.apply(self, args); }, self.delay * 1000);
}
}
self._socket.onmessage = function (event) { self.onmessage(event); }
return self;
}
Client.prototype.send = function (data, options) {
return this._socket.send(data, options)
}
Client.prototype.onmessage = function (event) {}
</script>

<script>
var c = new Client("ws://localhost:8080/ws");
c.onmessage = function(event) {
const parent = document.getElementById("messages");
const element = document.createElement("div");
const arrayBuffer = event.data;
message = Parse(new TextDecoder("utf-8").decode(arrayBuffer))
console.debug(message)
switch (message.Command) {
case "NOTICE":
case "PONG":
case "PRIVMSG":
element.textContent = "Received: " + message.Params;
break;
default:
element.textContent = "Received: " + message.Raw;
break;
}
parent.appendChild(element);
};
function enter() {
const encoder = new TextEncoder();
const input = document.getElementById("message");
let line = input.value;
let prefix = '';
let command = '';
let params = '';
let trailing = '';
if (line[0] !== '/') {
command = 'PRIVMSG'
params = '#channel'
trailing = line
} else {
let i = line.indexOf(' ');
if (i === -1) {
i = line.length;
}
command = line.substring(1, i);
command = command.toUpperCase();
switch (command) {
case 'NOTICE':
case 'PRIVMSG':
break;
default:
break;
}
line = line.substring(i);
// Params
i = line.indexOf(' :');
if (i === -1) {
i = line.length;
}
if (i !== 0) {
params = line.substring(1, i);
}
// Trailing
if (line.length - i > 2) {
trailing = line.substring(i + 2);
}
}
message = Message(prefix, command, params, trailing)
c.send(encoder.encode(message));
input.value = "";
};
</script>

<script>
function Message(prefix, command, params, trailing) {
let result = "";
if (prefix) {
result += `:${prefix} `;
}
result += command;
if (params) {
result += ` ${params}`;
}
if (trailing) {
result += ` :${trailing}`;
}
return result;
}
function Parse(line) {
line = line.replace(/\r$/, "");
line = line.replace(/\r\n$/, "");
const orig = line;
const message = {
Raw: orig,
Prefix: "",
Command: "",
Params: "",
Trailing: ""
};
// Prefix
if (line[0] === ':') {
const i = line.indexOf(" ");
message.Prefix = line.substring(1, i);
line = line.substring(i + 1);
}
// Command
let i = line.indexOf(" ");
if (i === -1) {
i = line.length;
}
message.Command = line.substring(0, i);
line = line.substring(i);
// Params
i = line.indexOf(" :");
if (i === -1) {
i = line.length;
}
if (i !== 0) {
message.Params = line.substring(1, i);
}
// Trailing
if (line.length - i > 2) {
message.Trailing = line.substring(i + 2);
}
return message;
}
</script>

</body>
</html>
60 changes: 60 additions & 0 deletions exp/http/socket/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package socket

import (
"log"
"time"

"github.com/cmilhench/x/exp/uuid"

"github.com/gorilla/websocket"
)

type Client struct {
conn *websocket.Conn
send chan []byte
id string
Name string
}

type MessageHandler func(*Client, []byte)

func NewClient(conn *websocket.Conn) *Client {
id, _ := uuid.New()
return &Client{
id: id,
conn: conn,
send: make(chan []byte),
}
}

func (client *Client) ReadMessages(fn MessageHandler) {
for {
_, msg, err := client.conn.ReadMessage()
if err != nil {
log.Printf("Read error: %v", err)
break
}
fn(client, msg)
}
}

func (client *Client) WriteMessages() {
for msg := range client.send {
err := client.conn.WriteMessage(websocket.BinaryMessage, msg)
if err != nil {
log.Printf("Write error: %v", err)
break
}
}
}

func (client *Client) Send(data []byte) {
client.send <- data
}

func (client *Client) Close() {
close(client.send)
deadline := time.Now().Add(5 * time.Second)
data := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
_ = client.conn.WriteControl(websocket.CloseMessage, data, deadline)
}
Loading

0 comments on commit a4397d7

Please sign in to comment.