通过redis学网络(1)

您所在的位置:网站首页 epoll模型中怎么把多个cfd返回给上层 通过redis学网络(1)

通过redis学网络(1)

2023-06-10 02:21| 来源: 网络整理| 查看: 265

image.png

本系列主要是为了对redis的网络模型进行学习,我会用golang实现一个reactor网络模型,并实现对redis协议的解析。

系列源码已经上传github

https://github.com/HobbyBear/tinyredis/tree/chapter1

redis的网络模型是基于epoll实现的,所以这一节让我们先基于epoll,实现一个最简单的服务端客户端通信模型。在实现前,先来简单的了解下epoll的原理。

为什么不用golang的原生的netpoll网络框架呢,这是因为netpoll框架虽然底层也是基于epoll实现,但是它提供给开发人员使用网络io方式依然是同步阻塞模式,一个连接单独的拿给一个协程去处理,为了更加真实的感受下redis的网络模型,我们不用netpoll框架,而是自己写一个非阻塞的网络模型。

epoll 网络通信原理

通常情况下服务端的处理客户端请求的逻辑是客户端每发起一个连接,服务端就单独起一个线程去处理这个连接的请求,对于go应用程序而言,则是启用一个协程去处理这个连接。 而采用epoll相关的api后,能够让我们在一个线程或者协程里去处理多个连接的请求。

一个套接字连接对应一个文件描述符,当收到客户端的连接请求时,可以将对应的文件描述符加入到epoll实例关注的事件中去。

在golang里,可以通过syscall.EpollCreate1 去创建一个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 方法参数的epfd则是EpollCreate1 返回的fd,EpollCtl的第二个参数则是代表客户端连接的fd,通过我们在获取到客户端连接后,后续的行为便是查看客户端是否有数据发送过来或者往客户端发送数据,这些在epoll api里用event事件去表示,分别对应了读event和写event,这便是EpollCtl第三个参数所代表的含义。

将这些感兴趣事件添加到epoll实例中后,就代表epoll实例后续会监听这些连接的读写事件的到达,那么读写事件到达后,用户程序又是如何知道的呢,这就要提到epoll相关的另一个api,EpollWait。

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

EpollWait的第二个参数是一个事件数组,用户应用程序调用EpollWait时传入一个固定长度的事件数组,然后EpollWait会将这个数组尽可能填满,这样用户程序便能知道有哪些事件类型到达了,EpollEvent类型如下所示:

type EpollEvent struct { Events uint32 Fd int32 Pad int32 }

其中fd则代表这些事件所关联的客户端连接的fd,通过这个fd,我们便可以对对应连接进行读写操作了。

而Events是个枚举类型,比较常用的枚举以及含义如下:

类型 解释 EPOLLIN 表示文件描述符可读。 EPOLLRDHUP 表示 TCP 连接的远程端点关闭或半关闭连接 EPOLLET 表示使用边缘触发模式来监听事件 EPOLLOUT 表示文件描述符可写 EPOLLERR 表示文件描述符发生错误时发生,这个事件不通过EpollCtl添加也能触发 EPOLLHUP 与EPOLLRDHUP类似同样表示连接关闭,在不支持EPOLLRDHUP的linux版本会触发,这个事件不通过EpollCtl添加也能触发

虽然epoll event还有其他类型,不过一般情况下监控这几种类型就足够了,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创建基于epoll的网络框架

了解完epoll的一些概念以后,现在来看下我们需要实现的网络框架模型是怎样的。我们先实现一个最简单的网络通信框架,客户端发送来消息,然后服务端打印收到的消息。

Pasted image 20230605160424.png

如上图所示,我们收到新的连接后,会调用epoll实例的EpollCtl方法将连接的可读事件添加到epoll实例中,接着调用EpollWait方法等待客户端再次发送消息时,让连接变为可读。

下面是程序的效果测试结果

效果测试

效果演示.png

启动了两个终端,其中右边的终端连接上redis以后,发送了1231,然后左边的终端收到后将收到的消息打印出来。

go代码实现

接着,我们来看看实际代码编写逻辑。

我们定义一个Server的结构体来代表epoll的server。

Conn是对golang原生连接类型net.Conn的包装,。

poll结构体是封装了对epoll api的调用。

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 调用Run方法后,才算Server正式启动了起来。

在Run 方法里,构建监听连接的listener,构建一个epoll实例,用于后续对事件的监听,同时把监听握手连接和处理连接可读数据分成了两个协程分别用accept方法,和handler方法执行。

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) 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循环中判断系统调用syscall.Read 的返回,如果是syscall.EAGAIN 那么会让当前协程睡眠,等待被唤醒。

syscall.EAGAIN 错误是在非阻塞io进行读写时才有可能产生的,在读取数据时,如果发现读缓冲区没有数据到达,则返回这个syscall.EAGAIN错误,在写入数据时,如果写缓冲区满了,也会返回这个错误。

既然golang的net.Conn下的read方法是阻塞的,那么我们就自己实现下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类型实现的Read方法,原生的连接类型是net.Conn,它有一个SyscallConn 能够获取到更加底层的连接类型,从这个类型能够获取到该网络连接的文件描述符fd,我们通过直接调用系统调用syscall.Read来从该网络连接读取数据。 并且碰到错误则直接返回。后续 syscall.EAGAIN错误会交给上层handler方法去进行处理。

总结

这节算是用golang去演示了下如何对epoll api的调用,并且能够实现最简单的客户端服务端通信,下一节我会讲解redis的网络模型是怎么样的,你可以从中了解到经常说的redis的单线程具体是指什么,了解到reactor网络模型是怎样的?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3