python并发编程(并发与并行,同步和异步,阻塞与非阻塞)

news2024/11/29 8:59:29

最近在学python的网络编程,学了socket通信,并利用socket实现了一个具有用户验证功能,可以上传下载文件、可以实现命令行功能,创建和删除文件夹,可以实现的断点续传等功能的FTP服务器。但在这当中,发现一些概念区分起来很难,比如并发和并行,同步和异步,阻塞和非阻塞,但是这些概念却很重要。因此在此把它总结下来。

1. 并发 & 并行

  并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。简言之,是指系统具有处理多个任务的能力。

  并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。简言之,是指系统具有同时处理多个任务的能力。

  下面我们来两个例子:

import threading #线程
import time


def music():
    print('begin to listen music {}'.format(time.ctime()))
    time.sleep(3)
    print('stop to listen music {}'.format(time.ctime()))


def game():
    print('begin to play game {}'.format(time.ctime()))
    time.sleep(5)
    print('stop to play game {}'.format(time.ctime()))


if __name__ == '__main__':
    music()
    game()
    print('ending.....')

music的时间为3秒,game的时间为5秒,如果按照我们正常的执行,直接执行函数,那么将按顺序顺序执行,整个过程8秒。

import threading #线程
import time


def music():
    print('begin to listen music {}'.format(time.ctime()))
    time.sleep(3)
    print('stop to listen music {}'.format(time.ctime()))


def game():
    print('begin to play game {}'.format(time.ctime()))
    time.sleep(5)
    print('stop to play game {}'.format(time.ctime()))


if __name__ == '__main__':
    t1 = threading.Thread(target=music) #创建一个线程对象t1 子线程
    t2 = threading.Thread(target=game) #创建一个线程对象t2 子线程

    t1.start()
    t2.start()

    # t1.join() #等待子线程执行完 t1不执行完,谁也不准往下走
    t2.join()

    print('ending.......') #主线程
    print(time.ctime())

  在这个例子中,我们开了两个线程,将music和game两个函数分别通过线程执行,运行结果显示两个线程同时开始,由于听音乐时间3秒,玩游戏时间5秒,所以整个过程完成时间为5秒。我们发现,通过开启多个线程,原本8秒的时间缩短为5秒,原本顺序执行现在是不是看起来好像是并行执行的?看起来好像是这样,听音乐的同时在玩游戏,整个过程的时间随最长的任务时间变化。但真的是这样吗?那么下面我来提出一个GIL锁的概念。

GIL(全局解释器锁):无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。
import time
from threading import Thread


def add():
    sum = 0
    i = 1
    while i<=1000000:
        sum += i
        i += 1
    print('sum:',sum)


def mul():
    sum2 = 1
    i = 1
    while i<=100000:
        sum2 = sum2 * i
        i += 1
    print('sum2:',sum2)


start = time.time()

add()
mul() #串行比多线程还快

print('cost time %s'%(time.time()-start))

import time
from threading import Thread


def add():
    sum = 0
    i = 1
    while i<=1000000:
        sum += i
        i += 1
    print('sum:',sum)


def mul():
    sum2 = 1
    i = 1
    while i<=100000:
        sum2 = sum2 * i
        i += 1
    print('sum2:',sum2)


start = time.time()
t1 = Thread(target=add)
t2 = Thread(target=mul)

l = []
l.append(t1)
l.append(t2)

for t in l:
   t.start()

for t in l:
    t.join()

print('cost time %s'%(time.time()-start))

 

  哎吆,这是怎么回事,串行执行比多线程还快?不符合常理呀。是不是颠覆了你的人生观,这个就和GIL锁有关,同一时刻,系统只允许一个线程执行,那么,就是说,本质上我们之前理解的多线程的并行是不存在的,那么之前的例子为什么时间确实缩短了呢?这里有涉及到一个任务的类型。

--任务: 1.IO密集型(会有cpu空闲的时间)  注:sleep等同于IO操作, socket通信也是IO  
        2.计算密集型
  而之前那个例子恰好是IO密集型的例子,后面这个由于涉及到了加法和乘法,属于计算密集型操作,那么,就产生了一个结论,多线程对于IO密集型任务有作用,
而计算密集型任务不推荐使用多线程。
  而其中我们还可以得到一个结论:由于GIL锁,多线程不可能真正实现并行,所谓的并行也只是宏观上并行微观上并发,本质上是由于遇到io操作不断的cpu切换
所造成并行的现象。由于cpu切换速度极快,所以看起来就像是在同时执行。
  --问题:没有利用多核的优势
    --这就造成了多线程不能同时执行,并且增加了切换的开销,串行的效率可能更高。

