自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go BIO/NIO探討:Net庫對(duì)Socket、Bind、listen、Accept的封裝

開發(fā) 前端
本文我們看看 Go net庫中 Server.ListenAndServe 的實(shí)現(xiàn)細(xì)節(jié)。

??前面一篇文章??提到,Go內(nèi)置的 net/http中使用了Blocking IO,主要體現(xiàn)在兩層 for 循環(huán)。但真的是這樣嗎?

本文我們看看 Go net庫中 Server.ListenAndServe 的實(shí)現(xiàn)細(xì)節(jié)。

net.Listen("tcp", addr) 方法通過系統(tǒng)調(diào)用 socket、bind、listen 生成 net.Listener 對(duì)象,在后面的for 循環(huán)中,通過系統(tǒng)調(diào)用 accept 等待新的tcp conn,將其包裝成一個(gè) conn 對(duì)象,在新的 goroutine 中對(duì)這個(gè)conn進(jìn)行處理。這里是典型的 per goroutine per connection 模型。這個(gè)環(huán)節(jié)看起來是阻塞的,但創(chuàng)建 socket 時(shí)設(shè)置了syscall.SOCK_NONBLOCK,對(duì)后來有什么影響?

// net/http/server.go struct Server
func (srv *Server) ListenAndServe() error {
ln, err := net.Listen("tcp", addr)
// ... 省略部分代碼
return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {
for {
// ...
rw, err := l.Accept()
// ...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}

net.Listener

net.Listen 觸發(fā)一系列的系統(tǒng)調(diào)用(主要是 socket、bind、listen),生成一個(gè) net.Listener 對(duì)象。這個(gè)函數(shù)創(chuàng)建兩類Listener: TCP 支持跨機(jī)器的網(wǎng)絡(luò)通信,UNIX支持本機(jī)的多進(jìn)程通信。

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
// ... 省略部分代碼
var l Listener
la := addrs.first(isIPv4)
switch la := la.(type) {
case *TCPAddr:
l, err = sl.listenTCP(ctx, la)
case *UnixAddr:
l, err = sl.listenUnix(ctx, la)
// ... 省略部分代碼
}

由于兩者都是先觸發(fā) syscall.Socket,我們從 socket 系統(tǒng)調(diào)用的視角來看兩者的區(qū)別。

// https://man7.org/linux/man-pages/man2/socket.2.html
#include <sys/socket.h>
int socket(int family, int type, int protocol);

socket() 創(chuàng)建一個(gè)用于網(wǎng)絡(luò)通信的endpoint,并返回對(duì)應(yīng)的套接字,也叫socket file descriptor。它是一個(gè) int 值,Linux C代碼里一般用 sockfd 作為變量名,而 Go net庫里一般用 fd 作為變量名。

第一個(gè)參數(shù) family 參數(shù)用來指定通信的協(xié)議族(protocol family),常用的enum值有:

  1. AF_UNIX/AF_LOCAL: Unix域協(xié)議, 用于本機(jī)的進(jìn)程間通信。
  2. AF_INET: IPV4協(xié)議。
  3. AF_INET6: IPV6協(xié)議。
  4. AF_ROUTE: 路由套接字。
  5. 全量Enum定義在Linux <sys/socket.h> 下。

第二個(gè)參數(shù) type 參數(shù)用來指定通信語義,常用enum值有:

  1. SOCK_STREAM=1: 基于TCP, 提供有序、可靠、雙向、基于連接的字節(jié)流,不限制消息長度,支持消息的優(yōu)先級(jí)傳輸。
  2. SOCK_DGRAM=2: 基于UDP, 支持?jǐn)?shù)據(jù)報(bào),不是基于連接的、不保證可靠性,且消息的最大長度是固定的。
  3. SOCK_RAW=3: 支持通過原始的網(wǎng)絡(luò)協(xié)議訪問。
  4. SOCK_RDM=4:。
  5. SOCK_SEQPACKET=5: 基于TCP, 提供有序、可靠、雙向、基于連接的字節(jié)流,但消息的最大長度是固定的,超出的部分會(huì)被丟棄。

除了這幾個(gè),還有兩個(gè)enum值在 Go net/http 被用到了,分別是:

  1. SOCK_NONBLOCK: 設(shè)置 accept 和 read/write操作為 O_NONBLOCK, 對(duì)應(yīng)的場景有:
  • 接收連接 accept: 同步模式下沒有新連接時(shí), 線程會(huì)被休眠, 異步模式下會(huì)返回EWOULDBLOCK/EAGAIN錯(cuò)誤。
  • read類操作: 同步模式下socket緩沖區(qū)沒有數(shù)據(jù)可讀時(shí), 線程會(huì)被休眠, 異步模式下會(huì)返回EWOULDBLOCK/EAGAIN錯(cuò)誤。
  • write類操作: 同步模式下socket緩沖區(qū)已滿無法寫入時(shí), 線程會(huì)被休眠, 異步模式下會(huì)返回EWOULDBLOCK/EAGAIN錯(cuò)誤。
  1. SOCK_CLOEXEC: 由于fork時(shí),子進(jìn)程默認(rèn)拷貝父進(jìn)程的數(shù)據(jù)空間、堆、棧等,當(dāng)然也包含socket, 通過設(shè)置這個(gè)flag, 可以保證fork出來的子進(jìn)程不持有父進(jìn)程創(chuàng)建的socket。

第三個(gè)參數(shù) protocol 指定通信協(xié)議,對(duì)于domain=AF_INET/AF_INET6來說,常見的enum值有 IPPROTO_TCP IPPROTO_UDP,全量。

socket() 返回一個(gè) socket file descriptor,但并沒有協(xié)議和地址與其關(guān)聯(lián)。對(duì)于tcp client端而言,可以由系統(tǒng)隨機(jī)指定一個(gè)端口;對(duì)于一個(gè) tcp server 而言,必須設(shè)置一個(gè)公開可訪問的ip地址和端口。bind函數(shù)實(shí)現(xiàn)了這個(gè)功能:

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// sockaddr 包含
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}

