从一个相对粗略的角度,每种协议大概可以认为由模型、机制和算法三部分构成。模型是协议对自己运行环境的假设;机制是协议应对问题的办法;算法作用于模型和机制,用于改进协议的运行效率。我们也可以从这三方面看TCP协议,这篇文章简述TCP协议的模型。

上下层模型

首先是下层协议,TCP运行于IP之上就不必说了。但是大多数协议都会比较鸡贼,不愿意把自己跟另外一个协议绑定太死。一般会都会提一下,说本协议默认运行在另外一个协议之上,但是不仅限于此,还能运行在其他协议之上。以TCP为例子,RFC793 是这么说的:

模型中也会定义TCP如何向上层提供服务。RFC793假设TCP默认是在操作系统层面实现,对上层的应用提供异步的数据传输服务。对此,TCP定义了几个服务接口:

  • OPEN和CLOSE,用于打开和关闭连接。
  • SEND和RECEIVE,用于发送和接收数据。
  • STATUS,用于查询连接的状态。

当然,上面的几个接口都是抽象的,在每种操作系统下,对应的系统调用接口都有所不同。比如在macOS系统下面,都是用的从BSD系统继承的接口:

  • OPEN对应:connect(2)
  • CLOSE对应:close(2)
  • SEND对应:send(2)或者write(2)
  • RECEIVE对应:recv(2)或者read(2)
  • STATUS对应:getsockopt(2)

Socket的概念

RFC793首次提出Socket的概念,说Socket等于“IP地址+端口号”。所以一个TCP连接是由一对Socket组成的,如下所示:

本地socket(地址+端口) <—> 远程socket(地址+端口)

从上面可以看出一个socket可以用于不同的TCP连接。所以服务器只需要开启一个侦听的socket,就可以应对不同的客户端发起的连接。

通常我们在编写程序的时候,需要通过以下三步来开启一个侦听socket:

  1. socket() 创建一个socket
  2. bind() 赋予这个socket相应的地址和端口
  3. listen() 让socket进入侦听状态

客户端发起连接的步骤如下:

  1. socket() 创建一个socket
  2. connect() 连接到一个远程socket(注意哦,本地socket的地址和端口是系统自动分配的)

除了socket的概念外,RFC793还指出进程可以拥有端口号,并且某些端口号可以用于广为人知的服务,比如HTTP的端口为80.

对于进程拥有端口号这个行为是大多操作系统的默认行为。一般情况下socket独享一个地址和端口,比如一个进程把socket绑定到一个地址之后,其他进程就无法再把其他socket绑定到相同地址了。但是可以指定例外的情况,如果用setsockopt指定了一个socket的SO_REUSEPORT属性,那么这个socket就不独自拥有所绑定的地址和端口了。

总结下,“进程拥有端口号”这个概念操作系统是这么支持的:

  1. 进程创建一个socket,并且不把这个socket分享给其他进程(比如创建子进程的时候不分享这个socket)
  2. 不多次一举调用setsockopt指定socket的SO_REUSEPORT属性
  3. 把socket绑定到一个地址和端口,这样这个端口就变成进程独有的了。

号外,BSD很早就支持SO_REUSEPORT了,但是Linux在3.9的时候才支持,具体可以参考这篇文章

一个Echo服务器的例子

下面用Python3写一个Echo服务器的例子:

  • 服务器绑定127.0.0.1:6001
  • 客户端绑定127.0.0.1:6002
  • 服务器返回所有客户端发送的字符串(即把字符串Echo回去)

服务端代码:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 6001))
s.listen(1)
counter = 0
while True:
    conn, addr = s.accept()
    counter += 1
    print('Connection counter = {}'.format(counter))
    data = conn.recv(1024)
    conn.send(data)
    conn.close()

客户端代码:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 如果客户端不bind的话,操作系统会自动分配地址,这里只是演示客户端是可以执行bind操作的
s.bind(('127.0.0.1', 6002))
s.connect(('127.0.0.1', 6001))
s.send(b'hello')
data = s.recv(1024)
print(data)
s.close()

(完)