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
:
Как при загадочном старте, буфер который вы передаете Read
в форме среза байтов, который должен иметь длинну больше чем нуль, для того, чтобы в него что-то прочитать. Это совмещается с передачей среза больше чем указателя на срез, потому что любое увеличение длинны среза внутри Read
не будет видно в вызове контекста без использования указателя. Относительно общий баг в использовании Read
это буфер с нулевой длинной.
Другой распространненый баг это игнорирвание предостережения выше, обрабатывать возвращенный байты до обработки ошибок. Это контрастирует с советом общей обработки ошибок, в большинстве программных контекстов на Go, и очень важно исправить реализацию основанных на net
поключений вообще.
Ниже пример обработчика который решает эти проблемы. Он читает в буфер с длинной больше чем ноль внутри бесконечного цикла, и прервыается только при ошибке. После каждого Read
, первый счетчик байтов буфера поглащается перед обработкой ошибок:
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
}
}
}
Этим методом, все данные отправленные подключением воспринимаются сервером как одно сообщение. Клиент должен закрыть подключение сигналом о конце сообщения. Чтение закрытого подключения вренуть ошибку io.EOF
, которая не должна быть обработа как обыная ошибка. Это просто сигнал о том, что сообщение закончилось, часто подсказка к началу обработки сообщения так как оно закончено.
Что происходит в ProcessMessage
, конечно, зависит от вашего приложения. Так как строка это по-факту срез байтов, только для чтения, это маленькая попытка для связи текстовых данных таким образом. Но байтовый срез так же распространненая валюта в стандартной библиотеке Go, и любые данные могут быть зашифрованы как срез байтов.
Всё что нам осталось - это сделать клиента. Клиент - это просто функция которая поднимает сокет файл для того чтобы создать подключение, откладывает закрытие подключения, и пишет байтовое сообщение в подключение. Не нужно беспокоится о размере сообщения, оно может быть очень большое, но код не изменится. Ниже пример с логированием ошибок:
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)
}
Следующим шагом может быть добавление ответа с подтверждением полученного. Для множества приложений, выше приведенная инструкция это всё что нужно для начала связи между GO процессами использующими Unix сокеты.