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.сокеты.