粘包
什么是粘包
- 只有tcp有粘包现象,udp永远不会粘包
- 所谓粘包问题,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
- tcp协议会根据自身特性,将间隔时间短和数据量小的数据,合并成一个数据块,然后进行封包和算法优化后进行流式发送到客户端
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住
粘包产生的原因
- 发送端(服务端):发送数据时间间隔很短,数据量小,会合到一起,产生粘包,要等缓冲区满才发送出去
- 接收端(客户端):服务端发送一段数据,客户端只接收一小部分(限流,1024),客户端下次在接收的时候还是从缓存区拿上次遗留的数据,产生粘包
拆包的发生情况
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
补充问题一:为何tcp是可靠传输,udp是不可靠传输
基于tcp的数据传输请参考我的另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的
而udp发送数据,对端是不会返回确认信息的,因此不可靠
补充问题二:send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
解决粘包问题*
-
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
- struct模块: 该模块可以把一个类型,如数字,转成固定长度的bytes >>>>struct.pack('i',1111111111111)
- 问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
- 我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
- 发送时:
- 先发报头长度
- 再编码报头内容然后发送最后发真实内容
- 接收时
- 先收报头长度,用struct取出来
- 根据取出的长度收取报头内容,然后解码,反序列化
- 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
基于通道的双向连接和标准输入,标准错误,来发送数据的描述信息(长度)来实现命令的执行和解决粘包
import socketimport struct#1、买手机phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#2、打电话phone.connect(('127.0.0.1',8091))#3、发收消息while True: cmd=input('>>: ').strip() if cmd == 'quit':break if not cmd:continue phone.send(cmd.encode('utf-8')) #先收报头 header=phone.recv(4) total_size=struct.unpack('i',header)[0] #循环收:10241 total_data=b'' recv_size=0 while recv_size < total_size: recv_data=phone.recv(1024) total_data+=recv_data recv_size+=len(recv_data) print(total_data.decode('gbk'))#4、挂电话phone.close()
import socketimport subprocessimport struct#1、买手机phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#2、绑定手机卡# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8091))#3、开机phone.listen(5)#4、等电话连接print('starting...')while True: #连接循环 conn,addr=phone.accept() print('IP:%s,PORT:%s' %(addr[0],addr[1])) #5、收发消息 while True: #通信循环 try: cmd=conn.recv(1024) #最大收1024 if not cmd:break #针对linux #执行命令 obj=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #发送数据的描述信息:长度 header=struct.pack('i',len(stdout)+len(stderr)) conn.send(header) #发送真实数据 conn.send(stdout) conn.send(stderr) except Exception: break #6、挂电话 conn.close()#7、关机phone.close()
基于自定义报头和struct模块来彻底解决粘包问题
import socketimport structimport json#1、买手机phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#2、打电话phone.connect(('127.0.0.1',8092))#3、发收消息while True: cmd=input('>>: ').strip() if cmd == 'quit':break if not cmd:continue phone.send(cmd.encode('utf-8')) #先收报头长度 obj=phone.recv(4) header_size=struct.unpack('i',obj)[0] #再收报头 header_bytes=phone.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) #最后循环收真实的数据 total_size=header_dic['total_size'] filename=header_dic['filename'] total_data=b'' recv_size=0 with open(filename,'wb') as f: while recv_size < total_size: recv_data=phone.recv(1024) total_data+=recv_data recv_size+=len(recv_data) print(total_data.decode('gbk'))#4、挂电话phone.close()
import socketimport subprocessimport structimport json#1、买手机phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#2、绑定手机卡# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8092))#3、开机phone.listen(5)#4、等电话连接print('starting...')while True: #连接循环 conn,addr=phone.accept() print('IP:%s,PORT:%s' %(addr[0],addr[1])) #5、收发消息 while True: #通信循环 try: cmd=conn.recv(1024) #最大收1024 if not cmd:break #针对linux #执行命令 obj=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #制作报头 header_dic = { 'filename': 'a.txt', 'total_size': len(stdout)+len(stderr), 'md5': 'asdfa123xvc123'} header_json = json.dumps(header_dic) header_bytes = header_json.encode('utf-8') #先发报头的长度 conn.send(struct.pack('i',len(header_bytes))) #再发送报头 conn.send(header_bytes) #最后发送真实数据 conn.send(stdout) conn.send(stderr) except Exception: break #6、挂电话 conn.close()#7、关机
文件上传
import socketimport os# 创建一个socket对象obj = socket.socket()# 服务端的IP和端口obj.connect(('127.0.0.1', 6542))# 用os模块获取要传送的文件总大小size = os.stat("old_file.txt").st_size# 把文件总大小发送给服务端obj.sendall(bytes(str(size), encoding="utf-8"))# 接受服务端返回的信息obj.recv(1024)# 以rb的模式打开一个要发送的文件dwith open("old_file.txt", "rb") as f: # 循环文件的所有内容 for line in f: # 发送给服务端 obj.sendall(line)# 关闭退出
import socket# 创建一个socket对象sk = socket.socket()# 允许连接的IP和端口sk.bind(('127.0.0.1', 6542))# 最大连接数sk.listen(5)while True: # 会一直阻塞,等待接收客户端的请求,如果有客户端连接会获取两个值,conn=创建的连接,address=客户端的IP和端口 conn, address = sk.accept() # 客户端发送过来的文件大小 file_size = str(conn.recv(1024),encoding="utf-8") # 给客户端发送已经收到文件大小 conn.sendall(bytes("ack", encoding="utf-8")) # 文件大小转换成int类型 total_size = int(file_size) # 创建一个默认的值 has_recv = 0 # 打开一个新文件,以wb模式打开 f = open('new_file.txt', 'wb') # 进入循环 while True: # 如果传送过来的大小等于文件总大小,那么就退出 if total_size == has_recv: break # 接受客户端发送过来的内容 data = conn.recv(1024) # 写入到文件当中 f.write(data) # 现在的大小加上客户端发送过来的大小 has_recv += len(data) # 关闭 f.close()
文件下载
from socket import *import structimport jsonimport hashlibdownload_dir=r'D:\\'client=socket(AF_INET,SOCK_STREAM)client.connect(('127.0.0.1',8000))while True: cmd=input('>>: ').strip() #get a.txt if not cmd:continue client.send(cmd.encode('utf-8')) #先收报头长度 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) filename=header_dic['filename'] abs_path=r'%s\%s' %(download_dir,filename) size=header_dic['size'] print(header_dic) #再收真实数据 recv_size=0 with open(abs_path,'wb') as f: m=hashlib.md5() while recv_size < size: line=client.recv(1024) f.write(line) m.update(line) recv_size+=len(line) client_md5=m.hexdigest() #最后收md5值 server_md5=client.recv(1024).decode('utf-8') if client_md5 != server_md5: os.remove(abs_path) print('文件已损坏,请重写下载')client.close()
from socket import *import osimport jsonimport structimport hashlibserver=socket(AF_INET,SOCK_STREAM)server.bind(('127.0.0.1',8000))server.listen(5)def get(filepath,conn): #制作报头 header_dic={ 'filename':os.path.basename(filepath),# C:\\\\1.png 'size':os.path.getsize(filepath), } header_json=json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #先发送报头的长度 conn.send(struct.pack('i',len(header_bytes))) #再发送报头 conn.send(header_bytes) #再发送真实的数据 with open(filepath,'rb') as f: m=hashlib.md5() for line in f: conn.send(line) m.update(line) #最后发送md5值 md5=m.hexdigest() print(md5) conn.send(md5.encode('utf-8'))while True: conn,addr=server.accept() while True: try: data=conn.recv(1024) cmd,filepath=data.decode('utf-8').split() #get C:\\\\1.png if cmd == 'get': get(filepath,conn) if not data:break except Exception as e: print(e) break conn.close()server.close()
UDP套接字
from socket import *client=socket(AF_INET,SOCK_DGRAM)while True: client.sendto(b'hello',('127.0.0.1',8082)) data,server_addr=client.recvfrom(1024) print(data.decode('utf-8'))
from socket import *import osprint(os.getpid())server=socket(AF_INET,SOCK_DGRAM)server.bind(('127.0.0.1',8080))while True: data,client_addr=server.recvfrom(1024) server.sendto(data.upper(),client_addr)server.close()
多道基础预习
http://www.cnblogs.com/zgd1234/p/7155523.html