golang将多路复异步io转成阻塞io的方法详解

前言

本文主要给大家介绍了关于golang 如何将多路复异步io转变成阻塞io的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍:

package main

import (
 "net"
)

func handleConnection(c net.Conn) {
 //读写数据
 buffer := make([]byte, 1024)
 c.Read(buffer)
 c.Write([]byte("Hello from server"))
}

func main() {
 l, err := net.Listen("tcp", "host:port")
 if err != nil {
 return
 }
 defer l.Close()
 for {
 c, err := l.Accept()
 if err!= nil {
 return
 }
 go handleConnection(c)
 }
}

对于我们都会写上面的代码,很简单,的确golang的网络部分对于我们隐藏了太多东西,我们不用像c++一样去调用底层的socket函数,也不用去使用epoll等复杂的io多路复用相关的逻辑,但是上面的代码真的就像我们看起来的那样在调用accept和read时阻塞吗?

// Multiple goroutines may invoke methods on a Conn simultaneously.
//官方注释:多个goroutines可能同时调用方法在一个连接上,我的理解就是所谓的惊群效应吧
//换句话说就是你多个goroutines监听同一个连接同一个事件,所有的goroutines都会触发,
//这只是我的猜测,有待验证。
type Conn interface {
 Read(b []byte) (n int, err error)
 Write(b []byte) (n int, err error)
 Close() error
 LocalAddr() Addr
 RemoteAddr() Addr
 SetDeadline(t time.Time) error
 SetReadDeadline(t time.Time) error
 SetWriteDeadline(t time.Time) error
}

type conn struct {
 fd *netFD
}

这里面又一个Conn接口,下面conn实现了这个接口,里面只有一个成员netFD.

// Network file descriptor.
type netFD struct {
 // locking/lifetime of sysfd + serialize access to Read and Write methods
 fdmu fdMutex

 // immutable until Close
 sysfd  int
 family  int
 sotype  int
 isConnected bool
 net   string
 laddr  Addr
 raddr  Addr

 // wait server
 pd pollDesc
}

func (fd *netFD) accept() (netfd *netFD, err error) {
 //................
 for {
 s, rsa, err = accept(fd.sysfd)
 if err != nil {
 nerr, ok := err.(*os.SyscallError)
 if !ok {
 return nil, err
 }
 switch nerr.Err {
 /* 如果错误是EAGAIN说明Socket的缓冲区为空,未读取到任何数据
    则调用fd.pd.WaitRead,*/
 case syscall.EAGAIN:
 if err = fd.pd.waitRead(); err == nil {
  continue
 }
 case syscall.ECONNABORTED:
 continue
 }
 return nil, err
 }
 break
 }
 //.........
 //代码过长不再列出,感兴趣看go的源码,runtime 下的fd_unix.go
 return netfd, nil
}

上面代码段是accept部分,这里我们注意当accept有错误发生的时候,会检查这个错误是否是syscall.EAGAIN,如果是,则调用WaitRead将当前读这个fd的goroutine在此等待,直到这个fd上的读事件再次发生为止。当这个socket上有新数据到来的时候,WaitRead调用返回,继续for循环的执行,这样以来就让调用netFD的Read的地方变成了同步“阻塞”。有兴趣的可以看netFD的读和写方法,都有同样的实现。

到这里所有的疑问都集中到了pollDesc上,它到底是什么呢?

const (
 pdReady uintptr = 1
 pdWait uintptr = 2
)

// Network poller descriptor.
type pollDesc struct {
 link *pollDesc // in pollcache, protected by pollcache.lock
 lock mutex // protects the following fields
 fd  uintptr
 closing bool
 seq  uintptr // protects from stale timers and ready notifications
 rg  uintptr // pdReady, pdWait, G waiting for read or nil
 rt  timer // read deadline timer (set if rt.f != nil)
 rd  int64 // read deadline
 wg  uintptr // pdReady, pdWait, G waiting for write or nil
 wt  timer // write deadline timer
 wd  int64 // write deadline
 user uint32 // user settable cookie
}

type pollCache struct {
 lock mutex
 first *pollDesc
}

pollDesc网络轮询器是Golang中针对每个socket文件描述符建立的轮询机制。 此处的轮询并不是一般意义上的轮询,而是Golang的runtime在调度goroutine或者GC完成之后或者指定时间之内,调用epoll_wait获取所有产生IO事件的socket文件描述符。当然在runtime轮询之前,需要将socket文件描述符和当前goroutine的相关信息加入epoll维护的数据结构中,并挂起当前goroutine,当IO就绪后,通过epoll返回的文件描述符和其中附带的goroutine的信息,重新恢复当前goroutine的执行。这里我们可以看到pollDesc中有两个变量wg和rg,其实我们可以把它们看作信号量,这两个变量有几种不同的状态:

  • pdReady:io就绪
  • pdWait:当前的goroutine正在准备挂起在信号量上,但是还没有挂起。
  • G pointer:当我们把它改为指向当前goroutine的指针时,当前goroutine挂起

继续接着上面的WaitRead调用说起,go在这里到底做了什么让当前的goroutine挂起了呢。

