【开源库学习】从OkHttp到Retrofit(其一 OkHttp)

news2024/12/24 2:08:11

从OkHttp到Retrofit

  • 主要流程
    • dispatcher
    • Interceptors
      • RetryAndFollowUpInterceptor
      • BridgeInterceptor
      • CacheInterceptor
      • ConnectInterceptor
      • CallServerInterceptor
    • 缓存
    • 连接池

主要流程

主要流程
okHttp的使用比较简单,通常需要首先初始化一个HttpClient,然后在每次发送请求的时候创建出一个request,并且将这个request包装成一个RealCall,RealCall就是真正执行请求的执行者,最后只需要调用call的execute方法同步执行或者异步执行就可以了。
示例代码如下:

class MainActivity : AppCompatActivity() {
    val client = OkHttpClient()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val url = "www.baidu.com"
        val request = Request.Builder().url(url).build()
        val call = client.newCall(request)
        //同步
        call.execute()
        //异步
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                TODO("Not yet implemented")
            }

            override fun onResponse(call: Call, response: Response) {
                TODO("Not yet implemented")
            }

        })
    }
}	

看上去OkHttp只有call和client两个组成,但是实际上client还包含了两个非常重要的类——用于分发request的dispatcher和用于拦截修改请求的Interceptors。

dispatcher

dispatcher流程
client中的dispacher主要负责分发和处理异步的request请求,首先来看看当一个异步的Call调用enqueue方法之后到底发生了什么:

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
    //判断是否重复执行
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
   	//transmitter发射器,看起来主要作用是监听一个call的发送生命周期
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
//#Dispatcher.class
void enqueue(AsyncCall call) {
	//加了同步锁
    synchronized (this) {
    //加入到等待队列
      readyAsyncCalls.add(call);
      if (!call.get().forWebSocket) {
      	//判断是否存在相同的Host,如果存在相同的host则复用之前的host计数器,
      	//这里主要是因为okHttp限制了同一host默认最多只能有5个请求
      	//这里的计数器采用了AtomicInteger
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }
private boolean promoteAndExecute() {
	... ...
    synchronized (this) {
      //遍历所有的准备队列,
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();
		//判断是在运行的call是否达到了最大值
        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        //判断同一host的call是否达到了最大值
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
        i.remove();
        //满足可执行条件,把call移动出来,计数器++,并且加入到executableCalls和runningAsyncCalls中
        asyncCall.callsPerHost().incrementAndGet();
        //executableCalls是指这一次会执行的call,可以看到每次调用promoteAndExecute都会执行一批次call
        executableCalls.add(asyncCall);
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

	//执行所有的executableCalls中的call
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      //executorService是一个懒汉式的单例线程池
      //核心线程数为0,空闲了60秒后,所有线程会被清空,
      //最大线程数无限制,由于运行队列有最大值,因此不需要限制线程数
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }
 //#AsyncCall.Class
 void executeOn(ExecutorService executorService) {
      assert (!Thread.holdsLock(client.dispatcher()));
      boolean success = false;
      try {
      	//开始执行
        executorService.execute(this);
        success = true;
      } catch (RejectedExecutionException e) {
        InterruptedIOException ioException = new InterruptedIOException("executor rejected");
        ioException.initCause(e);
        transmitter.noMoreExchanges(ioException);
        responseCallback.onFailure(RealCall.this, ioException);
      } finally {
        if (!success) {
        	//如果失败的则移除队列
          client.dispatcher().finished(this); // This call is no longer running!
        }
      }
    }

Interceptors

拦截器流程
从上文可以看到,call最后执行了execute方法,那么call具体的execute是什么样子的呢?

@Override protected void execute() {
	  ... ...
      transmitter.timeoutEnter();
      try {
        Response response = getResponseWithInterceptorChain();
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      } catch (IOException e) {
      	... ...
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

可以看到call调用了getResponseWithInterceptorChain方法,并且将返回的结果回调给了callback。
getResponseWithInterceptorChain里面添加了client中保存的自定义拦截器以及5个默认拦截器(这里采用了一个责任链的设计模式),对于每一个request请求首先会按照顺序过一遍每个拦截器,最后到达CallServerInterceptorCallServerInterceptor是真正和server发生请求的拦截器并且获取请求成功之后的response。获取到response之后,response又会倒序过一遍之前的所有拦截器。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      ... ...
     }
  }

这里不如来看看这5个原生的拦截器分别起了什么作用。

RetryAndFollowUpInterceptor

重试和

//最大重试次数
static final int MAX_FOLLOW_UPS = 20;

Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();
    int followUpCount = 0;
    while (true) {
        //从连接池中获取一个连接,如有可以相同请求可以复用连接则复用
        transmitter.prepareToConnect(request);
        //交给下一个拦截器
        Response response = realChain.proceed(request, transmitter, null);
        //判断是否需要重试或重定向,需要则返回新的Request
        Request followUp = followUpRequest(response, route);
        if (followUp == null) {
            return response;
        }
        RequestBody followUpBody = followUp.body();
        if (followUpBody != null && followUpBody.isOneShot()) {
            //如果RequestBody有值且只许被调用一次,直接返回response
            return response;
        }
        if (++followUpCount > MAX_FOLLOW_UPS) {
            //重试次数上限,结束
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
        //将新的请求赋值给request,继续循环
        request = followUp;
    }
}

BridgeInterceptor

桥接拦截器,总之就是处理request和response中的body和header,gzip也是在这里完成的。

Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    RequestBody body = userRequest.body();
    if (body != null) {
        requestBuilder.header("Content-Type", contentType.toString());
        //处理Content-Length、Transfer-Encoding
        //...
    }
    //处理Host、Connection、Accept-Encoding、Cookie、User-Agent、
    //...
    //放行,把处理好的新请求往下传递,得到Response
    Response networkResponse = chain.proceed(requestBuilder.build());
    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
	//处理新Response的Content-Encoding、Content-Length、Content-Type、gzip
    //返回新Response
    return responseBuilder.build();
}

CacheInterceptor

InternalCache cache;

Response intercept(Chain chain) throws IOException {
    //获取候选缓存
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    //创建缓存策略,根据策略返回需要发送的请求networkRequest,或者是缓存的cacheResponse
    CacheStrategy strategy = 
        new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    if (networkRequest == null && cacheResponse == null) {
        return new Response.Builder().code(504).xxx.build();
    }
    //如果不需要发送网络请求,直接返回缓存
    if (networkRequest == null) {
        return cacheResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse)).build();
    }
    //如果需要发送则把网络请求交给后面的拦截器处理
    Response networkResponse = chain.proceed(networkRequest);
    //处理完了之后如果缓存的cacheResponse不为空,并且server返回304缓存可用则更新并返回
    if (cacheResponse != null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {
            Response response = cacheResponse.newBuilder().xxx.build();
            //更新缓存,返回
            cache.update(cacheResponse, response);
            return response;
        }
    }
    //否则将网络response写入缓存
    Response response = networkResponse.newBuilder().xxx.build();
    cache.put(response);
    return response;
}

