Go之基于TCP/IP协议栈的socket通信

0 阅读9分钟

socket接口既可以提供网络中不同计算机上多个应用程序间的通信支持.也可以成为

单台计算机上多个应用程序间的通信手段.

客户端服务端交互流程:

函数net.Listen用于获取监听器.它接受两个string类型参数.第一个参数的含义是以何种协议监听给定的地址.在Go中这些协议由一些字符串字面量来表示.如下所示.

这个参数所代表的的是面向流的协议.TCP和SCTP都属于面向流的传输层协议.不同的是.TCP协议实现程序无法记录和感知任何消息边界.也无法从字节流分离出消息.而SCTP协议实现程序却可以做到这一点.

消息是数据包在TCP/IP协议栈的应用层中的称谓.消息边界与前面所说的数据边界的含义基本相同.两者区别在于.消息边界仅仅针对消息.数据边界针对的对象范围更广.

综上所述.net.listener函数的第一个参数的值必须是上面图片中的一个.对于基于TCP协议的socket来说.net.listener函数的第二个参数是laddr的值表示当前程序在网络中的标识.laddr是Local Address的简写.格式是host:port.host代表ip地址或主机名.而port代表当前程序欲监听的端口号.

注: host处的内容必须是与当前计算机对应的IP地址或者主机名.否则调用函数时会出错.如果host处的是主机名.那么API程序会先通过DNS找到与该主机名对应的IP地址.若host处的主机名没有在DNS注册.同样会出错.

TCP服务端第一步:


 func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:8082")
	if err != nil {
		panic(err)
	}
	conn, err := listen.Accept()
	if err != nil {
		panic(err)
	}
}

Listen函数的第一个结果值是net.listener类型的.它代表监听器.第二个结果值是一个error类型的.

当调用监听器的Accept方法时.流程会被阻塞.直到某个客户端程序与当前程序建立TCP连接.Accept会返回两个值.第一个结果值代表了当前TCP连接的net.Conn类型值.第二个结果依然是error类型的值.

net.Dial()方法:

func Dial(network, address string) (Conn, error) {
    var d Dialer
    return d.Dial(network, address)
}

Dial函数也是接受两个参数.network与net.Listen函数的第一个参数net含义非常类似.但是它比后者有更多的可选值.在发送数据之前不一定要先建立连接.像UDP协议和IP协议都是面向无连接型的协议.因此udp udp4 udp6 ip4和ip6都可以作为参数network的值.

第二个参数值address的含义与net.Listen函数的第二个参数laddr完全一致.如果想与前面刚刚开始监听的服务端程序连接的话.那么这个参数的值就是该服务端的地址.

func main() {
 
    dial, err := net.Dial("tcp", "127.0.0.1:8082")
    if err != nil {
       panic(err)
    }
}

Dial函数返回的第一个结果值是net.Conn类型的值.另一个是值error类型.

在网络中是存在延时现象的.因此在收到另一方的有效回应(无论成功或失败)之前.发送连接请求的一方往往会等待一段时间.上面的例子中在调用net.Dial函数的那一行会一直阻塞.超过等待时间后.函数就会结束执行.并返回相应的error类型值.在Go的net代码包中也存在相应的API.声明如下.

net.DialTimeout()函数:

func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {
    d := Dialer{Timeout: timeout}
    return d.Dial(network, address)
}

函数声明中的最后一个参数专门用于设定超时时间.它的类型是time.Duration(int64类型的别名类型).单位是纳秒.

注:在创建监听器并开始等待连接请求之后.一旦收到客户端的连接请求.服务端就会与客户端建立TCP连接(三次握手).当然.这个连接的建立过程是两端操作系统内核共同协调完成的.当成功建立连接后.不论服务端程序还是客户端程序.都会得到一个net.Conn类型的值.后面两端就可以分别通过格子的net.Conn类型的值交换数据了.

先要说明的是.Go的Socket编程API程序是在底层获取一个非阻塞式的socket实例.这意味着在该实例之上的数据读取操作也都是非阻塞的.在应用程序试图通过系统调用read从socket的接受缓冲区读取数据时.即使接收缓冲区中没有任何数据.操作系统内核也不会使系统调用read进入阻塞状态.而是直接返回一个错误码EAGAIN的错误.但是应用程序并不应该视此为一个真正的错误.而是应该忽略它.然后稍等片刻再去尝试读取.如果在读取数据的时候接收缓冲区有数据,系统调用read就会携带数据立即返回.即使缓冲区有一字节数据.也会这样.这个特性称为部分读.

另一方面.在应用程序试图向socket的发送缓冲区写入一段数据时.即使发送缓冲区已被填满,系统调用write也不会被阻塞.而是直接返回一个错误码为EAGAIN的错误.同样.应用程序应该忽略该错误并稍后在尝试写入数据.如果发送缓冲区中有少许剩余空间但不足以放入这段数据.那么系统调用write会尽可能写入一部分数据然后返回已写入的字节的数据量.这一特性称为部分写.

net.Conn类型是一个接口类型.包含了8个方法.

1).Read方法:

Read方法用于从socket的接收缓冲区中读取数据.方法声明如下.

func (c *conn) Read(b []byte) (int, error) {
    if !c.ok() {
       return 0, syscall.EINVAL
    }
    n, err := c.fd.Read(b)
    if err != nil && err != io.EOF {
       err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

该方法接收一个[]byte类型的参数.该参数的值相当于一个用来存放从连接上接收到的数据的容器.它的长度完全由应用程序决定.Read方法会把它当成空的容器并试图填满.该容器中相应的位置上的原元素的值将会被替换.为了避免混乱.应该总是让这个容器在填充之前保持绝对的干净.换句话说.传递给Read方法的参数值应该是一个不包含任何非零值元素的切片值.一般情况下.Read方法只有在把参数值填满之后返回.在有些情况下.Read方法在未填满参数值就返回了.这可能是由相关的网络数据缓存机制导致的.好在Read方法返回的第一个参数结果值可以帮助从中识别出真正的数据部分.

func main() {
    listen, err := net.Listen("tcp", "127.0.0.1:8082")
    if err != nil {
       panic(err)
    }
    conn, err := listen.Accept()
    bytes := make([]byte, 10)
    n, err := conn.Read(bytes)
    s := string(bytes[:n])
}

通过依据结果n对参数bytes做切片操作可以抽取出接收到得数据.

如果socket编程API程序在从socket的接收缓冲区读取数据时发现TCP连接已经被另一端关闭了.就会立即返回一个error类型值.这个error类型值与io.EOF变量的值是相等的.其中.io.EOF象征文件内容的完结.若该值为io.EOF.则意味着此TCP连接之上再无可读数据.

func main() {
    var dataBuffer bytes.Buffer
    b := make([]byte, 10)
    listen, err := net.Dial("tcp", "127.0.0.1:8082")
    if err != nil {
       fmt.Println(err)
    }
    for {
       n, err := listen.Read(b)
       if err != nil {
          if err == io.EOF {
             fmt.Println("the connection is closed.")
             listen.Close()
          } else {
             fmt.Println(err)
          }
          break
       }
       dataBuffer.Write(b[:n])
    }
    
}

2).Write方法:

func (c *conn) Write(b []byte) (int, error) {
    if !c.ok() {
       return 0, syscall.EINVAL
    }
    n, err := c.fd.Write(b)
    if err != nil {
       err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

Write方法用于向socket的发送缓冲区写入数据.

3).Close方法:

func (c *conn) Close() error {
    if !c.ok() {
       return syscall.EINVAL
    }
    err := c.fd.Close()
    if err != nil {
       err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return err
}

Close方法会关闭当前的连接.它不接受任何一个参数值.并返回一个error类型值.调用该方法后.对该连接值上的Read方法和Write方法或Close方法的任何调用都会使它们立即返回一个error类型的值.

注:如果调用Close方法时.Read或Write方法正在被调用且还未执行结束.那么它们也会立即结束执行并返回非nil的error类型值.即使它们处于阻塞状态也会这样.

4).LocalAddr和RemoteAddr:

// LocalAddr returns the local network address.
// The Addr returned is shared by all invocations of LocalAddr, so
// do not modify it.
func (c *conn) LocalAddr() Addr {
    if !c.ok() {
       return nil
    }
    return c.fd.laddr
}

// RemoteAddr returns the remote network address.
// The Addr returned is shared by all invocations of RemoteAddr, so
// do not modify it.
func (c *conn) RemoteAddr() Addr {
    if !c.ok() {
       return nil
    }
    return c.fd.raddr
}

它们都不接受任何参数并返回一个net.Addr类型的结果.其结果值代表了参与当前通信的某一端程序在网络中的地址.LocalAddr方法返回的结果值代表了本地地址.而RemoteAddr方法返回的结果代表了远程地址.

5).SetDeadLine SetReadDeadLine SetWriteDeadLine方法:

func (c *conn) SetDeadline(t time.Time) error {
    if !c.ok() {
       return syscall.EINVAL
    }
    if err := c.fd.SetDeadline(t); err != nil {
       return &OpError{Op: "set", Net: c.fd.net, Source: nil, Addr: c.fd.laddr, Err: err}
    }
    return nil
}

// SetReadDeadline implements the Conn SetReadDeadline method.
func (c *conn) SetReadDeadline(t time.Time) error {
    if !c.ok() {
       return syscall.EINVAL
    }
    if err := c.fd.SetReadDeadline(t); err != nil {
       return &OpError{Op: "set", Net: c.fd.net, Source: nil, Addr: c.fd.laddr, Err: err}
    }
    return nil
}

// SetWriteDeadline implements the Conn SetWriteDeadline method.
func (c *conn) SetWriteDeadline(t time.Time) error {
    if !c.ok() {
       return syscall.EINVAL
    }
    if err := c.fd.SetWriteDeadline(t); err != nil {
       return &OpError{Op: "set", Net: c.fd.net, Source: nil, Addr: c.fd.laddr, Err: err}
    }
    return nil
}

这三个方法都只接受一个time.Time类型值作为参数.并返回一个error类型值作为结果.SetDeadLine方法会设定在当前连接上的I/O操作(包括但不限于读和写)的超时时间.

注:这里的超时时间是一个绝对时间.如果调用SetDeadLine方法之后的I/O相关操作在到达此时时间还没有完成.它们就会被立即结束并返回一个非nil的error类型值.当以循环的方式不断尝试从一个连接上读取数据时.如果想要设定超时时间.就需要在每次读取数据操作之前都设定一次.

func main() {
    var dataBuffer bytes.Buffer
    b := make([]byte, 10)
    listen, err := net.Dial("tcp", "127.0.0.1:8082")
    if err != nil {
       fmt.Println(err)
    }
    for {
       listen.SetDeadline(time.Now().Add(time.Second))
       n, err := listen.Read(b)
       if err != nil {
          if err == io.EOF {
             fmt.Println("the connection is closed.")
             listen.Close()
          } else {
             fmt.Println(err)
          }
          break
       }
       dataBuffer.Write(b[:n])
    }

}

另一方面如果不需要设定超时时间了.就及时取消掉.以免干扰后续操作.

conn.SetDeadLine(time.Time{})

SetReadDeadLine方法和SetWriteDeadLine方法分别针对读操作和写操作.这里就不过多叙述了.

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

如果风会来.那么风会从哪个方向来.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路