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

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

UnixСокеты domainмежпроцессного socketsвзаимодействия offerпредлагают efficient,эффективное,безопасное secure,двухсторонее bidirectionalподключение communicationмежду betweenпроцессами processes on aна Unix/Linux machine.машинах. WhileВ channelsто areвремя greatкак forканалы communicatingотлично acrossиспользуются theдля goroutinesподключения ofмежду anгорутинами application,приложения, andи HTTP isвездесущь, ubiquitous,при whenподключении communicating betweenмежду Go applicationsприложениями(межпроцессороное (interprocessвзаимодействие) communication)запущенные runningна onтой theже sameмашине machineканалы channelsне areпомогают, noа help,подключление andк Unixсокету socketмежпроцессного communicationвзаимодействия isгораздо simpler,проще, moreэффективнее, efficient,и andболее moreбезопасно secure thanчем HTTP orили otherдругие internetинтернет protocolпротоколы communication.подключений.

Go’sВсё что вам нужно это только пакет net isдля theзапуска only package you need to start communicating:подключения:

  • Пакет Packagenet netпредоставляет providesперносимый aинтерфейс portableдля interface for networkсети I/O, including…Unix domain sockets. Although the package provides access to low-level networking primitives, most clients will need only the basic interface provided by the Dial, Listen, and Accept functions and the associated Conn and Listener interfaces.

Unfortunately those functions and interfaces are sparsely documented (with regard toвключая Unix domainсокеты. sockets

  • Так inже particular)пакет предоставляет доступ к низкоуровневым примитивам сети, andбольшинству there’sклиентов noтребуедсят officialтолько базовый интерфейс п редоставляемый Dial, Listen и Accept функциями и связанные Conn и Listener интферфейсы.
  • К сожалению эти фукнции и интерфейсы редко документированны(в частности сокеты межпроцессного взаимодействия), так же нет официального Go Blogблога postо onтом, howкак toс getчего startedначать withпри Unixработе socketс communication.сокетами. ThereПохоже seemsчто toнедостаток beхорошего aввдения dearth of good introductions onна StackOverflow andи Go blogs.блогах. MostБольшинство articlesстатей onо Unixсокетах sockets demonstrateпоказывают C implementations;реализацию; hereгде I’llя focusсосредоточусь onна howтом toкак getначать startedРаботать inс Go.

    First,Первое, aсокерт Unixпредставлен socketспециальным isфалом. representedВаш asсервер aслушает specialчерез file.файл, Yourпринимае serverподключения listensи onчитает theданные file,через acceptsэто connections,подключение. andВаш readsклиент dataиспользует fromфайл, thoseчтобы connections.создать Yourподключение clientи dialsзатем theпишет fileданные toв createэто aсамое connection and then writes data to the connection.подключение.

    YouВы mightвозможно thinkдумаете, youчто needвам toнужно createсоздать thisэтот specialспециальный fileфайл usingиспользуя theпакет os. package.В Thereнем you’llвы findможете aнайти постоянную FileMode, constant,ModeSocket, ModeSocket,которая thatможет mayвзывать callк outвам. toНо you.это Butне thisпоможет: isНезадокументированный noфункцонионал help:функции AnListen undocumentedзаключается featureв ofтом, the Listen function is that it will – it must – create the file for you, and it will exit with a potentially misleading errorчто (это должно) создать файл для вас, и он будет существовать с ошибкой вводящей в заблуждение: “bind: address already in use”), ifесли theфайл fileуже alreadyсуществует. exists.Отсюда Thereforeдла yourначала, first,нелогичный counterintuitiveшаг stepв inсоздании creatingсервера aэто serverудаление isфайла toкоторый deleteвы theсобираетесь fileслушать, youи wishтолько toпомто listenего to,можно and then start listening to it:слушать:

        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()
    

    ForДля aсервера serverкоторый toуправляет operateпостоянно indefinitelyи andобрабатывает handleмножество multipleпохожих simultaneousподключений, connections,вы you’llзахотите wantиспользовать anбесконечный infiniteцикл, loopв inкотором whichвы youпримете acceptподключение aв connectionlistener onи theначнете listener,новую andгорутину startдля aего new goroutine to handle it:обработки:

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

    YourВаш handlerобработчик shouldдолжен createсоздать aбуффер bufferлюбого ofжелаемого whateverразмера, sizeи youпрочитать like,в andнего readбесконечный intoцыкл theкоторый bufferостановится inкогда anRead infiniteвыдаст loopошибку. thatRead willедва stopдокументирован whenв Readnet errors.пакете. ReadСтранным isобразом, barelyвы documentedдолжны inзнать theчто netдальше package.нужно Strangely,смотреть youв mustдокументацию knowio toпакета lookдля toReader the io package documentation for the Reader interface:интерфейса:

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

    …to... learnчтобы importantузнать informationважную aboutинформацию howпро поведение Read behavesв inдругих otherпакетах packagesстандартной of the standard library:библиотеки:

    • Reader is- theинтерфейс interfaceкоторый thatоборачивает wrapsбазовый theметод basicRead
    • Read method.читает Read reads up to len(p) bytesбайтов intoв p.p. ItОн returnsвозвращяет theколичество numberпрочитанных of bytes read байтов(0 <= n <= len(p)) andв anyтом errorчисле encountered.любую Evenошибку. ifДаже если Read returnsвернет n < len(p), itон mayможет useиспользовать allвсё ofиз p asкак scratchиспорченное spaceпространство duringв theвремя call.вызова. IfЕсли someкакие-то dataданные isдоступны, availableно butдлинной notне равной len(p) bytes,байт, Read conventionallyвернеть returnsто whatчто isдоступно, availableвместо insteadтого, ofчтобы waitingчего-то forждать.
    • more.
    • Когда WhenRead Readвстречает encountersошибку anили errorEOF orусловние end-of-fileпосле conditionуспешного after successfully readingчтения n > 0 bytes,байтов, itон returnsвозвращает theколичество numberпрочитанных ofбайтов. bytesМожет read.вернуть Itnon-nil mayошибку returnиз theтого (non-nil)же errorвызова fromили theвернуть sameошибку call or return the error (and n == 0)0 fromиз aподпоследовательности subsequentвызовов. call.Экземпляр Anданного instanceконкретного ofслучая thisэто generalто caseчто isReader thatвозвращает a Reader returning a non-zero numberчисло ofбайтов bytesв atконце theпотока endввода, ofможет theтак inputже streamвернуть mayили return either err == EOF orили err == nil.nil. TheСледующий nextRead должен вернуть 0, EOF.
    • Функции должны всегда обрабатывать n>0 байтов возвращенные перед учитываением ошибки err. Выполнение этого условия обрабатывает I/O ошибки, которые случаются после чтения неких байтов и так же обоим позволено EOF поведение.
    • Реализация Read shouldобескураживает returnот 0,возвращения EOF.0 Callersбайтов shouldс alwaysnil processошибкой, theза nисключением >когда 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors. Implementations of Read are discouraged from returning a zero byte count with a nil error, except when len(p) == 0.0. CallersФункция shouldдолжна treatотнестись aк returnвозврату of0 0или andnil, nilкак asк indicatingиндикатору thatчто nothingничег happened;не inслучилось, particularв itчастности doesэто notне indicateговорит EOF.об ImplementationsEOF(конце mustфайла)
    • not
    • Реализация retainне p:должна удерживать 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.