3
\$\begingroup\$

So the task was to first create a custom TCP protocol (namely ebl0) with certain parameters in its header and a number of methods to use with it (e.g. Login, Create, Join etc.). I'm not listing it here because it was relatively easy. The next step was building a server using this protocol that complies to the following:

  • Has a 'heartbeat' functionality (detects a dead client: if a client is dead for 8 seconds, ping it, if no response is received within 8 more seconds, disconnect).

  • Can handle multiple clients/connections.

  • Works within the scope of ebl0 protocol and supports the following commands from ebl0 protocol : ping, pong, login, login acknowledge, list (list all current rooms for the user), create, create acknowledge, delete, delete acknowledge, join, join acknowledge, leave, leave acknowledge, message. Those are pretty self-explanatory.

  • Smaller features I've tried to include in the comments as I was working on this.

At this point I'd like to know if:

  • Something can be done better/faster.

  • I'm using mutexes correctly.

  • There's a way to hide implementation details (making the room and user maps) from the Server constructor.

  • There's generally a more idiomatic architecture (structs/methods).

Thank you for your time!

package main

import (
    "errors"
    "fmt"
    "io"
    "net"
    "strings"
    "sync"
    "sync/atomic"
    "time"

    "github.com/hex3r/go-learning/ebl0"
)

// Server contains server parameters
type Server struct {
    port  string
    rooms map[string]*Room
    users map[string]*User
}

// Conn contains connection parameters
type Conn struct {
    encoder   *ebl0.Encoder
    decoder   *ebl0.Decoder
    timeout   time.Duration
    isClosed  chan bool
    timeValue atomic.Value
}

// Room contains room parameters
type Room struct {
    users []string
}

// User contains user parameters
type User struct {
    rooms   []string
    address net.Conn
}

func getSliceElementIndex(slice []string, element string) int {

    for i, v := range slice {
        if v == element {
            return i
        }
    }

    return -1

}

func (conn *Conn) getLastReadTime() time.Time {
    return conn.timeValue.Load().(time.Time)
}

func (conn *Conn) setLastReadTime() {
    conn.timeValue.Store(time.Now())
}

func (conn *Conn) timeoutTimer() {

    i := 1

    for {

        // store last read time
        lastReadTime := conn.getLastReadTime()

        select {

        // handleConnection has quit, return
        case <-conn.isClosed:
            return

        // timeout reached
        case <-time.After((lastReadTime.Add(time.Duration(i) * conn.timeout)).Sub(time.Now())):
            // if last read time remains the same (client didn't send anything),
            // send PING to the client
            if lastReadTime == conn.getLastReadTime() && i == 1 {
                if err := conn.encoder.Ping(); err != nil {
                    fmt.Println(err)
                    return
                }
                i++
                // if this isn't the first attempt, reset the counter
            } else {
                i = 1
            }

        }

    }

}

