Beego框架的一條神祕日誌引起的思考

公司目前的後臺是用Beego框架搭的,而且爲了服務的不中斷升級,咱們開啓了Beego的Grace模塊,用於熱升級支持。一切都跑井井有理,直到有一天,領導甩出一些服務日誌,告知程序一直報錯:前端

2018/03/08 17:49:34 20848 Received SIGINT.
2018/03/08 17:49:34 20848 [::]:5490 Listener closed.
2018/03/08 17:49:34 20848 Waiting for connections to finish...
2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe:  accept tcp [::]:5490: use of closed network connection 20848
複製代碼

問題出在第4行,每次服務關閉時,都會報出use of closed network connection。按理說這時候網絡鏈接應該關閉了啊,進程都退出了,怎麼還Accept 5490端口?到Beego的Issues列表裏一搜,已經有人問過這個問題了(#2809),下面尚未人回答,搜也搜索不到,只剩最後一個工具了:看源碼。git

1. Grace模式

首先能夠確定,不開Grace模式的話,是沒有這些日誌打出來的,而是直接結束。所以咱們先要對Beego的Grace模式有一些瞭解。Beego官網對此必定的介紹:Grace模塊。大體是說他們參照:Grace_restart_in_golang這篇文章的思路實現的熱升級功能,文章很長,講述的思路很清晰,大致過程以下:github

image

開源中國翻譯-GracefulRestart 這篇中文翻譯說明的更通俗易懂。明白了熱升級的原理,咱們就能夠進入代碼中詳細尋找了。一切都從beego.Run()開始。golang

beego.Run()建立好了BeeApp對象,而且調用BeeApp.Run()執行。Run方法有不一樣的啓動模式,在此,咱們只關注Grace部分。編程

func (app *App) Run() {
	addr := BConfig.Listen.HTTPAddr
	...

	// run graceful mode
	if BConfig.Listen.Graceful {
		...
		if BConfig.Listen.EnableHTTP {
			go func() {
			    // 建立了GraceServer 是對http.Server的一層封裝
				server := grace.NewServer(addr, app.Handlers)
				...
				if err := server.ListenAndServe(); err != nil {
					logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
					endRunning <- true
				}
			}()
		}
		<-endRunning
		return
	}
	...
}
複製代碼

代碼裏能夠看到,logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))正是打出上述日誌的源頭:bash

2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe:  accept tcp [::]:5490: use of closed network connection 20848
複製代碼

那麼爲何會返回use of closed network connection這個錯誤呢?跟進ListenAndServe()方法中查看:網絡

func (srv *Server) ListenAndServe() (err error) {
    ...
    // 處理上圖中的熱升級信號(fork子進程),SIGINT、SIGTERM信號(進程結束信號)
	go srv.handleSignals()

    ...
    // 若是是子進程執行,Getppid()拿到父進程pid,而且Kill
	if srv.isChild {
		process, err := os.FindProcess(os.Getppid())
		if err != nil {
			log.Println(err)
			return err
		}
		err = process.Kill()
		if err != nil {
			return err
		}
	}

	log.Println(os.Getpid(), srv.Addr)
	return srv.Serve()
}
複製代碼

跟進Serve()方法:app

func (srv *Server) Serve() (err error) {
	srv.state = StateRunning
	//這裏咱們傳入了一個GraceListener,對net.Listener作了封裝,在後面會用到。
	err = srv.Server.Serve(srv.GraceListener)
	log.Println(syscall.Getpid(), "Waiting for connections to finish...")
	//此處會等待全部鏈接處理完成,對應圖中的父進程結束流程。
	srv.wg.Wait()
	srv.state = StateTerminate
	return
}
複製代碼

仍是調回了http.Server.Serve()方法,看這個方法:框架