2. 同步 & 异步

    对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

     1. 等待数据准备 (Waiting for the data to be ready)
     2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
同步:当进程执行IO(等待外部数据)的时候,-----等。同步(例如打电话的时候必须等)
异步:当进程执行IO(等待外部数据)的时候,-----不等,去执行其他任务,一直等到数据接收成功,再回来处理。异步(例如发短信)
当我们去爬取一个网页的时候,要爬取多个网站,有些人可能会发起多个请求,然后通过函数顺序调用。执行顺序也是先调用先执行。效率非常低。
下面我们看一下异步的一个例子:
import socket
import select

"""
########http请求本质,IO阻塞########
sk = socket.socket()
#1.连接
sk.connect(('www.baidu.com',80,)) #阻塞
print('连接成功了')
#2.连接成功后发送消息
sk.send(b"GET / HTTP/1.0\r\nHost: baidu.com\r\n\r\n")

#3.等待服务端响应
data = sk.recv(8096)#阻塞
print(data) #\r\n\r\n区分响应头和影响体

#关闭连接
sk.close()
"""
"""
########http请求本质,IO非阻塞########
sk = socket.socket()
sk.setblocking(False)
#1.连接
try:
    sk.connect(('www.baidu.com',80,)) #非阻塞,但会报错
    print('连接成功了')
except BlockingIOError as e:
    print(e)

#2.连接成功后发送消息
sk.send(b"GET / HTTP/1.0\r\nHost: baidu.com\r\n\r\n")

#3.等待服务端响应
data = sk.recv(8096)#阻塞
print(data) #\r\n\r\n区分响应头和影响体

#关闭连接
sk.close()
"""


class HttpRequest:
    def __init__(self,sk,host,callback):
        self.socket = sk
        self.host = host
        self.callback = callback

    def fileno(self):
        return self.socket.fileno()


class HttpResponse:
    def __init__(self,recv_data):
        self.recv_data = recv_data
        self.header_dict = {}
        self.body = None

        self.initialize()

    def initialize(self):
        headers, body = self.recv_data.split(b'\r\n\r\n', 1)
        self.body = body
        header_list = headers.split(b'\r\n')
        for h in header_list:
            h_str = str(h,encoding='utf-8')
            v = h_str.split(':',1)
            if len(v) == 2:
                self.header_dict[v[0]] = v[1]


class AsyncRequest:
    def __init__(self):
        self.conn = []
        self.connection = [] # 用于检测是否已经连接成功

    def add_request(self,host,callback):
        try:
            sk = socket.socket()
            sk.setblocking(0)
            sk.connect((host,80))
        except BlockingIOError as e:
            pass
        request = HttpRequest(sk,host,callback)
        self.conn.append(request)
        self.connection.append(request)

    def run(self):

        while True:
            rlist,wlist,elist = select.select(self.conn,self.connection,self.conn,0.05)
            for w in wlist:
                print(w.host,'连接成功...')
                # 只要能循环到,表示socket和服务器端已经连接成功
                tpl = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n"  %(w.host,)
                w.socket.send(bytes(tpl,encoding='utf-8'))
                self.connection.remove(w)
            for r in rlist:
                # r,是HttpRequest
                recv_data = bytes()
                while True:
                    try:
                        chunck = r.socket.recv(8096)
                        recv_data += chunck
                    except Exception as e:
                        break
                response = HttpResponse(recv_data)
                r.callback(response)
                r.socket.close()
                self.conn.remove(r)
            if len(self.conn) == 0:
                break


def f1(response):
    print('保存到文件',response.header_dict)


def f2(response):
    print('保存到数据库', response.header_dict)


url_list = [
    {'host':'www.youku.com','callback': f1},
    {'host':'v.qq.com','callback': f2},
    {'host':'www.cnblogs.com','callback': f2},
]

req = AsyncRequest()
for item in url_list:
    req.add_request(item['host'],item['callback'])

req.run()

  我们可以看到,三个请求发送顺序与返回顺序,并不一样,这样就体现了异步请求。即我同时将请求发送出去,哪个先回来我先处理哪个。

  即我们可以理解为:我打电话的时候只允许和一个人通信,和这个人通信结束之后才允许和另一个人开始。这就是同步。

           我们发短信的时候发完可以不去等待,去处理其他事情,当他回复之后我们再去处理,这样就大大解放了我们的时间。这就是异步。

  体现在网页请求上面就是我请求一个网页时候等待他回复,否则不接收其它请求,这就是同步。另一种就是我发送请求之后不去等待他是否回复,而去处理其它请求,当处理完其他请求之后,某个请求说,我的回复了,然后程序转而去处理他的回复数据。这就是异步请求。所以,异步可以充分cpu的效率。

