TCP:基于socket网络编程

2023/1/15

TCP/IP网络模型,实现了两种传输层协议:TCP和UDP。TCP是面向连接的流协议,为通信 的两端提供稳定可靠的数据传输服务;而UDP则提供了一种无需建立连接就可以发送数据包的方法。两种协议各有擅长的应用场景。 我们日常开发中使用最多的是TCP协议。基于TCP协议,我们实现了各种各样的满足用户需求的应用层协议。比如,我们常用的HTTP协议就是应用层协议的一种,而基于HTTP的Web编程就是一种针对应用层的网络编程。我们还可以基于传输层暴露给开发者的编程接口,实现应用层的自定义应用协议。目前各大主流操作系统平台中,最常用的传输层暴露给用户的网络编程接口,就是套接字(socket)。直接基于socket编程实现应用层通信业务,也是最常见的一种网络编程形式。

# TCP协议模式

基于TCP的自定义应用层协议通常有两种常见的定义模式:

  • 二进制模式:采用长度字段标识独立数据包的边界。采用这种方式定义的常见协议包括 MQTT(物联网最常用的应用层协议之一)、SMPP(短信网关点对点接口协议)等;
  • 文本模式:采用特定分隔符标识流中的数据包的边界,常见的包括HTTP协议等。 相比之下,二进制模式要比文本模式编码更紧凑也更高效.

# TCP Socket编程模型

常用的网络I/O模型

  • 阻塞I/O(Blocking I/O)
  • 非阻塞I/O(Non-Blocking I/O)
  • I/O多路复用(I/O Multiplexing)

# Go语言socket编程模型

Go语言socket编程模型是使用的阻塞I/O模型。

网络I/O操作都是系统调用,Goroutine执行I/O操作的话,一旦阻塞在系统调用上,就 会导致M也被阻塞,为了解决这个问题,Go设计者将这个“复杂性”隐藏在Go运行时中,他 们在运行时中实现了网络轮询器(netpoller),netpoller的作用,就是只阻塞执行网络I/O操作的Goroutine,但不阻塞执行Goroutine的线程(也就是M)。
这样一来,对于Go程序的用户层(相对于Go运行时层)来说,它眼中看到的goroutine采用了“阻塞I/O模型”进行网络I/O操作,Socket都是“阻塞”的。但实际上,这样的“假象”,是通过Go运行时中的netpoller I/O多路复用机制,“模拟”出来的,对应的、真实的底层操作系统Socket,实际上是非阻塞的。只是运行时拦截了针对底层Socket的系统调用返回的错误码,并通过netpoller和Goroutine调度,让Goroutine“阻塞”在用户层所看到的Socket描述符上。
比如:当用户层针对某个Socket描述符发起read操作时,如果这个Socket对应的连接上还没有数据,运行时就会将这个Socket描述符加入到netpoller中监听,同时发起此次读操作的Goroutine会被挂起。直到Go运行时收到这个Socket数据可读的通知,Go运行时会重新唤醒等待在这个Socket上准备读数据的那个Goroutine。而这个过程,从Goroutine的视角来看,就像是read操作一直阻塞在那个Socket描述符上一样。 而且,Go语言在网络轮询器(netpoller)中采用了I/O多路复用的模型。考虑到最常见的多路复用系统调用select有比较多的限制,比如:监听Socket的数量有上限(1024)、时间复杂度高,等等,Go运行时选择了在不同操作系统上,使用操作系统各自实现的高性能多路复 用函数,比如:Linux上的epoll、Windows上的iocp、FreeBSD/MacOS上的kqueue、 Solaris上的event port等,这样可以最大程度提高netpoller的调度和执行性能。

# 实现基于TCP的自定义应用层协议的通信服务端

设计方案

  • 使用二进制模式制定协议内容和格式