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

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 详解go语言对socket的抽象
# 服务端socket与客户端的交互流程
在正式介绍源码之前,我们需要简单的过一下socket通信的流程:
- 服务端创建
socket。 - 根据配置的端口号调用
bind绑定端口监听连接。 - 调用
accept阻塞监听连接。 - 客户端
socket通过connect和服务端建立连接(其底层实际上会经历一次TCP三次握手)。 - 双方进行数据收发。
- 完成通信后,客户端调用
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([]byte, 1024)
conn.Read(buf)
fmt.Printf("收到消息:%s \r\n", string(buf))
conn.Write([]byte("Message received."))
}()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
启动后我们用telnet建立连接连接,并随意出入一个字符串a,该程序就会输出如下消息:
收到消息:a
对应我们的终端也会收到程序的回复,然后连接被断开:
Message received.
遗失对主机的连接。
2
3
4
# 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)
}
2
3
4
5
步入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)
//......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
步入其内部逻辑查看,它会基于我们传入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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
最终步入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
}
2
3
4
5
6
7
8
9
10
# 接收新连接
完成监听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
}
2
3
4
5
6
7
8
9
查看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
}
2
3
4
5
6
7
8
9
以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 -1, nil, errcall, err
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 读数据
读写数据和监听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
}
2
3
4
5
6
7
8
9
查看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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 写数据
有了读数据的源码的学习基础,对于写数据的逻辑也就可以很直观的理解了,同样的调用原生socket的非阻塞写,若发现不可写(内核缓冲区已满)则将当前协程挂起,反之直接将数据到内核缓冲区等待发送:
func (c *conn) Write(b []byte) (int, error) {
//......
//调用当前socket的写方法
n, err := c.fd.Write(b)
//......
return n, err
}
2
3
4
5
6
7
最终我们也可以在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
}
}
//......
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 小结
自此我们关于go语言网络层抽象设计与实现的所有篇章都已完成,感谢您的支持。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
