暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

聊聊go语言对于socket的抽象

122

写在文章开头

go语言
对于网络抽象做了非常通用且高性能的封装,所以就从net
包源码入手介绍一下go语言
对于socket
的抽象。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。

详解go语言对socket的抽象

服务端socket与客户端的交互流程

在正式介绍源码之前,我们需要简单的过一下socket通信的流程:

  1. 服务端创建socket
  2. 根据配置的端口号调用bind
    绑定端口监听连接。
  3. 调用accept
    阻塞监听连接。
  4. 客户端socket
    通过connect
    和服务端建立连接(其底层实际上会经历一次TCP三次握手)
  5. 双方进行数据收发。
  6. 完成通信后,客户端调用close
    结束通信(这期间会经历4次挥手)

代码示例

我们给出下面这样一段代码,他通过Listen
创建服务端监听socket
,然后通过Accept
阻塞接收新连接,一旦收到连接后开启协程进行数据读写:

func main() {
 //绑定8080端口
 listen, err := net.Listen("tcp""localhost:8080")

 if err != nil {
  fmt.Println("Error:", err.Error())
  return
 }

 //设置程序结束后关闭监听
 defer listen.Close()

 for {
  //阻塞等待连接
  conn, err := listen.Accept()
  if err != nil {
   fmt.Println("Error:", err.Error())
   return
  }

  go func() {
   defer conn.Close()
   //读取消息到buf并回复客户端Message received.
   buf := make([]byte1024)
   conn.Read(buf)

   fmt.Printf("收到消息:%s \r\n"string(buf))
   conn.Write([]byte("Message received."))

  }()

 }
}

启动后我们用telnet建立连接连接,并随意出入一个字符串a,该程序就会输出如下消息:

收到消息:a 

对应我们的终端也会收到程序的回复,然后连接被断开:

 Message received.
                  
遗失对主机的连接。


go语言如何完成socket的创建

我们以Linux
系统为例,我们通过 net.Listen
方法创建TCP
监听socket
并绑定传入的端口号,其底层会调用内核创建socket
并将这个socket
文件描述符fd
封装到go
语言的netFD
对象。

我们从上文的Listen
函数为入口,可以看到其内部调用了ListenConfig
Listen
方法:

func Listen(network, address string) (Listener, error) {
 var lc ListenConfig
 //调用ListenConfig的Listen实现基于配置的TCP连接初始化
 return lc.Listen(context.Background(), network, address)
}

步入Listen
就可以看到基于配置初始化监听对象的核心逻辑:

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
    //......
 //封装监听对象
 sl := &sysListener{
  ListenConfig: *lc,
  network:      network,
  address:      address,
 }
 var l Listener
 la := addrs.first(isIPv4)
 switch la := la.(type) {
 case *TCPAddr:
  //创建监听socket
  l, err = sl.listenTCP(ctx, la)
    //......
}

步入其内部逻辑查看,它会基于我们传入ip端口号等配置封装一个sysListener
并调用listenTCP
得到一个TCPListener
对象,这就是我们的监听socket
对象。

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
 //......
 //封装监听对象
 sl := &sysListener{
  ListenConfig: *lc,
  network:      network,
  address:      address,
 }
 var l Listener
 la := addrs.first(isIPv4)
 switch la := la.(type) {
 case *TCPAddr:
  //生成TCP监听对象
  l, err = sl.listenTCP(ctx, la)
 case *UnixAddr:
  //......
 default:
 //......
 return l, nil
}

最终步入listenTCP
查看逻辑,就可以看到它会通过internetSocket
创建socket
并基于这个socket
的文件描述符fd
封装成TCPListener
返回:

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
 //......
 //调用internetSocket,其底层会根据操作系统调用不同的函数完成socket创建
 fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0"listen", ctrlCtxFn)
 if err != nil {
  return nil, err
 }
 //基于socket的文件描述符fd封装成TCPListener返回
 return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

接收新连接

完成监听socket
创建之后就可以进行监听并处理接入的连接,Accept
本上就调用socket
accept
方法获取socket
对象,如果没有连接则直接将当前服务端监听socket
的对应协程挂起,反之若收到新连接则基于内核函数封装成一个establish
socket
并将其封装成TCPConn
对象返回:

我们查看Accept
函数内部,即可看到核心调用accept
,其底层本质就是调用当前TCPListener
对应socket
accept
方法从而得到一个已建立连接且封装establish socket
的对象TCPConn

func (l *TCPListener) Accept() (Conn, error) {
 if !l.ok() {
  return nil, syscall.EINVAL
 }
 //调用当前socket的accept方法得到一个新连接TCPConn
 c, err := l.accept()
 //......
 return c, nil
}

查看accept
内部逻辑,如上文所说基于当前连接的socket
的文件描述符定位到socket
调用accept
阻塞监听新连接:

func (ln *TCPListener) accept() (*TCPConn, error) {
 //
 fd, err := ln.fd.accept()
 if err != nil {
  return nil, err
 }
 //基于新连接的fd封装成newTCPConn客户端连接
 return newTCPConn(fd, ln.lc.KeepAlive, nil), nil
}

Linux
为例,我们可在fd_unix.go
看到ln.fd.accept()
的实现,其本质就是调用accept方法获取就绪的socket
,若存在需要建立连接的socket
则返回,反之调用waitRead
将当前协程挂起,等待系统轮询得到当前监听socket
就绪的事件后将其唤醒:


func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
 //......

 for {
  //调用accept获取就绪socket信息
  s, rsa, errcall, err := accept(fd.Sysfd)
  if err == nil {
   return s, rsa, "", err
  }
  switch err {
  case syscall.EINTR:
   continue
  //若accept没有得到socket则调用waitRead将当前协程挂起
  case syscall.EAGAIN:
   if fd.pd.pollable() {
    if err = fd.pd.waitRead(fd.isFile); err == nil {
     continue
    }
   }
  //......
  }
  return -1nil, errcall, err
 }
}

读数据

读写数据和监听socket
获取就绪连接处理差不多,我们以Linux
系统读为例,调用read
进行数据读时就调用底层逻辑进行系统读,如果有就绪的读事件则返回处理,反之将当前协程挂起:

func (c *conn) Read(b []byte) (int, error) {
 if !c.ok() {
  return 0, syscall.EINVAL
 }
 //调用当前socket的read方法并返回
 n, err := c.fd.Read(b)
 //......
 return n, err
}

查看Read
底层实现即可看到它调用socket原生非阻塞读方法,若没有就绪的读数据则将协程挂起,若有数据则返回数据长度n:

func (fd *FD) Read(p []byte) (int, error) {
 //......
 
 for {
  //调用socket原生读方法
  n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
  if err != nil {
   n = 0
   //若返回EAGAIN 则说明当前非阻塞读没有得到就绪的数据,调用waitRead将协程挂起
   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
 }
}

写数据

有了读数据的源码的学习基础,对于写数据的逻辑也就可以很直观的理解了,同样的调用原生socket的非阻塞写,若发现不可写(内核缓冲区已满)则将当前协程挂起,反之直接将数据到内核缓冲区等待发送:

func (c *conn) Write(b []byte) (int, error) {
 //......
 //调用当前socket的写方法
 n, err := c.fd.Write(b)
 //......
 return n, err
}

最终我们也可以在fd_unix.go
看到Write
的核心逻辑非阻塞写的逻辑,若可写则写入后返回写入长度若非阻塞写失败,则将当前协程挂起,等待可写时唤醒:

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
 //......
 var nn int
 for {
  //......
  n, err := ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max])
  //......
  if n > 0 {
   nn += n
  }
  //返回写入长度nn
  if nn == len(p) {
   return nn, err
  }
  //......
  //若非阻塞写失败,则将当前协程挂起,等待可写时唤醒
  if err == syscall.EAGAIN && fd.pd.pollable() {
   if err = fd.pd.waitWrite(fd.isFile); err == nil {
    continue
   }
  }
  //......
 }
}

小结

自此我们关于go语言网络层抽象设计与实现的所有篇章都已完成,感谢您的支持。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注  “加群”  即可和笔者和笔者的朋友们进行深入交流。


文章转载自写代码的SharkChili,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论