func (server *Server) handleConnection(c net.Conn) {

    // initialize variables
    var err error
    var msg ebl0.Message
    var name string           // username
    isLoggedIn := false       // user login status
    var mutex = &sync.Mutex{} // syncing access to maps

    conn := Conn{
        encoder:  ebl0.NewEncoder(c),
        decoder:  ebl0.NewDecoder(c),
        timeout:  time.Duration(8) * time.Second,
        isClosed: make(chan bool),
    }

    defer func() {

        // close sync channel
        conn.isClosed <- true
        close(conn.isClosed)

        // close connection
        c.Close()

        mutex.Lock()

        // remove user from userlist on server
        delete(server.users, name)

        // iterate over all server rooms
        for roomName := range server.rooms {
            // check if disconnecting user is present in any of them
            if i := getSliceElementIndex(server.rooms[roomName].users, name); i != -1 {
                // remove user from the userlist of room
                server.rooms[roomName].users = append(server.rooms[roomName].users[:i], server.rooms[roomName].users[i+1:]...)
                fmt.Println(name, "left", roomName)
            }
        }

        mutex.Unlock()

        // custom error message for normal disconnect (EOF)
        if err == io.EOF {
            err = errors.New("quit")
        }

        fmt.Println(name, "disconnected. (", err, ")")

    }()

    // start timeout counter goroutine
    conn.setLastReadTime()
    go conn.timeoutTimer()

    for {

        // set read deadline to twice the timeout as per protocol rules
        c.SetReadDeadline(time.Now().Add(conn.timeout * 2))

        // decode the message
        msg, err = conn.decoder.Decode()
        if err != nil {
            return
        }

        // set most recent read time to current time and reset timeout
        conn.setLastReadTime()
        c.SetReadDeadline(time.Time{})

        // get command number of the message
        cmdNum := msg.Command()

        // if user attempts actions without logging in, disconnect
        if !isLoggedIn && cmdNum != 3 {
            name = "An anonymous user"
            return
        }

        switch cmdNum {

        case 1: // client sends PING, reply with PONG

            fmt.Println(name, "pinged the server.")

            if err = conn.encoder.Pong(); err != nil {
                return
            }

        case 3: // client sends LOGIN, acknowledge

            if !isLoggedIn {

                // get name
                name = string(msg.Payload())

                mutex.Lock()

                // if such user doesn't exist, acknowledge it
                if _, ok := server.users[name]; !ok {

                    server.users[name] = &User{
                        rooms:   make([]string, 0),
                        address: c,
                    }

                    isLoggedIn = true

                    fmt.Println(name, "logged in.")

                    if err = conn.encoder.LoginAck(1, ""); err != nil {
                        return
                    }

                    // if user exists
                } else {

                    if err = conn.encoder.LoginAck(0, "User under such name has already logged in."); err != nil {
                        return
                    }

                }

                mutex.Unlock()

                // if user has previously logged in
            } else {

                if err = conn.encoder.LoginAck(0, "You have already logged in."); err != nil {
                    return
                }

            }

        case 5: // client sends LIST, provide it with list of rooms

            mutex.Lock()

            // form list of rooms
            roomNames := make([]string, len(server.rooms))
            i := 0
            for roomName := range server.rooms {
                roomNames[i] = roomName
                i++
            }

            mutex.Unlock()

            // send list of rooms
            if err = conn.encoder.ListRooms(roomNames); err != nil {
                return
            }

        case 6: // client sends CREATE, try to create room and acknowledge

            // get room name
            roomName := string(msg.Payload())

            mutex.Lock()

            // if such room doesn't exist, create it
            if _, ok := server.rooms[roomName]; !ok {
                server.rooms[roomName] = &Room{make([]string, 0)}
                if err = conn.encoder.CreateAck(1, ""); err != nil {
                    return
                }
                fmt.Println(name, "created", roomName)
                // if it exists, send back error
            } else {
                if err = conn.encoder.CreateAck(0, "Room with such name already exists."); err != nil {
                    return
                }
            }

            mutex.Unlock()

        case 8: // client sends DELETE, try to delete room and acknowledge

            // get room name
            roomName := string(msg.Payload())

            mutex.Lock()

            // if such room exists
            if _, ok := server.rooms[roomName]; ok {

                // if player is not present in it
                if getSliceElementIndex(server.users[name].rooms, roomName) == -1 {

                    // iterate over all server users
                    for user := range server.users {
                        // check if room is present in any of their roomlists
                        if i := getSliceElementIndex(server.users[user].rooms, roomName); i != -1 {
                            // there are users in room, send error
                            if err = conn.encoder.DeleteAck(0, "There are users present in the room. They must leave to delete the room."); err != nil {
                                return
                            }
                        }
                    }

                    // delete room
                    delete(server.rooms, roomName)
                    // acknowledge
                    if err = conn.encoder.DeleteAck(1, ""); err != nil {
                        return
                    }
                    fmt.Println(name, "deleted", roomName)

                    // if player is in the room, send error
                } else {
                    if err = conn.encoder.DeleteAck(0, "You must leave the room first."); err != nil {
                        return
                    }
                }

                // if room doesn't exist, send error
            } else {
                if err = conn.encoder.DeleteAck(0, "Room with such name doesn't exist."); err != nil {
                    return
                }
            }

            mutex.Unlock()

        case 10: // client sends JOIN, try to join and acknowledge

            // get room name
            roomName := string(msg.Payload())

            mutex.Lock()

            // if such room exists
            if _, ok := server.rooms[roomName]; ok {

                // and if user is not in it already, join room
                if getSliceElementIndex(server.users[name].rooms, roomName) == -1 {

                    // add user to userlist of joined room
                    server.rooms[roomName].users = append(server.rooms[roomName].users, name)
                    // add joined room to roomlist of the user
                    server.users[name].rooms = append(server.users[name].rooms, roomName)
                    // acknowledge joining room
                    if err = conn.encoder.JoinAck(1, ""); err != nil {
                        return
                    }
                    fmt.Println(name, "joined", roomName)

                    // if user is already in the room, send error
                } else {

                    if err = conn.encoder.JoinAck(0, "You are already in this room."); err != nil {
                        return
                    }

                }

                // if room doesn't exist, send error
            } else {

                if err = conn.encoder.JoinAck(0, "Room with such name doesn't exist."); err != nil {
                    return
                }

            }

            mutex.Unlock()

        case 12: // client sends LEAVE, try to leave and acknowledge

            // get room name
            roomName := string(msg.Payload())

            mutex.Lock()

            // if such room exists
            if _, ok := server.rooms[roomName]; ok {

                // and if user is present in it, leave room
                if getSliceElementIndex(server.users[name].rooms, roomName) != -1 {

                    // find user in userlist of room
                    i := getSliceElementIndex(server.rooms[roomName].users, name)
                    // remove user from the userlist of room
                    server.rooms[roomName].users = append(server.rooms[roomName].users[:i], server.rooms[roomName].users[i+1:]...)
                    // find room in the roomlist of user
                    j := getSliceElementIndex(server.users[name].rooms, roomName)
                    // remove room from the roomlist of user
                    server.users[name].rooms = append(server.users[name].rooms[:j], server.users[name].rooms[j+1:]...)
                    // acknowledge leaving
                    if err = conn.encoder.LeaveAck(1, ""); err != nil {
                        return
                    }
                    fmt.Println(name, "left", roomName)

                    // if user is not in the room, send error
                } else {
                    if err = conn.encoder.LeaveAck(0, "You are not present in that room you want to leave."); err != nil {
                        return
                    }
                }
                // if room doesn't exist, send error
            } else {
                if err = conn.encoder.LeaveAck(0, "Room with such name doesn't exist."); err != nil {
                    return
                }
            }

            mutex.Unlock()

        case 14: // client sends MESSAGE, receive and send to everyone in the room

            // get room name and message
            payload := string(msg.Payload())
            payloadSlice := strings.Split(payload, "\n")
            roomName := payloadSlice[0]
            message := payloadSlice[1]
            fmt.Println("#", roomName, "@", name+":", message)

            mutex.Lock()

            // if room exists
            if _, ok := server.rooms[roomName]; ok {

                // if user is present in it
                if getSliceElementIndex(server.users[name].rooms, roomName) != -1 {

                    // iterate over all server users
                    for i := range server.users {

                        // if user is present in the room
                        if getSliceElementIndex(server.users[i].rooms, roomName) != -1 {

                            // send message to user
                            if err = ebl0.NewEncoder(server.users[i].address).RoomMessage(roomName, name, message); err != nil {
                                return
                            }

                        }

                    }

                }

            }

            mutex.Unlock()

        }

    }

}

func (server *Server) serve() {

    // listen on a port
    ln, err := net.Listen("tcp", server.port)
    if err != nil {
        fmt.Println(err)
        return
    }

    for {
        // accept connection
        c, err := ln.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }

        // handle connection
        go server.handleConnection(c)
    }
}

func main() {

    server := Server{
        port:  ":9999",
        rooms: make(map[string]*Room),
        users: make(map[string]*User),
    }

    server.serve()

}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.