Netty笔记:使用WebSocket协议开发聊天系统

转载请注明出处:http://blog.csdn.net/a906998248/article/details/52839425

前言,之前一直围绕着Http协议来开发项目,最近由于参与一个类似竞拍项目的开发,有这样一个场景,多个客户端竞拍一个商品,当一个客户端加价后,其它关注这个商品的客户端需要立即知道该商品的最新价格。
       这里有个问题,Http协议是基于请求/响应的,客户端发送请求,然后服务端响应返回,客户端是主动方,服务端被动的接收客户端的请求来响应,无法解决上述场景中服务端主动将最新的数据推送给客户端的需求。
       当然,有人会提出ajax轮询的方案,就是客户端不断的请求(假如1秒1次)最新竞拍价格。显然这种模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,但是Http request的Header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。

还有一种比较新颖的方案,long poll(长轮询)。利用长轮询,客户端可以打开指向服务端的Http连接,而服务器会一直保持连接打开,直到服务端数据更新再发送响应。虽然这种方式比ajax轮询有进步,但都存在一个共同问题:由于Http协议的开销,导致它们不适合用于低延迟应用。

一.WebSocket协议简介

WebSocket 是 Html5 开始提供的一种浏览器与服务器间进行全双工通信的网络技术。(全双工:同一时刻,数据可以在客户端和服务端两个方向上传输)

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了

二.相比传统Http协议的优点及作用
  1.Http协议的弊端:
    a.Http协议为半双工协议。(半双工:同一时刻,数据只能在客户端和服务端一个方向上传输)
    b.Http协议冗长且繁琐
    c.易收到攻击,如长轮询
    d.非持久化协议
  2.WebSocket的特性:
    a.单一的 TCP 连接,采用全双工模式通信
    b.对代理、防火墙和路由器透明
    c.无头部信息、Cookie 和身份验证
    d.无安全开销
    e.通过 ping/pong 帧保持链路激活
    f.持久化协议,连接建立后,服务器可以主动传递消息给客户端,不再需要客户端轮询

三.聊天实例
       前面提到过,WebSocket通信需要建立WebSocket连接,客户端首先要向服务端发起一个 Http 请求,这个请求和通常的 Http 请求不同,包含了一些附加头信息,其中附加信息"Upgrade:WebSocket"表明这是一个基于 Http 的 WebSocket 握手请求。如下:

[html] view plain copy

print?

  1. GET /chat HTTP/1.1
  2. Host: server.example.com
  3. Upgrade: websocket
  4. Connection: Upgrade
  5. Sec-WebSocket-Key: sdewgzgfewfsgergzgewrfaf==
  6. Sec-WebSocket-Protocol: chat, superchat
  7. Sec-WebSocket-Version: 13
  8. Origin: http://example.com

其中,Sec-WebSocket-Key是随机的,服务端会使用它加密后作为Sec-WebSocket-Accept的值返回;Sec-WebSocket-Protocol是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议;Sec-WebSocket-Version是告诉服务器所使用的Websocket Draft(协议版本)
  
       不出意外,服务端会返回下列信息表示握手成功,连接已经建立:

[html] view plain copy

print?

  1. HTTP/1.1 101 Switching Protocols
  2. Upgrade: websocket
  3. Connection: Upgrade
  4. Sec-WebSocket-Accept: sdgdfshgretghsdfgergtbd=
  5. Sec-WebSocket-Protocol: chat

到这里 WebSocket 连接已经成功建立,服务端和客户端可以正常通信了,此时服务端和客户端都是对等端点,都可以主动发送请求到另一端。

下面是前端和后端的实现过程,后端我采用了 Netty 的 API,因为最近在学 Netty,所以就采用了 Netty 中的 NIO 来构建 WebSocket 后端,我看了下网上也有用 Tomcat API 来实现,看起来也很简单,朋友们可以试试。前端使用HTML5 来构建,可以参考WebSocket接口文档,非常方便简单。

