在上一节中介绍了 socket 的 Listen 方法,这里进一步介绍 AcceptReadWrite 方法。

1. Accept

Accept 的核心逻辑在于:

func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}

通过 socket 返回的 fd 调用 accept 方法从 socket 上接收数据。accept 返回新 fd,通过该新 fd 建立 tcp 连接。并且通过 setKeepAlivesetKeepAlivePeriod 函数添加对应该新 fd 的 KeepAlive 属性:tcp_keepalive_time, tcp_keepalive_intvltcp_keepalive_probes

在 KeepAlive 函数中有一段函数 runtime.KeepAlive 比较有意思:

func setKeepAlive(fd *netFD, keepalive bool) error {
err := fd.pfd.SetsockoptInt(syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, boolint(keepalive))
runtime.KeepAlive(fd)
return wrapSyscallError("setsockopt", err)
}

它的存在是为了让 fd 不会被 GC 回收,更多信息可参考 issue_21402go 变量逃逸分析

继续看 accept 方法:

func (fd *netFD) accept() (netfd *netFD, err error) {
d, rsa, errcall, err := fd.pfd.Accept()
...
if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
return nil, err
} netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
return netfd, nil
}

netFD 包的是 pfd poll.FD,调用 pfd 的 Accept 方法返回 socket 上的系统文件描述符 d。将 d 包装成 netfd,接着通过 setAddr 设置 netfd 的本地地址 laddr 和 client 端地址 raddr。

poll.FDAccept 是重头戏了,接着看:

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
for {
s, rsa, errcall, err := accept(fd.Sysfd)
if err == nil {
return s, rsa, "", err
}
switch err {
case syscall.EINTR:
continue
case syscall.EAGAIN:
if fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
...
}
} func accept(s int) (int, syscall.Sockaddr, string, error) {
ns, sa, err := Accept4Func(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
switch err {
case nil:
return ns, sa, "", nil
...
}
} func accept4(s int, rsa *RawSockaddrAny, addrlen *_Socklen, flags int) (fd int, err error) {
r0, _, e1 := Syscall6(SYS_ACCEPT4, uintptr(s), uintptr(unsafe.Pointer(rsa)), uintptr(unsafe.Pointer(addrlen)), uintptr(flags), 0, 0)
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}

poll.FD 的 accept 方法中做了下面几件事:

  1. accept 函数经过 Accept4Func, accept4 到系统调用,通过系统调用号 SYS_ACCEPT4 和文件描述符 fd.Sysfd 返回作用在 socket 上的系统文件描述符和远端 socket 地址。
  2. 这里 accept 是非阻塞的,意味着即使没有 client 连接也会返回。此时返回的 err 类型为 syscall.EAGAIN
  3. 进入到 EAGAIN 错误类型中,会通过 fd.pd.pollable 方法判断是否为 true。如果为 true 阻塞当前 goroutine 直到有新的可读数据。

Accept 的实现简单介绍基本告一段落了,下面继续看 socket 的 ReadWrite 实现。

2. Read 和 Write

2.1 Read

Read 经过层层调用到 poll.FD 的 Read 方法:

func (fd *FD) Read(p []byte) (int, error) {
...
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
for {
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
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
}
}

从上述代码可以发现:

  • 网络处理逻辑通过层层封装走到 poll 的 Read,poll 是不区分文件还是网络数据的。因此,在 prepareRead 中需要通过 fd.isFile 判断。
  • maxRW 是能读取数据的最大字节,这里是 1G。原因分析在注释中:
// Darwin and FreeBSD can't read or write 2GB+ files at a time,
// even on 64-bit systems.
// The same is true of socket implementations on many systems.
// See golang.org/issue/7812 and golang.org/issue/16266.
// Use 1GB instead of, say, 2GB-1, to keep subsequent reads aligned.
  • ignoringEINTRIO 中通过 syscall.Read 函数,作用在系统调用上,通过系统调用号和文件描述符 fd.Sysfd 读取 socket 的数据到 p。
  • 类似 Accept,如果 ignoringEINTRIO 返回错误 syscall.EAGAIN,并且 fd.pd.pollable 是 true 的话,会阻塞当前 goroutine 等待读取数据。
  • 进入到 eofError 逻辑。对于文件,如果读到 EOF 则说明文件结束。对于网络数据,err 返回为 nil。

Write

类似于 ReadWrite 的核心逻辑在:

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
...
for {
max := len(p)
if fd.IsStream && max-nn > maxRW {
max = nn + maxRW
}
n, err := ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max])
if n > 0 {
nn += n
}
if nn == len(p) {
return nn, err
}
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitWrite(fd.isFile); err == nil {
continue
}
}
if err != nil {
return nn, err
}
if n == 0 {
return nn, io.ErrUnexpectedEOF
}
}
}

通过 syscall.Write 函数进入系统调用,执行 Write 调用作用于系统文件描述符 fd.Sysfd 写数据到 p。如果返回 EAGAINpollabletrue 的话则阻塞当前 goroutine 进入 waitWrite。直到数据写完,跳出 for 循环。