3. 阻塞 & 非阻塞

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
下面我们通过socket实现一个命令行功能来感受一下。
  
#服务端
from socket import *
import subprocess
import struct

ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
backlog = 5

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
tcp_server.bind(ip_port)
tcp_server.listen(backlog)

while True:
    conn, addr = tcp_server.accept()
    print('新的客户端链接:', addr)
    while True:
        try:
            cmd = conn.recv(buffer_size)
            print('收到客户端命令:', cmd.decode('utf-8'))

            #执行命令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)
            data_length = struct.pack('i',length)
            conn.send(data_length)
            conn.send(cmd_res)
        except Exception as e:
            print(e)
            break
    conn.close()


#客户端
from socket import *

ip_port = ('127.0.0.1',8000)
buffer_size = 1024
backlog = 5

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'))

    #解决粘包
    length = tcp_client.recv(4)
    length = struct.unpack('i',length)[0]

    recv_size = 0
    recv_msg = b''
    while recv_size < length:
        recv_msg += tcp_client.recv(buffer_size)
        recv_size = len(recv_msg)

    print(recv_msg.decode('gbk'))

开启了服务器和一个客户端之后,我们在客户端输入一些命令,然后正确显示,功能实现。这是在我再打开一个客户端,输入命令,发现服务器迟迟没有响应。

这个就是当一个客户端在请求的时候,当这个客户端没有结束的时候,服务器不会去处理其他客户端的请求。这时候就阻塞了。

如何让服务器同时处理多个客户端请求呢?

 View Code

这段代码通过socketserver模块实现了socket的并发。这个过程中,当一个客户端在向服务器请求的时候,另一个客户端也可以正常请求。服务器在处理一个客户端请求的时候,另一个请求没有被阻塞。

总结:只要有一丁点阻塞,就是阻塞IO。

   异步IO的特点就是全程无阻塞。

   有些人常把同步阻塞和异步非阻塞联系起来,但实际上经过分析,阻塞与同步,非阻塞和异步的定义是不一样的。同步和异步的区别是遇到IO请求是否等待。阻塞和非阻塞的区别是数据没准备好的情况下是否立即返回。关于这两点的区别,看了很多博客,最后总结如下。

阻塞和非阻塞指的是执行一个操作是等操作结束再返回,还是马上返回。

比如餐馆的服务员为用户点菜,当有用户点完菜后,服务员将菜单给后台厨师,此时有两种方式:

  • 第一种:就在出菜窗口等待,直到厨师炒完菜后将菜送到窗口,然后服务员再将菜送到用户手中;
  • 第二种:等一会再到窗口来问厨师,某个菜好了没?如果没有先处理其他事情,等会再去问一次;

第一种就是阻塞方式,第二种则是非阻塞的。

  同步和异步又是另外一个概念,它是事件本身的一个属性。还拿前面点菜为例,服务员直接跟厨师打交道,菜出来没出来,服务员直接知道,但只有当厨师将菜送到服务员手上,这个过程才算正常完成,这就是同步的事件。同样是点菜,有些餐馆有专门的传菜人员,当厨师炒好菜后,传菜员将菜送到传菜窗口,并通知服务员,这就变成异步的了。其实异步还可以分为两种:带通知的和不带通知的。前面说的那种属于带通知的。有些传菜员干活可能主动性不是很够,不会主动通知你,你就需要时不时的去关注一下状态。这种就是不带通知的异步。

对于同步和异步的事件,阻塞和非阻塞都是可以的。非阻塞又有两种方式:主动查询和被动接收消息。被动不意味着一定不好,在这里它恰恰是效率更高的,因为在主动查询里绝大部分的查询是在做无用功。对于带通知的异步事件,两者皆可。而对于不带通知的,则只能用主动查询。

  但是对于非阻塞和异步的概念有点混淆,非阻塞只是意味着方法调用不阻塞,就是说作为服务员的你不用一直在窗口等,非阻塞的逻辑是"等可以读(写)了告诉你",但是完成读(写)工作的还是调用者(线程)服务员的你等菜到窗口了还是要你亲自去拿。而异步意味这你可以不用亲自去做读(写)这件事,你的工作让别人(别的线程)来做,你只需要发起调用,别人把工作做完以后,或许再通知你,它的逻辑是“我做完了 告诉/不告诉 你”,他和非阻塞的区别在于一个是"已经做完"另一个是"可以去做"。

