上一章我们学习了如何编写网络客户端,建立socket套接字对象连接到服务器,并和服务器通信。接下来我们将介绍如何编写网络服务器、如何获取客户端的信息、 如何把活动记入日志,以及用不同方式来运行服务器。
2.1 准备连接
对于一个客户端来说,建立一个TCP连接的过程大致可分两步,即建立socket对象以及调用connect()来与服务器建立连接。对于服务器来说,这个过程分为如下四步:
2.1.1建立socket对象。
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2.1.2 设置socket选项(可选)。
python提供了setsockopt()和getsockopt()方法分别用于设置和获取套接字选项。
# 设置套接字选项,允许重新使用地址和端口。
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socket.SOL_SOCKET :表示设置的是套接字级别的选项。
socket.SO_REUSEADDR :选项名称,表示允许地址重用。
1 :表示True,是选项的值,表示启用该选项。
通常情况下,当服务器程序关闭后,会保留一段时间的套接字,即使服务器已经关闭,这段时间内新的套接字无法绑定到相同的地址和端口上。如果希望在服务器程序关闭后立即重新启动,并绑定到相同的地址和端口上,就可以使用 SO_REUSEADDR 选项。
# 获取套接字超时时间
timeout = server_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO)
print('Receive timeout:', timeout)
SOL_SOCKET常用到的选项
选项级别 | 功能 | 说明 |
---|---|---|
SO_REUSEADDR | 允许地址重用。 | 允许一个套接字绑定到一个已在使用中的地址,通常用于服务器程序在重启后快速重新启动。 |
SO_KEEPALIVE | 开启或关闭 TCP 的 keep-alive 功能。 | 当启用时,套接字会周期性地检测连接是否仍然活动。 |
SO_RCVBUF | 设置接收缓冲区大小 | |
SO_SNDBUF | 设置发送缓冲区的大小 | |
SO_REUSEPORT | 允许多个套接字绑定到相同的端口上。 | 通常用于多进程或多线程服务器,提高并发连接的性能。 |
SO_BROADCAST | 允许发送广播数据包。 | |
SO_ERROR | 获取套接字错误状态。 | |
SO_LINGER | 设置关闭套接字时的行为。 | 如果开启了 SO_LINGER,并设置了超时时间,那么当调用 close() 方法时,套接字会等待一段时间,直到发送或接收缓冲区中的数据被发送或超时。 |
SO_REUSEPORT | 允许多个套接字绑定到相同的端口上 | 通常用于负载均衡或者多进程/多线程服务器。 |
我们可以通过以下代码列出python所支持的选项列表
import socket
# 返回socket模块中所有属性和方法,并过滤出以‘SO_’开头的常量
socket_list = [x for x in dir(socket) if x.startswith('SO_')]
socket_list.sort()
for sockets in socket_list:
print(sockets)
2.1.3 绑定到一个端口。
server_socket.bind(('', 12345))
bind()函数的第一个参数是要绑定的IP地址,此处为空,表示可以绑定到所有接口和地址,第二个参数指定服务器监听连接的端口号。
2.1.4 监听连接。
server_socket.listen(5)
通过调用listen()函数通知操作系统准备接收连接。它只有一个参数,指明服务器在实际处理连接的时候,允许有多少个未决的连接在队列中等待。
2.2 接受连接
大多数服务器都设计成运行不确定时间(几个月甚至几年)和同时服务于多个连接。与此相反,客户端一般只有几个连接,并且会运行到任务完成或用户终止。
**通常使服务器持续运行的方法是使用一个无限循环。**如下例子:
while True:
client_sock, client_addr = server_socket.accept()
print('Connection from:', client_sock.getpeername())
client_sock.close()
通常情况下,无线循环是不好的,因为它会耗尽系统CPU的资源。但是,此处的无限循环是不同的,此处accept()是一个阻塞方法,用于等待客户端连接的请求。具体来说,accept()方法会阻塞程序的执行,直到有客户端尝试连接到服务器。一旦有客户端连接请求到达,该方法会返回一个包含两个元素的元组:连接到客户端的新套接字对象和客户端地址。一个停止并等待输入或输出的程序被称为阻塞的程序。
2.3 简单TCP服务器的实现及测试
以下是一个简单TCP服务器实现的完整代码,可用于同时处理多个客户端连接,但每个连接都是单线程的,即每个连接都是依次处理的。
import socket
# 1.建立socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.设置socket选项(可选)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3.绑定到一个端口
host = '' # 服务器地址,空字符串表示本机所有网络接口
port = 12345
server_socket.bind((host, port))
# 4.监听连接
server_socket.listen(5) # 最多允许5个连接
print(f'Server started on {host}:{port}')
while True:
# 接受客户端连接
client_socket, client_addr = server_socket.accept()
# 向客户端发送成功连接信息
client_socket.send(b'Successful Connection!\r\n')
print('Connected by', client_addr)
# 处理客户端请求
while True:
data = client_socket.recv(1024)
if not data:
break
print('Received:', data.decode('utf-8'))
# 回复客户端
client_socket.send(b'\r\nHello from server!')
client_socket.close()
现在对这个服务器程序进行测试,可以使用操作系统的telnet指令。具体步骤如下(以Windows系统为例):
- 运行程序
- 打开cmd终端,输入指令 telnet localhost 12345
Telnet 是一种网络协议,允许用户通过 TCP/IP 网络连接到远程主机,并在远程主机上执行命令。通常用于远程登录和远程管理。这条指令的意思是连接到本地主机的12345端口。执行本条指令时注意要打开本机telnet远程服务,具体如何打开此处不作表述,请有需要的读者自行网上查阅。
此时我们输入任意一个字符,我们将收到来自服务器的应答
服务器程序运行窗口如下
2.4 简单的UDP服务器的编写及测试
从客户端角度来看,使用UDP比TCP困难,因为客户端必须要注意丢失信息的问题。而另一方面,在服务器端使用UDP则要容易得多。我们在编写UDP服务器的时候,不用考虑丢失信息包的问题。毕竟,如果客户端来的信息包一直没到达的话,UDP服务器根本就不会知道曾经有客户端试图发送过请求。
关于如何在服务器端使用UDP,我们可以像使用TCP那样建立一个socket对象,设置选项。但由于UDP是无连接的协议,我们不必使用listen()或accept(),我们使用的是recvfrom()方法。recvfrom()方法类似于accept(),是一个阻塞方法,会阻塞程序执行,直到接收到数据为止。该方法返回接收到的数据和发送数据的源地址(客户端地址,包括IP和端口号)。
以下是一个简单的UDP服务器的程序实现:
import socket
host = ''
port = 12345
# 创建UDP socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定指定地址和端口
server_socket.bind((host, port))
print(f'Server started on {host}:{port}')
while True:
# 接收来自客户端的消息和地址
data, client_addr = server_socket.recvfrom(1024)
print(f'Received message "{data.decode('utf-8')}" from {client_addr}')
# 发送响应消息给客户端
server_socket.sendto(b'Hello,client!', client_addr)
下面介绍两种方法对UDP服务器进行测试(此处以Windows系统为例)
方法一 使用第三方工具Netcat(nc)进行测试
之前我们测试TCP服务器是用的Telnet指令,Telnet是一个基于文本的协议,通常用于与TCP服务器进行交互,它不适用于UDP协议,因为UDP协议是无连接的,无法像TCP那样建立持久的连接。
Netcat工具的安装及使用方法请读者自行上网查阅资料,此处不做过多说明。
- 运行服务器程序代码
- 安装好Netcat工具并配置好环境变量后,打开系统cmd窗口,输入指令 nc -u localhost 12345,回车,该指令向localhost(本机)的12345端口发送UDP数据包。接着在此窗口继续输入消息并回车即可向服务器端发送消息。此时也将收到来自UDP服务器的答复。
我们也可以使用管道操作符" | "将前一个命令的输出作为后一个命令的输入将两条指令合并成一条:echo Hello,Server! | nc -u localhost 12345
- 服务器收到数据并给出答复。
方法二 自定义UDP客户端脚本
以下是一个简单的UDP客户端脚本示例:
import socket
host = 'localhost'
port = 12345
# 创建UDP socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 需要发送的消息
message = 'Hello,Server!'
# 发送消息到服务器
client_socket.sendto(message.encode('utf-8'), (host, port))
# 接收服务器响应
response, server_addr = client_socket.recvfrom(1024)
print('Received response:', response.decode('utf-8'))
client_socket.close()
我们首先运行服务器:
然后直接运行客户端程序,我们将发送 “Hello,Server!” 消息到服务器,然后等待服务器的响应。
此时服务器将接收到客户端发送的消息并给出回复