年前就写了点儿的东西,今天抽空把连接的部分给补完整了,之前写这个也是为了练练手,敲点儿代码。网上基本上都是C#、PHP或者socket写的版本,当然我也发现有用mina写的。其实用什么语言写都是一样的,只不过是对协议的解析。所以无论是用mina还是socket,代码量都是差不多的。没发现NIO版本的,就写了一些。
最早看见的文章来自IBM《使用 HTML5 WebSocket 构建实时 Web 应用》。这里面讲述的很清楚,什么是web socket。唯一遗憾的是,这篇文章中的协议版本不够新,现在的chrome已经支持到version 13了,而文章中的版本还是10。所以在编码和解码上有些差异。本文写的是13版本的代码。当然再文章最后有个JS的客户端还是可以拿来应用的。
首先当然是协议,请求端的协议格式如下:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:9020
Origin: null
Sec-WebSocket-Key: SN3OSin4/Zok8kmgrD8qxQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: x-webkit-deflate-frame
这里最重要的就是Sec-WebSocket-Key,服务器端需要通过这个key计算出相应的Accept值,这个值的计算步骤如下:
- 获取Sec-WebSocket-Key中的值,注意不要有空格
- 将Sec-WebSocket-Key的值加上“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”后进行SHA1摘要
- 将摘要后的值做BASE64编码,生成相应的Sec-WebSocket-Accept值。
简单的用代码模拟下:
/**Sec-WebSocket-Key*/
int keyindex=request.indexOf("Sec-WebSocket-Key:");
String key=request.substring(keyindex+19,keyindex+43);
System.out.println("Sec-WebSocket-Key:"+key);
/**计算结果*/
String new_key=key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
byte[] key_sha1=SHA1Utils.SHA1(new_key);
String result=new String(Base64.encode(key_sha1));
/**返回协议*/
StringBuilder sb = new StringBuilder("HTTP/1.1 101 Switching Protocols\r\n");
sb.append("Upgrade: websocket\r\n");
sb.append("Connection: Upgrade\r\n");
sb.append("Sec-WebSocket-Accept:"+result+"\r\n\r\n");
所以这样一来,返回协议的格式也就出来了:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:Prqb8zq7hH8B5tU2ujXT9dM9Gio=
如果上述步骤都正确的话,连接部分就算是成功了,连接上后,这是一个长连接,浏览器和服务器之间就维持着这个连接,直到一方发生中断。后续的就是解析格式,获取通信的数据,这里需要了解web socket的协议格式:
当然这部分代码还没有写,不过有了协议,解析就是一个耐心和细心的工作了,至于更详细的相关web socket可以参考相关RFC文档:http://datatracker.ietf.org/doc/rfc6455/?include_text=1
最后贴上服务器端的代码:
package socketserver;
import it.sauronsoftware.base64.Base64;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class WebSocketServer {
private ServerSocketChannel ssc;
private Selector selector;
private Set<SocketChannel> keylist = Collections
.synchronizedSet(new HashSet<SocketChannel>());
public WebSocketServer() throws IOException {
init();
}
private void init() throws IOException {
ssc = ServerSocketChannel.open();
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(9020));
selector = Selector.open();
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
if (ssc.isOpen() && selector.isOpen()) {
System.out.println("listening port:" + 9020 + "....");
polling();
}
}
private void polling() throws IOException {
while (true) {
try {
int n = selector.select();
if (n == 0) {
continue;
}
} catch (Exception e) {
e.printStackTrace();
}
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
acceptOperation(key);
} else if (key.isReadable()) {
readOperation(key);
}else if(key.isWritable()){
writeOperation(key);
}
}
}
}
private void writeOperation(SelectionKey key) {
// TODO Auto-generated method stub
}
private void acceptOperation(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("coming: " + client.socket().getReuseAddress());
// 保存连接上的client
keylist.add(client);
/**welcome the clients*/
ByteBuffer dst = ByteBuffer.allocate(1024);
StringBuilder sb = new StringBuilder("client:");
sb.append(client + "arrival");
dst.put(sb.toString().getBytes());
dst.flip();
broadcast(keylist, dst, client);
}
private void readOperation(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer dst = ByteBuffer.allocate(1024);
int n = -1;
try {
n = client.read(dst);
} catch (Exception ex) {
System.err.println("client has disconnect...");
}
if (n == -1) {
System.out.println(client.socket().getRemoteSocketAddress()+" leave.............");
key.cancel();
client.close();
keylist.remove(client);
return;
}
dst.flip();
StringBuilder sb = new StringBuilder();
if (dst.hasRemaining()) {
while (dst.hasRemaining()) {
sb.append((char) dst.get());
}
if (sb.toString().contains("HTTP/1.1")) {
dst.flip();
/**处理协议*/
ByteBuffer buffer = processProtocol(dst);
writeHTTP(keylist, buffer, client);
} else {
dst.flip();
broadcast(keylist, dst, client);
}
}
System.out.println("receive:-----------------\r\n"+sb.toString()+"\r\n");
dst.clear();
}
private ByteBuffer processProtocol(ByteBuffer dst) {
StringBuilder request=new StringBuilder();
while(dst.hasRemaining()){
request.append((char)dst.get());
}
/**Sec-WebSocket-Key*/
int keyindex=request.indexOf("Sec-WebSocket-Key:");
String key=request.substring(keyindex+19,keyindex+43);
System.out.println("Sec-WebSocket-Key:"+key);
/**计算结果*/
String new_key=key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
byte[] key_sha1=SHA1Utils.SHA1(new_key);
String result=new String(Base64.encode(key_sha1));
/**返回协议*/
StringBuilder sb = new StringBuilder("HTTP/1.1 101 Switching Protocols\r\n");
sb.append("Upgrade: websocket\r\n");
sb.append("Connection: Upgrade\r\n");
sb.append("Sec-WebSocket-Accept:"+result+"\r\n\r\n");
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(sb.toString().getBytes());
buffer.flip();
System.out.println(sb.toString());
return buffer;
}
private void writeHTTP(Set<SocketChannel> set, ByteBuffer dst,
SocketChannel currentClient) throws IOException {
currentClient.write(dst);
}
private void broadcast(Set<SocketChannel> set, ByteBuffer dst,
SocketChannel currentClient) throws IOException {
System.out.println("now has-" + set.size() + "-clients");
Iterator<SocketChannel> iter = set.iterator();
while (iter.hasNext()) {
SocketChannel sc = iter.next();
if (sc == currentClient) {
continue;
}
try {
sc.write(dst);
} catch (IOException e) {
continue;
}
dst.flip();
}
}
public static void main(String[] args) throws IOException {
WebSocketServer wss = new WebSocketServer();
}
}
sha1部分,其实没必要把这个抽出来:
package socketserver;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class SHA1Utils {
private static final String ENCODE = "UTF-8";
private static MessageDigest sha1MD;
public static byte[] SHA1(String text) {
if (null == sha1MD) {
try {
sha1MD = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
return null;
}
}
try {
sha1MD.update(text.getBytes(ENCODE), 0, text.length());
} catch (UnsupportedEncodingException e) {
sha1MD.update(text.getBytes(), 0, text.length());
}
return sha1MD.digest();
}
}
客户端的例子可以从IBM那篇文章中找到。
@?-\¢ M?'\\