Python 绝技 —— TCP 服务器与客户端!
Python资源共享群:626017123
0x00 前言
本文首先主要阐述了因特网的核心协议 TCP 的相关内容。接着通过 Python 的 socket 模块这一实例来展示网络套接字的基本使用方法。最后提供了 TCP 服务器与客户端的 Python 脚本,并详细演示了两者之间的通信过程。
0x01 TCP 协议
TCP(Transmission Control Protocol)是网络层中采用字节流技术实现可靠连接传输的一种通信协议。
该协议的具体执行流程可被划分为三个关键环节——建立连接、传输数据以及断开连接。在这一过程中,“建立连接”和“断开连接”这两个环节分别对应于TCP的标准三步握手(Three-way Handshake)以及四步挥手(Four-way Handshake)机制,并构成了分析本文中TCP服务器与客户端通信机制的基础内容。
为了能更好地理解下述过程,对 TCP 协议头的关键区段做以下几点说明:
- 报文的功能在 TCP 协议头的标记符(Flags)区段中定义,该区段位于第 104~111 比特位,共占 8 比特,每个比特位对应一种功能,置 1 代表开启,置 0 代表关闭。例如,SYN 报文的标记符为 00000010,ACK 报文的标记符为 00010000,ACK + SYN 报文的标记符为 00010010。
- 报文的序列号在 TCP 协议头的序列号(Sequence Number)区段中定义,该区段位于第 32~63 比特位,共占 32 比特。例如,在「三次握手」过程中,初始序列号 seq 由数据发送方随机生成。
- 报文的确认号在 TCP 协议头的确认号(Acknowledgement Number)区段中定义,该区段位于第 64~95 比特位,共占 32 比特。例如,在「三次握手」过程中,确认号ack 为前序接收报文的序列号加 1,代表下一次期望接收到的报文序列号。
连接创建
所谓 «三次握手» ,是 TCP 服务器与客户端成功建立通信连接所必须经历的三个步骤之一;这三个步骤均需通过特定的报文依次传递信息。
通常情况下,在网络通信中,在收到SYN报文前,请确保您已准备好相应的处理逻辑以应对可能出现的情况。
Handshake Step 1
客户端向服务器发送 SYN 报文(SYN=1)请求建立连接。
此份报条的初始序列号为 seq = x ,acks字段值设为 0 ,发送完成后客户端切换至 SYN_SENT 状态
Handshake Step 2
当服务器接收到来自客户端的SYN报文时,在其响应中发送包含ACK和SYN的报文(其中ACK设为1、SYN设为1),以确认客户端已经发出建立连接请求,并同时发出建立连接的请求。
此次报文的其序列为 seq = y,其确认编号为 ack = x+1,发送完成之后,服务器已达到 SYN_RCVD 状态。
Handshake Step 3
当客户端接收到服务器发出的SYN报文时,在完成SYN三元组Establishment流程后生成并发送ACK报文数据包(其中ACK字段设置为1),以响应并确认服务器建立连接的请求。
此份报文中包含一个序列编号字段seq=x+1以及一个acks=y+1字段。发送操作完成后,在网络层将状态被更新为ESTABLISHED;在服务器接收该报文之后,在网络层也将状态被更新为ESTABLISHED
至此,「三次握手」过程全部结束,TCP 通信连接成功建立。
读者可参照以下「三次握手」的示意图进行理解:
连接终止(Connection Termination)
通常所说的「四次挥手」是指 TCP 服务器与客户端在完全终止通信连接过程中必须经过的四个步骤,在这个过程中所需的信息传递全部通过四个报文实现。
因为 TCP 通信连接具备双向传输的特点,在处理各方向时可采取独立关闭策略,并将其视为一对握手协议事件(亦即单工式通信场景)。其中一方若首发出具 FIN 指令的一方,则其作用即是指示希望断开与另一方的数据传输(直至接收端也释放相应资源)。
需要注意的是,在最先发送FIN报文的一方(也可能是服务器)的情况下,请您确保双方能够正确识别对方的意图。以下将通过一个具体的场景来为您演示这一过程:例如,在客户端发起关闭请求时,请您具体过程如下
Handshake Step 1
当客户端停止发送数据时,则此时向其生成相应的FIN报文(其中FIN字段设置为1),以发起关闭连接的请求。
该报文的初始序列编号设为 seq = u ,其确认编号设为 ack = 0。(当该报文中 ack字段值等于1时,则 ack字段值与客户端之前发送的相关报文存在关联) 。在发送完成后 ,客户端将处于 FIN_WAIT_1 状态。
Handshake Step 2
当服务器响应客户端发送来的FIN报文时,在其后续操作中会生成并返回ACK报文(其中ACK值为1),以确认客户端已发出关闭连接请求。
此次报文中包含的序列号为 seq = v, 其中确认字段值为 ack = u + 1;发送完成后,服务器会切换至 CLOSE_WAIT 状态.一旦客户端接收到该报文信息,则会切换至 FIN_WAIT_2 状态.
需要注意的是,在当前情况下,TCP 通信连接已处于半关闭状态——即客户端停止向服务器发送数据信息;然而,在这种状态下,并不影响客户端仍能够接收来自服务器的数据更新。
Handshake Step 3
当服务器停止向客户端发送数据时,则被其发送包含FIN和ACK字段值均为1的报文以请求关闭连接。
该报文的序列号值为seq= w(当服务器处于半关闭状态且无客户端数据传输记录时,则其序号值设为v加一)。确认号码设为ack等于u加一。发送完成后, 服务器转入LAST_ACK状态
Handshake Step 4
客户端接收到服务器发送的 FIN 加 Ack 信号后,在 Ack 信号中以 Ack = 1 确认已关闭连接请求。
此份报文中包含两个标识字段:seq字段值为u+1, ack字段值为w+1. 在完成数据传输之后, 客户端将处于等待响应的状态. 一旦服务器收到该报文并确认接收成功后, 则会切换至CLOSED状态; 若客户端持续等待2MSL时间未收到服务器的确认信息, 则判断服务器已正常停止运行.
至此,「四次挥手」过程全部结束,TCP 通信连接成功关闭。
读者可参照以下「四次挥手」的示意图进行理解:
0x02 Network Socket
网络套接字(Network Socket)是计算机网络中过程间的通信数据传输端点,在广义层面上也代表操作系统提供的过程间的通信机制。
过程间通信(Inter-Process Communication, IPC)的核心条件在于能够为每个过程提供独特的标识符。在本地环境下的进程中程通信中,默认可以通过进程ID(PID)来实现对各过程的唯一标识;然而,在网络环境中由于不同主机之间存在通信需求,则需要更加可靠的机制来保证标识符的一致性与安全性。因此,在网络环境中,默认情况下仅使用PID就无法满足需求;通常情况下需要采用IP地址、传输层协议以及端口号的组合来确保对网络内各过程的独特识别与区分。
小贴士:网络层中的 IP 地址能够唯一标识一台主机,在传输层中使用的 TCP/UDP 协议以及指定端口号能精确标识该主机运行的一个进程。需要注意的是,在同一台主机上可能同时运行使用 TCP 和 UDP 协议,并且它们之间可以通过相同的端口号进行区分。
为所有支持网络通信的编程语言提供的是一套套独特的 socket API;作为示例,我们可以通过 Python 3 来介绍如何通过交互过程实现服务器与客户端之间的 TCP 通信连接。
通过预先引入该过程的概念或内容,更容易理解后续关于TCP服务器和客户端的Python实现部分。
0x03 TCP 服务器
- 实现一个tcplink()子程序。
- 在连接建立之后, 将发送给客户端欢迎信息。
- 启动与客户端的数据交互循环。
- 发送询问信息给客户端。
- 接收客户端传回的数据对象。
- 如果接收到的数据对象等于退出标记, 则返回结束响应信息并终止数据交互循环。
- 如果接收到的数据对象不等于退出标记, 则返回问候响应信息, 其中问候内容基于客户端传回的对象进行填充。
- 关闭套接字, 禁止向客户端发送任何数据。
- 创建一个socket实例.
- 将socket实例绑定到服务器主机地址 ("127.0.0.1", 6000)处.
- 启用socket实例监听功能, 接收客户的连接请求.
- 进入监听客户连接请求的循环阶段.
- 收取客户的连接请求, 并获取与之通信的对象套接字conn以及对应的地址信息addr(其中addr是一个包含主机名和端口号的二元组).
- 利用多线程技术, 开启多个新线程来处理多个客户的连接请求.
- 启动新线程以维持通信活动.
0x04 TCP 客户端
- Line 5:生成 socket 对象(SO),其中第一个参数指定采用 IPv4 协议(AF_INET),第二个参数指定 TCP 协议(SOCK_STREAM)用于面向连接的网络通信。
- Line 6:尝试与本地主机上的 TCP 端口(127.0.0.1:6000)建立连接请求。
- Line 7:成功接收到服务器发送的欢迎信息 b"Welcome!\n" 并将其转换为字符串格式后打印输出结果。
- Line 9:定义一个非空字符串变量 data 并赋初值为 "client"(只要求非空字符串即可),用于接收服务器发来的询问信息 b"What's your name?"。
- Lines 10–4: 进入服务器数据交互循环阶段。
- Lines 13–17: 当数据输入为空时会重新进入循环以获取用户输入;当输入有效时则会将字符串转换为 bytes 类型数据后发送至服务器端;接收服务器返回的数据并将其 bytes 对象转换为字符串格式后打印输出结果;如果用户输入的是 "exit" 则终止数据交互循环阶段并关闭当前套接字连接;当收到的数据为空则不再向服务器发送任何数据完成任务流程。
0x05 TCP 进程间通信
为TCP服务器和客户端脚本分别命名为tcp_server.py和tcp_client.py后存入桌面文件夹,在PowerShell环境中演示流程。
小贴士:在复现过程中,请读者注意以下几点:第一点是确认电脑已安装 Python 3环境;第二点是作者已经修改了默认启动路径名 default为 default=python3。
单服务器 VS 单客户端
在第一个PowerShell窗口中执行命令python3 ./tcp_server.py时,服务器会提示"Waiting for connection..."并开始接收来自本地主机的TCP6000端口的数据流;
在第二个PowerShell窗口中执行命令python3 ./tcp_client.py时,在接收到客户端的数据流后会立即响应"Accept new connection from 127.0.0.1:42101"并建立与本地主机的TCP42101端口通信链接;
当客户端发送"Alice"和"Bob"这样的字符串时,服务器会回复相应的问候语和提示信息;
当客户端输入空字符串时,系统会要求用户重新输入;
当客户端发送"exit"指令时,系统会给出退出响应;
当前客户端和服务器之间的通信连接已处于关闭状态,但系统仍继续接受新的连接请求。
单服务器 VS 多客户端
通过其中一个 PowerShell 执行命令 python3 ./tcp_server.py 后发现服务端等待连接;
通过另三个 PowerShell 分别执行命令 python3 ./tcp_client.py 时服务端与本地主机的 TCP 42719、42721 和 42722 端口建立通信连接并输出欢迎信息及询问信息;
三台客户端分别向服务端发送 Client1、Client2 和 Client3 接收服务端的问候及询问;
所有客户端接收到 exit 指令后服务端输出结束指令;
服务端的所有通信连接已关闭 继续监听新的连接请求;
0x06 Python API Reference
socket 模块
在本节中详细阐述上述代码中使用到的内置模块 socket及其用于网络通信功能的实现。该模块是 Python 网络编程的基础组件之一。
socket() 函数
socket() 函数用于创建网络通信中的套接字对象。函数原型如下:
- socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) 其中family参数用于指定地址族(Address Family),其默认值为AF_INET(适用于IPv4网络通信),其他常见设置包括AF_INET6(适用于IPv6网络通信)。不同操作系统支持不同的family参数设置。
- type参数决定了套接字类型,默认设置为SOk_STREAM(对应TCP协议——面向连接);而常用的还有SOCK_DGRAM(对应UDP协议——无连接)。
- proto参数指定了使用的通信协议,默认设置为0,在大多数情况下该字段无需手动配置;但若family属性被设定为AF_CAN,则必须将proto字段设置为CAN_RAW或CAN_BCM以确保数据包正确传输。
- fileno参数指定文件描述符(File Descriptor),其默认值通常设为None;若用户指定该字段,则会同时影响前面三个字段的实际作用域。
创建完套接字对象后, 必须调用该对象的内置函数来实现网络通信功能. 注意, 在下面提到的函数原型中, «socket»指的是一个 socket 对象, 而不是之前所述的 socket 模块.
bind() 函数
bind() 函数负责将 IP 地址与端口号绑定至套接字对象上。须知套接字对象不得预先已被绑定,并且端口号不可已被占用;否则将会引发错误。函数原型如下:
- address 字段指示绑定到套接字上的地址。
其格式由套接字 family 参数决定。
当 family 选项设置为 AF_INET 时,
address 字段以 (host, port) 形式表示,
其中 host 为字符串类型的主机地址标识符,
port 则为整数值指定的端口号。
listen() 函数
listen() 函数用于 TCP 服务器开启套接字的监听功能。函数原型如下:
socket.listen([backlogging])中使用了 backlogging 参数 backlock,默认情况下表示套接字等待新连接前的最长连接数量。其中 backlock 参数通常设为 5,在实际应用中如果没有指定该参数,则系统会为其自动选择一个合适的数值。
connect() 函数
connect() 函数用于 TCP 客户端向 TCP 服务器发起连接请求。函数原型如下:
address参数用于指定套接字所需连接的地址位置信息。其格式由套接字family参数决定;当family参数设置为AF_INET时,默认情况下会启用IPV4地址解析功能;此时address参数以(host, port)的形式表示;其中host是基于字符串标识的主机地址名称;port是基于整数指定的端口号数值
accept() 函数
accept() 函数用于 TCP 服务器接受 TCP 客户端的连接请求。函数原型如下:
socket.accept函数返回一个由(conn, address)构成的有序对。其中 conn代表服务器与客户端之间传递数据所使用的套接字。address则表示客户端对应的IP地址及其端口号。通常以(host, port)的形式表示。
send() 函数
该函数负责将数据发送至远程套接字对象。请注意,在调用该函数之前必须确保本地套接字已成功连接到远程套接字;否则将导致错误。由此可见,在TCP协议下该函数仅适用于进程间的通信。函数原型如下:
bytes 参数用于表示即将发送的 bytes 数据对象。举个例子来说,在处理字符串 "hello world!" 时,我们需要使用 encode() 函数将其编码为 bytes 类型的对象 b"hello world!" 以确保能够顺利进行网络传输。
flags 是可选参数, 用于设置 send() 函数的特殊功能, 其默认值为 0. 它可以由一个或多个预定义值构成, 并通过使用位或运算符 | 分割. 详细信息, 请参阅 Unix 函数手册中的 send(2). 常见取值包括 MSG_OOB、MSG_EOR 等等
send() 函数的返回值是发送数据的字节数。
recv() 函数
recv接口 用于从远程套接字读取数据。请注意,在与 send方法 相比下,recv接口 既能应用于 TCP进程间的通信 也能应用于 UDP进程间的通信。函数原型如下:
socket.recv(bufsize[, flags])中,bufsize变量表示网络套接字可接收数据的最大字节数。值得注意的是,在使硬件设备与网络传输能够更好地匹配方面, bufsize变量的值最好设定为2的幂次方数,例如4096。
flags 是一种可选参数配置 recv() 函数的独特功能设定,默认情况下被指定为 0,并可能由若干预先定义的具体值构成,在使用时通常会以按位或运算符 |的方式进行分隔区分。为了进一步了解详细信息,请参阅 Unix 函数手册中的 recv(2)章节中 flags 参数的相关说明。其中 flags 参数的主要常见取值包括 MSG_OOB、MSG_PEEK 和 MSG_WAITALL 等。
recv()函数返回的内容是接收的bytes对象数据实例。例如,在接收bytes对象实例b'hello world!'时, 通常会采用decode()函数将其转换为字符串'b'hello world!''并进行打印输出。
close() 函数
close() 函数用于关闭本地套接字对象,释放与该套接字连接的所有资源。
threading 模块
在本节中阐述上述代码中所涉及的内建模块threading作为Python多线程的核心内容之一。
Thread() 类
Thread() 类能够生成新的线程实例,并支持通过调用其start()方法来启动新的线程。类原型如下:
- threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) 中的 group 参数将作为后续 ThreadGroup() 实现中使用的保留参数,默认情况下设为 None。
- target 参数指定了被 run() 函数启动执行的具体功能,默认情况下无具体功能会被调用。
- name 参数决定了线程名称,在默认设置下未指定时系统会自动生成名称,并采用「Thread-N」格式(N 从 1 开始递增)。
- args 参数表示 target 指定的功能所需的具体输入参数(使用元组表示),其默认值为空元组 () 无需额外输入即可运行目标功能。
- kwargs 参数对应于 target 指定的功能所需的关键字输入(使用字典表示),默认情况下也被设为空字典 {} 即无需指定任何关键字即可运行目标功能。
- daemon 参数用于标识进程是否属于守护进程:若设置为 True 则表示守护进程;若设置为 False 则表示非守护进程;若设置为 None 则继承父线程的 daemon 设置值
创建完线程对象后,需使用对象的内置函数控制多线程活动。
start() 函数
start() 函数用于开启线程活动。函数原型如下:
特别提示:在使用本系统时,请确保每个线程对象仅能触发一次start()函数以避免出现RuntimeError错误。
0x07 总结
本文阐述了TCP协议与socket编程的基本理论知识,并通过Python3语言实现了与演示了一个完整的TCP服务器与客户端通信系统,在该过程中巧妙地融入了基础的多线程技术支持。同时,在开发过程中对涉及的核心Python API函数进行了详细的记录与说明,建立了相应的参考索引文档以供后续学习者理解和掌握实现原理。
