WebSocket 入门
WebSocket是HTML5开始提供的一种浏览器与服务间进行全双工通信的网络技术,WebSocket通信协议于2011年被IETF定为标准RFC6455,WebSocket API 被W3C定为标准。 在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器间就形成了一条快速通道,两者就可以直接互相传送数据了。WebSocket基于TCP双向全双工进行消息传递。
单一的TCP连接,采用全双工模式通信对代理 、 防火墙和路由器透明无头部信息、Cookie和身份验证无安全开销通过ping/pong帧保持链路激活服务器可以主动传递消息给客户端
WebSocket连接建立
建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求信息例如:
GET /chat HTTP/1.1
Host:
server.com
Upgrade:
websocket
Connection:
Upgrade
Sec-WebSocket-Key:
dGhIIHNbXBsZSBub25jzQ==
Origin:
http://test.com
Sec-WebSocket-Protocal:
chat, superchat
Sec-WebSocket-Version:
13
客户端首先向服务器发起一个HTTP请求,和通常的HTTP请求不同,它包含了一些附加头信息,其中附加头信息“Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求。服务器端解析附加的头信息,然后生成应答信息返回给客户端。客户端与服务端的WebSocket连接就建立起来了。 服务端应答示例:
HTTP/1.1 101 Switching Protocols
Upgrade:
websocket
Connection:
Upgrade
Sec-WebSocket-Accept:
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocal:
chat
请求消息中的Sec-WebSocket-Key是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把”Sec-WebSocket-Key”加上魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11” ,使用SHA-1加密,然后进行BASE-64编码,将结果作为”Sec-WebSocket-Accept”头的值,返回给客户端。
WebSocket生命周期
略…
WebSocket连接关闭
略…
Netty WebSocket协议开发
WebSocket 服务端开发
WebSocket服务端接收到请求消息之后,先对消息的类型进行判断,如果不是WebSocket握手请求消息,则返回HTTP 400 BAD REQUEST响应给客户端,客户端的握手请求消息示例:
DefaultFullHttpRequest, decodeResult: success)
GET /websocket HTTP/
1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:
8080
Origin: http:
Sec
-WebSocket-Key: qwor7m5RbsgzZa8UT8bC5A
==
Sec
-WebSocket-Version:
13
Sec
-WebSocket-Extensions: x
-webkit-deflate-frame
Content
-Length:
0
服务端对握手请求消息进行处理, 构造握手响应返回,双方的socket连接正式建立。 连接建立成功后,到被关闭之前,双方都可以主动向对方发送消息。
代码实现 聊天室示例
HttpRequestHandler
import java
.io.File
import java
.io.RandomAccessFile
import java
.net.URISyntaxException
import java
.net.URL
import io
.netty.channel.Channel
import io
.netty.channel.ChannelFuture
import io
.netty.channel.ChannelFutureListener
import io
.netty.channel.ChannelHandlerContext
import io
.netty.channel.DefaultFileRegion
import io
.netty.channel.SimpleChannelInboundHandler
import io
.netty.handler.codec.http.DefaultFullHttpResponse
import io
.netty.handler.codec.http.DefaultHttpResponse
import io
.netty.handler.codec.http.FullHttpRequest
import io
.netty.handler.codec.http.FullHttpResponse
import io
.netty.handler.codec.http.HttpHeaders
import io
.netty.handler.codec.http.HttpResponse
import io
.netty.handler.codec.http.HttpResponseStatus
import io
.netty.handler.codec.http.HttpVersion
import io
.netty.handler.codec.http.LastHttpContent
import io
.netty.handler.ssl.SslHandler
import io
.netty.handler.stream.ChunkedNioFile
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String wsUri
private static final File INDEX
static {
URL location = HttpRequestHandler
.class.getProtectionDomain()
.getCodeSource()
.getLocation()
try {
String path = location
.toURI() +
"WebsocketChatClient.html"
path = !path
.contains(
"file:") ? path : path
.substring(
5)
INDEX = new File(path)
} catch (URISyntaxException e) {
throw new IllegalStateException(
"Unable to locate WebsocketChatClient.html", e)
}
}
public HttpRequestHandler(String wsUri) {
this
.wsUri = wsUri
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion
.HTTP_1_1, HttpResponseStatus
.CONTINUE)
ctx
.writeAndFlush(response)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx
.channel()
System
.out.println(
"Client:"+incoming
.remoteAddress()+
"异常")
// 当出现异常就关闭连接
cause
.printStackTrace()
ctx
.close()
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri
.equalsIgnoreCase(request
.getUri())) {
ctx
.fireChannelRead(request
.retain())
} else {
if (HttpHeaders
.is100ContinueExpected(request)) {
send100Continue(ctx)
}
RandomAccessFile file = new RandomAccessFile(INDEX,
"r")
HttpResponse response = new DefaultHttpResponse(request
.getProtocolVersion(), HttpResponseStatus
.OK)
response
.headers()
.set(HttpHeaders
.Names.CONTENT_TYPE,
"text/html; charset=UTF-8")
boolean keepAlive = HttpHeaders
.isKeepAlive(request)
if (keepAlive) {
response
.headers()
.set(HttpHeaders
.Names.CONTENT_LENGTH, file
.length())
response
.headers()
.set(HttpHeaders
.Names.CONNECTION, HttpHeaders
.Values.KEEP_ALIVE)
}
ctx
.write(response)
if (ctx
.pipeline()
.get(SslHandler
.class) == null) {
ctx
.write(new DefaultFileRegion(file
.getChannel(),
0, file
.length()))
} else {
ctx
.write(new ChunkedNioFile(file
.getChannel()))
}
ChannelFuture future = ctx
.writeAndFlush(LastHttpContent
.EMPTY_LAST_CONTENT)
if (!keepAlive) {
future
.addListener(ChannelFutureListener
.CLOSE)
}
file
.close()
}
}
}
TextWebSocketFrameHandler
import io
.netty.channel.Channel
import io
.netty.channel.ChannelHandlerContext
import io
.netty.channel.SimpleChannelInboundHandler
import io
.netty.channel.group.ChannelGroup
import io
.netty.channel.group.DefaultChannelGroup
import io
.netty.handler.codec.http.websocketx.TextWebSocketFrame
import io
.netty.util.concurrent.GlobalEventExecutor
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor
.INSTANCE)
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx
.channel()
for (Channel channel : channels) {
channel
.writeAndFlush(new TextWebSocketFrame(
"[SERVER] - " + incoming
.remoteAddress() +
" 加入"))
}
channels
.add(ctx
.channel())
System
.out.println(
"Client:"+incoming
.remoteAddress() +
"加入")
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx
.channel()
for (Channel channel : channels) {
channel
.writeAndFlush(new TextWebSocketFrame(
"[SERVER] - " + incoming
.remoteAddress() +
" 离开"))
}
System
.out.println(
"Client:"+incoming
.remoteAddress() +
"离开")
channels
.remove(ctx
.channel())
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx
.channel()
System
.out.println(
"Client:"+incoming
.remoteAddress()+
"在线")
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx
.channel()
System
.out.println(
"Client:"+incoming
.remoteAddress()+
"掉线")
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx
.channel()
System
.out.println(
"Client:"+incoming
.remoteAddress()+
"异常")
// 当出现异常就关闭连接
cause
.printStackTrace()
ctx
.close()
}
@Override
protected void messageReceived(ChannelHandlerContext ctx,TextWebSocketFrame msg) throws Exception {
Channel incoming = ctx
.channel()
for (Channel channel : channels) {
if (channel != incoming){
channel
.writeAndFlush(new TextWebSocketFrame(
"[" + incoming
.remoteAddress() +
"]" + msg
.text()))
} else {
channel
.writeAndFlush(new TextWebSocketFrame(
"[you]" + msg
.text() ))
}
}
}
}
WebsocketChatServer
import io
.netty.bootstrap.ServerBootstrap
import io
.netty.channel.ChannelFuture
import io
.netty.channel.ChannelOption
import io
.netty.channel.EventLoopGroup
import io
.netty.channel.nio.NioEventLoopGroup
import io
.netty.channel.socket.nio.NioServerSocketChannel
public class WebsocketChatServer {
private int port
public WebsocketChatServer(int port) {
this
.port = port
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup()
EventLoopGroup workerGroup = new NioEventLoopGroup()
try {
ServerBootstrap b = new ServerBootstrap()
b
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel
.class)
.childHandler(new WebsocketChatServerInitializer())
.option(ChannelOption
.SO_BACKLOG,
128)
.childOption(ChannelOption
.SO_KEEPALIVE, true)
System
.out.println(
"WebSocketChatServer 启动")
// 绑定端口,开始接收进来的连接
ChannelFuture f = b
.bind(port)
.sync()
// 等待服务器 socket 关闭
f
.channel()
.closeFuture()
.sync()
} finally {
workerGroup
.shutdownGracefully()
bossGroup
.shutdownGracefully()
System
.out.println(
"WebSocketChatServer 关闭")
}
}
public static void main(String[] args) throws Exception {
int port
if (args
.length >
0) {
port = Integer
.parseInt(args[
0])
} else {
port =
8888
}
new WebsocketChatServer(port)
.run()
}
}
WebsocketChatServerInitializer
import io
.netty.channel.ChannelInitializer
import io
.netty.channel.ChannelPipeline
import io
.netty.channel.socket.SocketChannel
import io
.netty.handler.codec.http.HttpObjectAggregator
import io
.netty.handler.codec.http.HttpServerCodec
import io
.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler
import io
.netty.handler.stream.ChunkedWriteHandler
public class WebsocketChatServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch
.pipeline()
pipeline
.addLast(new HttpServerCodec())
pipeline
.addLast(new HttpObjectAggregator(
64*
1024))
pipeline
.addLast(new ChunkedWriteHandler())
pipeline
.addLast(new HttpRequestHandler(
"/ws"))
pipeline
.addLast(new WebSocketServerProtocolHandler(
"/ws"))
pipeline
.addLast(new TextWebSocketFrameHandler())
}
}
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat
</title>
</head>
<body>
<form onsubmit="return false;">
<h3>WebSocket 聊天室:
</h3>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
<br>
<input type="text" name="message" style="width: 300px" value="Hello">
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录">
</form>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8888/ws");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data;
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "连接开启!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "连接被关闭";
};
} else {
console.error("你的浏览器不支持 WebSocket!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
console.error("连接没有开启.");
}
}
</script>
</body>
</html>