func net_runtime_pollWait(pd *pollDesc, mode int) int {
 err := netpollcheckerr(pd, int32(mode))
 if err != 0 {
 return err
 }
 // As for now only Solaris uses level-triggered IO.
 if GOOS == "solaris" {
 netpollarm(pd, mode)
 }
 for !netpollblock(pd, int32(mode), false) {
 err = netpollcheckerr(pd, int32(mode))
 if err != 0 {
 return err
 }
 // Can happen if timeout has fired and unblocked us,
 // but before we had a chance to run, timeout has been reset.
 // Pretend it has not happened and retry.
 }
 return 0
}

// returns true if IO is ready, or false if timedout or closed
// waitio - wait only for completed IO, ignore errors
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
 //根据读写模式获取相应的pollDesc中的读写信号量
 gpp := &pd.rg
 if mode == 'w' {
 gpp = &pd.wg
 }

 for {
 old := *gpp
 //已经准备好直接返回true
 if old == pdReady {
 *gpp = 0
 return true
 }
 if old != 0 {
 throw("netpollblock: double wait")
 }
  //设置gpp pdWait
 if atomic.Casuintptr(gpp, 0, pdWait) {
 break
 }
 }

 if waitio || netpollcheckerr(pd, mode) == 0 {
 gopark(netpollblockcommit, unsafe.Pointer(gpp), "IO wait", traceEvGoBlockNet, 5)
 }

 old := atomic.Xchguintptr(gpp, 0)
 if old > pdWait {
 throw("netpollblock: corrupted state")
 }
 return old == pdReady
}

当调用WaitRead时经过一段汇编最重调用了上面的net_runtime_pollWait函数,该函数循环调用了netpollblock函数,返回true表示io已准备好,返回false表示错误或者超时,在netpollblock中调用了gopark函数,gopark函数调用了mcall的函数,该函数用汇编来实现,具体功能就是把当前的goroutine挂起,然后去执行其他可执行的goroutine。到这里整个goroutine挂起的过程已经结束,那当goroutine可读的时候是如何通知该goroutine呢,这就是epoll的功劳了。

func netpoll(block bool) *g {
 if epfd == -1 {
 return nil
 }
 waitms := int32(-1)
 if !block {
 waitms = 0
 }
 var events [128]epollevent
retry:
 //每次最多监听128个事件
 n := epollwait(epfd, &events[0], int32(len(events)), waitms)
 if n < 0 {
 if n != -_EINTR {
 println("runtime: epollwait on fd", epfd, "failed with", -n)
 throw("epollwait failed")
 }
 goto retry
 }
 var gp guintptr
 for i := int32(0); i < n; i++ {
 ev := &events[i]
 if ev.events == 0 {
 continue
 }
 var mode int32
 //读事件
 if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
 mode += 'r'
 }
 //写事件
 if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
 mode += 'w'
 }
 if mode != 0 {
  //把epoll中的data转换成pollDesc
 pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
 netpollready(&gp, pd, mode)
 }
 }
 if block && gp == 0 {
 goto retry
 }
 return gp.ptr()
}

这里就是熟悉的代码了,epoll的使用,看起来亲民多了。pd:=*(**pollDesc)(unsafe.Pointer(&ev.data))这是最关键的一句,我们在这里拿到当前可读时间的pollDesc,上面我们已经说了,当pollDesc的读写信号量保存为G pointer时当前goroutine就会挂起。而在这里我们调用了netpollready函数,函数中把相应的读写信号量G指针擦出,置为pdReady,G-pointer状态被抹去,当前goroutine的G指针就放到可运行队列中,这样goroutine就被唤醒了。

