11 WebSocket
来源:互联网 发布:全国高校网络教育 编辑:程序博客网 时间:2024/06/11 07:38
本章介绍
- WebSocket
- ChannelHandler,Decoder and Encoder
- 引导一个Netty基础程序
- 测试WebSocket
“real-time-web”实时web现在随处可见,很多的用户希望能从web站点实时获取信息。Netty支持WebSocket实现,并包含了不同的版本,我们可以非常容易的实现WebSocket应用。使用Netty附带的WebSocket,我们不需要关注协议内部实现,只需要使用Netty提供的一些简单的方法就可以实现。本章将通过的例子应用帮助你来使用WebSocket并了解它是如何工作。
11.1WebSockets some background
关于WebSocket的一些概念和背景,可以查询网上相关介绍。这里不赘述。
11.2 面临的挑战
要显示“real-time”支持的WebSocket,应用程序将显示如何使用Netty中的WebSocket实现一个在浏览器中进行聊天的IRC应用程序。你可能知道从Facebook可以发送文本消息到另一个人,在这里,我们将进一步了解其实现。在这个应用程序中,不同的用户可以同时交谈,非常像IRC(Internet Relay Chat,互联网中继聊天)。
上图显示的逻辑很简单:
- 一个客户端发送一条消息
- 消息被广播到其他已连接的客户端
它的工作原理就像聊天室一样,在这里例子中,我们将编写服务器,然后使用浏览器作为客户端。带着这样的思路,我们将会很简单的实现它。
11.3 实现
WebSocket使用HTTP升级机制从一个普通的HTTP连接WebSocket,因为这个应用程序使用WebSocket总是开始于HTTP(s),然后再升级。什么时候升级取决于应用程序本身。直接执行升级作为第一个操作一般是使用特定的url请求。
在这里,如果url的结尾以/ws结束,我们将只会升级到WebSocket,否则服务器将发送一个网页给客户端。升级后的连接将通过WebSocket传输所有数据。逻辑图如下:
11.3.1 处理http请求
服务器将作为一种混合式以允许同时处理http和websocket,所以服务器还需要html页面,html用来充当客户端角色,连接服务器并交互消息。因此,如果客户端不发送/ws的uri,我们需要写一个ChannelInboundHandler用来处理FullHttpRequest。看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.channel.ChannelFuture;
4. import io.netty.channel.ChannelFutureListener;
5. import io.netty.channel.ChannelHandlerContext;
6. import io.netty.channel.DefaultFileRegion;
7. import io.netty.channel.SimpleChannelInboundHandler;
8. import io.netty.handler.codec.http.DefaultFullHttpResponse;
9. import io.netty.handler.codec.http.DefaultHttpResponse;
10.import io.netty.handler.codec.http.FullHttpRequest;
11. import io.netty.handler.codec.http.FullHttpResponse;
12.import io.netty.handler.codec.http.HttpHeaders;
13. import io.netty.handler.codec.http.HttpResponse;
14.import io.netty.handler.codec.http.HttpResponseStatus;
15. import io.netty.handler.codec.http.HttpVersion;
16.import io.netty.handler.codec.http.LastHttpContent;
17. import io.netty.handler.ssl.SslHandler;
18.import io.netty.handler.stream.ChunkedNioFile;
19.
20.import java.io.RandomAccessFile;
21.
22./**
23. * WebSocket,处理http请求
24. *
25. * @author c.k
26. *
27. */
28.public class HttpRequestHandler extends
29. SimpleChannelInboundHandler<FullHttpRequest> {
30. //websocket标识
31. private final String wsUri;
32.
33. public HttpRequestHandler(String wsUri) {
34. this.wsUri = wsUri;
35. }
36.
37. @Override
38. protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg)
39. throws Exception {
40. //如果是websocket请求,请求地址uri等于wsuri
41. if (wsUri.equalsIgnoreCase(msg.getUri())) {
42. //将消息转发到下一个ChannelHandler
43. ctx.fireChannelRead(msg.retain());
44. } else {//如果不是websocket请求
45. if (HttpHeaders.is100ContinueExpected(msg)) {
46. //如果HTTP请求头部包含Expect: 100-continue,
47. //则响应请求
48. FullHttpResponse response = new DefaultFullHttpResponse(
49. HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
50. ctx.writeAndFlush(response);
51. }
52. //获取index.html的内容响应给客户端
53. RandomAccessFile file = new RandomAccessFile(
54. System.getProperty("user.dir") + "/index.html", "r");
55. HttpResponse response = new DefaultHttpResponse(
56. msg.getProtocolVersion(), HttpResponseStatus.OK);
57. response.headers().set(HttpHeaders.Names.CONTENT_TYPE,
58. "text/html; charset=UTF-8");
59. boolean keepAlive = HttpHeaders.isKeepAlive(msg);
60. //如果http请求保持活跃,设置http请求头部信息
61. //并响应请求
62. if (keepAlive) {
63. response.headers().set(HttpHeaders.Names.CONTENT_LENGTH,
64. file.length());
65. response.headers().set(HttpHeaders.Names.CONNECTION,
66. HttpHeaders.Values.KEEP_ALIVE);
67. }
68. ctx.write(response);
69. //如果不是https请求,将index.html内容写入通道
70. if (ctx.pipeline().get(SslHandler.class) == null) {
71. ctx.write(new DefaultFileRegion(file.getChannel(), 0, file
72. .length()));
73. } else {
74. ctx.write(new ChunkedNioFile(file.getChannel()));
75. }
76. //标识响应内容结束并刷新通道
77. ChannelFuture future = ctx
78. .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
79. if (!keepAlive) {
80. //如果http请求不活跃,关闭http连接
81. future.addListener(ChannelFutureListener.CLOSE);
82. }
83. file.close();
84. }
85. }
86.
87. @Override
88. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
89. throws Exception {
90. cause.printStackTrace();
91. ctx.close();
92. }
93. }
11.3.2 处理WebSocket框架
WebSocket支持6种不同框架,如下图:
我们的程序只需要使用下面4个框架:
- CloseWebSocketFrame
- PingWebSocketFrame
- PongWebSocketFrame
- TextWebSocketFrame
我们只需要显示处理TextWebSocketFrame,其他的会自动由WebSocketServerProtocolHandler处理,看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.channel.ChannelHandlerContext;
4. import io.netty.channel.SimpleChannelInboundHandler;
5. import io.netty.channel.group.ChannelGroup;
6. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
7. import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
8.
9. /**
10. * WebSocket,处理消息
11. * @author c.k
12. *
13. */
14.public class TextWebSocketFrameHandler extends
15. SimpleChannelInboundHandler<TextWebSocketFrame> {
16. private final ChannelGroup group;
17.
18. public TextWebSocketFrameHandler(ChannelGroup group) {
19. this.group = group;
20. }
21.
22. @Override
23. public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
24. throws Exception {
25. //如果WebSocket握手完成
26. if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
27. //删除ChannelPipeline中的HttpRequestHandler
28. ctx.pipeline().remove(HttpRequestHandler.class);
29. //写一个消息到ChannelGroup
30. group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel()
31. + " joined"));
32. //将Channel添加到ChannelGroup
33. group.add(ctx.channel());
34. }else {
35. super.userEventTriggered(ctx, evt);
36. }
37. }
38.
39. @Override
40. protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
41. throws Exception {
42. //将接收的消息通过ChannelGroup转发到所以已连接的客户端
43. group.writeAndFlush(msg.retain());
44. }
45. }
11.3.3 初始化ChannelPipeline
看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.channel.Channel;
4. import io.netty.channel.ChannelInitializer;
5. import io.netty.channel.ChannelPipeline;
6. import io.netty.channel.group.ChannelGroup;
7. import io.netty.handler.codec.http.HttpObjectAggregator;
8. import io.netty.handler.codec.http.HttpServerCodec;
9. import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
10.import io.netty.handler.stream.ChunkedWriteHandler;
11.
12./**
13. * WebSocket,初始化ChannelHandler
14. * @author c.k
15. *
16. */
17. public class ChatServerInitializer extends ChannelInitializer<Channel> {
18. private final ChannelGroup group;
19.
20. public ChatServerInitializer(ChannelGroup group){
21. this.group = group;
22. }
23.
24. @Override
25. protected void initChannel(Channel ch) throws Exception {
26. ChannelPipeline pipeline = ch.pipeline();
27. //编解码http请求
28. pipeline.addLast(new HttpServerCodec());
29. //写文件内容
30. pipeline.addLast(new ChunkedWriteHandler());
31. //聚合解码HttpRequest/HttpContent/LastHttpContent到FullHttpRequest
32. //保证接收的Http请求的完整性
33. pipeline.addLast(new HttpObjectAggregator(64 * 1024));
34. //处理FullHttpRequest
35. pipeline.addLast(new HttpRequestHandler("/ws"));
36. //处理其他的WebSocketFrame
37. pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
38. //处理TextWebSocketFrame
39. pipeline.addLast(new TextWebSocketFrameHandler(group));
40. }
41.
42.}
WebSocketServerProtcolHandler不仅处理Ping/Pong/CloseWebSocketFrame,还和它自己握手并帮助升级WebSocket。这是执行完成握手和成功修改ChannelPipeline,并且添加需要的编码器/解码器和删除不需要的ChannelHandler。
看下图:
ChannelPipeline通过ChannelInitializer的initChannel(...)方法完成初始化,完成握手后就会更改事情。一旦这样做了,WebSocketServerProtocolHandler将取代HttpRequestDecoder、WebSocketFrameDecoder13和HttpResponseEncoder、WebSocketFrameEncoder13。另外也要删除所有不需要的ChannelHandler已获得最佳性能。这些都是HttpObjectAggregator和HttpRequestHandler。下图显示ChannelPipeline握手完成:
我们甚至没注意到它,因为它是在底层执行的。以非常灵活的方式动态更新ChannelPipeline让单独的任务在不同的ChannelHandler中实现。
11.4 结合在一起使用
一如既往,我们要将它们结合在一起使用。使用Bootstrap引导服务器和设置正确的ChannelInitializer。看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.bootstrap.ServerBootstrap;
4. import io.netty.channel.Channel;
5. import io.netty.channel.ChannelFuture;
6. import io.netty.channel.ChannelInitializer;
7. import io.netty.channel.EventLoopGroup;
8. import io.netty.channel.group.ChannelGroup;
9. import io.netty.channel.group.DefaultChannelGroup;
10.import io.netty.channel.nio.NioEventLoopGroup;
11. import io.netty.channel.socket.nio.NioServerSocketChannel;
12.import io.netty.util.concurrent.ImmediateEventExecutor;
13.
14.import java.net.InetSocketAddress;
15.
16./**
17. * 访问地址:http://localhost:2048
18. *
19. * @author c.k
20. *
21. */
22.public class ChatServer {
23.
24. private final ChannelGroup group = new DefaultChannelGroup(
25. ImmediateEventExecutor.INSTANCE);
26. private final EventLoopGroup workerGroup = new NioEventLoopGroup();
27. private Channel channel;
28.
29. public ChannelFuture start(InetSocketAddress address) {
30. ServerBootstrap b = new ServerBootstrap();
31. b.group(workerGroup).channel(NioServerSocketChannel.class)
32. .childHandler(createInitializer(group));
33. ChannelFuture f = b.bind(address).syncUninterruptibly();
34. channel = f.channel();
35. return f;
36. }
37.
38. public void destroy() {
39. if (channel != null)
40. channel.close();
41. group.close();
42. workerGroup.shutdownGracefully();
43. }
44.
45. protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
46. return new ChatServerInitializer(group);
47. }
48.
49. public static void main(String[] args) {
50. final ChatServer server = new ChatServer();
51. ChannelFuture f = server.start(new InetSocketAddress(2048));
52. Runtime.getRuntime().addShutdownHook(new Thread() {
53. @Override
54. public void run() {
55. server.destroy();
56. }
57. });
58. f.channel().closeFuture().syncUninterruptibly();
59. }
60.
61. }
另外,需要将index.html文件放在项目根目录,index.html内容如下:
[html] view plaincopy
1. <html>
2. <head>
3. <title>Web Socket Test</title>
4. </head>
5. <body>
6. <script type="text/javascript">
7. var socket;
8. if (!window.WebSocket) {
9. window.WebSocket = window.MozWebSocket;
10.}
11. if (window.WebSocket) {
12. socket = new WebSocket("ws://localhost:2048/ws");
13. socket.onmessage = function(event) {
14. var ta = document.getElementById('responseText');
15. ta.value = ta.value + '\n' + event.data
16. };
17. socket.onopen = function(event) {
18. var ta = document.getElementById('responseText');
19. ta.value = "Web Socket opened!";
20. };
21. socket.onclose = function(event) {
22. var ta = document.getElementById('responseText');
23. ta.value = ta.value + "Web Socket closed";
24. };
25. } else {
26. alert("Your browser does not support Web Socket.");
27. }
28.
29. function send(message) {
30. if (!window.WebSocket) { return; }
31. if (socket.readyState == WebSocket.OPEN) {
32. socket.send(message);
33. } else {
34. alert("The socket is not open.");
35. }
36.}
37. </script>
38. <form onsubmit="return false;">
39. <input type="text" name="message" value="Hello, World!"><input
40. type="button" value="Send Web Socket Data"
41. onclick="send(this.form.message.value)">
42. <h3>Output</h3>
43. <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
44. </form>
45. </body>
46.</html>
最后在浏览器中输入:http://localhost:2048,多开几个窗口就可以聊天了。
11.5 给WebSocket加密
上面的应用程序虽然工作的很好,但是在网络上收发消息存在很大的安全隐患,所以有必要对消息进行加密。添加这样一个加密的功能一般比较复杂,需要对代码有较大的改动。但是使用Netty就可以很容易的添加这样的功能,只需要将SslHandler加入到ChannelPipeline中就可以了。实际上还需要添加SslContext,但这不在本例子范围内。
首先我们创建一个用于添加加密Handler的handler初始化类,看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.channel.Channel;
4. import io.netty.channel.group.ChannelGroup;
5. import io.netty.handler.ssl.SslHandler;
6.
7. import javax.net.ssl.SSLContext;
8. import javax.net.ssl.SSLEngine;
9.
10.public class SecureChatServerIntializer extends ChatServerInitializer {
11. private final SSLContext context;
12.
13. public SecureChatServerIntializer(ChannelGroup group,SSLContext context) {
14. super(group);
15. this.context = context;
16. }
17.
18. @Override
19. protected void initChannel(Channel ch) throws Exception {
20. super.initChannel(ch);
21. SSLEngine engine = context.createSSLEngine();
22. engine.setUseClientMode(false);
23. ch.pipeline().addFirst(new SslHandler(engine));
24. }
25. }
最后我们创建一个用于引导配置的类,看下面代码:
[java] view plaincopy
1. package netty.in.action;
2.
3. import io.netty.channel.Channel;
4. import io.netty.channel.ChannelFuture;
5. import io.netty.channel.ChannelInitializer;
6. import io.netty.channel.group.ChannelGroup;
7. import java.net.InetSocketAddress;
8. import javax.net.ssl.SSLContext;
9.
10./**
11. * 访问地址:https://localhost:4096
12. *
13. * @author c.k
14. *
15. */
16.public class SecureChatServer extends ChatServer {
17. private final SSLContext context;
18.
19. public SecureChatServer(SSLContext context) {
20. this.context = context;
21. }
22.
23. @Override
24. protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
25. return new SecureChatServerIntializer(group, context);
26. }
27.
28. /**
29. * 获取SSLContext需要相关的keystore文件,这里没有 关于HTTPS可以查阅相关资料,这里只介绍在Netty中如何使用
30. *
31. * @return
32. */
33. private static SSLContext getSslContext() {
34. return null;
35. }
36.
37. public static void main(String[] args) {
38. SSLContext context = getSslContext();
39. final SecureChatServer server = new SecureChatServer(context);
40. ChannelFuture future = server.start(new InetSocketAddress(4096));
41. Runtime.getRuntime().addShutdownHook(new Thread() {
42. @Override
43. public void run() {
44. server.destroy();
45. }
46. });
47. future.channel().closeFuture().syncUninterruptibly();
48. }
49. }
11.6 Summary
- 11 WebSocket
- WebSocket
- WebSocket
- WebSocket
- websocket
- WebSocket
- WebSocket
- WebSocket
- WebSocket
- websocket
- websocket
- WebSocket
- WebSocket
- WebSocket
- WebSocket
- websocket
- websocket
- websocket
- 10 单元测试代码
- DP--二维背包--HDU - 2159
- MySQL学习笔记2-System administration(STOP or START MySQL)
- ecshop2.7.3土特产商城源码绿色大气整站带数据茶叶商城网站模板 修正
- pid学习整理
- 11 WebSocket
- junit测试时无法跳转到controller类中断点
- Ext xtype类型
- [转载]matlab figure
- [leetcode] 1. Two Sum
- delphi精要学习3
- 编译PHP make 时出现错误 make: *** [ext/fileinfo/libmagic/apprentice.lo] Error 1
- UIPickerview循环滚动
- spring事物配置方式和隔离等级说明