本項目地址:gof 一個支持百萬連接的websocket框架
本文提及的內(nèi)容包含在:epoll.go
一、Epoll模型處理數(shù)據(jù)的流程
關于Linux Epoll模型的原理,這里就不在做過多介紹了,在Nginx、Redis等的網(wǎng)絡并發(fā)模型方面,有諸多的詳細解釋。
Epoll模型中,應用是監(jiān)聽Epoll的Wait接口,來等待系統(tǒng)的推送,因此,比之select/poll等的實現(xiàn)方式略微復雜。
在整個Epoll模型的實現(xiàn)中,其主要的內(nèi)容包括:
1、創(chuàng)建一個全局的句柄(以下稱為 GlobalFd),并使用該句柄綁定指定的端口,從而進行數(shù)據(jù)的接收。
2、創(chuàng)建一個全局的Epoll模型,并將我們創(chuàng)建的GlobalFd放到Epoll模型中,讓系統(tǒng)內(nèi)核自己去監(jiān)聽該句柄。
3、調(diào)用Epoll對象的wait方法,去獲取當前Epoll中是否有新的消息。當Epoll推送給我們新的消息時,包含兩種情況:1. 當有新的連接進入到GlobalFd,Epoll模型會通過GlobalFd將消息推送給我們,我們可以通過一個syscall的read函數(shù)去取到對應的連接句柄(Fd),然后同樣將該Fd加入到Epoll對象中。2. 如果Epoll推送給我們的消息句柄不是GlobalFd,那么就說明是某個連接中有了新的消息,這個時候我們就要讀取該消息,并進行對應的處理。
二、用代碼實現(xiàn)我們的Epoll流程
1.創(chuàng)建GlobalFd
我們需要需要創(chuàng)建一個全局的句柄,并且指定一個端口,綁定到這個句柄上,這樣我們的應用就可以進行端口來進行消息的收發(fā)了。
GlobalFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
Log.Error("getScoket err:%v", err.Error())
os.Exit(1)
}
syscall是golang的標準庫,用來調(diào)用操作系統(tǒng)的接口。
syscall.Socket的作用是創(chuàng)建一個socket連接,接收三個參數(shù),分別為:
第一個參數(shù) domain
syscall.AF_INET,表示服務器之間的網(wǎng)絡通信
syscall.AF_UNIX 表示同一臺機器上的進程通信
syscall.AF_INET6 表示以IPv6的方式進行服務器之間的網(wǎng)絡通信
第二個參數(shù) typ
syscall.SOCK_RAW,表示使用原始套接字,可以構建傳輸層的協(xié)議頭部,啟用IP_HDRINCL的話,IP層的協(xié)議頭部也可以構造,就是上面區(qū)分的傳輸層socket和網(wǎng)絡層socket。
syscall.SOCK_STREAM, 基于TCP的socket通信,應用層socket。
syscall.SOCK_DGRAM, 基于UDP的socket通信,應用層socket。
第三個參數(shù) proto
IPPROTO_TCP 接收TCP協(xié)議的數(shù)據(jù)
IPPROTO_IP 接收任何的IP數(shù)據(jù)包
IPPROTO_UDP 接收UDP協(xié)議的數(shù)據(jù)
IPPROTO_ICMP 接收ICMP協(xié)議的數(shù)據(jù)
IPPROTO_RAW 只能用來發(fā)送IP數(shù)據(jù)包,不能接收數(shù)據(jù)。
該接口會返回兩個參數(shù),句柄fd和錯誤信息,如果錯誤信息為空,那么我就可以通過這個句柄去綁定IP。
2. 用GlobalFd綁定IP
綁定IP依然是執(zhí)行syscall中的函數(shù)來進行,首先我們需要確定IP和端口,然后再通過syscall.Listen()方法進行端口綁定。
//監(jiān)聽
addr := syscall.SockaddrInet4{Port: e.port}
ip := "0.0.0.0"
if e.ip != "" {
ip = e.ip
}
copy(addr.Addr[:], net.ParseIP(ip).To4())
if err := syscall.Bind(GlobalFd, &addr); err != nil {
Log.Error("bind err:%v", err.Error())
os.Exit(1)
}
if err := syscall.Listen(GlobalFd, 10); err != nil {
Log.Error("listen err:%v", err.Error())
os.Exit(1)
}
在綁定完成之后,我們就可以通過GlobalFd來進行各種消息的處理了。
3.創(chuàng)建Epoll對象,監(jiān)聽消息
對于句柄的監(jiān)聽,linux有select/poll,epoll等多種管理方式,其中epoll是目前大多數(shù)應用采用的方法。在使用Epoll模型的過程中,首先我們應該創(chuàng)建一個epoll對象。
//創(chuàng)建epfd
epFd, err := syscall.EpollCreate1(0)
Log.Info("getGlobalFd 創(chuàng)建的epfd為:%+v,e.fd:%d", epfd, e.socket)
if err != nil {
Log.Error("epoll_create1 err:%+v", err)
os.Exit(1)
}
通過EpollCreate函數(shù)可以創(chuàng)建一個Epoll對象。在golang中對于epoll對象的創(chuàng)建有兩個函數(shù):EpollCreate(size) 和EpollCreate1(size)。
EpollCreate函數(shù)需要傳入一個size,手動分配可承載句柄的大小。
而隨著linux的更新,epoll對于句柄的承載量變得不再有限制。但是由于linux的兼容性,依然要傳入一個大于等于0的數(shù)字。
4.保存epFd
在創(chuàng)建成功之后,我們需要將epFd保存下來,因為全局只需要實例化這一次epoll對象,以后的所有操作都是基于該對象完成的。
創(chuàng)建一個結(jié)構體,在其中包含以下內(nèi)容:
type EpollObj struct {
socket int //socket連接
epId int //epoll 創(chuàng)建的唯一描述符
ip string //socket監(jiān)聽的地址
port int //socket監(jiān)聽的端口
eventPool *sync.Pool //接收epoll消息
}
socket就是我們創(chuàng)建的GlobalFd句柄。
epId就是我們的Epoll對象的句柄。
ip和port是我們在實例化時需要傳入的ip和端口。
eventPoll在接收消息的時候才會用到,之后再詳細說明。
5.實現(xiàn)epoll對象中句柄的添加、刪除以及消息通知的方法
5.1 實現(xiàn)epoll對象的添加方法。
添加方法是通過syscall中的EpollCtl方法來完成的。
//EpollADD方法,添加、刪除監(jiān)聽的fd
//fd 需要監(jiān)聽的fd對象
//status syscall.EPOLL_CTL_ADD添加
func (e *EpollObj) eAdd(fd int) {
//通過EpollCtl將epfd加入到Epoll中,去監(jiān)聽
if err := syscall.EpollCtl(
e.epId,
syscall.EPOLL_CTL_ADD,
fd,
&syscall.EpollEvent{Events: EPOLLLISTENER, Fd: int32(fd)}); err != nil {
Log.Error("epoll_ctl add err:%+v,fd:%+v", err, fd)
os.Exit(1)
}
}
第一個參數(shù)是我們的Epoll對象的句柄。
第二個參數(shù)是我們的操作方式,添加方法為:syscall.EPOLL_CTL_ADD,
第三個參數(shù)是我們需要加入到Epoll模型中的句柄。
第四個參數(shù)是一個EpollEvent,其中第一個參數(shù)的 EPOLLLISTENER 是指我們需要對于哪些類型的句柄進行監(jiān)聽,第二個參數(shù)為當前的fd。
EPOLLLISTENER = syscall.EPOLLIN | syscall.EPOLLPRI | syscall.EPOLLERR | syscall.EPOLLHUP | unix.EPOLLET
其中,各個參數(shù)的意義為:
EPOLLIN 有可讀數(shù)據(jù)到來。
EPOLLOUT 有數(shù)據(jù)要寫。
EPOLLERR 該文件描述符發(fā)生錯誤。
EPOLLHUP 該文件描述符被掛斷。常見 socket 被關閉(read == 0)。
EPOLLRDHUP 對端已關閉鏈接,或者用 shutdown 關閉了寫鏈接。
EPOLLEXCLUSIVE 唯一喚醒事件,主要為了解決 epoll_wait 驚群問題。多線程下多個 epoll_wait 同時等待,只喚醒一個 epoll_wait 執(zhí)行。 該事件只支持 epoll_ctl 添加操作 EPOLL_CTL_ADD。
EPOLLET 邊緣觸發(fā)模式。
5.2 數(shù)據(jù)刪除操作
直接上代碼:
// syscall.EPOLL_CTL_DEL刪除
func (e *EpollObj) eDel(fd int) {
//通過EpollCtl將epfd加入到Epoll中,去監(jiān)聽
if err := syscall.EpollCtl(
e.epId,
syscall.EPOLL_CTL_DEL,
fd,
&syscall.EpollEvent{Events: EPOLLLISTENER, Fd: int32(fd)}); err != nil {
Log.Error("epoll_ctl del err:%+v,fd:%+v", err, fd)
os.Exit(1)
}
}
刪除操作中只是將syscall.EpollCtl 的第二個參數(shù)更改為:syscall.EPOLL_CTL_DEL。
5.3 接收消息操作
在接收Epoll消息的過程中,我們需要通過 syscall.EpollWait來進行。
該方法是一個阻塞的方法,只有當Epoll檢測到某個fd中有內(nèi)容的時候,才會推送給我們。
但是該方法并不是完全的事件通知模式,因此需要我們手動去獲取Epoll對象中的內(nèi)容。
獲取的流程為:
1、創(chuàng)建一個syscall.EpollEvent的切片,在這里我們通過緩沖池的方式來進行創(chuàng)建,這樣的好處是無需每次進來都分配一塊內(nèi)存,減少系統(tǒng)gc。
2、通過syscall.EpollWait方法來獲取當前是否有新的消息。該方法返回兩個參數(shù),第一個參數(shù)為需要處理的連接個數(shù),第二個為錯誤信息。如果第一個參數(shù)的值大于0,那么我們就可以判定當前epoll中一定是有新消息的。因為epoll每次返回的消息是一個數(shù)組,因此我們需要進行循環(huán)處理。
3、如果新消息的fd是GlobalFd,那么它就是一個新的連接,否則,就是用戶的連接中有了新的內(nèi)容。我們需要分別進行操作。
/*******************************************************************/
//ConnStatus 是一個枚舉類型,定義在common.go中。
//type ConnStatus int
//const (
// CONN_NEW ConnStatus = 1 //新連接
// CONN_CLOSE ConnStatus = 2 //關閉連接
// CONN_MESSAGE ConnStatus = 3 //處理消息
//)
/*******************************************************************/
func (e *EpollObj) eWait(handle func(fd int, connType ConnStatus)) error {
events := e.eventPool.Get().([]syscall.EpollEvent)
defer func() {
events := make([]syscall.EpollEvent, 1024)
e.eventPool.Put(events)
}()
n, err := syscall.EpollWait(e.epId, events[:], -1)
if err != nil {
Log.Error("epoll_wait err:%+v", err)
return err
}
if n > 0 {
fmt.Printf("events fds :%+v\n", events[:5])
}
for i := 0; i < n; i++ {
//如果是系統(tǒng)描述符,就建立一個新的連接
connType := CONN_MESSAGE //默認是讀內(nèi)容
if int(events[i].Fd) == e.socket {
connType = CONN_NEW
}
handle(int(events[i].Fd), connType)
}
return nil
}
通過這三個函數(shù),我們可以實現(xiàn)對于句柄的添加、刪除還有接收消息的操作。