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

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

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

系列源碼已經(jīng)上傳github:https://github.com/HobbyBear/tinyredis/tree/chapter1

redis的網(wǎng)絡模型是基于epoll實現(xiàn)的,所以這一節(jié)讓我們先基于epoll,實現(xiàn)一個最簡單的服務端客戶端通信模型。在實現(xiàn)前,先來簡單的了解下epoll的原理。

為什么不用golang的原生的netpoll網(wǎng)絡框架呢,這是因為netpoll框架雖然底層也是基于epoll實現(xiàn),但是它提供給開發(fā)人員使用網(wǎng)絡io方式依然是同步阻塞模式,一個連接單獨的拿給一個協(xié)程去處理,為了更加真實的感受下redis的網(wǎng)絡模型,我們不用netpoll框架,而是自己寫一個非阻塞的網(wǎng)絡模型。

epoll 網(wǎng)絡通信原理

通常情況下服務端的處理客戶端請求的邏輯是客戶端每發(fā)起一個連接,服務端就單獨起一個線程去處理這個連接的請求,對于go應用程序而言,則是啟用一個協(xié)程去處理這個連接。 而采用epoll相關的api后,能夠讓我們在一個線程或者協(xié)程里去處理多個連接的請求。

一個套接字連接對應一個文件描述符,當收到客戶端的連接請求時,可以將對應的文件描述符加入到epoll實例關注的事件中去。

在golang里,可以通過syscall.EpollCreate1 去創(chuàng)建一個epoll實例。

func EpollCreate1(flag int) (fd int, err error) 

其返回結果的fd就代表epoll實例的fd,當收到客戶端的連接請求時,便可以將客戶端連接的fd,通過EpollCtl 加入到epoll實例感興趣的事件當中。

func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) 

EpollCtl 方法參數(shù)的epfd則是EpollCreate1 返回的fd,EpollCtl的第二個參數(shù)則是代表客戶端連接的fd,通過我們在獲取到客戶端連接后,后續(xù)的行為便是查看客戶端是否有數(shù)據(jù)發(fā)送過來或者往客戶端發(fā)送數(shù)據(jù),這些在epoll api里用event事件去表示,分別對應了讀event和寫event,這便是EpollCtl第三個參數(shù)所代表的含義。

將這些感興趣事件添加到epoll實例中后,就代表epoll實例后續(xù)會監(jiān)聽這些連接的讀寫事件的到達,那么讀寫事件到達后,用戶程序又是如何知道的呢,這就要提到epoll相關的另一個api,EpollWait。

func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) 

EpollWait的第二個參數(shù)是一個事件數(shù)組,用戶應用程序調(diào)用EpollWait時傳入一個固定長度的事件數(shù)組,然后EpollWait會將這個數(shù)組盡可能填滿,這樣用戶程序便能知道有哪些事件類型到達了,EpollEvent類型如下所示:

type EpollEvent struct {
	Events uint32
	Fd     int32
	Pad    int32
}

其中fd則代表這些事件所關聯(lián)的客戶端連接的fd,通過這個fd,我們便可以對對應連接進行讀寫操作了。

而Events是個枚舉類型,比較常用的枚舉以及含義如下:

類型解釋
EPOLLIN表示文件描述符可讀。
EPOLLRDHUP表示 TCP 連接的遠程端點關閉或半關閉連接
EPOLLET表示使用邊緣觸發(fā)模式來監(jiān)聽事件
EPOLLOUT表示文件描述符可寫
EPOLLERR表示文件描述符發(fā)生錯誤時發(fā)生,這個事件不通過EpollCtl添加也能觸發(fā)
EPOLLHUP與EPOLLRDHUP類似同樣表示連接關閉,在不支持EPOLLRDHUP的linux版本會觸發(fā),這個事件不通過EpollCtl添加也能觸發(fā)

雖然epoll event還有其他類型,不過一般情況下監(jiān)控這幾種類型就足夠了,golang的netpoll框架在添加連接的文件描述符時事件時也只添加了這幾種類型。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)絡框架

了解完epoll的一些概念以后,現(xiàn)在來看下我們需要實現(xiàn)的網(wǎng)絡框架模型是怎樣的。我們先實現(xiàn)一個最簡單的網(wǎng)絡通信框架,客戶端發(fā)送來消息,然后服務端打印收到的消息。

如上圖所示,我們收到新的連接后,會調(diào)用epoll實例的EpollCtl方法將連接的可讀事件添加到epoll實例中,接著調(diào)用EpollWait方法等待客戶端再次發(fā)送消息時,讓連接變?yōu)榭勺x。

下面是程序的效果測試結果

效果測試

啟動了兩個終端,其中右邊的終端連接上redis以后,發(fā)送了1231,然后左邊的終端收到后將收到的消息打印出來。

go代碼實現(xiàn)

接著,我們來看看實際代碼編寫邏輯。