可以看到虽然我们在写tcp server看似一个阻塞的网络模型,在其底层实际上是基于异步多路复用的机制来实现的,只是把它封装成了跟阻塞io相似的开发模式,这样是使得我们不用去关注异步io,多路复用等这些复杂的概念以及混乱的回调函数。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Go语言中io.Reader和io.Writer的详解与实现

    一.前言 也许对这两个接口和相关的一些接口很熟悉了,但是你脑海里确很难形成一个对io接口的继承关系整天的概貌,原因在于godoc缺省并没有像javadoc一样显示官方库继承关系,这导致了我们对io接口的继承关系记忆不深,在使用的时候还经常需要翻文档加深记忆. 本文试图梳理清楚Go io接口的继承关系,提供一个io接口的全貌. 二.io接口回顾 首先我们回顾一下几个常用的io接口.标准库的实现是将功能细分,每个最小粒度的功能定义成一个接口,然后接口可以组成成更多功能的接口. 最小粒度的接口 typ

  • 深入解析Go语言的io.ioutil标准库使用

    今天我们讲解的是golang标准库里边的io/ioutil包–也就是package io/ioutil 1.ioutil.ReadDir(dirname string)这个函数的原型是这样的 func ReadDir(dirname string) ([]os.FileInfo, error) 不难看出输入的是dirname类型是string类型的 譬如"d:/go",然会是一个FileInfo的切片,其中FileInfo的结构是这样的 复制代码 代码如下: type FileInfo

  • GO语言的IO方法实例小结

    type PipeWriter 复制代码 代码如下: type PipeWriter struct {     // contains filtered or unexported fields } (1)func (w *PipeWriter) Close() error关闭管道,关闭时正在进行的Read操作将返回EOF,若管道内仍有未读取的数据,后续仍可正常读取 复制代码 代码如下: import (  "fmt"  "io" ) func main() {  

  • golang将多路复异步io转成阻塞io的方法详解

    前言 本文主要给大家介绍了关于golang 如何将多路复异步io转变成阻塞io的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: package main import ( "net" ) func handleConnection(c net.Conn) { //读写数据 buffer := make([]byte, 1024) c.Read(buffer) c.Write([]byte("Hello from server")) } fu

  • golang实现php里的serialize()和unserialize()序列和反序列方法详解

    Golang 实现 PHP里的 serialize() . unserialize() 安装 go get -u github.com/techleeone/gophp/serialize 用法 package main import ( "fmt" "github.com/techleeone/gophp/serialize" ) func main() { str := `a:1:{s:3:"php";s:24:"世界上最好的语言&

  • GoLang jwt无感刷新与SSO单点登录限制解除方法详解

    目录 前言 为什么使用JWT Cookie和Session token (header.payload.signature) token 安全性 基于token安全性的处理 客户端与服务端基于无感刷新流程图 golang实现atoken和rtoken 颁发token 校验token 无感刷新token 完整实现代码 SSO(Single Sign On)单用户登录以及无感刷新token 实现思路 实战代码 小结 前言 为什么使用JWT Jwt提供了生成token以及token验证的方法,而tok

  • 对Python3中dict.keys()转换成list类型的方法详解

    在python3中使用dict.keys()返回的不在是list类型了,也不支持索引,我们可以看一下下面这张图片 那么我们应该怎么办呢,其实解决的方法也是非常简单的,只需要使用list()就可以了,可以看下面的代码 list(dict.keys()) 我们可以看一下下面这张图片,现在就支持索引了 以上这篇Python3中dict.keys()转换成list类型就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • Golang实现程序优雅退出的方法详解

    目录 1. 背景 2. 常见的几种平滑关闭 2.1 http server 平滑关闭 2.2 gRPC server 平滑关闭 2.3 worker 协程平滑关闭 2.4 实现 io.Closer 接口的自定义服务平滑关闭 2.5 集成其他框架怎么做 1. 背景 项目开发过程中,随着需求的迭代,代码的发布会频繁进行,在发布过程中,如何让程序做到优雅的退出? 为什么需要优雅的退出? 你的 http 服务,监听端口没有关闭,客户的请求发过来了,但处理了一半,可能造成脏数据. 你的协程 worker

  • golang常用库之操作数据库的orm框架-gorm基本使用详解

    golang常用库:gorilla/mux-http路由库使用 golang常用库:配置文件解析库-viper使用 golang常用库:操作数据库的orm框架-gorm基本使用 一:字段映射-模型定义 gorm中通常用struct来映射字段. gorm教程中叫模型定义 比如我们定义一个模型Model: type User struct { gorm.Model UserId int64 `gorm:"index"` //设置一个普通的索引,没有设置索引名,gorm会自动命名 Birth

  • Android AsyncTask实现异步处理任务的方法详解

    Android AsyncTask实现异步处理任务的方法详解 在开发Android应用时必须遵守单线程模型的原则:Android UI操作并不是线程安全的并且这些操作必须在UI线程中执行. Android 单线程模型概念详解:http://www.jb51.net/article/112165.htm 在单线程模型中始终要记住两条法则: 不要阻塞UI线程 确保只在UI线程中访问Android UI工具包 当一个程序第一次启动时,Android会同时启动一个对应的主线程(Main Thread),

  • golang项目如何上线部署到Linu服务器(方法详解)

    Go作为Google2009年推出的语言,其被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言. 对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率.它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了. 到现在Go的开发已经是完全开放的,并且拥有一个活跃的社区. 下面看下golang项目如何上线部署到Linu服务器上. windows服务器 先本地编译 go build main.go 编译后会在同级目录生成可执行文件

  • 使用Tomcat Native提升Tomcat IO效率的方法详解

    简介 IO有很多种,从最开始的Block IO,到nonblocking IO,再到IO多路复用和异步IO,一步一步的将IO的性能提升做到极致. 今天我们要介绍一下怎么使用Tomcat Native来提升Tomcat IO的效率. Tomcat的连接方式 Tomcat中使用连接器来处理与外部客户端的通信.Connecter主要用来接受外部客户端的请求,并转交给处理引擎处理. 在Tomcat中有两种Connector.一种是 HTTP connector, 一种是AJP connector. HT

  • Golang实现文件夹的创建与删除的方法详解

    目录 创建文件夹 删除文件和文件夹 小结 学习笔记,写到哪是哪. 接着上一篇对纯文本文件读写操作,主要去实现一些文件夹操作. 创建文件夹 创建文件夹的时候往往要先判断文件夹是否存在. 样例代码如下 package main import ( "bufio" "fmt" "io" "os" ) //判断文件夹是否存在 func HasDir(path string) (bool, error) { _, _err := os.S

随机推荐