Lanucher用来启动WebSocket服务端

[java] view plain copy

print?

  1. import com.company.server.WebSocketServer;
  2. public class Lanucher {
  3. public static void main(String[] args) throws Exception {
  4. // 启动WebSocket
  5. new WebSocketServer().run(WebSocketServer.WEBSOCKET_PORT);
  6. }
  7. }

使用 Netty 构建的 WebSocket 服务

[java] view plain copy

print?

  1. import org.apache.log4j.Logger;
  2. import io.netty.bootstrap.ServerBootstrap;
  3. import io.netty.channel.Channel;
  4. import io.netty.channel.ChannelInitializer;
  5. import io.netty.channel.ChannelPipeline;
  6. import io.netty.channel.EventLoopGroup;
  7. import io.netty.channel.nio.NioEventLoopGroup;
  8. import io.netty.channel.socket.nio.NioServerSocketChannel;
  9. import io.netty.handler.codec.http.HttpObjectAggregator;
  10. import io.netty.handler.codec.http.HttpServerCodec;
  11. import io.netty.handler.stream.ChunkedWriteHandler;
  12. /**
  13. * WebSocket服务
  14. *
  15. */
  16. public class WebSocketServer {
  17. private static final Logger LOG = Logger.getLogger(WebSocketServer.class);
  18. // websocket端口
  19. public static final int WEBSOCKET_PORT = 9090;
  20. public void run(int port) throws Exception {
  21. EventLoopGroup bossGroup = new NioEventLoopGroup();
  22. EventLoopGroup workerGroup = new NioEventLoopGroup();
  23. try {
  24. ServerBootstrap b = new ServerBootstrap();
  25. b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<Channel>() {
  26. @Override
  27. protected void initChannel(Channel channel) throws Exception {
  28. ChannelPipeline pipeline = channel.pipeline();
  29. pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码
  30. pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装
  31. pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持
  32. pipeline.addLast("handler", new BananaWebSocketServerHandler()); // WebSocket服务端Handler
  33. }
  34. });
  35. Channel channel = b.bind(port).sync().channel();
  36. LOG.info("WebSocket 已经启动,端口:" + port + ".");
  37. channel.closeFuture().sync();
  38. } finally {
  39. bossGroup.shutdownGracefully();
  40. workerGroup.shutdownGracefully();
  41. }
  42. }
  43. }

WebSocket 服务端处理类,注意第一次握手是 Http 协议

[java] view plain copy

print?

  1. import io.netty.buffer.ByteBuf;
  2. import io.netty.buffer.Unpooled;
  3. import io.netty.channel.ChannelFuture;
  4. import io.netty.channel.ChannelFutureListener;
  5. import io.netty.channel.ChannelHandlerContext;
  6. import io.netty.channel.ChannelPromise;
  7. import io.netty.channel.SimpleChannelInboundHandler;
  8. import io.netty.handler.codec.http.DefaultFullHttpResponse;
  9. import io.netty.handler.codec.http.FullHttpRequest;
  10. import io.netty.handler.codec.http.FullHttpResponse;
  11. import io.netty.handler.codec.http.HttpHeaders;
  12. import io.netty.handler.codec.http.HttpResponseStatus;
  13. import io.netty.handler.codec.http.HttpVersion;
  14. import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
  15. import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
  16. import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
  17. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
  18. import io.netty.handler.codec.http.websocketx.WebSocketFrame;
  19. import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
  20. import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
  21. import io.netty.util.CharsetUtil;
  22. import org.apache.log4j.Logger;
  23. import com.company.serviceimpl.BananaService;
  24. import com.company.util.CODE;
  25. import com.company.util.Request;
  26. import com.company.util.Response;
  27. import com.google.common.base.Strings;
  28. import com.google.gson.JsonSyntaxException;
  29. /**
  30. * WebSocket服务端Handler
  31. *
  32. */
  33. public class BananaWebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
  34. private static final Logger LOG = Logger.getLogger(BananaWebSocketServerHandler.class.getName());
  35. private WebSocketServerHandshaker handshaker;
  36. private ChannelHandlerContext ctx;
  37. private String sessionId;
  38. @Override
  39. public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
  40. if (msg instanceof FullHttpRequest) { // 传统的HTTP接入
  41. handleHttpRequest(ctx, (FullHttpRequest) msg);
  42. } else if (msg instanceof WebSocketFrame) { // WebSocket接入
  43. handleWebSocketFrame(ctx, (WebSocketFrame) msg);
  44. }
  45. }
  46. @Override
  47. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  48. ctx.flush();
  49. }
  50. @Override
  51. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  52. LOG.error("WebSocket异常", cause);
  53. ctx.close();
  54. LOG.info(sessionId + "  注销");
  55. BananaService.logout(sessionId); // 注销
  56. BananaService.notifyDownline(sessionId); // 通知有人下线
  57. }
  58. @Override
  59. public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
  60. LOG.info("WebSocket关闭");
  61. super.close(ctx, promise);
  62. LOG.info(sessionId + " 注销");
  63. BananaService.logout(sessionId); // 注销
  64. BananaService.notifyDownline(sessionId); // 通知有人下线
  65. }
  66. /**
  67. * 处理Http请求,完成WebSocket握手<br/>
  68. * 注意:WebSocket连接第一次请求使用的是Http
  69. * @param ctx
  70. * @param request
  71. * @throws Exception
  72. */
  73. private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
  74. // 如果HTTP解码失败,返回HHTP异常
  75. if (!request.getDecoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) {
  76. sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
  77. return;
  78. }
  79. // 正常WebSocket的Http连接请求,构造握手响应返回
  80. WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HttpHeaders.Names.HOST), null, false);
  81. handshaker = wsFactory.newHandshaker(request);
  82. if (handshaker == null) { // 无法处理的websocket版本
  83. WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
  84. } else { // 向客户端发送websocket握手,完成握手
  85. handshaker.handshake(ctx.channel(), request);
  86. // 记录管道处理上下文,便于服务器推送数据到客户端
  87. this.ctx = ctx;
  88. }
  89. }
  90. /**
  91. * 处理Socket请求
  92. * @param ctx
  93. * @param frame
  94. * @throws Exception
  95. */
  96. private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
  97. // 判断是否是关闭链路的指令
  98. if (frame instanceof CloseWebSocketFrame) {
  99. handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
  100. return;
  101. }
  102. // 判断是否是Ping消息
  103. if (frame instanceof PingWebSocketFrame) {
  104. ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
  105. return;
  106. }
  107. // 当前只支持文本消息,不支持二进制消息
  108. if (!(frame instanceof TextWebSocketFrame)) {
  109. throw new UnsupportedOperationException("当前只支持文本消息,不支持二进制消息");
  110. }
  111. // 处理来自客户端的WebSocket请求
  112. try {
  113. Request request = Request.create(((TextWebSocketFrame)frame).text());
  114. Response response = new Response();
  115. response.setServiceId(request.getServiceId());
  116. if (CODE.online.code.intValue() == request.getServiceId()) { // 客户端注册
  117. String requestId = request.getRequestId();
  118. if (Strings.isNullOrEmpty(requestId)) {
  119. response.setIsSucc(false).setMessage("requestId不能为空");
  120. return;
  121. } else if (Strings.isNullOrEmpty(request.getName())) {
  122. response.setIsSucc(false).setMessage("name不能为空");
  123. return;
  124. } else if (BananaService.bananaWatchMap.containsKey(requestId)) {
  125. response.setIsSucc(false).setMessage("您已经注册了,不能重复注册");
  126. return;
  127. }
  128. if (!BananaService.register(requestId, new BananaService(ctx, request.getName()))) {
  129. response.setIsSucc(false).setMessage("注册失败");
  130. } else {
  131. response.setIsSucc(true).setMessage("注册成功");
  132. BananaService.bananaWatchMap.forEach((reqId, callBack) -> {
  133. response.getHadOnline().put(reqId, ((BananaService)callBack).getName()); // 将已经上线的人员返回
  134. if (!reqId.equals(requestId)) {
  135. Request serviceRequest = new Request();
  136. serviceRequest.setServiceId(CODE.online.code);
  137. serviceRequest.setRequestId(requestId);
  138. serviceRequest.setName(request.getName());
  139. try {
  140. callBack.send(serviceRequest); // 通知有人上线
  141. } catch (Exception e) {
  142. LOG.warn("回调发送消息给客户端异常", e);
  143. }
  144. }
  145. });
  146. }
  147. sendWebSocket(response.toJson());
  148. this.sessionId = requestId; // 记录会话id,当页面刷新或浏览器关闭时,注销掉此链路
  149. } else if (CODE.send_message.code.intValue() == request.getServiceId()) { // 客户端发送消息到聊天群
  150. String requestId = request.getRequestId();
  151. if (Strings.isNullOrEmpty(requestId)) {
  152. response.setIsSucc(false).setMessage("requestId不能为空");
  153. } else if (Strings.isNullOrEmpty(request.getName())) {
  154. response.setIsSucc(false).setMessage("name不能为空");
  155. } else if (Strings.isNullOrEmpty(request.getMessage())) {
  156. response.setIsSucc(false).setMessage("message不能为空");
  157. } else {
  158. response.setIsSucc(true).setMessage("发送消息成功");
  159. BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 将消息发送到所有机器
  160. Request serviceRequest = new Request();
  161. serviceRequest.setServiceId(CODE.receive_message.code);
  162. serviceRequest.setRequestId(requestId);
  163. serviceRequest.setName(request.getName());
  164. serviceRequest.setMessage(request.getMessage());
  165. try {
  166. callBack.send(serviceRequest);
  167. } catch (Exception e) {
  168. LOG.warn("回调发送消息给客户端异常", e);
  169. }
  170. });
  171. }
  172. sendWebSocket(response.toJson());
  173. } else if (CODE.downline.code.intValue() == request.getServiceId()) { // 客户端下线
  174. String requestId = request.getRequestId();
  175. if (Strings.isNullOrEmpty(requestId)) {
  176. sendWebSocket(response.setIsSucc(false).setMessage("requestId不能为空").toJson());
  177. } else {
  178. BananaService.logout(requestId);
  179. response.setIsSucc(true).setMessage("下线成功");
  180. BananaService.notifyDownline(requestId); // 通知有人下线
  181. sendWebSocket(response.toJson());
  182. }
  183. } else {
  184. sendWebSocket(response.setIsSucc(false).setMessage("未知请求").toJson());
  185. }
  186. } catch (JsonSyntaxException e1) {
  187. LOG.warn("Json解析异常", e1);
  188. } catch (Exception e2) {
  189. LOG.error("处理Socket请求异常", e2);
  190. }
  191. }
  192. /**
  193. * Http返回
  194. * @param ctx
  195. * @param request
  196. * @param response
  197. */
  198. private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
  199. // 返回应答给客户端
  200. if (response.getStatus().code() != 200) {
  201. ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8);
  202. response.content().writeBytes(buf);
  203. buf.release();
  204. HttpHeaders.setContentLength(response, response.content().readableBytes());
  205. }
  206. // 如果是非Keep-Alive,关闭连接
  207. ChannelFuture f = ctx.channel().writeAndFlush(response);
  208. if (!HttpHeaders.isKeepAlive(request) || response.getStatus().code() != 200) {
  209. f.addListener(ChannelFutureListener.CLOSE);
  210. }
  211. }
  212. /**
  213. * WebSocket返回
  214. * @param ctx
  215. * @param req
  216. * @param res
  217. */
  218. public void sendWebSocket(String msg) throws Exception {
  219. if (this.handshaker == null || this.ctx == null || this.ctx.isRemoved()) {
  220. throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息");
  221. }
  222. this.ctx.channel().write(new TextWebSocketFrame(msg));
  223. this.ctx.flush();
  224. }
  225. }

聊天服务接口和实现类

[java] view plain copy

print?

  1. import com.company.util.Request;
  2. public interface BananaCallBack {
  3. // 服务端发送消息给客户端
  4. void send(Request request) throws Exception;
  5. }

[java] view plain copy

print?

  1. import io.netty.channel.ChannelHandlerContext;
  2. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
  3. import java.util.Map;
  4. import java.util.concurrent.ConcurrentHashMap;
  5. import org.apache.log4j.Logger;
  6. import com.company.service.BananaCallBack;
  7. import com.company.util.CODE;
  8. import com.company.util.Request;
  9. import com.google.common.base.Strings;
  10. public class BananaService implements BananaCallBack {
  11. private static final Logger LOG = Logger.getLogger(BananaService.class);
  12. public static final Map<String, BananaCallBack> bananaWatchMap = new ConcurrentHashMap<String, BananaCallBack>(); // <requestId, callBack>
  13. private ChannelHandlerContext ctx;
  14. private String name;
  15. public BananaService(ChannelHandlerContext ctx, String name) {
  16. this.ctx = ctx;
  17. this.name = name;
  18. }
  19. public static boolean register(String requestId, BananaCallBack callBack) {
  20. if (Strings.isNullOrEmpty(requestId) || bananaWatchMap.containsKey(requestId)) {
  21. return false;
  22. }
  23. bananaWatchMap.put(requestId, callBack);
  24. return true;
  25. }
  26. public static boolean logout(String requestId) {
  27. if (Strings.isNullOrEmpty(requestId) || !bananaWatchMap.containsKey(requestId)) {
  28. return false;
  29. }
  30. bananaWatchMap.remove(requestId);
  31. return true;
  32. }
  33. @Override
  34. public void send(Request request) throws Exception {
  35. if (this.ctx == null || this.ctx.isRemoved()) {
  36. throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息");
  37. }
  38. this.ctx.channel().write(new TextWebSocketFrame(request.toJson()));
  39. this.ctx.flush();
  40. }
  41. /**
  42. * 通知所有机器有机器下线
  43. * @param requestId
  44. */
  45. public static void notifyDownline(String requestId) {
  46. BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 通知有人下线
  47. Request serviceRequest = new Request();
  48. serviceRequest.setServiceId(CODE.downline.code);
  49. serviceRequest.setRequestId(requestId);
  50. try {
  51. callBack.send(serviceRequest);
  52. } catch (Exception e) {
  53. LOG.warn("回调发送消息给客户端异常", e);
  54. }
  55. });
  56. }
  57. public String getName() {
  58. return name;
  59. }
  60. }

