Android:OKHttp

news2025/2/13 21:13:15

特点

  • 支持HTTP2/SPDY
  • Socket自动选择最好路线,并支持自动重连
  • 拥有自动维护的Socket连接池,减少握手次数
  • 拥有队列线程池,轻松写并发
  • 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩)
  • 实现基于Headers的缓存策略

基本使用

同步请求

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
      .url(url)
      .build();
Response response = client.newCall(request).execute();
return response.body().string();

异步请求

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
      .url(url)
      .build();
client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e("DEBUG", "##### onFailure: ", e);
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d("DEBUG", "##### response: " + response.body().string());
            }
        });

源码分析

Builder

OkHttpClient client = new OkHttpClient();

public OkHttpClient() {
    this(new Builder());
}

请求流程

在这里插入图片描述

同步请求

client.newCall(request).execute();//RealCall的execute方法

@Override public Response execute() throws IOException {
  synchronized (this) {//说明请求只能被执行一次
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.timeoutEnter();
  transmitter.callStart();
  try {
    client.dispatcher().executed(this);//由dispatcher这个核心调度类将请求加入队列
    return getResponseWithInterceptorChain();//获取HTTP请求结果,并会进行一系列拦截操作
  } finally {
    client.dispatcher().finished(this);//执行完毕操作,将线程从同步线程队列中移除
  }
 }

由dispatcher这个核心调度类将请求加入队列
getResponseWithInterceptorChain获取HTTP请求结果,并会进行一系列拦截操作

synchronized void executed(RealCall call) {
  runningSyncCalls.add(call);
}

执行完毕操作,将线程从同步线程队列中移除

void finished(RealCall call) {
   finished(runningSyncCalls, call);
 }
private <T> void finished(Deque<T> calls, T call) {
  Runnable idleCallback;
  synchronized (this) {
    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    idleCallback = this.idleCallback;
  }
  //异步方法中调用
  boolean isRunning = promoteAndExecute();
  if (!isRunning && idleCallback != null) {
    idleCallback.run();
  }
}

异步请求

将AsyncCall对象加入readyAsyncCalss队列中等待执行

@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  transmitter.callStart();
  //将AsyncCall对象加入readyAsyncCalss队列中等待执行
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

AsyncCall是RealCall的内部类,并且是NamedRunnable线程类
getResponseWithInterceptorChain()一样食获取HTTP请求结果,并会进行一系列拦截操作
client.dispatcher().finished(this)和同步方法中调用类似,但是异步的流程则完全不同

@Override protected void execute() {
  boolean signalledCallback = false;
  transmitter.timeoutEnter();
  try {
    Response response = getResponseWithInterceptorChain();
    signalledCallback = true;
    responseCallback.onResponse(RealCall.this, response);
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}
void finished(AsyncCall call) {
  call.callsPerHost().decrementAndGet();
  finished(runningAsyncCalls, call);
}

private <T> void finished(Deque<T> calls, T call) {
  Runnable idleCallback;
  synchronized (this) {
    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    idleCallback = this.idleCallback;
  }
  //异步方法中调用
  boolean isRunning = promoteAndExecute();
  if (!isRunning && idleCallback != null) {
    idleCallback.run();
  }
}

会遍历异步等待线程队列,并对正在执行的异步线程队列进行最大请求size,以及每个host最大请求size进行检查。

把异步等待线程放到正在执行线程队列中,并在等待线程队列中删除该线程,这样就把等待线程变成正在执行线程。

private boolean promoteAndExecute() {
  assert (!Thread.holdsLock(this));
  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();
      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
      i.remove();
      asyncCall.callsPerHost().incrementAndGet();
      executableCalls.add(asyncCall);
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService());
  }
  return isRunning;
}

Dispatcher

Dispatcher在builder中完成初始化

  • private int maxRequests = 64
    maxRequests:最大请求并发请求数64

  • private int maxRequestsPerHost = 5
    maxRequestsPerHost:每个主机的最大请求数5

  • private @Nullable Runnable idleCallback;

  • private @Nullable ExecutorService executorService;
    executorService:线程池,懒汉模式创建

  • private final Deque readyAsyncCalls = new ArrayDeque<>();
    readyAsyncCalls:异步等待线程队列,按顺序执行

  • private final Deque runningAsyncCalls = new ArrayDeque<>();
    runningAsyncCalls:正在运行的异步线程队列,运行异步调用,包括尚未完成的已取消呼叫

  • private final Deque runningSyncCalls = new ArrayDeque<>()
    runningSyncCalls:正在运行的同步线程队列,运行同步调用,包括尚未完成的已取消呼叫

ExecutorService

在OKHttp中,设置了不设上限的线程,不保留最小线程,线程空闲时,最大存活时间为60s,保证I/O任务中高阻塞低占用的过程,不会长时间卡在阻塞上。并通过maxRequests和maxRequestsPerHost来控制并发最大请求数。

public synchronized ExecutorService executorService() {
  if (executorService == null) {
    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
        new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
  }
  return executorService;
}

拦截器

在这里插入图片描述

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);
    if (transmitter.isCanceled()) {
      closeQuietly(response);
      throw new IOException("Canceled");
    }
    return response;
  } catch (IOException e) {
    calledNoMoreExchanges = true;
    throw transmitter.noMoreExchanges(e);
  } finally {
    if (!calledNoMoreExchanges) {
      transmitter.noMoreExchanges(null);
    }
  }
}
  • RetryAndFollowUpInterceptor:负责失败重试以及重定向
  • BridgeInterceptor:负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应
  • CacheInterceptor:负责读取缓存直接返回、更新缓存
  • ConnectInterceptor:负责和服务器建立连接
  • CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据

责任链模式,通过Interceptor,把Request转换为Response,每个Interceptor都有各自的责任和逻辑。

加入自定义拦截器

interceptors.addAll(client.interceptors());
......
if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
  }

HTTP实现

OKHttp主要依靠ConnectIntercepter和CallServerIntercepter
ConnectIntercepter建立与服务器的连接
CallServerIntercepter发送请求和读取响应

流程如下:

  • 根据请求的URL,createAddress()创建一个Address
  • 检查Address和Routes,是否可以从ConnectionPool获取一个链接
  • 如果获取链接失败,就会进行下一个路由选择,并重新尝试从ConnectionPool获取一个链接,若重新获取失败则会重新创建一个链接
  • 获取链接后,会与服务器建立一个直接的Socket链接,使用TLS安全通道或直接TLS链接
  • 发送HTTP请求,并获取响应

ConnectInterceptor

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Request request = realChain.request();
  Transmitter transmitter = realChain.transmitter();
  // We need the network to satisfy this request. Possibly for validating a conditional GET.
  boolean doExtensiveHealthChecks = !request.method().equals("GET");
  Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
  return realChain.proceed(request, transmitter, exchange);
}

Exchange可以传输HTTP请求和响应,并管理连接和事件

/** Returns a new exchange to carry a new request and response. */
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()");
    }
  }
  ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
  Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
  ......
  }
}

find方法会最终执行ExchangeFinder的findConnection方法,在发送HTTP请求之前的逻辑,都是这个方法中实现。

findConnection会返回一个链接,优先已存在的链接,次之从链接池中取出,最后才是重新创建链接
流程和前面提到的一样

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      ......

      if (result == null) {
        //2.根据 Address 从连接池获取连接
        // Attempt to get a connection from the pool.
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else if (nextRouteToTry != null) {
          selectedRoute = nextRouteToTry;
          nextRouteToTry = null;
        } else if (retryCurrentRoute()) {
          selectedRoute = transmitter.connection.route();
        }
      }
    }
    ......
    // 3. 重新选择路由
    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }

      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
      
        // 3. 重新选择路由,创建新的 `RealConnection`
        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    ......
    // 4. 进行 Socket 连接
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      // Last attempt at connection coalescing, which only occurs if we attempted multiple
      // concurrent connections to the same host.
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // We lost the race! Close the connection we created and return the pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        //把连接放入连接池中
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    ......
    return result;
  }

