当前位置: 首页 > news >正文

Android-okhttp详解

目录

一,介绍

二,简单使用

三,流程分析

四,分发器

 五,拦截器

 5.1 重试及重定向拦截器

5.1.1 重试

5.1.2 重定向

5.2 桥接拦截器

5.3 缓存拦截器

5.4 连接拦截器

5.5 请求服务器拦截器


一,介绍

OkHttp是当下Android使用最频繁的网络请求框架,由Square公司开源。Google在Android4.4以后开始将源码中 的HttpURLConnection底层实现替换为OKHttp,同时现在流行的Retrofit框架底层同样是使用OKHttp的。

okhttp的优点是:

1,支持Http1、Http2、Quic以及WebSocket

2,连接池复用底层TCP(Socket),减少请求延时

3,无缝的支持GZIP减少数据流量

4,缓存响应数据减少重复的网络请求

5,请求失败自动重试主机的其他ip,自动重定向

二,简单使用

首先需要添加依赖:

implementation 'com.squareup.okhttp3:okhttp:3.14.7'
//Okio库 是对Java.io和java.nio的补充,以便能够更加方便,快速的访问、存储和处理你的数据。OkHttp的底层使用该库作为支持。
implementation 'com.squareup.okio:okio:1.17.5'

然后在清单文件中添加需要的权限:网络权限和读写权限都是必不可少的

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

先来看同步请求:

public String url ="https://www.wanandroid.com/article/list/0/json";
private static final String TAG = Module1MainActivity.class.getName();/*** 同步请求* */
private void syncRequest() throws IOException {//创建OkHttpClientOkHttpClient okHttpClient =new OkHttpClient();//创建request 并将请求url传进去 设置为get请求方式Request request =new Request.Builder().url(url).get().build();//获得请求的call对象Call call = okHttpClient.newCall(request);//执行同步请求Response response = call.execute();//获得响应体ResponseBody body = response.body();//输出响应体Log.d(TAG,body.string());
}

然后异步请求:

/*** 异步请求* */
private void asyncRequest(){//创建OkHttpClientOkHttpClient okHttpClient =new OkHttpClient();//创建request 并将请求url传进去 设置为get请求方式Request request =new Request.Builder().url(url).get().build();//获得请求的call对象Call call = okHttpClient.newCall(request);//执行异步请求call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {Log.e(TAG,"请求失败:"+e.getMessage());}@Overridepublic void onResponse(Call call, Response response) throws IOException {ResponseBody body = response.body();String string = body.string();byte[] bytes = body.bytes();InputStream inputStream = body.byteStream();Log.e(TAG,"请求成功:"+string);}});
}

三,流程分析

OkHttp请求过程中主要用到OkHttpClient、Request、Call、Dispatcher, Response以及拦截器这几个类,他们之间的关系主要如下图:

下面我们来看一下创建Call对象的源码:

//获得请求的call对象
Call call = okHttpClient.newCall(request);

在OkhttpClient中: 

@Override public Call newCall(Request request) {return RealCall.newRealCall(this, request, false /* for web socket */);
}

接着走到RealCall的newRealCall方法:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {// Safely publish the Call instance to the EventListener.RealCall call = new RealCall(client, originalRequest, forWebSocket);call.transmitter = new Transmitter(client, call);return call;
}

这里会把OkhttpClient对象,Request对象传入到RealCall中,并创建RealCall对象。

同步请求会执行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);return getResponseWithInterceptorChain();} finally {client.dispatcher().finished(this);}
}

关键看这行代码:

client.dispatcher().executed(this);

这里会直接执行分发器的executed方法,然后调用责任链模式的拦截器方法:

return getResponseWithInterceptorChain();

异步请求会执行RealCall的enqueue方法:

@Override public void enqueue(Callback responseCallback) {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed");executed = true;}transmitter.callStart();client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

这里只是调用了分发器的enqueue方法。

四,分发器

分发器Dispatcher就是来调配请求任务的,内部包含一个线程池,一个异步请求等待队列readyAsyncCalls,一个异步请求正在执行队列runningAsyncCalls,一个同步请求正在执行队列runningSyncCalls

public final class Dispatcher {//异步请求同时存在的最大请求private int maxRequests = 64;//异步请求同一域名同时存在的最大请求private int maxRequestsPerHost = 5;//闲置任务(没有请求时可执行一些任务,由使用者设置)private @Nullable Runnable idleCallback;//异步请求使用的线程池private @Nullable ExecutorService executorService;//异步请求等待执行队列private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();//异步请求正在执行队列private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();//同步请求正在执行队列private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();}

那我们接着来看上面的同步请求执行到分发器的executed方法是怎么做的:

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

这里只是将call加入到了同步请求正在执行队列,因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。

然后来看看异步请求的enqueue方法:

void enqueue(AsyncCall call) {synchronized (this) {//加入到异步请求准备队列readyAsyncCalls.add(call);// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to// the same host.if (!call.get().forWebSocket) {AsyncCall existingCall = findExistingCallWithHost(call.host());if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);}}promoteAndExecute();
}

首先将call加入到了异步请求准备队列,然后执行了promoteAndExecute();

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;
}

这里面的逻辑是当正在执行的任务未超过最大限制64,同时runningCallsForHost(call) < maxRequestsPerHost同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。加入正在执行队列的任务直接执行,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。所以会执行asyncCall.executeOn(executorService());方法:

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!}}
}

这里不管是执行请求成功还是失败,都会走到finally里面的分发器的finished方法:

 //异步请求调用
