文章目录
参考
项目 | 描述 |
---|---|
Python 官方文档 | https://docs.python.org/zh-cn/3/ |
搜索引擎 | Google 、Bing |
描述
项目 | 描述 |
---|---|
操作系统 | Windows 10 专业版 |
PyCharm | 2023.1 (Professional Edition) |
Python | 3.10.6 |
TCP 服务器端与 TCP 客户端通信的基本流程
服务器端
-
创建一个监听套接字对象,指定地址族(
IPV4
或IPV6
)和套接字类型(TCP 套接字
或UDP 套接字
)。 -
通过使用
bind()
方法为监听套接字指定套接字地址。 -
调用
listen()
方法以指示监听套接字开始
对客户端的连接请求进行监听。 -
通过调用
accept()
方法接受客户端的连接请求,该方法将返回一个连接套接字及客户端套接字的套接字地址。 -
通过连接套接字与客户端进行通信,使用
recv()
方法接收客户端发送的数据,使用send()
方法向客户端发送响应。 -
重复步骤
5
,直到通信完成。 -
关闭服务器套接字。
客户端
-
创建一个客户端套接字对象,并为该套接字指定
地址族
和套接字类型
。 -
调用
connect()
方法通过服务器的监听套接字向服务器端发起连接请求。 -
通过客户端套接字与服务器进行通信,使用
send()
向服务器发送数据,使用recv()
方法接收服务器端返回的响应。 -
重复步骤
3
,直到通信完成。 -
关闭客户端套接字。
使用 socket 实现 TCP 服务器端
实现监听套接字
socket.socket()
在 socket
模块中,socket()
类用于创建 套接字对象
。
class socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None)
其中:
参数 | 描述 |
---|---|
family | 指定地址族类型,默认为 IPV4 。socket 模块提供常量 socket.AF_INET 和 socket.AF_INET6 分别用于指定地址族 IPV4 与 IPV6 。 |
type | 指定套接字类型,默认为 TCP 套接字 。socket 模块提供常量 socket.SOCK_STREAM 和 socket.SOCK_DGRAM 分别用于指定 TCP 套接字与 UDP 套接字。 |
proto | 指定套接字所使用的传输层协议,默认值为 0 。使用 默认值 0 意味着 socket() 将依据参数 family 与 type 的值自动选择合适的传输层协议。socket 模块提供了常量 socket.IPPROTO_TCP 和 socket.IPPROTO_UDP 分别用于指定传输层协议 TCP 与 UDP 。 |
fileno | 指定套接字所使用的 文件描述符(文件描述符是操作系统中用于标识打开文件或套接字的整数值) ,使用同一文件描述符的 套接字对象将 被视为同一个 套接字 。 |
Socket().bind()
socket
对象的 bind()
函数用于为 监听套接字
指定 套接字地址
。在 socket
编程中,套接字地址通常使用 元组
进行存储。其中,元组的第一个元素为 IP
地址,元组中的第二个元素为 端口号
。
IP 地址的选择
假设我们有一台服务器,具有两个网络接口 eth0
和 eth1
,分别对应 IP
地址 192.168.1.10
和 10.0.0.5
。
本地回环地址
如果我们使用 127.0.0.1
或 localhost
作为监听套接字的 IP 地址
,那么监听套接字将只监听来自 本地回环接口(Loopback Interface)
的连接请求,这意味着只有本机上的进程可以通过 localhost
或 127.0.0.1
与服务器建立连接,而来自其他网络接口的连接请求将被忽略。
本地回环接口是一个特殊的网络接口,通常用于本机内部的通信
。本地回环接口的 IP 地址
通常设置为127.0.0.1
,该 IP 地址通常被称为 本地主机地址
。
当监听套接字的监听地址被设置为 127.0.0.1
时,该套接字将仅监听来自本地回环接口的连接请求,而不会监听来自其他网络接口的连接。
注:
通过本地回环地址 127.0.0.1
或 localhost
访问主机时,数据不需要经过物理网络接口进行传输。
某一特定 IP 地址
如果我们使用某一特定的 IP
地址,如 192.168.1.10
作为监听套接字的 IP 地址
,那么监听套接字将仅监听来自 192.168.1.10
的连接请求。对于来自 10.0.0.5
的连接请求不予理会。
空字符串
当将一个 空字符串
作为套接字地址中的 IP
地址传递给 bind()
函数时,该函数会将监听套接字绑定到 系统上所有可用的网络接口
,这意味着监听套接字将监听所有来自可用的网络接口上的连接请求。
如果我们使用 空字符串
作为监听套接字地址的 IP 地址
,那么监听套接字将监听来自网络接口 eth0
、eth1
及 本地回环接口
的连接请求。
Socket().listen()
socket
对象的 listen()
方法的作用是告诉操作系统该套接字应该 开始监听传入的连接请求
。一旦调用了 listen()
方法,该套接字就将进入监听状态,并开始等待客户端的连接请求。
Socket().listen(backlog=5)
listen()
方法接收一个参数 backlog
,用于指定操作系统在未接受连接之前可以排队等待的 最大连接数
。
如果等待被处理的连接请求的数量即将超过最大连接数,则后续的客户端连接请求 可能
将被 拒绝
。参数 backlog
的取值为一个 大于或等于零的整数
,若参数值小于零,则该参数的 实际值
将为零。
监听套接字的实现
import socket as sk
# 规定监听套接字地址
HOST = ''
PORT = 8080
# 创建监听套接字
monitor = sk.socket()
# 为监听套接字指定套接字地址
monitor.bind((HOST, PORT))
# 启用监听套接字以监听来自客户端的连接请求
monitor.listen(100)
# 向控制台中输出服务器当前的运行状态
print(f'The Server is running at {"localhost" if HOST == "" else HOST}: {PORT}')
实现连接套接字
Socket().accept()
socket
的 accept()
方法的作用是在套接字进入监听模式后,阻塞当前线程
并等待客户端的连接请求。一旦有客户端连接请求到达,accept()
方法将返回一个 连接套接字对象
和 客户端套接字的地址
。通过连接套接字对象,服务器端将能够与客户端进行通信。
conn.send()
连接套接字对象的 send() 方法
的作用是将数据发送到 客户端套接字
,以便将数据传输到远程主机。send()
方法将返回已发送数据的 字节数
,若因为某些不可控因素导致数据 仅部分
发送至目标套接字,则程序需要自行处理数据未完全发送的情况。
连接套接字对象还具有一个与 send()
方法类似的方法 sendall()
,sendall()
方法的作用是将指定的数据发送到与当前套接字连接的另一端套接字。与 send()
方法不同的是,sendall()
方法会自动处理数据仅部分发送的情况,并保证所有数据都被发送出去。若数据无法 全部
发送至目标套接字,则该方法将 抛出异常
。
注:
使用 send()
或 sendall()
方法仅能够向远程主机传递二进制数据。通过调用字符串对象的 encode()
方法将能够得到该字符串对象的二进制数据形式。
监听套接字的实现
# 创建连接套接字
conn, addr = monitor.accept()
# 在接收到来自客户端的连接请求后
# 向控制台中输出相关提示信息。
print(f'The Server has received a connection from {addr[0]}: {addr[1]}')
# 向客户端发送成功连接至服务器的提示信息
# 在发送数据前需要通过 encode() 方法对发送数据进行 UTF-8 编码操作,
# 以将字符串类型数据转换为二进制数据。
conn.send('Can I help you?'.encode())
数据收发
conn.recv()
连接套接字对象的 recv()
方法,能够接收来自 客户端套接字
的数据,并将接收到的数据存储在指定大小的 缓冲区
中。
recv()
方法允许传递一个参数,用以指定存储数据的缓冲区大小(以 字节
为单位)。如果接收到的数据大小超过了设定缓冲区大小,那么服务器端将仅能够接收到缓冲区大小的数据。因此在实际使用时,通常需要在一个循环中多次调用recv()
方法,直到接收到所需的所有数据。
注:
recv()
方法是一个 阻塞调用
,即在没有接收到数据时,当前线程将会被阻塞,直到接收到数据或发生错误。可以通过调用 socket.setdefaulttimeout()
函数设置套接字的超时时长以控制阻塞的行为,当阻塞时长大于设定的超时时长时,recv()
方法将抛出异常。
具体实现
message = 'Content From Client'
while True:
# 接收信息并将其输出至控制台中。
# 由于接收到的数据为二进制数据,故在使用接收到的数据前
# 需要使用 decode() 方法对其进行解码操作。
message = conn.recv(9999).decode()
print(f'【Client】 {message}')
# 在客户端发送 Bye(不考虑大小写)后进行响应并终止循环
if message.lower() == 'bye':
conn.send('Bye'.encode())
break
# 通过控制台指定发送数据并通过 send() 函数将其发送至客户端
think = input('>>> ')
conn.send(think.encode())
关闭套接字
可以通过调用套接字对象的 close()
方法来关闭套接字及其现有的连接。
在套接字无需使用时,关闭套接字有助于防止资源泄露和保持系统的稳定性。如果不及时关闭套接字,可能会导致资源的浪费和应用程序的异常行为。此外,关闭套接字还可以 确保连接的清理和释放
,以便其他应用程序能够使用 相同的端口
进行通信。
# 关闭连接套接字
conn.close()
# 关闭监听套接字
monitor.close()
代码总汇
import socket as sk
# 规定监听套接字地址
HOST = ''
PORT = 8080
# 创建监听套接字
monitor = sk.socket()
# 为监听套接字指定套接字地址
monitor.bind((HOST, PORT))
# 启用监听套接字以监听来自客户端的连接请求
monitor.listen(100)
# 向控制台中输出服务器当前的运行状态
print(f'The Server is running at {"localhost" if HOST == "" else HOST}: {PORT}')
# 创建连接套接字
conn, addr = monitor.accept()
# 在接收到来自客户端的连接请求后
# 向控制台中输出相关提示信息。
print(f'The Server has received a connection from {addr[0]}: {addr[1]}')
# 向客户端发送成功连接至服务器的提示信息
# 在发送数据前需要通过 encode() 方法对发送数据进行 UTF-8 编码操作,
# 以将字符串类型数据转换为二进制数据。
conn.send('Can I help you?'.encode())
message = 'Content From Client'
while True:
# 接收信息并将其输出至控制台中。
# 由于接收到的数据为二进制数据,故在使用接收到的数据前
# 需要使用 decode() 方法对其进行解码操作。
message = conn.recv(9999).decode()
print(f'【Client】 {message}')
# 在客户端发送 Bye(不考虑大小写)后进行响应并终止循环
if message.lower() == 'bye':
conn.send('Bye'.encode())
break
# 通过控制台指定发送数据并通过 send() 函数将其发送至客户端
think = input('>>> ')
conn.send(think.encode())
# 关闭连接套接字
conn.close()
# 关闭监听套接字
monitor.close()
使用 socket 实现 TCP 客户端
Socket().connect()
socket
对象的 connect()
方法用于在客户端中通过 客户端套接字
向服务器端的 监听套接字
发起连接请求。
connect()
方法接收一个二元元组作为实参,该元组用于表示服务器端监听套接字的套接字地址。
connect()
方法没有返回值,它会尝试与指定的服务器建立连接。若连接建立失败,该方法将会抛出异常。
具体实现
import socket as sk
# 指定服务器端的监听套接字的套接字地址
HOST = '127.0.0.1'
PORT = 8080
# 创建客户端套接字
client = sk.socket()
# 通过 connect() 方法指定服务器端
# 监听套接字的套接字地址以向服务器端发起连接请求
client.connect((HOST, PORT))
message = 'Content From Server'
while True:
# 接收信息并将其输出至控制台中。
# 由于接收到的数据为二进制数据,故在使用接收到的数据前
# 需要使用 decode() 方法对其进行解码操作。
message = client.recv(9999).decode()
print(f'【Server】 {message}')
# 在服务器端发送 Bye(不考虑大小写)后进行响应并终止循环
if message.lower() == 'bye':
# 在发送数据前需要通过 encode() 方法对发送数据进行 UTF-8 编码操作,
# 以将字符串类型数据转换为二进制数据。
client.send('Bye'.encode())
break
# 通过控制台指定发送数据并通过 send() 函数将其发送至服务器端
think = input('>>> ')
client.send(think.encode())
# 关闭客户端套接字
client.close()
TCP 服务器与 TCP 客户端的双向通信
在进行 socket 编程
时,需要注意服务器端与客户端的 运行顺序
。TCP 服务器端
程序需要先于 TCP 客户端
程序运行。确保服务器先启动并能够对来自客户端的连接请求进行监听,然后客户端再启动并尝试连接到服务器端,这样可以确保服务器能够接受客户端的连接请求并建立有效的通信通道。
在运行上述 TCP 服务器端与 TCP 客户端实现后,存在如下运行效果。