HTTP的链接由result.connect完成
分为是否需要隧道链接
connectSocket连接socket,establishProtocol根据HTTP协议版本进行连接处理。

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener){
   if (protocol != null) throw new IllegalStateException("already connected");
   ......
   while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ......
      }
    }
    ......
}

ConnectSocket
使用 Okio,封装了Socket的读写操作, 建立连接后,就可以发送请求和获取响应。

private void connectSocket(int connectTimeout, int readTimeout, Call call,
   EventListener eventListener) throws IOException {
   ......
   try {
      //连接 socket
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }
   try {
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
}

CallServerInterceptor

CallServerInterceptor的intercept()方法里负责发送请求和获取响应。

具体操作都是通过Exchange来执行,Exchange通过各个功能模块再进行分发处理。
通过 Socket 发送 HTTP消息,会按照以下声明周期:

writeRequestHeaders发送 request Headers
如果有 request body,就通过 Sink 发送request body,然后关闭 Sink

readResponseHeaders获取 response Headers
通过Source读取 response body,然后关闭 Source

writeRequestHeaders

public void writeRequestHeaders(Request request) throws IOException {
    try {
      eventListener.requestHeadersStart(call);
      codec.writeRequestHeaders(request);
      eventListener.requestHeadersEnd(call, request);
    } catch (IOException e) {
      eventListener.requestFailed(call, e);
      trackFailure(e);
      throw e;
    }
  }
  
//实际执行的方法codec实现类Http1ExchangeCodec(前面根据HTTP协议版本选择)的writeRequest方法
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
  if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
  sink.writeUtf8(requestLine).writeUtf8("\r\n");
  for (int i = 0, size = headers.size(); i < size; i++) {
    sink.writeUtf8(headers.name(i))
        .writeUtf8(": ")
        .writeUtf8(headers.value(i))
        .writeUtf8("\r\n");
  }
  sink.writeUtf8("\r\n");
  state = STATE_OPEN_REQUEST_BODY;
}

readResponseHeaders

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
  if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
    throw new IllegalStateException("state: " + state);
  }
  try {
    StatusLine statusLine = StatusLine.parse(readHeaderLine());
    Response.Builder responseBuilder = new Response.Builder()
        .protocol(statusLine.protocol)
        .code(statusLine.code)
        .message(statusLine.message)//StatusLine解析HTTP版本信息
        .headers(readHeaders());//readHeaders()读取response header信息。
    if (expectContinue && statusLine.code == HTTP_CONTINUE) {
      return null;
    } else if (statusLine.code == HTTP_CONTINUE) {
      state = STATE_READ_RESPONSE_HEADERS;
      return responseBuilder;
    }
    state = STATE_OPEN_RESPONSE_BODY;
    return responseBuilder;
  } catch (EOFException e) {
    // Provide more context if the server ends the stream before sending a response.
    String address = "unknown";
    if (realConnection != null) {
      address = realConnection.route().address().url().redact();
    }
    throw new IOException("unexpected end of stream on "
        + address, e);
  }
}

response Body

if (forWebSocket && code == 101) {
  // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
  response = response.newBuilder()
      .body(Util.EMPTY_RESPONSE)
      .build();
} else {
  response = response.newBuilder()
      .body(exchange.openResponseBody(response))
      .build();
}

public ResponseBody openResponseBody(Response response) throws IOException {
  try {
    eventListener.responseBodyStart(call);
    String contentType = response.header("Content-Type");
    long contentLength = codec.reportedContentLength(response);
    Source rawSource = codec.openResponseBodySource(response);
    ResponseBodySource source = new ResponseBodySource(rawSource, contentLength);
    return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
  } catch (IOException e) {
    eventListener.responseFailed(call, e);
    trackFailure(e);
    throw e;
  }
}

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

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

相关文章

JDBC Apache—DBUtils 详解(通俗易懂)