func (srv *Server) Serve(l net.Listener) error {
	defer l.Close()
	...

	for {
	    //代碼在這裏阻塞,若是沒有鏈接進來的話。
		rw, e := l.Accept()
		if e != nil {
			select {
			//正常的結束流程
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			...
			
			//不正常的結束流程
			return e
		}
		
		//開啓一個go程處理新鏈接
		c := srv.newConn(rw)
		go c.serve(ctx)
	}
}
複製代碼

若是是正常結束,咱們應該會收到ErrServerClosed,這雖然是個Error,可是相似於io.EOF,是一個正常的結束流程,問題在於,咱們收到的並非ServerClosed,而是use of closed connection,由l.Accept()方法返回,那咱們進到Accept()一探究竟。tcp

2.Accept家族

前面說了,l.Accept()l是一個GraceListener,那咱們直接去看它的Accept()方法。

func (gl *graceListener) Accept() (c net.Conn, err error) {
    //調AcceptTCP()
	tc, err := gl.Listener.(*net.TCPListener).AcceptTCP()
	if err != nil {
		return
	}

    ...
    
    //每次新來一個鏈接+1,當鏈接處理完成時-1。 前面wg.Wait()等的就是這個值減爲0。
	gl.server.wg.Add(1)
	return
}
複製代碼

仍是調回了net.TCPListenerAcceptTCP(),去TCPListener下看看它的AcceptTCP()

func (l *TCPListener) AcceptTCP() (*TCPConn, error) {
	...
	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
}
複製代碼

這裏咱們看到返回了一個OpError,它的格式正如accept tcp [::]:5490: use of closed network connection所示,以accept開頭,有netaddr信息,還有一個Err的封裝,看來沒有找錯,錯誤就是從這裏發出來的,趕忙進到l.accept()看看:

func (ln *TCPListener) accept() (*TCPConn, error) {
	fd, err := ln.fd.accept()
	if err != nil {
		return nil, err
	}
	//建立了新的TCP鏈接
	return newTCPConn(fd), nil
}
複製代碼

這裏調了fd.accept()涉及到一點UNIX系統知識,fdfile descriptor(文件描述符),在UNIX中,一切皆文件,一個Socket鏈接,一個進程,均可以看做是一個個的文件,前面的圖中咱們介紹的熱升級技術,子進程之因此能拿到父進程的Socket鏈接,也是父進程在fork子進程的過程當中,把本身的Socket鏈接的文件做爲啓動參數傳遞給了子進程,從而讓子進程能夠經過這個文件接管新來的請求,咱們直接進入ln.fd.accept()看看這個fd當中有何玄機:

func (fd *netFD) accept() (netfd *netFD, err error) {
	d, rsa, errcall, err := fd.pfd.Accept()
	if err != nil {
		if errcall != "" {
			err = wrapSyscallError(errcall, err)
		}
		//有可能
		return nil, err
	}
    
	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 {
		fd.Close()
		//有可能
		return nil, err
	}
	...
	return netfd, nil
}

複製代碼

上面三處代碼,都有可能返回err,經過本地運行服務,發現啓動服務後,沒有鏈接進來時,(也就是說,代碼如今在Accept()阻塞,並無走到下面的控制流程中去),這時向其發送SIGINT信號,依然會打出use of closed connection日誌,這說明這個err就是由fd.pfd.Accept()方法拋出來的,進到這個方法裏看看詳情:

// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
    ...
	if err := fd.pd.prepareRead(fd.isFile); err != nil {
		return -1, nil, "", err
	}
	for {
	    //這個accept()是對accept系統調用的封裝方法
		s, rsa, errcall, err := accept(fd.Sysfd)
		...
	    switch err {
		case syscall.EAGAIN:
			if fd.pd.pollable() {
				if err = fd.pd.waitRead(fd.isFile); err == nil {
					continue
				}
			}
		...
		}
		return -1, nil, errcall, err
	}
}
複製代碼

函數裏的accept()是對accept系統調用的封裝,再往下已然超出這篇文章的範疇,那又是UNIX系統的一大篇知識,Accept家族到這結束,咱們得出的結論是:這個錯誤是UNIX系統發出的,與咱們的服務和Go語言都無關。這個結論無疑是鴕鳥式作法,儘管錯誤是系統發出的,可是系統不會無端的彙報錯誤,必然是什麼操做有問題,觸發系統彙報了這個錯誤。讓我門再仔細的掃視這個方法,它首先prepareRead()準備了一番,而後才進行accept系統調用,儘管不多是卡在這個prepareRead()方法上(由於咱們的服務啓動後,是能夠正常接受和處理鏈接的),可是咱們仍是能夠去看看這裏作了哪些prepare

