我们来写一个简单的基于 Netty 来手写 WebSocket 服务。首先,给大家介绍一下究竟什么是 WebSocket。
WebSocket
比如,我们以页面更新商品状态的应用来举例,也就是说页面上显示的商品列表要与服务端一致,这就需要页面显示的数据要实时从数据库更新。如果用 HTTP 协议我们应该怎么做呢?
HTTP 协议的实时更新方案
其实,有两个解决方案,分为短轮询和长轮询。我们先说短轮询。
HTTP 短轮询
短轮询是指为了尽量保证数据是最新的,客户端要通过 HTTP 协议以一定的频率请求服务端的数据,比如以5秒为间隔来访问服务端获取最新的商品列表数据,如下面
每到轮询间隔的时候,比如每5秒客户端都会向服务端发送更新数据的 GET HTTP 请求,然后服务端收到请求后会立刻把结果返回客户端。如果服务端的数据没有更新,就返回无更新的通知,如果服务端发生的数据更新就把新的商品列表返回给客户端。
弊端
缺点是很明显的:
- 大量无效连接:无论服务端是否更新了都会向服务端发送请求,如果商品列表更新的频率很低,这就造成了绝大部分的请求都是无效请求。同时,是每次请求都会建立 TCP 连接,获取完数据后会断开 TCP 连接,于是会造成对系统资源的巨大浪费。
- 实时性太差:5秒一次的间隔仍然不是实时,与真实的商品列表状态是不符的,特别是即时通讯或聊天室的场景,我们不可能容忍这么长时间的消息延迟。
于是,我们改造一下访问方式,我们引入 HTTP 长轮询。
HTTP 长轮询
HTTP 长轮询就是客户端首先向服务端发送更新数据的请求。然后,服务端接收到客户端更新数据的请求后,如果没有数据更新的话,并不会直接返回给客户端响应,而是等着 HTTTP 连接超时(比如超时时间30秒),这时如果数据还没有更新就会向客户端发送数据无更新的响应。但是,如果在超时时间内数据发生的变化,那么就把更新的商品列表数据立即返回给客户端。客户端收到响应后,无论是否得到了商品列表数据的更新数据,都会立即发送获取更新数据的请求,于是第二轮又开始了。
优点
- 请求量减少:与 HTTP 短连接比较,确实降低了客户端访问服务端的频率,因为我们可以增加服务端响应的超时时间。
为什么不能通过增加客户端的轮询间隔时间来减少请求量呢?
但是无法增加客户端超时时间,因为客户端超时时间就等于降低了客户端数据更新的频率,造成数据更新不及时。
- 数据更新及时: 因为客户端不是靠时间间隔轮询来获取数据的,而服务端收到客户端请求后只要有数据更新就会立刻把响应返回给客户端端。于是,就不存在数据更新不及时的问题。
缺点
大量连接挂起:问题是如果客户端很多的话,都会去主动请求服务端,同时服务端会把所有请求挂起直到超时,这就造成了连接资源的大量的浪费。
大量无效连接: 如果服务端更新频率很低,那么就会造成很多无效的连接。
综上所述,HTTP 短连接和长连接无法根本解决数据的实时通信问题,但是 WebSocket 是可以解决这些问题的,我们看看 WebSocket 的原理实现。
WebSocket 协议
与短连接 Http 不同,WebSocket 是一种长连接,而且是双工通信。主要体现在只要通过三次握手成功建立的连接,接下来的请求不会再建立去建立连接,同时服务端可以主动向客户端推送消息,如果有数据变动服务端会主动通知客户端。这样就会大大提升通信效率。
与 HTTP 协议一样 WebSocket 协议底层也是 TCP 协议。
WebSocket 的实现方式是把实现了双工通信的 WebSocket 模块嵌入到 HTTP 服务器中的一个 HTTP 页面中,这样你就可以用浏览器当作客户端来与这个 WebSocket 模块通信了。这样就可以通过浏览器 WebSocket 服务端实现双工长连接通信。
那么,我们简单来看一下 WebSocket 的通信原理的流程图:
首先,客户端使用 HTTP 协议向服务端请求连接,同时要求建立 WebSocket 连接,连接完成后,就可以实现客户端和服务端的双向通信了。
大家,可能有疑惑,不是 WebSocket 协议吗?为什么会出现 HTTP 协议。是这样的,WebSocket 连接是要依赖 HTTP 协议的。
下面,给大家抓一下 WebSocket 的请求头和响应头给大家分析一下:
大家先看请求头,通过这些参数就能知道这是个 HTTP 的请求头,但是与一般请求头不一样的是有两个参数我们需要注意一下:
- Connection:Update :意思是连接需要升级,就是说虽然是 HTTP 的连接请求但是需要连接升级。
- Update:websocket: 这里表示具体升级成什么协议呢,这里是 websocket 协议。
这时客户端已经把要求建立 websocket 的请求发送给服务端了,我们再看看 响应头的参数:
- Connection:Update :意思是连接需要升级,就是说虽然是 HTTP 的连接请求但是需要连接升级。
- Update:websocket: 这里表示具体升级成什么协议呢,这里是 websocket 协议。
响应头和请求头是一样的,也就是说 websocket 已经建立了,这时的通信连接就是 websocket 连接。
也就是说,websocket 连接的建立是依赖 HTTP 连接的。
接下来,我们来实现一个 WebSocket 服务端。
WebSocket 服务端实现
配套的源码源代码地址:github.com/sean417/net… 的 netty-websocket 模块。
服务端的搭建还是利用 Netty 现有的 Handler 来构建。
先看主类:
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossEventLoopGroup,workerEventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
// websocket pipeline
// 客户端的字节数组收到后,先用 HttpServerCodes 来解码
// 并转换为一个 HttpRequest 对象
.addLast(new HttpServerCodec())
// 太大的数据流会把数据流分为块来发送或接收。
.addLast(new ChunkedWriteHandler())
// 把分离的数据段聚合成一个完整的数据段,并定义聚合数据对象的大小。
.addLast(new HttpObjectAggregator(1024*32))
// 基于前面已经处理好的请求数据对象,这里基于Websocket 协议在做一次处理
// 基于 http 协议传输过来的数据, 封装内容, 可以是按 websocket 协议封装的 http 请求里的数据
// 必须在这拿出出 http 请求里的数据, 按照 websocket 协议来进行解析处理, 把数据提取出来作为 webSocket 的数据
//
.addLast(new WebSocketServerProtocolHandler("/websocket"))
// 业务逻辑处理
.addLast("netty-websocket-server-handler", new NettyWebSocketServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
logger.info("netty websocket server is started, listened["+port+"].");
channelFuture.channel().closeFuture().sync();
}finally {
bossEventLoopGroup.shutdownGracefully();
}
}
简单说明一下 Pipleline 的处理流程:
- 首先,通过 HttpServerCodec 来处理接收请求时解码已经发送响应时的编码,这个在 手写 HTTP 服务时,已经给大家介绍了。
- ChunkedWriteHandler 是用来把太大的数据流分成小一些的数据块来发送或接收。
- HttpObjectAggregator 是把多个分离的数据段聚合成一个完整的数据。
- WebSocketServerProtocolHandler:设置 WebSocket 服务的地址,同时把 HTTP 协议封装的数据格式转换为 WebSocket 协议的数据格式。
- NettyWebSocketServerHandler:这个是自定义的用于实现业务逻辑的 Handler.
大家可以看到,即使是连接建立成功了,后续的数据传输也是离不了 HTTP 协议的,就是说 HTTP 协议是 WebSocekt的外层,保持长连接还要靠 WebSocket 协议。
再来看一下客户端的代码:
var websocket;
// 浏览器连接服务端的方法
function connectServer() {
if("WebSocket" in window){
console.log("your browser supports websocket!")
// 连接websocket服务端地址
websocket = new WebSocket("ws://localhost:8998/websocket");
// 连接成功之后,客户端向 websocket 发送消息。
websocket.onopen = function (){
console.log("send request to websocket server");
// 向 websocket 发送消息
websocket.send("I am a websocket client.")
}
// 接收响应的回调方法
websocket.onmessage = function (ev) {
var response = ev.data;
console.log("receives response from netty server: "+response);
}
}
}
这些JS代码的注释已经写的很清楚了,主要流程是要指明要访问的 WebSocket 服务的路径,同时定义要想服务端发送的数据内容。然后,定义一个用于接收响应的回调方法。
我们看一下结果: