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

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服务端第一步:

go 复制代码
 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()方法:

go 复制代码
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完全一致.如果想与前面刚刚开始监听的服务端程序连接的话.那么这个参数的值就是该服务端的地址.

go 复制代码
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()函数:

go 复制代码
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的接收缓冲区中读取数据.方法声明如下.

go 复制代码
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方法返回的第一个参数结果值可以帮助从中识别出真正的数据部分.

go 复制代码
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连接之上再无可读数据.

go 复制代码
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方法:

go 复制代码
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方法:

go 复制代码
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:

go 复制代码
// 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方法:

go 复制代码
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类型值.当以循环的方式不断尝试从一个连接上读取数据时.如果想要设定超时时间.就需要在每次读取数据操作之前都设定一次.

go 复制代码
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 欢迎大家访问.提意见.

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

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

念何架构之路

相关推荐
开心就好202517 分钟前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默25 分钟前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦43 分钟前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl1 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6862 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情2 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player2 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明2 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展
武超杰3 小时前
Spring Boot入门教程
java·spring boot·后端
IT 行者3 小时前
Spring Boot 集成 JavaMail 163邮箱配置详解
java·spring boot·后端