Java NIO实现WebSocket服务器
来源:互联网 发布:sql server 修改语句 编辑:程序博客网 时间:2024/06/08 12:26
简介
在HTTP请求中,服务器往往处于被动的一方,通常都是客户端向服务器发送请求时,服务器才会做出响应,服务器并不会主动向客户端推送消息。因此WebSocket API就为此诞生。WebSocket API是HTML5中的一大特色,能够使得建立连接的双方在任意时刻相互推送消息,这意味着不同于HTTP,服务器服务器也可以主动向客户端推送消息了。
关于WebSocket的介绍,可以参考下一篇博文http://blog.csdn.net/zwto1/article/details/52493119#websocket%E5%8E%9F%E7%90%86
WebSocket协议的格式
为了实现一个能与H5的WebSocket API通信的服务器,我们需要先熟悉WebSocket数据包的格式。定的格式。
握手数据包
在一个连接建立以后,建立连接的双方才可以互相推送消息。双方通过握手即可建立一个连接。握手数据包的格式如下:
客户端向服务器发起请求
可以见到,客户端请求连接建立的数据包是一个字符串,而且第一行表明这实际上是一个HTTP报文。其中Connection: Upgrade以及Upgrade: websocket两字段就是用来告知服务器这是一个WebSocket握手请求。
服务器还要关心的一个字段是Sec-WebSocket-Key(倒数第二行),其值是一个随机base64字符串,服务器怎么处理该字符串请往下看。
服务器回应请求
可以看到HTTP状态码为101,同样,服务端也带有Connection和Upgrade字段来表明这是一个WebSocket数据包。
Sec-WebSocket-Accept字段是对请求报文中Sec-WebSocket-Key字段进行摘要运算的结果。其运算过程如下
1、将Sec-WebSocket-Key字段的值与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
2、对拼接后的字符串进行sha1运算,得到160位摘要(二进制)。
3、以base64的形式表示得到的摘要。
客户端会进行同样的运算,并且与服务器返回来的字段作对比,如果发现二者不相同,连接就无法建立了。
通信数据帧
通信数据帧的格式如下(参考官方文档https://www.rfc-editor.org/rfc/rfc6455.txt)
其中各个字段的含义如下
FIN: 1bit,表示这是否为分片的最后一个数据帧。这是考虑到发送的数据有可能被分片的情况,如果存在分片,将此字段置1就表明这是最后一个分片。如果不存在分片,此字段恒为1。因为只有一个分片就一定是最后一个分片。
RSV1, RSV2, RSV3: 各1bit,全0。现在暂时用不上,为了将来可能用于功能拓展保留的字段。
Opcode: 4bits
指出数据的类型,值的解释如下
MASK: 1bit
表明是否对数据进行掩码运算,置1表示使用掩码。从客户端向服务器发送的数据必须使用掩码。
Payload length: 7 bits, 7+16 bits, or 7+64 bits
表明数据的长度。
如果长度在0-125内,这7bits就表示数据的长度;
如果值为126,紧接着后面2字节(16bits)才表示数据的长度;
如果值为127,后面8字节(64bits)表示数据的长度。
Masking-key: 无 或 4 字节
如果掩码字段(MASK)置0,就不需要Masking-key。如果掩码字段为1,这4字节就是Masking-key,用它与数据部分进行异或运算。
Payload Data: 数据部分,长度可变。
关于其他详细说明可以参考官方文档,例如消息分片规则等。
实现一个WebSocket服务器(群聊天室例子)
为了更加深刻的理解这样一个协议,这里没有使用Java已经封装好操作的类库。
基于NIO监听端口
基于NIO中的ServerSocketChannel,实现一个接收并读取Socket内容的服务端套路如下。
public class WebSocketServer { private Selector serverSelector; private WebSocketListener socketListener; private boolean isRunning = true; public WebSocketServer(int serverPort, WebSocketListener socketListener) throws IOException { //初始化ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(serverPort)); serverSocketChannel.configureBlocking(false); //创建选择器 serverSelector = Selector.open(); //注册ServerSocketChannel的ACCEPT事件至选择器 serverSocketChannel.register(serverSelector, SelectionKey.OP_ACCEPT); this.socketListener = socketListener; } public void run() throws IOException { while (isRunning) { int selectCount = serverSelector.select(); if (selectCount == 0) continue; Iterator<SelectionKey> iterator = serverSelector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectKey = iterator.next(); if (selectKey.isAcceptable()) { //ACCEPT就绪,此时调用ServerSocketChannel的accept()方法可获得连接的SocketChannel对象,将其READ事件注册到选择器,就可以读取内容了。 ServerSocketChannel serverChannel = (ServerSocketChannel) selectKey.channel(); SocketChannel acceptSocketChannel = serverChannel.accept(); acceptSocketChannel.configureBlocking(false); //记得设置为非阻塞模式 acceptSocketChannel.register(serverSelector, SelectionKey.OP_READ); } else if (selectKey.isReadable()) { //TODO 读取并处理数据 } iterator.remove(); } } }}
会话管理
在本聊天室中,一个WebSocket连接就视为一个会话,也就是一个用户登录。定义ClientSession来管理每一个连接的SocketChannel。
public class ClientSession { private SocketChannel socketChannel; private String sessionID; public ClientSession(SocketChannel channel) { this.socketChannel = channel; try { MessageDigest sha1 = MessageDigest.getInstance("sha1"); sha1.update(Util.longToByteArray(System.currentTimeMillis())); BigInteger bi = new BigInteger(sha1.digest()); sessionID = bi.toString(16); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } public SocketChannel getSocketChannel() { return socketChannel; } public String getSessionID() { return sessionID; }}
会话的几种状态
对于聊天室的一个用户从建立连接到释放连接,服务端最关心的无非是四个关键点:会话建立,收到来自客户端的消息,会话关闭,抛出异常。可以将其抽象成接口。
interface WebSocketListener { void onOpen(ClientSession session) throws IOException; void onMessage(ClientSession session) throws IOException; void onException(ClientSession session, Exception ex); void onClose(ClientSession session) throws IOException;}
处理SocketChannel
前面一开始的关于ServerSocketChannel代码中,还没对SocketChannel进行处理,现在来补上。
public class WebSocketServer { private Selector serverSelector; private WebSocketListener socketListener; private boolean isRunning = true; //...部分代码省略 public void run() throws IOException { while (isRunning) { int selectCount = serverSelector.select(); if (selectCount == 0) continue; Iterator<SelectionKey> iterator = serverSelector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectKey = iterator.next(); if (selectKey.isAcceptable()) { //重复代码省略 } else if (selectKey.isReadable()) { try { SocketChannel socketChannel = (SocketChannel) selectKey.channel(); ClientSession session = (ClientSession) selectKey.attachment(); //用前面定义的ClientSession来作为SocketChannel的attach object,方便存储关于SocketChannel的其他信息,容易管理。 if (session == null) { //如果SocketChannel还没有被ClientSession绑定,认为这是一个新连接,需要完成握手 byte[] byteArray = Util.readByteArray(socketChannel); System.out.println(new String(byteArray)); WSProtocol.Header header = WSProtocol.Header.decodeFromString(new String(byteArray)); String receiveKey = header.getHeader("Sec-WebSocket-Key"); String response = WSProtocol.getHandShakeResponse(receiveKey); socketChannel.write(ByteBuffer.wrap(response.getBytes())); ClientSession newSession = new ClientSession(socketChannel); selectKey.attach(newSession); socketListener.onOpen(newSession); //会话打开 } else { //收到数据,交给上面定义的接口处理 socketListener.onMessage(session); } } catch (IOException e) { e.printStackTrace(); //出现异常,进行一系列处理 selectKey.channel().close(); selectKey.cancel(); ClientSession attSession = (ClientSession) selectKey.attachment(); socketListener.onException(attSession, e); //抛出异常 socketListener.onClose(attSession); //强制关闭抛出异常的连接 } } iterator.remove(); } } }}//...省略下部分代码
数据包的处理
数据包的处理放在单独一个类里面
class WSProtocol { static class Header { private Map<String, String> headers = new HashMap<>(); String getHeader(String key) { return headers.get(key); } static Header decodeFromString(String headers) { Header header = new Header(); Map<String, String> headerMap = new HashMap<>(); String[] headerArray = headers.split("\r\n"); for (String headerLine : headerArray) { if (headerLine.contains(":")) { int splitPos = headerLine.indexOf(":"); String key = headerLine.substring(0, splitPos); String value = headerLine.substring(splitPos + 1).trim(); headerMap.put(key, value); } } header.headers = headerMap; return header; } } static String getHandShakeResponse(String receiveKey) { String keyOrigin = receiveKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; MessageDigest sha1; String accept = null; try { sha1 = MessageDigest.getInstance("sha1"); sha1.update(keyOrigin.getBytes()); accept = new String(Base64.getEncoder().encode(sha1.digest())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } String echoHeader = ""; echoHeader += "HTTP/1.1 101 Switching Protocols\r\n"; echoHeader += "Upgrade: websocket\r\n"; echoHeader += "Connection: Upgrade\r\n"; echoHeader += "Sec-WebSocket-Accept: " + accept + "\r\n"; echoHeader += "\r\n"; return echoHeader; }}
另外,还需要实现接口来处理会话的几种状态。
class WebSocketListenerImpl implements WebSocketListener { private Map<String, ClientSession> connSessionMap = new HashMap<>(); @Override public void onOpen(ClientSession session) throws IOException { connSessionMap.put(session.getSessionID(), session); sendBoardCast(session.getSocketChannel().socket().getInetAddress().getHostName() + ":" + session.getSocketChannel().socket().getPort() + " Join", session); Log.info("session open: " + session.getSessionID()); } @Override public void onMessage(ClientSession session) throws IOException { SocketChannel socketChannel = session.getSocketChannel(); byte[] bytesData = Util.readByteArray(socketChannel); //opcode为8,对方主动断开连接 if ((bytesData[0] & 0xf) == 8) { throw new IOException("session disconnect."); } byte payloadLength = (byte) (bytesData[1] & 0x7f); byte[] mask = Arrays.copyOfRange(bytesData, 2, 6); byte[] payloadData = Arrays.copyOfRange(bytesData, 6, bytesData.length); for (int i = 0; i < payloadData.length; i++) { payloadData[i] = (byte) (payloadData[i] ^ mask[i % 4]); } String echoData = "[" + session.getSocketChannel().socket().getInetAddress().getHostAddress() + ":" + session.getSocketChannel().socket().getPort() + "]" + (new String(payloadData)); sendBoardCast(echoData, session); } @Override public void onException(ClientSession session, Exception ex) { Log.info("exception catch: " + ex.getMessage()); } @Override public void onClose(ClientSession session) throws IOException { connSessionMap.remove(session.getSessionID()); sendBoardCast(session.getSocketChannel().socket().getInetAddress().getHostName() + ":" + session.getSocketChannel().socket().getPort() + " Leave", session); Log.info("closed sessionId = " + session.getSessionID()); } private void sendBoardCast(String message, ClientSession ownSession) throws IOException { Iterator<ClientSession> iterator = connSessionMap.values().iterator(); while (iterator.hasNext()) { ClientSession nextSession = iterator.next(); if (nextSession == ownSession) { continue; } byte[] boardCastData = new byte[2 + message.getBytes().length]; boardCastData[0] = (byte) 0x81; boardCastData[1] = (byte) message.getBytes().length; System.arraycopy(message.getBytes(), 0, boardCastData, 2, message.getBytes().length); nextSession.getSocketChannel().write(ByteBuffer.wrap(boardCastData)); } }}
这里考虑的极其简单,都是数据长度不超过126,且都是文本不分片的情况,有兴趣的可以按照WebSocket的文档将数据操作的过程写完整。
最后写一张简单的测试页面
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Chat</title></head><script> var socket = null; var isConn = false; socket = new WebSocket('ws://127.0.0.1:8888'); socket.onerror = function (err) { console.log(err); addError('连接错误') }; socket.onopen = function () { isConn = true; addMessage('连接成功'); console.log('open'); }; socket.onmessage = function (event) { console.log(event.data); addMessage(event.data); }; socket.onclose = function () { console.log('close') }; function sendMessage() { var sendText = document.getElementById('input_text').value; if (!isConn) { addError('发送失败'); } else { if (!sendText) { addError('不要发送空消息'); } else { addMessage('<label style="font-style: oblique">' + '[我]' + sendText + '</label>'); socket.send(sendText); } } } function addMessage(message) { var textShow = document.getElementById('show-message'); textShow.innerHTML += message + '<br>' } function addError(error) { var textShow = document.getElementById('show-message'); textShow.innerHTML += '<label style="color: red">[Error] ' + error + '</label><br>' }</script><style> button { border-radius: 5px; padding: 8px; color: white; border: none; } input { border-radius: 4px; width: 300px; padding: 6px; border: 1px solid dodgerblue; } #show-message { border-radius: 4px; height: 320px; width: 480px; border: 1px double darkgray; padding: 8px; }</style><body><div id="show-message"></div><div style="margin-top: 20px"> <input id="input_text" type="text" placeholder="message"> <button onclick="sendMessage()" style="background-color: dodgerblue">Send</button></div></body></html>
运行效果
- Java NIO实现WebSocket服务器
- Java 时间服务器demo之NIO实现
- java NIO实现同步非阻塞服务器
- JAVA NIO 服务器与客户端实现示例
- websocket java服务器
- Java NIO服务器实例
- Java NIO服务器实例
- JAVA NIO 服务器(三)
- 【Java】BIO NIO 服务器
- WebSocket实现与服务器通信
- JAVA NIO non-blocking模式实现高并发服务器
- java nio实现服务器时遇到的问题
- [java]NIO服务器(ServerSocketChannel)开发的两种实现方式
- JAVA NIO non-blocking模式实现高并发服务器
- JAVA NIO使用非阻塞模式实现高并发服务器
- 采用Java nio 实现的一个简单的服务器
- JAVA NIO 服务器与客户端实现示例(代码1)
- JAVA NIO non-blocking模式实现高并发服务器
- px4原生源码学习二--实时操作系统篇
- 怎样知道做到
- 解决Failed to load class "org.slf4j.impl.StaticLoggerBinder"
- 操作符重载
- jsp页面格式化时间
- Java NIO实现WebSocket服务器
- 常用的布局及技巧
- px4原生源码学习三--Nuttx实时操作系统的使用
- java基础杂谈(二)之 switch的多重匹配
- JQuery判断是否为空
- 当当网爬虫
- grokdebuggera安装配置
- HadoopRPC框架-----模拟NameNode和Client通信
- delphi 窗体的创建和释放