网络编程----粘包以及粘包问题的解决、FTP上传
一、粘包现象
让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)
注意注意:
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码
且只能从管道里读一次结果
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 from socket import * 4 import subprocess 5 6 ip_port=('127.0.0.1',8080) 7 BUFSIZE=1024 8 9 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 10 tcp_socket_server.bind(ip_port) 11 tcp_socket_server.listen(5) 12 13 while True: 14 conn,addr=tcp_socket_server.accept() 15 print('客户端',addr) 16 17 while True: 18 cmd=conn.recv(BUFSIZE) 19 if len(cmd) == 0:break 20 21 act_res=subprocess.Popen(cmd.decode('utf-8'),shell=True, 22 stdout=subprocess.PIPE, 23 stdin=subprocess.PIPE, 24 stderr=subprocess.PIPE) 25 26 act_err=act_res.stderr.read() 27 if act_err: 28 ret=act_err 29 else: 30 ret=act_res.stdout.read() 31 32 conn.sendall(ret) 33 34 服务端
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 import socket 4 BUFSIZE=1024 5 ip_port=('127.0.0.1',8080) 6 7 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 res=s.connect_ex(ip_port) 9 10 while True: 11 msg=input('>>: ').strip() 12 if len(msg) == 0:continue 13 if msg == 'quit':break 14 15 s.send(msg.encode('utf-8')) 16 act_res=s.recv(BUFSIZE) 17 18 print(act_res.decode('utf-8'),end='') 19 20 客户端
上述程序是基于tcp的socket,在运行时会发生粘包
让我们再基于udp制作一个远程执行命令的程序
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 from socket import * 4 import subprocess 5 6 ip_port=('127.0.0.1',9003) 7 bufsize=1024 8 9 udp_server=socket(AF_INET,SOCK_DGRAM) 10 udp_server.bind(ip_port) 11 12 while True: 13 #收消息 14 cmd,addr=udp_server.recvfrom(bufsize) 15 print('用户命令----->',cmd) 16 17 #逻辑处理 18 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) 19 err=res.stderr.read() 20 print('错误====>',err) 21 if err: 22 back_msg=err 23 else: 24 back_msg=res.stdout.read() 25 print('返回结果',back_msg) 26 27 #发消息 28 udp_server.sendto(back_msg,addr) 29 udp_server.close() 30 31 服务端
1 from socket import * 2 ip_port=('127.0.0.1',9003) 3 bufsize=1024 4 5 udp_client=socket(AF_INET,SOCK_DGRAM) 6 7 8 while True: 9 msg=input('>>: ').strip() 10 udp_client.sendto(msg.encode('utf-8'),ip_port) 11 12 data,addr=udp_client.recvfrom(bufsize) 13 print(data.decode('utf-8'),end='') 14 15 客户端
上述程序是基于udp的socket,在运行时永远不会发生粘包
二、什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来
首先需要掌握一个socket收发消息的原理
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对一个一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
两种情况下会发生粘包
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 from socket import * 4 ip_port=('127.0.0.1',8080) 5 6 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 7 tcp_socket_server.bind(ip_port) 8 tcp_socket_server.listen(5) 9 10 11 conn,addr=tcp_socket_server.accept() 12 13 14 data1=conn.recv(10) 15 data2=conn.recv(10) 16 17 print('----->',data1.decode('utf-8')) 18 print('----->',data2.decode('utf-8')) 19 20 conn.close() 21 22 服务端
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 import socket 4 BUFSIZE=1024 5 ip_port=('127.0.0.1',8080) 6 7 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 res=s.connect_ex(ip_port) 9 10 11 s.send('hello'.encode('utf-8')) 12 s.send('feng'.encode('utf-8')) 13 14 客户端
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 from socket import * 4 ip_port=('127.0.0.1',8080) 5 6 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 7 tcp_socket_server.bind(ip_port) 8 tcp_socket_server.listen(5) 9 10 11 conn,addr=tcp_socket_server.accept() 12 13 14 data1=conn.recv(2) #一次没有收完整 15 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 16 17 print('----->',data1.decode('utf-8')) 18 print('----->',data2.decode('utf-8')) 19 20 conn.close() 21 22 服务端
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 import socket 4 BUFSIZE=1024 5 ip_port=('127.0.0.1',8080) 6 7 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 res=s.connect_ex(ip_port) 9 10 11 s.send('hello feng'.encode('utf-8')) 12 13 客户端
拆包的发生情况
当发送端缓冲区的长度大于网卡的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)
。。。。。。。。。
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
import json,struct #假设通过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
1 import socket,struct,json 2 import subprocess 3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 5 6 phone.bind(('127.0.0.1',8080)) 7 8 phone.listen(5) 9 10 while True: 11 conn,addr=phone.accept() 12 while True: 13 cmd=conn.recv(1024) 14 if not cmd:break 15 print('cmd: %s' %cmd) 16 17 res=subprocess.Popen(cmd.decode('utf-8'), 18 shell=True, 19 stdout=subprocess.PIPE, 20 stderr=subprocess.PIPE) 21 err=res.stderr.read() 22 print(err) 23 if err: 24 back_msg=err 25 else: 26 back_msg=res.stdout.read() 27 28 headers={'data_size':len(back_msg)} 29 head_json=json.dumps(headers) 30 head_json_bytes=bytes(head_json,encoding='utf-8') 31 32 conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度 33 conn.send(head_json_bytes) #再发报头 34 conn.sendall(back_msg) #在发真实的内容 35 36 conn.close() 37 38 服务端:定制稍微复杂一点的报头
1 from socket import * 2 import struct,json 3 4 ip_port=('127.0.0.1',8080) 5 client=socket(AF_INET,SOCK_STREAM) 6 client.connect(ip_port) 7 8 while True: 9 cmd=input('>>: ') 10 if not cmd:continue 11 client.send(bytes(cmd,encoding='utf-8')) 12 13 head=client.recv(4) 14 head_json_len=struct.unpack('i',head)[0] 15 head_json=json.loads(client.recv(head_json_len).decode('utf-8')) 16 data_len=head_json['data_size'] 17 18 recv_size=0 19 recv_data=b'' 20 while recv_size < data_len: 21 recv_data+=client.recv(1024) 22 recv_size+=len(recv_data) 23 24 print(recv_data.decode('utf-8')) 25 #print(recv_data.decode('gbk')) #windows默认gbk编码 26 27 客户端
笔记实例1:
1 #coding:utf-8 2 #买手机 3 import socket 4 import struct 5 import subprocess 6 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 7 #绑定电话卡 8 ip_port=('192.168.16.114',8081) 9 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 10 phone.bind(ip_port) 11 #开机 12 phone.listen(5) 13 #等待电话 14 15 #链接循环 16 while True: 17 conn,addr=phone.accept() 18 print('client addr',addr) 19 #通讯循环 20 while True: 21 try: 22 cmd=conn.recv(1024) 23 res=subprocess.Popen(cmd.decode('utf-8'), 24 shell=True, 25 stdout=subprocess.PIPE, 26 stderr=subprocess.PIPE) 27 out_res=res.stdout.read() 28 err_res=res.stderr.read() 29 data_size=len(out_res)+len(err_res) 30 #发送报头 31 conn.send(struct.pack('i',data_size)) 32 #发送数据部分 33 conn.send(out_res) 34 conn.send(err_res) 35 36 except Exception: 37 break 38 39 conn.close() 40 phone.close()
1 #买手机 2 import socket 3 import struct 4 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 5 #拨通电话 6 # ip_port = ('127.0.0.1', 8080) 7 ip_port = ('192.168.16.114', 8081) 8 phone.connect(ip_port) 9 # 通信循环 10 while True: 11 # 发消息 12 cmd = input('>>: ').strip() 13 if not cmd: continue 14 phone.send(bytes(cmd, encoding='utf-8')) 15 16 #收报头 17 baotou=phone.recv(4) 18 data_size=struct.unpack('i',baotou)[0] 19 20 # 收数据 21 recv_size=0 22 recv_data=b'' 23 while recv_size < data_size: 24 data=phone.recv(1024) 25 recv_size+=len(data) 26 recv_data+=data 27 28 print(recv_data.decode('utf-8')) 29 phone.close()
笔记实例2:
1 #coding:utf-8 2 #买手机 3 import socket 4 import struct 5 import json 6 import subprocess 7 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 #绑定电话卡 9 ip_port=('192.168.16.114',8081) 10 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 11 phone.bind(ip_port) 12 #开机 13 phone.listen(5) 14 #等待电话 15 16 #链接循环 17 while True: 18 conn,addr=phone.accept() 19 print('client addr',addr) 20 #通讯循环 21 while True: 22 try: 23 cmd=conn.recv(1024) 24 res=subprocess.Popen(cmd.decode('utf-8'), 25 shell=True, 26 stdout=subprocess.PIPE, 27 stderr=subprocess.PIPE) 28 out_res=res.stdout.read() 29 err_res=res.stderr.read() 30 data_size=len(out_res)+len(err_res) 31 head_dic={'data_size':data_size} 32 head_json=json.dumps(head_dic) 33 head_bytes=head_json.encode('utf-8') 34 35 #part1:先发报头的长度 36 head_len=len(head_bytes) 37 conn.send(struct.pack('i',head_len)) 38 #part2:再发送报头 39 conn.send(head_bytes) 40 #part3:最后发送数据部分 41 conn.send(out_res) 42 conn.send(err_res) 43 44 except Exception: 45 break 46 47 conn.close() 48 phone.close()
1 # 买手机 2 import socket 3 import struct 4 import json 5 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 # 拨通电话 7 # ip_port = ('127.0.0.1', 8080) 8 ip_port = ('192.168.16.114', 8081) 9 phone.connect(ip_port) 10 # 通信循环 11 while True: 12 # 发消息 13 cmd = input('>>: ').strip() 14 if not cmd: continue 15 phone.send(bytes(cmd, encoding='utf-8')) 16 17 # part1:先收报头的长度 18 head_struct=phone.recv(4) 19 head_len=struct.unpack('i',head_struct)[0] 20 21 # part2:再收报头 22 head_bytes=phone.recv(head_len) 23 head_json=head_bytes.decode('utf-8') 24 25 head_dic=json.loads(head_json) 26 print(head_dic) 27 data_size = head_dic['data_size'] 28 29 #part3:最后收数据 30 recv_size = 0 31 recv_data = b'' 32 while recv_size < data_size: 33 data = phone.recv(1024) 34 recv_size += len(data) 35 recv_data += data 36 37 print(recv_data.decode('utf-8')) 38 phone.close()
四、FTP上传
1 #######################服务端######################## 2 import socket 3 import struct 4 import json 5 import subprocess 6 import os 7 8 class MYTCPServer: 9 address_family = socket.AF_INET 10 11 socket_type = socket.SOCK_STREAM 12 13 allow_reuse_address = False 14 15 max_packet_size = 8192 16 17 coding='utf-8' 18 19 request_queue_size = 5 20 21 server_dir='你要接收的路径(自己修改)' #注意windows里面路径\\是两个斜杠 22 23 def __init__(self, server_address, bind_and_activate=True): 24 """Constructor. May be extended, do not override.""" 25 self.server_address=server_address 26 self.socket = socket.socket(self.address_family, 27 self.socket_type) 28 if bind_and_activate: 29 try: 30 self.server_bind() 31 self.server_activate() 32 except: 33 self.server_close() 34 raise 35 36 def server_bind(self): 37 """Called by constructor to bind the socket. 38 """ 39 if self.allow_reuse_address: 40 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 41 self.socket.bind(self.server_address) 42 self.server_address = self.socket.getsockname() 43 44 def server_activate(self): 45 """Called by constructor to activate the server. 46 """ 47 self.socket.listen(self.request_queue_size) 48 49 def server_close(self): 50 """Called to clean-up the server. 51 """ 52 self.socket.close() 53 54 def get_request(self): 55 """Get the request and client address from the socket. 56 """ 57 return self.socket.accept() 58 59 def close_request(self, request): 60 """Called to clean up an individual request.""" 61 request.close() 62 63 def run(self): 64 while True: 65 self.conn,self.client_addr=self.get_request() 66 print('from client ',self.client_addr) 67 while True: 68 try: 69 head_struct = self.conn.recv(4) 70 if not head_struct:break 71 72 head_len = struct.unpack('i', head_struct)[0] 73 head_json = self.conn.recv(head_len).decode(self.coding) 74 head_dic = json.loads(head_json) 75 76 print(head_dic) 77 #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} 78 cmd=head_dic['cmd'] 79 if hasattr(self,cmd): 80 func=getattr(self,cmd) 81 func(head_dic) 82 except Exception: 83 break 84 85 def put(self,args): 86 file_path=os.path.normpath(os.path.join( 87 self.server_dir, 88 args['filename'] 89 )) 90 91 filesize=args['filesize'] 92 recv_size=0 93 print('----->',file_path) 94 with open(file_path,'wb') as f: 95 while recv_size < filesize: 96 recv_data=self.conn.recv(self.max_packet_size) 97 f.write(recv_data) 98 recv_size+=len(recv_data) 99 print('recvsize:%s filesize:%s' %(recv_size,filesize)) 100 101 tcpserver1=MYTCPServer(('127.0.0.1',8080)) 102 103 tcpserver1.run()
1 ###########################客户端################################ 2 import socket 3 import struct 4 import json 5 import os 6 7 8 class MYTCPClient: 9 address_family = socket.AF_INET 10 11 socket_type = socket.SOCK_STREAM 12 13 allow_reuse_address = False 14 15 max_packet_size = 8192 16 17 coding='utf-8' 18 19 request_queue_size = 5 20 21 def __init__(self, server_address, connect=True): 22 self.server_address=server_address 23 self.socket = socket.socket(self.address_family, 24 self.socket_type) 25 if connect: 26 try: 27 self.client_connect() 28 except: 29 self.client_close() 30 raise 31 32 def client_connect(self): 33 self.socket.connect(self.server_address) 34 35 def client_close(self): 36 self.socket.close() 37 38 def run(self): 39 while True: 40 inp=input(">>: ").strip() 41 if not inp:continue 42 l=inp.split() 43 cmd=l[0] 44 if hasattr(self,cmd): 45 func=getattr(self,cmd) 46 func(l) 47 48 def put(self,args): 49 cmd=args[0] 50 filename=args[1] 51 if not os.path.isfile(filename): 52 print('file:%s is not exists' %filename) 53 return 54 else: 55 filesize=os.path.getsize(filename) 56 57 head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} 58 print(head_dic) 59 head_json=json.dumps(head_dic) 60 head_json_bytes=bytes(head_json,encoding=self.coding) 61 62 head_struct=struct.pack('i',len(head_json_bytes)) 63 self.socket.send(head_struct) 64 self.socket.send(head_json_bytes) 65 send_size=0 66 with open(filename,'rb') as f: 67 for line in f: 68 self.socket.send(line) 69 send_size+=len(line) 70 print(send_size) 71 else: 72 print('upload successful') 73 74 75 client=MYTCPClient(('127.0.0.1',8080)) 76 77 client.run()