Go socket 编程源码解析(下)的更多相关文章

  1. Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理过程源码解析

    参考 http://blog.csdn.net/caodaoxi/article/details/12970993 Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理 ...

  2. Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段

    目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...

  3. Dubbo服务调用过程源码解析④

    目录 0.服务的调用 1.发送请求 2.请求编码 3.请求的解码 4.调用具体服务 5.返回调用结果 6.接收调用结果 Dubbo SPI源码解析① Dubbo服务暴露源码解析② Dubbo服务引用源 ...

  4. Android开发——View绘制过程源码解析(二)

    0. 前言   View的绘制流程从ViewRoot的performTraversals开始,经过measure,layout,draw三个流程,之后就可以在屏幕上看到View了.上一篇已经介绍了Vi ...

  5. Spring bean 创建过程源码解析

    在上一篇文件 Spring 中 bean 注册的源码解析 中分析了 Spring 中 bean 的注册过程,就是把配置文件中配置的 bean 的信息加载到内存中,以 BeanDefinition 对象 ...

  6. Spark作业执行流程源码解析

    目录 相关概念 概述 源码解析 作业提交 划分&提交调度阶段 提交任务 执行任务 结果处理 Reference 本文梳理一下Spark作业执行的流程. Spark作业和任务调度系统是其核心,通 ...

  7. java架构之路-(SpringMVC篇)SpringMVC主要流程源码解析(下)注解配置,统一错误处理和拦截器

    我们上次大致说完了执行流程,也只是说了大致的过程,还有中间会出错的情况我们来处理一下. 统一异常处理 比如我们的运行时异常的500错误.我们来自定义一个类 package com.springmvcb ...

  8. Tars-Java网络编程源码分析

    作者:vivo 互联网服务器团队- Jin Kai 本文从Java NIO网络编程的基础知识讲到了Tars框架使用NIO进行网络编程的源码分析. 一.Tars框架基本介绍 Tars是腾讯开源的支持多语 ...

  9. redis启动过程源码解析

    redis整个程序的入口函数在server.c中的main函数,函数调用关系如下图1,调用顺序为从上到下,从左至右. 图1 redis启动函数调用图 main函数源码如下,1-55行根据配置文件和启动 ...

  10. dubbo系列四、dubbo服务暴露过程源码解析

    一.代码准备 1.示例代码 参考dubbo系列二.dubbo+zookeeper+dubboadmin分布式服务框架搭建(windows平台) 2.简单了解下spring自定义标签 https://w ...

随机推荐

  1. 【纯手工打造】时间戳转换工具(python)

    1.背景 最近发现一个事情,如果日志中的时间戳,需要我们转换成时间,增加可读性.或者将时间转换成时间戳,来配置时间.相信大多人和我一样,都是打开网页,搜索在线时间戳转换工具,然后复制粘贴进去.个人认为 ...

  2. 从 ECMAScript 6 角度谈谈执行上下文

    大家好,我是归思君 起因是最近了解JS执行上下文的时候,发现很多书籍和资料,包括<JavaScript高级程序设计>.<JavaScript权威指南>和网上的一些博客专栏,都是 ...

  3. NetSuite 开发日记:批量增删改

    一.批量插入/创建 使用 record.create() 插入数据时,一次只能插入一条,有多条数据需要插入时只能通过循环的方式,这样效率非常慢,耗时会随着数据量的增大而呈线性增长,有一种巧妙的方式可以 ...

  4. 基于源码去理解Iterator迭代器的Fail-Fast与Fail-Safe机制

    原创/朱季谦 在Java编程当中,Iterator迭代器是一种用于遍历如List.Set.Map等集合的工具.这类集合部分存在线程安全的问题,例如ArrayList,若在多线程环境下,迭代遍历过程中存 ...

  5. Android SDK Manager 报错“加载 SDK 组件信息失败”。(Android SDK Manager complains with "Loading SDK component information failed."

    [解决方案]: 将存储库设置更改为 Google .因此,在 android SDK 管理器上点击齿轮图标(设置),然后点击 Repository -> Google.

  6. React 类组件转换为函数式

    函数式的 React 组件更加现代,并支持有用的 hooks,现在流行把旧式的类组件转换为函数式组件.这篇文章总结了转换的一些通用的步骤和陷阱. 通用替换 定义 从 class (\w+) exten ...

  7. linux内核initcall放置在各个section中函数执行流程

    前言 linux以及嵌入式一些代码,我们看到core_initcall.device_initcall等等需要链接器分配各个section,并且在启动该模块时候执行.下面我们详细追溯一下执行过程. 作 ...

  8. ChatGPT的中转站(欧派API) oupuapi,不扶墙也能上楼

    开启智能生活新篇章:oupo中转站(欧派)--引领人工智能大模型的枢纽 在人工智能技术日新月异的今天,我们荣幸地向您推介oupo中转站(欧派)--这一汇聚各类顶尖人工智能大模型的平台.它不仅为技术研发 ...

  9. 从传统行业到半导体行业开发(YMS,DMS,EAP,EDA)

    一线开发人: 今天半导体YMS 项目快要收尾了,我的心情有点高兴,多年来我一直保持着写作的习惯,总是想写一些什么,今天但是又不知道从何说起.自己从传统的行业转向左半导体行业开发.从电*机如软件开发到电 ...

  10. nacos系列:spring cloud使用nacos实现配置管理和服务发现

    目录 版本说明 创建项目 版本说明 IDEA:2021.3 Maven:3.6.3 Jdk:17 Spring-Boot:2.6.13 Spring-Cloud:2021.0.5 Spring-Clo ...