开始
在本章的开头把代码奉上,大家下载下来对照的学习,这些代码都是运行通过的。
上节我们讲解了HTTP协议开发,但是,Http协议的开销问题,导致它们不适用于低延迟的应用。为了解决这些问题,我们引入了webSocket。
HTTP协议的弊端
我们来总结一下HTTP协议的弊端:
1.HTTP协议是半双工的协议。大家知道对讲机吗?它就是半双工的设备。当对方在说话时,你就不能说话了,也就是说一个时间点上,只能一个人在说话。现在已经被全双工的电话给替代掉了,现在对讲机这东西用的不多了。
2.HTTP协议冗长而繁琐。HTTP消息包含消息头,消息体,换行符等,采用文本形式传播,相比二进制通信协议,非常冗长且繁琐。
3.针对服务器推送的黑客攻击。例如长时间轮询。
为了解决以上HTTP协议效率底下的问题,WebSocket协议应运而生,它能更好的节约带宽和服务器资源并且实时通信。下面就让我们看看websocket协议的神奇之处。
WebSocket协议的特点
1.单一的TCP连接,采用全双工通信
2.对代理,防火墙,路由器透明。
3.无头部信息,cookie和身份验证
4.无安全开销
5.通过ping帧保持链路激活
6.服务器可以主动的对客户端进行发送消息,而不需要轮询。
总的来说我们webSocket被设计出来的目的就是为了取代轮询这种高消耗的方式。
websocket连接过程
建立连接过程如下:
1.客户端浏览器首先要向服务器发送一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加信息,其中附加信息“UpgradeWebSocket”表明这是一个申请协议升级的http请求。
2.服务器解析这些附加头信息,然后生成应答信息返回给客户端,这样客户端和服务器端的webSocket连接就建立起来了
3.这个连接会一直持续到客户端或者服务器的某一方主动关闭连接。
我们来看一下客户端向服务器请求的消息:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade:websocket
Connection:Upgrade
Sec-WeSocket-Key:dGhlweidxlkjsdZQ==
Origin:http://example.com
Sec-WebSocket-Protocol:chat,superchat
Sec-webSocket-Version:13
我们来看一下服务器返回给客户端的应答消息:
HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-accept:siefjosigdfsl=
Sec-WebSocket-Protocol:chat
请求消息中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要,把“Sec-WebSocket-Key”加上一个魔幻字符串“25EAFA5-E914-47DA-95CA-C54B0DC85B11”.使用SHA-1加密,然后进行BASE-64编码,将结果作为Sec-WebSocket-accept头的值,返回给客户端。
连接示意图如下:
websocket生命周期
握手成功后,客户端和服务器就可以通过”message”方式进行通信了,一个消息由一个或者多个帧组成。
websocket连接关闭。
为关闭websocket连接,客户端和服务器需要通过一个安全的方法关闭底层TCP连接及TLS会话。如果合适,丢弃任何可能接收的字节:必要时可以通过任何可用的手段关闭连接。
编码开始
需求:我们要编写一个webSocket服务器,支持WebSocket的浏览器通过webSocket协议发送请求给我们编写的webSocket服务器,服务器对请求消息进行判断,如果是合法的webSocket请求,则获取请求消息体,并在后面追加字符串:“欢迎使用Netty WebSocket 服务,现在时刻:系统时间”。
当然客户端也是我们编写的,其中内嵌js脚本去创建webSocket连接,如果握手成功打印:“打开websocket 服务正常,浏览器支持Websocket!”
首先对webSocket服务器的功能进行简单讲解。websocket服务端接收到请求消息后,先对消息的类型进行判断,如图所示:
我们就是判断upgrade取它的值判断是否是websocket,如果不是webSocket类型的请求消息,则返回HTTP 400 BAD REQUEST 响应给客户端。如果是服务器返回消息,双方连接正式建立。
由于篇幅问题,只对核心代码讲解,全部代码大家可以下载下来看看。
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//HttpServerCodec的作用是将请求和应答消息编码或者解码为HTTP消息;
ch.pipeline().addLast("http-codec", new HttpServerCodec());
//使用HttpObjectAggregator会把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
//支持处理异步发送大数据文件,但不占用过多的内存,防止发生内存泄漏,这里是向客户端发送html5文件
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
//这个是我们自定义的,处理文件服务器逻辑。主要功能还是在这个文件中
ch.pipeline().addLast("http-fileServerHandler", new WebSocketServerHandler());
}
});
Channel ch = b.bind(port).sync().channel();//这里写你本机的IP地址
System.out.println("web socket server started at port "+port+".");
System.out.println("open your browser and navigate to http://localhost:"+port+"/");
ch.closeFuture().sync();
} catch (Exception e) {
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
讲解:
HttpServerCodec讲解:HTTP协议的请求解码器和响应编码器即HttpServerCodec,它会将HTTP客户端请求转成HttpRequest对象,将HttpResponse对象编码成HTTP响应发送给客户端。
HttpObjectAggregator讲解:使用HttpObjectAggregator会把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse
下面我们来看一下核心类WebSocketServerHandler,我都做的详细的注释,希望大家能看懂。
/**
* @author 作者 YYD
* @version 创建时间:2016年8月17日 上午6:32:23
* @function 未添加
*/
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object>{
private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());
private WebSocketServerHandshaker handshaker;
/**
* 接收客户端发过来的消息并处理
* FullHttpRequest :
* 官网解释:Combine the {@link HttpRequest} and {@link FullHttpMessage}, so the request is a <i>complete</i> HTTP
* request.
* 这个请求是 代表http请求完成的标记。
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg)
throws Exception {
if(msg instanceof FullHttpRequest){//接收到客户端的握手请求,开始处理握手
handleHttpRequest(ctx,(FullHttpRequest)msg);
}else if(msg instanceof WebSocketFrame){//接收到客户端发过来的消息(只过滤文本消息),处理后发给客户端。
handleWebSocketFrame(ctx, (WebSocketFrame)msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handleHttpRequest(ChannelHandlerContext ctx,FullHttpRequest req) throws Exception{
/**
* 如果不成功或者消息头不包含"Upgrade",说明不是websocket连接,报400异常。
*/
if(!req.getDecoderResult().isSuccess()||(!"websocket".equals(req.headers().get("Upgrade")))){
sendHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.BAD_REQUEST));
return;
}
/**
* WebSocket是一种全新的协议,不属于http无状态协议,协议名为"ws",这意味着一个websocket连接地址会是这样的写法:
ws://127.0.0.1:8080/websocket。ws不是http,所以传统的web服务器不一定支持,需要服务器与浏览器同时支持, WebSocket才能正常运行,目前大部分浏览器都支持Websocket。
WebSocketServerHandshaker 官网的解释是:服务器端Web套接字打开和关闭握手基类
WebSocketServerHandshakerFactory 官网的解释是:自动检测正在使用的网络套接字协议的版本,并创建一个新的合适的 WebSocketServerHandshaker。
*/
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket",null,false);
handshaker = wsFactory.newHandshaker(req);//创建一个握手协议
if(handshaker == null){
/**
* Return that we need cannot not support the web socket version
* 返回不支持websocket 版本
*/
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
}else {
handshaker.handshake(ctx.channel(), req);//开始握手
}
}
/**
* 我们判断数据类型,只支持文本类型
* @param ctx
* @param frame
*/
private void handleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) {
if(frame instanceof CloseWebSocketFrame){//如是接收到的是关闭websocket,就关闭连接
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
if(frame instanceof PingWebSocketFrame){//如果信息是2进制数据,就反给它,
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
if(!(frame instanceof TextWebSocketFrame)){//哪果不是文本的数据,就报错。
throw new UnsupportedOperationException(String.format("%s frame types not supported",frame.getClass().getName()));
}
String request = ((TextWebSocketFrame)frame).text();
if(logger.isLoggable(Level.FINE)){
logger.fine(String.format("%s receive %s",ctx.channel(),request));
}
ctx.channel().write(new TextWebSocketFrame(request+",欢迎使用netty websocket 服务,现在时刻"+new Date().toString()));
}
private static void sendHttpResponse(ChannelHandlerContext ctx,FullHttpRequest req,FullHttpResponse res){
if(res.getStatus().code() != 200){
ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
setContentLength(res,res.content().readableBytes());
}
ChannelFuture f = ctx.channel().writeAndFlush(res);//发送消息
if(!isKeepAlive(req)||res.getStatus().code()!= 200){//如果断开连接,或者发送不成功,断开连接。
f.addListener(ChannelFutureListener.CLOSE);//关闭连接
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
上面的代码,我都做的详细的注释,大家可以对照着看看。
在handleHttpRequest方法中我们判断消息头中是否包含“Upgrade”字段,如果不包含就返回Http 400 响应。握手请求经过简单校验后,我们通过构造握手工厂,创建握手出理类webSocketServerHandshaker,通过它构造握手响应消息返回给客户端,同时将webSocket消息的编码和解码类动态添加到ChannelPipeline中,用于websocket消息的编解码。
下面来看看客户端浏览器代码
<html>
<head>
<meta charset="UTF-8">
netty websocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
var socket;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
if(window.WebSocket){
var socket = new WebSocket("ws://localhost:9090/websocket");
socket.onmessage = function(event){
var ta = document.getElementById('responseText');
ta.value="";
ta.value = event.data
};
socket.onopen = function(event){
var ta = document.getElementById('responseText');
ta.value = '';
ta.value = "打开websocket服务正常,浏览器支持websocket!";
};
socket.onclose = function(event){
var ta = document.getElementById('responseText');
ta.value = '';
ta.value = "websocket关闭!";
};
}else{
alert("抱歉,您的浏览器不支持WebSocket 协议!");
}
function send(message){
if(!window.WebSocket){
return;
}
if(socket.readyState == window.WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket 还没有建立连接!")
}
}
</script>
<form onsubmit="return false;">
<input type="text" name="message" vaule="netty最佳实践"/>
<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>
我们这里不加讲解了,大家看看就明白了。
看看客户端的运行效果
初始状态效果图
发送消息效果图
结尾
在本章的结尾把代码奉上,大家下载下来对照的学习,这些代码都是运行通过的。
好了就讲到这里吧,由于HttP协议本身存在的弊端,产生了websocket协议。希望对大家有所帮助。有不懂的可以联系我549544653@qq.com
同时鼓励自己,坚持就会有奇迹,加油!