基于 TCP & UDP 协议的 socket 通信
socket 通信
TCP版本:
# 最终版本,解决了 TCP 协议中的粘包问题
# 客户端 import socket import struct import json client = socket.socket() # 先生成一个客户端对象 client.connect(('127.0.0.1', 8080)) # 绑定服务端 # 里面接收一个值 - 元祖 元祖里面传两个值,分别为IP地址以及端口号 while True: msg = input('>>> ').strip().encode('utf-8') # 首先输入命令,并且转化为字节 if len(msg) == 0: continue # 如果字节长度为0 ,说明没有内容传入,就结束本次循环,重新输入指令 client.send(msg) # 接收服务端传来的字典报头 d_head = client.recv(4) # 解析字典报头,获取字典长度信息 d_len = struct.unpack('i', d_head)[0] # 解析报头必须后面添加索引[0] # 接收字典数据,并反序列化字典 d_bytes_data = client.recv(d_len) d = json.loads(d_bytes_data) # 接收真实结果 real_data = b'' recv_size = 0 while recv_size < d['file_size']: # 字典d里面有真实结果的字节长度信息 data = client.recv(1024) real_data += data # 拼接真实结果 recv_size += len(data) # 因为最后接收的不一定是1024,所以以每次接收的长度增值运算 print(real_data.decode('gbk')) # 因为windows终端默认使用的是GBK格式,所以以GBK解码
# 服务端 import socket import subprocess import json import struct server = socket.socket() # 先生成一个服务端对象 server.bind(('127.0.0.1', 8080)) # 里面接收一个值 - 元祖 元祖里面传两个值,分别为IP地址以及端口号 绑定自身IP server.listen(5) # 半连接池 while True: # 连接循环 conn, addr = server.accept() # 服务器一直处于等待用户访问状态 while True: # 通信循环 try: cmd = conn.recv(4) # 接收用户的传来的信息 if len(cmd) == 0: break # 判断用户传来的指令是否为空,空的话不执行命令,继续等待新的命令 # 通过subprocess模块在终端执行用户传来的命令 obj = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) res = obj.stdout.read() + obj.stderr.read() # 接收执行命令后的结果内容(包括正确的信息和错误的信息) d = {'file_size': len(res)} # 将结果内容的长度封在字典中,可以避免数字太大不能接收的情况 d_json = json.dumps(d) # 基于网络传输必须是二进制数据,所以要先序列化 # 要想传输字典,先告诉客户端字典的字节长度,所以先生成字典的报头,在传输字典,避免黏包问题 header = struct.pack('i', len(d)) conn.send(header) # 传送报头 conn.send(d_json) # 传送序列化之后的字典 conn.send(res) # 客户端得到字典后就可以获取真实结果的字节长度了,所以此时再将真实结果传送过去 except ConnectionResetError: break # 报错则跳出通信循环 conn.close()
例子:用TCP协议往服务端上传一个本地文件
# 以上传本地视频为例 # 本地视频位置:r'C:\Users\赵帅平\Desktop\d
# 客户端 import os import socket import json import struct client = socket.socket() client.connect(('127.0.0.1', 8888)) while True: # 循环打印电影列表 MOVIE_DIR = r'C:\Users\赵帅平\Desktop\day28\视频' movie_list = os.listdir(MOVIE_DIR) for index, movie in enumerate(movie_list, 1): print(index, movie) # 让用户进行选择 choice = input('please choose a movie to upload >>> ').strip() if choice.isdigit(): choice = int(choice) - 1 if choice in range(0, len(movie_list)): movie = movie_list[choice] # 拼接要上传电影的绝对路径 movie_path = os.path.join(MOVIE_DIR, movie) # 获取电影的大小 movie_size = os.path.getsize(movie_path) # 定义一个字典,将电影大小添加进去 d = { 'file_name': '常山赵子龙.mp4', # 自己定义的名字,可以不定义 'movie_size': movie_size } # 序列化字典,并生成一个字典报头 d_json = json.dumps(d) d_bytes = d_json.encode('utf-8') header = struct.pack('i', len(d_bytes)) # 发送字典报头 client.send(header) # 发送序列化后的字典 client.send(d_bytes) # 服务端接收到字典后就可以获取要长传的电影字节大小信息,所以上传电影 with open(movie_path, 'rb') as f: for line in f: client.send(line) # 等待客户端的返回结果 res = client.recv(17).decode('utf-8') a = 'upload complete !' if res == a: print('%s 上传成功!' % d.get('file_name')) else: print('choice is out of movie_list, please try again...') continue else:
print('choice must be number...')
# 服务端 import socket import struct import json server = socket.socket() server.bind(('127.0.0.1', 8888)) server.listen(5) # 半连接池 while True: conn, addr = server.accept() while True: try: # 接收字典报头 head = conn.recv(4) # 解析字典报头,获取字典字节长度 d_len = struct.unpack('i', head)[0] # 接收序列化后的字典 d_bytes = conn.recv(d_len) # 反序列化字典 d = json.loads(d_bytes.decode('utf-8')) # 循环写入 recv_size = 0 with open(d.get('file_name'), 'ab') as f: while recv_size < d['movie_size']: data = conn.recv(1024) recv_size += len(data) f.write(data) print('%s 接收上传完毕!' % d.get('file_name')) conn.send(b'upload complete !') except ConnectionResetError as e: print(e) break conn.close()
UDP版本:
UDP协议的特点:数据报协议(自带报头)
基于UDP协议传输数据 数据是不安全的
与TCP协议的区别:多个客户端可以实现并发的效果
服务端不存在,客户端也不会报错(sendto)
不会黏包
允许发空
# 客户端 import socket client = socket.socket(type=socket.SOCK_DGRAM) # UDP协议没有双向通道,所以不需要建立连接,直接进行通信循环 # 但需要指定要发送的地址:IP + port server_address = ('127.0.0.1', 9001) while True: client.sendto(b'hello', server_address) # 发送两个参数,一个为消息内容,一个为地址信息 且用sendto 发送 data, server_addr = client.recvfrom(1024) # 接收两个值, 一个是服务端消息,一个是服务端地址 print('来自服务端的消息:', data.decode('utf-8')) # 来自服务端的消息: HELLO print('服务端地址: ', server_addr) # 服务端地址: ('127.0.0.1', 9001)
# 服务端 import socket server = socket.socket(type=socket.SOCK_DGRAM) # ()内默认为TCP协议,传参以后就是UDP协议 server.bind(('127.0.0.1', 9001)) # UDP协议没有半连接池,所以不需要设置半连接池 # UDP协议没有双向通道,所以不需要accept() # 直接进入通信循环 while True: data, addr = server.recvfrom(1024) # 接收两个值,一个为客户端数据,一个是客户端IP地址+端口 # 注意是recvfrom print(data.decode('utf-8')) # hello print(addr) # ('127.0.0.1', 58709) server.sendto(data.upper(), addr) # 发送时候也是需要发送两个值,一个是要发送的内容,一个是要发去的地址+端口 注意是sendto
例子:用UDP协议写一个简易的QQ聊天
# 仅是本机客户端与本机服务端之间的交互
# 客户端 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_address = ('127.0.0.1', 9990) while True: msg = input('>>>') msg = '''这是来自客户端的消息: %s''' % msg client.sendto(msg.encode('utf-8'), server_address) data, server_addr = client.recvfrom(1024) print(data.decode('utf-8'))
# 服务端 import socket server = socket.socket(type=socket.SOCK_DGRAM) server.bind(('127.0.0.1', 9990)) while True: data, addr = server.recvfrom(1024) print(data.decode('utf-8')) msg = input('>>>') msg = '''这是来自服务端的消息: %s''' % msg server.sendto(msg.encode('utf-8'), addr)
socketserver的模块用法(了解):
TCP & UDP 协议使用socketserver模块
# 客户端用不着此模块,所以还是按照之前的正常编码 import socket import time # # TCP协议写法: # client = socket.socket() # 括号内默认为TCP协议,不用指定 # client.connect(('127.0.0.1', 8888)) # # while True: # client.send(b'hello') # data = client.recv(1024) # print(data.decode('utf-8')) # UDP协议写法: client = socket.socket(type=socket.SOCK_DGRAM) # UDP协议里面括号内需要指定协议 server_address = ('127.0.0.1', 8888) # UDP协议不是基于通道通信,所以不用建立连接关系,直接定义IP地址以及port while True: client.sendto(b'hello', server_address) # UDP协议里面sendto相当于TCP协议里面的send data, addr = client.recvfrom(1024) # UDP协议里面 recvfrom 相当于TCP协议里面的 recv 并且接收的是两个值:一个为数据,一个为服务端地址 print(data.decode('utf-8')) print(addr) time.sleep(1) # 因为太快,所以让CPU暂停一秒
# 服务端 import socketserver # TCP协议服务端写法: # class MyServer(socketserver.BaseRequestHandler): # 继承 # def handle(self): # 只能为handle # while True: # data = self.request.recv(1024) # 接收一个值 用request.recv接收 # print(self.client_address) # 客户端地址 内置有属性 # print(data.decode('utf-8')) # self.request.send(data.upper()) # 传一个值 # # # if __name__ == '__main__': # """只要有客户端连接 会自动交给自定义类中的handle方法去处理""" # server = socketserver.ThreadingTCPServer(('127.0.0.1', 8888), MyServer) # server.serve_forever() # 启动该服务对象 # UDP协议服务端写法: class MyServer(socketserver.BaseRequestHandler): def handle(self): while True: data, sock = self.request # 用request,自带接收效果,同时接收两个值;一个为消息数据,一个类似于conn print(data.decode('utf-8')) # 打印消息 print(self.client_address) # 内置有客户端的地址属性 sock.sendto(data.upper(), self.client_address) # 传两个值 if __name__ == '__main__': """只要有客户端连接 会自动交给自定义类中的handle方法去处理""" server = socketserver.ThreadingUDPServer(('127.0.0.1', 8888), MyServer) server.serve_forever() # 启动该服务对象