chatv3
k98kurz |
PRO |
07/10/23 08:48:50 PM UTC (Edited) |
0 ⭐ |
18143 👁️ |
Never ⏰ |
[chat, go, multithreaded]
package main
import (
"bufio"
"encoding/binary"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
)
type Client struct {
socket net.Conn
username string
inbox chan string
}
type ActiveClients struct {
mu sync.RWMutex
clients map[string]*Client
}
func (ac *ActiveClients) add(c *Client) bool {
ac.mu.Lock()
defer ac.mu.Unlock()
if ac.clients == nil {
ac.clients = make(map[string]*Client, 0)
}
if c.inbox == nil {
c.inbox = make(chan string, 100)
}
for username := range ac.clients {
if username == c.username {
return false
}
}
ac.clients[c.username] = c
return true
}
func (ac *ActiveClients) remove(c Client) bool {
ac.mu.Lock()
defer ac.mu.Unlock()
debug("Removing %v\n", c.username)
if ac.clients == nil {
return false
}
_, ok := ac.clients[c.username]
if ok {
delete(ac.clients, c.username)
}
return ok
}
func (ac *ActiveClients) forwardMessage(message string, from string) {
ac.mu.RLock()
defer ac.mu.RUnlock()
msgWithFrom := from + ": " + message
for username, c := range ac.clients {
if username != from {
debug("Attempting to forward to %v...", username)
select {
case c.inbox <- msgWithFrom:
debug("Forwarding message to %v\n", username)
default:
// if buffer is full, drop the message
debug("Message dropped because %v inbox is full.\n", c.username)
}
}
}
}
var wg sync.WaitGroup
var shouldDebug bool = false
func debug(message string, vals ...any) {
if shouldDebug {
fmt.Printf(message, vals...)
}
}
func uint16ToBytes(i uint16) []byte {
// return []byte{byte(i >> 8), byte(i & 0xFF)}
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, i)
return b
}
func bytesToUint16(b []byte) uint16 {
return binary.BigEndian.Uint16(b[:2])
}
func sendMessage(message string, connection net.Conn, milliseconds int) (int, error) {
var timeout time.Duration
msg_bytes := []byte(message)
msg_size := len(msg_bytes)
msg := append(uint16ToBytes(uint16(msg_size)), msg_bytes...)
if milliseconds > 0 {
timeout = time.Millisecond * time.Duration(milliseconds)
connection.SetWriteDeadline(time.Now().Add(timeout))
defer connection.SetWriteDeadline(time.Time{})
}
size, err := connection.Write(msg)
if err != nil {
if !strings.Contains(err.Error(), "i/o timeout") {
fmt.Println(err)
}
}
debug("Sent %v bytes to %v\n", size, connection.RemoteAddr())
return size, err
}
func receiveMessage(connection net.Conn, milliseconds int) (string, int, error) {
var msg_size [2]byte
var msg []byte
var timeout time.Duration
if milliseconds > 0 {
timeout = time.Millisecond * time.Duration(milliseconds)
connection.SetReadDeadline(time.Now().Add(timeout))
defer connection.SetReadDeadline(time.Time{})
}
size, err := connection.Read(msg_size[:])
if err != nil {
if !strings.Contains(err.Error(), "i/o timeout") {
fmt.Println(err)
}
}
if err != nil || size == 0 {
return "", size, err
}
size = int(bytesToUint16(msg_size[:]))
msg = make([]byte, size)
size, err = connection.Read(msg)
if err != nil {
if !strings.Contains(err.Error(), "i/o timeout") {
fmt.Println(err)
}
}
if err != nil || size == 0 {
return "", size, err
}
debug("Received message (%v bytes) from %v\n", size+2, connection.RemoteAddr())
return string(msg), size, err
}
func server(numberOfClients int, hostInterface string) {
shouldDebug = true
users := ActiveClients{}
listener, err := net.Listen("tcp", hostInterface)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for i := 0; i < numberOfClients; i++ {
wg.Add(1)
go listen(listener, &users)
}
wg.Wait()
}
func listen(listener net.Listener, users *ActiveClients) {
defer wg.Done()
var username string
var thisClient Client
var lg sync.WaitGroup
signalShouldEnd := make(chan bool, 1)
loop:
connection, err := listener.Accept()
if err != nil {
debug(err.Error() + "\n")
return
}
debug("Received connection from %v\n", connection.RemoteAddr())
hasSetUsername := false
lg.Add(2)
go func() {
defer lg.Done()
for {
// wait up to 100ms for the client to send a message
message, size, err := receiveMessage(connection, 100)
if err != nil {
if strings.Contains(err.Error(), "i/o timeout") {
// timed out, so go back to start of loop
// fmt.Println("i/o timeout")
continue
}
debug(err.Error())
select {
case signalShouldEnd <- true:
if users.remove(thisClient) {
users.forwardMessage("Goodbye "+thisClient.username, "server")
}
default:
}
return
}
if size > 0 {
if !hasSetUsername {
username = message
if username == "server" {
sendMessage("server: error: username is invalid; please send new username", connection, 0)
break
}
thisClient = Client{
socket: connection,
username: username,
}
ok := users.add(&thisClient)
if !ok {
sendMessage("server: error: username is invalid; please send new username", connection, 0)
break
} else {
users.forwardMessage("Welcome "+username, "server")
}
hasSetUsername = true
} else {
users.forwardMessage(message, username)
}
}
select {
case <-signalShouldEnd:
return
default:
}
}
}()
go func() {
defer lg.Done()
for {
select {
case incoming := <-thisClient.inbox:
debug("Message found in inbox; sending to %v\n", thisClient.username)
_, err := sendMessage(incoming, thisClient.socket, 0)
if err != nil {
if !strings.Contains(err.Error(), "i/o timeout") {
debug(err.Error() + "\n")
select {
case signalShouldEnd <- true:
if users.remove(thisClient) {
users.forwardMessage("Goodbye "+thisClient.username, "server")
}
default:
}
return
}
continue
}
case <-signalShouldEnd:
return
default:
time.Sleep(time.Millisecond * 10)
}
}
}()
lg.Wait()
debug("Recycling server socket for new client\n")
goto loop
}
func client(host string, username string) {
connection, err := net.Dial("tcp", host+":1337")
endSignals := []string{"bye", "quit", "exit"}
if err != nil {
debug("%v\n", err)
return
}
go func() {
for {
message, size, err := receiveMessage(connection, 100)
if err != nil {
if strings.Contains(err.Error(), "i/o timeout") {
continue
}
debug("%v\n", err)
os.Exit(0)
}
if size == 0 {
continue
}
// fmt.Printf("Received %v bytes: ", size)
fmt.Println(message)
}
}()
fmt.Println("Connected to server.")
_, err = sendMessage(username, connection, 0)
if err != nil {
fmt.Println(err)
return
}
for {
message := askInput("")
if contains(endSignals, message) {
return
}
_, err := sendMessage(message, connection, 0)
if err != nil {
fmt.Println(err)
return
}
}
}
func benchmark(host string, numberOfClients int, numberOfMessages int, baseMessage string) {
wg.Add(numberOfClients)
for i := 0; i < numberOfClients; i++ {
go func(i int) {
defer wg.Done()
var bg sync.WaitGroup
username := "benchmark" + strconv.Itoa(i)
connection, err := net.Dial("tcp", host+":1337")
if err != nil {
fmt.Println(err)
return
}
// current limitation: must wait for username to process
_, _ = sendMessage(username, connection, 100)
time.Sleep(time.Millisecond * 100)
bg.Add(numberOfMessages)
for d := 0; d < numberOfMessages; d++ {
go func(message string) {
defer bg.Done()
_, err = sendMessage(message, connection, 100)
if err != nil {
fmt.Println(err)
return
}
}(baseMessage + strconv.Itoa(d))
}
bg.Add(1)
go func() {
defer bg.Done()
for {
message, size, err := receiveMessage(connection, 2000)
if err != nil {
fmt.Println(err)
return
}
if size == 0 {
fmt.Println("aborting from empty receive")
return
}
fmt.Println(message)
}
}()
bg.Wait()
}(i)
}
wg.Wait()
}
func askInput(query string) string {
if query != "" {
fmt.Printf("%v ", query)
}
in := bufio.NewReader(os.Stdin)
data, _ := in.ReadString('\n')
data, _, _ = strings.Cut(data, "\n")
data, _, _ = strings.Cut(data, "\r")
return data
}
func contains[T comparable](list []T, query T) bool {
// contains function adapted from https://stackoverflow.com/a/10485970
for _, item := range list {
if item == query {
return true
}
}
return false
}
func usage(programName string) {
fmt.Printf("usage: %v server numerOfThreads [hostInterface]\n", programName)
fmt.Println("\thostInterface is of form hostname:port or IP:port")
fmt.Println("\tuse 0.0.0.0:port to listen on every available IP interface")
fmt.Println("\tdefault is localhost:1337")
fmt.Printf("usage: %v client hostIP username\n", programName)
fmt.Printf("usage: %v benchmark hostIP numberOfClients numberOfMessages baseMessage\n", programName)
fmt.Println("arguments must be in order shown")
}
func main() {
programName := os.Args[0]
nameParts := strings.Split(programName, "\\")
programName = nameParts[len(nameParts)-1]
if len(os.Args) < 2 {
usage(programName)
return
}
mode := os.Args[1]
switch mode {
case "server", "serve":
var numberOfClients int
if len(os.Args) < 3 {
usage(programName)
return
}
n, _ := strconv.ParseInt(os.Args[2], 10, 16)
numberOfClients = int(n)
var hostInterface string
if len(os.Args) > 3 {
hostInterface = os.Args[3]
} else {
hostInterface = "localhost:1337"
}
server(numberOfClients, hostInterface)
case "client", "connect":
var host string
var username string
if len(os.Args) < 4 {
usage(programName)
return
}
host = os.Args[2]
username = os.Args[3]
client(host, username)
case "benchmark":
var host string
var nClients string
var numberOfClients int
var nMsgs string
var numberOfMessages int
var message string
if len(os.Args) < 6 {
usage(programName)
return
}
host = os.Args[2]
nClients = os.Args[3]
nMsgs = os.Args[4]
message = os.Args[5]
n, _ := strconv.ParseInt(nClients, 10, 16)
numberOfClients = int(n)
n, _ = strconv.ParseInt(nMsgs, 10, 16)
numberOfMessages = int(n)
benchmark(host, numberOfClients, numberOfMessages, message)
default:
usage(programName)
}
}
// ISC License
// Copyleft (c) 2023 k98kurz
// Permission to use, copy, modify, and/or distribute this software
// for any purpose with or without fee is hereby granted, provided
// that the above copyleft notice and this permission notice appear in
// all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
// NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Comments
0 B
|👍
/👎
0 B
|👍
/👎