Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】

WebSockt

  • 系列
  • 前言
  • WebSocket
    • 与Socket、HTTP的关系
    • WebSocket协议
    • WebSocket作用
  • OKHttp与WebSocket
  • WebSocket源码
  • 总结

Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第1张图片

系列

OKHttp3–详细使用及源码分析系列之初步介绍【一】
OKHttp3–流程分析 核心类介绍 同步异步请求源码分析【二】
OKHttp3–Dispatcher分发器源码解析【三】
OKHttp3–调用对象RealCall源码解析【四】
OKHttp3–拦截器链RealInterceptorChain源码解析【五】
OKHttp3–重试及重定向拦截器RetryAndFollowUpInterceptor源码解析【六】
OKHttp3–桥接拦截器BridgeInterceptor源码解析及相关http请求头字段解析【七】
OKHttp3–缓存拦截器CacheInterceptor源码解析【八】
OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】
通过ConnectInterceptor源码掌握OKHttp3网络连接原理 呕心沥血第十弹【十】
OKHttp3-- 请求服务器拦截器CallServerInterceptor源码分析 【十一】

前言

这篇文章是有关OKHttp源码最后一篇文章了,OKHttp系列结束后准备对Glide出一个源码解读系列的博客;今天谈谈OKHttp对WebSocket的支持,WebSocket想必做Java开发的朋友们肯定很熟悉了,其实在Android上也是可以使用它的,至于怎么使用,就需要借助于OKHttp了

本篇文章将从以下方面解读:什么是WebSocket,它的由来,它与HTTP、Socket的区别,如何在app上借助OKHttp进行WebSocket开发,OKHttp是如何实现WebSocket协议的。

WebSocket

WebSocket作为一种解决web应用双向通信的协议由HTML5规范引出,是一种建立在TCP协议基础上的全双工通信的协议。它是一个应用层协议,它出现是为了解决HTTP的痛点,希望在服务器与浏览器之间建立一条不受限制的双向通信的通道。

为什么这么说呢?

想想以前如果在浏览器里打开一个实时展示股票行情的页面,每时每刻的股票信息怎么从服务器发送到浏览器,是不是客户端定时发送http请求,然后服务端返回数据,比如以前很多网站都用Ajax做短轮询,浏览器发送一次请求,服务器返回结果,但是这有个问题是假如浏览器的某次请求,服务端并没有数据更新需要推送到浏览器,那这次请求就是一次浪费的网络操作;虽然后来出现了Comet技术,是一种基于Ajax的长轮询(long-polling)方式,服务器端接收到请求后,会阻塞请求直到有数据或者超时才返回,这种方式听起来就可以感觉到是一种互联网快速发展背景下的无奈之举,不管是谁,实质都是基于HTTP请求,而HTTP协议有很多比较难搞的问题,比如:

  • HTTP协议是一个请求–响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说HTTP请求必须是一个Request对应一个Response,在HTTP/1.0中,一个HTTP连接发送一个Request接收一个Response,连接就结束了;在HTTP/1.1中,出来一个keep-alive,一个HTTP连接可以发送多个Request,接收多个Response;但是一个Request=一个Response在HTTP中是亘古不变的准则,同时response也是被动的,不能主动发起
  • 还有每次HTTP请求都会经历三次握手-四次挥手的操作,频繁的执行该操作非常消耗服务器资源,虽然在HTTP/1.1推出了keep-alive机制,但是并没有改变请求–响应的实现机制,持久化连接只是减少了握手次数
  • 持久化连接带来了一个严重的问题,就是HOLB(Head of Line Blocking),即排头阻塞,因为同一个持久连接中的请求依然是串行的,当某一个请求因为网络、服务器等原因阻塞了,那后面的所有请求都得不到处理
  • 还有HTTP请求的头部太大,每次请求都要发送重复的数据,非常消耗流量
  • 它们的实时性没办法满足股票、聊天这种业务需求

还有很多其它问题,这里就不再一一列举了,虽然这些问题在HTTP/2.0得到了很不错的解决,不过在它之前就有一种解决方式即WebSocket,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方

与Socket、HTTP的关系

虽然WebSocket名字里包含Socket,但是它跟Socket没有多少关系,Socket是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,不是协议;而WebSocket属于应用层协议。所以二者仅仅是名字像而已,类似于Java与JavaScript

