欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang基于epoll實(shí)現(xiàn)最簡(jiǎn)單網(wǎng)絡(luò)通信框架

 更新時(shí)間:2023年06月07日 14:04:24   作者:藍(lán)胖子的編程夢(mèng)  
這篇文章主要為大家詳細(xì)介紹了Golang如何基于epoll實(shí)現(xiàn)最簡(jiǎn)單網(wǎng)絡(luò)通信框架,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)學(xué)習(xí)

系列源碼已經(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)文章

  • 談?wù)刧olang的netpoll原理解析

    談?wù)刧olang的netpoll原理解析

    本文詳細(xì)介紹了Go語(yǔ)言中netpoll部分的實(shí)現(xiàn)細(xì)節(jié)和協(xié)程阻塞調(diào)度原理,特別是epoll在Linux環(huán)境下的工作原理,Go語(yǔ)言通過將epoll操作放在runtime包中,結(jié)合運(yùn)行時(shí)調(diào)度功能,實(shí)現(xiàn)了高效的協(xié)程I/O操作,感興趣的朋友跟隨小編一起看看吧
    2024-11-11
  • k8s在go語(yǔ)言中的使用及client?初始化簡(jiǎn)介

    k8s在go語(yǔ)言中的使用及client?初始化簡(jiǎn)介

    這篇文章主要為大家介紹了k8s在go語(yǔ)言中的使用及client?初始化簡(jiǎn)介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-04-04
  • go語(yǔ)言中的map如何解決散列性能下降

    go語(yǔ)言中的map如何解決散列性能下降

    近期對(duì)go語(yǔ)言的map進(jìn)行深入了解和探究,其中關(guān)于map解決大量沖突的擴(kuò)容操作設(shè)計(jì)的十分巧妙,所以筆者特地整理了這篇文章來(lái)探討一下go語(yǔ)言中map如何解決散列性能下降,文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下
    2024-03-03
  • 在Go中使用JSON(附demo)

    在Go中使用JSON(附demo)

    Go開發(fā)人員經(jīng)常需要處理JSON內(nèi)容,本文主要介紹了在Go中使用JSON,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • golang簡(jiǎn)易令牌桶算法實(shí)現(xiàn)代碼

    golang簡(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-04
  • go語(yǔ)言import報(bào)錯(cuò)處理圖文詳解

    go語(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-04
  • Go中的字典Map增刪改查、排序及其值類型

    Go中的字典Map增刪改查、排序及其值類型

    本文詳細(xì)介紹了Go語(yǔ)言中Map的基本概念、聲明初始化、增刪改查操作、反轉(zhuǎn)、排序以及如何判斷鍵是否存在等操作,Map是一種基于鍵值對(duì)的無(wú)序數(shù)據(jù)結(jié)構(gòu),鍵必須是支持相等運(yùn)算符的類型,值可以是任意類型,初始化Map時(shí)推薦指定容量以提高性能
    2024-09-09
  • Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器示例詳解

    Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器示例詳解

    這篇文章主要介紹了Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器的相關(guān)資料,需要的朋友可以參考下
    2017-09-09
  • 圖解Golang的GC垃圾回收算法

    圖解Golang的GC垃圾回收算法

    這篇文章主要介紹了圖解Golang的GC垃圾回收算法,詳細(xì)的介紹了三種經(jīng)典的算法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧
    2019-03-03
  • Go語(yǔ)言之結(jié)構(gòu)體與方法

    Go語(yǔ)言之結(jié)構(gòu)體與方法

    這篇文章主要介紹了Go語(yǔ)言之結(jié)構(gòu)體與方法,結(jié)構(gòu)體是由一系列具有相同類型或不同類型的數(shù)據(jù)構(gòu)成的數(shù)據(jù)集合。下面我們就一起來(lái)學(xué)習(xí)什么是Go語(yǔ)言之結(jié)構(gòu)體
    2021-10-10

最新評(píng)論