网络与并发(三)
Python-网络与并发
第三章 SocketServer
我们在上一章讲到简单的Socket的编程实现操作,那我们现在来完成基于服务器与客户端的协议之间交互信息
1. UPD
UDP — 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
UDP特点:
UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。 UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。 UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。
【适用情况】
UDP是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP一般用于多点通信和实时的数据业务,比如
- 语音广播
- 视频
- TFTP(简单文件传送)
- SNMP(简单网络管理协议)
- RIP(路由信息协议,如报告股票市场,航空信息)
- DNS(域名解释)
相比较于TCP注重速度流畅
UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。
那如何实现尼?
我们创建一个udp客户端程序的流程是简单的,具体步骤如下:
'''
1. 创建客户端套接字
2. 发送/接收数据
3. 关闭套接字
'''
打开我们的开发工具编译器
客户端如下代码:
发送数据
# -*- coding: UTF-8 -*-
# 文件名:client.py
# 导入 socket 模块
from socket import *
# 创建套接字
client_socket = socket(AF_INET, SOCK_DGRAM)
# 准备接收方地址
server_host_post = ('127.0.0.1', 8080)
# 发送数据时,python3需要将字符串转成byte
# encode(‘utf-8’)# 用utf-8对数据进行编码,获得bytes类型对象
client_data = input("请输入:").encode('utf8')
# 将拿到转为bytes类型data数据对象,通过socket中的sendto方法,将数据发送到ip+协议+端口对应的地址
client_socket.sendto(client_data, server_host_post)
# 如果发送成功我们就提示一句发送成功
print('发送成功')
# 关闭客户端
client_socket.close()
客户端的结构
(1)使用socket(),生成套接字描述符;
(2)通过host_post 结构设置服务器地址和监听端口;
(3)向服务器发送数据,sendto() ;
(4)关闭套接字,close() ;
运行看看
通过客户端发送成功,我们指定了端口,但是我们没有与服务器建立连接,没有确定8080端口的连接对象,可以将ip地址与端口随意需改符合要求的范围,都可以将数据正常发送
此时数据发送,这里就体现UDP数据的特性,面向无连接的通讯协议
这里我们接受数据该如何实现尼?往下看
服务器端如下代码:
接收数据
# -*- coding: UTF-8 -*-
# 文件名:server.py
# 导入 socket 模块
from socket import *
# 创建套接字对象
socket = socket(AF_INET, SOCK_DGRAM)
# 准备接收地址
host_post = ('127.0.0.1', 8080)
# 绑定地址、端口,类型为元组
socket.bind(host_post)
# 这里接受到的数据socket.recvfrom(1024)是一个元组形式
# 那这里可以看看具体的信息
data = socket.recvfrom(1024)
print(data)
print(data[0].decode('utf8'))
# 关闭连接
socket.close()
服务器端的结构
(1)使用函数socket(),生成套接字描述符;
(2)通过host_post 结构设置服务器地址和监听端口;
(3)使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(host_post)进行绑定;
(4)接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
(5)关闭套接字,使用close() 函数释放资源;
那让我们来看一下运行的效果与结果,
先运行我们的服务器,再运行我们的客户端,输入我们需要给到服务器的数据,
在客户端,观察发现向服务器发送消息的时候没有给客户端绑定端口?原来操作系统在此 做了些隐蔽的事情,当socket首先向服务器发消息时客户端自动选折IP和一个PORT与该socket关联了起来。
那客户端与服务器之间的交互,能不能绑定端口,更多是多人交互尼,来我们继续往下走
echo服务的应用 ,echo服务是一种非常有用的用于调试和检测的工具。该协议接收到什么原样发回,类似于日常生活中的“回声”,即存在回显
我们修改服务器与客户端的代码
客户端代码如下:
# -*- coding: UTF-8 -*-
# 文件名:client.py
import socket
# 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
# 准备接收方地址
server_host_post = ('127.0.0.1', 12345)
# 发送数据时,python3需要将字符串转成byte
# encode(‘utf-8’)# 用utf-8对数据进行编码,获得bytes类型对象
client_data = input("请输入:").encode('utf8')
# 将拿到转为bytes类型data数据对象,通过socket中的sendto方法,将数据发送到ip+协议+端口对应的地址
client_socket.sendto(client_data, server_host_post)
# 这里接受到的数据socket.recvfrom(1024)是一个元组形式,接受服务器返回来的信息
print('返回数据是:', client_socket.recvfrom(1024)[0].decode('utf8'))
# 关闭客户端
client_socket.close()
服务器代码如下:
# -*- coding: UTF-8 -*-
# 文件名:server.py
import socket
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定本地的相关信息
# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
server_host_post = ('', 12345)
server_socket.bind(server_host_post)
while True:
# 等待接收对方发送的数据
# 1024表示本次接收的最大字节数
client_data = server_socket.recvfrom(1024)
print(client_data[0])
# 返回给客户端消息
server_socket.sendto(client_data[0], client_data[1])
server_socket.close()
再次来看运行结果
没有给客户端指定唯一的端口与ip地址,而是通过服务器端口进行传输返回数据,
此时如果我在加入新的客户端,是否就可以完成多人聊天室的功能,UPD的广播形式,再看,我将复制一个客户端文件,命名为client1,现在将使用两个客户端对服务器进行发送消息
udp是TCP/IP协议族中的一种协议能够完成不同机器上的程序间的数据通信
- udp的服务器和客户端的区分:往往是通过`请求服务`和`提供服务`来进行区分
- 请求服务的一方称为:客户端
- 提供服务的一方称为:服务器
一般情况下,服务器端,需要绑定端口,目的是为了让其他的客户端能够正确发送到此进程
客户端,一般不需要绑定,而是让操作系统随机分配,这样就不会因为需要绑定的端口被占用而导致程序无法运行的情况
2. TFTP
TFTP(Trivial File Transfer Protocol,简单文件传输协议)
是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
特点:
- 简单
- 占用资源小
- 适合传递小文件
- 适合在局域网进行传递
- 端口号为69
- 基于UDP实现
TFTP服务器默认监听69号端口
当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送
服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面
操作码 | 功能 |
---|---|
1 | 读请求,即下载 |
2 | 写请求,即上传 |
3 | 表示数据包,即DATA |
4 | 确认码,即ACK |
5 | 错误 |
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了,TFTP数据包的格式如下:
那既然是基于UDP实现的代码,那我们来看是如何实现的
服务器端代码:
# -*- coding: UTF-8 -*-
# 文件名:TFTP_server.py
from socket import *
import struct
def download(filename, user_ip, user_port):
socket_down = socket(AF_INET, SOCK_DGRAM)
num = 0
try:
f = open(filename, 'rb')
except:
error_data = struct.pack('!HHHb', 5, 5, 5, num)
socket_down.sendto(error_data, (user_ip, user_port)) # 文件不存在时发送
exit() # 只会退出此线程
while True:
read_data = f.read(512)
send_data = struct.pack('!HH', 3, num) + read_data
socket_down.sendto(send_data, (user_ip, user_port)) # 数据第一次发送
if len(read_data) < 512:
print('传输完成, 对方下载成功')
exit()
recv_ack = socket_down.recv(1024) # 第二次接收
caozuoma, ack_num = struct.unpack("!HH", recv_ack)
# print(caozuoma,ack_num,len(read_data))
num += 1
if int(caozuoma) != 4 or int(ack_num) != num - 1:
exit()
f.close()
s = socket(AF_INET, SOCK_DGRAM)
s.bind(('', 69))
def main():
while 1:
recv_data, (user_ip, user_port) = s.recvfrom(1024) # 第一次客户连接69端口
print(recv_data, user_ip, user_port)
if struct.unpack('!b5sb', recv_data[-7:]) == (0, b'octet', 0):
caozuoma = struct.unpack('!H', recv_data[:2])
filename = recv_data[2:-7].decode('gb2312')
if caozuoma[0] == 1:
print('对方想下载数据', filename)
download(filename, user_ip, user_port)
if __name__ == '__main__':
main()
客户端代码:
# coding=utf-8
# 文件名:tftp_client.py
import struct
from socket import *
filename = 'test.jpg'
server_ip = '127.0.0.1'
send_data = struct.pack('!H%dsb5sb' % len(filename), 1, filename.encode('gb2312'), 0, 'octet'.encode('gb2312'), 0)
tftp_client = socket(AF_INET, SOCK_DGRAM)
# 第一次发送, 连接服务器69端口
tftp_client.sendto(send_data, (server_ip, 69))
# 打开二进制文件,追加写如文件
f = open(filename, 'ab')
while 1:
# 接收数据
recv_data = tftp_client.recvfrom(1024)
# 获取数据块编号
caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4])
rand_port = recv_data[1][1] # 获取服务器的随机端口
if int(caozuoma) == 5:
print('服务器返回: 文件不存在...')
break
print(caozuoma, ack_num, rand_port, len(recv_data[0]))
f.write(recv_data[0][4:])
if len(recv_data[0]) < 516:
break
ack_data = struct.pack("!HH", 4, ack_num)
# 回复ACK确认包
tftp_client.sendto(ack_data, (server_ip, rand_port))
运行结果如下
看到原本本地的数据是没有的,当然实际上我们就是从本地凭空生成或者是读取一个TFTP的文件,当我们通过此方式,将数据进行连接传输,那我们就能完成使用对TFTP协议的使用和交互数据,这里我们理解了关于基于UDP模式下的多用户模式的或者是高速率信息传输,但不保证数据的完整性,那我们在需要准确数据保证数据不会丢失的情况该怎么办尼?例如发送或者接受邮件,例如资料文件等传输,我们需要准确的数据信息,基于TCP的面向连接。
3. TCP
传输控制协议(TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,也是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
例如生活中的电话机,我想让别人能更够打通我的电话交流或者完成工作等等事务前,我还得需要做以下几件事情:
-
买个手机
-
插上手机卡
-
设计手机为正常接听状态(即能够响铃)
-
静静的等着别人拨打
如同上面的电话机过程一样,在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)recv/send接收发送数据
由此一个很简单的tcp服务器如下:
# coding=utf-8
# 文件名:tcp_server.py
from socket import *
# 创建socket
# SOCK_STREAM基于TCP
tcp_Socket = socket(AF_INET, SOCK_STREAM)
# 绑定本地信息
# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
host_port = ('', 12345)
tcp_Socket.bind(host_port)
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_Socket.listen(5)
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器
# newSocket用来为这个客户端服务
# tcp_Socket就可以省下来专门等待其他新客户端的链接
newSocket, clientAddr = tcp_Socket.accept()
# 接收对方发送过来的数据,最大接收1024个字节
recvData = newSocket.recv(1024)
print('接收到的数据为:', recvData.decode('utf8'))
# 发送一些数据到客户端
senData = "thank you !"
newSocket.send(senData.encode('utf8'))
# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
newSocket.close()
# 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
tcp_Socket.close()
由此我们的客户端创建好了。
客户端代码如下
# coding=utf-8
# 文件名:tcp_client.py
# 创建socket
tcp_client = socket(AF_INET, SOCK_STREAM)
# 链接服务器
host_port = ('127.0.0.1', 12345)
tcp_client.connect(host_port)
# 提示用户输入数据
sendData = input("请输入要发送的数据:")
# 发送数据给指定的客户端
tcp_client.send(sendData.encode('utf8'))
# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcp_client.recv(1024)
print('接收到的数据为:', recvData.decode('utf8'))
# 关闭套接字
tcp_client.close()
我们先来看看运行后的结果
在我们的服务器端,我们绑定IP+端口协议方式,将服务器的数据传输方式绑定,基于数据传输,返回指定数据连接服务器的信息,在这里面我们完成了对数据基于tcp的传输流程方式,当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
tcp注意点
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
- tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
- tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
那我们就可以来了解一下如何实现模拟QQ的聊天
模拟QQ的聊天
服务器代码
# coding=utf-8
# 文件名:qq_server.py
from socket import *
# 创建socket
tcp_server = socket(AF_INET, SOCK_STREAM)
# 绑定本地信息
host_port = ('', 12345)
tcp_server.bind(host_port)
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_server.listen(5)
while True:
# 如果有新的客户端来链接服务器,那么就产生一个信心的套接字专门为这个客户端服务器
# newSocket用来为这个客户端服务
# tcpSerSocket就可以省下来专门等待其他新客户端的链接
newSocket, host_port = tcp_server.accept()
while True:
# 接收对方发送过来的数据,最大接收1024个字节
recvData = newSocket.recv(1024)
# 如果接收的数据的长度为0,则意味着客户端关闭了链接
if len(recvData) > 0:
print('recv:', recvData)
else:
break
# 发送一些数据到客户端
sendData = input("send:")
newSocket.send(sendData.encode('utf8'))
# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
newSocket.close()
# 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
tcp_server.close()
客户端代码如下:
# coding=utf-8
# 文件名:qq_server.py
from socket import *
# 创建socket
tcp_client = socket(AF_INET, SOCK_STREAM)
# 链接服务器
host_port = ('127.0.0.1', 12345)
tcp_client.connect(host_port)
while True:
# 提示用户输入数据
sendData = input("send:")
if len(sendData) > 0:
tcp_client.send(sendData.encode('utf8'))
else:
break
# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcp_client.recv(1024)
print('recv:', recvData.decode('uft8'))
# 关闭套接字
tcp_client.close()
好我们直接来看看运行的结果
以上我们完成了简易的QQ通信功能,通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。双方间的数据传输都可以通过这一个连接进行。完成数据交换后,双方必须断开此连接,以释放系统资源。这种连接是一对一的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构