socket
socket,常译为套接字,也是一种IPC方法。但是与其他IPC方法不同的是,它可以通过网络连接让多个进程建立通信并相互传递数据,这使得通信双方是否在同一台计算机上变得无关紧要。
关于socket以及涉及到的网络协议和网络通信基础知识这里不再赘述。我们直接介绍go的相关socket的操作和接口方法。
listener, err := net.Listen("tcp", "127.0.0.1:8085")
Listen方法接收2参:协议和ip端口号,该操作包括了创建套接字,绑定端口和监听端口的操作,因此这是一个简便的方法。返回一个net.Listener的方法
conn, err := listener.Accept()
接收客户端连接
当调用监听器的Accept 方法时,流程会被阻塞,直到某个客户端程序与当前程序建立TCP连接。此时,Accept 方法会返回两个结果值:第一个结果值代表了当前TCP连接的net.Conn 类型值,而第二个结果值依然是一个error 类型的值。
conn, err := net.Dial("tcp", "127.0.0.1:8085")
这是客户端的方法,用于连接服务端的服务,三次握手的过程会在这里发生
要知道,网络中是存在延时现象的。因此,在收到另一方的有效回应(无论连接成功或失败)之前,发送连接请求的一方往往会等待一段时间,在上例中则表现为流程会在调用net.Dial 函数时阻塞一小段时间。其默认超时时间为75秒。
如果希望自定义连接的超时时间可以用下面的方法
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
接下来是 net.Conn 类型,它有read和write方法。
我们在之前的章节中用python实现多路复用+NIO的网络读写时知道,Linux提供了2种网络读写方式:阻塞和非阻塞方式。
Go的socket编程API程序在底层获取的是一个非阻塞式的socket实例,这意味着在该实例之上的数据读取操作也都是非阻塞式的。在应用程序试图通过系统调用read 从socket的接收缓冲区中读取数据时,即使缓冲区中没有任何数据,操作系统内核也不会使系统调用read 进入阻塞状态,而是直接返回一个错误码为EAGAIN的错误。但是,应用程序并不应该视此为一个真正的错误,而是应该忽略它,然后稍等片刻再去尝试读取。
另一方面,在应用程序试图向socket的发送缓冲区中写入一段数据时,即使发送缓冲区已被填满,系统调用write 也不会被阻塞,而是直接返回一个错误码为EAGAIN的错误。同样,应用程序应该忽略该错误并稍后再尝试写入数据。如果发送缓冲区中有少许剩余空间但不足以放入这段数据,那么系统调用write 会尽可能写入一部分数据然后返回已写入的字节的数据量。
你可能会问:前面说net.Listener 类型值的Accept 方法会在被调用时阻塞直至新连接的到来,与这里所说的非阻塞式的行为并不相符啊?!别急,请继续看接下来的说明。
Go的socket编程所提供的方法是经过了封装的,它为我们屏蔽了相关系统调用的EAGAIN错误,这使得有些socket编程API调用起来像是阻塞式的。但是,我们应该明确,它在底层使用的是非阻塞式的socket接口。
以下是net.Conn的所有接口
Read(b []byte) (n int, err error)
是一个阻塞的方法,它和文件读写的Read相同用法,需要注意的是每次传到Read的b参数都必须是一个空的切片,因为当在循环调用Read时传入一个容量为1024的b,如果第一次循环可以接收到1024个字节,但是不清空b就有进入下一次循环,而这次循环只接收到了500个字节的话,b的前500个字节就是正确的数据,而后面的524个字节没有被覆盖,是脏数据。
如果数据读取完毕,Read就会再次阻塞,而不会返回io.EOF的错误。只有当通信的另一端关闭tcp连接的时候Read才会返回一个 io.EOF的错误表示之后也无数据可读。
我们知道tcp协议中,数据是以字节流的方式传递,而且是没有数据边界的字节流。因此数据中需要掺入单字节的分隔符以充当边界,例如\n或者\t。
而我们读取的时候也需要将数据读取后通过分隔符将一大串的数据流整理为一个个的数据。
此时我们可以使用bufio的ReadBytes(‘\n’)的方法从一个网络连接中读取,每次调用ReadBytes他都会读取网络缓冲区中从开头到第一个\n的内容。我们结合for循环就可以每次都读取到切分好的数据。
就像这样:
reader := bufio.NewReader(conn)
line, err := reader.ReadBytes('\n')
当然,很多时候,消息边界的定位并不是查找一个单字节字符那么简单。比如,HTTP协议中规定,在HTTP消息的头部信息的末尾一定是连续的两个空行,即字符串"\r\n\r\n" 。获取到HTTP消息的头部信息之后,相关程序会通过其中的名为Content-Length的信息项得到HTTP消息的数据部分的长度。这样,一个HTTP消息就可以被切分出来了。为了满足这些较复杂的需求,bufio代码包为我们提供了一些更高级的API,例如bufio.NewScanner 函数、bufio.Scanner 类型及其方法,等等。
Write 方法
Write 方法用于向socket的发送缓冲区写入数据
Write(b []byte) (n int, err error)
同样,我们也可以使用代码包bufio中的API来使这里的写操作更加灵活。net.Conn 类型是一个io.Writer 接口的实现类型。所以,net.Conn 类型的值可以作为bufio.NewWriter 函数的参数值
writer := bufio.NewWriter(conn)
这样的好处是可以先将数据写入到用户态的缓冲区存贮着,等到数据量达到一定量(默认4k)再写入到内核缓冲,减少网络io次数。
可以通过调用bufio的以Write为名称前缀的方法来分批次地向其中的缓冲区写入数据,也可以通过调用它的ReadFrom 方法来直接从其他io.Reader 类型值(如文件,网络连接,buffer等)中读出并写入数据,还可以通过调用Reset 方法以达到重置和复用它的目的。在向其写入全部数据之后,应该调用它的Flush 方法,以保证其中的所有数据都真正写入到了它代理的对象(这里是由conn 变量表示的TCP连接)中。此外,还应该留心该缓冲写入器的缓冲区容量,它的默认值是4096个字节。
在调用以Write为名称前缀的方法时,如果作为参数值的数据的字节数量超出了此容量,那么该方法就会试图把这些数据的全部或一部分直接写入到它代理的对象中,而不会先在bufio的缓冲区中缓存这些数据。这可能并不是你希望的。为了解决此类问题,你可以通过调用bufio.NewWriterSize 函数来初始化一个缓冲写入器。该函数与bufio.NewWriter 函数非常类似,但它使你可以自定义缓冲区容量。
Close 方法
Close 方法会关闭当前的连接,它不接受任何参数并返回一个error 类型值。调用该方法之后,对该连接值(由示例中的conn 变量表示的值)上的Read 方法、Write 方法或Close 方法的任何调用都会使它们立即返回一个error 类型值。表示该error 类型值的变量已经被预置在了net代码包中,其提示信息是:
use of closed network connection
如果调用Close 方法时,Read 方法或Write 方法正在被调用且还未执行结束,那么它们也会立即结束执行并返回非nil 的error 类型值。即使它们正处于阻塞状态
conn的LocalAddr 和RemoteAddr 方法
单从名称上来看,你就可能已经猜到这两个方法的作用,它们都不接受任何参数并返回一个net.Addr 类型的结果。net.Addr 类型是一个接口类型,它的方法集合中有两个方法——Network 和String即地址和协议
SetDeadline 、SetReadDeadline 、SetWriteDeadline 方法
这3个方法都只接受一个time.Time 类型值作为参数,并返回一个error 类型值作为结果。SetDeadline 方法会设定在当前连接上的I/O操作(包括但不限于Read()和Write())的超时时间。注意,这里的超时时间是一个绝对时间!也就是说,如果调用SetDeadline 方法之后的相关I/O操作在到达此超时时间时还没有完成,那么它们就会被立即结束执行并返回一个非nil 的error 类型值。这个error 类型值由一个被预置在net 代码包中的包级私有变量表示。它的提示信息为"i/o timeout" 。注意,当你以循环的方式不断尝试从一个连接上读取数据时,如果想要设定超时时间,就需要在每次读取数据操作之前都设定一次,这正是因为在此设定的超时时间是一个绝对时间,并且它会对之后的每个I/O操作都起作用。
例如
b := make([]byte, 10)
for {
conn.SetDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(b)
// 省略若干条语句
}
记住是每次循环都要设置一次超时时间,因为SetDeadline设置的是绝对时间而非相对时间。
如果你不再需要设定超时时间了,就及时取消掉它,以免干扰后续的I/O操作。这一操作可以通过调用同样的方法来实现。如果给予SetDeadline 方法的参数值为time.Time 类型的零值,超时时间就会被取消掉。
conn.SetDeadline(time.Time{})
SetReadDeadline 方法和SetWriteDeadline 方法仅分别针对于读操作和写操作。这里说的读操作与conn的Read 方法的调用对应,而写操作则与conn的Write 方法的调用对应。对于写操作的超时,有一个问题需要明确,那就是即使一个写操作超时了,也不一定表示写操作完全没有成功。因为在超时之前,Write 方法背后的程序可能已经将一部分数据写到socket的发送缓冲区了。也就是说,即使Write 方法因操作超时而被迫结束,它的第一个结果值n也可能大于0 。这时,第一个结果值就表示在操作超时之前被真正写入的数据的字节数量。
另外,对SetDeadline 方法的调用相当于先后以同样的参数值对SetReadDeadline 方法和SetWriteDeadline 方法进行调用。如果你想统一设定所有相关的I/O 操作的超时时间,那么使用SetDeadline 方法肯定是最便捷的。但当你需要更细致的操作超时控制的时候,就需要用到后两个方法了。不过要记住,它们仅针对在当前net.conn之上的I/O操作。