从一个相对粗略的角度,每种协议大概可以认为由模型、机制和算法三部分构成。模型是协议对自己运行环境的假设;机制是协议应对问题的办法;算法作用于模型和机制,用于改进协议的运行效率。我们也可以从这三方面看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:
- socket() 创建一个socket
- bind() 赋予这个socket相应的地址和端口
- listen() 让socket进入侦听状态
客户端发起连接的步骤如下:
- socket() 创建一个socket
- connect() 连接到一个远程socket(注意哦,本地socket的地址和端口是系统自动分配的)
除了socket的概念外,RFC793还指出进程可以拥有端口号,并且某些端口号可以用于广为人知的服务,比如HTTP的端口为80.
对于进程拥有端口号这个行为是大多操作系统的默认行为。一般情况下socket独享一个地址和端口,比如一个进程把socket绑定到一个地址之后,其他进程就无法再把其他socket绑定到相同地址了。但是可以指定例外的情况,如果用setsockopt指定了一个socket的SO_REUSEPORT属性,那么这个socket就不独自拥有所绑定的地址和端口了。
总结下,“进程拥有端口号”这个概念操作系统是这么支持的:
- 进程创建一个socket,并且不把这个socket分享给其他进程(比如创建子进程的时候不分享这个socket)
- 不多次一举调用setsockopt指定socket的SO_REUSEPORT属性
- 把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()
(完)