Golang基于epoll實(shí)現(xiàn)最簡(jiǎn)單網(wǎng)絡(luò)通信框架
系列源碼已經(jīng)上傳github:https://github.com/HobbyBear/tinyredis/tree/chapter1
redis的網(wǎng)絡(luò)模型是基于epoll實(shí)現(xiàn)的,所以這一節(jié)讓我們先基于epoll,實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的服務(wù)端客戶端通信模型。在實(shí)現(xiàn)前,先來(lái)簡(jiǎn)單的了解下epoll的原理。
為什么不用golang的原生的netpoll網(wǎng)絡(luò)框架呢,這是因?yàn)閚etpoll框架雖然底層也是基于epoll實(shí)現(xiàn),但是它提供給開發(fā)人員使用網(wǎng)絡(luò)io方式依然是同步阻塞模式,一個(gè)連接單獨(dú)的拿給一個(gè)協(xié)程去處理,為了更加真實(shí)的感受下redis的網(wǎng)絡(luò)模型,我們不用netpoll框架,而是自己寫一個(gè)非阻塞的網(wǎng)絡(luò)模型。
epoll 網(wǎng)絡(luò)通信原理
通常情況下服務(wù)端的處理客戶端請(qǐng)求的邏輯是客戶端每發(fā)起一個(gè)連接,服務(wù)端就單獨(dú)起一個(gè)線程去處理這個(gè)連接的請(qǐng)求,對(duì)于go應(yīng)用程序而言,則是啟用一個(gè)協(xié)程去處理這個(gè)連接。 而采用epoll相關(guān)的api后,能夠讓我們?cè)谝粋€(gè)線程或者協(xié)程里去處理多個(gè)連接的請(qǐng)求。
一個(gè)套接字連接對(duì)應(yīng)一個(gè)文件描述符,當(dāng)收到客戶端的連接請(qǐng)求時(shí),可以將對(duì)應(yīng)的文件描述符加入到epoll實(shí)例關(guān)注的事件中去。
在golang里,可以通過syscall.EpollCreate1 去創(chuàng)建一個(gè)epoll實(shí)例。
func EpollCreate1(flag int) (fd int, err error)
其返回結(jié)果的fd就代表epoll實(shí)例的fd,當(dāng)收到客戶端的連接請(qǐng)求時(shí),便可以將客戶端連接的fd,通過EpollCtl 加入到epoll實(shí)例感興趣的事件當(dāng)中。
func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)
EpollCtl 方法參數(shù)的epfd則是EpollCreate1 返回的fd,EpollCtl的第二個(gè)參數(shù)則是代表客戶端連接的fd,通過我們?cè)讷@取到客戶端連接后,后續(xù)的行為便是查看客戶端是否有數(shù)據(jù)發(fā)送過來(lái)或者往客戶端發(fā)送數(shù)據(jù),這些在epoll api里用event事件去表示,分別對(duì)應(yīng)了讀event和寫event,這便是EpollCtl第三個(gè)參數(shù)所代表的含義。
將這些感興趣事件添加到epoll實(shí)例中后,就代表epoll實(shí)例后續(xù)會(huì)監(jiān)聽這些連接的讀寫事件的到達(dá),那么讀寫事件到達(dá)后,用戶程序又是如何知道的呢,這就要提到epoll相關(guān)的另一個(gè)api,EpollWait。
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
EpollWait的第二個(gè)參數(shù)是一個(gè)事件數(shù)組,用戶應(yīng)用程序調(diào)用EpollWait時(shí)傳入一個(gè)固定長(zhǎng)度的事件數(shù)組,然后EpollWait會(huì)將這個(gè)數(shù)組盡可能填滿,這樣用戶程序便能知道有哪些事件類型到達(dá)了,EpollEvent類型如下所示:
type EpollEvent struct { Events uint32 Fd int32 Pad int32 }
其中fd則代表這些事件所關(guān)聯(lián)的客戶端連接的fd,通過這個(gè)fd,我們便可以對(duì)對(duì)應(yīng)連接進(jìn)行讀寫操作了。
而Events是個(gè)枚舉類型,比較常用的枚舉以及含義如下:
類型 | 解釋 |
---|---|
EPOLLIN | 表示文件描述符可讀。 |
EPOLLRDHUP | 表示 TCP 連接的遠(yuǎn)程端點(diǎn)關(guān)閉或半關(guān)閉連接 |
EPOLLET | 表示使用邊緣觸發(fā)模式來(lái)監(jiān)聽事件 |
EPOLLOUT | 表示文件描述符可寫 |
EPOLLERR | 表示文件描述符發(fā)生錯(cuò)誤時(shí)發(fā)生,這個(gè)事件不通過EpollCtl添加也能觸發(fā) |
EPOLLHUP | 與EPOLLRDHUP類似同樣表示連接關(guān)閉,在不支持EPOLLRDHUP的linux版本會(huì)觸發(fā),這個(gè)事件不通過EpollCtl添加也能觸發(fā) |
雖然epoll event還有其他類型,不過一般情況下監(jiān)控這幾種類型就足夠了,golang的netpoll框架在添加連接的文件描述符時(shí)事件時(shí)也只添加了這幾種類型。netpoll的部分源碼如下:
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) }
如何用golang創(chuàng)建基于epoll的網(wǎng)絡(luò)框架
了解完epoll的一些概念以后,現(xiàn)在來(lái)看下我們需要實(shí)現(xiàn)的網(wǎng)絡(luò)框架模型是怎樣的。我們先實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的網(wǎng)絡(luò)通信框架,客戶端發(fā)送來(lái)消息,然后服務(wù)端打印收到的消息。
如上圖所示,我們收到新的連接后,會(huì)調(diào)用epoll實(shí)例的EpollCtl方法將連接的可讀事件添加到epoll實(shí)例中,接著調(diào)用EpollWait方法等待客戶端再次發(fā)送消息時(shí),讓連接變?yōu)榭勺x。
下面是程序的效果測(cè)試結(jié)果
效果測(cè)試
啟動(dòng)了兩個(gè)終端,其中右邊的終端連接上redis以后,發(fā)送了1231,然后左邊的終端收到后將收到的消息打印出來(lái)。
go代碼實(shí)現(xiàn)
接著,我們來(lái)看看實(shí)際代碼編寫邏輯。
我們定義一個(gè)Server的結(jié)構(gòu)體來(lái)代表epoll的server。
Conn是對(duì)golang原生連接類型net.Conn的包裝,。
poll結(jié)構(gòu)體是封裝了對(duì)epoll api的調(diào)用。
type Server struct { Poll *poll addr string listener net.Listener ConnMap sync.Map } type Conn struct { s *Server conn *net.TCPConn nfd int } type poll struct { EpollFd int }
接著來(lái)看下如何啟動(dòng)一個(gè)Server,NewServer是返回一個(gè)Server實(shí)例,Server 調(diào)用Run方法后,才算Server正式啟動(dòng)了起來(lái)。
在Run 方法里,構(gòu)建監(jiān)聽連接的listener,構(gòu)建一個(gè)epoll實(shí)例,用于后續(xù)對(duì)事件的監(jiān)聽,同時(shí)把監(jiān)聽握手連接和處理連接可讀數(shù)據(jù)分成了兩個(gè)協(xié)程分別用accept方法,和handler方法執(zhí)行。
func NewServ(addr string) *Server { return &Server{addr: addr, ConnMap: sync.Map{}} } func (s *Server) Run() error { listener, err := net.Listen("tcp", s.addr) if err != nil { return err } s.listener = listener epollFD, err := syscall.EpollCreate1(0) if err != nil { return err } s.Poll = &poll{EpollFd: epollFD} go s.accept() go s.handler() ch := make(chan int) <-ch return nil }
accept 方法里執(zhí)行的邏輯就是將握手完成的鏈接從全連接隊(duì)列里取出來(lái),將其連接的文件描述符和連接存儲(chǔ)到一個(gè)map里, 然后將對(duì)應(yīng)的文件描述符通過epoll的epollCtl 系統(tǒng)調(diào)用監(jiān)聽它的可讀事件,后續(xù)客戶端再使用這個(gè)連接發(fā)送數(shù)據(jù)時(shí),epoll就能監(jiān)聽到了。
func (s *Server) accept() { for { acceptConn, err := s.listener.Accept() if err != nil { return } var nfd int rawConn, err := acceptConn.(*net.TCPConn).SyscallConn() if err != nil { log.Error(err.Error()) continue } rawConn.Control(func(fd uintptr) { nfd = int(fd) }) // 設(shè)置為非阻塞狀態(tài) err = syscall.SetNonblock(nfd, true) if err != nil { return } err = s.Poll.AddListen(nfd) if err != nil { log.Error(err.Error()) continue } c := &Conn{ conn: acceptConn.(*net.TCPConn), nfd: nfd, s: s, } s.ConnMap.Store(nfd, c) } }
handler里的邏輯則是通過epoll Wait系統(tǒng)調(diào)用等待可讀事件產(chǎn)生,到達(dá)后,根據(jù)事件的文件描述符找到對(duì)應(yīng)連接,然后讀取對(duì)應(yīng)連接的數(shù)據(jù)。
func (s *Server) handler() { for { events, err := s.Poll.WaitEvents() if err != nil { log.Error(err.Error()) continue } for _, e := range events { connInf, ok := s.ConnMap.Load(int(e.FD)) if !ok { continue } conn := connInf.(*Conn) if IsClosedEvent(e.Type) { conn.Close() continue } if IsReadableEvent(e.Type) { buf := make([]byte, 1024) rd, err := conn.Read(buf) if err != nil && err != syscall.EAGAIN { conn.Close() continue } fmt.Println("收到消息", string(buf[:rd])) } } } }
主干代碼是比較容易理解的,但是用golang使用epoll 時(shí)有幾個(gè)點(diǎn) 需要注意下:
第一點(diǎn)是IsReadableEvent 的判斷方式,epoll的每個(gè)event 都有一個(gè)位掩碼,位掩碼是什么意思呢?比如EPOLLIN 的值 是0x1,二進(jìn)制就是00000001,EPOLLHUP 的值是0x10,二進(jìn)制表示是00010000,那么epoll wait系統(tǒng)調(diào)用的event要如何同時(shí)表示同一個(gè)文件描述符同時(shí)擁有這兩個(gè)事件呢? epoll 的event會(huì)將對(duì)應(yīng)的位掩碼設(shè)置為和對(duì)應(yīng)事件一致,比如同時(shí)擁有EPOLLIN和EPOLLHUP,那么event的值將會(huì)是00010001,所以利用與位運(yùn)算是不是就能判斷event是否具有某個(gè)事件了。因?yàn)?只有與1進(jìn)行與運(yùn)算結(jié)果才為1。
func IsReadableEvent(event uint32) bool { if event&syscall.EPOLLIN != 0 { return true } return false }
第二點(diǎn)是如何讀取連接的數(shù)據(jù), 我們后續(xù)要達(dá)到的目的是在同一個(gè)事件循環(huán)里能處理多個(gè)連接,所以要保證讀取連接中的數(shù)據(jù)時(shí)不能阻塞,通過調(diào)用golang的net.Conn下的read方法是阻塞的,其read實(shí)現(xiàn)最終會(huì)調(diào)用到下面的這個(gè)方法。
func (fd *FD) Read(p []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 := ignoringEINTRIO(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 } } } err = fd.eofError(n, err) return n, err } }
這個(gè)方法會(huì)在for循環(huán)中判斷系統(tǒng)調(diào)用syscall.Read 的返回,如果是syscall.EAGAIN 那么會(huì)讓當(dāng)前協(xié)程睡眠,等待被喚醒。
syscall.EAGAIN 錯(cuò)誤是在非阻塞io進(jìn)行讀寫時(shí)才有可能產(chǎn)生的,在讀取數(shù)據(jù)時(shí),如果發(fā)現(xiàn)讀緩沖區(qū)沒有數(shù)據(jù)到達(dá),則返回這個(gè)syscall.EAGAIN錯(cuò)誤,在寫入數(shù)據(jù)時(shí),如果寫緩沖區(qū)滿了,也會(huì)返回這個(gè)錯(cuò)誤。
既然golang的net.Conn下的read方法是阻塞的,那么我們就自己實(shí)現(xiàn)下conn的Read方法。
func (c *Conn) Read(p []byte) (n int, err error) { rawConn, err := c.conn.SyscallConn() if err != nil { return 0, err } rawConn.Read(func(fd uintptr) (done bool) { n, err = syscall.Read(int(fd), p) if err != nil { return true } return true }) return }
的Read方法是我們自定義的Conn類型實(shí)現(xiàn)的Read方法,原生的連接類型是net.Conn,它有一個(gè)SyscallConn 能夠獲取到更加底層的連接類型,從這個(gè)類型能夠獲取到該網(wǎng)絡(luò)連接的文件描述符fd,我們通過直接調(diào)用系統(tǒng)調(diào)用syscall.Read來(lái)從該網(wǎng)絡(luò)連接讀取數(shù)據(jù)。 并且碰到錯(cuò)誤則直接返回。后續(xù) syscall.EAGAIN錯(cuò)誤會(huì)交給上層handler方法去進(jìn)行處理。
總結(jié)
這節(jié)算是用golang去演示了下如何對(duì)epoll api的調(diào)用,并且能夠?qū)崿F(xiàn)最簡(jiǎn)單的客戶端服務(wù)端通信,下一節(jié)我會(huì)講解redis的網(wǎng)絡(luò)模型是怎么樣的,你可以從中了解到經(jīng)常說的redis的單線程具體是指什么,了解到reactor網(wǎng)絡(luò)模型是怎樣的?
到此這篇關(guān)于Golang基于epoll實(shí)現(xiàn)最簡(jiǎn)單網(wǎng)絡(luò)通信框架的文章就介紹到這了,更多相關(guān)Golang epoll網(wǎng)絡(luò)通信框架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
k8s在go語(yǔ)言中的使用及client?初始化簡(jiǎn)介
這篇文章主要為大家介紹了k8s在go語(yǔ)言中的使用及client?初始化簡(jiǎn)介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04golang簡(jiǎn)易令牌桶算法實(shí)現(xiàn)代碼
這篇文章主要介紹了golang簡(jiǎn)易令牌桶算法實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04go語(yǔ)言import報(bào)錯(cuò)處理圖文詳解
今天本來(lái)想嘗試一下go語(yǔ)言中公有和私有的方法,結(jié)果import其他包的時(shí)候直接報(bào)錯(cuò)了,下面這篇文章主要給大家介紹了關(guān)于go語(yǔ)言import報(bào)錯(cuò)處理的相關(guān)資料,需要的朋友可以參考下2023-04-04Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器示例詳解
這篇文章主要介紹了Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器的相關(guān)資料,需要的朋友可以參考下2017-09-09