okhttp框架之通信过滤Socket域名

2021-04-07  本文已影响0人  xuefeng_apple

HttpClient: 在android 5.0就被从源码中移除了
HttpUrlConnection: 偏底层,不适合直接使用,封装起来也比较麻烦。
Volley:适合数据量小但是频繁的网络操作,对大文件下载表现糟糕。
Okhttp:目前主推的网络库,全面支持各种网络请求、文件上传下载;性能高效,底层线程池提高请求的复用性;优秀的代码设计。但是也需要进行二次封装。
Retrofit:对Okhttp的二次封装。

这些网络是是如何调用到TCP/IP socket的呢, 下面就简单分析okhttp源码,解析TCP/IP socket 流程。
在某些工程的设计中,由于历史原因,可能需要兼容不同的网络库,主要思路就是做个中间层,能够切换不同的网络库。


1- okhttp 框架

框架图:


上图是OkHttp的总体架构,大致可以分为以下几层:
Interface——接口层:接受网络访问请求
Protocol——协议层:处理协议逻辑
Connection——连接层:管理网络连接,发送新的请求,接收服务器访问
Cache——缓存层:管理本地缓存
I/O——I/O层:实际数据读写实现
Inteceptor——拦截器层:拦截网络访问,插入拦截逻辑

调用流程图:


主要核心就是拦截器


参考:https://square.github.io/okhttp/interceptors/

http封装:


2- okhttp 调用socket

BridgeInterceptor:

@Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    //添加请求头信息
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }
        
      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
    
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
  }

下面我们看Socket是如何连接的,这就要看ConnectInterceptor类

@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");
    //这个newStream开始进行连接的
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

newStream:

 public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
    //这个方法是找到一个合适的连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
          //创建一个HttpCodec,用于Encodes HTTP requests and decodes HTTP responses请求编码和响应解码
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

findHealthyConnection:

/**
   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
   * until a healthy connection is found.
   */
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }
      return candidate;
    }
  }

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 {
    ...省略代码、通过判断去查找或者创建一个RealConnection
    // Do TCP + TLS handshakes. This is a blocking operation.
    //这个地方开始连接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
  }

connect:

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    ...省略代码
    while (true) {
      try {
      //如果是通过Https代理的时候返回true
        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) {
        ...省略代码关闭连接、创建异常
   }

    if (route.requiresTunnel() && rawSocket == null) {
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

connectSocket:

/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    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;
    }

    // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
    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);
      }
    }
  }

我们是在Android下所以就是AndroidPlatform,代码如下

 @Override public void connectSocket(Socket socket, InetSocketAddress address,
      int connectTimeout) throws IOException {
    try {
    //进行网络连接
      socket.connect(address, connectTimeout);
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } catch (SecurityException e) {
      // Before android 4.3, socket.connect could throw a SecurityException
      // if opening a socket resulted in an EACCES error.
      IOException ioException = new IOException("Exception in connect");
      ioException.initCause(e);
      throw ioException;
    } catch (ClassCastException e) {
      // On android 8.0, socket.connect throws a ClassCastException due to a bug
      // see https://issuetracker.google.com/issues/63649622
      if (Build.VERSION.SDK_INT == 26) {
        IOException ioException = new IOException("Exception in connect");
        ioException.initCause(e);
        throw ioException;
      } else {
        throw e;
      }
    }
  }

2- AndroidPlatform 继续分析

connect:
libcore/ojluni/src/main/java/java/net/Socket.java

public void connect(SocketAddress endpoint, int timeout){
        InetSocketAddress epoint = (InetSocketAddress) endpoint;
        InetAddress addr = epoint.getAddress ();
        int port = epoint.getPort();
        checkAddress(addr, "connect");

  //这里可以侦测到app 访问了那些域名,端口是多少,pid
  //根据pid ,系统/proc/pid/cmdline 含有了包名
        System.out.println(addr);   //打印server 地址
        System.out.println(epoint.getHostName());  //打印的hostname 
        System.out.println(port);   //打印端口
        System.out.println("pid="+Libcore.os.getpid()); //打印PID
 
       impl.connect(addr, port);
}

3-继续分析socket.getOutputStream())

其他使用方式

String path = "/getDemo.php";  
SocketAddress dest = new InetSocketAddress(this.host, this.port);  
socket.connect(dest);  
OutputStreamWriter streamWriter = new OutputStreamWriter(socket.getOutputStream());  
bufferedWriter = new BufferedWriter(streamWriter);  
          
bufferedWriter.write("GET " + path + " HTTP/1.1\r\n");  
bufferedWriter.write("Host: " + this.host + "\r\n");  
bufferedWriter.write("\r\n");  
bufferedWriter.flush(); 

getOutputStream如何调用的呢?

libcore/ojluni/src/main/java/java/net/AbstractPlainSocketImpl.java
socket.getOutputStream() 重点,要看这里是如何实现的,能否在这里进行字段的过滤:
-->impl.getOutputStream()  [impl: SocketImpl ], getOutputStream就是SocketImpl子类实现的
   -->class AbstractPlainSocketImpl extends SocketImpl 
   -->getOutputStream [AbstractPlainSocketImpl.java]
      -->SocketOutputStream extends FileOutputStream
        -->FileOutputStream
          --->write [对应上面的bufferedWriter.write("Host: " + this.host + "\r\n");  ]
           ---> IoBridge.write(fd, b, off, len);

IoBridge: 不在继续追查

okhttp3.1.2-org-source-code
链接:https://pan.baidu.com/s/1zMKVtP43nL37uZFDn5kj4Q
提取码:e4pn

REF:
https://www.jianshu.com/p/5ea4b7108168
https://blog.csdn.net/qqqq245425070/article/details/100162964
https://www.jianshu.com/p/7491e9d4c236

上一篇下一篇

猜你喜欢

热点阅读