0. socket 介绍

Liunx 中一切皆文件。通过文件描述符和系统调用号可以实现对任何设备的访问。同样的,socket 也是一种文件描述符。通过 socket 可以建立网络传输。对于 TCP 和 UDP 来说,其底层都是基于 socket 进行网络通信。

本文通过代码示例介绍 socket 以加深对 socket 的理解。

1. 代码示例

从代码入手逐层分析 socket 实现。server 端代码如下:

addr := "default-route-openshift-image-registry.apps.xxx.net:6445"
tcpAddr, err := net.ResolveTCPAddr("tcp4", addr)
if err != nil {
fmt.Println(err)
} listener, err := net.ListenTCP("tcp4", tcpAddr)
result := make(chan error)
go handleResult(result) for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
} go handleClient(conn, result)
}

注:handleClient 代码可参考 server.go

上述 server 端代码有几点要注意的是:

  • 可使用 for + goroutine 的方式实现并发。
  • server 端可使用 net.Conn 的 SetReadDeadline 实现读数据的长连接,更多内容可参考 net 包。
  • server 端不应报错,而应将错误结果传给 client,由 client 处理。
  • 读取网络数据需要指定读取字节大小,防止 flood attack。
  • 如果子 goroutine 调用 os.Exit() 主程序也会退出。

client 端代码如下:

tcpConn, err := net.DialTCP("tcp4", nil, tcpAddr)
defer tcpConn.Close() if err != nil {
fmt.Println(err)
} for {
if _, err = tcpConn.Write([]byte("hello, server")); err != nil {
fmt.Println("Error: ", err)
break
} reply := make([]byte, 256)
_, err := tcpConn.Read(reply)
if err != nil {
fmt.Println("Error: ", err)
break
} time.Sleep(30 * time.Second)
}

从代码可以看出:

  • 长连接仅当 client 或 server 有一方关闭连接时,连接才关闭。对于上述 client 端代码会循环写数据到 socket,直到 server 端读取超时,关闭长连接。

2. socket 实现

下面重点介绍实现 socket 网络传输的 Listen,Accept,Read and Write 方法。socket 结构图如下:

2.1 Listen

示例中调用 net 包的 ListenTCP 函数实现 socket 的监听:

listener, err := net.ListenTCP("tcp4", tcpAddr)

type TCPListener struct {
fd *netFD
lc ListenConfig
}

ListenTCP 返回一个 TCPListener 类型对象 listener,该对象实现 Listener 接口:

// src/net/net.go

type Listener interface {
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error) // Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error // Addr returns the listener's network address.
Addr() Addr
}

其包括一个网络文件描述符 netFD 和配置 ListenConfig:

type netFD struct {
pfd poll.FD // immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
} type ListenConfig struct {
Control func(network, address string, c syscall.RawConn) error
KeepAlive time.Duration
}

顺着 ListenTCP 往下走,部分代码做简化处理:

func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) {
sl := &sysListener{network: network, address: laddr.String()}
ln, err := sl.listenTCP(context.Background(), laddr)
if err != nil {
return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: laddr.opAddr(), Err: err}
}
return ln, nil
} func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
if err != nil {
return nil, err
}
return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
} func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn)
}

internetSocket 的传参 syscall.SOCK_STREAM 是流式 socket 的系统调用号,对应的是 TCP 协议。另一种 syscall.SOCK_DGRAM 是数据报式 socket,对应的是 UDP 协议。

socket 函数返回的是网络文件描述符 netFD:

type netFD struct {
pfd poll.FD // immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}

socket 函数是这里介绍的重点:

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
s, err := sysSocket(family, sotype, proto)
if err != nil {
return nil, err
} if fd, err = newFD(s, family, sotype, net); err != nil {
poll.CloseFunc(s)
return nil, err
}
}

它做了两块事情,第一通过 sysSocket 执行系统调用返回 socket 的文件描述符 id。第二通过 netFD 返回文件描述符结构。

首先看 sysSocket 逻辑:

func sysSocket(family, sotype, proto int) (int, error) {
s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
...
return s, nil
} // go/src/syscall/syscall_unix.go
func Socket(domain, typ, proto int) (fd int, err error) {
fd, err = socket(domain, typ, proto)
return
} func socket(domain int, typ int, proto int) (fd int, err error) {
r0, _, e1 := RawSyscall(SYS_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto))
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
} func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {
return RawSyscall6(trap, a1, a2, a3, 0, 0, 0)
} // go/src/syscall/asm_plan9_amd64.s
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
MOVQ trap+0(FP), BP // syscall entry
// slide args down on top of system call number
LEAQ a1+8(FP), SI
LEAQ trap+0(FP), DI
CLD
MOVSQ
MOVSQ
MOVSQ
MOVSQ
MOVSQ
MOVSQ
SYSCALL
MOVQ AX, r1+56(FP)
MOVQ AX, r2+64(FP)
MOVQ AX, err+72(FP)
RET

可以看到,通过层层调用到汇编,根据系统调用号 SYS_SCOKET 执行系统调用,返回 socket 的文件描述符 fd。

接着通过 netFD 包含该文件描述符生成网络文件描述符对象:

func newFD(sysfd, family, sotype int, net string) (*netFD, error) {
ret := &netFD{
pfd: poll.FD{
Sysfd: sysfd,
IsStream: sotype == syscall.SOCK_STREAM,
ZeroReadIsEOF: sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW,
},
family: family,
sotype: sotype,
net: net,
}
return ret, nil
}

生成网络文件描述符后,socket 继续执行:

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
...
if laddr != nil && raddr == nil {
switch sotype {
case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
case syscall.SOCK_DGRAM:
if err := fd.listenDatagram(laddr, ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
}
}
if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
}

