黏包现象 并发编程

黏包现象

黏包现象产生与tcp协议的通信中
1.服务端连续执行三次recv
2.客户端连续执行三次send
问题:服务端一次性接收到了客户端三次的消息 该现象称为"黏包现象"

image

黏包现象产生的原因:
	1.不知道每次的数据到底多大
	2.TCP也称为流式协议:数据像水流一样绵绵不绝没有间隔(TCP会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送)

所以问题就在于如何得知一次send 到底传输的数据量的大小?
我们无法得知即将接受的数据量有多大。所以需要让客户端统计数据包大小,先提前告知服务端即将传来的数据大小。

避免黏包现象的核心思路\关键点:
		如何明确即将接收的数据具体有多大
		如何将长度变化的数据全部制作成固定长度的数据

struct模块

import struct

info = b'hello big baby'
print(len(info))  # 数据真实的长度(bytes)  14
res = struct.pack('i', len(info))  # 将数据打包成固定的长度 i是固定的打包模式
print(len(res))  # 打包之后长度为(bytes)   4           报头

real_len = struct.unpack('i', res)
print(real_len)  # (14,)             根据固定长度的报头 解析出真实数据的长度


desc = b'hello my baby I will take you to play big ball'
print(len(desc))  # 数据真实的长度(bytes)  46
res1 = struct.pack('i', len(desc))
print(len(res1))  # 打包之后长度为(bytes)  4           报头

real_len1 = struct.unpack('i', res1)
print(real_len1)  # (46,)              根据固定长度的报头 解析出真实数据的长度

struct.pack() struct.unpack()

报头相当于先锋兵,他说:我后面有十万大军!
报头信息可以告知服务端,即将到来的数据包的大小。且报头信息的长度是固定的。

# 1.必须传入整数类型
import struct
info = b'hello big baby'  # 将要传输的信息
res = struct.pack('i', info)  # 打包信息产生报头  # 这里会失败  # struct.error: required argument is not an integer
'''struct.pack(打包的模式,将要传输的信息的字节数)'''

更多模式:
image

# 2.打包后的报头为bytes类型 且 字节数固定为4
import struct

info = b'hello big baby'
res = struct.pack('i', len(info))
print(res, type(res), len(res))  # b'\x0e\x00\x00\x00' <class 'bytes'> 4
# 3.unpack方法的使用
import struct

info = b'hello big baby'
res = struct.pack('i', len(info))
unpack = struct.unpack('i', res)
print(unpack)  # (14,)  # 结果是一个元祖

文件过大无法打包

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。但还是有问题。

'''问题:struct模块无法打包数据量较大的数据 就算换更大的模式也不行'''
import struct
res = struct.pack('i', 12312222222233)  # struct.error: argument out of range
'''问题2:报头能否传递更多的信息  比如电影大小 电影名称 电影评价 电影简介'''

黏包问题解决

先发送一个信息字典,信息字典里包含了之后要发送的数据大小的信息。

'''终极解决方案:字典作为报头打包 效果更好 数字更小'''
import struct

data_dict = {
    'file_name': 'xxx老师教学.avi',
    'file_size': 123132131232342342423423423423432423432,
    'file_info': '内容很精彩 千万不要错过',
    'file_desc': '一代神作 私人珍藏'
}
import json
data_json = json.dumps(data_dict)
print(len(data_json.encode('utf8')))  # 真实字典的长度  252
res = struct.pack('i', len(data_json.encode('utf8')))
print(len(res))  # 4
黏包问题终极方案
    客户端
        1.制作真实数据的信息字典(数据长度、数据简介、数据名称)
        2.利用struct模块制作字典的报头
        3.发送固定长度的报头(解析出来是字典的长度)
        4.发送字典数据
        5.发送真实数据
    服务端
        1.接收固定长度的字典报头
        2.解析出字典的长度并接收
        3.通过字典获取到真实数据的各项信息
        4.接收真实数据长度

黏包实战

'''发送端'''
import socket
import os
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1', 8081))

