Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Go: Ввдение в сокеты межпроцессного взаимодействия

Сокеты межпроцессного взаимодействия предлагают эффективное,безопасное двухсторонее подключение между процессами на Unix/Linux машинах. В то время как каналы отлично используются для подключения между горутинами приложения, и HTTP вездесущь, при подключении между Go приложениями(межпроцессороное взаимодействие) запущенные на той же машине каналы не помогают, а подключление к сокету межпроцессного взаимодействия гораздо проще, эффективнее, и более безопасно чем HTTP или другие интернет протоколы подключений.

Всё что вам нужно это только пакет net для запуска подключения:

  • Пакет net предоставляет перносимый интерфейс для сети I/O, включая Unix сокеты.
  • Так же пакет предоставляет доступ к низкоуровневым примитивам сети, большинству клиентов требуедсят только базовый интерфейс п редоставляемый Dial, Listen и Accept функциями и связанные Conn и Listener интферфейсы.

К сожалению эти фукнции и интерфейсы редко документированны(в частности сокеты межпроцессного взаимодействия), так же нет официального Go блога о том, как с чего начать при работе с сокетами. Похоже что недостаток хорошего ввдения на StackOverflow и Go блогах. Большинство статей о сокетах показывают C реализацию; где я сосредоточусь на том как начать Работать с Go.

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

Вы возможно думаете, что вам нужно создать этот специальный файл используя пакет os. В нем вы можете найти постоянную FileMode, ModeSocket, которая может взывать к вам. Но это не поможет: Незадокументированный функцонионал функции Listen заключается в том, что (это должно) создать файл для вас, и он будет существовать с ошибкой вводящей в заблуждение: “bind: address already in use”, если файл уже существует. Отсюда дла начала, нелогичный шаг в создании сервера это удаление файла который вы собираетесь слушать, и только помто его можно слушать:

    os.Remove(cfg.SocketFile)
    listener, err := net.Listen("unix", cfg.SocketFile)
    if err != nil {
        log.Fatalf("Unable to listen on socket file %s: %s", cfg.SocketFile, err)
    }
    defer listener.Close()

Для сервера который управляет постоянно и обрабатывает множество похожих подключений, вы захотите использовать бесконечный цикл, в котором вы примете подключение в listener и начнете новую горутину для его обработки:

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatalf("Error on accept: %s", err)
        }
        go message.HandleConn(conn)
    }

Ваш обработчик должен создать буффер любого желаемого размера, и прочитать в него бесконечный цыкл который остановится когда Read выдаст ошибку. Read едва документирован в net пакете. Странным образом, вы должны знать что дальше нужно смотреть в документацию io пакета для Reader интерфейса:

    type Reader interface {
            Read(p []byte) (n int, err error)
    }

... чтобы узнать важную информацию про поведение Read в других пакетах стандартной библиотеки:

  • Reader - интерфейс который оборачивает базовый метод Read
  • Read читает len(p) байтов в p. Он возвращяет количество прочитанных байтов(0<= n <= len(p)) в том числе любую ошибку. Даже если Read вернет n < len(p), он может использовать всё из p как испорченное пространство в время вызова. Если какие-то данные доступны, но длинной не равной len(p) байт, Read вернеть то что доступно, вместо того, чтобы чего-то ждать.
  • Когда Read встречает ошибку или EOF условние после успешного чтения n > 0 байтов, он возвращает количество прочитанных байтов. Может вернуть non-nil ошибку из того же вызова или вернуть ошибку n == 0 из подпоследовательности вызовов. Экземпляр данного конкретного случая это то что Reader возвращает non-zero число байтов в конце потока ввода, может так же вернуть или err == EOF или err == nil. Следующий Read должен вернуть 0, EOF.
  • Функции должны всегда обрабатывать n>0 байтов возвращенные перед учитываением ошибки err. Выполнение этого условия обрабатывает I/O ошибки, которые случаются после чтения неких байтов и так же обоим позволено EOF поведение.
  • Реализация Read обескураживает от возвращения 0 байтов с nil ошибкой, за исключением когда len(p) == 0. Функция должна отнестись к возврату 0 или nil, как к индикатору что ничег не случилось, в частности это не говорит об EOF(конце файла)
  • Реализация не должна удерживать p:

As cryptically stated above, the buffer you pass to Read in the form of a byte slice must have a length greater than zero for anything to be read into it. This is consistent with passing a slice rather than a pointer to a slice, because any increase of the length of the slice inside Read would not be visible in the calling context without use a of a pointer. A relatively common bug in using Read is to pass it a buffer of length zero.

Another common bug is to ignore the admonishment above to process the bytes returned prior to handling errors. This contrasts with common error handling advice in the majority of programming contexts in Go, and is important to correct implementation of net-based communication in general.

Here is an example handler that addresses those concerns. It reads into a buffer with length greater than zero inside an infinite loop, breaking only on error. After each Read, the first count bytes of the buffer are consumed prior to error handling:

func HandleConn(c net.Conn) {
    received := make([]byte, 0)
    for {
        buf := make([]byte, 512)
        count, err := c.Read(buf)
        received = append(received, buf[:count]...)
        if err != nil {
            ProcessMessage(received)
            if err != io.EOF {
                log.Errorf("Error on read: %s", err)
            }
            break
        }        
    }
}

By this method, all data sent on a connection is understood by the server as being one message. The client should close the connection to signal the end of the message. Reading the closed connection will return error io.EOF, which is routine and does not need to be handled as an error. It simply signifies that the message is ended, often a cue to begin processing the message now that it is complete.

What goes in ProcessMessage is, of course, dependent on your application. Since a string is basically a read-only slice of bytes, it is little effort to communicate text data in this way. But byte slices are also common currency in the Go standard library, and any data can be encoded as a slice of bytes.

All that’s left now is to make a client. The client is simply a function that dials up the socket file to create a connection, defers closing the connection, and writes the message bytes to the connection. One does not have to worry about what size the message is, it can be arbitrarily large, the code does not change. Here is an example with logrus-style error logging:

type Sender struct {
    Logger     *log.Logger
    SocketFile string
}

func (s *Sender) SendMessage(message []byte) {
    c, err := net.Dial("unix", s.SocketFile)
    if err != nil {
        s.Logger.Errorf("Failed to dial: %s", err)
    }
    defer c.Close()
    count, err := c.Write(message)
    if err != nil {
        s.Logger.Errorf("Write error: %s", err)
    }
    s.Logger.Infof("Wrote %d bytes", count)
}

A next step could be to add responses to acknowledge receipts. For many applications, the above is all that is needed to start communicating between Go processes using Unix domain sockets.