第三模块 第25章 网络编程
https://www.cnblogs.com/linhaifeng/articles/6129246.html
思路整理:
数据在通过网络传输之前, 需要经过层层协议的加工包装, 这些协议就是五层协议. 为了简便开发, 出现了socket模块, 对这五层协议进行了封装.
使用socket模块进行网络通信需要注意的问题:
1. 循环通信
2. 服务端和多个客户端通信
3. 解决常见bugs:
1. 提示端口已被占用
2. 客户端发送的消息为空
3. 客户端单方面中断连接
4. 黏包现象
osi七层协议:
1. 五层协议:
物理层: 传输010110的二进制信号
数据链路层: Ethernet协议
规定数据包含数据头和数据部分, 数据头包含原地址和目标地址, 两者都是MAC地址, MAC地址存在于网卡上, 全球唯一.
基于MAC地址的广播的形式进行通信, 只适用于局域网.
网络层: IP协议
也分为数据头和数据部分, 数据头部分包含源IP地址和目标IP地址. 它们到了数据链路层都算作数据. IP地址标识所在子网.
IP地址和MAC地址能找到全球唯一的计算机.
ARP协议: 将IP地址解析成MAC地址, 仅靠IP地址就能找到计算机.
传输层: TCP/UDB协议
基于端口工作, 能定位到计算机上的软件. 计算机上每启动一个软件就对应一个端口(0-6535).
格式也是数据头和数据, 两者同为下一层的数据.
IP+端口号能定位全球范围内唯一的软件.
包含两种协议: tcp协议和udp协议.
tcp协议下数据以数据流的方式传输, 在传输数据之前需要先建立连接通道. 遵循三次握手和四次挥手的原则. 传输效率低.
udp协议下数据以包的形式传输, 传输数据之前无需建立连接通道, 只负责丢出数据, 不管是否收到数据, 传输数据不可靠. 传输效率高
SYN=1表示请求建立连接, ACK=1+x, 其中1表示同意建立连接, 其中x与seq中的x相对应, 表示回复的消息适用于上一个请求.
应用层: 可以规定自己的协议: http协议, ftp协议
产生数据, 格式为数据头和数据
以上五层可以分为两类: 自定义和他定义型.
2. 什么是socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
2.1 基础代码
# 服务端 import socket #1.买手机 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # print(phone) # <socket.socket fd=872, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> # family=AddressFamily.AF_INET表示基于网络的套接字, type=SocketKind.SOCK_STREAM表示使用的协议是tcp协议. #2.绑定手机卡 phone.bind(('127.0.0.1', 8080)) # bind()中需要传入元祖, 第一个参数是字符串类型的IP地址, 应该为运行软件的计算机的IP地址. # '127.0.0.1'表示本机地址, 客户端与服务端运行在同一台机器上, 供测试用. # 第二个参数是端口号, 范围为0-65535, 0-1024是给操作系统用的. #3.开机 phone.listen(5) # 其中, 5表示最大挂起的连接数. #4.等电话建立连接 # res = phone.accept() # 运行服务端后, 服务端会卡在此处. # print(res) # (<socket.socket fd=816, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, # laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 15205)>, ('127.0.0.1', 15205)) # 其中, <socket.socket fd=816, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 15205)>表示链接通道 # ('127.0.0.1', 15205)为客户端的IP和端口号. conn,client_addr = phone.accept() #5.收发消息 data = conn.recv(1024) # 1024代表1024个字节, 为接收数据的最大数. print('客户端的数据',data) conn.send(data.upper()) #6.挂电话 conn.close() #7.关机 phone.close() # 服务端有两种套接字:phone和conn # 注意: 重复运行客户端, 可能或报错, 说端口被占用, 可以修改端口.
# 客户端 import socket #1.买手机 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #2.拨号 phone.connect(('127.0.0.1',8080)) # 连接服务端的IP和端口 #3.发收消息 phone.send('hello'.encode('utf-8')) # 发送的内容必须是bytes. data = phone.recv(1024) print(data) #4.关闭 phone.close() # 客户端只有一种套接字:phone
2.2 循环通信(在基础代码的基础上加上循环)
# 服务端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn,addr = phone.accept() while True: data = conn.recv(1024).decode('utf-8') print('客户端消息:%s'%data) msg = input('>>>') conn.send(msg.encode('utf-8')) conn.close() phone.close()
# 客户端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8080)) while True: msg = input('>>>').strip() phone.send(msg.encode('utf-8')) data = phone.recv(1024).decode('utf-8') print('服务端消息:%s'%data) phone.close()
2.3 bug的解决方法
# 服务端 # 三种bug的解决方式: # bug1: 提示端口已被占用 解决方案1:更换端口号, 解决方案2:... # bug2: 客户端发送的消息为空 解决方案:让客户端继续输入, 直至不为空 # bug3: 客户端单方面中断连接 解决方案:windows系统下使用异常处理, linux系统下跳出循环 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 如果报错, 提示端口已被占用, 可以使用以下代码解决, 或者修改端口号 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn,addr = phone.accept() while True: try: data = conn.recv(1024).decode('utf-8') #如果客户端发的是空, 操作系统拿不到消息, 则不会发送, 服务端什么也收不到, 就阻塞在了这里. # recv()操作是应用程序向操作系统发送请求, 让其从网卡将数据收回来. # 收到数据后将数据从操作系统内存拷到应用程序内存. # if not data:break # 如果客户端单方面中断连接, 可以采用以上方法, 这种解决方案仅适用于linux系统 print('客户端消息:', data) msg = input('>>>').encode('utf-8') conn.send(msg) except ConnectionResetError: # 适用于windows操作系统 break conn.close() phone.close() # 如果客户端单方面断开连接, 服务端会出现死循环(linxu), 或者报错(windows).
# 客户端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while True: msg = input('>>>').strip().encode('utf-8') #可以发空, 但是操作系统接收到空消息后认为什么也没收到, 不会发送消息. if not msg: # 防止客户端发送空消息 continue phone.send(msg) # 将信息从应用程序的内存发送到操作系统的内存, # 操作系统参照传输层, 网络层, 数据链路层, 物理层对数据进行层层包装, 然后经由网卡发送. data = phone.recv(1024).decode('utf-8') print(data) phone.close()
2.4 服务端与多个客户端通信
# 服务端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: # 链接循环:只能一个一个来, 关掉一个才能运行第二个. 要真正实现与多个客户端通信, 则需要并发编程. conn,addr = phone.accept() while True: try: data = conn.recv(1024).decode('utf-8') print('客户端消息:', data) msg = input('>>>').encode('utf-8') conn.send(msg) except ConnectionResetError: break conn.close() phone.close()
# 客户端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while True: msg = input('>>>').strip().encode('utf-8') if not msg: continue phone.send(msg) data = phone.recv(1024).decode('utf-8') print(data) phone.close()
2.5 模拟ssh远程执行命_项目分析
如果所使用的服务器是托管的, 则需要通过网络远程控制服务器.
#知识点补充 ''' windows: 1. dir:查看某一个文件夹下的子文件名与子文件夹名 2. ipconfig: 查看本地网卡的ip信息 3. tasklist: 查看运行的进程 linux: 对应的分别为: 1. ls 2. ifconfig 3. ps aux ''' # import os # res = os.system('dir F:\python全栈开发\课件') # print(res) # 执行后的结果会直接交给终端, 赋值并不能拿到结果. # 打印结果为0表示执行成功, 否则表示执行失败. 所以, 不能使用本方法. import subprocess obj = subprocess.Popen('dir F:\python全栈开发\课件',shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(obj) print('stdout1:',obj.stdout.read().decode('gbk')) # 从正确管道读取结果 print('stdout2:',obj.stdout.read().decode('gbk')) # 取走后再取就取不出来了 print('stderr1:',obj.stderr.read().decode('gbk')) # 从错误管道读取结果
# 服务端 import socket import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: # 链接循环:只能一个一个来, 关掉一个才能运行第二个. 要真正实现与多个客户端通信, 则需要并发编程. conn,addr = phone.accept() while True: try: # 1. 接收命令 cmd = conn.recv(1024) # 2. 执行命令, 拿到结果 obj = subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() # 3. 把命令的结果返回给客户端 conn.send(stdout+stderr) # 使用+并不是在stdout的基础上直接加, 而是开辟新的内存将两者放进去, 可以进一步进行优化 except ConnectionResetError: break conn.close() phone.close()
# 客户端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while True: # 1. 发送命令 msg = input('>>>').strip().encode('utf-8') if not msg: continue phone.send(msg) # 2. 拿到命令结果, 并打印 data = phone.recv(1024).decode('gbk') print(data) phone.close()
2.6 黏包现象
2.6.1 黏包底层原理
send()操作: 应用程序想发送数据, 则需要先将自己内存中的数据copy到操作系统的内存, 然后由操作系统通过网卡发送出去. 至于如何发, 什么时候发都由操作系统控制, 不受应用程序的约束.
阶段1(copy数据): 将数据从应用程序内存copy到操作系统内存, 耗时短.
recv()操作: 应用程序通知操作系统, 调网卡接收数据, 先放在操作系统内存, 然后copy到应用程序的内存.
阶段1(等数据): 数据到达操作系统内存, 非常耗时.
阶段2(copy数据): 数据从操作系统内存到达应用程序内存.
不管是send还是recv, 都不是直接接收对方的数据, 而是操作自己的操作系统内存. 并不是一次send对应一次recv.
在发送时, 为了降低网络的IO次数, 采用Nagle优化算法, 将多次间隔小且数据量小的数据合并成一个大的数据块, 然后进行封包. 这种方式提高了效率, 但是会造成黏包现象.
黏包现象可能发生在发送端, 也可能发生在接收端.
两种黏包现象:
1. 连续的小包可能会被优化算法组合到一起进行发送. 这种黏包现象发生在输出缓冲区.
2. 第一次如果发送的数据大小 2000B, 接收端一次性接收大小为1024, 这就导致剩下的内容会被下一次recv接收到, 导致结果错乱. 这种黏包现象发生在输入缓冲区.
2.6.2 黏包现象的解决方案
2.6.2.1 简单版
# 知识点补充 import struct res = struct.pack('i',1234) print(res,type(res),len(res)) # b'\xd2\x04\x00\x00' <class 'bytes'> 4 obj = struct.unpack('i',res) print(obj) # (1234,)
# 服务端 import socket import subprocess import struct server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8001)) server.listen(5) while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() # 把命令的结果返回给客户端 # 第一步: 制作固定长度的报头 total_size = len(stdout) + len(stderr) header = struct.pack('i',total_size) # 第二步: 把数据的长度发送给客户端 conn.send(header) # 报头要有固定长度 # 第三步: 再发送真实的数据 conn.send(stdout) # 利用黏包现象 conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
# 客户端 import socket import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8001)) while True: msg = input('>>>').strip().encode('utf-8') if not msg: continue client.send(msg) # 拿命令结果 # 第一步: 先收固定长度的报头 header = client.recv(4) # 第二步: 从报头中解析出对真实数据的描述信息(数据长度) total_size = struct.unpack('i',header)[0] print('真实总长度++++++++++++++++++++++++',total_size) # 第三步: 接收真实的数据 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) recv_data+=data recv_size+=len(data) # 操作系统内存(缓存)不可能无限大, 所以接收的数据量写无限大没有意义. print('接收的总长度>>>>>>>>>>>>>>>>>>>>>>>>>',recv_size) print(recv_data.decode('gbk')) client.close()
# 客户端收到的结果 >>>ipconfig 真实总长度++++++++++++++++++++++++ 1076 接收的总长度>>>>>>>>>>>>>>>>>>>>>>>>> 1076 Windows IP 配置 以太网适配器 以太网: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 以太网 2: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 以太网 3: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 本地连接* 2: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 WLAN: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::5a:5a50:8164:3554%11 IPv4 地址 . . . . . . . . . . . . : 192.168.202.102 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.202.1 以太网适配器 蓝牙网络连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 隧道适配器 本地连接* 14: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : >>>
2.6.2.2 终极版
#知识补充 import struct res = struct.pack('i',1234) # i模式对数字大小有限制, 4bytes. l模式能够支持的范围更大, 8bytes. print(res,type(res),len(res)) # b'\xd2\x04\x00\x00' <class 'bytes'> 4 obj = struct.unpack('i',res) print(obj) # (1234,)
# 服务端 import socket import subprocess import struct import json server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8001)) server.listen(5) while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() # 第一步: 制作报头 total_size = len(stdout) + len(stderr) header = { 'filename':'a.txt', 'md5':'xxxx', 'total_size':total_size } header_bytes = json.dumps(header).encode('utf-8') # 第二步: 先发送报头的长度 conn.send(struct.pack('i',len(header_bytes)))# 第三步: 发送报头 conn.send(header_bytes) # 第四步: 发送真实的数据 conn.send(stdout) # 利用黏包现象 conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
# 客户端 import socket import struct import json client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8001)) while True: msg = input('>>>').strip().encode('utf-8') if not msg: continue client.send(msg) # 第一步: 先收固定长度的报头长度 obj = client.recv(4) header_size = struct.unpack('i', obj)[0] print(header_size) # 第二步: 收报头 header_bytes = client.recv(header_size) # 第三步: 从报头中解析出对真实数据的描述信息(数据长度) header_json = header_bytes.decode('utf-8') header_dic = json.loads(header_json) total_size = header_dic['total_size'] print('真实总长度++++++++++++++++++++++++',total_size) # 第四步: 接收真实的数据 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) recv_data+=data recv_size+=len(data) # 操作系统内存(缓存)不可能无限大, 所以接收的数据量写无限大没有意义. print('接收的总长度>>>>>>>>>>>>>>>>>>>>>>>>>',recv_size) print(recv_data.decode('gbk')) client.close()
2.7 实现文件传输功能
2.7.1 基础版
# 服务端 import socket import subprocess import struct import json import os server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8001)) server.listen(5) path = 'F:\python全栈开发\python\day25 网络编程\文件传输功能实现\server\share' while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) file_name = cmd.decode('utf-8').split()[1] # 把命令的结果返回给客户端 # 第一步: 制作报头 total_size = os.path.getsize(r'%s\%s'%(path,file_name)) header = { 'file_name':file_name, 'total_size':total_size } header_json = json.dumps(header) header_bytes = header_json.encode('utf-8') # 第二步: 先发送报头的长度 conn.send(struct.pack('i',len(header_bytes))) print(len(header_bytes)) # 第三步: 发送报头 conn.send(header_bytes) # 第四步: 再发送真实的数据 with open(r'%s\%s'%(path,file_name),mode='rb') as f: for line in f: conn.send(line) except ConnectionResetError: break conn.close() server.close()
# 客户端 import socket import struct import json client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8001)) path = 'F:\python全栈开发\python\day25 网络编程\文件传输功能实现\client\download' while True: msg = input('>>>').strip().encode('utf-8') if not msg: continue client.send(msg) # 第一步: 先收固定长度的报头长度 obj = client.recv(4) header_size = struct.unpack('i', obj)[0] print(header_size) # 第二步: 收报头 header_bytes = client.recv(header_size) # 第三步: 从报头中解析出对真实数据的描述信息 header_json = header_bytes.decode('utf-8') header_dic = json.loads(header_json) total_size = header_dic['total_size'] file_name = header_dic['file_name'] print('真实总长度++++++++++++++++++++++++',total_size) # 第四步: 接收真实的数据 recv_size = 0 with open(r'%s\%s'%(path,file_name),mode='wb') as f: while recv_size < total_size: data = client.recv(1024) f.write(data) recv_size+=len(data) # 操作系统内存(缓存)不可能无限大, 所以接收的数据量写无限大没有意义. print('接收的总长度>>>>>>>>>>>>>>>>>>>>>>>>>',recv_size) client.close()
2.7.2 函数版
# 服务端 import socket import subprocess import struct import json import os def get(conn, cmd, path): file_name = cmd.decode('utf-8').split()[1] total_size = os.path.getsize(r'%s\%s' % (path, file_name)) header = { 'file_name': file_name, 'total_size': total_size } header_json = json.dumps(header) header_bytes = header_json.encode('utf-8') conn.send(struct.pack('i', len(header_bytes))) conn.send(header_bytes) with open(r'%s\%s' % (path, file_name), mode='rb') as f: for line in f: conn.send(line) def put(conn, cmd, path): pass def run(): server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8001)) server.listen(5) path = 'F:\python全栈开发\python\day25 网络编程\文件传输功能实现\函数版\server\share' while True: conn,addr = server.accept() while True: try: cmd = conn.recv(1024) if cmd.decode('utf-8').split()[0] == 'get': get(conn, cmd, path) elif cmd.decode('utf-8').split()[0] == 'put': put(conn, cmd, path) except ConnectionResetError: break conn.close() server.close() if __name__ == '__main__': run()
# 客户端 import socket import struct import json def get(client,path): obj = client.recv(4) header_size = struct.unpack('i', obj)[0] header_bytes = client.recv(header_size) header_json = header_bytes.decode('utf-8') header_dic = json.loads(header_json) total_size = header_dic['total_size'] file_name = header_dic['file_name'] recv_size = 0 with open(r'%s\%s' % (path, file_name), mode='wb') as f: while recv_size < total_size: data = client.recv(1024) f.write(data) recv_size += len(data) def put(client,path): pass def run(): client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8001)) path = 'F:\python全栈开发\python\day25 网络编程\文件传输功能实现\函数版\client\download' while True: msg = input('>>>').strip() if not msg: continue client.send(msg.encode('utf-8')) if msg.split()[0] == 'get': get(client,path) elif msg.split()[0] == 'put': put(client,path) client.close() if __name__ == '__main__': run()
2.7.3 面向对象版
# 服务端 import socket import struct import json import subprocess import os class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize)) tcpserver1=MYTCPServer(('127.0.0.1',8080)) tcpserver1.run()
# 客户端 import socket import struct import json import os class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return else: filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful') client=MYTCPClient(('127.0.0.1',8080)) client.run()
2.8 基于UDP协议的套接字介绍
# 服务端 # udp协议又称数据报协议 # 一次send必须对应一次recv # 不会黏包, 即便发空也是一个完整的包. # QQ是基于udp协议写的, 有些查询操作是基于udp协议写的 import socket server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) server.bind(('127.0.0.1', 8001)) while True: data,client_addr = server.recvfrom(1024) # 如果数据长度为5, 而只接收1个, 则只会收1个, 其余的数据会丢失. windows中会报错. print(data) # (b'hello', ('127.0.0.1', 53762)) server.sendto(data.upper(),client_addr) server.close()
# 客户端 import socket client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) while True: msg = input('>>>').strip() client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8001)) data,server_addr = client.recvfrom(1024) print(data, server_addr) client.close()