2
\$\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\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.