TCP是传输层协议,WebSocket和HTTP都是基于TCP的应用层协议,但是WebSocket是双向通信协议,HTTP 是单向的,同时WebSocket 是需要握手进行建立连接的;只是WebSocket 在建立握手时,数据是通过 HTTP 传输的,但是建立之后,在真正传输数据时候是不需要 HTTP 协议的,如下图
Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第2张图片
WebSocket只在连接建立时需要使用到HTTP协议,其余部分都是自己独立的

WebSocket协议

WebSocket连接的建立需要客户端发送特殊的HTTP请求进行握手,这里分享一个官方测试网站websocket.org,如下图

Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第3张图片
点击connect进行连接,这时候会发送一个HTTP请求进行连接,打开开发者工具可以看到其请求头部和响应头部信息如图
Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第4张图片
Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第5张图片

可以看到这是一个类似于HTTP协议的报文,但是跟我们平时发送的HTTP请求头部信息是有很大不同的:

  • 该get请求的url地址不是类似http://这种,而是以ws://开头的
  • Connection: Upgrade表示连接类型是Upgrade,但是Upgrade是Upgrade: websocket,表示将该请求升级为websocket请求;这两个是Websocket的核心了,告诉Apache、Nginx等服务器,将这个连接转换为WebSocket连接
  • Sec-WebSocket-Key: gHTIArwwRC39dfextq+nfg==:是一个Base64 encode的值,这个是浏览器随机生成的,与响应头的该字段值进行比较,用于验证协议是否为WebSocket协议而非HTTP协议
  • Sec-WebSocket-Version:指定了WebSocket的协议版本

同时可以看到响应头部的响应码【Status Code: 101 Web Socket Protocol Handshake】,响应码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议

现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方,这后面就跟HTTP协议没啥关系了

接下来再点击下发送按钮,看看开发者工具,如图
Android App如何借助OKHttp使用WebSocket实现与服务器实时双向通信【十二】_第6张图片

可以看到并没有产生新的连接,而是复用上面建立的WebSocket连接

WebSocket作用

说了这么多还没讲到WebSocket到底有什么作用,或者说有什么优点;上面讲到HTTP的痛点或者问题的时候提到过,当你的业务需要实时跟服务器交互,不管是短轮训还是长轮训,都需要消耗很大的服务器资源,还有服务器的被动性,还有每次都要发送重复的头部信息等,而WebSocket可以解决这些问题。

WebSocket 解决的第一个问题是,通过第一个 HTTP request 建立了 TCP 连接之后,之后的交换数据都不需要再发 HTTP request了,使得这个长连接变成了一个真长连接。但是不需要发送 HTTP header就能交换数据显然和原有的 HTTP 协议是有区别的,所以它需要对服务器和客户端都进行升级才能实现。此外还有 multiplexing 功能,几个不同的 URI 可以复用同一个 WebSocket 连接。这些都是原来的 HTTP 不能做到的。

WebSocket还是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求,就像使用Socket一样;而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。


OKHttp与WebSocket

WebSocket虽然是H5提出的,但不仅仅应用于Web应用上,在Android客户端,也可以使用,一般用下面两种库使用WebSocket:

  • OkHttp :16年OkHttp就加入了WebSocket支持包,最新版本已经将ws融合进来,直接可以使用
  • Java-WebSocket :Java实现的WebSocket协议

这里以OKHttp为例,先看看如何基于OKHttp进行WebSocket开发

public class WebSocketDemo {

    private final String TAG = WebSocketDemo.class.getSimpleName();

    private OkHttpClient CLIENT ;
    private WebSocket mWebSocket;

    private static final WebSocketDemo ourInstance = new WebSocketDemo();

    public static WebSocketDemo getDefault() {
        return ourInstance;
    }

    private WebSocketDemo() {
        CLIENT = new OkHttpClient.Builder()
                .writeTimeout(5,TimeUnit.SECONDS)
                .readTimeout(5, TimeUnit.SECONDS)
                .connectTimeout(5, TimeUnit.SECONDS)
                .build();
    }

    public void connect(String url){
        if (mWebSocket != null) {
            mWebSocket.cancel();
        }
        Request request = new Request.Builder()
                .url(url)
                .build();
        mWebSocket = CLIENT.newWebSocket(request,new SocketListener());
    }

    public void sendMessage(String message){
        mWebSocket.send(message);
    }

