nginx为什么能干爆那么其它的web服务器,采用epoll可以说是根本原因之一了。epoll甚至单机可以扛住上百万的并发。那么,这篇文章尽可能挑重点的讲一讲epoll,绝不简单的复制粘贴。本文使用的包为golang.org/x/sys/unix,与linux原生函数相差不大。
1 epoll的诞生
从前,一台服务器只能一个链接对应一个客户端链接,一般会起一个线程去处理一个链接。链接多一点,会把服务器爆烂。那大家就奔着复用链接节省资源去发明新东西。所以,在若干个技术迭代之后(例如select, poll),在linux2.6 产生了epoll。
2 epoll的工作流程
- 新建一个
epoll socket - 哟,这里来了一个新的客户端连接
- 将你想处理的新连接的“事件”说明一下,比如发数据、断开等等,并让epoll socket全权处理
- 新连接有“新数据事件”要处理,epoll 去把这个连接相关信息打包,送进一个数组给你处理
- 你开始处理这个数组中的数据
是不是一看上文的流程就明白了,一个epoll socket就处理了许多链接,自然是节约资源了~
2.1 epoll 涉及的函数
别急着看代码,先看看函数定义吧,代码会有的。
新建一个epoll socket
ListenFd, err := unix.Socket(domain, typ, proto int)
第一个参数domain的相关参数如下:
AF_UNIX(本机通信)AF_INET(TCP/IP – IPv4)AF_INET6(TCP/IP – IPv6)
除了AF 外,还有一个PF, AF =
Address FamilyPF =Protocol Family一般情况下,AF_INET=PF_INET,所以,理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx。当然AF_INET和PF_INET的值是相同的,混用也不会有太大的问题。
第二个参数其中 “type”参数指的是套接字类型,常用的类型有:
SOCK_STREAM(TCP流)SOCK_DGRAM(UDP数据报)SOCK_RAW(原始套接字)
第三个参数就是协议,很多的啦,没什么好说的,
哟,这里来了一个新的客户端连接
unix.Listen(ListenFd, n)
unix.Accept(ListenFd)
Listen函数就是侦听ListendFd对应的socket,n就是最大连接数量。 Accpet就是接受来自ListenFD对应的socket的连接。
将你想处理的新连接的“事件”说明一下,比如发数据、断开等等,并让epoll socket全权处理
unix.SetNonblock(conn, 是否要非阻塞)
e := unix.EpollEvent{
Events: 读|写|断开等一些时件,
Fd: 连接过来的fd, // 这里一定不要忘记写!!!!!!!!!!!!!!!!!!!!!!!!
}
unix.EpollCtl(epollFd,增|删|改, 连接, 上面的e)
EPOLL的事件还是非常多的, 如:
EPOLLHUP = 0x10
EPOLLIN = 0x1
EPOLLMSG = 0x400
EPOLLONESHOT = 0x40000000
EPOLLOUT = 0x4
EPOLLPRI = 0x2
EPOLLRDBAND = 0x80
EPOLLRDHUP = 0x2000
EPOLLRDNORM = 0x40
EPOLLWAKEUP = 0x20000000
EPOLLWRBAND = 0x200
我就是列一下,大火看看就行,以后程序中用到的我会特别说明。
新连接有“新数据事件”要处理,epoll 去把这个连接相关信息打包,送进一个数组给你处理
nready, err := unix.EpollWait(epollFd, events数组, 超时时间 )
// events[i].Events, enents[i].Fd 就可以根据事件处理相关连接了捏
// 比如读 unix.Read(int(fd), buf)
// 比如写 unix.Write(int(fd), buf)
2.2 重中之重,工作模式!
一共有两种工作模式:水平触发与边缘触发。他们的区别就是触发的次数,及操作(读,断开)对应的事件(EPOLLIN之类的)不同。
触发次数不同
以读为例,水平触发的情况下,只要缓存中有东西没读完,就是一直在events中添加相关信息通知你读,但是边缘触发的情况下,只会通知你读一次。
epoll中,水平触发与边缘触发的事件异同
比如链接断开事件,在水平触发中,会触发EPOLLHUP与EPOLLIN两个事件。
来自文心一言的总结:
相同点:
- 两种触发模式都会在文件描述符就绪时触发相应的事件。
- 两种触发模式都会在文件描述符非就绪时不会触发相应的事件。
不同点:
- 水平触发(LT):只要满足条件(例如读缓冲区有数据),就会触发一个事件。在这种模式下,如果文件描述符关联的读内核缓冲区由空转化为非空,就会发出可读信号进行通知。如果文件描述符关联的内核写缓冲区由满转化为不满,就会发出可写信号进行通知。这种模式的优点是编程出错误可能性要小一点,因为内核会告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
- 边缘触发(ET):每当状态发生变化时就触发一个事件。在这种模式下,如果文件描述符关联的读内核缓冲区由空转化为非空,就会发出可读信号进行通知;如果文件描述符关联的内核写缓冲区由满转化为不满,就会发出可写信号进行通知。与水平触发不同,边缘触发仅在状态发生变化时触发一次,比如当文件描述符由非就绪状态变为就绪状态时。这种模式可能会导致在处理事件时出现一些问题,因为如果错过了第一次触发,那么事件可能会被漏掉。
综上所述,选择使用哪种触发模式取决于具体的使用场景和需求。如果需要更可靠的通知机制,可以使用水平触发;如果需要更高的响应速度,可以使用边缘触发。
如何设置工作模式
很多文章都提到了工作模式,但是怎么在代码中设置工作模式不太明显。在事件注册的时候,在事件里加一个| unix.EPOLLET
e := unix.EpollEvent{
Events: unix.EPOLLPRI | unix.EPOLLIN | unix.EPOLLRDHUP | unix.EPOLLET,
Fd: int32(conn),
}
- 边缘触发
将socket 设计为non-block(即
unix.SetNonblock(conn, 是否要非阻塞)),然后监听特定的事件! - 水平触发 无所谓