到目前为止,在这本书中,我们已经讨论了网络上节点之间的通信。但并不是所有的网络编程都只在不同的节点之间进行。应用程序有时可能需要与在同一节点上托管的数据库等服务进行通信.
除了指定IP和端口访问数据库外,还有一种方法:Unix域套接字。其使用文件系统来决定数据包目标地址,允许在同个节点的进程和另外的进程交换数据,使用IPC(inter-process communication)。
本章首先确切地定义了什么是Unix域套接字,以及如何控制对它们的读写访问。接着,您通过Go的net包探索其中三种类型,并各自编写echo服务。最后,您将编写一个使用Unix域套接字根据用户和组ID信息对客户端进行身份验证的服务。
什么是Unix域套接字
Unix域套接字将套接字寻址原则应用到文件系统中,每个Unix域套接字在文件系统上都有一个关联的文件,它对应于网络套接字的IP地址和端口号.您可以通过读写此文件来与侦听套接字的服务进行通信。同样,您也可以利用文件系统的所有权和权限来控制对套接字的读写访问。Unix域套接字通过绕过操作系统的网络堆栈来提高效率,消除了流量路由的开销。出于同样的原因,在使用Unix域套接字时不需要担心分片或数据包排序。如果您选择放弃Unix域套接字,并在与本地服务通信时专门使用网络套接字(例如,将应用程序连接到本地数据库、内存缓存等),那么您将忽略显著的安全优势和性能提升。
虽然这个系统带来了明显的优势,但它有一个注意事项:Unix域套接字是使用本地节点,因此您不能使用它们与其他远程节点通信,就像与网络套接字一样。因此,如果您希望将服务移动到另一个节点或需要应用程序的最大可移植性,则Unix域套接字可能并不太适合。要保持通信,需首先迁移到网络套接字。
绑定到Unix域套接字文件
当代码使用 net.Listen, net.ListenUnix或net.ListenPacket 函数尝试绑定到未使用的Unix域套接字地址时,将创建Unix域套接字文件。如果该地址的套接字文件已经存在,则操作系统将返回一个错误指示该地址正在使用中。在大多数情况下,只需删除现有的Unix域套接字文件可以解决这个错误。
更改套接字文件的所有权和权限
一旦服务绑定到套接字文件,您就可以使用GO的os包来修改文件的所有权和读/写权限。可以使用os.Chown函数来修改。
err := os.Chown("/path/to/socket/file", -1, 100)
其中-1是拥有者的ID,100是拥有者所在组的ID。
grp, err := user.LookupGroup("users")
上面是查看users这个组的group ID.
err := os.Chmod("/path/to/socket/file", os.ModeSocket|0660)
了解unix套接字类型
有3种:streaming sockets(类似TCP)/datagram sockets(类似UDP)/sequence packet sockets(类似TCP和UDP两者的结合),Go使用unix,unixgram和unixpacket来标明。
(1)unix streaming socket
下列代码不论Linux,macOS和Windows都支持。
先是echo.go
package echo
import (
"context"
"net"
)
func streamingEchoServer(ctx context.Context, network string,
addr string) (net.Addr, error) {
s, err := net.Listen(network, addr)
if err != nil {
return nil, err
}
go func() {
go func() {
<-ctx.Done()
_ = s.Close()
}()
for {
conn, err := s.Accept()
if err != nil {
return
}
go func() {
defer func() { _ = conn.Close() }()
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
return
}
_, err = conn.Write(buf[:n])
if err != nil {
return
}
}
}()
}
}()
return s.Addr(), nil
}
下列是echo_test.go
package echo
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"testing"
)
func TestEchoServerUnix(t *testing.T) {
dir, err := ioutil.TempDir("", "echo_unix")
if err != nil {
t.Fatal(err)
}
defer func() {
if rErr := os.RemoveAll(dir); rErr != nil {
t.Error(rErr)
}
}()
ctx, cancel := context.WithCancel(context.Background())
socket := filepath.Join(dir, fmt.Sprintf("%d.sock", os.Getpid()))
rAddr, err := streamingEchoServer(ctx, "unix", socket)
if err != nil {
t.Fatal(err)
}
err = os.Chmod(socket, os.ModeSocket|0666)
if err != nil {
t.Fatal(err)
}
conn, err := net.Dial("unix", rAddr.String())
if err != nil {
t.Fatal(err)
}
defer func() { _ = conn.Close() }()
msg := []byte("ping")
for i := 0; i < 3; i++ { // write 3 "ping" messages
_, err = conn.Write(msg)
if err != nil {
t.Fatal(err)
}
}
buf := make([]byte, 1024)
n, err := conn.Read(buf) // read once from the server
if err != nil {
t.Fatal(err)
}
expected := bytes.Repeat(msg, 3)
if !bytes.Equal(expected, buf[:n]) {
t.Fatalf("expected reply %q; actual reply %q", expected,
buf[:n])
}
_ = closer.Close()
<-done
}
另外两个socket由于windows不支持,所以暂时偷懒跳过。