这个类实现SimpleChannelInboundHandler,SimpleChannelInboundHandler是一个抽象类,实现了中定义的channelRead方法,但同时定义了一个抽象的messageReceived方法,因此我们在MyWebSocketServerHandler类中,不需要实现channelRead方法,但需要实现messageReceived方法。当然,我们还需要覆盖ChannelHandlerAdapter的channelActive方法和channelInactive方法,因为我们想在客户端连接和关闭时做一些事情。
1、channelActive和channelInactive方法
这两个方法分别在新的客户端连接到服务端时触发,我们仅仅是在这两个方法中进行Channel的添加和移除操作,并输出一些内容到控制台而已。
2、messageReceived方法
真正的业务逻辑在这个方法里。在这个方法中,我们针对客户端的请求类型进行处理——因为我们不知道客户端到底会是什么样子以及会以何种方式请求服务端。如果客户端是以WebSocket的方式(即ws://)请求的,我们调用handlerWebSocketFrame进行处理,否则调用handleHttpRequest方法。
3、handleWebSocketFrame方法
这个方法用于处理WebSocket请求,即WebSocket握手完成后(即handshaker已经初始化)的消息。
首先需要判断WebSocket请求(即WebSocketFrame类)的具体类型,以进行不同的操作。
这里需要介绍一下WebSocket 数据传输格式中的Opcode定义。Opcode是一个4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
* %x0 表示连续消息片断
* %x1 表示文本消息片断
* %x2 表示二进制消息片断
* %x3-7 为将来的非控制消息片断保留的操作码
* %x8 表示连接关闭
* %x9 表示心跳检查的ping
* %xA 表示心跳检查的pong
* %xB-F 为将来的控制消息片断的保留操作码
根据这个,我们可以对frame的类型进行判断:
首先判断是不是WebSocket关闭操作:
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
如果是,关闭handshaker,即WebSocket会话。
然后判断是不是Ping消息,如果是,则发送一个Pong消息给客户端:
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
WebSocket中,Ping、Pong消息用于心跳检查。客户端使用的叫做Ping,服务端对之进行的响应叫做Pong。
注意,凡是引用到frame时,都要retain一下。因为netty所有io操作都是异步的,这样做是防止frame在还没有用完的时候就被释放掉了。
然后判断消息的类型是否是文本消息:
if (!(frame instanceof TextWebSocketFrame)) {
System.out.println("本例程仅支持文本消息,不支持二进制消息");
throw newUnsupportedOperationException(String.format(
"%s frame types notsupported", frame.getClass().getName()));
}
如果不是,抛出UpsupportedOperationException错误。
如果前面三种情况都不是,则frame应该是一个合法的WebSocket文本消息了,我们进行接下来的处理:
String request = ((TextWebSocketFrame) frame).text();
System.out.println("服务端收到:" + request);
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("%s received %s", ctx.channel(),request));
}
TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()+ctx.channel().id() + ":" + request);
// 群发
Global.group.writeAndFlush(tws);
打印收到的内容,记录日志,然后最后一句实现群发。
3、handleHttpRequest方法
这个方法处理HTTP请求。一个WebSocket会话的开始其实是由一个HTTP请求开始的。根据HTTP1.1的定义,这个HTTP请求的头信息中必须包含一个Upgrade:websocket的key-value。如果不包含则表明这不是一个标准的WebSocket会话的开始,我们可以调用sendHttpResponse输出一个Bad Request错误:
if (!req.getDecoderResult().isSuccess()
||(!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
接下来开始进行WebSocket连接,这是通过创建一个WebSocket的handshaker对象来完成的:
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:7397/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
如果handshaker创建失败,发送错误消息,否则开始进行握手动作:
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
4、sendHttpResponse方法
这个方法用于向客户端输出一些HTTP消息。
首先它判断服务器要输出的是不是HTTP200状态(准备就绪),如果不是,表明出错了,将HTTP状态码输出给客户端:
if (res.getStatus().code() != 200) {
ByteBufbuf = Unpooled.copiedBuffer(res.getStatus().toString(),CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
ChannelFuture f =ctx.channel().writeAndFlush(res);
然后关闭连接。当然要判断一下keep alive 和 HTTP 200标志:
if (!isKeepAlive(req) || res.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
根据HTTP1.1协议的规定,
五、NettyServer.java
这个类代表了服务端主线程。在run方法中,我们将所有类串联在一起。对于netty客户端,需要用到两个group:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
其中bossGroup用于所有channel,workGroup则应用于某个channel。
然后是规定动作ServerBootstrap,group,channel以及handler等等,这些代码都非常模式化,恐怕不需要再多做说明了:
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(newChildChannelHandler());
System.out.println("服务端开启等待客户端连接 ... ...");
Channel ch = b.bind(7397).sync().channel();
ch.closeFuture().sync();
六、index.html
这个就是所谓的客户端了,是跑在浏览器里的东东。这个文件随便你放在那里(不需要放到web服务器上),反正用浏览器一打开,其中的js脚本就会生成一个WebSocket客户端:
<!DOCTYPE html PUBLIC "-//W3C//DTDXHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<metahttp-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>无标题文档</title>
</head>
</head>
<script type="text/javascript">
varsocket;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
if(window.WebSocket){
socket = newWebSocket("ws://localhost:7397/websocket");
socket.onmessage = function(event){
varta = document.getElementById(‘responseText‘);
ta.value+= event.data+"\r\n";
};
socket.onopen = function(event){
varta = document.getElementById(‘responseText‘);
ta.value= "打开WebSoket 服务正常,浏览器支持WebSoket!"+"\r\n";
};
socket.onclose = function(event){
varta = document.getElementById(‘responseText‘);
ta.value= "";
ta.value= "WebSocket 关闭"+"\r\n";
};
}else{
alert("您的浏览器不支持WebSocket协议!");
}
function send(message){
if(!window.WebSocket){return;}
if(socket.readyState== WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket连接没有建立成功!");
}
}
</script>
<body>
<form onSubmit="return false;">
<input type = "text"name="message" value="Netty The Sinper"/>
<br/><br/>
<input type="button"value="发送 WebSocket 请求消息" onClick="send(this.form.message.value)"/>
<hr color="blue"/>
<h3>服务端返回的应答消息</h3>
<textarea id="responseText"style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>
function send(message){
if(!window.WebSocket){return;}
if(socket.readyState== WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket连接没有建立成功!");
}
}