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 {
    //创建OkHttpClient
    OkHttpClient 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(){
    //创建OkHttpClient
    OkHttpClient 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() {
        @Override
        public void onFailure(Call call, IOException e) {
            Log.e(TAG,"请求失败:"+e.getMessage());
        }
        @Override
        public 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一定是true
    boolean 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:
        // 如果用户不允许重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 从响应头取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根据location 配置新的请求 url
        HttpUrl 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与head
        if (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;
    }

    @Override
    public 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协议报文的封装与解析

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot的网上摄影工作室开发与实现 | 含论文、任务书、选题表

随着互联网技术的不断发展&#xff0c;摄影爱好者们越来越需要一个在线平台来展示和分享他们的作品。基于SpringBoot的网上摄影工作室应运而生&#xff0c;它不仅为用户提供了一个展示摄影作品的平台&#xff0c;还为管理员提供了便捷的管理工具。本文幽络源将详细介绍该系统的…

关于低代码技术架构的思考

我们经常会看到很多低代码系统的技术架构图&#xff0c;而且经常看不懂。是因为技术架构图没有画好&#xff0c;还是因为技术不够先进&#xff0c;有时候往往都不是。 比如下图&#xff1a; 一个开发者&#xff0c;看到的视角往往都是技术层面&#xff0c;你给用户讲React18、M…

新手从零开始使用飞牛fnOS搭建家庭数据管理中心体验NAS系统

文章目录 前言1. VMware安装飞牛云&#xff08;fnOS&#xff09;1.1 打开VMware创建虚拟机1.3 初始化系统 2. 安装Cpolar工具3. 配置远程访问地址4. 远程访问飞牛云NAS5. 固定远程访问地址 前言 今天和大家分享一款国产NAS系统飞牛fnOS&#xff0c;如果你是新手用户&#xff0…

【学术会议征稿】第五届能源、电力与先进热力系统学术会议(EPATS 2025)

能源、电力与先进热力系统设计是指结合物理理论、工程技术和计算机模拟&#xff0c;对能源转换、利用和传输过程进行设计的学科领域。它涵盖了从能源的生产到最终的利用整个流程&#xff0c;旨在提高能源利用效率&#xff0c;减少能源消耗和环境污染。 重要信息 官网&#xf…

games101-(3/4)变换

缩放&#xff1a; 对称 切变 旋转 考虑&#xff08;1.0&#xff09;这个点 同理考虑&#xff08;0&#xff0c;1&#xff09;点即可 齐次方程 考虑在二维的坐标点后面增加一个维度 所有的仿射变换都可以写成齐次坐标的形式 a b c d 是线性变换 tx ty 是平移&#xff1b; …

LangChain:使用表达式语言优化提示词链

在 LangChain 里&#xff0c;LCEL 即 LangChain Expression Language&#xff08;LangChain 表达式语言&#xff09;&#xff0c;本文为你详细介绍它的定义、作用、优势并举例说明&#xff0c;从简单示例到复杂组合示例&#xff0c;让你快速掌握LCEL表达式语言使用技巧。 定义 …

【Samba】Ubuntu20.04 Windows 共享文件夹

【Samba】Ubuntu20.04 Windows 共享文件夹 前言整体思路检查 Ubuntu 端 和 Windows 网络通信是否正常创建共享文件夹安装并配置 Samba 服务器安装 Samba 服务器创建 Samba 用户编辑 Samba 配置文件重启 Samba 服务器 在 Windows 端 访问 Ubuntu 的共享文件夹 前言 本文基于 Ub…

基于Springboot + vue实现的洗衣店订单管理系统

“前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff1a;人工智能学习网站” &#x1f496;学习知识需费心&#xff0c; &#x1f4d5;整理归纳更费神。 &#x1f389;源码免费人人喜…

卡特兰数学习

1&#xff0c;概念 卡特兰数&#xff08;英语&#xff1a;Catalan number&#xff09;&#xff0c;又称卡塔兰数&#xff0c;明安图数。是组合数学中一种常出现于各种计数问题中的数列。它在不同的计数问题中频繁出现。 2&#xff0c;公式 卡特兰数的递推公式为&#xff1a;f(…

2025_1_27 C语言内存,递归,汉诺塔问题

1.c程序在内存中的布局 代码段&#xff08;Code Segment&#xff09; 位置&#xff1a;通常位于内存的最低地址。 用途&#xff1a;存储程序的可执行指令。 特点&#xff1a;只读&#xff0c;防止程序运行时被修改。数据段&#xff08;Data Segment&#xff09; 位置&#xf…

解锁数字经济新动能:探寻 Web3 核心价值

随着科技的快速发展&#xff0c;我们正迈入一个全新的数字时代&#xff0c;Web3作为这一时代的核心构成之一&#xff0c;正在为全球数字经济带来革命性的变革。本文将探讨Web3的核心价值&#xff0c;并如何推动数字经济的新动能。 Web3是什么&#xff1f; Web3&#xff0c;通常…

活动回顾和预告|微软开发者社区 Code Without Barriers 上海站首场活动成功举办!

Code Without Barriers 上海活动回顾 Code Without Barriers&#xff1a;AI & DATA 深入探索人工智能与数据如何变革行业 2025年1月16日&#xff0c;微软开发者社区 Code Without Barriers &#xff08;CWB&#xff09;携手 She Rewires 她原力在大中华区的首场活动“AI &…

Day27-【13003】短文,线性表两种基本实现方式空间效率、时间效率比较?兼顾优点的静态链表是什么?如何融入空闲单元链表来解决问题?

文章目录 本次内容总览第四节&#xff0c;两种基本实现方式概览两种基本实现方式的比较元素个数n大于多少时&#xff0c;使用顺序表存储的空间效率才会更高&#xff1f;时间效率比较&#xff1f;*、访问操作&#xff0c;也就是读运算&#xff0c;读操作1、插入&#xff0c;2、删…

python -m pip和pip的主要区别

python -m pip和pip的主要区别在于它们与Python环境的关联方式和安装路径。‌ ‌与Python环境的关联方式‌&#xff1a; pip 是直接使用命令行工具来安装Python包&#xff0c;不指定特定的Python解释器。如果系统中存在多个Python版本&#xff0c;可能会导致安装的包被安装到…

C语言从入门到进阶

视频&#xff1a;https://www.bilibili.com/video/BV1Vm4y1r7jY?spm_id_from333.788.player.switch&vd_sourcec988f28ad9af37435316731758625407&p23 //枚举常量 enum Sex{MALE,FEMALE,SECRET };printf("%d\n", MALE);//0 printf("%d\n", FEMALE…

Blazor-Blazor Web App项目结构

让我们还是从创建项目开始&#xff0c;来一起了解下Blazor Web App的项目情况 创建项目 呈现方式 这里我们可以看到需要选择项目的呈现方式&#xff0c;有以上四种呈现方式 ● WebAssembly ● Server ● Auto(Server and WebAssembly) ● None 纯静态界面静态SSR呈现方式 WebAs…

新版IDEA创建数据库表

这是老版本的IDEA创建数据库表&#xff0c;下面可以自己勾选Not null&#xff08;非空),Auto inc&#xff08;自增长),Unique(唯一标识)和Primary key&#xff08;主键) 这是新版的IDEA创建数据库表&#xff0c;Not null和Auto inc可以看得到&#xff0c;但Unique和Primary key…

某公交管理系统简易逻辑漏洞+SQL注入挖掘

视频教程在我主页简介或专栏里 目录: 某公交管理系统挖掘 SQL注入漏洞 越权漏洞 某公交管理系统挖掘 SQL注入漏洞 前台通过给的账号密码,进去 按顺序依次点击1、2、3走一遍功能点&#xff0c;然后开启抓包点击4 当点击上图的4步骤按钮时&#xff0c;会抓到图下数据包&a…

通过案例研究二项分布和泊松分布之间关系(2)

通过案例研究二项分布和泊松分布之间关系 2. 汽车出事故的概率p与保险公司盈利W之间的关系3.通过遗传算法多次迭代计算控制p为多少时公司盈利最大(1) 计算过程(2) 结果及分析(计算过程详见附录二程序) 4.改变思路求解固定p为0.01时,保险费用如何设置公司可获得最大利润(1)计算过…

mysql 学习5 mysql图形化界面DataGrip下载 安装 使用

一、下载和安装 下载&#xff1a; 其他版本 - DataGrip PS&#xff1a;安装目录最好不要有中文。 C:\Program Files\JetBrains\DataGrip 2023.3.4 二、 夸克网盘分享 当前电脑是下载到 &#xff1a;D:\Ctool\mysql\datagrip2023_3_4\datagrip2024\jetbra-datagrip\scripts …