深入理解 Golang: 网络编程

news2024/9/24 23:23:05

Go 中的 Epoll

关于计算机网络分层与 TCP 通信过程过程此处不再赘述。

  • 考虑到 TCP 通信过程中各种复杂操作,包括三次握手,四次挥手等,多数操作系统都提供了 Socket 作为 TCP 网络连接的抽象。
  • Linux -> Internet domain socket -> SOCK_STREAM
  • Linux 中 Socket 以 “文件描述符” FD 作为标识

在进行 Socket 通信时,服务端同时操作多个 Socket,此时便需要 IO 模型操作方案。:

  • 阻塞 IO。传统 C/C++ 方案,同步读写 Socket(一个线程一个 Socket),线程陷入内核态,当读写成功后,切换回用户态继续执行。
  • 非阻塞 IO。应用会不断自旋轮询,直到 Socket 可以读写,如果暂时无法收发数据,会返回错误。
  • Epoll 多路复用。提供了事件列表,不需要查询各个 Socket。其注册多个 Socket 事件,调用 epoll ,当有事件发生则返回。

Epoll 是 Linux 下的 event poll,Windows 中为 IOCP, Mac 中为 kqueue。

在 Go 中,内部采用结合阻塞模型和多路复用的方法。在这里就不再是线程操作 Socket,而是 Goroutine 协程。每个协程关心一个 Socket 连接:

  • 在底层使用操作系统的多路复用 IO。
  • 在协程层次使用阻塞模型。
  • 阻塞协程时,休眠协程。

我们知道,Go 是一个跨平台的语言,不同平台/操作系统下提供的 Epoll 实现不同,所以 Go 在 Epoll/IOCP/kqueue 上再独立了一层 epoll 抽象层,用于屏蔽各个系统的差异性,抽象各系统对多路复用器的实现。
在这里插入图片描述

Go Network Poller 多路复用器的抽象,以 Linux 为例:

  • Go Network Poller 对于多路复用器的抽象和适配
  • epoll_create() -> netpollinit()
  • epoll_ctl() -> netpollopen()
  • epoll_wait() -> netpoll()
// Integrated network poller (platform-independent part).
// A particular implementation (epoll/kqueue/port/AIX/Windows)
// must define the following functions:
//
// func netpollinit()
//     Initialize the poller. Only called once.
//
// func netpollopen(fd uintptr, pd *pollDesc) int32
//     Arm edge-triggered notifications for fd. The pd argument is to pass
//     back to netpollready when fd is ready. Return an errno value.
//
// func netpollclose(fd uintptr) int32
//     Disable notifications for fd. Return an errno value.
//
// func netpoll(delta int64) gList
//     Poll the network. If delta < 0, block indefinitely. If delta == 0,
//     poll without blocking. If delta > 0, block for up to delta nanoseconds.
//     Return a list of goroutines built by calling netpollready.
//
// func netpollBreak()
//     Wake up the network poller, assumed to be blocked in netpoll.
//
// func netpollIsPollDescriptor(fd uintptr) bool
//     Reports whether fd is a file descriptor used by the poller.

上诉所有方法的实现都在 %GOROOT/src/runtime/netpoll_epoll.go%

netpollinit() 新建多路复用器

  1. 新建 Epoll,不同系统对应不同的实现方式。
  2. 新建一个 Pipe 管道用于中断 Epoll。
  3. 将 “管道有数据到达” 事件注册到 Epoll 中。
func netpollinit() {
    var errno uintptr

    // 1. 新建 Epoll,不同系统对应不同的实现方式
    epfd, errno = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
    if errno != 0 {
        println("runtime: epollcreate failed with", errno)
        throw("runtime: netpollinit failed")
    }

    // 用来中断 Epoll 的管道
    r, w, errpipe := nonblockingPipe()
    if errpipe != 0 {
        println("runtime: pipe failed with", -errpipe)
        throw("runtime: pipe failed")
    }

    // 3. 注册事件
    ev := syscall.EpollEvent{
        Events: syscall.EPOLLIN,
    }
    *(**uintptr)(unsafe.Pointer(&ev.Data)) = &netpollBreakRd
    errno = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, r, &ev)
    if errno != 0 {
        println("runtime: epollctl failed with", errno)
        throw("runtime: epollctl failed")
    }
    netpollBreakRd = uintptr(r)
    netpollBreakWr = uintptr(w)
}

netpollopen() 插入事件

  1. 传入一个 Socket 的 FD 和 pollDesc 指针。
  2. pollDesc 指针是 Socket 相关详细信息。
  3. pollDesc 指针中记录了哪个协程在休眠等待此 Socket。
  4. 将 Socket 的可读/可写/断开事件注册到 Epoll 中。
func netpollopen(fd uintptr, pd *pollDesc) uintptr {
    var ev syscall.EpollEvent

    // 事件类型
    ev.Events = syscall.EPOLLIN | syscall.EPOLLOUT | syscall.EPOLLRDHUP | syscall.EPOLLET
    *(**pollDesc)(unsafe.Pointer(&ev.Data)) = pd
    return syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, int32(fd), &ev)
}

netpoll() 查询事件

  1. 调用 EpollWait() 方法,查询有哪些事件发生
  2. 根据 Socket 相关的 pollDesc 信息,返回哪些协程可以唤醒。
func netpoll(delay int64) gList {
    // 1. 查询哪些事件发生
    n, errno := syscall.EpollWait(epfd, events[:], int32(len(events)), waitms)
    // ...
    if errno != 0 {
        if errno != _EINTR {
            println("runtime: epollwait on fd", epfd, "failed with", errno)
            throw("runtime: netpoll failed")
        }
        // If a timed sleep was interrupted, just return to
        // recalculate how long we should sleep now.
        if waitms > 0 {
            return gList{}
        }
        goto retry
    }
    // 2. 根据 Socket 相关的 pollDesc 信息,返回哪些协程可以唤醒。
    var toRun gList
    for i := int32(0); i < n; i++ {
        ev := events[i]
        if ev.Events == 0 {
            continue
        }

        if *(**uintptr)(unsafe.Pointer(&ev.Data)) == &netpollBreakRd {
            if ev.Events != syscall.EPOLLIN {
                println("runtime: netpoll: break fd ready for", ev.Events)
                throw("runtime: netpoll: break fd ready for something unexpected")
            }
            if delay != 0 {
                var tmp [16]byte
                read(int32(netpollBreakRd), noescape(unsafe.Pointer(&tmp[0])), int32(len(tmp)))
                netpollWakeSig.Store(0)
            }
            continue
        }

        var mode int32
        if ev.Events&(syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
            mode += 'r'
        }
        if ev.Events&(syscall.EPOLLOUT|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
            mode += 'w'
        }
        if mode != 0 {
            pd := *(**pollDesc)(unsafe.Pointer(&ev.Data))
            pd.setEventErr(ev.Events == syscall.EPOLLERR)
            netpollready(&toRun, pd, mode)
        }
    }

    // 协程列表
    return toRun
}

Go Network Poller

Network Poller 初始化

  • 初始化一个 Network Poller。
  • 调用 netpollinit() 新建多路复用器。
// %GOROOT%src/runtime/netpoll.go
func poll_runtime_pollServerInit() {
    netpollGenericInit()
}

func netpollGenericInit() {
    // 每个 Go 应用只初始化一次
    if netpollInited.Load() == 0 {
        lockInit(&netpollInitLock, lockRankNetpollInit)
        lock(&netpollInitLock)
        if netpollInited.Load() == 0 {
            // 新建多路复用器
            netpollinit()
            netpollInited.Store(1)
        }
        unlock(&netpollInitLock)
    }
}

关于 pollDesc,是 runtime 包对 Socket 的详细描述:

type pollDesc struct {
    _    sys.NotInHeap
    link *pollDesc // in pollcache, protected by pollcache.lock
    fd   uintptr   // constant for pollDesc usage lifetime
    atomicInfo atomic.Uint32 // atomic pollInfo


    rg atomic.Uintptr // pdReady, pdWait, G waiting for read or pdNil
    wg atomic.Uintptr // pdReady, pdWait, G waiting for write or pdNil

    lock    mutex // protects the following fields
    closing bool
    user    uint32    // user settable cookie
    rseq    uintptr   // protects from stale read timers
    rt      timer     // read deadline timer (set if rt.f != nil)
    rd      int64     // read deadline (a nanotime in the future, -1 when expired)
    wseq    uintptr   // protects from stale write timers
    wt      timer     // write deadline timer
    wd      int64     // write deadline (a nanotime in the future, -1 when expired)
    self    *pollDesc // storage for indirect interface. See (*pollDesc).makeArg.
}

