上面讲了使用使用Socket搭建多客户端的连接与通信。
那么如果在Netty中使用WebSocket进行长连接通信要怎么实现。
WebSocket
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
WebSocket 属性
以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:
属性 | 描述 |
Socket.readyState | 只读属性 readyState 表示连接状态,可以是以下值:
|
Socket.bufferedAmount | 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。 |
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
事件 | 事件处理程序 | 描述 |
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:
方法 | 描述 |
Socket.send() | 使用连接发送数据 |
Socket.close() | 关闭连接 |
注:
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
实现
在此基础上,在src下新建包com.badao.NettyWebSocket
然后新建服务端类WebSocketServer
package com.badao.NettyWebSocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class WebSocketServer {
public static void main(String[] args) throws Exception
{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new WebSocketInitializer());
//绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(70).sync();
channelFuture.channel().closeFuture().sync();
}finally {
//关闭事件组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务端的搭建在上面已经讲解,这里又添加了Netty自带的日志处理器LoggingHandler
然后又添加了自定义的初始化器WebSocketInitializer
所以新建类WebSocketInitializer
package com.badao.NettyWebSocket;
import io.netty.channel.Channel;
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 WebSocketInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(8192));
pipeline.addLast(new WebSocketServerProtocolHandler("/badao"));
pipeline.addLast(new WebSocketHandler());
}
}
因为Netty也是基于Http的所以这里需要添加HttpServerCodec处理器等。
要想实现WebSocket功能,主要是添加了WebSocketServerProtocolHandler这个WebSocket服务端协议处理器。注意这里的参数需要添加一个WebSocket的路径,这里是/badao
最后添加自定义的处理器WebSocketHandler 用来做具体的处理
所以新建类WebSocketHandler
package com.badao.NettyWebSocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.time.LocalDateTime;
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("收到消息:"+msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)"));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生");
ctx.close();
}
}
使其继承SimpleChannelInboundHandler注意此时的泛型类型为TextWebSocketFrame
然后重写channelRead0方法用来对收到数据时进行处理
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("收到消息:"+msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)"));
}
在服务端将收到的消息进行输出并给客户端发送数据。
然后重写handlerAdded方法,在客户端与服务端建立连接时进行操作
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
}
这里通过通道的id方法的asLongText方法获取连接的唯一标志。
然后在服务端输出。
同理重写handlerRemoved方法,在断掉连接时将连接的唯一标志进行输出。
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
}
最后重写出现异常时的处理方法,在出现异常时将连接关闭。
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生");
ctx.close();
}
至此WebSocket服务端搭建完成,然后客户端通过JS就能实现。
在项目目录下src下新建webapp目录,在此目录下新建badao.html
修改html的代码为
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>公众号:霸道的程序猿</title>
</head>
<body>
<script type="text/javascript">
var socket;
if(window.WebSocket)
{
socket = new WebSocket("ws://localhost:70/badao")
socket.onmessage=function (ev) {
var ta = document.getElementById("responseText");
ta.value = ta.value+"\n"+ev.data;
}
socket.onopen = function (ev) {
var ta = document.getElementById("responseText");
ta.value = "连接开启";
}
socket.onclose = function (ev) {
var ta = document.getElementById("responseText");
ta.value = ta.value+"\n连接关闭";
}
}
else{
alert("当前浏览器不支持WebSocket")
}
function send(message) {
if(!window.WebSocket)
{
return;
}else
{
if(socket.readyState = WebSocket.OPEN)
{
socket.send(message);
}
else
{
alert("连接尚未开启");
}
}
}
</script>
<form>
<textarea name="message" style="width: 400px;height: 200px">
</textarea>
<input type="button" value="发送数据" onclick="send(this.form.message.value)">
<h3>服务端输出:</h3>
<textarea id="responseText" style="width: 400px;height: 200px">
</textarea>
</form>
</body>
</html>
在js中通过windows.WebSocket判断是否支持WebSocket
如果支持则
socket = new WebSocket("ws://localhost:70/badao")
建立连接,url的写法前面的ws://是固定的类似http://
后面的是跟的ip:端口号/上面配置的WebSocket路径
然后就是以这个WebSocket对象为中心进行连接和数据的显示。
下面的回调方法onopen会在建立连接成功后回调,onclose会在断掉连接后回调,
onmessage会在收到服务端发送的数据时回调并通过ev.data获取数据。
客户端向服务端发送数据时调用的是socket的send方法,通过
if(socket.readyState = WebSocket.OPEN)
判断连接已经成功建立。
实现长连接通信
运行WebSocketServer的main方法,然后在badao.html上右击选择运行
建立连接成功后会在服务端输出连接的id,在客户端会显示连接开启。
此时如果刷新浏览器,服务端会输出一次断开连接和建立连接
再将服务端停掉,客户端会输出连接关闭
再重新启动服务端,在上面的输入框输入内容并点击发送数据
服务端会收到消息并输出,并向客户端发送一个消息。
继续发送也是如此