一次偶然的机会在群里有人提问到这样的问题,一台socket通信服务器,用其他客户端联接正常,用websocket就不行。
于是自己写了个程序验证一下,也就开始了websocket 的hello word之旅。
先了解下websocket的通信原理。
这里简单说明一下WebSocket握手的过程。
当Web应用程序调用new WebSocket(url)接口时,Browser就开始了与地址为url的WebServer建立握手连接的过程。
1. TCP握手阶段, Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
2. websocket 握手阶段, 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。
例如:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat,superchat
Sec-WebSocket-Version: 13
3. WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
4. Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到 onerror消息,并且能知道连接失败的原因。
第一步是走TCP底层三次握手,第二步和第三步是http协议。
回到之前的问题:一台socket通信服务器,用其他客户端联接正常,用websocket就不行。
是因为其他客户端没有第二步和第三步的websocket握手,直接进入传输业务数据,用websocket传输需要websocket握手,socket服务端把握手的报文当做业务数据格式处理,服务端报错了,服务端没有响应websocket握手包给websocket客户端,websocket客户端当做连接失败处理。
要让socket服务端支持websocket的通讯,必须先支持第二步和第三步的websocket握手,接下来看netty是怎么支持的。
以netty 5.0版本为例
privateclassChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("http-codec",newHttpServerCodec());
ch.pipeline().addLast("aggregator",newHttpObjectAggregator(65536)) ;
ch.pipeline().addLast("http-chunked",newChunkedWriteHandler());
ch.pipeline().addLast("handler",newWebSocketServerHandler());
}
}
Websocket握手是基于http协议,先添加一些http编解码器。下面是WebSocketServerHandler的实现
public classWebSocketServerHandler extends SimpleChannelInboundHandler<Object>{
/**
* 日志
*/
privatestaticfinalLogger logger=
Logger.getLogger(WebSocketServerHandler.class.getName());
/**
* 全局websocket
*/
privateWebSocketServerHandshaker handshaker;
@Override
protectedvoidmessageReceived(ChannelHandlerContext ctx, Object msg)
throws Exception {
//普通HTTP接入
if(msg instanceof FullHttpRequest){
handleHttpRequest(ctx,(FullHttpRequest) msg);
}else if(msg instanceofWebSocketFrame){ //websocket帧类型已连接
handleWebSocketFrame(ctx,(WebSocketFrame) msg);
}
}
@Override
publicvoidchannelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
privatevoidhandleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request){
//如果http解码失败则返回http异常并且判断消息头有没有包含Upgrade字段(协议升级)
if(!request.decoderResult().isSuccess()
|| (!"websocket".equals(request.headers().get("Upgrade"))) ){
sendHttpResponse(ctx,request, newDefaultFullHttpResponse(
HttpVersion.HTTP_1_1,HttpResponseStatus.BAD_REQUEST));
return ;
}
//构造握手响应返回
WebSocketServerHandshakerFactory ws = newWebSocketServerHandshakerFactory("", null,false);
handshaker = ws.newHandshaker(request);
if(handshaker == null){
//版本不支持
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
}else{
handshaker.handshake(ctx.channel(), request);
}
}
/**
* websocket帧
* @param ctx
* @param frame
*/
privatevoidhandleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
//判断是否关闭链路指令
if(frame instanceof CloseWebSocketFrame){
handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());
return ;
}
//判断是否Ping消息 -- ping/pong心跳包
if(frame instanceof PingWebSocketFrame){
ctx.channel().write(newPongWebSocketFrame(frame.content().retain()));
return ;
}
//本程序仅支持文本消息,不支持二进制消息
if(!(frame instanceof TextWebSocketFrame)){
throw new UnsupportedOperationException(
String.format("%s frame types not supported", frame.getClass().getName()));
}
//返回应答消息 text文本帧
String request = ((TextWebSocketFrame)frame).text();
//打印日志
if(logger.isLoggable(Level.FINE)){
logger.fine(String.format("%s received %s",ctx.channel(), request));
}
//发送到客户端websocket
ctx.channel().write(newTextWebSocketFrame(request
+ ", 欢迎使用Netty WebSocket服务,现在时刻:"
+ new java.util.Date().toString()));
}
/**
* response
* @param ctx
* @param request
* @param response
*/
privatestaticvoidsendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest request,FullHttpResponse response){
//返回给客户端
if(response.status().code() !=HttpResponseStatus.OK.code()){
ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(),CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
HttpHeaderUtil.setContentLength(response,response.content().readableBytes());
}
//如果不是keepalive那么就关闭连接
ChannelFuture f =ctx.channel().writeAndFlush(response);
if(!HttpHeaderUtil.isKeepAlive(response)
|| response.status().code() !=HttpResponseStatus.OK.code()){
f.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 异常出错
*/
@Override
publicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
重点讲述下messageReceived的实现
protectedvoidmessageReceived(ChannelHandlerContext ctx, Object msg)
throws Exception {
//普通HTTP接入,websocket握手协议是http请求,返回websocket握手包
if(msg instanceof FullHttpRequest){
handleHttpRequest(ctx,(FullHttpRequest) msg);
}else if(msg instanceofWebSocketFrame){ //websocket连接已建立,websocket帧类型编解码
handleWebSocketFrame(ctx,(WebSocketFrame) msg);
}
}