目录
一. WebSocket协议
1. WebSocket协议基础
2. WebSocket协议特点
二. Netty服务器与浏览器之间的WebSocket通信
1. 浏览器WebSocket组件
2. 浏览器WebSocket代码
3. 服务端WebSocket组件
4. 服务端代码
三. Netty服务器与客户端之间的WebSocket通信
1. 服务端开发
1.1 WebSocketServerProtocolHandler
1.2 服务端代码
2. 客户端开发
2.1 WebSocketClientProtocolHandler的使用
2.2 客户端代码
3. 使用WebSocketClientProtocolHandler开发客户端注意事项
一. WebSocket协议
1. WebSocket协议基础
WebSocket协议是和Http协议同地位的应用层协议,都是基于TCP协议之上,但是其是以Http协议为基础的。未了解决Http协议半双工通信模式且数据冗杂的缺点,HTML5提出了WebSocket协议,WebSocket协议是全双工通信,解决了客户端和服务端之间的实时通信问题。浏览器和服务器只需完成一次握手,两者之间就可以创建一个持久性的TCP连接,此后服务器和客户端通过此TCP连接进行双向实时通信。
2. WebSocket协议特点
(1)协议格式
为了建立WebSocket协议连接,客户端会会先向服务器发送一个HTTP请求。即WebSocket协议的第一次握手请求消息由HTTP协议承载,不过该HTTP消息是一个“加强的”的消息,表示客户端接下来要进行WebSocket协议通信了。该消息格式如下:
--- request header ---
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:8001
Origin: http://127.0.0.1:8001
Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw==
Sec-WebSocket-Version: 13
其中我们发现,请求消息头中多了一个 “Upgrade: websocket” ,表示这是一个请求WebSocket连接的升级的HTTP请求,服务器端通过对HTTP消息头的解析来判断该消息是一个普通消息还是一个请求消息。
(2)WebSocket握手
如果接收到WebSocket协议的第一次握手消息,则服务器端需要返回一个握手响应,完成后双方就可以通过建立起来的通道进行实时双向通信了。那为什么TCP已经进行三次握手了,这里WebSocket协议还需要一次握手呢?这是因为:TCP的握手用来保证链接的建立,WebSocket的握手是在TCP链接建立后告诉服务器这是个WebSocket链接,服务器你要按WebSocket的协议来处理这个TCP链接。
二. Netty服务器与浏览器之间的WebSocket通信
1. 浏览器WebSocket组件
浏览器在JS中提供了进行WebSocket协议通信的组件WebSocket对象,其相关API如下所示(WebSocket API):
(1)构造器
var ws = new WebSocket("ws://localhost:8080");
//申请一个WebSocket对象,参数是服务端地址,同http协议使用http://开头一样,WebSocket协议的url使用ws://开头,另外安全的WebSocket协议使用wss://开头
(2)相关方法
Socket.send() | 使用连接发送数据(文本类型,二进制,二进制大文件,数组) |
Socket.close() | 关闭连接(发送一个CLOSEFrame数据桢) |
(3)属性及回调
属性 | 描述 |
Socket.readyState | 只读属性 readyState 表示连接状态,可以是以下值:
|
当webSocket对象的readyState发生变化时调用如下回调方法:
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
aWebSocket.onopen = function(event) {
console.log("WebSocket is open now.");
};
2. 浏览器WebSocket代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Netty WebSocket 客户端</title>
</head>
<body>
<script language = "JavaScript">
if("WebSocket" in window){
//创建并尝试连接
var ws = new WebSocket("ws://localhost:8000");
//回调方法
ws.onopen = function()
{
// Web Socket 已连接上
var ta = document.getElementById('responseText');
ta.value="打开WebSocket服务正常,浏览器支持WebSocket协议";
};
ws.onmessage = function (event)
{
// Web Socket 收到服务器消息
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = event.data;
};
ws.onclose = function(event)
{
// Web Socket 关闭
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = "WebSocket服务关闭";
};
}else{
alert("您的浏览器不支持 WebSocket!");
}
function send(message){
if(!window.WebSocket){return;}
if(ws.readyState == WebSocket.OPEN){
ws.send(message);
}else{
alert("WebSocket连接没有建立成功!");
}
}
</script>
<form onSubmit="return false;">
<input type="text" name="message" value="Netty-WebSocket实践"/>
<br><br>
<input type="button" VALUE="发送 WebSocket 请求消息" onClick="send(this.form.message.value)"/>
<hr color="blue"/>
<h3>服务器返回的应答消息</h3>
<textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>
3. 服务端WebSocket组件
(1)WebSocket数据形式
WebSocket以数据桢的形式传输数据,Netty服务器端数据桢类为WebSocketFrame,其子类数据桢类型分为控制帧和数据桢,其中:
- 控制帧:
- CloseWebSocketFrame:关闭WebSocket连接时会发送CloseFrame来通知另一端请求关闭连接;
- PingWebSocketFrame/PongWebSocketFrame:为了维持服务器和客户端之间的长连接,两端之间需要发送心跳信息来维持连接。其中Ping消息是探测消息,Pong消息是对Ping的响应;
- 数据桢:
- BinaryWebSocketFrame: 二进制类型数据桢;
- TextWebSocketFrame:文本类型数据帧;
(2)服务端握手组件
Netty服务器端为了完成WebSocket的一次握手,提供了封装的类 WebSocketServerHandshaker来处理WebSocket打开连接和关闭连接的事务。其中WebSocketServerHandshaer的核心方法如下:
限定符和类型 | 方法和说明 |
|
Performs the closing handshake(关闭WebSocket连接) |
|
Performs the opening handshake.(握手响应) |
其中,handshake() 方法的部分主要源码如下所示,在握手响应过程中主要进行两个操作:一是动态添加WebSocket协议的编解码器用于接下来WebSocket协议信息的编解码;二是返回给客户端握手成功的消息response。
if (ctx == null) {
//实际会走这里
ctx = p.context(HttpServerCodec.class);
if (ctx == null) {
promise.setFailure(new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
return promise;
}
//加入ws解码器和编码器
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
//记录HttpServerCodec的名字
encoderName = ctx.name();
} else {
p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());
encoderName = p.context(HttpResponseEncoder.class).name();
p.addBefore(encoderName, "wsencoder", newWebSocketEncoder());
}
//写入握手响应response
channel.writeAndFlush(response);
}
(3)handshaker创建与维护
在Netty服务器端我们使用 WebSocketServerHandshakerFactory 来方便的创建一个新的handshaker,其相关API如下所示:
public WebSocketServerHandshakerFactory(java.lang.String webSocketURL, java.lang.String subprotocols, boolean allowExtensions)
Constructor specifying the destination web socket location
参数:
webSocketURL
- URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be sent to this URL.
subprotocols
- CSV of supported protocols. Null if sub protocols not supported.allowExtensions - Allow extensions to be used in the reserved bits of the web socket frame
public WebSocketServerHandshaker newHandshaker(HttpRequest req)
Instances a new handshaker
返回:
A new WebSocketServerHandshaker for the requested web socket version. Null if web socket version is not supported.
public static ChannelFuture sendUnsupportedVersionResponse(Channel channel)
Return that we need cannot not support the web socket version
4. 服务端代码
public class WebSocketHandlerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
// 先来的一定是HTTP消息,所以一定先添加HTTP编解码器
ChannelPipeline pipeline = arg0.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new WebSocketServerHandler());
//WebSocket编解码器会在握手时动态添加
}
}
public class WebSocketServerHandler extends ChannelInboundHandlerAdapter {
private WebSocketServerHandshaker handhaker = null;//握手处理类
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("channel is active");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("channel is inactive");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof FullHttpRequest) {//第一次接入要进行握手
handHttpRequest(ctx, (FullHttpRequest)msg);
System.out.println("http");
}else if(msg instanceof WebSocketFrame) {//WebSocket通信
System.out.println("websocket");
handWebSocketFrame(ctx, (WebSocketFrame)msg);
}
}
private void handHttpRequest(ChannelHandlerContext ctx,FullHttpRequest request) {//进行握手处理
if(!request.decoderResult().isSuccess()||!request.headers().get("Upgrade").equals("websocket")) {//解码失败或非WebSocket请求消息(一定是小写websocket!!!)
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);
byte[] msgBytes = HttpResponseStatus.BAD_REQUEST.toString().getBytes();
response.content().writeBytes(msgBytes);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, msgBytes.length);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return;
}
System.out.println("success");
//构造握手工厂对8888/websocket握手消息进行响应
WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws://localhost:8888/websocket", null, false);
handhaker = factory.newHandshaker(request);//构造握手对象
if(handhaker == null) {//不支持socket_vision
factory.sendUnsupportedVersionResponse(ctx.channel());//返回不支持消息响应
}else {
handhaker.handshake(ctx.channel(), request);//进行握手响应添加编解码器
}
}
private void handWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) {//进行消息处理
//关闭链路消息
if(frame instanceof CloseWebSocketFrame) {
handhaker.close(ctx.channel(), (CloseWebSocketFrame)frame.retain());//关闭链路
return;
}
//Ping/Pong消息
if(frame instanceof PingWebSocketFrame) {
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
return;
}
//非文本数据桢
if(!(frame instanceof TextWebSocketFrame)) {
System.out.println("服务器不支持非文本消息!");
return;
}
//构造响应返回
String request = ((TextWebSocketFrame)frame).text();
System.out.println("浏览器消息 : " + request);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
String time = df.format(new java.util.Date());
String response = "欢迎使用WebSocket服务!现在时刻 : 北京时间" + time;
ctx.channel().writeAndFlush(new TextWebSocketFrame(response));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
cause.printStackTrace();
ctx.channel().close();
}
}
三. Netty服务器与客户端之间的WebSocket通信
1. 服务端开发
1.1 WebSocketServerProtocolHandler
这个处理程序为您运行websocket服务器做了所有繁重的工作。它负责websocket握手以及控制帧(Close、Ping、Pong)的处理。文本和二进制数据帧将传递给管道中的下一个处理程序(由您实现)进行处理。此处理程序的实现假定您只想运行websocket服务器,而不处理其他类型的HTTP请求(如GET和POST)。
- 特点:自动处理握手请求 + 拦截处理控制帧 + 拦截其他HTTP协议信息返回拒绝响应;
- 优点:使得服务器可以专注于对TextFrame和BinaryFrame的处理,不用去管其他繁杂的握手流程;
- 缺点:服务器只能单一的处理WebSocket协议,其他HTTP协议消息都被拦截了;
其构造函数如下:
public WebSocketServerProtocolHandler(java.lang.String websocketPath, java.lang.String subprotocols, boolean allowExtensions, int maxFrameSize)
(2)WebSocketServerProtocolHandler部分源码
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
ChannelPipeline cp = ctx.pipeline();
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null){
// Add the WebSocketHandshakeHandler before this one.
//WebSocketHandshakeHandler 负责对握手消息进行处理
ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols,
allowExtensions, maxFramePayloadLength, checkStartsWith));
}
}
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
//关闭连接
if (frame instanceof CloseWebSocketFrame) {
WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel());
if (handshaker != null) {
frame.retain();
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame);
} else {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
return;
}
super.decode(ctx, frame, out);
}
(3)WebSocketServerProtocolHandshakeHandler部分源码
//处理第一次来的升级版HTTP请求
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
final FullHttpRequest req = (FullHttpRequest) msg;
if (checkStartsWith) {
if (!req.getUri().startsWith(websocketPath)) {
ctx.fireChannelRead(msg);
return;
}
} else if (!req.getUri().equals(websocketPath)) {
ctx.fireChannelRead(msg);
return;
}
try {
if (req.getMethod() != GET) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
return;
}
//构造握手
final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,
allowExtensions, maxFramePayloadSize);
final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {//构造失败--不支持此版本
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {//返回响应
final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
handshakeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
ctx.fireExceptionCaught(future.cause());
} else {
ctx.fireUserEventTriggered(
WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
}
}
});
WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
//处理完握手后立即替换当前handler
ctx.pipeline().replace(this, "WS403Responder",
WebSocketServerProtocolHandler.forbiddenHttpRequestResponder());
}
} finally {
req.release();
}
}
1.2 服务端代码
public class SocketHandlerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = socketChannel.pipeline();
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
pipeline.addLast("httoCodec",new HttpServerCodec());
//netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
pipeline.addLast("httpAggregator",new HttpObjectAggregator(65535));
//pipeline.addLast(new TestHandler());
//WebSocket辅助handler
//ws://server:port/context_path
//ws://localhost:9999/ws
//参数指的是contex_path
pipeline.addLast("protocolHandler",new WebSocketServerProtocolHandler("/connection",null,true,65535));
//自定义处理器处理text和binary数据桢
pipeline.addLast("ServerHandler",new ServerSocketHandler());
}
}}
public class ServerSocketHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("serverChannel is Active");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("clientChannel is InActive");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof TextWebSocketFrame){
TextWebSocketFrame frame = (TextWebSocketFrame)msg;
System.out.println("Server收到消息 : " + frame.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame(frame.text()));
}else if(msg instanceof BinaryWebSocketFrame){
BinaryWebSocketFrame frame = (BinaryWebSocketFrame)msg;
System.out.println("收到二进制消息长度 : "+frame.content().readableBytes());
BinaryWebSocketFrame binaryWebSocketFrame=new BinaryWebSocketFrame(Unpooled.buffer().writeBytes("xxx".getBytes()));
ctx.channel().writeAndFlush(binaryWebSocketFrame);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
cause.printStackTrace();
ctx.channel().close();
}
}
2. 客户端开发
2.1 WebSocketClientProtocolHandler的使用
这个处理程序handler为您运行websocket客户机做了所有繁重的工作。它负责websocket握手以及乒乓球、乒乓球帧的处理。文本和二进制数据帧将传递给管道中的下一个处理程序(由您实现)进行处理。此外,closeFrame可以选择是否传递给下一个处理程序,因为您可能希望在关闭连接之前检查它。如果handleCloseFrames为false,则默认为true。此实现将在与远程服务器的连接完成后建立websocket连接。
(1)WebSocketClientProtocolHandler部分源码
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
//handleCloseFrames为Boolean,用于设置CloseFrame是否要拦截,可在构造器设置,若为true则拦截自动处理
if (handleCloseFrames && frame instanceof CloseWebSocketFrame) {
ctx.close();
return;
}
super.decode(ctx, frame, out);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
ChannelPipeline cp = ctx.pipeline();
if (cp.get(WebSocketClientProtocolHandshakeHandler.class) == null) {
//handler添加以后立即添加握手处理handler自动发送握手请求
// Add the WebSocketClientProtocolHandshakeHandler before this one.
ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(),
new WebSocketClientProtocolHandshakeHandler(handshaker));
}
}
(2)WebSocketClientProtocolHandshakeHandler部分源码
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
//添加后立即进行握手
handshaker.handshake(ctx.channel()).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
ctx.fireExceptionCaught(future.cause());
} else {
ctx.fireUserEventTriggered(
WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_ISSUED);
}
}
});
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//frame消息则放掉
if (!(msg instanceof FullHttpResponse)) {
ctx.fireChannelRead(msg);
return;
}
//处理http握手回应消息
FullHttpResponse response = (FullHttpResponse) msg;
try {
//当前握手消息还没完成则该http消息会握手响应
if (!handshaker.isHandshakeComplete()) {
handshaker.finishHandshake(ctx.channel(), response);
ctx.fireUserEventTriggered(
WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE);
//握手完毕移出该handler
ctx.pipeline().remove(this);
return;
}
//否则报错,不处理其他消息
throw new IllegalStateException("WebSocketClientHandshaker should have been non finished yet");
} finally {
response.release();
}
}
2.2 客户端代码
public class ClientHandlerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = arg0.pipeline();
pipeline.addLast("httpCodec",new HttpClientCodec());// = responseDecoder + requestEncoder
pipeline.addLast("httpAggregator",new HttpObjectAggregator(65535));
//pipeline.addLast(new TestHandler());
URI uri = new URI("ws://localhost:8787/connection");
pipeline.addLast("protocolHandler",new WebSocketClientProtocolHandler(uri, WebSocketVersion.V13, (String)null, true, new DefaultHttpHeaders(), 65535));
pipeline.addLast("ClientHandlder",new ClientSocketHandler());
}
}
public class ClientSocketHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("clientChannel is Active");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println("clientChannel is InActive");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof TextWebSocketFrame) {
TextWebSocketFrame frame = (TextWebSocketFrame)msg;
System.out.println("Client收到消息 : " + frame.text());
}else if(msg instanceof BinaryWebSocketFrame) {
BinaryWebSocketFrame frame = (BinaryWebSocketFrame)msg;
System.out.println("Client收到二进制消息长度 : "+frame.content().readableBytes());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
cause.printStackTrace();
ctx.channel().close();
}
}
3. 使用WebSocketClientProtocolHandler开发客户端注意事项
(1)不要在WebSocketClientProtocolHandler后的自定义handler中使用Active()来发送测试信息,这样服务器端会收不到信息。因为pipline中的多个handler需要通过fireXXX()方法来传递事件包括Active/InActive/Read ,而 WebSocketClientProtocolHandler源码中是没有传递Active的,所以之后自定义handler的Active不会执行,这就导致测试没有响应。
(2)可以使用线程进行测试,等连接握手成功以后再调用方法发送消息,测试代码如下:
public class Main {
public static void main(String[] arg0) {
// TODO Auto-generated constructor stub
SocketServer server = new SocketServer();
SocketClient client = new SocketClient();
server.start();
client.start();
try {
Thread.currentThread().sleep(5000);
client.sendMessage("Hello GoodMorning");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(3)使用系统组件WebSocketHandler比较局限,但比较方便,如果想更灵活的进行WebSocket连接,可以使用handshaker来自己进行连接握手处理,相关代码可以在源码网站有示例。