前端html5聊天页面及js

[html] view plain copy

print?

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Netty WebSocket 聊天实例</title>
  6. </head>
  7. <script src="jquery.min.js" type="text/javascript"></script>
  8. <script src="map.js" type="text/javascript"></script>
  9. <script type="text/javascript">
  10. $(document).ready(function() {
  11. var uuid = guid(); // uuid在一个会话唯一
  12. var nameOnline = ‘‘; // 上线姓名
  13. var onlineName = new Map(); // 已上线人员, <requestId, name>
  14. $("#name").attr("disabled","disabled");
  15. $("#onlineBtn").attr("disabled","disabled");
  16. $("#downlineBtn").attr("disabled","disabled");
  17. $("#banana").hide();
  18. // 初始化websocket
  19. var socket;
  20. if (!window.WebSocket) {
  21. window.WebSocket = window.MozWebSocket;
  22. }
  23. if (window.WebSocket) {
  24. socket = new WebSocket("ws://localhost:9090/");
  25. socket.onmessage = function(event) {
  26. console.log("收到服务器消息:" + event.data);
  27. if (event.data.indexOf("isSucc") != -1) {// 这里需要判断是客户端请求服务端返回后的消息(response)
  28. var response = JSON.parse(event.data);
  29. if (response != undefined && response != null) {
  30. if (response.serviceId == 1001) { // 上线
  31. if (response.isSucc) {
  32. // 上线成功,初始化已上线人员
  33. onlineName.clear();
  34. $("#showOnlineNames").empty();
  35. for (var reqId in response.hadOnline) {
  36. onlineName.put(reqId, response.hadOnline[reqId]);
  37. }
  38. initOnline();
  39. $("#name").attr("disabled","disabled");
  40. $("#onlineBtn").attr("disabled","disabled");
  41. $("#downlineBtn").removeAttr("disabled");
  42. $("#banana").show();
  43. } else {
  44. alert("上线失败");
  45. }
  46. } else if (response.serviceId == 1004) {
  47. if (response.isSucc) {
  48. onlineName.clear();
  49. $("#showBanana").empty();
  50. $("#showOnlineNames").empty();
  51. $("#name").removeAttr("disabled");
  52. $("#onlineBtn").removeAttr("disabled");
  53. $("#downlineBtn").attr("disabled","disabled");
  54. $("#banana").hide();
  55. } else {
  56. alert("下线失败");
  57. }
  58. }
  59. }
  60. } else {// 还是服务端向客户端的请求(request)
  61. var request = JSON.parse(event.data);
  62. if (request != undefined && request != null) {
  63. if (request.serviceId == 1001 || request.serviceId == 1004) { // 有人上线/下线
  64. if (request.serviceId == 1001) {
  65. onlineName.put(request.requestId, request.name);
  66. }
  67. if (request.serviceId == 1004) {
  68. onlineName.removeByKey(request.requestId);
  69. }
  70. initOnline();
  71. } else if (request.serviceId == 1003) { // 有人发消息
  72. appendBanana(request.name, request.message);
  73. }
  74. }
  75. }
  76. };
  77. socket.onopen = function(event) {
  78. $("#name").removeAttr("disabled");
  79. $("#onlineBtn").removeAttr("disabled");
  80. console.log("已连接服务器");
  81. };
  82. socket.onclose = function(event) { // WebSocket 关闭
  83. console.log("WebSocket已经关闭!");
  84. };
  85. socket.onerror = function(event) {
  86. console.log("WebSocket异常!");
  87. };
  88. } else {
  89. alert("抱歉,您的浏览器不支持WebSocket协议!");
  90. }
  91. // WebSocket发送请求
  92. function send(message) {
  93. if (!window.WebSocket) { return; }
  94. if (socket.readyState == WebSocket.OPEN) {
  95. socket.send(message);
  96. } else {
  97. console.log("WebSocket连接没有建立成功!");
  98. alert("您还未连接上服务器,请刷新页面重试");
  99. }
  100. }
  101. // 刷新上线人员
  102. function initOnline() {
  103. $("#showOnlineNames").empty();
  104. for (var i=0;i<onlineName.size();i++) {
  105. $("#showOnlineNames").append(‘<tr><td>‘ + (i+1) + ‘</td>‘ +
  106. ‘<td>‘ + onlineName.element(i).value + ‘</td>‘ +
  107. ‘</tr>‘);
  108. }
  109. }
  110. // 追加聊天信息
  111. function appendBanana(name, message) {
  112. $("#showBanana").append(‘<tr><td>‘ + name + ‘: ‘ + message + ‘</td></tr>‘);
  113. }
  114. $("#onlineBtn").bind("click", function() {
  115. var name = $("#name").val();
  116. if (name == null || name == ‘‘) {
  117. alert("请输入您的尊姓大名");
  118. return;
  119. }
  120. nameOnline = name;
  121. // 上线
  122. send(JSON.stringify({"requestId":uuid, "serviceId":1001, "name":name}));
  123. });
  124. $("#downlineBtn").bind("click", function() {
  125. // 下线
  126. send(JSON.stringify({"requestId":uuid, "serviceId":1004}));
  127. });
  128. $("#sendBtn").bind("click", function() {
  129. var message = $("#messageInput").val();
  130. if (message == null || message == ‘‘) {
  131. alert("请输入您的聊天信息");
  132. return;
  133. }
  134. // 发送聊天消息
  135. send(JSON.stringify({"requestId":uuid, "serviceId":1002, "name":nameOnline, "message":message}));
  136. $("#messageInput").val("");
  137. });
  138. });
  139. function guid() {
  140. function S4() {
  141. return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  142. }
  143. return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
  144. }
  145. </script>
  146. <body>
  147. <h1>Netty WebSocket 聊天实例</h1>
  148. <input type="text" id="name" value="佚名" placeholder="姓名" />
  149. <input type="button" id="onlineBtn" value="上线" />
  150. <input type="button" id="downlineBtn" value="下线" />
  151. <hr/>
  152. <table id="banana" border="1" >
  153. <tr>
  154. <td width="600" align="center">聊天</td>
  155. <td width="100" align="center">上线人员</td>
  156. </tr>
  157. <tr height="200" valign="top">
  158. <td>
  159. <table id="showBanana" border="0" width="600">
  160. <!--
  161. <tr>
  162. <td>张三: 大家好</td>
  163. </tr>
  164. <tr>
  165. <td>李四: 欢迎加入群聊</td>
  166. </tr>
  167. -->
  168. </table>
  169. </td>
  170. <td>
  171. <table id="showOnlineNames" border="0">
  172. <!--
  173. <tr>
  174. <td>1</td>
  175. <td>张三</td>
  176. <tr/>
  177. <tr>
  178. <td>2</td>
  179. <td>李四</td>
  180. <tr/>
  181. -->
  182. </table>
  183. </td>
  184. </tr>
  185. <tr height="40">
  186. <td></td>
  187. <td></td>
  188. </tr>
  189. <tr>
  190. <td>
  191. <input type="text" id="messageInput"  style="width:590px" placeholder="巴拉巴拉点什么吧" />
  192. </td>
  193. <td>
  194. <input type="button" id="sendBtn" value="发送" />
  195. </td>
  196. </tr>
  197. </table>
  198. </body>
  199. </html>

