golang tcp socket 那些事兒

前言

前幾年剛學 golang 時聽過這麼個論調:golang 要制霸雲計算行業。具體是不是這樣筆者就不知道了。不過這也體現出 golang 在網絡編程這一塊的實力可見一斑。今天我們就探討下網絡編程中的 socket 編程的那些事兒。

socket 入門

概念

socket,中文是套接字的意思。套接字又是幹什麼的?是負責進程間通信的。進程間通信有很多種,socket 在進程間通信中起到了什麼樣的作用呢?起到了跨越千山萬水和某一臺主機的進程通信,也就是網絡中兩個節點進行通信。那 A 機器的進程怎麼和 B 機器的進程進行通信?套接字是這麼設計的:找到 B 機器的 ip 和端口就行。很簡潔明瞭,對方的 ip 鎖定了,就是鎖定對方主機;如果端口號再鎖定,進程也鎖定。所以,套接字能進程間通信也就順理成章了。

實現

既然套接字 = ip+port,那麼怎麼實現一個套接字呢?這裏筆者加一點對架構的探討,如果是你,你怎麼實現這個套接字?換句話說就是要暴露(這裏指的是操作系統去暴露)哪些接口供應用程序調用?

那順着這個思路,我們來看看操作系統都是怎麼暴露 socket 接口的,先看看服務端的接口

創建套接字,用的名字不是 create 而是直接用的 socket,看下代碼(c 語言格式)

int socket(int domain, int type, int protocol)

domain 指的是 ip 地址的類型,比如是 ipv4、ipv6 或者本地套接字;type 就是指的是數據格式,準確地講就是是 tcp 還是 udp 等;protocol 這個字段好像廢棄了,默認是 0。

綁定用的名字就是 bind,看下面代碼(c 語言格式):

bind(int fd, sockaddr * addr, socklen_t len)

第一個參數 fd 就是 socket 函數的返回值,可以理解爲就是一個文件描述符,linux 一切皆文件嘛。第二個是 sockaddr 類型的指針,這個是一種通用的地質類型,就是因爲通用了,所以纔有第三個參數。第三個參數是第二個參數的解析標準。因爲第二個參數是通用格式,只能根據長度字段來判斷第二個參數該如何從通用解析到具體。這裏要注意的是,客戶端套接字可以不需要(最好也別)手動調用 bind,因爲操作系統會分配端口號的,因爲防止端口複用。

下面再來看看一個監聽的操作,也就是 listen。這個 listen 操作指的是服務端創建套接字要調用的,相當於通知外界我已經在這個端口和 ip 工作了,趕緊來撩我吧。這裏要注意的是,listen 這個函數是發生在 bind 之後。看下面聲明(C 語言格式)

int listen (int socketfd, int backlog)

第一個參數,socketfd 是套接字描述符,也就是 socket 函數的返回值,和 bind 函數的第一個參數是一樣一樣的。第二個參數 backlog 是未完成連接隊列的大小,這個參數決定了這個套接字能處理多少併發,原則上越大處理併發越大,但是併發多了消耗資源也挺多,這個就需要一定策略了。其實 linux 系統中這個參數默認不允許修改。

現在服務器端的從創建到綁定再到監聽,一系列的準備都搞定,客戶端可以開始撩了。一旦客戶端和服務器端撩上了,服務器端的操作系統內核就要通知應用程序有人來撩你了,那服務器端就要爲客戶端服務了。這個連接建立的過程就是 accept,來看看代碼:

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)

第一個參數 listensocketfd 指的是 listen 函數的返回值,也就是監聽套接字。第二、第三個參數其實不是參數,是返回值。因爲 C 語言不支持多返回值(這裏要感謝 golang 支持),不得已才這樣玩。cliaddr 和 addrlen 是客戶端的地址結構和長度。當然,accept 函數還有個函數返回值,是個 int 類型,這個返回值很有意思,其實也是個套接字描述符,可以理解爲 listensockfd 的副本。爲啥叫副本呢,因爲客戶端和服務端今後通信的實際套接字就是這個副本。爲啥不是 listensockfd 本身呢?很簡單,如果 listensockfd 和客戶端關閉連接了,那麼服務端也不能繼續提供服務了,也就是服務端只服務了一個客戶,那是萬萬不行的。