这也是同步和异步最大的区别,就是同步在有通知时可以进行相关操作,而异步有通知时则代表操作已经完成

再举一个例子:

  去书店借一本书,同步就是我要亲自到书店,问老板有没有这本书,阻塞就是老板查询的时候(读写)我只能在那等着,老板找到书后把书交给我,这就是同步阻塞。

  我亲自到书店借书,老板在找这本书的时候,我可以去干别的,然后每隔一段时间去问老板书找到了没有,也可以等老板找到书以后通知我,这就是同步非阻塞。

  我想借本书,找个人帮我去借,借到书以后再通知我,这就是异步,我只发起调用,但是本身并不参与这个事件,而是让别的线程去做这个事。

  同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。

  阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。

  帮我借书的那个人有没有借到书,我可以打电话问他(轮询),也可以等他通知我,这是异步的通知;在借书的过程中借书的那个人可以轮询的方式查看书是否已经找到(缓冲区有没有数据),找到了你可以把它拿走,也可以等老板找到书后通知我,这是非阻塞的通知与轮询。

  这里面其实涉及到的知识点还很多,这里只是凭我的记忆简单总结了一下,以后会补充更多。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363905.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《JeecgBoot系列》 如何设计表单实现“下拉组件二级联动“ ? 以省市二级联动为例

《JeecgBoot系列》 如何设计表单实现"下拉组件二级联动" ? 以省市二级联动为例 一、准备字典表 1.1 创建字典表 CREATE TABLE sys_link_table ( id int NULL, pid int NULL, name varchar(64) null );1.2 准备数据 idpidname1全国21浙江省32杭州市42宁波市51江苏…

postgres 源码解析50 LWLock轻量锁--1

简介 postgres LWLock&#xff08;轻量级锁&#xff09;是由SpinLock实现&#xff0c;主要提供对共享存储器的数据结构的互斥访问。LWLock有两种锁模式&#xff0c;一种为排他模式&#xff0c;另一种是共享模式&#xff0c;如果想要读取共享内存中的内容&#xff0c;需要在读取…

kafka如何动态消费新增topic主题

一、背景使用spring-kafka客户端&#xff0c;每次新增topic主题&#xff0c;都需要硬编码客户端并重新发布服务&#xff0c;操作麻烦耗时长&#xff0c;对于业务逻辑相似场景&#xff0c;创建新主题动态监听可以用kafka-batch-starter组件二、核心设计点1、动态接入消息&#x…

Scala模式匹配详解(第八章:基本语法、模式守卫、模式匹配类型)(尚硅谷笔记)

模式匹配第 8 章 模式匹配8.1 基本语法8.2 模式守卫8.3 模式匹配类型8.3.1 匹配常量8.3.2 匹配类型8.3.3 匹配数组8.3.4 匹配列表8.3.5 匹配元组8.3.6 匹配对象及样例类8.4 变量声明中的模式匹配8.5 for 表达式中的模式匹配8.6 偏函数中的模式匹配(了解)第 8 章 模式匹配 Scal…

Redis:缓存穿透、缓存雪崩和缓存击穿(未完待续)

Redis的缓存穿透、缓存雪崩和缓存击穿一. 缓存穿透1.1 概念1.2 造成的问题1.3 解决方案1.4 案例&#xff1a;查询商铺信息&#xff08;缓存穿透的实现&#xff09;二. 缓存雪崩2.1 概念2.2 解决方案三. 缓存击穿&#xff08;热点key&#xff09;3.1 概念3.2 解决方案3.3 案例&a…

网络基础概述

1.计算机网络背景 ​ 计算机刚刚发展的时候&#xff0c;是没有网络的&#xff0c;每一台计算机都是相互独立的。后来&#xff0c;人们有了多人协作的需求&#xff0c;人们就想办法把多台计算机用“线”连接起来&#xff0c;实现数据共享。后来&#xff0c;连接到一起的电脑越来…

地球板块运动vr交互模拟体验教学提高学生的学习兴趣

海陆变迁是地球演化史上非常重要的一个过程&#xff0c;它不仅影响着地球的气候、地貌、生物多样性等方面&#xff0c;还对人类文明的演化产生了深远的影响。为了帮助学生更加深入地了解海陆变迁的过程和机制&#xff0c;很多高校教育机构开始采用虚拟现实技术进行教学探究。 V…

Go语言进阶与依赖管理-学习笔记

