這是一個建立於
的文章,其中的資訊可能已經有所發展或是發生改變。
我們知道, Go語言為並發編程提供了簡潔的編程方式, 你可以以"同步"的編程風格來並發執行代碼, 比如使用go關鍵字新開一個goroutine。 對於網路編程,Go標準庫和運行時內部採用 epoll/kqueue/IoCompletionPort來實現基於 event-loop的網路非同步處理,但是通過netpoll的方式對外提供同步的訪問。具體代碼可以參考 runtime/netpoll、net和internal/poll。
Package poll supports non-blocking I/O on file descriptors with polling.
This supports I/O operations that block only a goroutine, not a thread.
This is used by the net and os packages.
It uses a poller built into the runtime, with support from the
runtime scheduler.
當然,我們平常不會設計到這些封裝的細節,正常使用net包就很方便的開發網路程式了, 但是,如果我們想自己實現基於epoll的 event-loop網路程式呢?
基於epoll的簡單程式
man epoll可以查看epoll的相關介紹。下面這個例子來自tevino, 採用edge-triggered方式處理事件。
它採用 syscall.Socket、syscall.SetNonblock、syscall.Bind、syscall.Listen系統調用來監聽連接埠,然後採用syscall.EpollCreate1、syscall.EpollCtl、syscall.EpollWait來關聯這個監聽的file descriptor, 一旦有新的串連的事件過來,使用syscall.Accept接收串連請求,並對這個串連file descriptor調用syscall.EpollCtl監聽資料事件。一旦串連有資料ready, 調用syscall.Read讀資料,調用syscall.Write寫資料。
來自https://gist.github.com/tevino/3a4f4ec4ea9d0ca66d4f
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
package mainimport ("fmt""net""os""syscall")const (EPOLLET = 1 << 31MaxEpollEvents = 32)func echo(fd int) {defer syscall.Close(fd)var buf [32 * 1024]bytefor {nbytes, e := syscall.Read(fd, buf[:])if nbytes > 0 {fmt.Printf(">>> %s", buf)syscall.Write(fd, buf[:nbytes])fmt.Printf("<<< %s", buf)}if e != nil {break}}}func main() {var event syscall.EpollEventvar events [MaxEpollEvents]syscall.EpollEventfd, err := syscall.Socket(syscall.AF_INET, syscall.O_NONBLOCK|syscall.SOCK_STREAM, 0)if err != nil {fmt.Println(err)os.Exit(1)}defer syscall.Close(fd)if err = syscall.SetNonblock(fd, true); err != nil {fmt.Println("setnonblock1: ", err)os.Exit(1)}addr := syscall.SockaddrInet4{Port: 2000}copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4())syscall.Bind(fd, &addr)syscall.Listen(fd, 10)epfd, e := syscall.EpollCreate1(0)if e != nil {fmt.Println("epoll_create1: ", e)os.Exit(1)}defer syscall.Close(epfd)event.Events = syscall.EPOLLINevent.Fd = int32(fd)if e = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, &event); e != nil {fmt.Println("epoll_ctl: ", e)os.Exit(1)}for {nevents, e := syscall.EpollWait(epfd, events[:], -1)if e != nil {fmt.Println("epoll_wait: ", e)break}for ev := 0; ev < nevents; ev++ {if int(events[ev].Fd) == fd {connFd, _, err := syscall.Accept(fd)if err != nil {fmt.Println("accept: ", err)continue}syscall.SetNonblock(fd, true)event.Events = syscall.EPOLLIN | EPOLLETevent.Fd = int32(connFd)if err := syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, connFd, &event); err != nil {fmt.Print("epoll_ctl: ", connFd, err)os.Exit(1)}} else {go echo(int(events[ev].Fd))}}}} |
上面的基於epoll只是一個簡單的event-loop處理原型,而且在有些平台下(MAC OS)也不能執行,事件的處理也很粗糙,如果你想實現一個完整的event-loop的網路程式, 可以參考下節的庫。
evio
evio是一個效能很高的event-loop網路程式庫,代碼簡單,功能強大。它直接使用 epoll和kqueue系統調用,除了Go標準net庫提供了另外一種思路, 類似libuv和libevent。
這個庫實現redis和haproxy等同的包處理機制,但並不想完全替代標準的net包。對於一個需要長時間啟動並執行請求(大於1毫秒), 比如資料庫訪問、身分識別驗證等,建議還是使用Go net/http庫。
你可能知道, 由很多基於event-loop的程式, 比如Nginx、Haproxy、redis、memcached等,效能都非常不錯,而且它們都是單線程啟動並執行,非常快。
這個庫還有一個好處, 你可以在一個event-loop中處理多個network binding。
一個簡單的例子:
1234567891011121314 |
package mainimport "github.com/tidwall/evio"func main() {var events evio.Eventsevents.Data = func(id int, in []byte) (out []byte, action evio.Action) {out = inreturn}if err := evio.Serve(events, "tcp://localhost:5000", "tcp://192.168.0.10:5001", "tcp://192.168.0.10:5002","unix://socket"); err != nil {panic(err.Error())}} |
作者對效能做了對比,效能非常不錯。