'''任何文件都是下列思路 图片 视频 文本 ...'''
# 1.获取真实数据大小
file_size = os.path.getsize(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt')
# 2.制作真实数据的字典数据
data_dict = {
    'file_name': '有你好看.txt',
    'file_size': file_size,
    'file_desc': '内容很长 准备好吃喝 我觉得营养快线挺好喝',
    'file_info': '这是我的私人珍藏'
}
# 3.制作字典报头
data_dict_bytes = json.dumps(data_dict).encode('utf8')
data_dict_len = struct.pack('i', len(data_dict_bytes))
# 4.发送字典报头
client.send(data_dict_len)  # 报头本身也是bytes类型 我们在看的时候用len长度是4
# 5.发送字典
client.send(data_dict_bytes)
# 6.最后发送真实数据
with open(r'/Users/jiboyuan/PycharmProjects/day36/xx老师合集.txt', 'rb') as f:
    for line in f:  # 一行行发送 和直接一起发效果一样 因为TCP流式协议的特性
        client.send(line)
import time
time.sleep(10)
'''接受端'''
import socket
import struct
import json


server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)

sock, addr = server.accept()
# 1.接收固定长度的字典报头
data_dict_head = sock.recv(4)
# 2.根据报头解析出字典数据的长度
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 3.接收字典数据
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes)  # 自动解码再反序列化
# 4.获取真实数据的各项信息
total_size = data_dict.get('file_size')
with open(data_dict.get('file_name'), 'wb') as f:
    f.write(sock.recv(total_size))
'''接收真实数据的时候 如果数据量非常大 recv括号内直接填写该数据量 不太合适 我们可以每次接收一点点 反正知道总长度'''
total_size = data_dict.get('file_size')
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
    while recv_size < total_size:
        data = sock.recv(1024)
        f.write(data)
        recv_size += len(data)
        print(recv_size)

UDP协议(了解)

1.UDP服务端和客户端'各自玩各自的'
2.UDP不会出现多个消息发送合并
3.UDP不会黏包 recvfrom(1024) 超过指定的字节数时 超过的部分就丢弃 传几次就接受几次

操作系统发展史

穿孔卡片阶段

计算机很庞大 使用很麻烦 一次只能给一个人使用 期间很多时候计算机都不工作
好处:程序员独占计算机 为所欲为
坏处:计算机利用率太低 浪费资源

联机批处理系统

提前使用磁带一次性录入多个程序员编写的程序 然后交给计算机执行
CPU工作效率有所提升 不用反复等待程序录入

脱机批处理系统

高速磁带的读取速度很快,极大地提升了CPU的利用率
image

并发编程

探讨这些技术时,默认计算机只有一个cpu。

单道技术

单道技术:所有的程序排队执行
image

单道技术补充说明

从硬盘读取数据到内存不用CPU参与的吗?
第一个问题:是的,从硬盘读数据到内存的过程是不需要CPU参与的,CPU发指令通知硬盘控制器去读,接下来等待硬盘控制器读取完成就可以了,这个等待的时间CPU是空闲的,可以让出控制权做其他的事情。
第二个问题:CPU的调度是针对于进程的基于时间片的调度,每一个进程给一定的时间片进行轮转。而我们一般所说的异步IO是线程级别的,线程是在操作系统层面支持的,主要是防止主线程卡死。

作者:邵丁丑
链接:https://www.zhihu.com/question/324715694/answer/1324241868
来源:知乎

多道技术(重要)

多道技术:乘着CPU执行的时候 硬盘继续读取
image
隐喻:(重要)

'''
饭店只有一个服务员 但是同时来了五桌客人
请问:如何让五桌客人都感觉服务员在服务它们?
拿菜单让客人看!客人看的时候 去给别人点菜!只要有空闲 就立即切换到其他桌 如此往复。
'''

多道技术之切换

计算机的CPU在两种情况下会切换
此时被切换的程序会被剥夺cpu的使用权,在原地停着:

# 1.程序有IO操作 输入\输出操作
  比如:input、time.sleep、read、write 
# 2.程序长时间占用cpu
  我们得让多个程序都能被cpu运行起来 雨露均沾

多道技术之保持状态

cpu光会不停切换还是不够的
cpu每次切换走之前都需要保存当前操作的状态 下次切换回来基于上次的进度继续执行
'''比如你做饭时遇到有客人敲门 ---> 做菜 开门 接着做菜'''
CPU很快 能让人产生错觉 是好多服务员在服务客人

