美文网首页
okhttp框架之通信过滤Socket域名

okhttp框架之通信过滤Socket域名

作者: xuefeng_apple | 来源:发表于2021-04-07 13:48 被阅读0次

    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

    相关文章

      网友评论

          本文标题:okhttp框架之通信过滤Socket域名

          本文链接:https://www.haomeiwen.com/subject/ppaykltx.html