运行方式:

1.运行Lanucher来启动后端的 WebSocket服务

2.打开Resources下的banana.html页面即可在线聊天,如下:

当有人上线/下线时,右边的"上线人员"会动态变化

综上,WebSocket 协议用于构建低延迟的服务,如竞拍、股票行情等,使用 Netty 可以方便的构建 WebSocket 服务,需要注意的是,WebSocket 协议基于 Http协议,采用 Http 握手成功后,就可以进行 TCP 全双工通信了。

GitHub上源码:https://github.com/leonzm/websocket_demo

参考:
《Netty 权威指南》

知乎上关于WebSocket

Websocket使用实例解读 -- tomcat

WebSocket API 接口

HTML5 WebSockets 教程

时间: 2024-08-03 18:00:18

Netty笔记:使用WebSocket协议开发聊天系统的相关文章

netty(4)高级篇-Websocket协议开发

一.HTTP协议的弊端 将HTTP协议的主要弊端总结如下: (1) 半双工协议:可以在客户端和服务端2个方向上传输,但是不能同时传输.同一时刻,只能在一个方向上传输. (2) HTTP消息冗长:相比于其他二进制协议,有点繁琐. (3) 针对服务器推送的黑客攻击,例如长时间轮询. 现在很多网站的消息推送都是使用轮询,即客户端每隔1S或者其他时间给服务器发送请求,然后服务器返回最新的数据给客户端.HTTP协议中的Header非常冗长,因此会占用很多的带宽和服务器资源. 比较新的技术是Comet,使用

