大家好,我是皮皮
今天皮皮和大家分享一下如何自己设计实现epoll调用
epoll的介绍:
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率
为什么要自己实现epoll:
go官方本身就提供了一个net包,然后底层也是用的epoll,但是,采用的是来一个链接就启动一个goroutine,对于短链接来说,可以通过goroutine池进行复用,可是对于长链接服务或者工具如rpc等,如果继续使用官方自带的net库就会有大量的goroutine进行常驻,熟悉go的调度模型mpg就会知道,goroutine并不是越多,速度就越快。当goroutine以几十万或者上百万个的时候程序就会非常缓慢。不仅仅如此,因为goroutine是属于有栈协程,所以会消耗大量的内存。所以对于高并发流量或者大量长连接服务场景来说,go的net包就不是特别的适合,所以需要单独开发调度epoll,只需要几十个goroutine就可以维护几十万上百万链接以及百万流量,可以大大节省内存资源以及延迟消耗
开始使用,写一个小例子
go get "golang.org/x/sys/unix"
复制代码
然后注意下面epoll的三个方法
unix.EpollCreate1
unix.EpollCtl
unix.EpollWait
复制代码
第一个方法是创建一个epoll的句柄
第二个方法是把需要监听的句柄加入到epoll中
第三个方法是等待事件的到来
然后看下面的demo
package main
import (
"fmt"
"golang.org/x/sys/unix"
"net"
"runtime"
)
type eventList struct {
size int
events []unix.EpollEvent
}
func newEventList(size int) *eventList {
return &eventList{size, make([]unix.EpollEvent, size)}
}
func main(){
run()
}
func run() {
// 获取是tcp的listenFd
ListenFd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, unix.IPPROTO_TCP)
if err != nil {
fmt.Println("create socket err", err)
return
}
err = unix.SetsockoptInt(ListenFd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
fmt.Println("set socket err", err)
return
}
tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:9091")
if err != nil {
fmt.Println("resove err", err)
return
}
sa := &unix.SockaddrInet4{
Port: tcpAddr.Port,
}
// 绑定的端口
err = unix.Bind(ListenFd, sa)
if err != nil {
fmt.Println("socket bind err", err)
return
}
var n int
if n > 1<<16-1 {
n = 1<<16 - 1
}
// 监听服务
err = unix.Listen(ListenFd, n)
if err != nil {
fmt.Println("listen err", err)
return
}
// 开启epoll
el := newEventList(128)
epollFd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)
if err != nil {
unix.Close(epollFd)
fmt.Println("create fd err", err)
return
}
go func() {
buf := make([]byte,1024)
var msec = -1
for {
// 有读写事件到来就会得到通知
nready, err := unix.EpollWait(epollFd, el.events, msec)
if nready <= 0 {
msec = -1
runtime.Gosched()
continue
}
msec = 0
if err != nil {
if err == unix.EINTR {
continue
}
fmt.Println("listenner wait err", err)
return
}
for i := 0; i < nready; i++ {
if el.events[i].Events & unix.EPOLLERR | unix.EPOLLHUP | unix.EPOLLRDHUP | unix.EPOLLIN | unix.EPOLLPRI != 0 {
fd := el.events[i].Fd
n,err := unix.Read(int(fd),buf)
if err != nil {
unix.Close(int(el.events[i].Fd))
break
}
if n > 0 {
ev := unix.EpollEvent{
Events: unix.EPOLLOUT,
Fd: fd,
}
if err := unix.EpollCtl(epollFd,unix.EPOLL_CTL_MOD,int(fd),&ev);err != nil{
fmt.Println("get data err")
continue
}
fmt.Println("[suc]",string(buf[:n]))
}
if n <= 0 {
ev := unix.EpollEvent{
Events: unix.EPOLLOUT|unix.EPOLLIN|unix.EPOLLERR|unix.EPOLLHUP,
Fd: fd,
}
if err := unix.EpollCtl(epollFd,unix.EPOLL_CTL_DEL,int(fd),&ev);err != nil{
fmt.Println("close epoll err")
return
}
unix.Close(int(fd))
}
}
}
}
}()
for {
// 获取连接
conn, _, err := unix.Accept(ListenFd)
if err != nil {
fmt.Println("accept err", err)
return
}
err = unix.SetNonblock(conn, true)
if err != nil {
fmt.Println("block err", err)
return
}
ev := unix.EpollEvent{}
ev.Fd = int32(conn)
ev.Events = unix.EPOLLPRI | unix.EPOLLIN
// 把链接注册到epoll中
err = unix.EpollCtl(epollFd, unix.EPOLL_CTL_ADD, conn, &ev)
if err != nil {
fmt.Println("epoll ctl err",err)
return
}
}
}
复制代码
总结:
看了上面的代码总结一下:
1、创建socket
2、绑定创建的socket
3、开始监听服务
4、开启epoll
5、接收链接注册到epoll中
6、有读写事件的时候就收到通知,进行相应的操作
我们可以写一个客户端模拟一下
package main
import (
"fmt"
"net"
)
var (
addr = "127.0.0.1:9091"
)
func main(){
conn,err := net.Dial("tcp",addr)
if err != nil {
fmt.Println("dial err",err)
return
}
conn.Write([]byte("hello world"))
}
复制代码
好了,今天皮皮就分享到这里了
更多硬核技术分享,欢迎关注我的公众号,DevOps云计算
欢迎前来加群讨论 723597617