- 本文目的
最近项目中使用到WebSocket 需要java 对WebSocket 进行一个封装,来回折腾了几次,最后发现xlightweb 比自己封装的代码 NB 多了(哎,功底不行!),但是,值得庆幸的时原理都差不多,起码没偏离。接下来介绍WebSocket 和 Java 怎么调用以及工具。希望对大家有用。
- WebSocket 简介
WebSockets是在一个(TCP)接口进行双向通信的技术,PUSH技术类型。同时WebSockets仍将基于W3C标准,目前为止,Chrome和Safari的最新版本浏览器已经支持WebSockets了。(更多关于websocket请上google)
- WebSocket Handshake
我重点结合xSocket 和 xlightweb 来说明WebSocket 的握手过程。
最开始开发是也没太在意这个握手其实一般,要它何用,不用去管它。 但是,有一个场景是服务器需要在握手是返回动态加密字符串,在握手后其他的消息交互中使用到。这就很有必要对 Handshake 成功后返回的 response header 进行解析了。
- xSocket 和 xlightweb
xSocket 是 对NIO 进行的封装非常好用, xlightweb 是在xSocket 基础上面扩展了对web协议的支持,比如Http, websocket ,如果这两个不太清楚也请上google搜索。
- WebSocket 之 Java API
大家都知道WebSocket 是以 0x00, 0xFF 开头和结尾来包住有效数据的,每个消息都是如此。 使用xSocket 的时候开始的时候 只知道 readByteBufferByDelimiter(String)或者 readByteBufferByDelimiter(length),还有一个就是按字节读取,很 2 的想法是怎么把0x00 转成字符串(非常2)。
好吧,既然xSocket 没有提供按字节 Delimiter 的,那就按字节读取,存入ByteBuffer[] 中,然后在遍历ByteBuffer[] ,使用 startWith == 0x00 && endWith == 0xFF 进行处理。大致原理如下:
- onData 中开启一个读取线程,有数据就 connect.getByte()
- 读取的数据存入 ByteBuffer[] 中
- 定期匹配 0x00, 0xFF ,匹配成功后 返回可用message,剩余的ByteBuffer 缓存后,等待下一次匹配。
基本是这样,但是后面发现自己代码没办法想HttpClient一样有效的处理 requestHeader 或者 responseHeader,虽然可以根据文本进行解析,分割。但是还是觉得不脱,代码不美观。到目前为止自己用xSocket 封装的代码已经基本够用了。再一次寻觅下看见了 xlightweb 已经封装了 websocket 的交互,查看代码和测试之后就毅然改用xlightweb 了。
xlightweb 的 websocket 交互代码:
HttpClient httpClient = new HttpClient();
httpClient.setConnectTimeoutMillis(60 * 1000);
IWebSocketConnection webSocketConnection = httpClient.openWebSocketConnection("ws://localhost:8080" , "Sample", new WebSocketHandler());
responseHeader = webSocketConnection.getUpgradeResponseHeader();
3,4行代码就已经处理了 websocket 的握手和返回的 responseHeader 了。 这个只是开始,我想要看见的是它怎么解析 0x00,0xFF 最后得到有效数据的。那么在看看 WebSocketHandler (自己取的类名)
public class WebSocketHandler implements IWebSocketHandler, IHttpRequestHandler {
@Override
public void onConnect(IWebSocketConnection webStream) throws IOException {
Log.i(TAG, "on connect, id : " + webStream.getId());
reconnect.set(false);
}
@Override
public void onDisconnect(IWebSocketConnection webStream) throws IOException {
Log.e(TAG, "on disconnect, id : " + webStream.getId());
reconnect.set(true);
}
@Override
public void onMessage(IWebSocketConnection webStream) throws IOException {
Log.i(TAG, "on message, id : " + webStream.getId());
TextMessage msg = webStream.readTextMessage();
taskExecutor.execute(new WebSocketTask( msg.toString() ));
}
@Override
public void onRequest(IHttpExchange exchange) throws IOException, BadMessageException {
}
}
重要的就是加粗的两行代码,其实就是 readTextMessage 就可以了,下面一行是我使用的多线程来分发message 避免阻塞通道。 好了,到目前为止着急使用Java API 来操作 websocket 的已经够用了:资源下载地址: http://xsocket.sourceforge.net/download.htm
想知道 readTextMessage 我们继续看看和学习 xlightweb 的代码
TestMessage 是 extends WebSocketMessage, WebSocketMessage 派生出来的有三个类:TextMessage,BinaryMessage,CloseMessage。
webStream.readTextMessage() 调用后返回的也是 WebSocketMessage,代码如下:
public WebSocketMessage readMessage() throws BufferUnderflowException, SocketTimeoutException, ClosedChannelException, IOException {
long start = System.currentTimeMillis();
long remainingTime = receiveTimeoutSec;
do {
synchronized (inQueue) {
IOException ioe = exceptionRef.getAndSet(null);
if (ioe != null) {
throw ioe;
}
if (isDisconnected.get()) {
throw new ClosedChannelException();
}
if (inQueue.isEmpty()) {
try {
inQueue.wait(remainingTime);
} catch (InterruptedException ie) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
} else {
inQueueVersion++;
return inQueue.remove(0);
}
}
remainingTime = HttpUtils.computeRemainingTime(start, receiveTimeoutSec);
} while (remainingTime > 0);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("receive timeout " + receiveTimeoutSec + " sec reached. throwing timeout exception");
}
throw new SocketTimeoutException("timeout " + receiveTimeoutSec + " sec reached");
}
其实就是开了一个循环在 remainngTime > 0 内有效。超过时间就抛出异常。 但是,就看见从inQueue 里面获取东西,什么时候写入到 inQueue 呢。 大家要记住xSocket是封装的NIO,xlightweb是借助xSocket 扩展的web 协议内容。 那么好吧,肯定有一个 Handler 来对应处理接收的数据,在WebSocketConnection 类中找到 WebSocketProtocolHandler,代码如下:
private final class WebSocketProtocolHandler implements IConnectHandler, IDataHandler, IDisconnectHandler {
// network data
private ByteBuffer rawBuffer = null;
public boolean onConnect(INonBlockingConnection connection) throws IOException, BufferUnderflowException, MaxReadSizeExceededException {
........
return true;
}
public boolean onData(INonBlockingConnection connection) throws IOException, BufferUnderflowException, ClosedChannelException, MaxReadSizeExceededException {
if (connection.isOpen()) {
// copying available network data into raw data buffer
int available = connection.available();
ByteBuffer[] data = null;
if (available > 0) {
data = connection.readByteBufferByLength(available);
}
onData(data);
}
return true;
}
void onData(ByteBuffer[] data) throws IOException {
if (data == null) {
if (rawBuffer == null) {
rawBuffer = ByteBuffer.allocate(0);
}
} else {
if (rawBuffer == null) {
rawBuffer = HttpUtils.merge(data);
} else {
rawBuffer = HttpUtils.merge(rawBuffer, data);
}
}
parse(rawBuffer);
if (!rawBuffer.hasRemaining()) {
rawBuffer = null;
}
}
void parse(ByteBuffer buffer) throws IOException {
while (buffer.hasRemaining()) {
WebSocketMessage msg = WebSocketMessage.parse(buffer);
if (msg == null) {
return;
} else {
if (msg.isTextMessage()) {
synchronized (inQueue) {
inQueueVersion++;
inQueue.add(msg);
inQueue.notifyAll();
}
synchronized (webSocketHandlerGuard) {
if (webSocketHandlerAdapter != null) {
webSocketHandlerAdapter.onMessage(WebSocketConnection.this);
}
}
} else if (msg.isCloseMessage()) {
if (isCloseMsgSent.get()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] echo close msg reveived. Destroying connection");
}
writeMessageIgnoreClose(msg);
destroy();
// peer initiated close
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] close msg reveived. echoing it and destroying connection");
}
isCloseMsgSent.set(true);
writeMessageIgnoreClose(msg);
destroy();
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] binary message received. The ws draft does not longer allow binary messages. Ignoring it");
}
}
}
}
}
在这里把数据从 onData 中收取下了并且进行转换后存入到 inQueue 中。这里已经知道它是怎么收取数据了,就差最后一步,怎么转化数据,分离有效信息的。看到这几行代码,回想下自己原来写的也可以这么做就行了。( )
if (connection.isOpen()) {
// copying available network data into raw data buffer
int available = connection.available(); //这里还没有仔细查看,如果谁有时间查看了,希望也转告我原理
ByteBuffer[] data = null;
if (available > 0) {
data = connection.readByteBufferByLength(available);
}
onData(data);
}
收到数据时还有一个 合并ByteBuffer 的过程,这个和我之前自己实现的大同小异,如果有不明白的可以自己查看代码。 收到数据后进行 parse,我们可以看到是调用了WebSocketMessage 的parse 方法,上面使用的是 TextMessage,所以对应调用的是 TextMessage 的parser 方法,代码如下:
private static final byte START_BYTE_TEXTFRAME = (byte) 0x00;
private static final byte END_BYTE = (byte) 0xFF;
static TextMessage parse(ByteBuffer buffer) throws IOException {
int savePos = buffer.position();
int saveLimit = buffer.limit();
while (buffer.hasRemaining()) {
byte b = buffer.get();
if ((b & END_BYTE) == END_BYTE) {
int pos = buffer.position();
buffer.limit(buffer.position() - 1);
buffer.position(savePos);
ByteBuffer msg = buffer.slice();
buffer.limit(saveLimit);
buffer.position(pos);
return new TextMessage(msg);
}
}
buffer.position(savePos);
buffer.limit(saveLimit);
return null;
}
到这里算是明白,清楚了。 就是遍历 ByteBuffer 然后匹配 END_BYTE 就可以 返回 new TextMessage 了。
xSocket 和 xlightweb 还可以做很多事情,我做得也就是基本功能,如果有更深入了解的人可以互相讨论,希望对大家有用。
补充一句:为什么直接用 OxFF可可以进行分包?
因为协议规定使用 UTF-8 进行编码,0xFF 是不会出现在数据 data 中的。