客戶端新建連接的時候也需要創建套接字,和上面的 socket 函數是一樣的。當客戶端新建了 socket 後,是需要和服務器端連接的,這個連接的建立就是靠 connect 函數完成的:

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)

sockfd 是套接字建立後返回值,servaddr 和 addrlen 是指向套接字地址結構的指針和該結構的大小。套接字地址結構必須有服務器的 ip 和端口號。這裏要注意幾點:

連接建立之後,客戶端和服務器就可以信息交互,也就涉及到了套接字的讀寫。先來看看寫:

ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

這三個函數都可以向套接字中寫入數據,但是用法不一樣 write 是常見的文件寫入函數,send 是爲了發送一些緊急的數據,TCP 協議特定情況下需要使用的到。向多重緩衝區發送數據,就是 sendmsg 函數。這裏要注意,往套接字寫數據的時候,寫完了並不代表對端收到了。寫完了僅僅是寫到操作系統緩衝成功了,至於對端收到與否是操作系統和 TCP 協議決定的。並且如果操作系統的緩衝區慢了,寫會阻塞,直到全部寫入緩衝區。

從套接字中讀數據:

ssize_t read (int socketfd, void *buffer, size_t size)

這裏的 read 函數是指操作系統從套接字中最多讀取多少個字節,並把讀取到的數據村道 buffer 中。返回值是實際讀了多少字節,如果返回值是 - 1 表示出錯。如果爲 0 表示 EOF,也就是可能對端斷開連接。

最後我們再來探討下套接字的關閉。當客戶端和服務器完成交互時,就要關閉套接字。一來釋放掉文件描述符,二來服務端釋放掉端口號。當然還有其他的資源,這裏就不一一贅述,看下有哪些可以關閉套接字的操作吧:

int close(int sockfd)
int shutdown(int sockfd, int howto)

很容易想到的就是 close 操作了,類似於對文件操作一樣。不過這裏的 close 比文件的關閉還不太一樣,這有點多態的意思。那我們來看看 close 和 shutdown 有什麼區別吧:

golang socket 實踐

理解了 socket 的常規接口之後,我們探討下 golang 中是如何使用 socket;

建立連接

服務器端監聽端口:

l, err := net.Listen("tcp"":9999")
if err != nil {
   log.Println(err)
   return
}
for {
   c, err := l.Accept()
   if err != nil {
      log.Println(err)
      break
   }
}

// 客戶端建立連接核心代碼:
c, err := net.Dial("tcp"":9999")
if err != nil {
   log.Println(err)
   return
}

代碼可以看出,服務器端操作套接字的接口就是兩個函數 Listen 和 Accept,不過要注意的是,這裏的 Listen 和 Accept 和前面我們探討的系統調用中的 listen 和 accept 不是一回事,這裏的內部實現最終還是調用上面探討過的幾個系統調用。

客戶端建立連接的接口就是一個函數 Dial,其實還有一個帶着超時時間的接口:DialTimeout 和一個帶有上下文環境的 DialContext。但是不管怎麼樣,客戶端建立連接在 golang 中一個函數就搞定。

當連接建立完成時,客戶端和服務端都會得到一個叫 Conn 的實例,這個就是真正去進行讀寫交互的。但是天有不測風雲,連接的建立往往不是一帆風順。有這麼幾種情況要注意:

讀寫

服務端與客戶端建立連接之後就用各自的 conn 實例去進行讀寫,進而實現業務邏輯。套接字通信的過程中,筆者遇到過這麼幾種情況:阻塞、超時、意外關閉、多 goroutine 讀寫。下面我們看看這幾種情況發生的原因。

阻塞