根据 laddr 和 raddr 判断执行 socket 的行为,如果是 laddr 有值而 raddr 无值,则判断当前行为为 listen。通过判断 socket 类型确定 listen 的是 TCP 还是 UDP socket。以 TCP socket 为例,走到 listenStream 方法:

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
...
if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
return os.NewSyscallError("bind", err)
}
if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
return os.NewSyscallError("listen", err)
}
}

首先调用 Bind 方法执行系统调用绑定本地 addr 到 socket:

func bind(s int, addr unsafe.Pointer, addrlen _Socklen) (err error) {
_, _, e1 := Syscall(SYS_BIND, uintptr(s), uintptr(addr), uintptr(addrlen))
if e1 != 0 {
err = errnoErr(e1)
}
return
} func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {
runtime_entersyscall()
r1, r2, err = RawSyscall6(trap, a1, a2, a3, 0, 0, 0)
runtime_exitsyscall()
return
}

这里的参数要稍加介绍下,第一个参数为系统调用号,第二个是 socket 文件描述符,第三个是绑定到 socket 上的 addr,第四个是 addr 的长度。对于 socket bind 系统调用来说,这些信息都是需要的。

socket 绑定后继续执行就需要 Listen socket,类似地执行 SYS_LISTEN 系统调用:

func Listen(s int, n int) (err error) {
_, _, e1 := Syscall(SYS_LISTEN, uintptr(s), uintptr(n), 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}

通过以上分析可以看出,通过层层调用系统调用实现 socket 的创建,绑定和监听。


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. Laravel学习笔记之Session源码解析(上)

    说明:本文主要通过学习Laravel的session源码学习Laravel是如何设计session的,将自己的学习心得分享出来,希望对别人有所帮助.Laravel在web middleware中定义了 ...

  8. hadoop学习记录--hdfs文件上传过程源码解析

    本节并不大算为大家讲接什么是hadoop,或者hadoop的基础知识因为这些知识在网上有很多详细的介绍,在这里想说的是关于hdfs的相关内容.或许大家都知道hdfs是hadoop底层存储模块,专门用于 ...

  9. java架构之路-(SpringMVC篇)SpringMVC主要流程源码解析(上)源码执行流程

    做过web项目的小伙伴,对于SpringMVC,Struts2都是在熟悉不过了,再就是我们比较古老的servlet,我们先来复习一下我们的servlet生命周期. servlet生命周期 1)初始化阶 ...

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

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

随机推荐

  1. flchart库判断当前点击的底部title的index

    使用flchart库,版本0.35,在点击柱状图时,当柱状图的高度为0,默认选中热区很小,很难点击选中对应区域,如图一 9和10的柱状图高度为0. 查了源码,貌似没有单独针对底部title设置点击事件 ...

  2. VMware安装虚拟机详细步骤

    在VMware中安装CentOS7 01.目录 CentOS7的下载 CentOS7的配置 CentOS7的安装 CentOS7的网络配置 自动获取IP 固定获取IP 02.安装前提 准备工作: 提前 ...

  3. 云端开炉,线上训练,Bert-vits2-v2.2云端线上训练和推理实践(基于GoogleColab)

    假如我们一定要说深度学习入门会有一定的门槛,那么设备成本是一个无法避开的话题.深度学习模型通常需要大量的计算资源来进行训练和推理.较大规模的深度学习模型和复杂的数据集需要更高的计算能力才能进行有效的训 ...

  4. mysql 定时 数据库备份并上传到另一台服务器上,上传结束并删除源文件

    首先总共有两个脚本: #!/bin/bash:主要用于进行数据库备份.压缩.删除,单独运行命令是:bash  XXX.sh #!/usr/bin/expect:主要用于进行数据备份文件的上传,单独运行 ...

  5. CTFshow元旦水友赛 CRYPTO WP

    CRYPTO 新年祝福 题目 加油!为跨年夜还在努力的自己加油! ctfshow全体工作人员,祝您学业有成,阖家幸福! 解码下面base64 Y3Rmc2hvd3vmlK/ku5jlrp3lj6Pku ...

  6. 深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包

    1. 堆栈 在JavaScript中,内存堆是内存分配的地方,调用栈是代码执行的地方. 原始类型的保存方式:在变量中保存的是值本身,所以原始类型也被称之为值类型. 对象类型的保存方式:在变量中保存的是 ...

  7. 使用XDT提高开发效率

    使用XDT提高开发效率 XDT介绍 XDT(XML Document Transformation)技术是一种用于对XML文档进行转换的技术.它通常用于在部署或配置过程中,根据不同的环境或条件自动修改 ...

  8. 开发篇1:使用原生api和Langchain调用大模型

    对大模型的调用通常有以下几种方式:方式一.大模型厂商都会定义http风格的请求接口,在代码中可以直接发起http请求调用:方式二.在开发环境中使用大模型厂商提供的api:方式三.使用开发框架Langc ...

  9. 2023-08-26:请用go语言编写。开心一下的智力题: 有一个村庄,一共250人, 每一个村民要么一定说谎,要么只说真话, 村里有A、B、C、D四个球队,且每个村民只会喜欢其中的一支球队, 但是说

    2023-08-26:请用go语言编写.开心一下的智力题: 有一个村庄,一共250人, 每一个村民要么一定说谎,要么只说真话, 村里有A.B.C.D四个球队,且每个村民只会喜欢其中的一支球队, 但是说 ...

  10. 实践案例丨CenterNet-Hourglass论文复现

    摘要:本案例是CenterNet-Hourglass论文复现的体验案例,此模型是对Objects as Points 中提出的CenterNet进行结果复现. 本文分享自华为云社区<Center ...