func (pd *pollDesc) prepareRead(isFile bool) error {
	return pd.prepare('r', isFile)
}

func (pd *pollDesc) prepare(mode int, isFile bool) error {
	if pd.runtimeCtx == 0 {
		return nil
	}
	res := runtime_pollReset(pd.runtimeCtx, mode)
	return convertErr(res, isFile)
}
複製代碼

最終調用了Runtimeruntime_pollReset()對IO輪詢器進行重置,而且convertRuntime拋出的Err,進入這個convertErr()看看:

func convertErr(res int, isFile bool) error {
	switch res {
	case 0:
		return nil
	case 1:
		return errClosing(isFile)
	case 2:
		return ErrTimeout
	}
    ...
}

// ErrNetClosing is returned when a network descriptor is used after it has been closed. 
var ErrNetClosing = errors.New("use of closed network connection")

// Return the appropriate closing error based on isFile.
func errClosing(isFile bool) error {
	if isFile {
		return ErrFileClosing
	}
	return ErrNetClosing
}
複製代碼

終於,咱們在不可能拋出這個Err的地方發現了這個Erruse of closed network connection。這話說的實在有些繞,由於咱們知道prepareRead()是沒有返回Err的,否則咱們的服務也不可能啓動而且監聽端口,因此雖然這個地方聲明瞭這個ErrNetClosing對象,可是卻不是這裏拋出來的Err。前面的分析咱們已經得出結論,Err是系統返回給咱們的。分析到了這裏又中斷了,不過咱們能夠拓展下思路,看看這個ErrNetClosing的註釋寫了什麼:大意是說,當使用一個被關閉的網絡描述符時,這個Error會被返回,那麼咱們找找這個ErrNetClosing在那裏被使用到了,在prepareRead()方法附近,咱們發現了下面的這個方法:

//修改了上述變量ErrNetClosing的值
var ErrNetClosing = errors.New("use of closed network connection --- 改")

func (pd *pollDesc) wait(mode int, isFile bool) error {
    ...
	res := runtime_pollWait(pd.runtimeCtx, mode)
	println(" 沒錯,錯誤就是我拋出來的 res:", res)
	//若是res爲1 ,拋出錯誤ErrNetClosing。若是res爲0,err = nil。
	return convertErr(res, isFile)
}

複製代碼

上面的代碼是我對原有代碼進行修改,加上一些日誌,如今從新運行,發送SIGINT信號,看看日誌有什麼變化:

注意:若是修改了go標準庫中的代碼,你須要go build -a ,添加a參數,意思是全部的代碼都從新編譯,這也包括go標準庫中的代碼。

沒錯,錯誤就是我拋出來的    res:0
   沒錯,錯誤就是我拋出來的    res:0
2018/03/09 11:42:57 31164 0.0.0.0:5490
2018/03/09 11:43:09 31164 Received SIGINT.
2018/03/09 11:43:09 31164 [::]:5490 Listener closed.
2018/03/09 11:43:09 31164 Waiting for connections to finish...
   沒錯,錯誤就是我拋出來的    res:1
2018/03/09 11:43:09 [C] [asm_amd64.s:2337] ListenAndServe:  accept tcp [::]:5490: use of closed network connection --- 改 31164
複製代碼

res=0時,一切都沒什麼問題,當接收到SIGINT,從日誌能夠看到,res這時候爲1,這就致使了convertErr()返回錯誤ErrNetClosing。看起來,這個wait()就是拋出這個錯誤的真正源頭了。然而,咱們雖然找到了它,但卻沒有解決咱們的疑問,爲何res會被系統置爲1,最終致使咱們收到ErrNetClosing?究竟是什麼操做致使了這個錯誤?並且,看到這個wait()方法,不由又有新的疑問:爲何Accept()家族最後阻塞在Accept系統調用上,錯誤倒是這個wait()返回的,wait()Accept()家族有着怎樣的關聯呢?

3. 網絡編程中的 AcceptWait

看來Go幫咱們作了太多太多,咱們只知道,服務端在ListenAndServe()中阻塞,更進一步,是在fd.Accept()方法中阻塞,等待鏈接的到來。而前面的分析咱們卻發現了一個wait()方法,無疑,wait更能比accept表達出阻塞等待的含義,咱們再次審視fd.Accept()方法:

// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
    ...
	if err := fd.pd.prepareRead(fd.isFile); err != nil {
		return -1, nil, "", err
	}
	for {
	    //這個accept()是對accept系統調用的封裝方法,其實它是返回了的,沒有阻塞
		s, rsa, errcall, err := accept(fd.Sysfd)
	    ...
	    switch err {
		case syscall.EAGAIN:
			if fd.pd.pollable() {
			    //這裏調用了waitRead(),說明了上面的accept()方法其實沒有阻塞,真正的阻塞在這裏。
				if err = fd.pd.waitRead(fd.isFile); err == nil {
					continue
				}
			}
		}
		return -1, nil, errcall, err
	}
}

func (pd *pollDesc) waitRead(isFile bool) error {
	return pd.wait('r', isFile)
}
複製代碼

終於,咱們發現了waitRead(),而它正是wait()方法的一層封裝。形勢已經清晰明瞭了,咱們陷入了網絡會阻塞在Accept()方法這個思惟定勢中,一直在Accept的調用鏈中追尋不得結果,如今來看,網絡阻塞在Accept()這句話沒錯,可是是對應用層的代碼有效,在系統底層,fd.Accept()完成會返回syscall.EAGAIN這個Err,正是捕捉到這個syscall.EAGAIN,讓咱們的代碼停在waitRead()中,直到有鏈接過來。那麼,wait()方法實際上是對runtimeruntime_pollWait的一層封裝,要知道wait()的具體內容,還要去runtime包中尋找。

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
	err := netpollcheckerr(pd, int32(mode))
	if err != 0 {
		return err
	}
    ...
    //代碼將會阻塞在這裏,若是netpollblock()不返回true,代碼將一直循環。
	for !netpollblock(pd, int32(mode), false) {
	    //循環會不斷檢查是否有錯誤,有錯誤則退出
		err = netpollcheckerr(pd, int32(mode))
		if err != 0 {
			return err
		}
	}
	return 0
}

func netpollcheckerr(pd *pollDesc, mode int32) int {
	if pd.closing {
	    //pd關閉會致使這裏返回 1
		return 1 // errClosing
	}
	if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
		return 2 // errTimeout
	}
	return 0
}
複製代碼

至此咱們已經找出了res=1是出自何處,正是netpollcheckerr()返回了1,致使res接收到了1,從而拋出ErrNetClosing錯誤,這個錯誤致使咱們接收到use of closed netword connection這句日誌。鏈條到此終止,但是咱們如今是隻知其然,而不知其因此然。到底經歷了一個怎樣的過程,致使了pd.closing變成了true?這個錯誤又會不會影響到咱們的業務邏輯,可不能夠忽略?爲何一樣是服務關閉,只有開啓Graceful模式的服務才拋出這個錯誤呢?很明顯要解決這些疑問,咱們要調轉方向,回到ListenAndServe(),找找服務是怎麼被Closed的。

4. Close

func (srv *Server) ListenAndServe() (err error) {
    ...
    // 處理上圖中的熱升級信號(fork子進程),SIGINT、SIGTERM信號(進程結束信號)
	go srv.handleSignals()

    //若是是子進程,getListener()拿到的仍是父進程的監聽器,若是是父進程,建立新的監聽器。
	l, err := srv.getListener(addr)
	if err != nil {
		log.Println(err)
		return err
	}

    //對監聽器作包裝
	srv.GraceListener = newGraceListener(l, srv)
    ...
}