    public void sendMessage(byte... data){
        ByteString bs = ByteString.of(data);
        mWebSocket.send(bs);
    }

    public void close(int code, String reason){
        mWebSocket.close(code,reason);
    }

    class SocketListener extends WebSocketListener{

        @Override
        public void onOpen(WebSocket webSocket, Response response) {
            super.onOpen(webSocket, response);
            Log.i(TAG,"onOpen response="+response);
        }

        @Override
        public void onMessage(WebSocket webSocket, String text) {
            super.onMessage(webSocket, text);
            Log.i(TAG,"onMessage text="+text);
        }

        @Override
        public void onMessage(WebSocket webSocket, ByteString bytes) {
            super.onMessage(webSocket, bytes);
            Log.i(TAG,"onMessage bytes="+bytes);
        }

        @Override
        public void onClosing(WebSocket webSocket, int code, String reason) {
            super.onClosing(webSocket, code, reason);
            Log.i(TAG,"onClosing code="+code);
        }

        @Override
        public void onClosed(WebSocket webSocket, int code, String reason) {
            super.onClosed(webSocket, code, reason);
            Log.i(TAG,"onClosed code="+code);
        }

        @Override
        public void onFailure(WebSocket webSocket, Throwable t, Response response) {
            super.onFailure(webSocket, t, response);
            Log.i(TAG,"onFailure t="+t.getMessage());
        }
    }

}

可以看到用法非常简单,主要就是通过OkHttpClient实例化一个WebSocket,然后用来发送消息,同时还配置了一个回调,里面有各种回调方法,比如onOpen是在服务器连接成功后回调,onMessage是服务器发送消息给客户端时回调的,其它方法看名字也能看得懂,要注意的是这些方法都是在子线程回调的,不要直接更新UI。

至于要实现什么定制需求,就看开发者自己在这个基础上进行封装了,比如你想实现IM即时通讯类的功能,那就在onMessage回调中要处理好消息数据的解析,判断数据属于哪个用户;实现消息推送功能简单点,数据解析没有那么复杂


WebSocket源码

接下来从源码看看OKHttp是如何实现WebSocket协议的

第一步就是实例化一个WebSocket,代码如下

  @Override 
  public WebSocket newWebSocket(Request request, WebSocketListener listener) {
    RealWebSocket webSocket = new RealWebSocket(request, listener, new Random());
    webSocket.connect(this);
    return webSocket;
  }
    public RealWebSocket(Request request, WebSocketListener listener, Random random) {
      if (!"GET".equals(request.method())) {
        throw new IllegalArgumentException("Request must be GET: " + request.method());
      }
      this.originalRequest = request;
      this.listener = listener;
      this.random = random;
    
      byte[] nonce = new byte[16];
      random.nextBytes(nonce);
      this.key = ByteString.of(nonce).base64();
    
      this.writerRunnable = new Runnable() {
        @Override public void run() {
          try {
            while (writeOneFrame()) {
            }
          } catch (IOException e) {
            failWebSocket(e, null);
          }
        }
      };
    }

RealWebSocket是WebSocket的实现类,其构造方法中第一个处理就是判断当前请求的方法是不是【GET】请求,如果不是,就抛出异常;原因在上面讲WebSocket协议的时候说过,它需要发送一个HTTP的get请求进行握手;接下来就是一些赋值,包括Sec-WebSocket-Key头的随机字符串,最后还实例化了一个writerRunnable,里面主要是一个while循环不断的从队列中取出数据发送到服务器

接下来就是调用connect方法连接服务器

  public void connect(OkHttpClient client) {
    client = client.newBuilder()
        .protocols(ONLY_HTTP1)
        .build();
    final int pingIntervalMillis = client.pingIntervalMillis();
    final Request request = originalRequest.newBuilder()
        .header("Upgrade", "websocket")
        .header("Connection", "Upgrade")
        .header("Sec-WebSocket-Key", key)
        .header("Sec-WebSocket-Version", "13")
        .build();
    call = Internal.instance.newWebSocketCall(client, request);
    call.enqueue(new Callback() {
      @Override public void onResponse(Call call, Response response) {
        try {
          checkResponse(response);
        } catch (ProtocolException e) {
          failWebSocket(e, response);
          closeQuietly(response);
          return;
        }

        // Promote the HTTP streams into web socket streams.
        StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
        streamAllocation.noNewStreams(); // Prevent connection pooling!
        Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);

        // Process all web socket messages.
        try {
          listener.onOpen(RealWebSocket.this, response);
          String name = "OkHttp WebSocket " + request.url().redact();
          initReaderAndWriter(name, pingIntervalMillis, streams);
          streamAllocation.connection().socket().setSoTimeout(0);
          loopReader();
        } catch (Exception e) {
          failWebSocket(e, null);
        }
      }

      @Override public void onFailure(Call call, IOException e) {
        failWebSocket(e, null);
      }
    });
  }