netty websocket协议开发

websocket的好处我们就不用多说了,就是用于解决长连接.服务推送等需要的一种技术. 以下我们来看一个例子: 1 package com.ming.netty.http.websocket; 2 3 import java.net.InetSocketAddress; 4 5 import io.netty.bootstrap.ServerBootstrap; 6 import io.netty.channel.ChannelFuture; 7 import io.netty.channel

Netty笔记——技术点汇总

目录 · Linux网络IO模型 · 文件描述符 · 阻塞IO模型 · 非阻塞IO模型 · IO复用模型 · 信号驱动IO模型 · 异步IO模型 · BIO编程 · 伪异步IO编程 · NIO编程 · Buffer和Channel · 深入Buffer · Selector · AIO编程 · 四种IO编程对比及选择Netty的原因 · Netty入门 · 开发与部署 · Hello World · 粘包/拆包问题 · 问题及其解决 · LineBasedFrameDecoder · Delim

WebSocket 协议

1. WebSocket 协议开发. WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通讯的网络技术,WebSocket通信协议于2011年被IETF定为标准 RFC6455 , WebSocket API 被W3C定为标准. 在 WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快熟通道,两者就可以直接互相传送数据了,WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接受消息,相

WebSocket协议探究(二)

一 复习和目标 1 复习 协议概述: WebSocket内置消息定界并且全双工通信 WebSocket使用HTTP进行协议协商,协商成功使用TCP连接进行传输数据 WebScoket数据格式支持二进制和文本 初始握手和计算响应键值 消息格式 关闭握手 2 目标 Nodejs实现WebSocket服务器 Netty实现WebSocket服务器 Js api实现WebSocket客户端 二 Nodejs实现WebScoket服务器 1 概述 Node.js 使用事件驱动, 非阻塞I/O 模型而得以轻