前文我們已經提到過,我們往 socket 中寫數據的時候,其實並不是對方接收到了,而是把數據寫到了操作系統的緩衝區。就是因爲這個緩衝區的原因,纔有了諸多異常(當然,沒有緩衝區問題更多,至少操作系統幫我們屏蔽了很多細節),阻塞就是其中之一。造成阻塞的原因有這麼幾個:

超時

有些讀寫操作可能要有時間限制,所以就用了 SetReadDeadline 的函數去設置超時時間,當超過這種時間限制時會發生阻塞。看下面代碼:

conn.SetReadDeadline(time.Now().Add(time.Microsecond * 10))
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))

經過這兩個函數對 conn 進行設置後,讀寫操作在 10 微秒沒響應的話會報超時的異常。

意外關閉

通信的過程中,如果某一方突然關閉,那另一方會有啥反應?在實踐的過程中,筆者總結如下:

多 goroutine 讀寫

如果多個 goroutine 對 conn 進行讀寫,就會有多重讀,多重寫兩種情況,socket 是全雙工,所以讀寫之間互不影響。

多 goroutine 讀的時候,其實沒什麼影響。因爲讀的話,反正讀到了也是不同業務場景下的東西,多重讀不會引發安全問題(不會重複讀)。但是有一點就是,有可能一個業務包會被兩個不同的 goroutine 讀取到,比如 goroutine A 讀到了業務包的前半部分,goroutine B 讀到了業務包的後半部分;這是 runtime 對業務數據的截取導致。

多 goroutine 寫的時候,就有問題了。多個 goroutine 寫不能每個寫一半,必須保證每次寫是原子操作,好在 golang 內部實現寫的時候加了鎖,這個我們後續探討。所以,我們要一次性將數據寫入 socket,不要分佈寫。

golang socket 源碼分析

本文這裏分別針對客戶端和服務器端的源碼進行分析,不過在源碼分析前,我們先了解下 I/O 多路複用的 epoll 機制。

I/O 多路複用之 epoll

多路複用是一種爲了應對高併發,比如 C10K 問題而提出的一種解決方案。多路複用,複用的是線程不是 I/O。多路複用中支持單進程同時監聽多個文件描述符並且阻塞等待,並在某個文件描述符可讀或者可寫的時候收到通知。

著名的多路複用實現有 select、poll、epoll(linux 環境),select 和 poll 這裏就不多贅述了,重點關注 epoll。epoll 就三個系統調用的函數:

int epoll_create(int size); 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll 的工作原理

epoll 的核心靠的就是事件驅動,當某個套接字註冊到 epoll 實例上時,會立即和網卡建立關係,也就是爲套接字註冊一個回調函數:ep_poll_callback,當網卡有數據了,內核調用這個回調函數來把這個套接字(fd)加入到一個叫 rdllist 的雙向鏈表中。epoll_wait 的職責就是檢查這個雙向鏈表有無可用套接字,無則阻塞,有責返回該套接字。

非阻塞 I/O

再看源碼前還有一個需要了解的就是非阻塞 I/O,也就是調用 I/O 操作的時候我們可以被立即返回,不用等待 I/O 操作完再進行下一步。爲什麼要了解非阻塞 I/O 呢,因爲多路複用要和非阻塞 I/O 搭配才能發揮更大的作用,不然如果是同步 I/O,可能會卡在那個 epoll_wait 上。

圖 1,異步非阻塞 I/O 圖示

從上圖可以看出,用戶進程不用阻塞等待數據返回,也不用不停詢問內核準備好了沒,這樣可以充分利用 CPU 乾點其他的事。

服務端套接字源碼追蹤

先看服務端套接字的創建、監聽、讀寫模型代碼實現:

package main

import (
   "fmt"
   "net"
)

func main() {
   l, err := net.Listen("tcp"":9999")
   if err != nil {
      fmt.Println("listen error: ", err)
      return
   }

   for {
      conn, err := l.Accept()
      if err != nil {
         fmt.Println("accept error: ", err)
         break
      }
      go ConnHandler(conn)
   }
}
func ConnHandler(conn net.Conn) {
   defer conn.Close()
   packet := make([]byte, 1024)
   for {
      // 沒有可讀數據阻塞
      _, _ = conn.Read(packet)
      // 不可寫則阻塞
      _, _ = conn.Write(packet)
   }
}