ConnectInterceptor

创建连接的拦截器

Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    Transmitter transmitter = realChain.transmitter();
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //创建一个交换器Exchange
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
    return realChain.proceed(request, transmitter, exchange);
}

CallServerInterceptor

Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();
    //写请求头
    exchange.writeRequestHeaders(request);
    Response.Builder responseBuilder = null;
    //处理请求体body...
    //读取响应头
    responseBuilder = exchange.readResponseHeaders(false);
    //构建响应
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    //读取响应体
    response = response.newBuilder()
        .body(exchange.openResponseBody(response))
        .build();
    return response;
}

缓存

上面提到了okHttp存在一个缓存拦截器,那么这个缓存拦截器是怎么实现的呢?

//CacheStrategy.java
//内部类工厂,生产CacheStrategy
static class Factory {
    //一些字段:servedDate、lastModified、expires、etag...
    Factory(long nowMillis, Request request, Response cacheResponse) {
        this.nowMillis = nowMillis;
        this.request = request;
        this.cacheResponse = cacheResponse;
        if (cacheResponse != null) {
            //解析cacheResponse,把参数赋值给自己的成员变量
            this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
            //...
            Headers headers = cacheResponse.headers();
            for (int i = 0, size = headers.size(); i < size; i++) {
                String fieldName = headers.name(i);
                String value = headers.value(i);
                if ("Date".equalsIgnoreCase(fieldName)) {
                    servedDate = HttpDate.parse(value);
                    servedDateString = value;
                } else if (xxx){
                    //...
                }
            }
        }
    }

