当前位置:首页 > 杂谈 > 正文内容

通过redis学网络(1)-用go基于epoll实现最简单网络通信框架-redis的eval方法

2023-08-02 08:47:15TONY杂谈59

本系列主要是为了对redis的网络模型进行学习,我会用golang实现一个reactor网络模型,并实现对redis协议的解析。系列源码已经上传githubhttps://github.com/HobbyBear/tinyredis/tree/chapter1redis的网络模型是基于epoll实现的,所以这一节让我们先基于epoll,实现一个最简单的服务端客户端通信模型。在实现前,先来简单的了解下epoll的原理。为什么不用golang的原生的netpoll网络框架呢,这是因为netpoll框架虽然底层也是基于epoll实现,但是它提供给开发人员使用网络io方式依然是同步阻塞模式,一个连接单独的拿给一个协程去处理,为了更加真实的感受下redis的网络模型,我们不用netpoll框架,而是自己写一个非阻塞的网络模型。epoll 网络通信原理通常情况下服务端的处理客户端请求的逻辑是客户端每发起一个连接,服务端就单独起一个线程去处理这个连接的请求,对于go应用程序而言,则是启用一个协程去处理这个连接。而采用epoll相关的api后,能够让我们在一个线程或者协程里去处理多个连接的请求。一个套接字连接对应一个文件描述符,当收到客户端的连接请求时,可以将对应的文件描述符加入到epoll实例关注的事件中去。在golang里,可以通过syscall.EpollCreate1去创建一个epoll实例。funcEpollCreate1(flag int)(fd int, err error)其返回结果的fd就代表epoll实例的fd,当收到客户端的连接请求时,便可以将客户端连接的fd,通过EpollCtl 加入到epoll实例感兴趣的事件当中。funcEpollCtl(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。funcEpollWait(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是个枚举类型,比较常用的枚举以及含义如下:

虽然epoll event还有其他类型,不过一般情况下监控这几种类型就足够了,golang的netpoll框架在添加连接的文件描述符时事件时也只添加了这几种类型。netpoll的部分源码如下:funcnetpollopen(fd uintptr, pd *pollDesc)int32{var ev epollevent ev.events = EPOLLIN EPOLLOUT EPOLLRDHUP EPOLLET *(**pollDesc)(unsafe.Pointer(&ev.data))= pdreturn -epollctl(epfd, EPOLLCTLADD, 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方法执行。funcNewServ(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() if err != nil { return err } s.Poll =&poll{EpollFd: epollFD} go s.accept() go s.handler() ch := make(chanint)<-ch returnnil}accept 方法里执行的逻辑就是将握手完成的链接从全连接队列里取出来,将其连接的文件描述符和连接存储到一个map里,然后将对应的文件描述符通过epoll的epollCtl 系统调用监听它的可读事件,后续客户端再使用这个连接发送数据时,epoll就能监听到了。func(s *Server)accept(){ for { acceptConn, err := s.listener.Accept() if err != nil { return } var nfd int rawConn, err := acceptConn.(*net.TCPConn).SyscallConn() if err != nil { log.Error(err.Error()) continue } rawConn.Control(func(fd uintptr){ nfd = int(fd)})//设置为非阻塞状态 err = syscall.SetNonblock(nfd, true) if err != nil { return } err = s.Poll.AddListen(nfd) if err != nil { log.Error(err.Error()) continue } c :=&Conn{ conn: acceptConn.(*net.TCPConn), nfd: nfd, s: s,} s.ConnMap.Store(nfd, c)} }handler里的逻辑则是通过epoll Wait系统调用等待可读事件产生,到达后,根据事件的文件描述符找到对应连接,然后读取对应连接的数据。func(s *Server)handler(){ for { events, err := s.Poll.WaitEvents() if err != nil { log.Error(err.Error()) continue } for , e := range events { connInf, ok := s.ConnMap.Load(int(e.FD)) if !ok { continue } conn := connInf.(*Conn) if IsClosedEvent(e.Type){ conn.Close() continue } if IsReadableEvent(e.Type){ buf := make([]byte,1024) rd, err := conn.Read(buf) if err != nil && err != syscall.EAGAIN { conn.Close() continue } fmt.Println("收到消息", string(buf[:rd]))} }} }主干代码是比较容易理解的,但是用golang使用epoll 时有几个点需要注意下:第一点是IsReadableEvent 的判断方式,epoll的每个event 都有一个位掩码,位掩码是什么意思呢?比如EPOLLIN 的值是0x1,二进制就是00000001,EPOLLHUP 的值是0x10,二进制表示是00010000,那么epoll wait系统调用的event要如何同时表示同一个文件描述符同时拥有这两个事件呢? epoll 的event会将对应的位掩码设置为和对应事件一致,比如同时拥有EPOLLIN和EPOLLHUP,那么event的值将会是00010001,所以利用与位运算是不是就能判断event是否具有某个事件了。因为1只有与1进行与运算结果才为1。funcIsReadableEvent(event uint32)bool {if event&syscall.EPOLLIN !={returntrue }returnfalse}第二点是如何读取连接的数据,我们后续要达到的目的是在同一个事件循环里能处理多个连接,所以要保证读取连接中的数据时不能阻塞,通过调用golang的net.Conn下的read方法是阻塞的,其read实现最终会调用到下面[gf]1f447[/gf][gf]1f3fb[/gf][gf]1f447[/gf][gf]1f3fb[/gf][gf]1f447[/gf][gf]1f3fb[/gf]的这个方法。func(fd *FD)Read(p []byte)(int, error){ if err := fd.readLock(); err != nil { return, err } defer fd.readUnlock() iflen(p)=={ // If the caller wanted a zero byte read, return immediately // without trying (but after acquiring the readLock).// Otherwise syscall.Read returns 0, nil which looks like // io.EOF.// TODO(bradfitz): make it wait for readability?(Issue 15735) return 0, nil } if err := fd.pd.prepareRead(fd.isFile); err != nil { return, err } if fd.IsStream && len(p)> maxRW { p = p[:maxRW]} for { n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p) if err != nil { n = 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, err } rawConn.Read(func(fd uintptr)(done bool){ n, err = syscall.Read(int(fd), p) if err != nil { returntrue } returntrue }) return}[gf]1f446[/gf][gf]1f3fb[/gf][gf]1f446[/gf][gf]1f3fb[/gf]的Read方法是我们自定义的Conn类型实现的Read方法,原生的连接类型是net.Conn,它有一个SyscallConn 能够获取到更加底层的连接类型,从这个类型能够获取到该网络连接的文件描述符fd,我们通过直接调用系统调用syscall.Read来从该网络连接读取数据。并且碰到错误则直接返回。后续 syscall.EAGAIN错误会交给上层handler方法去进行处理。总结这节算是用golang去演示了下如何对epoll api的调用,并且能够实现最简单的客户端服务端通信,下一节我会讲解redis的网络模型是怎么样的,你可以从中了解到经常说的redis的单线程具体是指什么,了解到reactor网络模型是怎样的?

“通过redis学网络(1)-用go基于epoll实现最简单网络通信框架-redis的eval方法” 的相关文章

申通快递:公司尚未掌握您提到的信息

申通快递:公司尚未掌握您提到的信息

  申通快递(002468)04月04日在投资者关系平台上答复了投资者关心的问题。   投资者:请问申通快递有自己的网点智能客服机器人聊天系统吗?谢谢   申通快递董秘:您好,感谢您的关注,公司自主研发的申小蜜机器人,是专门为网点客服打造的集商家管理、售后问题件服务等于一体...

俄罗斯卢布汇率4月上涨35%,成全球表现最佳的货币

俄罗斯卢布汇率4月上涨35%,成全球表现最佳的货币

KlipC报道:俄罗斯卢布兑美元16日升穿64,兑欧元汇率攀升至近五年高位,俄罗斯央行初步估算,卢布汇率在4月已经上涨34%,俄罗斯央行还预计,2022年俄罗斯GDP将下降8%到10%。...

个体店和企业店官方账号如何操作成为混合经营账号

个体店和企业店官方账号如何操作成为混合经营账号

个体店和企业店官方账号如何操作成为混合经营账号第一步:进入抖店官方账号页面,选择模式操作入口:【抖店电脑端-店铺-经营账号管理-官方账号】 顶部出现文案提醒:账号经营模式选择,点击按钮【选择模式】 第二步:进入账号经营模式选择页面,仔细查看每一种模式的差异第三步:根据经营诉...

抖店登陆流程分享,新手也能轻松入驻

抖店登陆流程分享,新手也能轻松入驻

根据《2022年国货市场发展报告》相关数据显示,九成消费者看好国货发展,半数左右消费者认可国货的高性价比和产品质量。国货的崛起,影响因素是方方面面的。中国的日益强大,我们的民族自信、文化自信越来越强;同时,技术的发展,产业升级,市场监管规范,也让消费者对国货品牌信心越来越足。根据抖音电商商...

首届抖音生活服务生态伙伴大会在蓉举办:让好生态助力好生意

首届抖音生活服务生态伙伴大会在蓉举办:让好生态助力好生意

  在抖音生活服务,服务商、机构、达人、商家等多个角色,共同形成了一个彼此关联的生态体系。好生态才能成就好生意,如何制定公平公正的规则制度、打造扎实有效的产品工具、共建健康有序的经营环境,成为生意长效发展的关键。   4月25日,2023抖音生活服务生态伙伴大会在成都举行...

PCBA生产制造过程中的助焊剂你了解多少?

PCBA生产制造过程中的助焊剂你了解多少?

助焊剂助焊剂是指在焊接工艺中能帮助和促进焊接过程,同时具有保护作用、阻止氧化反应的化学物质。在SMT贴片加工中,锡膏里的助焊剂是必不可少的,适当的助焊剂不仅能去除氧化物,防止金属表面再氧化,而且能提高可焊性,促进能量传递到焊接区域。  助焊剂的种类电子产品的组装与维修中常用的有松...