這是一個典型的 goroutine-per-connection 模式,使用這種模式可以實現同步的邏輯,也就是說 golang 已經爲我們屏蔽了底層所有的異步 I/O 以及協程切換等操作,完完全全負責寫業務邏輯即可。這主要歸功於我們在 goroutine 文中介紹的那個 netpoll,現在終於都串起來了。這個 netpoll 的最終實現就是 epoll。下面,我們先看看代碼中第 9 行的 listen 的實現,看看這個 socket 生成到底是怎麼一步步最終到了系統調用的 socket 函數的(省略了部分代碼,只留了主線):

// net/dial.go
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
 ...
 switch la := la.(type) {
 // 因爲我們是 TCP,所以進入這個 case
 case *TCPAddr:
  l, err = sl.listenTCP(ctx, la)
 ...
}

// 上一步的 sl.listenTCP net/tcpsock_posix.go
func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
 // 這裏又調用了 internetSocket
 fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
 ...
}

// 上一步的 internetSocket net/ipsock_posix.go
func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
 ...
 return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn)
}

// 上一步的 socket net/sock_posix.go
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
 s, err := sysSocket(family, sotype, proto)
 if err != nil {
  return nil, err
 }
 ...
}

// 上一步的 sysSocket net/sys_cloexec.go
func sysSocket(family, sotype, proto int) (int, error) {
 // See ../syscall/exec_unix.go for description of ForkLock.
 syscall.ForkLock.RLock()
 s, err := socketFunc(family, sotype, proto)
 ...
}

// 上一步的 socketFunc net/hook_unix.go
var (
 ...
 socketFunc        func(int, int, int) (int, error)  = syscall.Socket
 ...
)

// 上一步的 syscall.Socket syscall/syscall_unix.go
func Socket(domain, typ, proto int) (fd int, err error) {
 if domain == AF_INET6 && SocketDisableIPv6 {
  return -1, EAFNOSUPPORT
 }
 fd, err = socket(domain, typ, proto)
 return
}

如上述代碼的註釋所示,我們一步步找到了 socket 的最終系統調用的地方,所以這也正好符合了我們前文所述的 socket() 系統調用的描述。讀者可自行沿着主線一步步找到其他的系統調用。比如 bind、listen 如下:

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
 var err error
 if err = setDefaultListenerSockopts(fd.pfd.Sysfd); err != nil {
  return err
 }
 var lsa syscall.Sockaddr
 if lsa, err = laddr.sockaddr(fd.family); err != nil {
  return err
 }
 if ctrlFn != nil {
  c, err := newRawConn(fd)
  if err != nil {
   return err
  }
  if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
   return err
  }
 }
 if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
  return os.NewSyscallError("bind", err)
 }
 if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
  return os.NewSyscallError("listen", err)
 }
 if err = fd.init(); err != nil {
  return err
 }
 lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
 fd.setAddr(fd.addrFunc()(lsa), nil)
 return nil
}

上述代碼的第 19 行、22 行分別調用了系統調用的 bind 和 listen。我們再來看看這個套接字的讀和寫的最終源碼實現:

// internal/poll/fd_unix.go
func (fd *FD) Read([]byte) (int, error) {
 if err := fd.readLock(); err != nil {
  return 0, err
 }
 defer fd.readUnlock()
 if len(p) == 0 {
  // If the caller wanted a zero byte read, return immediately
  // without trying (but after acquiring the readLock).
  // Otherwise syscall.Read returns 0, nil which looks like
  // io.EOF.
  // TODO(bradfitz): make it wait for readability? (Issue 15735)
  return 0, nil
 }
 if err := fd.pd.prepareRead(fd.isFile); err != nil {
  return 0, err
 }
 if fd.IsStream && len(p) > maxRW {
  p = p[:maxRW]
 }
 for {
  n, err := syscall.Read(fd.Sysfd, p)
  if err != nil {
   n = 0
   if err == syscall.EAGAIN && fd.pd.pollable() {
    if err = fd.pd.waitRead(fd.isFile); err == nil {
     continue
    }
   }

   // On MacOS we can see EINTR here if the user
   // pressed ^Z.  See issue #22838.
   if runtime.GOOS == "darwin" && err == syscall.EINTR {
    continue
   }
  }
  err = fd.eofError(n, err)
  return n, err
 }
}