    CacheStrategy get() {
        CacheStrategy candidate = getCandidate();
        if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
            //返回策略,交给拦截器
            return new CacheStrategy(null, null);
        }
        return candidate;
    }

    CacheStrategy getCandidate() {
        //根据header字段,得到各种策略,交给拦截器...
        return new CacheStrategy(xxx);
    }
}

Cache类中实现了InternalCache接口,这个接口将缓存写入到了磁盘里面,okHttp使用LRU算法来管理缓存。

//Cache.java
InternalCache internalCache = new InternalCache() {
    @Override public Response get(Request request) throws IOException {
        return Cache.this.get(request);//读取
    }

    @Override public CacheRequest put(Response response) throws IOException {
        return Cache.this.put(response);//写入
    }

    //...
};

Response get(Request request) {
    String key = key(request.url()); //键
    DiskLruCache.Snapshot snapshot; //缓存快照
    Entry entry;
    snapshot = cache.get(key); //cache是okhttp的DiskLruCache
    if (snapshot == null) {
        return null; //没缓存,直接返回
    }
    //快照得到输入流,用于创建缓存条目
    entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    //得到响应
    Response response = entry.response(snapshot);
    return response;
}

CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    if (!requestMethod.equals("GET")) {
        //不是get请求,不缓存
        return null;
    }
    //封装成日志条目
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    editor = cache.edit(key(response.request().url()));
    //写入缓存
    entry.writeTo(editor);
    return new CacheRequestImpl(editor);
}

连接池

之前说过RetryAndFollowUpInterceptor中通过Transmitter获取了一个connection,之后又在ConnectInterceptor中创建了一个交换器Exchange。这两者之间有什么关系呢?
Exchange内部包含了两个类ExchangeFinder以及ExchangeCodec
ExchangeFinder的职责是找到一个合适的连接和ExchangeCodec,而ExchangeCodec的职责是负责对请求和返回消息进行解码。
RetryAndFollowUpInterceptor中,Transmitter创建了一个exchangeFinder,并且把连接池传入,之后在ConnectInterceptor中又根据这个exchangeFinder,创建了一个Exchange,并且调用find方法获取合适的connection和ExchangeCodec。

public void prepareToConnect(Request request) {
	//复用旧的
    if (this.request != null) {
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; // Already ready.
      }
     ... ...
    //没有旧的就开始重新创建
    this.request = request;
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }
#Transmitter.Class
//创建一个新的Exchange
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if (exchange != null) {
        throw new IllegalStateException("cannot make a new request because the previous response "
            + "is still open: please call response.close()");
      }
    }
    //调用find方法找到合适链接和解码器
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    //创建新的Exchange
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }
#ExchangeFinder.Class
public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
      ... ...
    try {
    //找到一个合适的连接,并且根据这个连接创建出ExchangeCodec,ExchangeCodec中持有了这个Connection
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      return resultConnection.newCodec(client, chain);
    } 
    ... ...
  }

到这里Connection、Exchange和Transmitter的关系就大致明白了。
那么OkHttp又是如何管理连接的呢?
之前说过ExchangeFinder中会尝试获取一个connection,这里最终会调用到RealConnectionPool#transmitterAcquirePooledConnection方法:

boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
      @Nullable List<Route> routes, boolean requireMultiplexed) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      //transmitter要求多路复用,当前连接不能多路复用跳过
      if (requireMultiplexed && !connection.isMultiplexed()) continue;
      //当前连接不能够携带流分配地址。。。看不懂
      if (!connection.isEligible(address, routes)) continue;
      //获取当前连接,并且返回true
      transmitter.acquireConnectionNoEvents(connection);
      return true;
    }
    //没获取到
    return false;
  }

可以看到RealConnectionPool中维护了一个连接池,以及一个单个线程的线程池用于定期清除长期不用的连接:

public final class RealConnectionPool {
//后台线程用于清理过期的连接。 每个连接池最多运行一个线程。 线程池执行器允许池本身被垃圾收集。
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  private final Runnable cleanupRunnable = () -> {
  	... ...//执行具体的清除操作
  };
  //连接池
  private final Deque<RealConnection> connections = new ArrayDeque<>();//标志位,判断是否在清除
  boolean cleanupRunning;
  //put 操作,每次put的时候会去判断是否需要清除下连接
  //ExchangeFinder中获取connection的时候如果没有复用的就会新创建一个connection并且put到这里
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

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

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

相关文章

基于ubuntu的STM32嵌入式软件开发(四)——应用软件工程的修改、Makefile及编译脚本的编写

本文主要介绍基于标准库函数移植的STM32的应用软件工程的修改&#xff0c;主要涉及到文件内容修改、Makefile文件编写、编译脚本编写等内容&#xff0c;其中编译脚本是基于arm-none-eabi-gcc的交叉编译器撰写的。程序亲测可以正常编译&#xff0c;生成.bin和.hex的可烧录镜像文…

笔记--学习mini3d代码

主要是记录学习mini3d代码时&#xff0c;查的资料&#xff1b; 从github下载的代码&#xff1a; GitHub - skywind3000/mini3d: 3D Software Renderer in 700 Lines !!3D Software Renderer in 700 Lines !! Contribute to skywind3000/mini3d development by creating an a…

富文本QTextEdit

<1> QTextEdit支持富文本处理&#xff0c;即文档中可使用多种格式&#xff0c;如文字、图片、表格等… <2> 文档的光标主要基于QTextCursor类&#xff0c;文档的框架主要基于QTextDocument类。 <3> 一个富文本的文档结构主要分为几种元素&#xff1a;框架&am…

公司只有我一个测试人员...也没有朋友经验可以借鉴,我该怎么办?

近日看到一个帖子&#xff1a; 我所在的公司目前就我一个测试&#xff0c;我一个人对接开发&#xff0c;对接产品&#xff0c;公司也没什么流程&#xff0c;我不知道我该做什么&#xff0c;也没有前人经验可以借鉴&#xff0c;我该怎么办&#xff1f; 看到有很多刚刚步入测试行…

报名成人学历,还有没有必要申请学士学位?

很多同学在报名成人学历的时候并不重视学位证书&#xff0c;认为拿到毕业证就行了。 其实&#xff0c;学位证的重要性有时候真的不亚于毕业证。 别人要求必须双证&#xff0c;你一个毕业证就不顶事了。 下面我们就来了解下学位证的用处&#xff0c;以及三大成人学历提升方式&am…

微服务一 实用篇 - 3. Docker

《微服务一 实用篇 - 3. Docker》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《微服务一 实用篇 - 3. Docker》《微服务一 实用篇 - 3. Docker》1.初识Docker1.1.什么是Docker1.1.1.应用部署的环境问题1.1.2.Docker解决依赖兼容问题1.1.3.Docker解决操…

干货文稿|详解深度半监督学习

分享嘉宾 | 范越文稿整理 | William嘉宾介绍Introduction to Semi-Supervised Learning传统机器学习中的主流学习方法分为监督学习&#xff0c;无监督学习和半监督学习。这里存在一个是问题是为什么需要做半监督学习&#xff1f;首先是希望减少标注成本&#xff0c;因为目前可以…

软件测试自动化Java篇【Selenium+Junit 5】

文章目录Selenium环境部署自动化测试例子常见的元素操作窗口等待浏览器的操作弹窗选择器执行脚本文件上传浏览器参数Junit 5导入依赖Junit 4 和 Junit5 注解对比断言测试顺序参数化单参数多参数动态参数测试套件指定类来运行测试用例指定包名来运行包下测试用例Selenium 为什么…

【线程安全篇】

线程安全之原子性问题 x &#xff0c;在字节码文件中对应多个指令&#xff0c;多个线程在运行多个指令时&#xff0c;就存在原子性、可见性问题 赋值 多线程场景下&#xff0c;一个指令如果包含多个字节码指令&#xff0c;那么就不再是原子操作。因为赋值的同时&#xff0c…

智慧工地AI视频分析系统 opencv

智慧工地AI视频分析系统通过pythonopencv网络模型图像识别技术&#xff0c;智慧工地AI视频分析算法自动识别现场人员穿戴是否合规。本算法模型中用到opencv技术&#xff0c;OpenCV基于C实现&#xff0c;同时提供python, Ruby, Matlab等语言的接口。OpenCV-Python是OpenCV的Pyth…

研讨会回顾 | Perforce版本控制工具Helix Core入华十年,携手龙智赋能企业大规模研发

2023年2月28日&#xff0c;龙智联合全球领先的数字资产管理工具厂商Perforce共同举办Perforce on Tour网络研讨会&#xff0c;主题为“赋能‘大’研发&#xff0c;助力‘快’交付”。 作为Perforce Helix Core产品在中国地区的唯一授权合作伙伴&#xff0c;龙智董事长何明女士为…

六、GoF之工厂模式

设计模式&#xff1a;一种可以被重复利用的解决方案。 GoF&#xff08;Gang of Four&#xff09;&#xff0c;中文名——四人组。 《Design Patterns: Elements of Reusable Object-Oriented Software》&#xff08;即《设计模式》一书&#xff09;&#xff0c;1995年由 Eric…

Sidecar-详解 JuiceFS CSI Driver 新模式

近期发布的 JuiceFS CSI Driver v0.18 版本中&#xff0c;我们提供了一种全新的方式访问文件系统&#xff0c;即 JuiceFS 客户端以 Sidecar 方式运行于应用 Pod 中&#xff0c;且客户端与应用同生命周期。 这个全新的功能将帮助用户在 Serverless Kubernetes 环境中使用 Juice…

【Python每日一练】总目录(不断更新中...)

Python 2023.03 20230303 1. 两数之和 ★ 2. 组合总和 ★★ 3. 相同的树 ★★ 20230302 1. 字符串统计 2. 合并两个有序链表 3. 下一个排列 20230301 1. 只出现一次的数字 2. 以特殊格式处理连续增加的数字 3. 最短回文串 Python 2023.02 20230228 1. 螺旋矩阵 …

基于k3s部署KubeSphere

目录相关文档准备工作安装K3S安装KubeSphere相关文档 k3s官网&#xff1a;https://docs.k3s.io/zh/quick-start k3s所有版本查看&#xff1a;https://github.com/k3s-io/k3s/tags kubesphere文档&#xff1a;https://kubesphere.io/zh/docs/v3.3/quick-start/minimal-kubesp…

2023爱分析·RPA软件市场厂商评估报告:容智信息

目录 1. 研究范围定义 2. RPA软件市场分析 3. 厂商评估&#xff1a;容智信息 4. 入选证书 1. 研究范围定义 RPA即Robotic Process Automation&#xff08;机器人流程自动化&#xff09;&#xff0c;是一种通过模拟人与软件系统的交互过程&#xff0c;实现由软件机器人…

【python+selenium自动化测试实战项目】全面、完整、详细

今天整理一下实战项目的代码共大家学习。 不说废话&#xff0c;直接上项目 项目简介 项目名称&#xff1a;**公司电子零售会员系统 项目目的&#xff1a;实现电子零售会员系统项目自动化测试执行 项目版本&#xff1a;v1.0 项目目录 项目环境 本版 python 36 pip insat…

Linux开放的端口太多了?教你一招找出所有开放的端口,然后直接干掉!

基于服务器安全性维护的目的&#xff0c;查看所有开放的端口是通常采取的第一步&#xff0c;从中检查出可疑或者不必要的端口并将其关掉。关于查看开放的端口&#xff0c;方法不止一种&#xff0c;比如lsof 命令&#xff0c;还可以使用 ss 命令。 查看开放的端口 今天我们就介…

分布式缓存 Memcached Linux 系统安装

1.Memcached简介 Memcached是一个开源、高性能&#xff0c;将数据分布于内存中并使用key-value存储结构的缓存系统。它通过在内存中缓存数据来减少向数据库的频繁访问连接的次数&#xff0c;可以提高动态、数据库驱动之类网站的运行速度。 Memcached在使用是比较简单的&#…

Clip:学习笔记

Clip 文章目录Clip前言一、原理1.1 摘要1.2 引言1.3 方法1.4 实验1.4.1 zero-shot Transfer1.4.2 PROMPT ENGINEERING AND ENSEMBLING1.5 局限性二、总结前言 阅读论文&#xff1a; Learning Transferable Visual Models From Natural Language Supervision CLIP 论文逐段精读…