目录 一、前言 二、Apache—DBUtils的引入 1.传统使用ResultSet的缺点 : 2.改进方法 : 3.改进方法的模拟实现 : 三、Apache—DBUtils的使用 1.基本介绍 : 2.准备工作 : 3.DBUtils查询(DQL) : 4.query方法源码分析 : 5.DBUtils处理(DML) : 四、总结 一、前言 第六节…

Web3下的去中心化契约

随着Web3的兴起&#xff0c;智能合约成为了这一新兴领域中最为重要的概念之一。智能合约是一种在区块链上执行的可编程代码&#xff0c;其作用类似于传统世界中的合约&#xff0c;但具有更多的灵活性和安全性。本文将介绍智能合约的基本概念、工作原理以及在Web3下的应用场景。…

如何提取视频里面的音频?简单三个方法即可完成!

分享3个简单易上手的视频提取音频方法&#xff0c;这些方法可以帮助你单独提取保存视频中的人物对话音频内容和背景音乐&#xff0c;并且提取成功的音频文件还可用于其他视频创作。 方法一&#xff1a;PR提取音频 Adobe Premiere Pro&#xff08;简称PR&#xff09;不仅可以用…

番外篇2 离线服务器 环境安装与配置

&#xff08;离线远程服务器旧版torch的卸载与安装问题&#xff09; Step4: 查看自己是否已经成功安装了Anaconda,输入此命令conda --version -------------------------------------------------------------------------------------------------------- Step1:离线创建con…

为什么浏览器突然打不开网页了?

苏生不惑第433 篇原创文章&#xff0c;将本公众号设为星标&#xff0c;第一时间看最新文章。 前几天写了什么是cookie总有人问我 Cookie 是什么&#xff1f;&#xff0c;说到Charles代理工具&#xff0c;但是为什么关了它就打不开网页呢&#xff1f;今天来说说这个。 Charles可…

企企通联合创始人兼总架构师杨华:剖析SRM顾问长期主义项目实践

近日&#xff0c;国产大飞机C919商业首飞成功引起广泛关注&#xff0c;此后&#xff0c;我们的出行选择中新增了一项“自己国家的大飞机”&#xff0c;给国人带来了更多的期待和自豪。 走难而正确的路&#xff0c;国产大飞机C919从项目立项到“一飞冲天”&#xff0c;花了十六年…

激活函数ReLU和SiLU的区别

文章目录 前言ReLU&#xff08;Rectified Linear Unit&#xff09;Leaky ReLUFReLU&#xff08;Flatten ReLU&#xff09;SiLU&#xff08;Sigmoid Linear Unit&#xff09;总结 前言 在这里&#xff0c;我就简单写一下两个激活函数的概念以及区别&#xff0c;详细的过程可以看…

2023年新风口,抖音的产业带服务商招募?怎么开通?

抖音电商致力于成为用户发现并获得优价好物的首选平台。众多抖音创作者通过短视频/直播等丰富的内容形式&#xff0c;给用户提供更个性化、更生动、更高效的消费体验。抖音电商积极引入优质合作伙伴&#xff0c;为品牌发展、商家变现提供多元的服务。 抖音产业带服务商招募区域…

操作系统常识

4.进程同步 1.什么是临界区&#xff1f;什么是临界资源 在计算机系统中&#xff0c;临界资源指的是被多个并发执行的线程或进程共享访问的某个资源&#xff0c;如共享内存区、共享文件等。 临界区指的是访问临界资源的那部分代码片段&#xff0c;它是一段需要保护的代码区域…

项目管理工具究竟能否提升效率?看看它们的作用和优势

随着各种类型的项目不断涌现&#xff0c;项目管理工具在现代社会变得越来越重要。作为一个项目经理&#xff0c;需要确保项目在时间和预算方面得到控制&#xff0c;并且达成预期的效果。在这个过程中&#xff0c;项目管理工具可以带来很多帮助。 首先 项目管理工具可以为项目经…

推动绿色计算 共迎绿色未来|2023开放原子全球开源峰会绿色基础设施技术分论坛圆满收官