func (srv *Server) handleSignals() {
	var sig os.Signal

	signal.Notify(
		srv.sigChan,
		hookableSignals...,
	)

	pid := syscall.Getpid()
	for {
		sig = <-srv.sigChan
		...
		switch sig {
		case syscall.SIGHUP:
			log.Println(pid, "Received SIGHUP. forking.")
			err := srv.fork()
			...
		case syscall.SIGINT:
			log.Println(pid, "Received SIGINT.")
			srv.shutdown()
		case syscall.SIGTERM:
			log.Println(pid, "Received SIGTERM.")
			srv.shutdown()
		...
	}
}
複製代碼

這裏能夠看到ListenAndServe()被調用時,會啓動一個goroutine,等待系統信號的到來,一旦收到SIGHUP信號,則立刻fork一個子進程;若是收到SIGINT或者SIGTERM信號,則會調用stv.shutdown()關閉鏈接,看看stv.shutdown()作了些什麼:

func (srv *Server) shutdown() {
	...
	srv.state = StateShuttingDown
	if DefaultTimeout >= 0 {
		go srv.serverTimeout(DefaultTimeout)
	}
	err := srv.GraceListener.Close()
	...
}

func (srv *Server) serverTimeout(d time.Duration) {
	...
	time.Sleep(d)
	//當d時間過去後,進程結束休眠,強制將計數器置0
	for {
		if srv.state == StateTerminate {
			break
		}
		//計數器減1
		srv.wg.Done()
	}
}
複製代碼

這裏調用了GraceListenerClose(),前面咱們說過,這個GraceListener其實是對TCPListener的封裝。主要是在Accept()中添加一個計數器,當有鏈接來了,計數器加1,鏈接處理完成,計數器減1。固然,若是網絡有延遲,或者客戶端有鏈接被掛起致使計數器不爲0,通過DefaultTimeout(60秒)後,serverTimeout()會強制把計數器置爲0。那麼,咱們進入Close()方法看看如何關閉監聽。

func (gl *graceListener) Close() error {
	if gl.stopped {
		return syscall.EINVAL
	}
	//簡單的向stop channer 發送nil信號
	gl.stop <- nil
	//等待TCPListener.Close()執行完畢
	return <-gl.stop
}

func newGraceListener(l net.Listener, srv *Server) (el *graceListener) {
	el = &graceListener{
		Listener: l,
		stop:     make(chan error),
		server:   srv,
	}
	//開啓一個goroutine,不阻塞代碼
	go func() {
	    //等待Close()的nil信號
		<-el.stop
		el.stopped = true
		el.stop <- el.Listener.Close()
	}()
	return
}
複製代碼

這裏涉及到goroutine間的通訊,在咱們新建這個GraceListener時,就已經在監聽結束信號了,代碼將在<-el.stop阻塞,直到gl.stop<-nil語句被執行,stoped被置爲true,且調用TCPListener.Close()並等待其執行完畢。那麼TCPListener.Close()又執行了什麼操做?

func (l *TCPListener) Close() error {
	...
	if err := l.close(); err != nil {
		return &OpError{Op: "close", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
	}
	return nil
}

func (ln *TCPListener) close() error {
	return ln.fd.Close()
}
複製代碼

Close()方法是對close()方法的封裝,由於Go中沒有訪問修飾符,方法首字母的大小寫就表明這個方法是公有的仍是私有的。因此Close()-close()也算是Go的特點了。close()方法調用了fd.close(),前面咱們分析過了,這個fd是指文件描述符,也就是當前的Socket鏈接,這個鏈接被關閉,咱們就不能接受新鏈接了。

func (fd *netFD) Close() error {
	runtime.SetFinalizer(fd, nil)
	return fd.pfd.Close()
}

func (fd *FD) Close() error {
	...
	fd.pd.evict()
	return fd.decref()
}

func (pd *pollDesc) evict() {
	if pd.runtimeCtx == 0 {
		return
	}
	runtime_pollUnblock(pd.runtimeCtx)
}
複製代碼

最終仍是調用了runtime_pollUnblock(),直接看runtime包的源碼:

func poll_runtime_pollUnblock(pd *pollDesc) {
	lock(&pd.lock)
	if pd.closing {
		throw("runtime: unblock on closing polldesc")
	}
	pd.closing = true
	...
}
複製代碼

還記得咱們分析Wait過程說的pd.closing,正是它爲true致使了res=1:

//若是netpollblock()不返回true,代碼將一直循環
	for !netpollblock(pd, int32(mode), false) {
	    //循環會不斷檢查是否有錯誤,有錯誤則退出
		err = netpollcheckerr(pd, int32(mode))
	    ...
	}
	---------
	if pd.closing {
	    //pd關閉會致使這裏返回 1
		return 1 // errClosing
	}
複製代碼

如今能夠清晰的瞭解到發生了什麼了,上面的代碼吧pd.closing置爲true,而Accpet()所在的goroutine檢查到這個值發生了改變,因而終止了Accept()過程並報錯,這些goroutine的關係以下圖所示:

goroutine分析

上圖中的三個goroutine,從最右邊的shutdown()開始看起,一步一步的完成關閉流程。最左邊的是Accept()所在的goroutine通常咱們正常結束程序的話,這個goroutine會正常返回。可是咱們爲了實現Graceful模式,還新建了兩個goroutine,正是這兩個新建的goroutine合做把當前的網絡鏈接關閉,Accept()所在的goroutine不瞭解當前情況,覺得是意外的關閉,致使咱們收到那行Err日誌。可是,這種關閉並不會對現有程序有什麼影響,由於是咱們知道是本身主動執行的這個過程。

5. 後記

       Beego日誌的分析過程算是結束了,文章很長,能看到最後的小夥伴都是棒棒的,雖然我中途考慮過度成兩篇,權衡之下仍是做罷,由於這些分析先後相連,也是我思路的文字記錄,若是分開,老是有被強行打斷之感,想必讀者也會以爲麻煩,不如一鼓作氣,刨根問底來的痛快。爲了照顧讀者,以及用手機閱讀的人,此文引用的代碼儘可能精簡,而且在該註釋的地方作了註釋,望能在這信息爆炸的時代,讀者(也包括將來的我)能儘快的從中提取出關鍵信息來。
       在我預計要寫這篇文章時,遠沒有如今這麼長,不過是發現了Accept阻塞時,fd.close()之後系統會報'use of network connection'錯誤,但不影響業務邏輯。可是隨着文章的推動,一個又一個問題不斷冒出來,促使我更深刻的去尋找答案,也算是一個意外的學習過程。也使我最近一直在考慮,追溯一個問題的答案,到底要到何種地步?在編程技術被高度封裝的今天,咱們要窺探到語言底層爲止?仍是操做系統層面?亦或者深刻到彙編,對每一個寄存器,每條指令都有所涉獵?前人和咱們把計算機這座山越堆越高,咱們站在高處,是否有一天會望不到山下的景象了,也許窮盡一輩子也學不盡最底下的東西。最近還據說JS如今被各類封裝,甚至不少語言都是先編譯成JS再運行,有人說JS都快成前端的彙編了。又據說了Flutter,這個框架封裝了Android和iOS的開發流程,提供更高層次的接口兼容兩個平臺的開發。不由有些迷茫,新潮的技術層出不窮,去年RN、Weex還火的不行,今年就出了跨平臺開發的大殺器,年年翻新,還學的過來麼?
       此次的分析過程卻是對這些問題有了一點心得,文中對Accept過程分析到Accept系統調用就沒再往下了,在對wait函數的分析中,分析到了runtime也就中止了,至於那些系統內核的事兒,一律沒有涉及(也不懂)。由於這些現有的分析,足以造成對文章開頭那個問題的解釋,而且得出不會影響現有業務流程的結論。畢竟你的領導,他想要的只有結果。若是你先有的知識能hold住你的工做,那就不必作無謂的涉獵。畢竟學以至用,最終仍是爲了那份薪水服務。若是想要更好的薪水,找到更好的工做,致使能力上hold不住,那天然而然會去主動補充本身。畢竟人生苦短,我用Python少加班,多幹幹本身喜歡的事,多陪陪重要的人。

6. 參考文獻

Golang網絡:核心API實現剖析

epoll 或者 kqueue 的原理是什麼?

直接修改go語言包中的源碼,編譯程序,修改是否生效?

Graceful Restart in Golang

Socket層實現系列 — accept()的實現

相關文章
相關標籤/搜索