Network Poller 新增监听 Socket

  • 在 pollCache 链表中分配一个 pollDesc。
  • 初始化 pollDesc,rg,wg 都为 0。
  • 调用 netpollopen() 插入事件
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
    // 分配 pollDesc
    pd := pollcache.alloc()
    lock(&pd.lock)
    wg := pd.wg.Load()
    if wg != pdNil && wg != pdReady {
        throw("runtime: blocked write on free polldesc")
    }
    rg := pd.rg.Load()
    if rg != pdNil && rg != pdReady {
        throw("runtime: blocked read on free polldesc")
    }

    // 初始化 pollDesc
    pd.fd = fd
    pd.closing = false
    pd.setEventErr(false)
    pd.rseq++
    pd.rg.Store(pdNil)
    pd.rd = 0
    pd.wseq++
    pd.wg.Store(pdNil)
    pd.wd = 0
    pd.self = pd
    pd.publishInfo()
    unlock(&pd.lock)

    // 插入事件
    errno := netpollopen(fd, pd)
    if errno != 0 {
        pollcache.free(pd)
        return nil, int(errno)
    }
    return pd, 0
}

Network Poller 收发数据

  • Socket 已经可读写时
    • runtime 的 g0 协程循环调用 netpoll() 方法。
    • 发现 Socket 可读写时,给对应的 rgwg 置为 pdReady(1)。
    • 协程调用 poll_runtime_pollWait()
    • 判断 rgwg 已置为 pdReady(1),返回 0。
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
    // ...
    // 判断 `rg` 或 `wg` 已置为 pdReady(1),返回 0。
    for !netpollblock(pd, int32(mode), false) {
        errcode = netpollcheckerr(pd, int32(mode))
        if errcode != pollNoError {
            return errcode
        }
    }
    return pollNoError
}
  • Socket 暂时无法读写时
    • runtime 的 g0 协程循环调用 netpoll() 方法。
    • 协程调用 poll_runtime_pollWait()
    • 发现对应的 rgwg 为 0。
    • 给对应的 rgwg 置为协程地址。
    • 休眠等待。
    • 当发现 Socket可读写时,查看对应的 rgwg
    • 若为协程地址,则返回该地址。
    • 调度器开始调度该协程。
      在这里插入图片描述

Socket 通信

net 包中的 Socket 会被定义为一个 netFD 结构体:

type netFD struct {
    // 最终指向的 runtime 中的 Socket 结构体
    pfd poll.FD

    family      int
    sotype      int
    isConnected bool // handshake completed or use of association with peer
    net         string
    laddr       Addr
    raddr       Addr
}

在这里插入图片描述

Server 端

以 TCP 协议为例:

net 的 net.Listen() 操作:

  1. 新建 Socket,并执行 bind 操作
  2. 新建一个 FD(net 包对 Socket 的详情描述)。
  3. 返回一个 TCPListener 对象
  4. TCPListenerFD 信息加入监听。
func main() {
    ln, err := net.Listen("tcp", ":8888")
    if err != nil {
        panic(err)
    }
}

TCPListener 本质是一个 LISTEN 状态的 Scoket。

TCPListener.Accept() 操作:

  1. 直接调用 Socket 的 accept()
  2. 如果失败,休眠等待新的连接。
  3. 将新的 Socket 包装成 TCPConn 变量返回。
  4. TCPConnFD 信息加入监听。
func main() {
    ln, err := net.Listen("tcp", ":8888")
    if err != nil {
        panic(err)
    }

    conn, err := ln.Accept()
    if err != nil {
        panic(err)
    }
    defer conn.Close()
}

TCPConn 本质是一个 ESTANBLISHED 状态的 Scoket。

TCPConn 收发数据

func main() {
    // 1. 监听端口
    ln, err1 := net.Listen("tcp", ":8888")
    if err1 != nil {
        panic(err1)
    }

    // 2. 建立连接
    conn, err2 := ln.Accept()
    if err2 != nil {
        panic(err2)
    }
    defer conn.Close()

    var recv [1024]byte
    // 使用 bufio 标准库提供的缓冲区功能
 send := bufio.NewReader(conn)

    for {

        // 3. 获取数据
        _, err3 := conn.Read(recv[:])
        if err3 != nil {
            break
        }

        fmt.Printf("n: %v\n", string(recv[:]))

        // 4. 发送数据
        msg, err := send.ReadString('\n')
        if strings.ToUpper(msg) == "Q" {
            return
        }
        if err != nil {
            return
        }
        _, err4 := conn.Write([]byte(msg))
        if err4 != nil {
            break
        }
    }
}

Client 端

func main() {
    // 与服务端建立连接
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        panic(err)
    }
    
    var recv [1024]byte

    send := bufio.NewReader(os.Stdin)
    for {
        s, _ := send.ReadString('\n')
        if strings.ToUpper(s) == "Q" {
            return  
        }

        // 发送数据
        _, err = conn.Write([]byte(s))
        if err != nil {
            panic(err)
        }

        // 接收数据 
        _, err := conn.Read(recv[:])
        if err != nil {
            break
        }
        fmt.Printf(":%v\n", string(recv[:]))
    }
}

