Android 源码系列之,深入浅出WebSocket,打造自己的即时聊天交互系统

       转载请注明出处:http://blog.csdn.net/llew2011/article/details/72987090

       在上篇文章Android 源码系列之<十五>,深入浅出WebSocket,打造自己的即时聊天交互系统<上>中主要讲解了WebSocket协议,然后通过实战方式展示了WebSocket的通信过程,这篇文章我将从源码的角度带领小伙伴们深入理解一下autobahn以及okhttp的Socket通信源码,如果你对上述项目的WebSocket实现比较清楚了,请跳过本文(*^__^*) ……

        我们首先从GitHub上clone来下autobahn的源码,运行一下该项目,效果如下:       


       autobahn给出的demo我稍微做了修改,IP和端口号是我从网上找的一个在线测试地址,当点击Connect按钮后就会链接输入框给定的IP地址和端口号,然后进行链接操作,链接成功后通过Toast弹出服务器端返回的消息,表示Socket链接已经建立,可以进行通信了,然后我们可以在下方的输入框中输入任意内容给服务器,为了弄清楚通信过程,我们通过分析点击Connect按钮看看发生的流程,点击CONNECT按钮会调用start()方法,源码如下:

private void start() {

    final String wsuri = "ws://" + mHostname.getText() + ":" + mPort.getText();

    mStatusline.setText("Status: Connecting to " + wsuri + " ..");

    setButtonDisconnect();

    try {
        mConnection.connect(wsuri, new WebSocketConnectionHandler() {
            @Override
            public void onOpen() {
                mStatusline.setText("Status: Connected to " + wsuri);
                savePrefs();
                mSendMessage.setEnabled(true);
                mMessage.setEnabled(true);
            }

            @Override
            public void onTextMessage(String payload) {
                alert("Got echo: " + payload);
                Log.e("---WebSocket---", payload);
            }

            @Override
            public void onClose(int code, String reason) {
                alert("Connection lost.");
                mStatusline.setText("Status: Ready.");
                setButtonConnect();
                mSendMessage.setEnabled(false);
                mMessage.setEnabled(false);
            }
        });
    } catch (WebSocketException e) {
        Log.d(TAG, e.toString());
    }
}
       start()方法就是进行链接Socket的核心,首先根据输入框输入的IP地址和端口生成一个wsuri地址(该处值为:ws://121.40.165.18:8088),然后设置Connect按钮为不可点击状态,最后通过mConnection的connect()方法进行Socket链接,mConnection的定义如下:
private final WebSocket mConnection = new WebSocketConnection();
       mConnection是WebSocket类型,该类是抽象类,它的实现类是WebSocketConnection,那也就是说mConnection调用的connect()方法走的是WebSocketConnection的connect()方法,其源码如下:
public void connect(String wsUri, WebSocket.ConnectionHandler wsHandler) throws WebSocketException {
    connect(wsUri, null, wsHandler, new WebSocketOptions(), null);
}
       connect()方法又调用了其重载方法connect(),源码如下:
public void connect(String wsUri, String[] wsSubprotocols, WebSocket.ConnectionHandler wsHandler, WebSocketOptions options, List<BasicNameValuePair> headers) throws WebSocketException {
    // don't connect if already connected .. user needs to disconnect first
    if (isConnected()) {
        throw new WebSocketException("already connected");
    }
    // parse WebSockets URI
    try {
        mWsUri = new URI(wsUri);

        if (!mWsUri.getScheme().equals("ws") && !mWsUri.getScheme().equals("wss")) {
            throw new WebSocketException("unsupported scheme for WebSockets URI");
        }

        mWsScheme = mWsUri.getScheme();

        if (mWsUri.getPort() == -1) {
            if (mWsScheme.equals("ws")) {
                mWsPort = 80;
            } else {
                mWsPort = 443;
            }
        } else {
            mWsPort = mWsUri.getPort();
        }

        if (mWsUri.getHost() == null) {
            throw new WebSocketException("no host specified in WebSockets URI");
        } else {
            mWsHost = mWsUri.getHost();
        }

        if (mWsUri.getRawPath() == null || mWsUri.getRawPath().equals("")) {
            mWsPath = "/";
        } else {
            mWsPath = mWsUri.getRawPath();
        }

        if (mWsUri.getRawQuery() == null || mWsUri.getRawQuery().equals("")) {
            mWsQuery = null;
        } else {
            mWsQuery = mWsUri.getRawQuery();
        }
    } catch (URISyntaxException e) {
        throw new WebSocketException("invalid WebSockets URI");
    }

    mWsSubprotocols = wsSubprotocols;
    mWsHeaders = headers;
    mWsHandler = wsHandler;

    // make copy of options!
    mOptions = new WebSocketOptions(options);

    // set connection active
    mActive = true;

    // reset value
    onCloseCalled = false;

    // use async connector on short-lived background thread
    new WebSocketConnector().start();
}
       该方法首先判断链接是否建立,如果建立则直接抛了一个异常,因为链接一旦建立了就不需要再次建立,然后检测给定的URI协议和端口是否符合规范,如果都符合WebSocket协议规范则启动后台线程WebSocketConnector来进行消息通信,该类源码如下:
/**
 * Asynchronous socket connector.
 */
private class WebSocketConnector extends Thread {

    public void run() {
        Thread.currentThread().setName("WebSocketConnector");

        /*
         * connect TCP socket
         */
        try {
            if (mWsScheme.equals("wss")) {
                mSocket = SSLSocketFactory.getDefault().createSocket();
            } else {
                mSocket = SocketFactory.getDefault().createSocket();
            }

            // the following will block until connection was established or
            // an error occurred!
            mSocket.connect(new InetSocketAddress(mWsHost, mWsPort), mOptions.getSocketConnectTimeout());

            // before doing any data transfer on the socket, set socket
            // options
            mSocket.setSoTimeout(mOptions.getSocketReceiveTimeout());
            mSocket.setTcpNoDelay(mOptions.getTcpNoDelay());

        } catch (IOException e) {
            onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT,
                    e.getMessage());
            return;
        }
        if (isConnected()) {
            try {
                // create & start WebSocket reader
                createReader();

                // create & start WebSocket writer
                createWriter();

                // start WebSockets handshake
                WebSocketMessage.ClientHandshake hs = new WebSocketMessage.ClientHandshake(
                        mWsHost + ":" + mWsPort);
                hs.mPath = mWsPath;
                hs.mQuery = mWsQuery;
                hs.mSubprotocols = mWsSubprotocols;
                hs.mHeaderList = mWsHeaders;
                mWriter.forward(hs);

                mPrevConnected = true;

            } catch (Exception e) {
                onClose(WebSocketConnectionHandler.CLOSE_INTERNAL_ERROR,
                        e.getMessage());
            }
        } else {
            onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT,
                    "Could not connect to WebSocket server");
        }
    }
}
       WebSocketConnector线程启动后先根据传递的scheme判断创建哪种类型的Socket,Socket对象创建完成后根据IP地址和端口号调用其connec()方法和服务器建立链接,然后设置超时等属性,接下来判断该链接是否建立成功,如果链接创建成功,则分别调用createReader()和createWriter()方法创建读写线程进行消息的接收和发送,先看一下createReader()方法,源码如下:
/**
 * Create WebSockets background reader.
 */
protected void createReader() throws IOException {

    mReader = new WebSocketReader(mMasterHandler, mSocket, mOptions, "WebSocketReader");
    mReader.start();

    if (DEBUG) Log.d(TAG, "WS reader created and started");
}
       createReader()方法启动了WebSocketReader线程并把相关参数传递给了该线程,我们直接进入该线程的run()方法看一下,源码如下:
@Override
public void run() {
    try {
        do {
            // blocking read on socket
            int len = mBufferedStream.read(mMessageData, mPosition, mMessageData.length - mPosition);
            mPosition += len;
            if (len > 0) {
                // process buffered data
                while (consumeData()) {
                }
            } else if (mState == STATE_CLOSED) {
                mStopped = true;
            } else if (len < 0) {
                notify(new WebSocketMessage.ConnectionLost());
                mStopped = true;
            }
        } while (!mStopped);
    } catch (WebSocketException e) {
        notify(new WebSocketMessage.ProtocolViolation(e));
    } catch (SocketException e) {
        // BufferedInputStream throws when the socket is closed,
        // eat the exception if we are already in STATE_CLOSED.
        if (mState != STATE_CLOSED && !mSocket.isClosed()) {
            // wrap the exception and notify master
            notify(new WebSocketMessage.ConnectionLost());
        }
    } catch (Exception e) {
        // wrap the exception and notify master
        notify(new WebSocketMessage.Error(e));
    } finally {
        mStopped = true;
    }
}
       run()方法里边是个do while循环,因为接收端要接收对方发送的消息,因此需要一直不断循环的来读取消息。首先通过mBufferedStream的read()方法往mMessageData的byte数组中读取数据,mBufferedStream是通过Socket来创建的,mBufferedStream = new BufferedInputStream(mSocket.getInputStream(), mOptions.getMaxFramePayloadSize() + 14);如果读取到了数据,这调用consumeData()方法,该方法如下所示:
/**
 * Consume data buffered in mFrameBuffer.
 */
private boolean consumeData() throws Exception {
    if (mState == STATE_OPEN || mState == STATE_CLOSING) {
        return processData();
    } else if (mState == STATE_CONNECTING) {
        return processHandshake();
    } else if (mState == STATE_CLOSED) {
        return false;
    } else {
        // should not arrive here
        return false;
    }
}
       该方法主要是根据Socket的链接状态进行不同的消息处理,Socket链接建立后首先是进行握手验证,验证服务器端是否支持Websocket协议等,此时调用了processHandshake()方法,该方法验证完成后会返回true或false。接下来握手验证通过后就是调用processData()方法了,该方法源码如下:
/**
 * Process incoming WebSockets data (after handshake).
 */
private boolean processData() throws Exception {

    // outside frame?
    if (mFrameHeader == null) {

        // need at least 2 bytes from WS frame header to start processing
        if (mPosition >= 2) {

            byte b0 = mMessageData[0];
            boolean fin = (b0 & 0x80) != 0;
            int rsv = (b0 & 0x70) >> 4;
            int opcode = b0 & 0x0f;

            byte b1 = mMessageData[1];
            boolean masked = (b1 & 0x80) != 0;
            int payload_len1 = b1 & 0x7f;

            // now check protocol compliance

            if (rsv != 0) {
                throw new WebSocketException("RSV != 0 and no extension negotiated");
            }

            if (masked) {
                // currently, we don't allow this. need to see whats the final spec.
                throw new WebSocketException("masked server frame");
            }

            if (opcode > 7) {
                // control frame
                if (!fin) {
                    throw new WebSocketException("fragmented control frame");
                }
                if (payload_len1 > 125) {
                    throw new WebSocketException("control frame with payload length > 125 octets");
                }
                if (opcode != 8 && opcode != 9 && opcode != 10) {
                    throw new WebSocketException("control frame using reserved opcode " + opcode);
                }
                if (opcode == 8 && payload_len1 == 1) {
                    throw new WebSocketException("received close control frame with payload len 1");
                }
            } else {
                // message frame
                if (opcode != 0 && opcode != 1 && opcode != 2) {
                    throw new WebSocketException("data frame using reserved opcode " + opcode);
                }
                if (!mInsideMessage && opcode == 0) {
                    throw new WebSocketException("received continuation data frame outside fragmented message");
                }
                if (mInsideMessage && opcode != 0) {
                    throw new WebSocketException("received non-continuation data frame while inside fragmented message");
                }
            }

            int mask_len = masked ? 4 : 0;
            int header_len;

            if (payload_len1 < 126) {
                header_len = 2 + mask_len;
            } else if (payload_len1 == 126) {
                header_len = 2 + 2 + mask_len;
            } else if (payload_len1 == 127) {
                header_len = 2 + 8 + mask_len;
            } else {
                // should not arrive here
                throw new Exception("logic error");
            }

            // continue when complete frame header is available
            if (mPosition >= header_len) {

                // determine frame payload length
                int i = 2;
                long payload_len;
                if (payload_len1 == 126) {
                    payload_len = ((0xff & mMessageData[i]) << 8) | (0xff & mMessageData[i + 1]);
                    if (payload_len < 126) {
                        throw new WebSocketException("invalid data frame length (not using minimal length encoding)");
                    }
                    i += 2;
                } else if (payload_len1 == 127) {
                    if ((0x80 & mMessageData[i]) != 0) {
                        throw new WebSocketException("invalid data frame length (> 2^63)");
                    }
                    payload_len = ((long) (0xff & mMessageData[i]) << 56) |
                            ((long) (0xff & mMessageData[i + 1]) << 48) |
                            ((long) (0xff & mMessageData[i + 2]) << 40) |
                            ((long) (0xff & mMessageData[i + 3]) << 32) |
                            ((long) (0xff & mMessageData[i + 4]) << 24) |
                            ((long) (0xff & mMessageData[i + 5]) << 16) |
                            ((long) (0xff & mMessageData[i + 6]) << 8) |
                            ((long) (0xff & mMessageData[i + 7]));
                    if (payload_len < 65536) {
                        throw new WebSocketException("invalid data frame length (not using minimal length encoding)");
                    }
                    i += 8;
                } else {
                    payload_len = payload_len1;
                }

                // immediately bail out on frame too large
                if (payload_len > mOptions.getMaxFramePayloadSize()) {
                    throw new WebSocketException("frame payload too large");
                }

                // save frame header metadata
                mFrameHeader = new FrameHeader();
                mFrameHeader.mOpcode = opcode;
                mFrameHeader.mFin = fin;
                mFrameHeader.mReserved = rsv;
                mFrameHeader.mPayloadLen = (int) payload_len;
                mFrameHeader.mHeaderLen = header_len;
                mFrameHeader.mTotalLen = mFrameHeader.mHeaderLen + mFrameHeader.mPayloadLen;
                if (masked) {
                    mFrameHeader.mMask = new byte[4];
                    for (int j = 0; j < 4; ++j) {
                        mFrameHeader.mMask[i] = (byte) (0xff & mMessageData[i + j]);
                    }
                    i += 4;
                } else {
                    mFrameHeader.mMask = null;
                }

                // continue processing when payload empty or completely buffered
                return mFrameHeader.mPayloadLen == 0 || mPosition >= mFrameHeader.mTotalLen;

            } else {

                // need more data
                return false;
            }
        } else {

            // need more data
            return false;
        }

    } else {

        /// \todo refactor this for streaming processing, incl. fail fast on invalid UTF-8 within frame already

        // within frame

        // see if we buffered complete frame
        if (mPosition >= mFrameHeader.mTotalLen) {

            // cut out frame payload
            byte[] framePayload = null;
            if (mFrameHeader.mPayloadLen > 0) {
                framePayload = new byte[mFrameHeader.mPayloadLen];
                System.arraycopy(mMessageData, mFrameHeader.mHeaderLen, framePayload, 0, mFrameHeader.mPayloadLen);
            }
            mMessageData = Arrays.copyOfRange(mMessageData, mFrameHeader.mTotalLen, mMessageData.length + mFrameHeader.mTotalLen);
            mPosition -= mFrameHeader.mTotalLen;

            if (mFrameHeader.mOpcode > 7) {
                // control frame
                if (mFrameHeader.mOpcode == 8) {

                    int code = 1005; // CLOSE_STATUS_CODE_NULL : no status code received
                    String reason = null;

                    if (mFrameHeader.mPayloadLen >= 2) {

                        // parse and check close code - see http://tools.ietf.org/html/rfc6455#section-7.4
                        code = (framePayload[0] & 0xff) * 256 + (framePayload[1] & 0xff);
                        if (code < 1000
                                || (code >= 1000 && code <= 2999 &&
                                code != 1000 && code != 1001 && code != 1002 && code != 1003 && code != 1007 && code != 1008 && code != 1009 && code != 1010 && code != 1011)
                                || code >= 5000) {

                            throw new WebSocketException("invalid close code " + code);
                        }

                        // parse and check close reason
                        if (mFrameHeader.mPayloadLen > 2) {

                            byte[] ra = new byte[mFrameHeader.mPayloadLen - 2];
                            System.arraycopy(framePayload, 2, ra, 0, mFrameHeader.mPayloadLen - 2);

                            Utf8Validator val = new Utf8Validator();
                            val.validate(ra);
                            if (!val.isValid()) {
                                throw new WebSocketException("invalid close reasons (not UTF-8)");
                            } else {
                                reason = new String(ra, "UTF-8");
                            }
                        }
                    }
                    onClose(code, reason);
                    // We have received a close, so lets set the state as early as possible.
                    // It seems that Handler() has a lag to deliver a message, so if the onClose
                    // is sent to master and just after that the other peer closes the socket,
                    // BufferedInputReader.read() will throw an exception which results in
                    // our code sending a ConnectionLost() message to master.
                    mState = STATE_CLOSED;

                } else if (mFrameHeader.mOpcode == 9) {
                    // dispatch WS ping
                    onPing(framePayload);

                } else if (mFrameHeader.mOpcode == 10) {
                    // dispatch WS pong
                    onPong(framePayload);

                } else {

                    // should not arrive here (handled before)
                    throw new Exception("logic error");
                }

            } else {
                // message frame

                if (!mInsideMessage) {
                    // new message started
                    mInsideMessage = true;
                    mMessageOpcode = mFrameHeader.mOpcode;
                    if (mMessageOpcode == 1 && mOptions.getValidateIncomingUtf8()) {
                        mUtf8Validator.reset();
                    }
                }

                if (framePayload != null) {

                    // immediately bail out on message too large
                    if (mMessagePayload.size() + framePayload.length > mOptions.getMaxMessagePayloadSize()) {
                        throw new WebSocketException("message payload too large");
                    }

                    // validate incoming UTF-8
                    if (mMessageOpcode == 1 && mOptions.getValidateIncomingUtf8() && !mUtf8Validator.validate(framePayload)) {
                        throw new WebSocketException("invalid UTF-8 in text message payload");
                    }

                    // buffer frame payload for message
                    mMessagePayload.write(framePayload);
                }

                // on final frame ..
                if (mFrameHeader.mFin) {

                    if (mMessageOpcode == 1) {

                        // verify that UTF-8 ends on codepoint
                        if (mOptions.getValidateIncomingUtf8() && !mUtf8Validator.isValid()) {
                            throw new WebSocketException("UTF-8 text message payload ended within Unicode code point");
                        }

                        // deliver text message
                        if (mOptions.getReceiveTextMessagesRaw()) {

                            // dispatch WS text message as raw (but validated) UTF-8
                            onRawTextMessage(mMessagePayload.toByteArray());

                        } else {

                            // dispatch WS text message as Java String (previously already validated)
                            String s = new String(mMessagePayload.toByteArray(), "UTF-8");
                            onTextMessage(s);
                        }

                    } else if (mMessageOpcode == 2) {

                        // dispatch WS binary message
                        onBinaryMessage(mMessagePayload.toByteArray());

                    } else {

                        // should not arrive here (handled before)
                        throw new Exception("logic error");
                    }

                    // ok, message completed - reset all
                    mInsideMessage = false;
                    mMessagePayload.reset();
                }
            }

            // reset frame
            mFrameHeader = null;

            // reprocess if more data left
            return mPosition > 0;

        } else {

            // need more data
            return false;
        }
    }
}
       processData()方法比较长,读起来也比较费力,这里就不再详细解读了,总之这块的代码的实现一定是严格按照WebSocket协议来解析数据的,如果有小伙伴对该协议还不太清楚,请参照上文。processData()方法解析到数据后会调用相关回调方法最终把数据通过Handler传递到主线程。这是createReader()方法的主要流程,那我们现在看一下createWriter()方法是如何操作的,源码如下:
/**
 * Create WebSockets background writer.
 */
protected void createWriter() throws IOException {

    mWriterThread = new HandlerThread("WebSocketWriter");
    mWriterThread.start();
    mWriter = new WebSocketWriter(mWriterThread.getLooper(), mMasterHandler, mSocket, mOptions);
}
       createWriter()方法通过HandlerThread创建了WebSocketWriter对象,如果有对HandlerThread不熟悉的小伙伴可以阅读一下我之前的一篇文章: Android 源码系列之<七>从源码的角度深入理解IntentService及HandlerThread。createWriter()方法执行完毕后,当在测试demo中输入数据点击SEND按钮后辗转调用的是WebSocketConnection的sendTextMessage()方法,该方法中调用的是mWriter的forward()方法,源码如下:
/**
 * Call this from the foreground (UI) thread to make the writer
 * (running on background thread) send a WebSocket message on the
 * underlying TCP.
 *
 * @param message Message to send to WebSockets writer. An instance of the message
 *                classes inside WebSocketMessage or another type which then needs
 *                to be handled within processAppMessage() (in a class derived from
 *                this class).
 */
public void forward(Object message) {
    // We have already quit, we are no longer sending messages.
    if (!mActive) {
        if (DEBUG) Log.d(TAG, "We have already quit, not processing further messages");
        return;
    }
    Message msg = obtainMessage();
    msg.obj = message;
    sendMessage(msg);
}
       forward()方法仅仅是发送了一个Message,该方法目的是进行线程的调度,把发送操作切换到子线程中,我们看一下processMessage()方法,源码如下:
/**
 * Sends a WebSockets frame. Only need to use this method in derived classes which implement
 * more message types in processAppMessage(). You need to know what you are doing!
 *
 * @param opcode  The WebSocket frame opcode.
 * @param fin     FIN flag for WebSocket frame.
 * @param payload Frame payload or null.
 * @param offset  Offset within payload of the chunk to send.
 * @param length  Length of the chunk within payload to send.
 */
protected void sendFrame(int opcode, boolean fin, byte[] payload, int offset, int length) throws IOException {

    // first octet
    byte b0 = 0;
    if (fin) {
        b0 |= (byte) (1 << 7);
    }
    b0 |= (byte) opcode;
    write(b0);

    // second octet
    byte b1 = 0;
    if (mOptions.getMaskClientFrames()) {
        b1 = (byte) (1 << 7);
    }

    long len = length;

    // extended payload length
    if (len <= 125) {
        b1 |= (byte) len;
        write(b1);
    } else if (len <= 0xffff) {
        b1 |= (byte) (126 & 0xff);
        write(b1);
        write(new byte[]{(byte) ((len >> 8) & 0xff),
                (byte) (len & 0xff)});
    } else {
        b1 |= (byte) (127 & 0xff);
        write(b1);
        write(new byte[]{(byte) ((len >> 56) & 0xff),
                (byte) ((len >> 48) & 0xff),
                (byte) ((len >> 40) & 0xff),
                (byte) ((len >> 32) & 0xff),
                (byte) ((len >> 24) & 0xff),
                (byte) ((len >> 16) & 0xff),
                (byte) ((len >> 8) & 0xff),
                (byte) (len & 0xff)});
    }

    byte mask[] = null;
    if (mOptions.getMaskClientFrames()) {
        // a mask is always needed, even without payload
        mask = newFrameMask();
        write(mask[0]);
        write(mask[1]);
        write(mask[2]);
        write(mask[3]);
    }

    if (len > 0) {
        if (mOptions.getMaskClientFrames()) {
            /// \todo optimize masking
            /// \todo masking within buffer of output stream
            for (int i = 0; i < len; ++i) {
                payload[i + offset] ^= mask[i % 4];
            }
        }
        mBufferedOutputStream.write(payload, offset, length);
    }
}

       sendFrame()方法把我们发送的数据根据WebSocket协议封装成符合规范的协议帧然后通过mBufferedOutputStream的write()方法发送出去了。整套流程其实很简单,就是通过Socket管道然后按照WebSocket协议进行消息的发送和接收。当然在项目中如果有精力也可以自己约定一套协议,只要都遵循了该协议都是没有问题的。

       原本打算把okhttp的WebSocket模块也讲解一下由于篇幅原因,这里就不再讲解了,okhttp的WebSocket模块同样遵循的是WebSocket协议,只是实现上和autobahn略有不同罢了,如果有小伙伴对okhttp的WebSocket模块感兴趣,请自行查阅。最后,感谢小伙伴们的收看(*^__^*) …






发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/72987090