netty高级篇(3)-HTTP协议开发

一.HTTP协议简介 应用层协议http,发展至今已经是http2.0了,拥有以下特点: (1) CS模式的协议 (2) 简单 - 只需要服务URL,携带必要的请求参数或者消息体 (3) 灵活 - 任意类型,传输内容类型由HTTP消息头中的Content-Type加以标记 (4) 无状态 - 必须借助额外手段,比如session或者cookie来保持状态 1.1 HTTP请求消息(HttpRequest) 客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line

MQTT协议笔记之mqtt.io项目Websocket协议支持

前言 MQTT协议专注于网络.资源受限环境,建立之初不曾考虑WEB环境,倒也正常.虽然如此,但不代表它不适合HTML5环境. HTML5 Websocket是建立在TCP基础上的双通道通信,和TCP通信方式很类似,适用于WEB浏览器环境.虽然MQTT基因层面选择了TCP作为通信通道,但我们添加个编解码方式,MQTT Over Websocket也可以的. 这样做的好处,MQTT的使用范畴被扩展到HTML5.桌面端浏览器.移动端WebApp.Hybrid等,多了一些想像空间.这样看来,无论是移动端

HTTP和WebSocket协议

websocket是一个新的基于TCP的应用层协议,只需要一次连接,以后的数据不需要重新建立连接,可以直接发送,它是基于TCP的,属于和HTTP相同的地位, 通过消息的方式触发. HTTP HTTP的地址格式如下: http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] 协议和host不分大小写 HTTP消息 一个HTTP消息可能是request或

刨根问底 HTTP 和 WebSocket 协议(下)

上篇介绍了HTTP1.1协议的基本内容,这篇文章将继续分析WebSocket协议,然后对这两个进行简单的比较. WebSocket WebSocket协议还很年轻,RFC文档相比HTTP的发布时间也很短,它的诞生是为了创建一种「双向通信」的协议,来作为HTTP协议的一个替代者.那么首先看一下它和HTTP(或者HTTP的长连接)的区别. 为什么要用 WebSocket 来替代 HTTP 上一篇中提到WebSocket的目的就是解决网络传输中的双向通信的问题,HTTP1.1默认使用持久连接(pers