这里就能看的很明显了,分段来分析:

  • 在实例化Request的时候,添加了Upgrade、Connection等请求头,构建符合WebSocket协议握手规范的HTTP请求

  • 接下来获取到响应后调用checkResponse验证响应是否是符合规范的,比如响应码是否是101,有没有Upgrade、websocket响应头;Sec-WebSocket-Accept值是否与之间生成的随机字符串相同

      void checkResponse(Response response) throws ProtocolException {
        if (response.code() != 101) {
          throw new ProtocolException("Expected HTTP 101 response but was '"
              + response.code() + " " + response.message() + "'");
        }
    
        String headerConnection = response.header("Connection");
        if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
          throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
              + headerConnection + "'");
        }
    
        String headerUpgrade = response.header("Upgrade");
        if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
          throw new ProtocolException(
              "Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
        }
    
        String headerAccept = response.header("Sec-WebSocket-Accept");
        String acceptExpected = ByteString.encodeUtf8(key + WebSocketProtocol.ACCEPT_MAGIC)
            .sha1().base64();
        if (!acceptExpected.equals(headerAccept)) {
          throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
              + acceptExpected + "' but was '" + headerAccept + "'");
        }
      }
    
  • 检查合格后,WebSocket连接就正式建立了,这时候就将刚才的HTTP流转成web socket流,得到Streams这么个对象,定义如下

      public abstract static class Streams implements Closeable {
        public final boolean client;
        public final BufferedSource source;//输入流
        public final BufferedSink sink;//输出流
    
        public Streams(boolean client, BufferedSource source, BufferedSink sink) {
          this.client = client;
          this.source = source;
          this.sink = sink;
        }
      }
    
  • 接下来调用initReaderAndWriter实例化输出流输入流,得到WebSocketWriter/WebSocketReader,实际I/O操作由它们执行;同时设置Socket的超时时间为0,也就是永不超时,维持这个连接

  • 最后就是调用loopReader方法,循环读取服务器消息

      public void loopReader() throws IOException {
        while (receivedCloseCode == -1) {
          reader.processNextFrame();
        }
      }
      void processNextFrame() throws IOException {
          readHeader();
          if (isControlFrame) {
            readControlFrame();
          } else {
            readMessageFrame();
          }
      }
      private void readHeader() throws IOException {
          ......
            try {
              b0 = source.readByte() & 0xff;
            } finally {
              source.timeout().timeout(timeoutBefore, TimeUnit.NANOSECONDS);
            }
           ......
       }
    

    这里重点看下source.readByte方法,其Source实例化在Okio的source方法

      private static Source source(final InputStream in, final Timeout timeout) {
        return new Source() {
          @Override public long read(Buffer sink, long byteCount) throws IOException {
            try {
              ......
              int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
              return bytesRead;
            } catch (AssertionError e) {
              if (isAndroidGetsocknameError(e)) throw new IOException(e);
              throw e;
            }
          }
    		......
        };
      }
    

    可以看到最终调用的是Socket的输入流的read方法,这里也是IO阻塞模型,等待接收消息

到此,WebSocket从连接建立到消息读取发送基本上分析完了,相信大家应该清楚了OKHttp是如何实现WebSocket协议的

总结

到这里,有关OKHttp源码解读系列也就结束了,从今年1月份开始,直到现在写了11篇它的源码解读文章,中间也停了三个多月,总的来看虽然花了这么多时间,但还是感触颇深,收获满满;站在巨人肩膀上能让你处在不同层次上看问题,也从中学到了不少代码设计模式,设计理念,看到一些功能的实现方式,也提高自己的代码能力,让我在自己的项目中也能借鉴一些开发经验,好处多多

你可能感兴趣的:(【开源框架源码解析】)