okhttp之websocket源码解析(未完)

本文要求:大概了解使用方法,知道如何通过okhttp建立websocket,四个回调的名字最好大概记住,因为后面会直接使用。

websocket协议有两种消息类型, 被称为frame, 帧,一种是消息类, 就是我们普通通信使用的, 一种是控制通信用的,比如关闭用的close帧,心跳相关的ping帧和pong帧。

 if (isControlFrame) {
      readControlFrame();
    } else {
      readMessageFrame();
    }

而发射的概念就是write a frame,到服务端, 接受呢就是响应的read a frame

两者最为关键的源码就是runWriter()loopReader()这两个函数.

本文先从程序的入口开始查看设计和逻辑, 最后再从最贴近业务的,暴露给应用层使用的 发射,接受,以及回调 来总结归纳一下。

可以说市面上大部分的博客都是写的人都是一知半解,写出来的更是千篇一律,大部分仅仅讲解了demo级别的使用方法,缺失了很多情况的处理, 以及重要的注意点。

1. 入口

val client = OkHttpClient.Builder()
client.newWebSocket(request, object : WebSocketListener() {}    

还是从OkHttpClient开始的

/**
   * Uses {@code request} to connect a new web socket.
   */
  @Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
    RealWebSocket webSocket = new RealWebSocket(request, listener, new Random(), pingInterval);
    webSocket.connect(this);
    return webSocket;
  }

newWebSocket()函数里完成的事情:

  • 初始化RealWebSocket
  • 调用RealWebSocket$connect()

1.1初始化

public RealWebSocket(Request request, WebSocketListener listener, Random random,
      long pingIntervalMillis) {
    if (!"GET".equals(request.method())) {
      throw new IllegalArgumentException("Request must be GET: " + request.method());
    }
    this.originalRequest = request;
    this.listener = listener;
    this.random = random;
    this.pingIntervalMillis = pingIntervalMillis;

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

这里request和random用来建立连接,把http1升级成websocket。

this.pingIntervalMillis是指定心跳的间隔, OkHttpClient.Builder()构建的时候默认是0,默认的话,就不会发射ping帧了,后面会提到。

还有重要的一步是初始化了this.writerRunnable,这个runnable,是后续所有write操作(write就是客户端发送的操作,read就是接受服务端发来的操作)最终调用的任务(runWriter)

这个任务是无限循环writeOneFrame()函数

注释基本写的非常的清楚了:

尝试从queue里取出一帧,然后send他,pong帧优先级高于message帧和close帧,会优先发射,如果一个调用者入队了一个被pong帧跟随的message帧,那么会发射pong帧,让message帧跟随, pong帧当他们入队的时候, 永远会被下一个执行。

如果queue已经空了, 或者websokcet断开连接了, 就不能send帧了。

这样会什么都不做,只是返回false,否则会返回true且立刻再次调用本方法直到返回false。

这个方法只能呗writter thread调用, 可能只有一个线程在同一时间调用这个方法(应该是一定的吧, 因为会检查是否有锁)。

看下源码其实pongQueue和messageAndCloseQueue是两个queue,先处理pongQueue的逻辑

  • 如果poll出了pong帧,就直接
    writer.writePong(pong);发射

  • 如果没有poll出pong帧,那么处理messageAndCloseQueue

1.1.1处理messageAndCloseQueue

如果没有pong的话, messageAndCloseQueue.poll()取出消息messageOrClose

if(messageOrClose is null)
        return false队列空了, 这个while死循环就断掉了
if(messageOrClose is Close帧)
    if(receivedCloseCode!= -1,也就这是从服务器接收到的close帧)
        关闭excutor
    else 证明是自己发射的close帧
        优雅的关闭,60秒后调用call.cancel 
        
    不管优不优雅是不是自己enqueue的Close帧,都会执行下面的代码
    发射close帧;
    如果是服务端接受到的close帧那么会回调onClosed()方法,通知调用者;

最后return true
            
  /** The close code from the peer, or -1 if this web socket has not yet read a close frame. */
  private int receivedCloseCode = -1;

初始化的代码基本就这些了,但却非常的重要,因为他定义了所有与发射相关的逻辑。

1.2connect()函数

public void connect(OkHttpClient client) {
    client = client.newBuilder()
        .eventListener(EventListener.NONE)
        .protocols(ONLY_HTTP1)
        .build();
    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, streams);
          streamAllocation.connection().socket().setSoTimeout(0);
          loopReader();
        } catch (Exception e) {
          failWebSocket(e, null);
        }
      }

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

connect函数完成了websocket协议要求的升级过程(如果不了解,请简单了解下websocket协议),可以看到他添加了一些请求头, 并且调用了一次http1的握手协议, 如果失败的话,回调failWebSocket给用户通知。

主要看下升级成功的逻辑, 此时已经是websocket协议了, 可以看到上来就检查response,确认无误后,回调onOpen通知用户。下面是几件重要的步骤

  • 初始化reader和writer
  • loopReader()死循环读服务端发来的消息
  • 设置超时时间为0,证明是阻塞IO
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 前言 最近公司有项目需要用WebSocket完成及时通信的需求,这里来学习一下。 WebScoket简介 在以前的...
    Misery_Dx阅读 5,412评论 0 15
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • <!DOCTYPE html> 查看源 window.WRM=window.WRM||{};window....
    SMSM阅读 875评论 1 0
  • 每天的千言万语,想说的不过一句“我想你了” 你能狠心放下我,我就能狠心不打扰你,这次我一定做得到,放心好了~ 祝你...
    李岩心阅读 142评论 0 0
  • 文/叶栖儿 1. 得了一场重感冒。 2. 没人心疼你。 不久前看到一篇写《北京遇上西雅图之不二情书...
    叶栖儿阅读 765评论 2 1