进程理论

进程与程序的区别:
程序:一堆死代码(还没有被运行起来)
进程:正在运行的程序(被运行起来了)

进程的调度算法

来了5个进程,cpu先执行谁? 如何分配资源?
image

先来先服务调度算法(FCFS)

image
当耗时长的进程先启动时 cpu会一直运行长作业进程 而忽视端作业进程。
特点:对短作业不友好

短作业优先调度算法(SJ/PF)

image
特点:对长作业不友好

时间片轮转调度算法+多级反馈队列(重要)

时间片轮转

把CPU的时间划分为很多片(时隙)
比如5s的时间 划分为5个时间片 1 1 1 1 1
先给每个进程用1秒!保证程序起来。

如下图,是时间片为1的例子:
A程序先到,用了1秒CPU就跑到队尾排队。在这1秒内B来了,他看A还在用cpu就先等着A用完。以此类推...
image

多级反馈队列

对于某些进程1s时间太短,塞牙缝!!
对于使用过多个1秒时间片,但是未执行完的程序,将其放入一个新的队列,这个队列里可能每次给你分配2s的cpu使用权限。如果还没执行完就再换下一个队列,这个队列每次发5s的时间片。以此类推...(类似python垃圾回收机制)

如下图: 先执行就绪队列1队首的程序,执行时间为S1。执行完之后跑去就绪队列2,执行队首程序,执行时间为S2,再跑去就绪队列3,以此类推...
image
进程所在的队列越往下层,每次分发的时间越长 但是优先级越低。
也就是说:当1s队列有新程序进来会先执行1s队列的程序!
为什么要这样做?因为要让用户觉得 新打开的程序 马上能运行 因此cpu马上给他分一个时间片。

程序阻塞的情况

如果在程序使用cpu时,发生了IO操作,程序阻塞,cpu该怎么做?
此时分给该进程的时间片并未用完,此是cpu会立即剥夺该进程的使用权,并且保持状态方便下一次接着运行。然后因为该进程阻塞,他会从就绪队列退出,当阻塞解除之后再回到就绪队列的队尾。(请先看 进程的三状态)

总结和补充

时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
      显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。
在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。
      在轮转法中,加入到就绪队列的进程有3种情况:
      1. 一种是分给它的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。
      2. 另一种情况是分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。
      3. 第三种情况就是新创建进程进入就绪队列。
      如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。  
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。
而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

多级反馈队列

进程的三状态图

image

# 就绪态
	所有的进程在被cpu执行之前都必须先进入就绪态等待
# 运行态
	cpu正在执行
# 阻塞态
	进程运行过程中出现了IO操作 阻塞态无法直接进入运行态 需要先进入就绪态

例子:
image

补充