其中 sockfd 參數(shù)是 socket函數(shù)的返回值,后面兩個(gè)參數(shù)指定協(xié)議類型和地址。

當(dāng)socket被創(chuàng)建以后,它并不能被動(dòng)地接收創(chuàng)建連接請(qǐng)求,此時(shí)它只能作為一個(gè)client使用。要被動(dòng)地接收請(qǐng)求,轉(zhuǎn)化為server,需要依賴 listen函數(shù)。該函數(shù)調(diào)用以后,sockfd的狀態(tài)會(huì)從 closed 轉(zhuǎn)換為 listen (netstat 命令可以進(jìn)行查看)。listen函數(shù)的聲明如下:

#include <sys/socket.h>

int listen(int sockfd, int backlog);

第一個(gè)參數(shù) sockfd 是 socket函數(shù)的返回值,第二個(gè)參數(shù)指定了處于ESTABLISHED狀態(tài)的sockets的隊(duì)列大小(從Linux 2.2起), 而不是處于 SYNC_RCVD狀態(tài)的sockets隊(duì)列的大小。這里提到的兩個(gè)狀態(tài)在TCP連接的三次握手中有所定義:

tcp三次握手

backlog 默認(rèn)值是0x80即128,通??梢耘渲迷谖募?br>/proc/sys/net/ipv4/tcp_max_syn_backlog 中,同時(shí)受到/proc/sys/net/core/somaxconn 的限制。在 Go 中,這個(gè)參數(shù)可以通過 func maxListenerBacklog() int 獲取。如果隊(duì)列滿了,Client端會(huì)收到 ECONNREFUSED 錯(cuò)誤,即 connection refused。

小結(jié)一下,Linux操作系統(tǒng)層面創(chuàng)建一個(gè)tcp server,走的邏輯是:

  1. socket函數(shù)創(chuàng)建一個(gè)套接字 sockfd,默認(rèn)狀態(tài)是 Closed。
  2. bind函數(shù)綁定 sockfd 與特定的協(xié)議地址,比如 tcp 0.0.0.0:8080。
  3. listen函數(shù)修改sockfd的狀態(tài)為 LISTEN,內(nèi)核開始監(jiān)聽套接字,三次握手建立連接。