void finished(AsyncCall call) {finished(runningAsyncCalls, call, true);}//同步请求调用
void finished(RealCall call) {finished(runningSyncCalls, call, false);}private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {int runningCallsCount;Runnable idleCallback;synchronized (this) {//不管异步还是同步,执行完后都要从队列移除(runningSyncCalls/runningAsyncCalls)if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");if (promoteCalls) promoteCalls();//异步任务和同步任务正在执行的和runningCallsCount = runningCallsCount();idleCallback = this.idleCallback;}// 没有任务执行执行闲置任务if (runningCallsCount == 0 && idleCallback != null) {idleCallback.run();}}

只有异步任务才会存在限制与等待,所以在执行完了移除正在执行队列中的元素后,异步任务结束会 执行promoteCalls():

private void promoteCalls() {//如果任务满了直接返回if (runningAsyncCalls.size() >= maxRequests) return; //没有等待执行的任务,返回if (readyAsyncCalls.isEmpty()) return; //遍历等待执行队列for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {AsyncCall call = i.next();//等待任务想要执行,还需要满足:这个等待任务请求的Host不能已经存在5个了if (runningCallsForHost(call) < maxRequestsPerHost) {i.remove();runningAsyncCalls.add(call);executorService().execute(call);}if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.}}

在满足条件下,会把等待队列中的任务移动到runningAsyncCalls并交给线程池执行。

上面的异步请求流程可以总结为下图:

需要注意的是,okhttp的线程池的等待队列采用的是SynchronousQueue,这样就避免了任务的等待问题,所以它是无等待,最大并发的。线程池这里不详细讲解,可以查看文章Android 多线程并发详解_android多线程并发处理-CSDN博客

Android多线程讲解二_android线程专题讲解-CSDN博客 

 五,拦截器

Okhttp的拦截器除了我们自定义的拦截器之外,主要有五大拦截器:

1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后 ,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。

2、桥接拦截器:在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的 行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。 4、连接拦截器:在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后 不进行额外的处理。

5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

 Okhttp的拦截器主要采用了责任链的设计模式,关于责任链设计模式,请查考文章Android设计模式--责任链模式_android 责任链模式-CSDN博客

 其流程如下:

 5.1 重试及重定向拦截器

RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向。

5.1.1 重试

请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。

RouteException:

 catch (RouteException e) {//路由异常,连接未成功,请求还没发出去if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {throw e.getLastConnectException();}releaseConnection = false;continue;} 

IOException:

catch (IOException e) {//请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)// HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是trueboolean requestSendStarted = !(e instanceof ConnectionShutdownException);if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;releaseConnection = false;continue;}

两个异常都是根据recover 方法判断是否能够进行重试,如果返回true,则表示允许重试。

private boolean recover(IOException e, StreamAllocation streamAllocation,boolean requestSendStarted, Request userRequest) {streamAllocation.streamFailed(e);//1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试if (!client.retryOnConnectionFailure()) return false;//2、如果是RouteException,不用管这个条件,// 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)return false;//3、不是属于重试的异常 不重试if (!isRecoverable(e, requestSendStarted)) return false;//4、没有可以用来连接的路由路线 不重试if (!streamAllocation.hasMoreRoutes()) return false;return true;}

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断:

private boolean isRecoverable(IOException e, boolean requestSendStarted) {// 出现协议异常,不能重试
if (e instanceof ProtocolException) {return false;}// 如果不是超时异常,不能重试
if (e instanceof InterruptedIOException) {return e instanceof SocketTimeoutException && !requestSendStarted;}// SSL握手异常中,证书出现问题,不能重试
if (e instanceof SSLHandshakeException) {if (e.getCause() instanceof CertificateException) {return false;}}// SSL握手未授权异常 不能重试
if (e instanceof SSLPeerUnverifiedException) {return false;}return true;}

5.1.2 重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重 定向的判断。重定向的判断位于 followUpRequest 方法:

private Request followUpRequest(Response userResponse) throws IOException {if (userResponse == null) throw new IllegalStateException();Connection connection = streamAllocation.connection();Route route = connection != null? connection.route(): null;int responseCode = userResponse.code();final String method = userResponse.request().method();switch (responseCode) {// 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权
case HTTP_PROXY_AUTH:Proxy selectedProxy = route != null
享学课堂? route.proxy(): client.proxy();if (selectedProxy.type() != Proxy.Type.HTTP) {throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using 
proxy");}return client.proxyAuthenticator().authenticate(route, userResponse);// 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” case HTTP_UNAUTHORIZED:return client.authenticator().authenticate(route, userResponse);// 308 永久重定向 // 307 临时重定向case HTTP_PERM_REDIRECT:case HTTP_TEMP_REDIRECT:// 如果请求方式不是GET或者HEAD,框架不会自动重定向请求if (!method.equals("GET") && !method.equals("HEAD")) {return null;}// 300 301 302 303 case HTTP_MULT_CHOICE:case HTTP_MOVED_PERM:case HTTP_MOVED_TEMP:case HTTP_SEE_OTHER:// 如果用户不允许重定向,那就返回nullif (!client.followRedirects()) return null;// 从响应头取出location String location = userResponse.header("Location");if (location == null) return null;// 根据location 配置新的请求 urlHttpUrl url = userResponse.request().url().resolve(location);// 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向if (url == null) return null;// 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());if (!sameScheme && !client.followSslRedirects()) return null;Request.Builder requestBuilder = userResponse.request().newBuilder();/***  重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,*  即只有 PROPFIND 请求才能有请求体*///请求不是get与headif (HttpMethod.permitsRequestBody(method)) {final boolean maintainBody = HttpMethod.redirectsWithBody(method);// 除了 PROPFIND 请求之外都改成GET请求if (HttpMethod.redirectsToGet(method)) {requestBuilder.method("GET", null);} else {RequestBody requestBody = maintainBody ? userResponse.request().body() : null;requestBuilder.method(method, requestBody);}// 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉if (!maintainBody) {
享学课堂requestBuilder.removeHeader("Transfer-Encoding");requestBuilder.removeHeader("Content-Length");requestBuilder.removeHeader("Content-Type");}}// 在跨主机重定向时,删除身份验证请求头if (!sameConnection(userResponse, url)) {requestBuilder.removeHeader("Authorization");}return requestBuilder.url(url).build();// 408 客户端请求超时 case HTTP_CLIENT_TIMEOUT:// 408 算是连接失败了,所以判断用户是不是允许重试if (!client.retryOnConnectionFailure()) {return null;}// UnrepeatableRequestBody实际并没发现有其他地方用到if (userResponse.request().body() instanceof UnrepeatableRequestBody) {return null;}// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求
了if (userResponse.priorResponse() != null&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {return null;}// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。if (retryAfter(userResponse, 0) > 0) {return null;}return userResponse.request();// 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求case HTTP_UNAVAILABLE:if (userResponse.priorResponse() != null&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {return null;}if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {return userResponse.request();}return null;default:return null;}}

如果此方法返回空,那就表 示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是, 我们的 followup 在拦截器中定义的最大次数为20次。

5.2 桥接拦截器

BridgeInterceptor ,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设 置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。

补全请求头:

 在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的

2、如果使用gzip返回的数据,则使用 GzipSource 包装便于解析。

5.3 缓存拦截器

CacheInterceptor,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存在Get请求的缓存)

步骤为:

1、从缓存中获得对应请求的响应缓存

2、创建CacheStrategy ,创建时会判断是否能够使用缓存,在CacheStrategy 中存在两个成员: networkRequest 与cacheResponse

 3、交给下一个责任链继续处理

4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过CacheStrategy判断

总结:

1、如果从缓存获取的Response是null,那就需要使用网络请求获取响应;

2、如果是Https请求,但是又丢失了 握手信息,那也不能使用缓存,需要进行网络请求;

3、如果判断响应码不能缓存且响应头有no-store标识,那 就需要进行网络请求;

4、如果请求头有no-cache标识或者有If-Modified-Since/If-None-Match,那么需要进行 网络请求; 5、如果响应头没有no-cache标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行 网络请求;

6、如果缓存过期了,判断响应头是否设置Etag/Last-Modified/Date,没有那就直接使用网络请求否 则需要考虑服务器返回304; 并且,只要需要进行网络请求,请求头中就不能包含only-if-cached,否则框架直接返回504!

缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则 下一个拦截器为 ConnectInterceptor

5.4 连接拦截器

ConnectInterceptor ,打开与目标服务器的连接,并执行下一个拦截器。

源码如下:

/*** Opens a connection to the target server and proceeds to the next interceptor.*/
public final class ConnectInterceptor implements Interceptor {public final OkHttpClient client;public ConnectInterceptor(OkHttpClient client) {this.client = client;}@Overridepublic Response intercept(Chain chain) throws IOException {RealInterceptorChain realChain = (RealInterceptorChain) chain;Request request = realChain.request();StreamAllocation streamAllocation = realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.boolean doExtensiveHealthChecks = !request.method().equals("GET");HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);RealConnection connection = streamAllocation.connection();return realChain.proceed(request, streamAllocation, httpCodec, connection);}
}

首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在这里。

当一个请求发出,需要建立连接,连接建立后需要使用流用来读写数据 ,而这个StreamAllocation就是协调请 求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。 这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了 输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。

StreamAllocation 中简单来说就是维护连接: RealConnection ——封装了Socket与一个Socket连接池。

这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。

5.5 请求服务器拦截器

CallServerInterceptor,利用HttpCodec发出请求到服务器并且解析生成Response。

这个拦截器的主要作用就是完成HTTP协议报文的封装与解析

相关文章:

Android-okhttp详解

目录 一&#xff0c;介绍 二&#xff0c;简单使用 三&#xff0c;流程分析 四&#xff0c;分发器 五&#xff0c;拦截器 5.1 重试及重定向拦截器 5.1.1 重试 5.1.2 重定向 5.2 桥接拦截器 5.3 缓存拦截器 5.4 连接拦截器 5.5 请求服务器拦截器 一&#xff0c;介绍 OkHttp是当下…...

CNN-BiLSTM卷积双向长短期记忆神经网络时间序列预测(Matlab完整源码和数据)

CNN-BiLSTM卷积双向长短期记忆神经网络时间序列预测&#xff08;Matlab完整源码和数据&#xff09; 目录 CNN-BiLSTM卷积双向长短期记忆神经网络时间序列预测&#xff08;Matlab完整源码和数据&#xff09;预测效果基本介绍 CNN-BiLSTM卷积双向长短期记忆神经网络时间序列预测一…...

数字图像处理:实验七

uu们&#xff01;这是我们目前数字图像系列的最后一张&#xff0c;之后有关人工智能结合的数字图像处理咸鱼哥正在学习和创作中&#xff0c;所以还请大家给咸鱼哥点时间&#xff0c;同时也提前预祝大家2025年新春快乐&#xff01;&#xff08;咸鱼哥真诚的祝愿每一个人&#xf…...

Excel分区间统计分析(等步长、不等步长、多维度)

在数据分析过程中&#xff0c;可能会需要统计不同数据区间的人数、某个数据区间的平均值或者进行分组区间统计&#xff0c;本文从excel函数到数据透视表的方法&#xff0c;从简单需求到复杂需求&#xff0c;采用不同的方法进行讲解&#xff0c;尤其是通过数据透视表的强大功能大…...

QWindow类使用介绍与代码演示

深入浅出C++ Qt开发技术专栏 https://blog.csdn.net/yao_hou/category_9276099.html?spm=1001.2014.3001.5482 文章目录 QWindow主要功能和特性常用的函数示例代码适用场景QWindow父类QSurface`QSurface` 类概述主要功能和特性常用的函数相关的子类示例代码`QSurface` 的实际应…...

OpenCV图像显示imshow()函数——详解

OpenCV图像显示imshow()函数——详解 本文目录&#xff1a; 零、时光宝盒 一、OpenCV 图像显示使用imshow()函数语法 二、imshow()显示图片时发生图片显示不全的解决方法 解决办法&#xff08;1&#xff09; 解决办法&#xff08;2&#xff09; 总结 三、imshow()图像显…...

Oracle 12c 中的 CDB和PDB的启动和关闭

一、简介 Oracle 12c引入了多租户架构&#xff0c;允许一个容器数据库&#xff08;Container Database, CDB&#xff09;托管多个独立的可插拔数据库&#xff08;Pluggable Database, PDB&#xff09;。本文档旨在详细描述如何启动和关闭CDB及PDB。 二、容器数据库 (CDB) 2.1…...

二次封装的方法

二次封装 我们开发中经常需要封装一些第三方组件&#xff0c;那么父组件应该怎么传值&#xff0c;怎么调用封装好的组件原有的属性、插槽、方法&#xff0c;一个个调用虽然可行&#xff0c;但十分麻烦&#xff0c;我们一起来看更简便的方法。 二次封装组件&#xff0c;属性怎…...

【BQ3568HM开发板】如何在OpenHarmony上通过校园网的上网认证

引言 前面已经对BQ3568HM开发板进行了初步测试&#xff0c;后面我要实现MQTT的工作&#xff0c;但是遇到一个问题&#xff0c;就是开发板无法通过校园网的认证操作。未认证的话会&#xff0c;学校使用的深澜软件系统会屏蔽所有除了认证用的流量。好在我们学校使用的认证系统和…...

安装Ubuntu22.04

1.引用教程 如何安装Ubuntu Server 22.04 LTS_ubuntu22.04 server-CSDN博客 2.空间分配 要使用 docker 比较多所以分别的 docker 空间大...

Charles 4.6.7 浏览器网络调试指南:流量过滤与分析(六)

1. 概述 在网络调试和优化过程中&#xff0c;Charles 不仅可以实现简单的网络抓包操作&#xff0c;还支持更高级的抓包技巧和流量分析功能。这些功能能够帮助开发者深入挖掘网络请求的细节&#xff0c;为复杂问题提供有效的解决方案。本文将重点讲解 Charles 的过滤规则、自定…...

浅拷贝(Shallow Copy)和深拷贝(Deep Copy)

浅拷贝&#xff08;Shallow Copy&#xff09;和深拷贝&#xff08;Deep Copy&#xff09;是对象复制的两种方式&#xff0c;它们在处理对象内部引用类型成员时有不同的行为。下面我将详细解释这两种拷贝的区别&#xff0c;并提供具体的 Java 示例代码。 浅拷贝&#xff08;Shal…...

解决双系统引导问题:Ubuntu 启动时不显示 Windows 选项的处理方法

方法 1&#xff1a;检查 GRUB 引导菜单是否隐藏 启动进入 Ubuntu 系统。打开终端&#xff0c;输入以下命令编辑 GRUB 配置文件&#xff1a;sudo nano /etc/default/grub检查以下配置项&#xff1a; GRUB_TIMEOUT0&#xff1a;如果是 0&#xff0c;将其改为一个较大的值&#x…...

宏_wps_宏修改word中所有excel表格的格式_设置字体对齐格式_删除空行等

需求&#xff1a; 将word中所有excel表格的格式进行统一化&#xff0c;修改其中的数字类型为“宋体&#xff0c; 五号&#xff0c;右对齐&#xff0c; 不加粗&#xff0c;不倾斜”&#xff0c;其中的中文为“宋体&#xff0c; 五号&#xff0c; 不加粗&#xff0c;不倾斜” 数…...

行政办公管理系统的需求设计和实现

前言&#xff1a; 总裁办/综合部&#xff0c;作为综合行政管理部门服务于整个公司&#xff0c;工作职责包含从最基础的行政综合到协调督办、对外政务、品牌建设等等&#xff0c;工作量繁多而且琐碎。如何通过信息化来实现标准化和常态化的管理手段&#xff0c;确保总裁办的各项…...

996引擎 - NPC-动态创建NPC

996引擎 - NPC-动态创建NPC 创建脚本服务端脚本客户端脚本参考资料有个小问题,创建NPC时没有控制朝向的参数。所以。。。自己考虑怎么找补吧。 创建脚本 服务端脚本 Mir200\Envir\Market_Def\test\test001-3.lua -- NPC入口函数 function main(player)-- 获取玩家的用户名…...

HTML<hgroup>标签

例子&#xff1a; 使用hgroup元素标记标题和段落是相关的&#xff1a; <hgroup> <h2>Norway</h2> <p>The land with the midnight sun.</p> </hgroup> 定义和用法&#xff1a; 标签<hgroup>用于包围标题和一个或多个<p&g…...

GIS 中的 SQLAlchemy:空间数据与数据库之间的桥梁

利用 SQLAlchemy 在现代应用程序中无缝集成地理空间数据导言 地理信息系统&#xff08;GIS&#xff09;在管理城市规划、环境监测和导航系统等各种应用的空间数据方面发挥着至关重要的作用。虽然 PostGIS 或 SpatiaLite 等专业地理空间数据库在处理空间数据方面非常出色&#…...

机试题——最小矩阵宽度

题目描述 给定一个矩阵&#xff0c;包含 N * M 个整数&#xff0c;和一个包含 K 个整数的数组。 现在要求在这个矩阵中找一个宽度最小的子矩阵&#xff0c;要求子矩阵包含数组中所有的整数。 输入描述 第一行输入两个正整数 N&#xff0c;M&#xff0c;表示矩阵大小。 接下…...

【C++图论】1761. 一个图中连通三元组的最小度数|2005

本文涉及知识点 C图论 LeetCode1761. 一个图中连通三元组的最小度数 给你一个无向图&#xff0c;整数 n 表示图中节点的数目&#xff0c;edges 数组表示图中的边&#xff0c;其中 edges[i] [ui, vi] &#xff0c;表示 ui 和 vi 之间有一条无向边。 一个 连通三元组 指的是 …...

【力扣:新动计划,编程入门 —— 题解 ③】

—— 25.1.26 231. 2 的幂 给你一个整数 n&#xff0c;请你判断该整数是否是 2 的幂次方。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果存在一个整数 x 使得 n 2x &#xff0c;则认为 n 是 2 的幂次方。 示例 1&#xff1a; 输入&#xff1a;…...

「全网最细 + 实战源码案例」设计模式——工厂方法模式

核心思想 简单工厂模式是一种创建者模式&#xff0c;它通过一个工厂类负责创建不同类型的对象&#xff0c;根据传入的参数决定实例化的具体类&#xff0c;也被称为“静态工厂方法”模式&#xff0c;因为工厂方法通常是静态的。 结构 1. 工厂类&#xff1a; 提供一个静态方法…...

SpringBoot使用MockMVC通过http请求controller控制器调用测试

说明 在Spring Boot中编写测试控制器调用是一个常见的需求,通常使用Spring的测试框架来完成。Spring Boot提供了多种方式来测试控制器,包括使用MockMvc进行模拟HTTP请求和响应的测试。 基本示例 1. 创建Spring Boot项目 首先,确保你已经创建了一个Spring Boot项目。如果…...

电子应用设计方案105:智能家庭AI拖把系统设计

智能家庭 AI 拖把系统设计 一、引言 智能家庭 AI 拖把系统旨在为用户提供更高效、便捷和智能化的地面清洁解决方案&#xff0c;减轻家务劳动负担。 二、系统概述 1. 系统目标 - 自动清洁地面&#xff0c;包括吸尘、拖地和擦干功能。 - 智能识别地面材质和污渍程度&#xff0c…...

Android13源码下载和编译过程详解

前言 作为Android开发者人人都应该有一份自己Android源码,这样我们就可以随时对自己有疑惑的地方通过亲手调试来加强理解 一 源码下载 1.1 配置要求 官方推荐配置请参考&#xff1a;AOSP使用入门文档&#xff0c;重点有如下几项&#xff1a; 1.1.1 硬件配置要求 至少需要…...

Greenplum临时表未清除导致库龄过高处理

1.问题 Greenplum集群segment后台日志报错 2.回收库龄 master上执行 vacuumdb -F -d cxy vacuumdb -F -d template1 vacuumdb -F -d rptdb 3.回收完成后检查 仍然发现segment还是有库龄报警警告信息发出 4.检查 4.1 在master上检查库年龄 SELECT datname, datfrozen…...

SD-WAN站点和客户端的区别

很多用户在使用比扬云SD-WAN的时候不清楚站点和员工客户端的区别&#xff0c;尤其是很多从ZeroTier迁移过来的用户&#xff0c;这篇文章我们会详细介绍比扬云SD-WAN的站点和客户端使用场景。 ZeroTier只有客户端&#xff0c;没有站点&#xff0c;但是有路由配置&#xff0c;允…...

Arduino大师练成手册 -- 控制 MH-SD 卡模块

要在 Arduino 上控制 MH-SD 卡模块&#xff0c;你可以按照以下步骤进行&#xff1a; 硬件连接 VCC&#xff1a;连接到 Arduino 的 3.3V 或 5V 引脚&#xff08;根据模块的要求&#xff09;。 GND&#xff1a;连接到 Arduino 的 GND 引脚。 CS&#xff1a;连接到 Arduino 的…...

【MQ】RabbitMq的可靠性保证

消息队列中的可靠性主要是分为三部分&#xff1a; 消息不丢失&#xff1a;确保消息从生产者发送到消费者消息不丢失消息不重复&#xff1a;确保消息不被重复消费消息顺序性&#xff1a;确保消费的顺序性 解决方案主要有以下几部分&#xff1a; 消息不丢失 生产者确认机制持久…...

自签证书的dockerfile中from命令无法拉取镜像而docker的pull命令能拉取镜像

问题现象&#xff1a; docker pull images拉取镜像正常 dockerfile中的from命令拉取镜像就会报出证书错误。报错信息如下&#xff1a; [bjxtbwj-kvm-test-jenkins-6-243 ceshi_dockerfile]$ docker build . [] Building 0.4s (3/3) FINISHED …...

LAPD协议

实现LAPD&#xff08;Link Access Procedure on the D-channel&#xff09;协议的具体步骤和代码示例会比较复杂&#xff0c;通常涉及到底层网络编程和对ISDN协议的深入理解。以下是一个更详细的实现指导&#xff0c;主要集中在C/C环境中。 环境准备 确保你有一个支持ISDN的开发…...

jupyter版本所引起的扩展插件问题

文章目录 如何永久切换python安装源为https://mirrors.aliyun.com/pypi/simple方法一&#xff1a;通过配置文件永久设置&#xff08;推荐&#xff09;步骤 1&#xff1a;创建或修改 pip 配置文件步骤 2&#xff1a;验证配置是否生效 方法二&#xff1a;通过命令行直接配置效果验…...

连接 OpenAI 模型:基础操作

在这一部分中&#xff0c;我们将介绍如何连接 OpenAI 模型&#xff0c;设置 API 密钥&#xff0c;并使用 Spring AI 的 ChatClient 与 OpenAI 模型进行简单的对话。Spring AI 为集成 OpenAI 模型提供了方便的工具&#xff0c;使得开发者能够更轻松地与 GPT 系列模型进行交互。 …...

Vue.js组件开发-实现对视频预览

在 Vue 中实现视频文件预览 实现步骤 创建 Vue 组件&#xff1a;构建一个 Vue 组件用于处理视频文件的选择和预览。文件选择&#xff1a;添加一个文件输入框&#xff0c;允许用户选择视频文件。读取文件&#xff1a;监听文件选择事件&#xff0c;使用 FileReader API 读取所选…...

基于 Arduino Uno 和 RFID-RC522 的 RFID 卡号读取技术详解

引言 射频识别&#xff08;RFID&#xff09;技术因其非接触式、高效性和低成本的特点&#xff0c;广泛应用于门禁系统、物流管理和智能设备等领域。本文将通过 Arduino Uno 和 MFRC522 RFID 模块&#xff0c;手把手教你实现 RFID 卡号的读取&#xff0c;并提供完整的代码解析和…...

AI Agent的测试与监控:保障稳定性的实战经验

在前面的文章中&#xff0c;我们讨论了 AI Agent 的各个核心模块。今天&#xff0c;我想聊聊如何保障 AI Agent 的稳定性。说实话&#xff0c;这个话题我一直很关注&#xff0c;因为在生产环境中&#xff0c;稳定性往往比功能更重要。 从一次线上事故说起 还记得去年一个深夜…...

Servlet项目依赖管理

一、Servlet 相关技术选型 思路: 先选择 Servlet 容器&#xff0c;再找支持当前 JDK 版本的 Servlet 容器版本。 已知Servlet容器有两种市场占用率较高&#xff0c;Tomcat&Jetty。接下来分别按照上述思路进行选择容器版本。 Tomcat 官网: https://tomcat.apache.org/ 版本…...

戴尔电脑设置u盘启动_戴尔电脑设置u盘启动多种方法

最近有很多网友问&#xff0c;戴尔台式机怎么设置u盘启动&#xff0c;特别是近两年的戴尔台式机比较复杂&#xff0c;有些网友不知道怎么设置&#xff0c;其实设置u盘启动有两种方法&#xff0c;下面小编教大家戴尔电脑设置u盘启动方法。 戴尔电脑设置u盘启动方法一、戴尔进入b…...

大数据治理实战指南:数据质量、合规与治理架构

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 引言 随着企业数字化转型的加速&#xff0c;大数据已成为驱动业务决策的核心资产。然而&#xff0c;数据治理的缺失或不完善&…...

2025年01月26日Github流行趋势

项目名称&#xff1a;onlook 项目地址url&#xff1a;https://github.com/onlook-dev/onlook项目语言&#xff1a;TypeScript历史star数&#xff1a;4871今日star数&#xff1a;207项目维护者&#xff1a;Kitenite, drfarrell, iNerdStack, abhiroopc84, apps/dependabot项目简…...

03-画P封装(制作2D+添加3D)

画P封装的方法2D制作3D添加 使用P封装自己画0603格式的电阻的P封装1. 看规格书,找参数2. 创建一个新的P封装3. 灯泡两侧放焊盘4.设置焊盘大小和形状5.根据坐标定义中间间隔: L/2原则6. 画最外层丝印(丝印层直接围住即可)7.在平面的P封装上,添加3D立体封装库 立创商城下载P封装向…...

WPF5-x名称空间

1. x名称空间2. x名称空间内容3. x名称空间内容分类 3.1. x:Name3.2. x:Key3.3. x:Class3.4. x:TypeArguments 4. 总结 1. x名称空间 “x名称空间”的x是映射XAML名称空间时给它取的名字&#xff08;取XAML的首字母&#xff09;&#xff0c;里面的成员&#xff08;如x:Class、…...

UDP 广播组播点播的区别及联系

1、网络IP地址的分类 组播地址是分类编址的IPv4地址中的D类地址&#xff0c;又叫多播地址&#xff0c;他的前四位必须是1110&#xff0c;所以网络地址的二进制取值范围是11100000~11101111对应的十进制为 224~~239。所以以224~239开头的网络地址都是组播地址。 组播地址的功能…...

SSM开发(二) MyBatis两种SQL配置方式及其对比

目录 一、MyBatis两种SQL配置方式 二、使用XML映射文件配置SQL语句 三、使用注解配置SQL语句 四、两种方式对比 总结 1、注解 2、XML配置 五、MyBatis多数据源的两种配置方式 参考 一、MyBatis两种SQL配置方式 MyBatis 提供了两种方式来配置SQL语句:注解(如 @Select…...

leetcode——删除链表的倒数第N个节点(java)

给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[] 示例 3&#xf…...

SpringBoot支持动态更新配置文件参数

前言 博主介绍&#xff1a;✌目前全网粉丝3W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技术内容&#xff1a;Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。 博主所有博客文件…...

DeepSeek-R1,用Ollama跑起来

# DeepSeek-R1横空出世&#xff0c;超越OpenAI-o1&#xff0c;教你用Ollama跑起来 使用Ollama在本地运行DeepSeek-R1的操作指南。 DeepSeek-R1作为第一代推理模型&#xff0c;在数学、代码和推理任务上表现优异&#xff0c;与OpenAI-o1模型不相上下。 将此类模型部署到本地&am…...

基于OpenCV实现的答题卡自动判卷系统

一、图像预处理 🌄 二、查找答题卡轮廓 📏 三、透视变换 🔄 四、判卷与评分 🎯 五、主函数 六、完整代码+测试图像集 总结 🌟 在这篇博客中,我将分享如何使用Python结合OpenCV库开发一个答题卡自动判卷系统。这个系统能够自动从扫描的答题卡中提取信…...

VS Code i18n国际化组件代码code显示中文配置 i18n ally

VUE项目做i18n国际化之后&#xff0c;代码中的中文都变成了code这时的代码就会显得非常难读&#xff0c;如果有一个插件能把code转换成中文显示就好了 vscode插件搜索“i18n ally” 在项目根文件夹下创建文件&#xff1a;.vscode/settings.json settings.json 内容如下 {"…...

后盾人JS--闭包明明白白

延伸函数环境生命周期 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> <…...