Go中的网络编程(二)| 青训营笔记

96 阅读4分钟

Network Poller 如何工作

Network Poller初始化

  • poll_runtime_pollServerInit()
  • 使用原子操作保证只初始化一次
  • 调用netpollinit()

pollcache与pollDesc

type pollCache struct {
	lock mutex
	first *pollDesc // 放链表头
}

type pollDesc struct {
	link *pollDesc // 后续指针
	// ...
	fd uintptr // socket的ID
	rg uintptr // pdReady(1), pdWait(2), 等待读的协程的地址
	wg uintptr // pdReady(1), pdWait(2), 等待写的协程的地址
}
  • pollcache: 一个带锁的链表头
  • polDesc: 链表的成员
  • pollDesc是runtime包对Socket的详细描述(记录了哪些协程对该socket感兴趣)
  • rg, rw: 初始(0), pdReady(1), pdWait(2), 或者等待协程G的地址

Pasted image 20230523214244.png

Network Poller新增监听Socket

  • poll_runtime_pollOpen()
  • 在pollcache链表中分配一个pollDesc
  • 初始化pollDesc(rg, rw为0)
  • 调用netpollopen(见上节, 注册各种epoll事件)

Network Poller收发数据

收发数据分为两个场景:

  • 协程需要收发数据时, Socket已经可读可写
  • 协程需要收发数据时, Socket暂时无法读写

场景1: Socket已经可读可写

  • runtime循环调用netpoll()方法(g0协程, 最终是通过垃圾回收器调用, gcStart, 一个hook)
  • 发现Socket可读写时, 给对应的rg或者wg置为pdReady(1)
  • 协程调用poll_runtime_pollWait()
  • 判断rg或者wg已经置为pdReady(1), 返回0

场景2: Socket暂时无法读写

  • runtime循环调用netpoll()方法
  • 协程调用poll_runtime_pollWait()
  • 发现对应的rg或者wg为0
  • 给对应的rg或者wg置为协程地址
  • 休眠等待
  • runtime循环调用netpoll方法
  • 发现Socket可读写时, 查看对应的rg或者wg
  • 若为协程地址, 返回协程地址(有协程在监听)
  • 调度器开始调度对应协程

总结

  • Network Poller是Runtime的强大工具
  • 抽象了多路复用器的操作
  • Network Poller可以自动监测多个Socket状态
  • 在Socket状态可用时,快速返回成功
  • 在Socket状态不可用时,休眠等待

Go 抽象Socket

net包

  • net包是go原生的网络包
  • net包支持了TCP, UDP, HTTP等网络操作
lis, err := net.Listen("tcp", ":8888") // 监听8888端口
if err != nil {
	panic(err)
}
conn, err := lis.Accept()
if err != nil {
	panic(err)
}
var body [100]byte
for {
	_, err := conn.Read(body[:])
	if err != nil {
		break
	}
	fmt.Printf("收到消息: %s\n", body)
	_, err = conn.Write(body[:])
	if err != nil {
		break
	}
}

net.Listen()

  • 新建Socket, 并执行bind操作
  • 新建一个FD(net包对Socket的详情描述)
  • 返回一个TCPListener对象
  • 将TCPListener的FD信息加入监听
  • TCPListener对象本质上是一个Listen状态的Socket

TCPListener.Accept()

  • 直接调用Socket的accept()
  • 如果失败,休眠等待新的连接
  • 将新的Socket包装为TCPConn变量返回
  • 将TCPConn的FD信息加入监听
  • TCPConn本质上是一个ESTABLISHED状态的Socket

TCPConn.Read() / Write()

  • 直接调用Socket原生读写方法
  • 如果失败,休眠等待可读/可写
  • 被唤醒后调用系统Socket

总结

  • net包抽象了TCP网络操作
  • 使用net.Listen()得到TCPListener(LISTEN状态的Socket)
  • 使用TCPListener..Accept()得到TCPConn(ESTABLISHED)
  • TCPConn.Read() / Write() 进行读写Socket的操作
  • Network Poller 作为上述功能的底层支撑

Go搭建TCP Server

结合阻塞模型和多路复用

Pasted image 20230523230339.png

  • 用主协程监听Listener
  • 每个Conn使用一个新携程处理
package main

import (
	"fmt"
	"net"
)

func handleConnection(conn net.Conn) {
	defer func() {
		if err := conn.Close(); err != nil {
			panic(err)
		}
	}()

	var body [100]byte
	for {
		_, err := conn.Read(body[:])
		if err != nil {
			break
		}
		fmt.Printf("收到消息: %s\n", body)
		_, err = conn.Write(body[:])
		if err != nil {
			break
		}
	}
}

func main() {
	lis, err := net.Listen("tcp", ":8888") // 监听8888端口
	if err != nil {
		panic(err)
	}
	for {
		conn, err := lis.Accept()
		if err != nil {
			panic(err)
		}
		go handleConnection(conn)
	}
}

总结

系统IO模型

  • 操作系统提供了Socket作为TCP通信的抽象
  • lO模型指的是操作Socket的方案
  • 阻塞模型最利于业务编写,但是性能差
  • 多路复用性能好,但业务编写麻烦

Epoll的抽象

  • Go将多路复用器的操作进行了抽象和适配:
    • 将新建多路复用器抽象为了netpollinit()
    • 将插入监听事件抽象为了netpollopent()
    • 将查询事件抽象为了netpoll()
    • 但不是返回事件, 而是返回等待事件的协程列表

Network Poller的原理

  • Network Poller是Runtime的强大工具
  • 抽象了多路复用器的操作
  • Network Poller可以自动监测多个Socket状态
  • 在Socket状态可用时,快速返回成功
  • 在Socket状态不可用时,休眠等待

Net包

  • net包抽象了TCP网络操作使用net.Listen()得到TCPListener(LISTEN状态的Socket)
  • 使用TCPListener.Accept()得到TCPConn(ESTABLISHED)
  • TCPConn.Read()/Write进行读写Socket的操作
  • Network Poller作为上述功能的底层支撑

goroutine-per-connection

  • 用主协程监听Listener
  • 每个Conn使用一个新协程处理
  • 结合了多路复用的性能和阻塞模型的简洁