Если вы видите что-то необычное, просто сообщите мне. 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передаете toRead в форме среза байтов, который должен иметь длинну больше чем нуль, для того, чтобы в него что-то прочитать. Это совмещается с передачей среза больше чем указателя на срез, потому что любое увеличение длинны среза внутри Read inне theбудет formвидно ofв aвызове byteконтекста sliceбез mustиспользования haveуказателя. aОтносительно lengthобщий greaterбаг thanв zeroиспользовании forRead 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-basednet 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.Read, 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ошибку toio.EOF, 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библиотеке currencyGo, 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между communicatingGO betweenпроцессами Go processes usingиспользующими Unix domain sockets.сокеты.