python-网络并发模型

3. 网络并发模型

3.1 网络并发模型概述

  • 什么是网络并发

    在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。

    在这里插入图片描述

  • 循环网络模型问题

    循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。

3.2 多进程/线程并发模型

多进程/线程并发模中每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程,多任务并发模型也是实际工作中最为常用的服务端处理模型。

  • 模型特点

    • 优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
    • 缺点: 资源消耗较大
    • 适用情况:客户端请求较复杂,需要长时间占有服务器。
  • 创建流程

    • 创建网络套接字
    • 等待客户端连接
    • 有客户端连接,则创建新的进程/线程具体处理客户端请求
    • 主进程/线程继续等待处理其他客户端连接
    • 如果客户端退出,则销毁对应的进程/线程
多进程并发模型示例:"""
基于多进程的网络并发模型
重点代码 !!创建tcp套接字
等待客户端连接
有客户端连接,则创建新的进程具体处理客户端请求
父进程继续等待处理其他客户端连接
如果客户端退出,则销毁对应的进程
"""
from socket import *
from multiprocessing import Process
import sys# 地址变量
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)# 处理客户端具体请求
def handle(connfd):while True:data = connfd.recv(1024)if not data:breakprint(data.decode())connfd.close()# 服务入口函数
def main():# 创建tcp套接字tcp_socket = socket()tcp_socket.bind(ADDR)tcp_socket.listen(5)print("Listen the port %d"%PORT)# 循环连接客户端while True:try:connfd, addr = tcp_socket.accept()print("Connect from", addr)except KeyboardInterrupt:tcp_socket.close()sys.exit("服务结束")# 创建进程 处理客户端请求p = Process(target=handle, args=(connfd,),daemon=True)p.start()if __name__ == '__main__':main()
多线程并发模型示例:
"""
基于多线程的网络并发模型
重点代码 !!思路: 网络构建    线程搭建    /   具体处理请求
"""
from socket import *
from threading import Thread# 处理客户端具体请求
class Handle:# 具体处理请求函数 (逻辑处理,数据处理)def request(self, data):print(data)# 创建线程得到请求
class ThreadServer(Thread):def __init__(self, connfd):self.connfd = connfdself.handle = Handle()super().__init__(daemon=True)# 接收客户端的请求def run(self):while True:data = self.connfd.recv(1024).decode()if not data:breakself.handle.request(data)self.connfd.close()# 网络搭建
class ConcurrentServer:"""提供网络功能"""def __init__(self, *, host="", port=0):self.host = hostself.port = portself.address = (host, port)self.sock = self.__create_socket()def __create_socket(self):tcp_socket = socket()tcp_socket.bind(self.address)return tcp_socket# 启动服务 --> 准备连接客户端def serve_forever(self):self.sock.listen(5)print("Listen the port %d" % self.port)while True:connfd, addr = self.sock.accept()print("Connect from", addr)# 创建线程t = ThreadServer(connfd)t.start()if __name__ == '__main__':server = ConcurrentServer(host="0.0.0.0", port=8888)server.serve_forever()  # 启动服务

ftp 文件服务器

【1】 分为服务端和客户端,要求可以有多个客户端同时操作。

【2】 客户端可以查看服务器文件库中有什么文件。

【3】 客户端可以从文件库中下载文件到本地。

【4】 客户端可以上传一个本地文件到文件库。

【5】 使用print在客户端打印命令输入提示,引导操作