回到 Go net 庫的處理流程,我們關(guān)注的函數(shù)是 sysListener.listenTCP:

// net/tcpsock_posix.go struct sysListener
func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
if err != nil {
return nil, err
}
return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

對(duì)于一個(gè) tcp server,實(shí)例 sl 可以被這樣賦值,系統(tǒng)調(diào)用被封裝在函數(shù) internetSocket 里:

sl := &sysListener{
ListenConfig: *lc, // lc 是默認(rèn)值
network: network, // string "tcp"
address: address, // string "0.0.0.0:8080"
}

對(duì)于一個(gè) tcp server,函數(shù) internetSocket 接收到的參數(shù)可以是:

func internetSocket(
ctx context.Context, // context.Background()
net string, // "tcp"
laddr sockaddr, // &TCPAddr{IP:"0.0.0.0",Port:8080,Zone:"不知道是啥"}, DNS服務(wù)讀取的地址
raddr sockaddr, // nil, os=aix|windows|openbsd && mode="dail" 才需要
sotype int, // syscall.SOCK_STREAM
proto int, // 0
mode string, // "listen"
ctrlFn func(string, string, syscall.RawConn) error // sl.ListenConfig.Control
) (fd *netFD, err error) {

函數(shù) internetSocket 同樣只是一層封裝,內(nèi)部調(diào)用的是函數(shù) socket。函數(shù)socket內(nèi)部按照順序調(diào)用了socket/bind/listen,返回一個(gè)套接字,這個(gè)套接字使用network poller,支持異步IO。函數(shù) socket的主要邏輯如下:

// 通過sysSocket執(zhí)行socket系統(tǒng)調(diào)用
// 返回一個(gè)套接字s
s, err := sysSocket(family, sotype, proto)

// 封裝int類型的套接字為一個(gè)結(jié)構(gòu)體
if fd, err = newFD(s, family, sotype, net); err != nil {
// ...省略部分代碼

// 對(duì)于 SOCK_STREAM, SOCK_SEQPACKET類型,調(diào)用bind和listen
if laddr != nil && raddr == nil {
switch sotype {
case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil

對(duì)于linux tcp server 而言,函數(shù) sysSocket 的關(guān)鍵只有一行代碼:

s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)

SOCK_STREAM、SOCK_NONBLOCK 和 SOCK_CLOEXEC 的語義前面已經(jīng)講過,不再贅述。

socketFunc 定義存放在 net/hook_unix.go 里,與listenFunc在一塊:

// Placeholders for socket system calls.
socketFunc func(int, int, int) (int, error) = syscall.Socket
connectFunc func(int, syscall.Sockaddr) error = syscall.Connect
listenFunc func(int, int) error = syscall.Listen
getsockoptIntFunc func(int, int, int) (int, error) = syscall.GetsockoptInt

通過sysSocket拿到套接字以后,通過函數(shù)newFD將其封裝成一個(gè)結(jié)構(gòu)體,類型是 *net.netFD:

func newFD(sysfd, family, sotype int, net string) (*netFD, error) {
ret := &netFD{
pfd: poll.FD{
Sysfd: sysfd,
IsStream: sotype == syscall.SOCK_STREAM,
ZeroReadIsEOF: sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW,
},
family: family,
sotype: sotype,
net: net,
}
return ret, nil
}

其中,結(jié)構(gòu)體內(nèi)部poll.FD定義了讀寫的邏輯,它封裝了6個(gè)系統(tǒng)調(diào)用:

readSyscallName     = "read"
readFromSyscallName = "recvfrom"
readMsgSyscallName = "recvmsg"
writeSyscallName = "write"
writeToSyscallName = "sendto"
writeMsgSyscallName = "sendmsg"

在創(chuàng)建套接字時(shí),已經(jīng)設(shè)置了 SOCK_NONBLOCK flag,如果沒有可用的連接,讀寫數(shù)據(jù)時(shí),會(huì)收到 EWOULDBLOCK/EAGAIN 錯(cuò)誤。Go net庫的處理是等待一段時(shí)間,我們看其中一個(gè)例子:

// ReadMsgInet4 is ReadMsg, but specialized for syscall.SockaddrInet4.
func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.SockaddrInet4) (int, int, int, error) {
if err := fd.readLock(); err != nil {
return 0, 0, 0, err
}
defer fd.readUnlock()
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, 0, 0, err
}
for {
n, oobn, sysflags, err := unix.RecvmsgInet4(fd.Sysfd, p, oob, flags, sa4)
if err != nil {
if err == syscall.EINTR {
continue
}
// TODO(dfc) should n and oobn be set to 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, oobn, sysflags, err
}
}

回到 func (sl *sysListener) listenTCP 方法,函數(shù) internetSocket 返回一個(gè)套接字結(jié)構(gòu)體的實(shí)例,用來構(gòu)建 TCPListener 對(duì)象 &TCPListener{fd: fd, lc: sl.ListenConfig}。后面 accept tcp conn 時(shí),會(huì)用到 net.netFD 的 accept 方法,后者只是封裝了 poll.DF 的 Accept 方法。

回到 net/http 下的 struct Server 的 ListenAndServe 方法,它包含兩步:

  1. net.Listen 方法獲取 ln *TCPListener。
  2. srv.Serve(ln)。

前面詳細(xì)說明了第一步的細(xì)節(jié),后面我們看第二步如何Serve。

TCPListenr.Accept

對(duì)于 linux下的 tcp server,系統(tǒng)調(diào)用 accept 發(fā)生在 socket、bind、listen 之后,它從內(nèi)核中的 ESTABLISHED 隊(duì)列中獲取一個(gè)建立完成的鏈接。通過函數(shù)socket生成的套接字sockfd可以是阻塞或非阻塞(NONBLOCK),它的函數(shù)聲明如下:

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict addrlen);

#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict addrlen, int flags);

對(duì)于阻塞/非阻塞的套接字, accept 的表現(xiàn)并不相同:

  1. 阻塞的sockfd: 調(diào)用方會(huì)一直被阻塞,直到有一個(gè)ESTABLISHED的tcp conn。
  2. 非阻塞的sockfd: 函數(shù)accept會(huì)返回 EAGAIN 或 EWOULDBLOCK 的錯(cuò)誤。

Go net庫使用的是非阻塞的套接字,我們看這部分代碼的邏輯:

// net/net.go struct Server
func (srv *Server) Serve(l net.Listener) error {
for {
rw, err := l.Accept()
// ... 省略部分代碼

這里 ln 的類型是 *TCPListener, 其方法Accept的定義如下:

// Accept implements the Accept method in the Listener interface; it
// waits for the next call and returns a generic Conn.
func (l *TCPListener) Accept() (Conn, error) {
if !l.ok() {
return nil, syscall.EINVAL
}
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

struct TCPListener 的結(jié)構(gòu)如下, accept 依賴成員變量 fd *net.netFD,它通過 pdf poll.FD 的 Accept 方法獲取client端的套接字,并封裝成一個(gè) net.netFD 對(duì)象:

// TCPListener is a TCP network listener. Clients should typically
// use variables of type Listener instead of assuming TCP.
type TCPListener struct {
fd *netFD
lc ListenConfig
}

func (fd *netFD) accept() (netfd *netFD, err error) {
d, rsa, errcall, err := fd.pfd.Accept()
// ... 省略部分代碼

if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
return nil, err
}
if err = netfd.init(); err != nil {
netfd.Close()
return nil, err
}
lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
return netfd, nil
}

繼續(xù)看 poll.FD 的方法 Accept。它內(nèi)部是一個(gè) for 循環(huán),先嘗試通過系統(tǒng)調(diào)用accpt4 獲取一個(gè)套接字,結(jié)果會(huì)有下面幾種情況:

  1. 獲取成功, err == nil, 函數(shù)直接return即可。
  2. syscall.EINTR 表示系統(tǒng)調(diào)用期間收到操作系統(tǒng)的信號(hào),但并沒有實(shí)質(zhì)的錯(cuò)誤發(fā)生,所以選擇重試。
  3. syscall.EAGAIN 表示目前并沒有establish新的tcp conn,處理是通過 waitRead 將當(dāng)前goroutine掛起,等待被喚醒。
  4. syscall.ECONNABORTED 表示遠(yuǎn)程連接已經(jīng)在ESTABLISHED隊(duì)列,還沒有被Accept時(shí),client端放棄連接。
  5. 其他錯(cuò)誤: 返回一個(gè)錯(cuò)誤。
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
if err := fd.readLock(); err != nil {
return -1, nil, "", err
}
defer fd.readUnlock()

if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
s, rsa, errcall, err := accept(fd.Sysfd)
if err == nil {
return s, rsa, "", err
}
switch err {
case syscall.EINTR:
continue
case syscall.EAGAIN:
if fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
case syscall.ECONNABORTED:
// This means that a socket on the listen
// queue was closed before we Accept()ed it;
// it's a silly error, so try again.
continue
}
return -1, nil, errcall, err
}
}

// 代碼路徑: internal/poll/sock_cloexec.go

// Wrapper around the accept system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
func accept(s int) (int, syscall.Sockaddr, string, error) {
ns, sa, err := Accept4Func(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
// ... 省略部分代碼

另外可以看到,accept4 系統(tǒng)調(diào)用時(shí),傳入了 SOCK_NONBLOCK 和 SOCK_CLOEXEC 兩個(gè) flag,socket 系統(tǒng)調(diào)用也使用了這兩個(gè) flag。

通過 ln.Accept 獲取到ESTABLISHED連接的套接字以后,就可以對(duì)遠(yuǎn)端的client進(jìn)行服務(wù)了。

在本文中,總共有兩類套接字(socket):

  1. server端監(jiān)聽的套接字, 通過socket系統(tǒng)調(diào)用創(chuàng)建。它的生命周期和server同樣長。
  2. 已連接的套接字, 通過accept系統(tǒng)調(diào)用創(chuàng)建。它的生命周期比較短,尤其是對(duì)于應(yīng)用層是HTTP短鏈接的情況。

??第一篇??文章"Go BIO/NIO探討(1):Gin框架中如何處理HTTP請(qǐng)求"中,我們提到了兩層 for 循環(huán),本文只是講了第一層。從阻塞、非阻塞的角度來看,TCPListener.Accept 方法看起來是block的實(shí)現(xiàn),但底層的套接字和系統(tǒng)調(diào)用設(shè)置了 NONBLOCK flag,可以說是基于 NONBLOCK 的方式實(shí)現(xiàn)的。單純從網(wǎng)絡(luò)的視角看,這稱得上是 Non-blocking IO 了。

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2023-03-07 08:00:12

netpollGo

2023-03-31 07:49:51

syscall庫Echo Serve

2023-03-06 08:37:58

JavaNIO

2011-03-31 10:41:49

BIONIOIO

2020-04-16 15:20:43

PHP前端BIO

2022-04-16 16:52:24

Netty網(wǎng)絡(luò)服務(wù)器客戶端程序

2018-09-19 14:53:02

NIOBIO運(yùn)行

2021-06-10 09:52:33

LinuxTCPAccept

2010-01-19 09:19:02

C++封裝

2014-12-11 09:20:30

TCP

2020-10-10 19:37:27

BIO 、NIO 、A

2020-10-10 07:00:16

LinuxSocketTCP

2020-10-14 14:31:37

LinuxTCP連接

2009-12-24 16:56:21

ADO.NET庫

2022-04-13 07:59:23

IOBIONIO

2021-06-11 17:26:06

代碼Java網(wǎng)絡(luò)編程

2023-07-11 08:40:02

IO模型后臺(tái)

2019-10-18 08:22:43

BIONIOAIO

2010-09-15 09:03:44

JavaScript

2021-08-13 12:05:15

Goneturl
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)