Python全栈之路系列----之-----网络编程(粘包与命令执行/数据传输)

粘包

什么是粘包

  1. 只有tcp有粘包现象,udp永远不会粘包
  2. 所谓粘包问题,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
  3. tcp协议会根据自身特性,将间隔时间短和数据量小的数据,合并成一个数据块,然后进行封包和算法优化后进行流式发送到客户端
  4. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住

粘包产生的原因

  1. 发送端(服务端):发送数据时间间隔很短,数据量小,会合到一起,产生粘包,要等缓冲区满才发送出去
  2. 接收端(客户端):服务端发送一段数据,客户端只接收一小部分(限流,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个自己足够用了)
  1. 发送时:
  2. 先发报头长度
  3. 再编码报头内容然后发送最后发真实内容
  4. 接收时
  5. 先收报头长度,用struct取出来
  6. 根据取出的长度收取报头内容,然后解码,反序列化
  7. 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

 

基于通道的双向连接和标准输入,标准错误,来发送数据的描述信息(长度)来实现命令的执行和解决粘包

import socket
import 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()
客户端1.0
import socket
import subprocess
import 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()
服务端1.0

 

基于自定义报头和struct模块来彻底解决粘包问题

import socket
import struct
import 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()
客户端2.0
import socket
import subprocess
import struct
import 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、关机
服务端2.0

 

文件上传

import socket
import 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的模式打开一个要发送的文件d
with 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 struct
import json
import hashlib

download_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()
客户端(md5)

 

from socket import *
import os
import json
import struct
import hashlib

server=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()
服务端(md5)

 

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 os


print(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

 

posted @ 2017-09-26 17:06  太上老君门下一只虾  阅读(152)  评论(0编辑  收藏  举报