Go 多进程编程-socket(套接字)
多进程编程–套接字
文章目录
- 多进程编程--套接字
- 套接字
- 1. 从系统调用看
- 参数1: 通信域
- 参数2: 类型
- 参数3: 协议
- 返回值
- 2. 基于 TCP/IP 协议栈的 socket 通信
- 用 Go 的 socket api 编程
- socket 流程简介
- 实际编码(服务端)
- Listen()
- Accept()
- 实际编码(客户端)
- Dial()
- 小总结
- 关于 `net.Conn`
- 1. Read 方法
- 2. Write 方法
- 3. Close 方法
- 4. LocalAddr 和 RemoteAddr 方法
- 5. SetDeadline, SetReadDeadline, SetWriteDeadline 方法
文章是书籍: 《 Go 并发编程实战 》的读书笔记.
套接字
1. 从系统调用看
socket 常译作套接字, 同样是一种 IPC 方法. 与其他 IPC 方法不同之处是: 可以通过网络连接让多个进程建立通信并且相互传递数据, 这也就使得通信双方不用是一台单机的进程. 事实上这也就是 socket 的目标之一: 使得通信端的位置透明化.
大多数操作系统有自己的 socket 实现, 大多数编程语言同样也支持 socket.
以下从操作系统的 socket 接口讲起.
先看 Linux 系统自带的 socket 系统调用, 函数声明如下:
int socket(int domain, int type, int protocal)
这个系统调用的功能是创建一个 socket 实例, 接受这三个参数分别是:
- 通信域
- 类型
- 通信所用的协议
参数1: 通信域
每个 socket 必定存在于一个通信域中, 通信域决定了这个 socket 的地址格式和通信范围:
通信域 | 含义 | 地址形式 | 通信范围 |
---|---|---|---|
AF_INET | IPv4域 | IPv4地址(4Byte), 端口号(2Byte) | 在基于IPv4协议的网络中任意两台计算机之上的两个应用程序 |
AF_INET6 | IPv6域 | IPv6地址(16Byte), 端口号(2Byte) | 在基于IPv6协议的网络中任意两台计算机之上的两个应用程序 |
AF_UNIX | Unix域 | 路径名称 | 在同一台计算机上的两个应用程序 |
三个通信域都用 AF_ 作为前缀, AF 是 address family 的缩写, 意思就是 地址族, 这也暗示了每个域的 socket 地址有不同的格式. 此外, IPv4 和 IPv6 通信实在网络范围内, Unix 域通信则是单台计算机范围内.
参数2: 类型
socket 有许多类型, 包括 SOCK_STREAM
, SOCK_DGRAM
, 以及更底层的 SOCK_RAW
, 针对某个较新型数据传输技术的 SOCK_SEQPACKET
.
以上 socket 类型的相关特性如表所示(展示了不同 socket 类型的 5 个特性):
特性 | SOCK_DGRAM | SOCK_RAW | SOCK_SEQPACKET | SOCK_STREAM |
---|---|---|---|---|
数据形式 | 数据报 | 数据报 | 字节流 | 字节流 |
数据边界 | 有 | 有 | 有 | 没有 |
逻辑链接 | 没有 | 没有 | 有 | 有 |
数据有序性 | 不能保证 | 不能保证 | 能保证 | 能保证 |
传输可高兴 | 不具备 | 不具备 | 具备 | 具备 |
有两种数据形式:
- 数据报
- 字节流
以数据报为数据格式意味着数据接收方的 socket 接口程序可以意识到数据的边界然后对他们做切分, 这样能省去接收方应用程序寻找数据边界和切分数据的工作量.
以字节流为数据形式, 实际上是传输一个字节接着一个字节的串, 可以想象其为一个很长的字节数组.
常规情况下, 字节流不能体现出哪些字节在哪个数据包, 所以 socket 接口程序无法从其中分离出独立的数据包, 只能把这件事交给应用程序完成.
不过类型为 SOCK_SEQPACKET
的 socket 接口程序是个例外. 数据发送方的 socket 接口程序是可以记录这个数据边界的.此处的数据边界就是应用程序每次发送的字节流片段之间的分界点, 这些数据边界信息会随着字节流一同发往数据接收方.
数据接收方的 socket 接口程序会根据数据边界将字节流切分成(或者说是还原成)若干个字节流片段然后按照需要依次传递给应用程序.
-
对于 面向有连接的 socket,必须先建立逻辑连接。
在连接建好之后,通信双方可以很方便地互相传输数据。并且,由于连接已经暗含了双方的地址,所以在传输数据的时候不必再指定目标地址。
两个面向有链接的 socket 之间建立连接后,它们发送的数据就只能发送到连接的另一端。
-
对于 面向无连接的 socket 则完全不同,这类 socket 在通信时无需建立连接。它们传输的每一个数据包都是独立的,并且会直接发送到网络上。
这些数据包中都含有目标地址,因此每个数据包都可能传输至不同的目的地。
在面向无连接的 socket 上,数据流只能是单向的。也就是说,我们不能使用同一个面向无连接的 socket 实例既发送数据又接收数据。
socket 是否面向连接很大程度决定了数据传输的有序性和可靠性.
存在逻辑链接, 通信双方才能通过部分手段保证从数据发送方法的数据能 及时, 正确, 有序 到达数据接收方, 被数据接收方接受.
然后是一个特殊的 socket 类型: SOCK_RAW
, 该类型提供了一个直接通过底层传输数据的方法(TCP/IP协议栈中的IP层). 为保证安全, 该应用程序要有超级用户权限(su / sudo)才可使用. 而且用这种方法成本也比较高, 应用需要自己构建数据的传输格式(就像是自己构建一个类似于 TCP 的数据段格式 / UDP 的数据包格式).所以说一般情况下都不用该类型的 socket.
参数3: 协议
调用系统调用 socket()
时, 一般将 0 作为第三个参数值传入, 含义是让操作系统内核依据第一个参数和第二个参数自行决定一个 socket 协议, 也就是说 socket 的通信域和类型域所用协议之间有对应关系如下(两两组合):
决定因素 | SOCK_DGRAM | SOCK_RAW | SOCK_SEQPACKET | SOCK_STREAM |
---|---|---|---|---|
AF_INET | UDP | IPv4 | SCTP | TCP / SCTP |
AF_INET6 | UDP | IPv6 | SCTP | TCP / SCTP |
AF_UNIX | 有效 | 无效 | 有效 | 有效 |
-
TCP (Transmission Control Protocol, 传输控制协议)
-
UDP (User Datagram Protocol, 用户数据报协议)
-
SCTP (Stream Control Transmission Protocol,流控制传输协议)
以上三者都是 TCP/IP 协议栈中的传输层协议.
- IPv4
- IPv6
分别代表 TCP/IP 协议栈中的网络互连层协议 IP (Internet Protocol, 网际协议)的第4个版本和第6个版本.
- “有效” 表示该通信域和类型的组合会使内核选择某个内部的 socket 协议
- “无效” 表示该通信域和类型的组合不合法
Go 提供的 socket API 中同样设计这些组合, 并且使用一些专门的字符串字面量表示他们.
返回值
最后再看返回值, 在无错误发生情况下, socket()
将返回一个 int 类型的值, 该值是 socket 实例唯一标识符的文件描述符.
得到该标识符之后就可以利用 socket 办事了, 比如说绑定和监听端口, 发送和接收数据, 关闭 socket 实例一系列方法调用.
2. 基于 TCP/IP 协议栈的 socket 通信
之前说了 socket 可以在网络上不同主机多个应用程序之间用于通信, 同时也可以在但主机上多个应用程序之间通信. 不过多数情况下用 socket 都是为了在不同主机之间通信, 这种情况下一般是基于 TCP / IP 协议栈.
用 Go 的 socket api 编程
socket 流程简介
这里用 Go 完成一个服务端程序和客户端程序, 两者通过 socket 进行通信. 这是一个简单示例, 在通信流程中, 客户端程序和服务端程序建立连接后酯交换一次数据, 而在实际的应用场景中则会进行多次.
注意: 下图中虚线框之内的子流程一般会循环很多次
使用函数 Listen()
获取一个监听器, 函数原型如下:
func Listen(net, laddr string) (Listener, error)
接受两个 string
类型作为参数
-
参数1: 表示以何种协议监听给定的地址, 在 Go 中用一些字符串字面量表示, 如表:
字面量 socket 协议 备注 “tcp” TCP 无 “tcp4” TCP 网络互联层协议仅支持IPv4 “tcp6” TCP 网络互联层协议仅支持IPv6 “udp” UDP 无 “udp4” UDP 网络互联层协议仅支持IPv4 “udp6” UDP 网络互联层协议仅支持IPv6 “unix” 有效 可以看作通信域为AF_UNIX且类型为SOCK_STREAM时内核采用的默认协议 “unixgram” 有效 可以看作通信域为AF_UNIX且类型为SOCK_DGRAM时内核采用的默认协议 “unixpacket” 有效 可以看作通信域为AF_UNIX且类型为SOCK_SEQPACKET时内核采用的默认协议 这个参数代表的一定是 面向流 的协议. (TCP 和 SCTP 时面向流的). 不同的是, 基于 TCP 实现的应用无法记录和感知到任何消息边界(也就是说没办法从字节流里分理处消息), 不过基于 SCTP 构建的应用程序是可以做到的.
(ps: 此处说的消息其实是数据包在 TCP/IP 协议栈的应用层中的称谓, 消息边界也就和数据边界的概念基本相同. 区别是消息边界仅针对消息, 数据边界针对的对象范围更广泛. 此外数据段是基于 TCP 协议编写的应用程序为了使数据流满足网络传输要求做的分段, 和这里说的区分独立消息的消息边界没关系.)
综上:
net.Listen
函数的第一个参数一定是tcp
,tcp4
,tcp6
,unix
,unixpacket
中的一种.tcp4
和tcp6
分别仅与基于 IPv4 的 TCP 协议和基于 IPv6 的 TCP 协议相对应. tcp 表示 socket 所用的 TCP 协议会兼容这两个版本的 IP 协议.unix
和unixpacket
分别代表两个通信域为 Unix 域的内部 socket 协议, 依照二者构建的应用程序仅能够用于本地计算机上不同应用之间的通信. -
参数2:
对于使用 TCP 协议的 socket 而言,
laddr
的值表示当前程序在网络中的标识.laddr
是 Local Address 的缩写, 格式为 host:port-
其中的 host 表示 IP 地址或者主机名, 此处的内容一定要是和当前的计算机对应的 IP 地址或者是主机名, 否则调用该函数会报错.
host 填的是主机名的话, 函数调用时内部会先走 DNS(Domain Name System, 域名系统)找到和该主机名对应的 IP 地址. 如果 host 处的主机名没有在 DNS 注册, 那么也会报错.
-
port 代表当前程序要监听的端口号
-
举个例子: 127.0.0.1:8085
-
实际编码(服务端)
Listen()
第一步: 调用 net.Listen()
监听, 等待客户端连接 (Listen()
就像是用c写时, socket()
, bind()
, listen()
函数三合一)
listener, err := net.Listen("tcp", "127.0.0.1:8085")
net.Listen()
调用后返回二值:
- 值1:
net.Listener
类型, 代表监听器 - 值2:
error
类型, 标识错误
Accept()
第二步: 调用 Accept()
方法接受连接
conn, err := listener.Accept()
调用监听器(变量listener
)的 Accept
方法时, 流程将被阻塞, 直到某个客户端程序与当前程序建立了 TCP 连接, 之后函数返回两值:
- 值1:
net.Conn
类型, 代表了当前的 TCP 连接 - 值2:
error
类型, 参数值非法时为 nil
实际编码(客户端)
Dial()
第一步: 调用 net.Dial()
函数向指定的网络地址发送连接建立申请 (net.Dial
就像是c的 socket()
, bind()
, connect()
函数三合一)
func Dial(network, address string) (Conn, error)
函数 net.Dial()
也接受两个参数。
-
其中,
network
与net.listen
函数的第一个参数net
含义非常类似, 但是它比后者拥有更多的可选值, 因为在发送数据之前不一定要先建立连接。像UDP协议和IP协议都是面向无连接型的协议, 所以
udp
、udp4
、udp6
、ip
、ip4
和ip6
都可以作为参数network
的值。其中,
udp4
和udp6
分别代表了仅基于IPv4的UDP协议和仅基于IPv6的UDP协议,而udp
代表的UDP协议则在它基于的IP协议的版本上没有任何限制。另外,
unixgram
也是network
参数的可选值之一. 与unix
和unixpacket
相同,unixgram
也代表了一种基于 Unix 域的内部 socket 协议。但不同的是,后者以数据报作为传输形式。 -
函数
net.Dial
的第二个参数address
的含义与net.listen
函数的第二个参数laddr
完全一致。如果想与前面刚刚开始监听的服务端程序连接, 那么这个参数的值就是该服务端的地址,即为127.0.0.1:8085。
因此,这个参数的名称
address
其实也可由 raddr (RemoteAddress) 代替。名称 laddr 和raddr 都是相对的, 前者指的是当前程序所使用的地址(本地地址), 而后者则指的是参与通信的另一端所使用的地址(远程地址)。
在net代码包的函数或方法声明中,会经常见到这两个参数名称。
函数有两个返回值:
-
值1: 类型为
net.Conn
, -
值2: 类型为
error
, 参数值非法时为 nil.特殊情况下, 对基于 TCP 协议的连接请求而言, 当远程地址上没有程序正在执行 Listen() 监听, 那么会让
net.Dial()
函数返回一个非nil
的值
网络中存在时延问题, 在收到一方有效回应(成功和失败都可以)之前, 发送连接请求一方就会始终阻塞等待, 超过等待时间(超时(timeout)时间)之后, 函数执行就会结束然后返回相应的 error
类型的值.
不同操作系统对于不同协议的连接请求设定有不同的超时时间, 在 Linux 上, 基于 TCP 协议的连接请求, 超时时间一般设定为 75 秒, 和其它超时时间这算是一个比较短的时间. 操作系统为了解决问题, 允许用户自定义超时时间.
net.Dial()
函数对应的设定超时时间的函数是 net.DialTimeout
, 函数声明如下:
func DialTimeout(network, address stirng, timeout time.Duration) (Conn, error)
第三个参数专门用来设定超时时间, 类型是 time.Duration
(一个 int64
类型的别名类型), 单位为纳秒.
比如说这样一个例子, 可以在请求 TCP 连接的同时把超时时间设定为 2 秒:
conn, err := net.DialTimeout("tcp", "127.0.0.1:8085", 2 * time.Second)
小总结
以上三个 API 就能完成客户端和服务器之间的 socket 连接建立了, Go 封装了系统调用的一系列 socket API 使得使用起来更简单, 比如说本地地址绑定(bind
) 就隐藏在了 net.Listen()
之中.
服务器启动监听并且等待连接请求之后, 一旦受到客户端连接请求, 服务端就会和客户端建立 TCP 连接(三次握手)
连接建立后, 客户端和服务器都会获得一个类型为 net.Conn
的值, 之后就可以用这个变量来做操作了.
注意: GO 的 socket api 在底层获取一个非阻塞的 socket 实例, 意味着该实例上的数据读取操作都是非阻塞式.
比如应用程序用系统调用 read()
从 socket 接收缓冲区读数据,
- 就算是接收缓冲区内没数据,
read()
也不会阻塞, 而是返回一个错误码为EAGAIN
的错误. 但是应用程序应该忽略这个错误, 等一会之后再读取. - 如果在读取数据时接收缓冲区有数据, 那么
read()
会直接返回这些数据, 不论多少数据都是如此, 哪怕只有一个字节. 这个特性被称为部分读(partial read) - 写入数据也是如此, 就算是发送缓冲区满了, 调用系统调用
write()
也不会阻塞, 而是直接返回错误码为EAGAIN
的错误, 同样的应用程序应该忽略这个错误, 等一会之后再写入. - 如果发送缓冲区有少量空间但是不够放一段数据,
write()
会尽可能写入部分数据然后返回已经写入了的字节的数据量, 这个特性称为部分写(partial write). 应用程序应当在每次调用write()
之后都对返回结果做检查, 如果发现了数据没有被完全写入, 那么就要继续写入剩下的数据.
对于非阻塞式风格的 socket 接口来说, 除了 read()
和 write()
之外, 系统调用 accept( )
也是非阻塞风格. accept()
不会被阻塞来等待连接, 而是会直接返回一个错误码 EAGAIN
. 这里和前面说的 net.Listener
的 accept()
行为(被调用时阻塞直到新连接到来)不符, 现在解释原因:
Go 的 socket API 实际上对底层系统调用做了一系列的封装, 让某些非阻塞式的系统调用API变为阻塞式的GO socket API, 不过应当清楚他们在底层仍然为非阻塞式.
- 系统调用
write()
写方法, 就是一个非阻塞式 API, GO 实现将其变为阻塞式 API, 直到把所有的数据全部写入到 socket 的发送缓冲区之后该 API 才会返回, 除非写入过程中发生了某些错误. - 系统调用
read()
读方法, 同样也是非阻塞式 API, 在 GO 中也是非阻塞式的(保留了原系统调用的特性). 原因在于 TCP 协议之上传输的数据全都是字节流形式, 所以数据接收方无法感知数据边界(消息边界). 所以 socket API 也不知道应该在什么时候返回, 那么把这个数据切分和分批返回的任务上抛给应用程序自己实现就是好做法. 我们只需要在应用程序内对部分读做一些额外处理就好.
关于 net.Conn
net.Conn
一个接口类型, 在方法集合中有 8 个方法, 定义了可在一个链接上发生的所有行为.
1. Read 方法
Read
用于从 socket 接收缓冲区中读数据, 方法声明如下:
Read(b []byte) (n int, err error)
接受一个 byte切片, 也就是一个用来存放从连接上接收到的数据的容器.
Read 方法会尝试填满这个切片, 如果原来有数据那就替换相应位置, 所以用的时候要保证传入的每次都是一个空容器(切片内的值为全0).
- 一般情况下,
Read()
只有在填满这个切片之后才返回 - 部分情况下,
Read()
在还没填满之前就返回了, 这种情况可能是相关的网络数据缓存机制导致的. - 不过不论如何, 如果
Read()
还没有填满切片, 而该切片的靠后部分又存在着遗留的元素值(传入之前未清零), 那么在用的时候就一定要注意数据混乱问题了.
Read()
返回二值
- 值1: 本次操作实际读取到的字节数, 也就是往参数那个切片里写入的字节数
- 值2: 一个错误
使用示例:
b := make([]byte, 10)
n, err := conn.Read(b)
if err != nil {// handle error here...
}
content := string(b[:n])
然后详细说一下这里的错误处理问题:
- 如果应用程序在从 socket 的接收缓冲区读数据时发现 TCP 连接已经被另一端关闭了, 那么就会立即返回一个 error , 其值为
io.EOF
, 这个值象征文件内容完结了. 也就是说在这个 TCP 连接之上没有可以再读取的数据了, 该 TCP 连接也就没用可以关掉了. 示例如下:
// 用来存接收到的所有数据
var dataBuffer bytes.Bufferb := make([]byte, 10)
for {// 读从网络上接收到的数据n, err := conn.Read(b)if err != nil {// 如果 TCP 正常关闭了if err == io.EOF {fmt.Println("The connection is closed.")// 直接释放这个连接conn.Close()} else {fmt.Printf("Read Error:%s\n", err)}break}// 没有错误的话, 把数据写入到 dataBufferdataBuffer.Write(b[:n])
}
上面的代码展现了一个基本的在 TCP 连接上读数据的过程.
这个例子很粗糙, 因为我们用 TCP/IP 协议栈的话, 将得到一个字节流, 把得到的数据一股脑写入到缓冲区没什么作用, 我们应当做一些切分操作.
这里使用 bufio
中的 API 实现一些复杂的数据切分操作, bufio
是 Buffered I/O
的缩写, bufio
提供了与带缓存的 I/O 操作有关的 API, 比如说通过包装不带缓存的 I/O类型值的方式增强功能. 此处的 net.Conn
类型实现了 io.Reader
类型的唯一方法 Read
, 所以说可以用 bufio.NewReader()
函数包装变量 conn
, 就像是这样:
reader := bufio.NewReader(conn)
之后通过调用 ReadBytes()
方法一次获取经过切分后的数据. ReadBytes()
方法接受一个 byte
类型的值, 这个参数值是通信两端协商一致的消息边界(比如可以是一个特殊的字符). 示例如下:
line, err := reader.ReadBytes('\n')
这里用换行符作为消息边界, 一般情况下每次调用 ReadBytes()
方法之后, 都能得到一段以该消息边界为结尾的数据, 不过多数时候, 消息边界定位不是简单的查找一个单字节字符就完事了.
在 HTTP 协议中就规定了, HTTP 消息的头部信息结尾一定是连续的两个空行, 也就是字符串: "\r\n\r\n"
, 获取到头部信息后, 相关程序会通过其中名为 Content-Length
的信息项得到 HTTP 消息的数据部分的长度, 有了这个关键信息就可以切分数据了.
为了满足这个要求, bufio
包为我们提供了一些更高级的 API, 比如说函数 bufio.NewScanner()
, 类型 bufio.Scanner
以及一些其他方法.
2. Write 方法
Write()
用来向 socket 发送缓冲区写入数据. 方法声明如下:
Write(b []byte) (n int, err error)
同样可以用代码包 bufio
中的 API 简化并且让这里的写操作更灵活.
net.Conn
类型也是一个实现了 io.Writer
接口的类型, 所以说 net.Conn
类型的值可以作为函数 bufio.NewWriter()
的参数来用:
writer := bufio.NewWriter(conn)
与前面示例中的变量 reader
类似,writer
可以看作是针对变量 conn
(代表一个TCP连接)的一个缓冲写入器。
- 可以通过调用其上的以
Write
为名称前缀的方法来分批次地向其中的缓冲区写入数据, - 也可以通过调用它的
ReadFrom
方法来直接从其他io.Reader
类型值中读出并写入数据, - 还可以通过调用
Reset
方法以达到重置和复用它的目的。 - 在向其写入全部数据之后,务必调用
Flush
方法,保证其中的所有数据都真正写入到了它代理的对象(这里是由conn
变量表示的TCP连接)中。 - 此外,要注意该缓冲写入器的缓冲区容量,其默认值是4096个字节。
- 在调用以
Write
为名称前缀的方法时,如果参数的字节数量超出了此容量,那么该方法就会尝试把这些数据的全部或一部分直接写入到它代理的对象中,而不会先在缓冲写入器自己的缓冲区中缓存这些数据。也就是说此时缓冲这一作用就失效了. 为了解决这个问题,可以通过调用bufio.NewriterSize
函数来初始化一个缓冲写入器。该函数与bufio. Newwriter
函数非常类似,但它可以自定义缓冲区容量。
3. Close 方法
Close()
方法将会关闭当前连接, 该方法不接受任何参数, 返回一个类型为 Error
的值. 调用Close()
方法之后, 对已关闭的链接再调用 Read()
, Write()
, Close()
都会立刻返回一个 Error
类型值, 这个错误已在 net
包里定义了, 提示信息是这样的: use of closed network connection
, 所以看到这个错误信息之后就要立刻想到是不是发生了上面的错误.
另外, 调用 Close()
方法时, Read()
或者 Write()
如果正处在调用中而且还没有执行结束的话, 同样读写方法也会立即结束执行然后返回一个非 nil
的 Error
类型值, 就算是读写仍处在阻塞之中也是这样.
4. LocalAddr 和 RemoteAddr 方法
这两个方法都不接受参数, 返回值是一个 net.Addr
类型值.
返回值是参与当前通信的某一端程序在网络中的地址.
然后是与该方法相关的一个类型 net.Addr
, 这是一个接口类型, 方法集合中共有两个方法:
-
Network()
, 返回当前连接所使用的协议的名称, 比如:conn.LocalAddr().Network()
将会返回
“tcp”
-
String()
, 返回一个相应地址, 对于IPv4, 返回的地址格式为: host:port .比如: “127.0.0.1:8085”, 当客户端连接到来时, 使用如下语句获取该连接另一端程序的网络地址:
conn.RemoteAddr().String()
另外对于客户端来说, 准备连接服务器之前, 如果在通信时没有指定本地地址, 那么使用如下语句可以获取到操作系统内核为该客户端程序分配的网络地址:
conn.LocalAddr().String()
5. SetDeadline, SetReadDeadline, SetWriteDeadline 方法
这三个方法都只接收一个 time.Time
类型的值作为参数, 返回一个 error
类型的值作为结果.
-
SetDeadline()
, 方法用来设定当前连接上的 I/O 操作(包括而且不仅限于读写)的超时时间.此处的超时时间是一个绝对时间, 如果在调用
SetDeadline
之后相关 I/O 操作到达了指定的超时时间没完成, 那么将立即结束执行然后返回一个error
, 这个error
在包net
中设定, 提示信息为“i/o timeout”
.这个特性意味着, 如果要设定一个超时时间, 那么一定是每次读取数据之前都执行一次这个函数(因为这个函数执行后对所有的 I/O 操作都起效).
-
错误示例:
b := make([]byte, 10) conn.SetDeadline(time.Now().Add(2 * time.Second)) for {n, err := conn.Read(b)// handle error and some else op here ... }
这样操作, 只有第一次有两秒时间, 之后的所有都是超时.
-
正确用法:
b := make([]byte, 10) for {conn.SetDeadline(time.Now().Add(2 * time.Second))n, err := conn.Read(b)// handle error and some else op here ... }
-
讲了第一个函数, 后面两个也就很明确了, 有一些点要注意:
-
SetDeadline()
仅仅针对当前连接上的 I/O 有效(也就是指定的conn
). -
调用
SetDeadline()
等价于先后用同样的参数调用SetReadDeadline()
和SetWriteDeadline()
, 在需要细粒度控制时, 肯定是要分开调用. -
SetReadDeadline()
, 对于写操作来说, 即使超时了也不意味着完全失败了, chao’shi之前,Write()
方法背后肯定把一部分数据写入到socket
缓冲区了, 所以Write()
方法返回值可能大于0
, 这个返回值也就代表了超时之前真正写入的数据的字节数量.}
这样一来, 每次 `Read()` 在两秒钟内结束就不会报超时错误了.
讲了第一个函数, 后面两个也就很明确了, 有一些点要注意:
-
SetDeadline()
仅仅针对当前连接上的 I/O 有效(也就是指定的conn
). -
调用
SetDeadline()
等价于先后用同样的参数调用SetReadDeadline()
和SetWriteDeadline()
, 在需要细粒度控制时, 肯定是要分开调用. -
SetReadDeadline()
, 对于写操作来说, 即使超时了也不意味着完全失败了, chao’shi之前,Write()
方法背后肯定把一部分数据写入到socket
缓冲区了, 所以Write()
方法返回值可能大于0
, 这个返回值也就代表了超时之前真正写入的数据的字节数量.