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

这篇具有很好参考价值的文章主要介绍了通过redis学网络(1)-用go基于epoll实现最简单网络通信框架。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

本系列主要是为了对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的一些概念以后,现在来看下我们需要实现的网络框架模型是怎样的。我们先实现一个最简单的网络通信框架,客户端发送来消息,然后服务端打印收到的消息。

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

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

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

效果测试

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

启动了两个终端,其中右边的终端连接上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)  
   <-ch  
   return nil  
}

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。

func IsReadableEvent(event uint32) bool {
	if event&syscall.EPOLLIN != 0 {
		return true
	}
	return false
}

第二点是如何读取连接的数据, 我们后续要达到的目的是在同一个事件循环里能处理多个连接,所以要保证读取连接中的数据时不能阻塞,通过调用golang的net.Conn下的read方法是阻塞的,其read实现最终会调用到下面👇🏻👇🏻👇🏻的这个方法。

func (fd *FD) Read(p []byte) (int, error) {  
   if err := fd.readLock(); err != nil {  
      return 0, err  
   }  
   defer fd.readUnlock()  
   if len(p) == 0 {  
      // 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 0, err  
   }  
   if fd.IsStream && len(p) > 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网络模型是怎样的?文章来源地址https://www.toymoban.com/news/detail-474273.html

到了这里,关于通过redis学网络(1)-用go基于epoll实现最简单网络通信框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Qt 实现简单的tcp网络通信

    背景: 最近计网要结课了,匆忙之间有个计网实验还没做。 上网这里查查那里查查,随便搞搞 然后在 这篇文章里找到了能够实现的代码 自己想着把它图形化一下,最后在超级棒棒糖的帮助下实现了。 工具头文件tool.h 该头文件用于添加一些要用到的库,直接引用这个库,比

    2024年02月01日
    浏览(39)
  • 【网络编程】利用套接字实现一个简单的网络通信(UDP实现聊天室 附上源码)

    源IP地址(Source IP Address): 源IP地址是数据包发送方(或数据流出发点)的唯一标识符。它用于在互联网或本地网络中定位发送数据包的设备或主机。源IP地址是数据包的出发点,即数据从这个地址开始传送,向目的IP地址指示的设备发送。 在TCP/IP协议中,源IP地址通常由发

    2024年02月14日
    浏览(83)
  • Redis的实现一:c、c++的网络通信编程技术,先实现server和client的通信

    由于,本人是主修java的,所以以下内容可能不是很精通,各位看完后尽可评论。 以下皆是在linux的描述 Socket()函数 :创建用于通信的端点并返回描述符。 它的第一个参数 便是通信域,这里我举出常用的。 AF_INET是ipv4的。 AF_INET6是ipv6的。 它的第二个参数 便是套接字具有指定

    2024年01月16日
    浏览(41)
  • 【网络原理】使用Java基于TCP搭建简单客户端与服务器通信

    TCP服务器与客户端的搭建需要借助以下API ServerSocket 是创建TCP服务端Socket的API。 ServerSocket 构造方法 : 方法签名 方法说明 ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口 ServerSocket 方法: 方法签名 方法说明 Socket accept() 开始监听指定端口(创建时绑定的端

    2024年03月12日
    浏览(79)
  • 「网络编程」第二讲:网络编程socket套接字(三)_ 简单TCP网络通信程序的实现

    「前言」文章是关于网络编程的socket套接字方面的,上一篇是网络编程socket套接字(二),下面开始讲解!  「归属专栏」网络编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句」 I do not know where to go,but I have been on the road. 我不知

    2024年02月11日
    浏览(50)
  • 【Unity】Socket网络通信(TCP) - 实现简单的多人聊天功能

    多客户端连接服务器其原理是在服务端保存客户端连入后与客户端通信的socket,由于等待客户端连接会阻塞主线程,所以结合多线程就能实现多客户端连入功能。多人聊天只需要将A客户端发来的消息,转发给除A客户端外的其他客户端,即可实现。如果你还不怎么熟悉服务端

    2024年02月03日
    浏览(62)
  • Redis的实现二: c、c++的网络通信编程技术,让服务器处理多个client

           看过上期的都知道,我是搞java的,所以对这些可能理解不是很清楚,各位看完可以尽情发言。       在服务器端网络编程中,有三种处理并发连接的方法。       它们是: 分叉 、 多线程 和 事件循环 。分叉为每个客户端连接创建新进程,以实现并发性。多线程使用

    2024年02月01日
    浏览(43)
  • 【JavaEE】_基于UDP实现网络通信

    目录 1. 服务器 1.1 实现逻辑 1.2 代码 1.3 部分代码解释 2. 客户端 2.1 实现逻辑 2.2 代码 2.3 客户端部分代码解释 3. 程序运行结果 4. 服务器客户端交互逻辑 普通服务器:收到请求,根据请求计算响应,返回响应; 回显服务器:忽略计算,直接将收到的请求作为响应返回; (

    2024年01月21日
    浏览(39)
  • 基于UDP/TCP的网络通信编程实现

    红色是心中永不褪色的赤诚 操作系统为网络编程提供了 Socket api , Socket是基于TCP/IP协议的网络通信的基本单元, 基于Socket的网络程序开发就是 网络编程. 由于直接与应用层联系的是传输层, 所以针对应用层协议(TCP, UDP), Shocket提供了三种套接字, 分别是 流套接字(使用TCP) , 数据报

    2024年02月08日
    浏览(52)
  • GO学习之 网络通信(Net/Http)

    1、GO学习之Hello World 2、GO学习之入门语法 3、GO学习之切片操作 4、GO学习之 Map 操作 5、GO学习之 结构体 操作 6、GO学习之 通道(Channel) 7、GO学习之 多线程(goroutine) 8、GO学习之 函数(Function) 9、GO学习之 接口(Interface) 10、GO学习之 网络通信(Net/Http) 11、GO学习之 微框架(Gin) 12、GO学习

    2024年02月13日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包