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

深入Go語言網(wǎng)絡(luò)庫的基礎(chǔ)實(shí)現(xiàn)

開發(fā) 前端 開發(fā)工具
目前很多高性能的基礎(chǔ)網(wǎng)絡(luò)服務(wù)器都是采用的C語言開發(fā)的,比如:Nginx、Redis、memcached等,它們都是基于”事件驅(qū)動(dòng) + 事件回掉函數(shù)”的方式實(shí)現(xiàn),也就是采用epoll等作為網(wǎng)絡(luò)收發(fā)數(shù)據(jù)包的核心驅(qū)動(dòng)。

Go語言的出現(xiàn),讓我見到了一門語言把網(wǎng)絡(luò)編程這件事情給做“正確”了,當(dāng)然,除了Go語言以外,還有很多語言也把這件事情做”正確”了。我一直堅(jiān)持著這樣的理念——要做"正確"的事情,而不是"高性能"的事情;很多時(shí)候,我們?cè)谧鱿到y(tǒng)設(shè)計(jì)、技術(shù)選型的時(shí)候,都被“高性能”這三個(gè)字給綁架了,當(dāng)然不是說性能不重要,你懂的。

目前很多高性能的基礎(chǔ)網(wǎng)絡(luò)服務(wù)器都是采用的C語言開發(fā)的,比如:Nginx、Redis、memcached等,它們都是基于”事件驅(qū)動(dòng) + 事件回掉函數(shù)”的方式實(shí)現(xiàn),也就是采用epoll等作為網(wǎng)絡(luò)收發(fā)數(shù)據(jù)包的核心驅(qū)動(dòng)。不少人(包括我自己)都認(rèn)為“事件驅(qū)動(dòng) + 事件回掉函數(shù)”的編程方法是“反人類”的;因?yàn)榇蠖鄶?shù)人都更習(xí)慣線性的處理一件事情,做完***件事情再做第二件事情,并不習(xí)慣在N件事情之間頻繁的切換干 活。為了解決程序員在開發(fā)服務(wù)器時(shí)需要自己的大腦不斷的“上下文切換”的問題,Go語言引入了一種用戶態(tài)線程goroutine來取代編寫異步的事件回掉 函數(shù),從而重新回歸到多線程并發(fā)模型的線性、同步的編程方式上。

