Как получить доступ к переменной через все подключенные клиенты TCP в Go? - PullRequest
0 голосов
/ 29 мая 2019

Я устанавливаю tcp-сервер в проекте для домашних животных, который я пишу на ходу.Я хочу иметь возможность поддерживать часть всех подключенных клиентов, а затем изменять ее всякий раз, когда новый клиент подключается или отключается от моего сервера.

Мое главное умственное препятствие сейчас заключается в том, должен ли я объявлять уровень пакета.или просто передать фрагмент в мой обработчик.

Моей первой мыслью было объявить мой ClientList фрагмент (я знаю, что фрагмент не может быть моим лучшим вариантом здесь, но я решилоставьте все как есть) как переменную уровня пакета.Хотя я думаю, что это сработает, я видел несколько постов, не одобряющих их использование.

Моя другая мысль состояла в том, чтобы объявить ClientList в качестве фрагмента в моей основной функции, а затем я передал ClientList к моей HandleClient функции, поэтому, когда клиент подключается / отключается, я могу вызвать AddClient или RemoveClient, передать этот фрагмент и добавить / удалить соответствующего клиента.

Эта реализация рассматривается ниже.Определенно есть другие проблемы с кодом, но я застрял, пытаясь обернуть голову вокруг чего-то, что кажется очень простым.

type Client struct {
    Name string  
    Conn net.Conn
}

type ClientList []*Client

// Identify is used to set the name of the client
func (cl *Client) Identify() error {
// code here to set the client's name in the based on input from client
}

// This is not a threadsafe way to do this - need to use mutex/channels
func (cList *ClientList) AddClient(cl *Client) {
    *cList = append(*cList, cl)
}
func (cl *Client) HandleClient(cList *ClientList) {
    defer cl.Conn.Close()
    cList.AddClient(cl)
    err := cl.Identify()
    if err != nil {
        log.Println(err)
        return
    }
    for {
        err := cl.Conn.SetDeadline(time.Now().Add(20 * time.Second))
        if err != nil {
            log.Println(err)
            return
        }
        cl.Conn.Write([]byte("What command would you like to perform?\n"))
        netData, err := bufio.NewReader(cl.Conn).ReadString('\n')
        if err != nil {
            log.Println(err)
            return
        }
        cmd := strings.TrimSpace(string(netData))
        if cmd == "Ping" {
            cl.Ping() //sends a pong msg back to client
        } else {
            cl.Conn.Write([]byte("Unsupported command at this time\n"))
        }

    }
}
func main() {
    arguments := os.Args

    PORT := ":" + arguments[1]
    l, err := net.Listen("tcp4", PORT)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer l.Close()
    fmt.Println("Listening...")

    // Create a new slice to store pointers to clients
    var cList ClientList

    for {
        c, err := l.Accept()
        if err != nil {
            log.Println(err)
            return
        }
        // Create client cl1
        cl1 := Client{Conn: c}

        // Go and handle the client
        go cl1.HandleClient(&cList)
    }
}

Из моего первоначального тестирования,это похоже на работу.Я могу распечатать свой список клиентов и вижу, что добавляются новые клиенты, и их имя добавляется после вызова Identify().

Когда я запускаю его с флагом -race, я получаю предупреждения о гонке данных, поэтому я знаю, что мне понадобится поточный способ обработки добавления клиентов.То же самое касается удаления клиентов, когда я добавляю это в.

Существуют ли какие-либо другие проблемы, которые я мог бы упустить, передав свой ClientList в HandleClient, или какие-либо преимущества, которые я получил бы от объявления ClientList в качестве переменной уровня пакетавместо этого?

1 Ответ

3 голосов
/ 29 мая 2019

Несколько проблем с этим подходом.

Во-первых, ваш код содержит гонку данных: каждое TCP-соединение обслуживается отдельной программой, и все они пытаются изменить срез одновременно.

Вы можете попытаться создать свой код с помощью go build -race (или go install -race - независимо от того, что вы используете) и увидеть его сбой при включенных проверках во время выполнения.

Это легко исправить.Самый простой подход - добавить переменную мьютекса в тип ClientList:

type ClientList struct {
  mu      sync.Mutex
  clients []*Client
}

… и заставить методы типа удерживать мьютекс, пока они мутируют поле clients, например так:

func (cList *ClientList) AddClient(cl *Client) {
  cList.mu.Lock()
  defer cList.mu.Unlock()

  cList.clients = append(cList.clients, o)
}

(Если вы когда-нибудь столкнетесь с типичным шаблоном использования вашего ClientList типа, это часто вызывать методы, которые только читают содержащийся список, вы можете начать использовать sync.RWLockвместо этого введите, что позволяет использовать несколько одновременных читателей.)

Во-вторых, я бы разделил часть, которая «идентифицирует» клиента, из функции-обработчика.На данный момент в обработчике, если идентификация завершается неудачно, обработчик завершается, но клиент не исключается из списка.

Я бы сказал, что было бы лучше определить его заранее и запускать обработчик только после того, как клиентсчитается, что все в порядке.

Также, возможно, стоит добавить отложенный вызов к чему-то вроде RemoveClient в верхней части тела обработчика, чтобы клиент был должным образом удален из списка, когда обработчик завершил работу с ним.

IOW, я бы ожидал увидеть что-то вроде этого:

func (cl *Client) HandleClient(cList *ClientList) {
    defer cl.Conn.Close()

    err := cl.Identify()
    if err != nil {
        log.Println(err)
        return
    }

    cList.AddClient(cl)
    defer cList.RemoveClient(cl)

    // ... the rest of the code
}
...