在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。

  (1)就绪(Ready)状态

  当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

  (2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

  (3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

进程的并行与并发(重要)

# 并行
多个进程同时执行 必须要有多个CPU参与 
单个CPU无法实现并行
eg:两人比赛赛跑
>>> 真正意义上的同时运行两个程序 一个cpu负责一个程序
# 并发 
多个进程看上去像同时执行 单个CPU可以实现 多个CPU肯定也可以
eg:一个服务员服务多个客人
>>> cpu在两个程序之间快速切换

请判断下列两句话孰对孰错:
	我写的程序很牛逼,运行起来之后可以实现14个亿的并行量 要14个亿的CPU 并行量必须要对等的cpu实现
    我写的程序很牛逼,运行起来之后可以实现14个亿的并发量 一个cpu够吗
    
以后我们的项目会追求高并发。    
一个服务同时服务(切换)很多客人 并发!
   
ps:国内最牛逼的>>>:12306 
能抗的并发量很大 一个服务员来回切换几百个人没问题!

练习

展示某个目录下多个文件内容选择性下载或者上传:

'''发送端'''

import socket
import os
import struct
import json


BASE_DIR = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_DIR,'db')
if not os.path.exists(DB_PATH):
    os.mkdir(DB_PATH)

file_name_list = os.listdir(DB_PATH)
print(file_name_list)

# 1.制作真实数据的字典数据
data_dict = {
    'file_name': [],
    'file_size': [],
}
for file in file_name_list:
    local_path = os.path.join(DB_PATH, file)
    file_size = os.path.getsize(local_path)
    data_dict['file_name'].append(file)
    data_dict['file_size'].append(file_size)

# 2.建立链接
client = socket.socket()
client.connect(('192.168.1.81', 8081))

# 3.制作字典报头
data_dict_bytes = json.dumps(data_dict).encode('utf8')
data_dict_len = struct.pack('i', len(data_dict_bytes))


flag = True
while flag:
    # 4.发送字典报头
    client.send(data_dict_len)  # 报头本身也是bytes类型 我们在看的时候用len长度是4
    # 5.发送字典
    client.send(data_dict_bytes)
    # 6.接受对方的信息
    msg = client.recv(1024).decode()
    # 7.拼接路径
    send_file = os.path.join(DB_PATH, msg)
    # 8.发送真实的数据
    with open(send_file, 'rb') as f:
        for line in f:  # 一行行发送 和直接一起发效果一样 因为TCP流式协议的特性
            client.send(line)
    print('nihap')
    flag = client.recv(10)
    print(len(flag))
    if len(flag) < 2:
        flag = False
'''接受端'''

import socket
import struct
import json

server = socket.socket()
server.bind(('192.168.1.81', 8081))
server.listen(5)
sock, addr = server.accept()

while True:
    # 1.接收固定长度的字典报头
    data_dict_head = sock.recv(4)
    # 2.根据报头解析出字典数据的长度
    data_dict_len = struct.unpack('i', data_dict_head)[0]
    # 3.接收字典数据
    data_dict_bytes = sock.recv(data_dict_len)
    data_dict = json.loads(data_dict_bytes)  # 自动解码再反序列化

    # 4.获取真实数据的各项信息
    for num,name in enumerate(data_dict.get('file_name')):
        print(f'编号:{num} 文件名:{name}')
    user_choice = input('输入要接受的文件:')
    if user_choice.isdigit():
        user_choice = int(user_choice)
    file_name = data_dict.get('file_name')[user_choice]
    real_name = file_name.split('.', maxsplit=1).pop(0)
    file_exten = file_name.split('.', maxsplit=1).pop()
    print(file_exten)
    total_size = data_dict.get('file_size')[user_choice]
    print(f'文件的字节数:{total_size}')
    page, lost = divmod(total_size, 1024)
    print(f'文件{page}mb {lost}bytes')
    # 5.告诉别人你要哪个文件
    sock.send(file_name.encode('utf8'))

    # 6.接受文件
    if file_exten == 'txt':
        file_bytes = 0
        time = 1
        with open(f'{real_name}'+'.'+f'{file_exten}', 'w', encoding='utf8') as f:
            while file_bytes < total_size:
                data = sock.recv(1024)
                f.write(data.decode('utf8'))
                file_bytes += len(data)
                print(f'循环:{time} data={len(data)}', '本次传输比率=%.3f%%' % (len(data) / total_size),
                      '进度:%.2f%%' % ((file_bytes / total_size) * 100))
                time += 1
        print(f'写入文件的字节数:{file_bytes}')
        user_choice2 = input('要继续接受吗?')
        if user_choice2 == 'y':
            sock.send(b'next')
        else:
            sock.send(b'q')
            break

    else:
        file_bytes = 0
        time = 1
        with open(f'{real_name}'+'.'+f'{file_exten}', 'wb') as f:
            while file_bytes < total_size:
                data = sock.recv(1024)  # 接受到16进制bytes类型  # 这里为什么传输的bytes类型跟以前不同? 无法用decode('utf8')解码
                f.write(data)
                file_bytes += len(data)
                print(f'循环:{time} data={len(data)}', '本次传输比率=%.3f%%'%(len(data)/total_size), '进度:%.2f%%'%((file_bytes/total_size)*100))
                time += 1
        print(f'写入文件的字节数:{file_bytes}')
        user_choice2 = input('要继续接受吗?')
        if user_choice2 == 'y':
            sock.send(b'next')
        else:
            sock.send(b'q')
            break
posted @ 2022-11-17 17:35  passion2021  阅读(104)  评论(0编辑  收藏  举报