Desenvolvimento

5 abr, 2019

Unix Domain Socket com Golang

Publicidade

Unix domain socket

Unix Domain Sockets ou IPC socket é uma forma muito prática e segura de trocar informações entre processos. Essa forma de IPC usa um arquivo como endereço/name space no lugar de um IP e uma porta, como seria em uma comunicação via rede.

Uma coisa importante para ter em mente é que como vamos usar um arquivo, o servidor é responsável por ele, e se não existir ele será criado automaticamente, mas se não existir você vai receber um erro com algo como “bind: address already in use”, que significa que o arquivo já existe e o servidor não tem como reaproveitar um arquivo que já existe.

O correto é fazer shutdown elegantemente, fechar e apagar o arquivo antes de derrubar o servidor. E dependendo do sistema, pode ser interessante verificar se o arquivo já existe e apagar antes de subir o servidor.

Apesar da facilidade, como usamos um arquivo como endereço, não dá pra usar pra trocar informação entre maquinas diferentes, e quem fica responsável por manter essa comunicação é o kernel.

O arquivo é apenas um name space, e nenhum byte será escrito no arquivo – ele vai ocupar zero espaço de disco, já que toda a comunicação acontece na RAM e é gerenciada pelo kernel.

Outra coisa muito importante é que: usar Unix domain socket é um recurso padrão de qualquer ambiente POSIX, mas não está presente por padrão no Windows.

Exemplos

Servidor

A conexão de cliente e servidor é muito parecida com a que estamos acostumados quando conectamos via TCP/IP. Basicamente ficamos ouvindo o namespace indicado pelo arquivo e esperamos por conexões como ficaríamos ouvindo uma porta TCP.

Daí, quando essa conexão chega, usamos a função Accept do pacote net e passamos para uma goroutine com o handler dessa conexão.

package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	l, err := net.Listen("unix", "/tmp/echo.sock")
	if err != nil {
		panic(err)
	}

	for {
		f, err := l.Accept()
		if err != nil {
			panic(err)
		}

		go func(c io.ReadWriter) {
			for {
				buf := make([]byte, 512)
				n, err := c.Read(buf)
				if err != nil {
					return
				}

				fmt.Printf("echo: %s\n", buf[:n])
				_, err = c.Write(buf[:n])
				if err != nil {
					panic(err)
				}
			}
		}(f)
	}
}

Cliente

No nosso exemplo de cliente criamos duas linhas de processamento: uma é a goroutine, que vai ficar reatando tudo que vier pela conexão, e a outra é a própria função main, que ficará em loop enviando dados para o servidor.

Não precisa ser assim. Dependendo de como você quer que seu sistema funcione, você pode, por exemplo, enviar uma mensagem para o servidor e subir uma goroutine para tratar apenas do timeout. O único cuidado é que essas funções bloqueiam o processamento.

package main

import (
	"fmt"
	"io"
	"net"
	"time"
)

func main() {
	f, err := net.Dial("unix", "/tmp/echo.sock")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	go func(r io.Reader) {
		buf := make([]byte, 1024)
		for {
			n, errf := r.Read(buf)
			if errf != nil {
				panic(err)
			}
			fmt.Printf("recebido: %s\n", buf[:n])
		}
	}(f)

	for {
		data := []byte("olá mundo")
		fmt.Printf("enviando: %s\n", data)
		_, err = f.Write([]byte("olá mundo"))
		if err != nil {
			panic(err)
		}

		time.Sleep(time.Duration(400) * time.Millisecond)
	}
}

Servidor usando netcat

Para testes podemos usar também o netcat:

nc -lU /tmp/echo.sock && rm /tmp/echo.sock

Cliente usando netcat:

nc -U /tmp/echo.sock

Exemplos

Executando o servidor echo:

go run echo/server/main.go

Em outro console, execute:

go run echo/client/main.go

Implementação do gonf usando npipe no Windows e Unix Domain Socket: https://github.com/gofn/gofn/blob/master/provision/docker_windows.go / https://github.com/gofn/gofn/blob/master/provision/docker_unix.go

Nossos encontros ocorrem todas as quintas, às 22h00, para participar entre no canal de Go no Telegram.