用Go語言寫一個(gè)最簡(jiǎn)單的echo服務(wù)器:

  1. package main 
  2.  
  3. import ( 
  4.     "log" 
  5.     "net" 
  6.  
  7. func main() { 
  8.     ln, err := net.Listen("tcp"":8080"
  9.     if err != nil { 
  10.             log.Println(err) 
  11.             return 
  12.     } 
  13.     for { 
  14.             conn, err := ln.Accept() 
  15.             if err != nil { 
  16.                 log.Println(err) 
  17.                 continue 
  18.             } 
  19.  
  20.             go echoFunc(conn) 
  21.     } 
  22.  
  23. func echoFunc(c net.Conn) { 
  24.     buf := make([]byte1024
  25.  
  26.     for { 
  27.             n, err := c.Read(buf) 
  28.             if err != nil { 
  29.                 log.Println(err) 
  30.                 return 
  31.             } 
  32.  
  33.             c.Write(buf[:n]) 
  34.     } 

main函數(shù)的過程就是首先創(chuàng)建一個(gè)監(jiān)聽套接字,然后用一個(gè)for循環(huán)不斷的從監(jiān)聽套接字上Accept新的連接,***調(diào)用echoFunc函數(shù)在建立的連接上干活。關(guān)鍵代碼是:

  1. go echoFunc(conn) 

每收到一個(gè)新的連接,就創(chuàng)建一個(gè)“線程”去服務(wù)這個(gè)連接,因此所有的業(yè)務(wù)邏輯都可以同步、順序的編寫到echoFunc函數(shù)中,再也不用去關(guān)心網(wǎng)絡(luò) IO是否會(huì)阻塞的問題。不管業(yè)務(wù)多復(fù)雜,Go語言的并發(fā)服務(wù)器的編程模型都是長(zhǎng)這個(gè)樣子??梢钥隙ǖ氖牵趌inux上Go語言寫的網(wǎng)絡(luò)服務(wù)器也是采用的 epoll作為***層的數(shù)據(jù)收發(fā)驅(qū)動(dòng),Go語言網(wǎng)絡(luò)的底層實(shí)現(xiàn)中同樣存在“上下文切換”的工作,只是這個(gè)切換工作由runtime的調(diào)度器來做了,減少了 程序員的負(fù)擔(dān)。

弄明白網(wǎng)絡(luò)庫的底層實(shí)現(xiàn),貌似只要弄清楚echo服務(wù)器中的Listen、Accept、Read、Write四個(gè)函數(shù)的底層實(shí)現(xiàn)關(guān)系就可以了。本文將采用自底向上的方式來介紹,也就是從***層到上層的方式,這也是我閱讀源碼的方式。底層實(shí)現(xiàn)涉及到的核心源碼文件主要有:
net/fd_unix.go
net/fd_poll_runtime.go
runtime/netpoll.goc
runtime/netpoll_epoll.c
runtime/proc.c (調(diào)度器)

netpoll_epoll.c文件是Linux平臺(tái)使用epoll作為網(wǎng)絡(luò)IO多路復(fù)用的實(shí)現(xiàn)代碼,這份代碼可以了解到epoll相關(guān)的操作(比 如:添加fd到epoll、從epoll刪除fd等),只有4個(gè)函數(shù),分別是runtime·netpollinit、 runtime·netpollopen、runtime·netpollclose和runtime·netpoll。init函數(shù)就是創(chuàng)建epoll 對(duì)象,open函數(shù)就是添加一個(gè)fd到epoll中,close函數(shù)就是從epoll刪除一個(gè)fd,netpoll函數(shù)就是從epoll wait得到所有發(fā)生事件的fd,并將每個(gè)fd對(duì)應(yīng)的goroutine(用戶態(tài)線程)通過鏈表返回。用epoll寫過程序的人應(yīng)該都能理解這份代碼,沒 什么特別之處。

  1. void 
  2. runtime·netpollinit(void
  3.     epfd = runtime·epollcreate1(EPOLL_CLOEXEC); 
  4.     if(epfd >= 0
  5.         return
  6.     epfd = runtime·epollcreate(1024); 
  7.     if(epfd >= 0) { 
  8.         runtime·closeonexec(epfd); 
  9.         return
  10.     } 
  11.     runtime·printf("netpollinit: failed to create descriptor (%d)\n", -epfd); 
  12.     runtime·throw("netpollinit: failed to create descriptor"); 

runtime·netpollinit函數(shù)首先使用runtime·epollcreate1創(chuàng)建epoll實(shí)例,如果沒有創(chuàng)建成功,就換用 runtime·epollcreate再創(chuàng)建一次。這兩個(gè)create函數(shù)分別等價(jià)于glibc的epoll_create1和 epoll_create函數(shù)。只是因?yàn)镚o語言并沒有直接使用glibc,而是自己封裝的系統(tǒng)調(diào)用,但功能是等價(jià)于glibc的??梢酝ㄟ^man手冊(cè)查 看這兩個(gè)create的詳細(xì)信息。

  1. int32 
  2. runtime·netpollopen(uintptr fd, PollDesc *pd) 
  3.     EpollEvent ev; 
  4.     int32 res; 
  5.      
  6.     ev.events = EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET; 
  7.     ev.data = (uint64)pd; 
  8.     res = runtime·epollctl(epfd, EPOLL_CTL_ADD, (int32)fd, &ev); 
  9.     return -res; 

添加fd到epoll中的runtime·netpollopen函數(shù)可以看到每個(gè)fd一開始都關(guān)注了讀寫事件,并且采用的是邊緣觸發(fā),除此之外還關(guān)注了一個(gè)不常見的新事件EPOLLRDHUP,這個(gè)事件是在較新的內(nèi)核版本添加的,目的是解決對(duì)端socket關(guān)閉,epoll本身并不能直接感知到這個(gè)關(guān)閉動(dòng)作的問題。注意任何一個(gè)fd在添加到epoll中的時(shí)候就關(guān)注了EPOLLOUT事件的話,就立馬產(chǎn)生一次寫事件,這次事件可能是多余浪費(fèi)的。

#p#

epoll操作的相關(guān)函數(shù)都會(huì)在事件驅(qū)動(dòng)的抽象層中去調(diào)用,為什么需要這個(gè)抽象層呢?原因很簡(jiǎn)單,因?yàn)镚o語言需要跑在不同的平臺(tái)上,有Linux、Unix、Mac OS X和Windows等,所以需要靠事件驅(qū)動(dòng)的抽象層來為網(wǎng)絡(luò)庫提供一致的接口,從而屏蔽事件驅(qū)動(dòng)的具體平臺(tái)依賴實(shí)現(xiàn)。runtime/netpoll.goc源文件就是整個(gè)事件驅(qū)動(dòng)抽象層的實(shí)現(xiàn),抽象層的核心數(shù)據(jù)結(jié)構(gòu)是:

  1. struct PollDesc 
  2.     PollDesc* link; // in pollcache, protected by pollcache.Lock 
  3.     Lock;       // protectes the following fields 
  4.     uintptr fd; 
  5.     bool    closing; 
  6.     uintptr seq;    // protects from stale timers and ready notifications 
  7.     G*  rg; // G waiting for read or READY (binary semaphore) 
  8.     Timer   rt; // read deadline timer (set if rt.fv != nil) 
  9.     int64   rd; // read deadline 
  10.     G*  wg; // the same for writes 
  11.     Timer   wt; 
  12.     int64   wd; 
  13. }; 

每個(gè)添加到epoll中的fd都對(duì)應(yīng)了一個(gè)PollDesc結(jié)構(gòu)實(shí)例,PollDesc維護(hù)了讀寫此fd的goroutine這一非常重要的信息。可以大膽的推測(cè)一下,網(wǎng)絡(luò)IO讀寫操作的實(shí)現(xiàn)應(yīng)該是:當(dāng)在一個(gè)fd上讀寫遇到EAGAIN錯(cuò)誤的時(shí)候,就將當(dāng)前goroutine存儲(chǔ)到這個(gè)fd對(duì)應(yīng)的PollDesc中,同時(shí)將goroutine給park住,直到這個(gè)fd上再此發(fā)生了讀寫事件后,再將此goroutine給ready激活重新運(yùn)行。事實(shí)上的實(shí)現(xiàn)大概也是這個(gè)樣子的。

事件驅(qū)動(dòng)抽象層主要干的事情就是將具體的事件驅(qū)動(dòng)實(shí)現(xiàn)(比如: epoll)通過統(tǒng)一的接口封裝成Go接口供net庫使用,主要的接口也是:創(chuàng)建事件驅(qū)動(dòng)實(shí)例、添加fd、刪除fd、等待事件以及設(shè)置DeadLine。runtime_pollServerInit負(fù)責(zé)創(chuàng)建事件驅(qū)動(dòng)實(shí)例,runtime_pollOpen將分配一個(gè)PollDesc實(shí)例和fd綁定起來,然后將fd添加到epoll中,runtime_pollClose就是將fd從epoll中刪除,同時(shí)將刪除的fd綁定的PollDesc實(shí)例刪除,runtime_pollWait接口是至關(guān)重要的,這個(gè)接口一般是在非阻塞讀寫發(fā)生EAGAIN錯(cuò)誤的時(shí)候調(diào)用,作用就是park當(dāng)前讀寫的goroutine。

runtime中的epoll事件驅(qū)動(dòng)抽象層其實(shí)在進(jìn)入net庫后,又被封裝了一次,這一次封裝從代碼上看主要是為了方便在純Go語言環(huán)境進(jìn)行操作,net庫中的這次封裝實(shí)現(xiàn)在net/fd_poll_runtime.go文件中,主要是通過pollDesc對(duì)象來實(shí)現(xiàn)的:

  1. type pollDesc struct { 
  2.     runtimeCtx uintptr 

 注意:此處的pollDesc對(duì)象不是上文提到的runtime中的PollDesc,相反此處pollDesc對(duì)象的runtimeCtx成員才是指向的runtime的PollDesc實(shí)例。pollDesc對(duì)象主要就是將runtime的事件驅(qū)動(dòng)抽象層給再封裝了一次,供網(wǎng)絡(luò)fd對(duì)象使用。

  1. var serverInit sync.Once 
  2.  
  3. func (pd *pollDesc) Init(fd *netFD) error { 
  4.     serverInit.Do(runtime_pollServerInit) 
  5.     ctx, errno := runtime_pollOpen(uintptr(fd.sysfd)) 
  6.     if errno != 0 { 
  7.         return syscall.Errno(errno) 
  8.     } 
  9.     pd.runtimeCtx = ctx 
  10.     return nil 

pollDesc對(duì)象最需要關(guān)注的就是其Init方法,這個(gè)方法通過一個(gè)sync.Once變量來調(diào)用了runtime_pollServerInit函數(shù),也就是創(chuàng)建epoll實(shí)例的函數(shù)。意思就是runtime_pollServerInit函數(shù)在整個(gè)進(jìn)程生命周期內(nèi)只會(huì)被調(diào)用一次,也就是只會(huì)創(chuàng)建一次epoll實(shí)例。epoll實(shí)例被創(chuàng)建后,會(huì)調(diào)用runtime_pollOpen函數(shù)將fd添加到epoll中。

網(wǎng)絡(luò)編程中的所有socket fd都是通過netFD對(duì)象實(shí)現(xiàn)的,netFD是對(duì)網(wǎng)絡(luò)IO操作的抽象,linux的實(shí)現(xiàn)在文件net/fd_unix.go中。netFD對(duì)象實(shí)現(xiàn)有自己的init方法,還有完成基本IO操作的Read和Write方法,當(dāng)然除了這三個(gè)方法以外,還有很多非常有用的方法供用戶使用。

  1. // Network file descriptor. 
  2. type netFD struct { 
  3.     // locking/lifetime of sysfd + serialize access to Read and Write methods 
  4.     fdmu fdMutex 
  5.  
  6.     // immutable until Close 
  7.     sysfd       int 
  8.     family      int 
  9.     sotype      int 
  10.     isConnected bool 
  11.     net         string 
  12.     laddr       Addr 
  13.     raddr       Addr 
  14.  
  15.     // wait server 
  16.     pd pollDesc 

通過netFD對(duì)象的定義可以看到每個(gè)fd都關(guān)聯(lián)了一個(gè)pollDesc實(shí)例,通過上文我們知道pollDesc對(duì)象最終是對(duì)epoll的封裝。

  1. func (fd *netFD) init() error { 
  2.     if err := fd.pd.Init(fd); err != nil { 
  3.         return err 
  4.     } 
  5.     return nil 

netFD對(duì)象的init函數(shù)僅僅是調(diào)用了pollDesc實(shí)例的Init函數(shù),作用就是將fd添加到epoll中,如果這個(gè)fd是***個(gè)網(wǎng)絡(luò)socket fd的話,這一次init還會(huì)擔(dān)任創(chuàng)建epoll實(shí)例的任務(wù)。要知道在Go進(jìn)程里,只會(huì)有一個(gè)epoll實(shí)例來管理所有的網(wǎng)絡(luò)socket fd,這個(gè)epoll實(shí)例也就是在***個(gè)網(wǎng)絡(luò)socket fd被創(chuàng)建的時(shí)候所創(chuàng)建。

  1. for { 
  2.     n, err = syscall.Read(int(fd.sysfd), p) 
  3.     if err != nil { 
  4.         n = 0 
  5.         if err == syscall.EAGAIN { 
  6.             if err = fd.pd.WaitRead(); err == nil { 
  7.                 continue 
  8.             } 
  9.         } 
  10.     } 
  11.     err = chkReadErr(n, err, fd) 
  12.     break 

上面代碼段是從netFD的Read方法中摘取,重點(diǎn)關(guān)注這個(gè)for循環(huán)中的syscall.Read調(diào)用的錯(cuò)誤處理。當(dāng)有錯(cuò)誤發(fā)生的時(shí)候,會(huì)檢查這個(gè)錯(cuò)誤是否是syscall.EAGAIN,如果是,則調(diào)用WaitRead將當(dāng)前讀這個(gè)fd的goroutine給park住,直到這個(gè)fd上的讀事件再次發(fā)生為止。當(dāng)這個(gè)socket上有新數(shù)據(jù)到來的時(shí)候,WaitRead調(diào)用返回,繼續(xù)for循環(huán)的執(zhí)行。這樣的實(shí)現(xiàn),就讓調(diào)用netFD的Read的地方變成了同步“阻塞”方式編程,不再是異步非阻塞的編程方式了。netFD的Write方法和Read的實(shí)現(xiàn)原理是一樣的,都是在碰到EAGAIN錯(cuò)誤的時(shí)候?qū)?dāng)前goroutine給park住直到socket再次可寫為止。

本文只是將網(wǎng)絡(luò)庫的底層實(shí)現(xiàn)給大體上引導(dǎo)了一遍,知道底層代碼大概實(shí)現(xiàn)在什么地方,方便結(jié)合源碼深入理解。Go語言中的高并發(fā)、同步阻塞方式編程的關(guān)鍵其實(shí)是”goroutine和調(diào)度器”,針對(duì)網(wǎng)絡(luò)IO的時(shí)候,我們需要知道EAGAIN這個(gè)非常關(guān)鍵的調(diào)度點(diǎn),掌握了這個(gè)調(diào)度點(diǎn),即使沒有調(diào)度器,自己也可以在epoll的基礎(chǔ)上配合協(xié)程等用戶態(tài)線程實(shí)現(xiàn)網(wǎng)絡(luò)IO操作的調(diào)度,達(dá)到同步阻塞編程的目的。

***,為什么需要同步阻塞的方式編程?只有看多、寫多了異步非阻塞代碼的時(shí)候才能夠深切體會(huì)到這個(gè)問題。真正的高大上絕對(duì)不是——“別人不會(huì),我會(huì);別人寫不出來,我寫得出來。”

原文鏈接:http://skoo.me/go/2014/04/21/go-net-core/

責(zé)任編輯:林師授 來源: skoo's notes
相關(guān)推薦

2023-12-30 10:22:57

Go語言函數(shù)開發(fā)

2023-11-30 08:09:02

Go語言

2022-10-24 00:48:58

Go語言errgroup

2023-11-01 08:08:50

Go語言傳遞請(qǐng)求

2021-10-16 17:53:35

Go函數(shù)編程

2024-03-25 07:22:50

GolangMySQL數(shù)據(jù)庫

2021-01-23 12:47:19

MySQL數(shù)據(jù)庫Go語言

2024-01-07 19:54:51

2023-11-06 13:32:38

Go編程

2022-05-09 10:36:05

PythonPyScript開發(fā)者

2017-08-31 11:28:47

Slice底層實(shí)現(xiàn)

2023-01-30 08:16:39

Go語言Map

2024-03-26 00:17:51

Go語言IO

2024-05-29 08:05:15

Go協(xié)程通信

2024-03-29 09:12:43

Go語言工具

2024-02-26 19:38:20

GitHubGo庫Golang

2021-06-09 09:06:52

Go語言算法

2021-02-06 18:19:54

TimeGo語言

2023-04-02 23:13:07

Go語言bufio

2024-04-07 00:04:00

Go語言Map
點(diǎn)贊
收藏

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