6 月 11 日&#xff0c;2023 开放原子全球开源峰会绿色基础设施技术分论坛圆满举行。蚂蚁集团 4 位专家带来了蚂蚁在探索打磨“绿色计算”实践中的核心技术领域一线观察。 蚂蚁集团高级技术专家、数据中间件负责人李玉明 李玉明分享了《开源分布式事务框架 Seata 以及其在金融…

电商--抢购架构总结

文章目录 背景业务流程业务难点技术难点技术方案技术方向具体落地客户端流控网关流控容器流控后端接口流控数据库流控 流控总结优化读取加速异步化流程处理系统扩容 压测监控 总结参考文献 背景 这是个在做NFT电商项目时遇到的场景&#xff0c;要求运营可以商家某个系列的NFT商…

顺序栈与链栈

简介 栈和队列是两种重要的线性结构。从数据结构角度看&#xff0c; 栈和队列也是线性表&#xff0c; 其特殊性在于栈和队列的基本操作是线性表操作的子集&#xff0c; 它们是操作受限的线性表。 栈 (stack) 是限定仅在表尾进行插入或删除操作的线性表。 因此&#xff0c; 对…

计算字母出现次数【存在括号计算】

计算字母出现次数【存在括号计算】 此代码考虑到了本问题的大多可能情况&#xff0c;闲话少述&#xff0c;代码中的注释很丰富。 代码绝对可以解决你的问题&#xff01; 不行你就评论&#xff0c;回复速度超快 作者时间YaoChongChong2023年6月14日10&#xff1a;40 Descript…

T8151B T8310 T8311罗克韦尔自动化可信通信接口

​ T8151B T8310 T8311罗克韦尔自动化可信通信接口 T8151B T8310 T8311罗克韦尔自动化可信通信接口 DCS控制器正反作用怎么判断&#xff1f; dcs控制器的正反作用可以在工程师站更改&#xff0c;比如中控系统那就在工程师站的操作界面把控制系统上的调节阀位号点开就会有正反作…

【华为云分布式消息服务RocketMQ】

MD[华为云分布式消息服务RocketMQ] 华为云分布式消息服务RocketMQ,使用指南 说明1&#xff1a;华为云rocketmq默认是集群4.8版本&#xff0c;而非单机版。 说明2&#xff1a;华为云rocketmq兼容性较好&#xff0c;一般不需要进行SDK改造。 1.创建/购买分布式消息服务RocketM…

水文水动力模型在城市内涝、城市排水、海绵城市规划设计中教程

详情点击链接&#xff1a;水文水动力模型在城市内涝、城市排水、海绵城市规划设计中应用教程 一&#xff0c;CAD、GIS水力建模过程 1.1复杂城市排水管网系统快速建模&#xff1a;通过标准化的步骤&#xff0c;利用CAD数据、GIS数据建立SWMM模型。在建模的不同阶段发挥不同软…

生成AI(三)—创建自己的MidJorney

背景&#xff1a;MidJorney是面向互联网的图像AIGC产品&#xff0c;在政企内部&#xff0c;存在大量需求训练内部的知识作为自己的AIGC工具。基本需求是信息安全考虑&#xff0c;合规考虑。 目标&#xff1a;通过自准备的数据训练MidJorney同类模型&#xff0c;成为私有化部署…

【基于容器的部署、扩展和管理】3.9 云原生容器的安全性和合规性

往期回顾&#xff1a; 第一章&#xff1a;【云原生概念和技术】 第二章&#xff1a;【容器化应用程序设计和开发】 第三章&#xff1a;【3.1 容器编排系统和Kubernetes集群的构建】 第三章&#xff1a;【3.2 基于容器的应用程序部署和升级】 第三章&#xff1a;【3.3 自动…

关于Android的帧动画,补间动画,属性动画的使用和总结。(附源码)

说明&#xff1a;内容有点多&#xff0c;可以分块阅读&#xff0c;后续可能会拆分为三讲 一. Android的动画总结 一 . 帧动画 帧动画其实就是通过连续播放图片来模拟动画效果 以下是俩种实现方式&#xff1a; 1. xml文件的方式 首先在drawable下建立animation_lufi.xml <?…