1. WebSocket介绍
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。简单的建立握手的时序图如下:
握手过程:
- Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
- 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。
- WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
- Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
2. Tomcat 7中的Websocket架构
如图所示,因为Websocket通信分为握手和数据传输两个过程,两个过程中需要用到的处理方式是不一样的,握手过程是基于HTTP 1.1基础上的,而数据传输是直接基于TCP的流传输。
握手过程中,在HttpServletRequest的基础上,封装了WsHttpServletRequest类,添加了对Request的失效操作函数invalidate()。而在数据通信时,接受和处理数据过程中,基于org.apache.coyote.http11.upgrade.UpgradeInbound重新封装了用于处理数据输入流的类StreamInbound,并在StreamInbound的基础上扩展生成了用于消息处理的类MessageInbound。在这两个数据处理类中均留有onData,onTextData/onBinaryData,onOpen,onClose等事件操作函数接口,这些接口将在载入的代码类中实现业务逻辑。在用于数据输出流的类WsOutbound则是封装了UpgradeOutbound对象实例,基于UpgradeOutbound对象的基础上,添加了websocket响应有关的处理逻辑。这里处理函数均为同步调用的函数,保证websocket响应的时序性。
Tomcat中Websocket的处理流程如下:
- 接收客户端发来的握手请求,Coyote.http11连接器对socket进行解析,形成HttpServletRequest发送给Container。
- Container中的相应WebsocketServlet处理请求,如不接受连接请求,则返回,如接受连接请求,则对请求作出响应,建立起客户端和服务器的socket连接。
- 服务器此时可以通过WsOutbound发送数据给客户端,同时通过StreamInbound监听socket。
- 如果接收到客户端发来的数据,则将socket数据解析成frame,判断frame类型,通过事件分发数据到不同的逻辑处理流程。
- 数据返回时调用WsOutbound对返回的数据进行封装处理,发送给客户端
3. 代码分析
- WebSocketServlet
这个类负责WS的握手过程,通过对HTTP请求头的判断确定是否接受连接请求。接受连接请求后则建立websocket数据连接,连接建立过程如下所示:
WsHttpServletRequestWrapper wrapper = new WsHttpServletRequestWrapper(req); //将HttpServletRequest封装可进行失效操作的WsHttpServletRequestWrapper StreamInbound inbound = createWebSocketInbound(subProtocol, wrapper); //建立数据连接,监听对应的端口 wrapper.invalidate(); //握手完,对这个Request进行invalidate处理
2. StreamInbound
这个类最关键的是onData()函数,即接收到数据后的处理函数。这个函数里对接受的数据进行解析,并根据操作码分发给不同的处理函数。
WsInputStream wsIs = new WsInputStream(processor, getWsOutbound()); //根据当前的Processor和定制的WsOutbound输出流对象,构建输入流的解析对象 try { WsFrame frame = wsIs.nextFrame(true); //查找数据中的下一个Frame while (frame != null) { byte opCode = frame.getOpCode(); //查找Frame中的操作码 if (opCode == Constants.OPCODE_BINARY) { doOnBinaryData(wsIs); //处理Binary数据 } else if (opCode == Constants.OPCODE_TEXT) { InputStreamReader r = new InputStreamReader(wsIs, new Utf8Decoder()); doOnTextData(r); //处理文本数据 } else if (opCode == Constants.OPCODE_CLOSE){ closeOutboundConnection(frame); //数据发送完毕,发送close frame return SocketState.CLOSED; } else if (opCode == Constants.OPCODE_PING) { getWsOutbound().pong(frame.getPayLoad()); //发送pong frame } else if (opCode == Constants.OPCODE_PONG) { } else { closeOutboundConnection( Constants.STATUS_PROTOCOL_ERROR, null); return SocketState.CLOSED; } frame = wsIs.nextFrame(false); } }
3. MessageInbound
该类是StreamInbound的扩展类,实现了对文本数据的解析函数。文本处理过程中,主要用到了ByteBuffer和CharBuffer,通过对Buffer的操作实现文本数据的解析。
4. WsOutbound
该类是处理Websocket输出流的类,实现了Websocket几个close,pong,ping和正常数据响应frame。比如在输出文本数据的处理函数里:
public synchronized void writeTextData(char c) throws IOException { if (closed) { //数据流已关闭 throw new IOException(sm.getString("outbound.closed")); } if (cb.position() == cb.capacity()) { //没有数据可以返回 doFlush(false); } if (text == null) { text = Boolean.TRUE; } else if (text == Boolean.FALSE) { //如果已经写好数据准备传输 flush(); //输出数据 text = Boolean.TRUE; } cb.append(c); //将添加到CharBuffer中 }
5. WsInputStream
这个类主要用于输入流的解析,将数据从InputStream中解析成websocket的frame。类的关键逻辑在read()函数中:
public int read(byte b[], int off, int len) throws IOException { makePayloadDataAvailable(); //确保有Payload数据可供读取 if (remaining == 0) { //frame数据已经读到尾 return -1; } if (len > remaining) { //重置可读取数据长度 len = (int) remaining; } int result = processor.read(true, b, off, len); //调用Processor进行数据读取 if(result == -1) { return -1; } for (int i = off; i < off + result; i++) { b[i] = (byte) (b[i] ^ frame.getMask()[(int) ((readThisFragment + i - off) % 4)]); //获取帧的Mask } remaining -= result; readThisFragment += result; //已读帧数据 return result; }