// internal/poll/fd_unix.go
func (fd *FD) Write([]byte) (int, error) {
 if err := fd.writeLock(); err != nil {
  return 0, err
 }
 defer fd.writeUnlock()
 if err := fd.pd.prepareWrite(fd.isFile); err != nil {
  return 0, err
 }
 var nn int
 for {
  max := len(p)
  if fd.IsStream && max-nn > maxRW {
   max = nn + maxRW
  }
  n, err := syscall.Write(fd.Sysfd, p[nn:max])
  if n > 0 {
   nn += n
  }
  if nn == len(p) {
   return nn, err
  }
  if err == syscall.EAGAIN && fd.pd.pollable() {
   if err = fd.pd.waitWrite(fd.isFile); err == nil {
    continue
   }
  }
  if err != nil {
   return nn, err
  }
  if n == 0 {
   return nn, io.ErrUnexpectedEOF
  }
 }
}

讀寫的代碼不難找,其實就是 conn 實例的讀寫,當然最終也是調用的系統調用的讀和寫。我這裏列出代碼的意義是要注意,conn 的讀和寫都是鎖住的,注意看上述代碼的第 3 行和第 44 行,這也充分驗證了我們上述的讀和寫的 goroutine 安全問題。

epoll 的封裝分析

golang 的 tcp socket 編程最偉大的地方在於它封裝了基於 epoll 多路複用的非阻塞 I/O,讓我們用同步的思維寫異步的程序,這麼做的有點就是業務邏輯不那麼分散,看起來流暢。

那 golang 是怎麼實現對 epoll 的封裝的呢?有這麼幾個數據結構要了解一下:

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

type netFD struct {
 pfd poll.FD

 // immutable until Close
 family      int
 sotype      int
 isConnected bool // handshake completed or use of association with peer
 net         string
 laddr       Addr
 raddr       Addr
}

type FD struct {
 // Lock sysfd and serialize access to Read and Write methods.
 fdmu fdMutex

 // System file descriptor. Immutable until Close.
 Sysfd int

 // I/O poller.
 pd pollDesc

 // Writev cache.
 iovecs *[]syscall.Iovec

 // Semaphore signaled when file is closed.
 csema uint32

 // Non-zero if this file has been set to blocking mode.
 isBlocking uint32

 // Whether this is a streaming descriptor, as opposed to a
 // packet-based descriptor like a UDP socket. Immutable.
 IsStream bool

 // Whether a zero byte read indicates EOF. This is false for a
 // message based socket connection.
 ZeroReadIsEOF bool

 // Whether this is a file rather than a network socket.
 isFile bool
}

type pollDesc struct {
 runtimeCtx uintptr
}

首先這個 TCPListener 是負責監聽網絡情況,比如有沒有連接到來等。這裏有個重要的數據結構就是 netFD,這是網絡描述符,不是前文我們講的 fd,其中第 24 行的 Sysfd 纔是真正意義上的 fd。這個 netFD 真正重要的是 pfd 下的 pd 字段,也就是第 27 行。這個字段是 pollDesc 類型,從名字可以看出,肯定是和多路複用有關,但是 unitptr 僅僅是個指針,所以這時候我們就要猜測是不是在運行時,也就是 runtime 包裏呢?之所以這麼猜是因爲源代碼裏充斥着 go:linkname 指令。我們就進去來看看 pollDesc 的相關信息:

// runtime/netpoll.go
type pollDesc struct {
 link *pollDesc // in pollcache, protected by pollcache.lock

 // The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
 // This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
 // pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
 // proceed w/o taking the lock. So closing, rg, rd, wg and wd are manipulated
 // in a lock-free way by all operations.
 // NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
 // that will blow up when GC starts moving objects.
 lock    mutex // protects the following fields
 fd      uintptr
 closing bool
 user    uint32  // user settable cookie
 rseq    uintptr // protects from stale read timers
 rg      uintptr // pdReady, pdWait, G waiting for read or nil
 rt      timer   // read deadline timer (set if rt.f != nil)
 rd      int64   // read deadline
 wseq    uintptr // protects from stale write timers
 wg      uintptr // pdReady, pdWait, G waiting for write or nil
 wt      timer   // write deadline timer
 wd      int64   // write deadline
}

這個 pollDesc 在多路複用中起到了什麼作用呢?答,golang 中的網絡輪詢器就是監聽這個 pollDesc 的狀態來做出相應的響應,就好像 epoll 監聽 fd 的可讀或者可寫一樣。

這裏的 pollDesc 筆者理解的是有點入口(可能叫多態或者接口)的意思,比如吧,如果是 linux,那麼編譯器進入的是 epoll 的邏輯;如果是其他的操作系統,那麼就是其他操作系統的多路複用邏輯。

那既然是對 linux 下的 epoll 的封裝,我們得找到具體的實現,看下面代碼:

// runtime/netpoll_epoll.go
func netpollinit() {
 epfd = epollcreate1(_EPOLL_CLOEXEC)
 if epfd >= 0 {
  return
 }
 epfd = epollcreate(1024)
 if epfd >= 0 {
  closeonexec(epfd)
  return
 }
 println("runtime: epollcreate failed with", -epfd)
 throw("runtime: netpollinit failed")
}

func netpollopen(fd uintptr, pd *pollDesc) int32 {
 var ev epollevent
 ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
 *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
 return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd)&ev)
}

func netpoll(block bool) gList {
 if epfd == -1 {
  return gList{}
 }
 waitms := int32(-1)
 if !block {
  waitms = 0
 }
 var events [128]epollevent
retry:
 n := epollwait(epfd, &events[0], int32(len(events)), waitms)
 if n < 0 {
  if n != -_EINTR {
   println("runtime: epollwait on fd", epfd, "failed with", -n)
   throw("runtime: netpoll failed")
  }
  goto retry
 }
 var toRun gList
 for i := int32(0); i < n; i++ {
  ev := &events[i]
  if ev.events == 0 {
   continue
  }
  var mode int32
  if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
   mode += 'r'
  }
  if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
   mode += 'w'
  }
  if mode != 0 {
   pd := *(**pollDesc)(unsafe.Pointer(&ev.data))

   netpollready(&toRun, pd, mode)
  }
 }
 if block && toRun.empty() {
  goto retry
 }
 return toRun
}

從上面代碼的第 7、20、33 行,我們看到了 epoll 三個系統調用的身影。所以到此爲止,我們對 golang tcp socket 的實現都找到了最最底層的系統調用。那最後我們總結下:

首先,Listen 和 Accept 都是能創建套接字的,只不過一個是監聽套接字,一個是連接套接字。這兩個套接字都會被加入到 epoll(linux)中來進行監聽;
其次,網絡輪詢器,也就是 netpoller 對 pollDesc 的監聽就是封裝了 epoll 對 fd 的監聽。爲什麼要封裝呢?因爲這是爲了 goroutine 的調度,讓需要 I/O 調用的讓出線程,在 netpoller 上準備好數據;

總結

本文我們探討了套接字的相關係統調用、epoll 的原理以及 golang 中對這兩者的封裝。再重複一遍:golang 的 tcp socket 是同步阻塞的,但是其底層實現是異步非阻塞的,並且也支持多路複用。之所以同步阻塞是因爲方便開發者寫邏輯,之所以底層是異步非阻塞是爲了方便 runtime 調度,防止因爲阻塞在系統調用上而失去了對 goroutine 的控制權。

轉自:

gopherliu.com/2017/08/10/golang-tcp-socket/

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vLcv1xVSGi8Upz0ux0GHqA