欢迎Golang 网络通信解析系列文章的第一个部分: net 包解析。net 包中充满了几个与底层操作系统紧密结合的网络原语,使我们能够构建世界级的生产网络应用程序。Docker、Kubernetes、CoreDNS、Traefik 等知名项目都是用 Go 构建的,并以其快速高效地在各种客户端服务器服务之间传输数据包的能力而闻名。
在本文中,我们将深入研究操作系统 (GOOS),了解套接字及其工作原理,并开始拼凑一个模型来理解连接和网络。为了演示,我们假设我们使用的是基于 Unix 的 API。
Socket
套接字是一个相当古老的概念,最初于 1983 年在 4.2BSD 实现中被广泛引入,然后溢出到几乎所有 UNIX 和其他系统。套接字基本上是一种扩展到网络系统的进程间通信 (IPC) 方法。在其他形式的 IPC(例如管道和消息队列)仅限于机器本身的情况下,可以打开套接字,并且来自外部源的通信请求可以链接到可用的套接字上。为了真正理解它们的作用,我们看一些用于调用套接字 API 的典型方法,并观察从一台主机到另一台主机的连接流。
套接字 API
Socket API 中有大约 5 个核心方法,想要联网的应用程序必须调用这些方法。它们是 socket() 、 bind() 、 connect() 、 listen() 和 accept() 在本文中,我们将研究 socket() 和 bind()
1. socket()
根据Linux 手册页(,
套接字的创建主要由内核执行,因此 Go 应用程序必须通过使用 syscall 库中实现的系统调用来调用它。让我们检查一下发生此系统调用的 Go 代码。
// Unix implementation of socket() system call
int socket(int domain, int type, int protocol);
// Windows implementation of socket() system call
SOCKET WSAAPI socket(int af, int type, int protocol);
代码片段 1: Socket 系统调用的实现: 请注意,Unix 和 Windows 显示的签名是相似的,尽管它们位于不同的 GOOS 上。
让我们来看看每个参数信息。
domain: 域是指在通信中使用的协议族。这有时被称为地址族。下图中展示了一些 domain 可用的参数信息:
Unix 内核还可以用 AF_UNIX 在同一台机器上的内核内执行通信 (不用于外部网络)。
type: 类型表示要使用的底层传输的类型。表示 SOCK_STREAM 为可靠的、有序的、面向连接的消息 (想想 TCP) 以及无 SOCK_DGRAM 连接、不可靠的消息 (想想 UDP 或 UNIX 连接) 。
protocol: 协议是指支持所选套接字的单一协议。这些通常在给定的协议族中,并且通常此值默认为 0。
2.bind()
下一个重要的系统调用是该 bind() 方法。通过系统调用创建 socket 后,套接字存在但是没有附在任何地址。bind() 通过使用从套接字创建返回的文件描述符来定位套接字,并将网络地址附加到它。
// Unix implementation of bind() system call
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Windows implementation of bind() system call
int WSAAPI bind(
SOCKET s,
const sockaddr *name,
int namelen
);
代码片段 2: bind() 系统调用的实现: 注意类似的函数签名。
Bind 接受以下参数:
sockfd: 这表示创建后返回的套接字的文件描述符。请记住,在套接字方法 socket() 中,返回类型是文件描述符。为了绑定我们创建的套接字,我们使用文件描述符作为绑定方法的第一个参数。
socketaddr: 是一个指向结构体的指针,该结构体包含我们创建的套接字可以绑定的地址。地址必须是“众所周知的”,即在绑定之前固定和解析。绑定后地址不应该改变,否则需要创建一个新的套接字。 DNS 解析应该首先发生,我们将在下一篇文章中讨论这个问题。
socklen: 指地址结构的大小
Bind 很重要,因为它提供了一个众所周知的寻找套接字的路径。它还使内核能够广播套接字的位置,以便外部系统可以知道连接的位置。可以在创建套接字后省略对绑定的调用,在这种情况下,内核将在 localhost 上选择一个随机可用端口并绑定到它。我们可能已经使用过 httptest.NewServer() 在使用不提供指定地址选项并让内核处理它的方法时。类似地,当在 IDE(例如 Goland)中使用调试器时,当调试器运行时,内核会通过省略对 bind 的调用来为调试器选择一个端口,因此每次都返回一个不同的端口。一旦调试器停止,套接字就会被销毁。
Go 的 Socket 和 Bind 的实现
是时候将我们的注意力转移到 Go 上了。我们已经看到了 socket() 和 bind的目的 calls() ,但问题仍然存在,它们在哪里以及如何实现。
// This unimplemnted function is found in the syscall/syscall_linx_386.go file and is referenced by the socket function below.
// It is implemented in the syscall/asm_linux_s390x.s file
func rawsocketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err Errno)
// The bind method mirrors the Unix API for bind calls. A call to rawsocketcall is made (see methods above)
func bind(s int, addr unsafe.Pointer, addrlen _Socklen) (err error) {
_, e := socketcall(_BIND, uintptr(s), uintptr(addr), uintptr(addrlen), 0, 0, 0)
if e != 0 {
err = e
}
return
}
// The socket method mirrors the Unix API for Socket calls. A call to rawsocketcall is made (see method above and below)
func socket(domain int, typ int, proto int) (fd int, err error) {
fd, e := rawsocketcall(_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto), 0, 0, 0)
if e != 0 {
err = e
}
return
}
// func rawsocketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err int)
// Kernel interface gets call sub-number and pointer to a0.
// This method is found in the syscall/asm_linux_s390x.s file
TEXT ·rawsocketcall(SB),NOSPLIT,$0-72
MOVD$SYS_SOCKETCALL, R1// syscall entry
MOVDcall+0(FP), R2// socket call number
MOVD$a0+8(FP), R3// pointer to call arguments
MOVD$0, R4
MOVD$0, R5
MOVD$0, R6
MOVD$0, R7
SYSCALL
MOVD$0xfffffffffffff001, R8
CMPUBLTR2, R8, oksock1
MOVD$-1, n+56(FP)
NEGR2, R2
MOVDR2, err+64(FP)
RET
oksock1:
MOVDR2, n+56(FP)
MOVD$0, err+64(FP)
RET
代码片段 3: Unix Socket 和 Bind 系统调用的 Golang 实现
golang-socket-syscall.go 上面代码片段 3 中的文件展示了 socket() 具有类似于 UNIX socket() API 签名的方法。更有趣的是,socket() 函数中调用的 rawsocketall() 实际上使用汇编码实现的。汇编代码似乎执行底层系统调用并将从 Go 应用程序接收到的相关参数传递给内核。
bind() 的实现也与 socket 的非常相似,也是通过调用底层 rawsocketall() 方法实现的。区别在于它使用 _BIND 参数调用 rawsocketcall。这只是一个整数常量,它指的是整数 2,其中 _SOCKET 指的是 1。其他系统调用有自己唯一的常量整数标识符。
关于文件描述符的快速说明
我们在代码片段 1 的 socket() 中看到的系统调用返回一个整数,这个整数将非常重要,并将为以下几篇文章奠定基础。 文件描述符 这个术语应该被记住,因为当我们开始创建、读取和写入连接时他会经常出现。您现在可以考虑的一件事是,为什么它被称为“文件”描述符。我会在以后的文章中回答这个问题。
现在,我认为这是一个停下来的好地方,因为我们现在已经了解了套接字和必须执行的一些核心系统调用来创建和绑定地址到它们。在下一篇文章中,我们将了解客户端-服务器服务之间的套接字连接是如何工作的。