我們定義一個Server的結構體來代表epoll的server。

Conn是對golang原生連接類型net.Conn的包裝,。

poll結構體是封裝了對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
}

接著來看下如何啟動一個Server,NewServer是返回一個Server實例,Server 調(diào)用Run方法后,才算Server正式啟動了起來。

在Run 方法里,構建監(jiān)聽連接的listener,構建一個epoll實例,用于后續(xù)對事件的監(jiān)聽,同時把監(jiān)聽握手連接和處理連接可讀數(shù)據(jù)分成了兩個協(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í)行的邏輯就是將握手完成的鏈接從全連接隊列里取出來,將其連接的文件描述符和連接存儲到一個map里, 然后將對應的文件描述符通過epoll的epollCtl 系統(tǒng)調(diào)用監(jiān)聽它的可讀事件,后續(xù)客戶端再使用這個連接發(fā)送數(shù)據(jù)時,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)  
      })  
      // 設置為非阻塞狀態(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)生,到達后,根據(jù)事件的文件描述符找到對應連接,然后讀取對應連接的數(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 時有幾個點 需要注意下:

第一點是IsReadableEvent 的判斷方式,epoll的每個event 都有一個位掩碼,位掩碼是什么意思呢?比如EPOLLIN 的值 是0x1,二進制就是00000001,EPOLLHUP 的值是0x10,二進制表示是00010000,那么epoll wait系統(tǒng)調(diào)用的event要如何同時表示同一個文件描述符同時擁有這兩個事件呢? epoll 的event會將對應的位掩碼設置為和對應事件一致,比如同時擁有EPOLLIN和EPOLLHUP,那么event的值將會是00010001,所以利用與位運算是不是就能判斷event是否具有某個事件了。因為1只有與1進行與運算結果才為1。

func IsReadableEvent(event uint32) bool {
	if event&syscall.EPOLLIN != 0 {
		return true
	}
	return false
}

第二點是如何讀取連接的數(shù)據(jù), 我們后續(xù)要達到的目的是在同一個事件循環(huán)里能處理多個連接,所以要保證讀取連接中的數(shù)據(jù)時不能阻塞,通過調(diào)用golang的net.Conn下的read方法是阻塞的,其read實現(xiàn)最終會調(diào)用到下面的這個方法。

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  
   }  
}

這個方法會在for循環(huán)中判斷系統(tǒng)調(diào)用syscall.Read 的返回,如果是syscall.EAGAIN 那么會讓當前協(xié)程睡眠,等待被喚醒。

syscall.EAGAIN 錯誤是在非阻塞io進行讀寫時才有可能產(chǎn)生的,在讀取數(shù)據(jù)時,如果發(fā)現(xiàn)讀緩沖區(qū)沒有數(shù)據(jù)到達,則返回這個syscall.EAGAIN錯誤,在寫入數(shù)據(jù)時,如果寫緩沖區(qū)滿了,也會返回這個錯誤。

既然golang的net.Conn下的read方法是阻塞的,那么我們就自己實現(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類型實現(xiàn)的Read方法,原生的連接類型是net.Conn,它有一個SyscallConn 能夠獲取到更加底層的連接類型,從這個類型能夠獲取到該網(wǎng)絡連接的文件描述符fd,我們通過直接調(diào)用系統(tǒng)調(diào)用syscall.Read來從該網(wǎng)絡連接讀取數(shù)據(jù)。 并且碰到錯誤則直接返回。后續(xù) syscall.EAGAIN錯誤會交給上層handler方法去進行處理。

總結

這節(jié)算是用golang去演示了下如何對epoll api的調(diào)用,并且能夠實現(xiàn)最簡單的客戶端服務端通信,下一節(jié)我會講解redis的網(wǎng)絡模型是怎么樣的,你可以從中了解到經(jīng)常說的redis的單線程具體是指什么,了解到reactor網(wǎng)絡模型是怎樣的?

到此這篇關于Golang基于epoll實現(xiàn)最簡單網(wǎng)絡通信框架的文章就介紹到這了,更多相關Golang epoll網(wǎng)絡通信框架內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 談談golang的netpoll原理解析

    談談golang的netpoll原理解析

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

    k8s在go語言中的使用及client?初始化簡介

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

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

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

    在Go中使用JSON(附demo)

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

    golang簡易令牌桶算法實現(xiàn)代碼

    這篇文章主要介紹了golang簡易令牌桶算法實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-04-04
  • go語言import報錯處理圖文詳解

    go語言import報錯處理圖文詳解

    今天本來想嘗試一下go語言中公有和私有的方法,結果import其他包的時候直接報錯了,下面這篇文章主要給大家介紹了關于go語言import報錯處理的相關資料,需要的朋友可以參考下
    2023-04-04
  • Go中的字典Map增刪改查、排序及其值類型

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

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

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

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

    圖解Golang的GC垃圾回收算法

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

    Go語言之結構體與方法

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

最新評論