goroutine-per-connection style code

一个协程服务一个新的连接

package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

func handleConnection(conn net.Conn) {
    defer conn.Close()

    var recv [1024]byte
    // 使用 bufio 标准库提供的缓冲区功能
    send := bufio.NewReader(conn)

    for {

        // 3. 获取数据
        _, err3 := conn.Read(recv[:])
        if err3 != nil {
            break
        }

        fmt.Printf("n: %v\n", string(recv[:]))

        // 4. 发送数据
        msg, err := send.ReadString('\n')
        if strings.ToUpper(msg) == "Q" {
            return
        }
        if err != nil {
            return
        }
        _, err4 := conn.Write([]byte(msg))
        if err4 != nil {
            break
        }
    }
}

func main() {
    // 1. 监听端口
    ln, err1 := net.Listen("tcp", ":8888")
    if err1 != nil {
        panic(err1)
    }

    for {
        // 2. 建立连接
        conn, err2 := ln.Accept()
        if err2 != nil {
            panic(err2)
        }

        go handleConnection(conn)
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

layui中文、以及图标乱码解决方案

最终解决方案…手动对js文件中的中文&#xff0c;用unicode进行编码

修改 ChatGLM2-6B 自我认知的 Lora 微调教程

修改 ChatGLM2-6B 自我认知的 Lora 微调教程 0. 背景1. 部署微调项目2. 数据集说明3. 模型监督微调(Lora)4. 模型效果测试5. 导出微调模型6. 调用导出的模型 0. 背景 现在开始学习微调&#xff0c;主要学习 Lora 微调。 这次尝试了修改 ChatGLM2-6B 自我认知&#xff0c;文章…

2023.7.2-【for语言】:输入一个整数,并输入该整数对应个数的整数,求他们的和与平均值

程序&#xff1a; int a;int b0;int c;int sum0;double ave;printf("请输入待求整数的个数&#xff1a;");scanf("%d",&a);for (b 1; b<a; b){printf("整数%d&#xff1a;", b);scanf("%d", &c);sum c;}printf("以上…

vite中的env环境变量

一、vite中使用env环境变量基本介绍 Vite 是一种现代化的前端构建工具&#xff0c;旨在提供快速的开发和构建体验。在 Vite 中&#xff0c;env 环境变量是一种用于在项目中设置和访问全局变量的机制。通过 env 变量&#xff0c;可以在不同环境下配置不同的参数&#xff0c;实现…

时间序列分解 | Matlab 互补集合经验模态分解(CEEMD)的信号分解

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列分解 | Matlab 互补集合经验模态分解(CEEMD)的信号分解 部分源码 %---------------------

Java基础---为什么不能用浮点数表示金额

目录 缘由 十进制转二进制 不是所有数都能用二进制表示 IEEE 754 避免精度丢失 缘由 因为不是所有的小数都能用二进制表示&#xff0c;所以&#xff0c;为了解决这个问题&#xff0c;IEEE提出了一种使用近似值表示小数的方式&#xff0c;并且引入了精度的概念这就是我们所…

Docker部署.Net7.0

1、新建项目 勾选启用Docker,会自动生成Dockerfile文件 2、生成镜像 打开PowerShell 进入项目解决方案目录路径 把项目打包成镜像 //镜像名称net7. 注意镜像名称后面的空格和点符号必须有docker build -t net7.0 .打包完成后可以看到项目的镜像 3、创建容器并启动 //…

C++文件操作 - 写操作----简单示例

C文件操作 - 写操作 一、什么是文件 内存中存放的数据在计算机关机后就会消失。要长久保存数据&#xff0c;就要使用硬盘、光盘、U盘等设备。为了便于数据的管理和检索&#xff0c;引入了“文件”的概念。 一篇文章、一段视频、一个可执行程序&#xff0c;都可以被保存为一个文…

BeanShell:多线程环境下Interpreter解释器的优化使用

BeanShell是用 Java 编写的一个小型、免费、可嵌入的 Java 代码的脚本解释器。 BeanShell动态执行标准Java语法&#xff0c;并使用通用语法对其进行扩展 脚本编写便利性&#xff0c;适用于 Java 的轻量级脚本。本文说明在并发环境下对BeanShell更加优化的使用方式。 简单示例 …

geoserver加载arcgis server瓦片地图显示异常问题处理

1.全能地图下载的瓦片conf.xml格式有问题首先要修改格式&#xff0c;conf.cdi文件也需要修改格式&#xff0c;修改为UTF-8或者UTF-8无BOM编码(不同的notepadd显示不同) 2. 下载的conf.xml坐标系默认从最小级别开始&#xff0c;一定要把前几级也补全&#xff0c;从0级开始 <L…

diffusion model

(正课)Diffusion Model 原理剖析 (1_4) (optional)_哔哩哔哩_bilibili(正课)Diffusion Model 原理剖析 (1_4) (optional)是【授权】李宏毅2023春机器学习课程的第42集视频&#xff0c;该合集共计64集&#xff0c;视频收藏或关注UP主&#xff0c;及时了解更多相关视频内容。http…

高中学习3大主流国际课程-申请国外大学本科

目录 作用 3大主流国际课程是什么 A-Level AP课程 IB 3大主流国际课程对比 作用 帮助学生申请国外大学本科。 如果能够选择到适合的国际课程&#xff0c;未来的留学规划就相当于成功了一半 3大主流国际课程是什么 A-Level、AP、IB三大国际课程 A-Level A-Level课程&a…

Kali Linux基础使用

Kali Linux基础使用 一、搭建渗透测试攻击环境1.1、Vmware workstation1.2、下载与安装1.3、安装渗透攻击机1.3.1、配置root用户登录1.3.2、普通用户切换到root用户1.3.3、修改kali语言1.3.4、网络配置1.3.4.1、桥接网络1.3.4.2、NAT1.3.4.3、仅主机 1.4、编辑网络文件 二、Lin…

05 - C++学习笔记: 一维数组和多维数组

数组是一种非常重要的数据结构&#xff0c;它允许用连续的方式存储和访问一组相同类型的数据。无论是存储学生成绩、处理图像数据还是解决复杂的数学问题&#xff0c;数组都发挥着重要的作用。 1️⃣ 一维数组的定义与引用 ✨ 倒序输出 在C中&#xff0c;一维数组是一种存储…

阿斯巴甜再亮红灯,配料表“瘦身”成趋势

【潮汐商业评论/原创】 6月以来&#xff0c;北方大部分地区出现近40度的高温天气&#xff0c;北京更是多年不遇的发布高温红色预警&#xff0c;酷暑难耐的Allen发现自己今年在水饮的消耗比往年高了好几倍&#xff0c;每天最快乐的时候莫过于一罐冰镇可乐&#xff0c;一口下去仿…

C/C++开发,opencv基于FileStorage读写文件介绍及示例

目录 一、FileStorage类 1.1 FileStorage类说明 1.2 FileStorage类写入说明 1.3 FileStorage类读取说明 二、FileStorage类应用示例 2.1 应用代码 2.2 工程组织&#xff08;Makefile&#xff09; 2.3 编译及测试 一、FileStorage类 1.1 FileStorage类说明 FileStorage类在ope…

6.S081——并发与锁部分(自旋锁的实现)——xv6源码完全解析系列(9)

0.briefly speaking 并发(Concurrency)指的是在多处理器系统(multiprocessor)中的并行&#xff0c;线程切换和中断导致的多个指令流交互错杂的情况&#xff0c;再和现代处理器体系结构中的多发射、乱序执行、Cache一致性等话题结合起来&#xff0c;这几乎变成了一个相当相当复…

装饰器设计模式应⽤-JDK源码⾥⾯的Stream IO流

装饰器设计模式在JDK源码⾥⾯应⽤场景 抽象组件&#xff08;Component&#xff09;&#xff1a;InputStream 定义装饰⽅法的规范被装饰者&#xff08;ConcreteComponent) : FileInputStream、ByteArrayInputStream Component的具体实现&#xff0c;也就是我们要装饰的具体对…

09-属性描述符Object.getOwnPropertyDescriptor(),原始数据不可重写

把原始数据作为属性值传入新对象中&#xff0c;发生原始数据修改丢失的问题怎么办&#xff1f; 应该使用Object.defineProperty()设置该属性用Object.defineProperty()设置的属性&#xff0c;默认writable、enumerable、configurable均为false并且自定义提醒该属性设置了不可重…

掌握Python的X篇_6_常量与变量、常见运算符、字符型变量

文章目录 1. 常量与变量1.1 常量与变量定义1.2 数字型变量 2. 常见运算符3. 字符型变量( 字符 )3.1 字符串变量的格式化 1. 常量与变量 简单理解&#xff0c;直接使用的数据&#xff0c;就是常量&#xff0c;最常见的常量有数字和字符串 采用ipython进行交互式编程 1.1 常量…