TCP解决粘包+传输文件
# 解决粘包
**只有TCP有粘包现象,UDP永远不会粘包**
**粘包原因** 本质是不知道要收多少
1.tcp一次收的过多,需要下次才接收完,造成粘包
2.tcp发到内核态内存是几条内容较少的消息,TCP有Nigon算法,把多个内容较少的包合成一个,操作系统再发出去,所以客户端只会收一次,就全收到
TCP:A端与B端有通信连接,A端send,B端就能收到
UDP:A端一次sendto,B端必须有一次recvfrom与之对应,B端才能真正收到
UDP 如果 一次发的>一次收的,收不了的部分全丢
**解决思路**
1. 第一次发数据量,第二次发数据
2. 选定前n个字节,包含长度,其余为数据
**jason解决办法** 报头为dict,可存取文件的额外信息,方便后续处理
服务端
```python
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1',8081))
server.listen(5)
while True:
conn, addr = server.accept()
print('连接成功')
while True:
try:
cmd = conn.recv(1024)
print('接收成功')
# tcp客户端发空,会导致服务端夯住(udp不存在此问题,因自带报头,为地址和端口信息)
if len(cmd) == 0:break
cmd = cmd.decode('utf-8')
#利用subprocess开启新进程,可接收命令,并调用shell去执行这个字符串
obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
#res接收subprocess的shell标准化输出和标准化报误信息
res = obj.stdout.read() + obj.stderr.read()
#将模块返回的信息的长度,和其他需要提供的信息,做出字典
d = {'file_size':len(res),'info':'xxxxxxx'}
#字典序列化
json_d = json.dumps(d)
# 1.先制作一个提示客户端,将要发送的字典长度的报头
# (struct模块,将int类型的长度,转化为二进制字符串,只占4字节)
header = struct.pack('i',len(json_d))
# header2=struct.pack('i',len(json_d.encode('utf8'))) 与上句效果相同,算的都是bytes长度
# 2.发送字典报头
conn.send(header)
# 3.发送字典
conn.send(json_d.encode('utf-8'))
# 4.再发真实数据
conn.send(res)
# conn.send(obj.stdout.read())
# conn.send(obj.stderr.read())
except ConnectionResetError:
break
conn.close()
```
客户端
```python
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1',8081))
print('连接成功')
while True:
msg = input('>>>:').encode('utf-8')
# tcp客户端发空,会导致服务端夯住(udp不存在此问题,因为自带报头,为地址和端口的信息)
if len(msg) == 0:continue
client.send(msg)
# 1.先接收字典报头
header_dict = client.recv(4)
# 2.解析报头 获取字典的长度
dict_size = struct.unpack('i',header_dict)[0] # 解包的时候一定要加上索引0
# 3.接收字典数据
dict_bytes = client.recv(dict_size)
dict_json = json.loads(dict_bytes.decode('utf-8'))
# 4.从字典中获取信息
recv_size = 0
real_data = b''
# 必须是 < ,最后一次若本来可以刚好发完 即 recv_size = dict_json,大不了再接收一次
# 若改为 = ,最后一次本来收完,却还满足判断条件,收到的就是空了,会夯住
while recv_size < dict_json.get('file_size'):
data = client.recv(1024)
real_data += data
recv_size += len(data)
print(real_data.decode('gbk'))
```
**egon解决办法**
https://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
https://www.cnblogs.com/gala/archive/2011/09/22/2184801.html
服务端
```python
from socket import *
import subprocess
import struct
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
conn,addr=tcp_server.accept()
print('新的客户端链接',addr)
while True:
#收
try:
cmd=conn.recv(buffer_size)
if not cmd:break
print('收到客户端的命令',cmd)
#执行命令,得到命令的运行结果cmd_res
res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
err=res.stderr.read()
if err:
cmd_res=err
else:
cmd_res=res.stdout.read()
#发
if not cmd_res:
cmd_res='执行成功'.encode('gbk')
length=len(cmd_res)
# 按照给定的格式(fmt),把数据封装成字符串
# 即直接把整型变成字符串
data_length=struct.pack('i',length)
# 这里粘包无所谓,客户端知道代表长度是4个字节
conn.send(data_length)
conn.send(cmd_res)
except Exception as e:
print(e)
break
```
客户端
```python
from socket import *
import struct
from functools import partial
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
cmd=input('>>: ').strip()
if not cmd:continue
if cmd == 'quit':break
tcp_client.send(cmd.encode('utf-8'))
#解决粘包
# int类型的数据,二进制标准长度就是4,所以先收4个
length_data=tcp_client.recv(4)
# 取[0]第一个值就是,解析就是“int”型
length=struct.unpack('i',length_data)[0]
recv_msg=''.join(iter(partial(tcp_client.recv, buffer_size), b''))
# 这一段出了问题,可学习研究下 应该是缺一个 减 的函数
#解析: 1.tcp_client.recv()函数接收信息,每次最多buffer_size个bytes,
# 利用偏函数把tcp_client.recv()函数的第一个参数buffer_size固定住
# 2.利用iter(),把偏函数变成可迭代对象
# 3.每次从内核态内存获取一段数据,并拼接到'',直到tcp_client.recv内容为b''
print('命令的执行结果是 ',recv_msg.decode('gbk'))
tcp_client.close()
```
补充:
```python
#iter(object, sentinel)
#只写一个参数:把第一个变成迭代器
#加第二个参数:sentinel,迭代到sentinel内容立即停下
#偏函数:固定函数的第一个参数
from functools import partial
def add(x,y):
return x+y
#指定add函数,把1传给add第一个参数
func=partial(add,1)
print(func(1)) #2
print(func(1)) #3
```
pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
unpack(fmt, string) 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
socket里的sendall,基于send(),就是一个死循环在send,直到所有数据发送。
而send一次最多8k(8096bytes)较好,MTU网卡最大传输单元1500bytes,发送过大,数据就要分片重组,丢一个就失真了
**subprocess**
https://www.cnblogs.com/linhaifeng/articles/6129246.html#_label9
**避免通信循环中的报错**
1.服务端在conn.recv()下一句
用if 为空 :break 解决conn断开,服务端一直收空消息造成的死循环
2.服务端在通信循环 用处理客户端进程非正常中断造成的报错(远程主机强迫关闭了一个现有的连接)
try:
#通信循环
except Exception as e:
print(e)
break
传输文件-客户端上传至服务端
```python
import socket
import os
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,addr = server.accept()
while True:
try:
header_len = conn.recv(4)
# 解析字典报头
header_len = struct.unpack('i',header_len)[0]
# 再接收字典数据
header_dic = conn.recv(header_len)
real_dic = json.loads(header_dic.decode('utf-8'))
# 获取数据长度
total_size = real_dic.get('file_size')
# 循环接收并写入文件
recv_size = 0
with open(real_dic.get('file_name'),'wb') as f:
while recv_size < total_size:
data = conn.recv(1024)
f.write(data)
recv_size += len(data)
print('上传成功')
except ConnectionResetError as e:
print(e)
break
conn.close()
```
客户端
```python
import socket
import json
import os
import struct
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
# 获取电影列表 循环展示
MOVIE_DIR = r'D:\python脱产10期视频\day25\视频'
movie_list = os.listdir(MOVIE_DIR)
# print(movie_list)
for i,movie in enumerate(movie_list,1):
print(i,movie)
# 用户选择
choice = input('please choice movie to upload>>>:')
# 判断是否是数字
if choice.isdigit():
# 将字符串数字转为int
choice = int(choice) - 1
# 判断用户选择在不在列表范围内
if choice in range(0,len(movie_list)):
# 获取到用户想上传的文件路径
path = movie_list[choice]
# 拼接文件的绝对路径
file_path = os.path.join(MOVIE_DIR,path)
# 获取文件大小
file_size = os.path.getsize(file_path)
# 定义一个字典
res_d = {
'file_name':'性感荷官在线发牌.mp4',
'file_size':file_size,
'msg':'注意身体,多喝营养快线'
}
# 序列化字典
json_d = json.dumps(res_d)
json_bytes = json_d.encode('utf-8')
# 1.先制作字典格式的报头
header = struct.pack('i',len(json_bytes))
# 2.发送字典的报头
client.send(header)
# 3.再发字典
client.send(json_bytes)
# 4.再发文件数据(打开文件循环发送)
with open(file_path,'rb') as f:
for line in f:
client.send(line)
else:
print('not in range')
else:
print('must be a number')
```