1 语言进阶 1.1 Goroutine 线程&#xff1a;内核态&#xff0c;栈MB级别 协程&#xff1a;用户态&#xff0c;轻量级线程&#xff0c;栈KB级 1.2 CSP 提倡通信实现共享内存 1.3 Channel 创建方法 make(chan 元素类型&#xff0c;缓冲区大小&#xff09; 无缓冲通道&#x…

【Storm】【二】安装

1 准备 1.1 准备linux服务器 本文搭建的是3节点的集群&#xff0c;需要3台linux服务器&#xff0c;我这里使用的是centos7版本的linux虚拟机&#xff0c;虚拟机网络配置如下&#xff1a; 主节点&#xff1a; master 192.168.92.90 从节点&#xff1a; slave1 192.168.92.…

Git 教程

目录1.简介&#xff1a;2.安装Git3.Git 如何工作状态区域4.使用Git5.Git配置5.1 创建仓库 - repository5.2 配置5.2.1 --global5.2.2 检查配置6. 查看工作区的文件状态6.1什么是工作区6.2 如果显示乱码的解决方式7.在工作区添加单个文件8. 添加工作区文件到暂存区9. 创建版本10…

Docker启动RabbitMQ,实现生产者与消费者

目录 一、Docker拉取镜像并启动RabbitMQ 二、Hello World &#xff08;一&#xff09;依赖导入 &#xff08;二&#xff09;消息生产者 &#xff08;三&#xff09;消息消费者 三、实现轮训分发消息 &#xff08;一&#xff09;抽取工具类 &#xff08;二&#xff09;启…

网络安全——数链路层据安全协议

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.数据链路层安全协议简介 1.数据链路安全性 二.局域网数据链路层协议 1.…

git应用笔记(三)

在新增虚拟机linux的基础上&#xff0c;做git的下载与提交 1、初始化自己的用户名和邮箱。 git config --global user.name “输入你的用户名” git config --global user.email “输入你的邮箱” 2、将本地公钥及配置如图1复制粘贴到虚拟机当前用户.ssh\目录下 4929a0205f43…

面渣逆袭:分布式十二问,万字图文详解

大家好&#xff0c;我是老三&#xff0c;不管今年金三银四如何&#xff0c;面渣逆袭系列继续&#xff0c;这期我们来看看分布式。 分布式理论 1. 说说CAP原则&#xff1f; CAP原则又称CAP定理&#xff0c;指的是在一个分布式系统中&#xff0c;Consistency&#xff08;一致性…

硬件学习 软件Cadence day07 PCB 底板电路图布线

1.根据原理图的元器件&#xff0c; 选择在 PCB 芯片制作的元器件 &#xff08;allegro中原理图和pcb中元件的交互&#xff09; 1.首先完成下列操作 可以尝试先关闭再打开&#xff0c; 等下操作的时候就好 发现新增的发光物体&#xff01;&#xff01; 2.完成操作 &#xff0c;…

Web3中文|香港拟允许比特币交易,瞄准“全球web3中心”

香港再次成为全球加密行业关注的焦点。在美国SEC对于加密货币交易所Kraken、BUSD发行商Paxos以及BA的重磅打击对比下&#xff0c;香港从去年开始持续拥抱Web3的姿态&#xff0c;让投资者开始押注香港。2023年2月20日&#xff0c;香港证监会宣布&#xff0c;就适用于虚拟资产交易…

Linux:基于libevent读写管道代码

基于libevent读写管道代码&#xff1a; 读端&#xff1a; #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <event2/event.h> #include…

gdb的简单练习

题目来自《ctf安全竞赛入门》1.用vim写代码vim gdb.c#include "stdio.h" #include "stdlib.h" void main() {int i 100;int j 101;if (i j){printf("bingooooooooo.");system("/bin/sh");}elseprintf("error............&quo…

Python 算法交易实验48 表字段设计

说明 虽然说的是表&#xff0c;实际上用的是Mongo集合 基于ADBS(APIFunc DataBase Service)可以构造一个供后续研究、生产长时间使用的数据基础&#xff0c;这个基础包括了&#xff1a; 1 队列服务。通过队列&#xff0c;数据可以通过API实现零担和批量两种模式的快速存储。2 …

ideal创建maven项目

前置工作本机安装mavenIdea 设置使用本机maven 工具Settings--->Maven开始创建maven项目创建maven项目&#xff0c;勾选通过模板创建&#xff0c;选择 maven-archetype-webapp 模板GroupId: 公司名倒序ArtifactId: 项目名设置本地maven仓库配置项目文件显示名&#xff0c;和…