Go高并发系列 一 :如何自己设计epoll网络库

·  阅读 452

大家好,我是皮皮

今天皮皮和大家分享一下如何自己设计实现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

分类:
代码人生
标签:
分类:
代码人生
标签:
收藏成功!
已添加到「」, 点击更改