参考代码:######################### 服务端 ############################
from socket import *
from threading import Thread
import os
from time import sleep# 文件库
FTP = "/home/tarena/FTP/"# 处理客户端具体请求
class Handle:def __init__(self, connfd):self.connfd = connfddef do_list(self):filelist = os.listdir(FTP)if filelist:self.connfd.send(b"OK")sleep(0.1)# 发送文件列表files = "\n".join(filelist)self.connfd.send(files.encode())else:self.connfd.send(b"FAIL")def do_get(self, filename):try:file = open(FTP + filename, 'rb')except:self.connfd.send(b"FAIL")else:self.connfd.send(b"OK")sleep(0.1)#  发送文件while True:data = file.read(1024)if not data:breakself.connfd.send(data)file.close()sleep(0.1)self.connfd.send(b"##")def do_put(self, filename):# 判断文件是否存在if os.path.exists(FTP + filename):self.connfd.send(b"FAIL")else:self.connfd.send(b"OK")# 接收文件file = open(FTP + filename, 'wb')while True:data = self.connfd.recv(1024)if data == b"##":breakfile.write(data)file.close()def request(self):while True:data = self.connfd.recv(1024).decode()# 分情况具体处理请求函数tmp = data.split(' ')if not data or tmp[0] == "EXIT":breakelif tmp[0] == "LIST":self.do_list()elif tmp[0] == "GET":# tmp-> [GET,filename]self.do_get(tmp[1])elif tmp[0] == "PUT":self.do_put(tmp[1])# 创建线程得到请求
class FTPThread(Thread):def __init__(self, connfd):self.connfd = connfdself.handle = Handle(connfd)super().__init__(daemon=True)# 接收客户端的请求def run(self):self.handle.request()self.connfd.close()# 网络搭建
class ConcurrentServer:"""提供网络功能"""def __init__(self, *, host="", port=0):self.host = hostself.port = portself.address = (host, port)self.sock = self.__create_socket()def __create_socket(self):tcp_socket = socket()tcp_socket.bind(self.address)return tcp_socket# 启动服务 --> 准备连接客户端def serve_forever(self):self.sock.listen(5)print("Listen the port %d" % self.port)while True:connfd, addr = self.sock.accept()print("Connect from", addr)# 创建线程t = FTPThread(connfd)t.start()if __name__ == '__main__':server = ConcurrentServer(host="0.0.0.0", port=8880)server.serve_forever()  # 启动服务########################### 客户端 ###############################"""
文件服务器客户端
"""
from socket import *
import sys
from time import sleep# 具体发起请求,逻辑处理
class Handle:def __init__(self):self.server_address = ("127.0.0.1", 8880)self.sock = self.__connect_server()def __connect_server(self):tcp_socket = socket()tcp_socket.connect(self.server_address)return tcp_socketdef do_list(self):self.sock.send(b"LIST")  # 发送请求response = self.sock.recv(1024)  # 接收响应if response == b"OK":# 接收文件列表 file1\nfile2\n..files = self.sock.recv(1024 * 1024)print(files.decode())else:print("获取文件列表失败")def do_exit(self):self.sock.send(b"EXIT")self.sock.close()sys.exit("谢谢使用")def do_get(self, filename):request = "GET " + filenameself.sock.send(request.encode())  # 发送请求response = self.sock.recv(128)  # 接收响应if response == b"OK":file = open(filename, 'wb')# 接收文件内容,写入文件while True:data = self.sock.recv(1024)if data == b"##":breakfile.write(data)file.close()else:print("该文件不存在")def do_put(self, filename):try:file = open(filename, 'rb')except:print("该文件不存在")else:filename = filename.split("/")[-1]  # 获取文件名request = "PUT " + filenameself.sock.send(request.encode())response = self.sock.recv(128)if response == b"OK":# 发送文件while True:data = file.read(1024)if not data:breakself.sock.send(data)file.close()sleep(0.1)self.sock.send(b"##")else:print("上传失败")# 图形交互类
class FTPView:def __init__(self):self.__handle = Handle()def __display_menu(self):print()print("1. 查看文件")print("2. 下载文件")print("3. 上传文件")print("4. 退   出")print()def __select_menu(self):item = input("请输入选项:")if item == "1":self.__handle.do_list()elif item == "2":filename = input("要下载的文件:")self.__handle.do_get(filename)elif item == "3":filename = input("要上传的文件:")self.__handle.do_put(filename)elif item == "4":self.__handle.do_exit()else:print("请输入正确选项!")def main(self):while True:self.__display_menu()self.__select_menu()if __name__ == '__main__':ftp = FTPView()ftp.main()  # 启动

3.3 IO并发模型

3.3.1 IO概述
  • 什么是IO

    在程序中存在读写数据操作行为的事件均是IO行为,比如终端输入输出 ,文件读写,数据库修改和网络消息收发等。

  • 程序分类

    • IO密集型程序:在程序执行中有大量IO操作,而运算操作较少。消耗cpu较少,耗时长。
    • 计算密集型程序:程序运行中运算较多,IO操作相对较少。cpu消耗多,执行速度快,几乎没有阻塞。
  • IO分类:阻塞IO ,非阻塞IO,IO多路复用等。

3.3.2 阻塞IO
  • 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
  • 效率:阻塞IO效率很低。但是由于逻辑简单所以是默认IO行为。
  • 阻塞情况
    • 因为某种执行条件没有满足造成的函数阻塞
      e.g. accept input recv
    • 处理IO的时间较长产生的阻塞状态
      e.g. 网络传输,大文件读写
3.3.3 非阻塞IO
  • 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。
  • 设置套接字为非阻塞IO

    sockfd.setblocking(bool)
    功能:设置套接字为非阻塞IO
    参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
    
  • 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。

    sockfd.settimeout(sec)
    功能:设置套接字的超时时间
    参数:设置的时间
    
    非阻塞IO示例:
    """
    设置非阻塞的套接字
    """
    from socket import *
    from time import sleep, ctime# 日志文件模拟与网络无关IO
    file = open("my.log", "a")# 创建tcp套接字
    sock = socket()
    sock.bind(("127.0.0.1", 8888))
    sock.listen(5)# 设置为非阻塞
    # sock.setblocking(False)# 设置超时事件
    sock.settimeout(3)# 循环处理客户端连接
    while True:try:connfd, addr = sock.accept()print("Connect from", addr)except timeout as e:# 模拟一个与accept 无关的事件msg = "%s : %s\n" % (ctime(), e)file.write(msg)except BlockingIOError as e:# 模拟一个与accept 无关的事件msg = "%s : %s\n" % (ctime(), e)file.write(msg)sleep(2)else:# accept 正常执行data = connfd.recv(1024)print(data.decode())
3.3.4 IO多路复用
  • 定义

    同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。

  • 具体方案

    • select方法 : Windows Linux Unix
    • epoll方法: Linux
  • select 方法

rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数: rlist  列表  读IO列表,添加等待发生的或者可读的IO事件wlist  列表  写IO列表,存放要可以主动处理的或者可写的IO事件xlist  列表 异常IO列表,存放出现异常要处理的IO事件timeout  超时时间返回值: rs 列表  rlist中准备就绪的IOws 列表  wlist中准备就绪的IOxs 列表  xlist中准备就绪的IO
select 方法示例:
"""
IO多路复用 基础演示 select
"""
from select import select
from socket import *# 创建几个IO对象
tcp_sock = socket()
tcp_sock.bind(("0.0.0.0",8888))
tcp_sock.listen(5)file = open("my.log",'r')udp_sock = socket(AF_INET,SOCK_DGRAM)print("开始监控IO")
rs,ws,xs = select([file,udp_sock],[file,udp_sock],[])
print("rlist:",rs)
print("wlist:",ws)
print("xlist:",xs)
  • epoll方法
ep = select.epoll()
功能 : 创建epoll对象
返回值: epoll对象
ep.register(fd,event)   
功能: 注册关注的IO事件
参数:fd  要关注的IOevent  要关注的IO事件类型常用类型EPOLLIN  读IO事件(rlist)EPOLLOUT 写IO事件 (wlist)EPOLLERR 异常IO  (xlist)e.g. ep.register(sockfd,EPOLLIN|EPOLLERR)ep.unregister(fd)
功能:取消对IO的关注
参数:IO对象或者IO对象的fileno
events = ep.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IOevents格式  [(fileno,event),()....]每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
epoll方法示例:
"""
IO多路复用 基础演示 epoll
"""
from select import *
from socket import *# 创建几个IO对象
tcp_sock = socket()
tcp_sock.bind(("0.0.0.0",8888))
tcp_sock.listen(5)file = open("my.log",'r+')udp_sock = socket(AF_INET,SOCK_DGRAM)# 创建epoll对象
ep = epoll()# 关注IO对象
ep.register(tcp_sock,EPOLLIN)
ep.register(udp_sock,EPOLLOUT|EPOLLERR)# 建立查找字典
map = {tcp_sock.fileno():tcp_sock,udp_sock.fileno():udp_sock,
}print("开始监控IO")
events = ep.poll()
print(events) # 就绪的IO# 不再关注
ep.unregister(udp_sock)
del map[udp_sock.fileno()]
  • select 方法与epoll方法对比
    • epoll 效率比select要高
    • epoll 同时监控IO数量比select要多
    • epoll 支持EPOLLET触发方式
3.3.5 IO并发模型

利用IO多路复用等技术,同时处理多个客户端IO请求。

  • 优点 : 资源消耗少,能同时高效处理多个IO行为

  • 缺点 : 只针对处理并发产生的IO事件

  • 适用情况:HTTP请求,网络传输等都是IO行为,可以通过IO多路复用监控多个客户端的IO请求。

  • 网络并发服务实现过程

    【1】将套接字对象设置为关注的IO,通常设置为非阻塞状态。

    【2】通过IO多路复用方法提交,进行IO监控。

    【3】阻塞等待,当监控的IO有事件发生时结束阻塞。

    【4】遍历返回值列表,确定就绪的IO事件类型。

    【5】处理发生的IO事件。

    【6】继续循环监控IO发生。

IO多路复用并发模型################################# select 方法 ####################################
"""
基于select的并发服务模型
使用函数完成
"""
from select import select
from socket import *# 服务器地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST,PORT)# 监控列表
rlist = []
wlist = []
xlist = []# 处理客户端连接
def connect_client(sock):connfd, addr = sock.accept()print("Connect from", addr)connfd.setblocking(False)rlist.append(connfd)  # 增加关注对象# 处理客户端消息
def handle_client(connfd):data = connfd.recv(1024)# 处理客户端退出if not data:rlist.remove(connfd) # 不再关注connfd.close()returnprint(data.decode())connfd.send(b"Thanks")def main():# 创建监听套接字sock = socket()sock.bind(ADDR)sock.listen(3)# 配合非阻塞IO防止网络中断带来的内部阻塞sock.setblocking(False)rlist.append(sock) #  初始监控的IO对象# 循环监控关注的IO发生while True:rs,ws,xs = select(rlist,wlist,xlist)for r in rs:if r is sock:connect_client(r) # 连接客户端else:handle_client(r) # 处理客户端消息if __name__ == '__main__':main()################################ epoll 方法 ################################
"""
基于epoll的并发服务模型
使用类实现
"""
from select import *
from socket import *class EpollServer:def __init__(self, host="", port=0):self.host = hostself.port = portself.address = (host, port)self.sock = self._create_socket()self.ep = epoll()self.map = {} # 查找字典def _create_socket(self):sock = socket()sock.bind(self.address)sock.setblocking(False)return sock# 处理客户端连接def _connect_client(self, fd):connfd, addr = self.map[fd].accept()print("Connect from", addr)connfd.setblocking(False)# 增加关注对象,设置边缘触发self.ep.register(connfd, EPOLLIN | EPOLLET)self.map[connfd.fileno()] = connfd  # 维护字典# 处理客户端消息def _handle_client(self, fd):data = self.map[fd].recv(1024)# 处理客户端退出if not data:self.ep.unregister(fd)  # 不再关注self.map[fd].close()del self.map[fd]  # 从字典删除returnprint(data.decode())self.map[fd].send(b"Thanks")# 启动服务def serve_forever(self):self.sock.listen(3)print("Listen the port %d" % self.port)self.ep.register(self.sock, EPOLLIN)  # 设置关注self.map[self.sock.fileno()] = self.sockwhile True:events = self.ep.poll()# 循环查看哪个IO发生就处理哪个for fd, event in events:if fd == self.sock.fileno():self._connect_client(fd)elif event == EPOLLIN:self._handle_client(fd)if __name__ == '__main__':ep = EpollServer(host="0.0.0.0", port=8888)ep.serve_forever()  # 启动服务

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跨平台WPF音乐商店应用程序

目录 一 简介 二 设计思路 三 源码 一 简介 支持在线检索音乐,支持实时浏览当前收藏的音乐及音乐数据的持久化。 二 设计思路 采用MVVM架构,前后端分离,子界面弹出始终位于主界面的中心。 三 源码 视窗引导启动源码: namesp…

知名在线市场 Etsy 允许在其平台上销售 AI 艺术品,但有条件限制|TodayAI

近日,以手工和复古商品著称的在线市场 Etsy 宣布,将允许在其平台上销售 AI 生成的艺术品。这一举措引发了广泛关注和争议。尽管 Etsy 正在接受 AI 艺术的潮流,但平台对这一类商品的销售设置了一些限制。 根据 Etsy 新发布的政策,…

51单片机(STC8H8K64U/STC8051U34K64)_RA8889驱动TFT大屏_I2C_HW参考代码(v1.3) 硬件I2C方式

本篇介绍单片机使用硬件I2C方式控制RA8889驱动彩屏。 提供STC8H8K64U和STC8051U34K64的参考代码。 【硬件部份】STC8H8K64U/STC8051U34K64 RA8889开发板 7寸TFT 800x480 1. 实物连接图:STC8H8K64URA8889开发板,使用P2口I2C接口: 2.实物连…

【QT】信号与槽(概述、使用、自定义、连接方式、其他说明)

一、信号和槽概述 在 Qt 中,用户和控件的每次交互过程称为一个事件。比如 “用户点击按钮” 是一个事件,“用户关闭窗口” 也是一个事件。每个事件都会发出一个信号,例如用户点击按钮会发出 “按钮被点击” 的信号,用户关闭窗口…

windows edge自带的pdf分割工具(功能)

WPS分割pdf得会员,要充值!网上一顿乱找,发现最简单,最好用,免费的还是回到Windows。 Windows上直接在edge浏览器打开PDF,点击 打印 按钮,页面下选择对应页数 打印机 选择 另存为PDF,然后保存就…

记录一下在Hyper-v中动态磁盘在Ubuntu中不完全用到的问题(扩展根目录)

在之前给hyper虚拟机的Ubuntu分配磁盘有20G; 后来在Ubuntu中查看磁盘发现有一个分区没用到: 贴的图片是完成扩展后的 之前这里是10G,然后有个dev/sda4的分区,也是10G,Type是Microsoft Basic Data; …

【LeetCode】填充每个节点的下一个右侧节点指针 II

目录 一、题目二、解法完整代码 一、题目 给定一个二叉树: struct Node { int val; Node *left; Node *right; Node *next; } 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NUL…

spring security源码追踪理解(一)

一、前言 近期看了spring security相关的介绍,再加上项目所用若依框架的底层安全模块也是spring security,所以想从源码的角度加深下对该安全模块的理解(看源码之前,我们要先有个意识,那就是spring security安全模块主…

【面试题】Redo log和Undo log

Redo log 介绍Redo log之前我们需要了解一下,mysql数据操作的流程: 上述就是数据操作的流程图,可以发现sql语句并不是直接操作的磁盘而是通过操作内存,然后进行内存到磁盘的一个同步。这里我们必须要了解一些区域: 缓…

c++基础(类和对象中)(类的默认成员函数)

目录 一.构造函数(类似初始化) 1.概念 2.构造函数的特点 二.析构函数(类似 销毁对象/空间) 三.拷贝构造函数(类似复制粘贴的一种 初始化 ) 1.概念: 2.拷贝构造的特点: 四.赋值运算符重载&#xff08…

IDEA的工程与模块管理

《IDEA破解、配置、使用技巧与实战教程》系列文章目录 第一章 IDEA破解与HelloWorld的实战编写 第二章 IDEA的详细设置 第三章 IDEA的工程与模块管理 第四章 IDEA的常见代码模板的使用 第五章 IDEA中常用的快捷键 第六章 IDEA的断点调试(Debug) 第七章 …

STM32的ADC详解

目录 一、ADC简介 二、ADC的时钟 三、ADC特性 四、ADC功能说明 五、规则通道和注入通道 1.规则通道 2.注入通道 3.区别 六、数据寄存器 1.右对齐 2.左对齐 七、转换模式 1.单次转换模式 2.续转换模式 3.扫描模式 4.区别 八、程序实现 1.需求 2.ADC初始化 3.A…

ipv6 基础学习(一)

IPv6 为什么要有IPV6? IPv4地址空间有限:IPv4使用32位地址,最多可提供约43亿个地址。随着互联网设备数量的爆炸式增长,这些地址已经几乎耗尽。 IPv6地址空间庞大:IPv6使用128位地址,可以提供大约3.410^3…

爬虫自己做的

1.urllib 1.1基本使用 1.2 下载(图片,页面,视频) 1.3 get 1.3.1 quote 中文变成对应uncode编码 当url 的wd中文时 quote是将中文变成对应uncode编码 然后拼接成完整的url 1.3.2urlencode方法 wd有多个参数 1.3.3ajas get实例 …

【Git远程操作】理解分布式管理 | 创建远程仓库

目录 1.理解分布式管理 多人协作开发 2.创建远程仓库 2.1仓库名&路径 2.2初始化仓库&设置模板 1.理解分布式管理 目前我们学习的所有内容都是在本地来完成的。(add /commit /版本撤销回退/分支管理) Git是一个分布式 的版本控制系统。 分支…

动漫风格动漫404网站维护HTML源码

源码介绍 动漫风格动漫404网站维护HTML源码,源码由HTMLCSSJS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面 效果预览 源码下载 动漫风格动漫404网站维护HTML源码

【存储学习笔记】1:机械硬盘(Hard Drive Disk)结构和寻址方式

目录 HDD的结构HDD的寻址方式CHS寻址(不适用于等密度结构磁盘)LBA寻址(目前普遍使用的线性寻址方式) HDD的寻址速度 HDD的结构 盘面(Platter):单面或者双面覆盖着用于记录数据的磁性物质&#x…

Gateway源码分析:路由Route、断言Predicate、Filter

文章目录 源码总流程图说明GateWayAutoConfigurationDispatcherHandlergetHandler()handleRequestWith()RouteToRequestUrlFilterReactiveLoadBalancerClientFilterNettyRoutingFilter 补充知识适配器模式 详细流程图 源码总流程图 在线总流程图 说明 Gateway的版本使用的是…

配置单区域OSPF

目录 引言 一、搭建基础网络 1.1 配置网络拓扑图如下 1.2 IP地址表 二、测试每个网段都能单独连通 2.1 PC0 ping通Router1所有接口 2.2 PC1 ping通Router1所有接口 2.3 PC2 ping通Router2所有接口 2.4 PC3 ping通Router2所有接口 2.5 PC4 ping通Router3所有接口 2.…

【Gitlab】记一次升级 Gitlab 后 API 失效的问题

背景 前段时间,因内部使用的 Gitlab 版本存在漏洞,需要进行升级,于是乎,将 Gitlab 从 16.6.0 升级到 16.11.3。而我们项目有个接口是用于获取 Gitlab 上的开发人员。 然后,今天,突然发现这个接口获取不到…