Netty笔记:使用WebSocket协议开发聊天系统
2016-10-17 16:51
549 查看
前言,之前一直围绕着Http协议来开发项目,最近由于参与一个类似竞拍项目的开发,有这样一个场景,多个客户端竞拍一个商品,当一个客户端加价后,其它关注这个商品的客户端需要立即知道该商品的最新价格。
这里有个问题,Http协议是基于请求/响应的,客户端发送请求,然后服务端响应返回,客户端是主动方,服务端被动的接收客户端的请求来响应,无法解决上述场景中服务端主动将最新的数据推送给客户端的需求。
当然,有人会提出ajax轮询的方案,就是客户端不断的请求(假如1秒1次)最新竞拍价格。显然这种模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,但是Http request的Header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。
还有一种比较新颖的方案,long poll(长轮询)。利用长轮询,客户端可以打开指向服务端的Http连接,而服务器会一直保持连接打开,直到服务端数据更新再发送响应。虽然这种方式比ajax轮询有进步,但都存在一个共同问题:由于Http协议的开销,导致它们不适合用于低延迟应用。
转载请注明出处:http://blog.csdn.net/a906998248/article/details/52839425
一.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 握手请求。如下:
这里有个问题,Http协议是基于请求/响应的,客户端发送请求,然后服务端响应返回,客户端是主动方,服务端被动的接收客户端的请求来响应,无法解决上述场景中服务端主动将最新的数据推送给客户端的需求。
当然,有人会提出ajax轮询的方案,就是客户端不断的请求(假如1秒1次)最新竞拍价格。显然这种模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,但是Http request的Header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。
还有一种比较新颖的方案,long poll(长轮询)。利用长轮询,客户端可以打开指向服务端的Http连接,而服务器会一直保持连接打开,直到服务端数据更新再发送响应。虽然这种方式比ajax轮询有进步,但都存在一个共同问题:由于Http协议的开销,导致它们不适合用于低延迟应用。
转载请注明出处:http://blog.csdn.net/a906998248/article/details/52839425
一.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 握手请求。如下:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: sdewgzgfewfsgergzgewrfaf== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com[/code] 其中,Sec-WebSocket-Key是随机的,服务端会使用它加密后作为Sec-WebSocket-Accept的值返回;Sec-WebSocket-Protocol是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议;Sec-WebSocket-Version是告诉服务器所使用的Websocket Draft(协议版本)
不出意外,服务端会返回下列信息表示握手成功,连接已经建立:HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: sdgdfshgretghsdfgergtbd= Sec-WebSocket-Protocol: chat
到这里 WebSocket 连接已经成功建立,服务端和客户端可以正常通信了,此时服务端和客户端都是对等端点,都可以主动发送请求到另一端。
下面是前端和后端的实现过程,后端我采用了 Netty 的 API,因为最近在学 Netty,所以就采用了 Netty 中的 NIO 来构建 WebSocket 后端,我看了下网上也有用 Tomcat API 来实现,看起来也很简单,朋友们可以试试。前端使用HTML5 来构建,可以参考WebSocket接口文档,非常方便简单。
Lanucher用来启动WebSocket服务端import com.company.server.WebSocketServer; public class Lanucher { public static void main(String[] args) throws Exception { // 启动WebSocket new WebSocketServer().run(WebSocketServer.WEBSOCKET_PORT); } }
使用 Netty 构建的 WebSocket 服务import org.apache.log4j.Logger; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.stream.ChunkedWriteHandler; /** * WebSocket服务 * */ public class WebSocketServer { private static final Logger LOG = Logger.getLogger(WebSocketServer.class); // websocket端口 public static final int WEBSOCKET_PORT = 9090; public void run(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel channel) throws Exception { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码 pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装 pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持 pipeline.addLast("handler", new BananaWebSocketServerHandler()); // WebSocket服务端Handler } }); Channel channel = b.bind(port).sync().channel(); LOG.info("WebSocket 已经启动,端口:" + port + "."); channel.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
WebSocket 服务端处理类,注意第一次握手是 Http 协议import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.util.CharsetUtil; import org.apache.log4j.Logger; import com.company.serviceimpl.BananaService; import com.company.util.CODE; import com.company.util.Request; import com.company.util.Response; import com.google.common.base.Strings; import com.google.gson.JsonSyntaxException; /** * WebSocket服务端Handler * */ public class BananaWebSocketServerHandler extends SimpleChannelInboundHandler<Object> { private static final Logger LOG = Logger.getLogger(BananaWebSocketServerHandler.class.getName()); private WebSocketServerHandshaker handshaker; private ChannelHandlerContext ctx; private String sessionId; @Override public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { // 传统的HTTP接入 handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) { // WebSocket接入 handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { LOG.error("WebSocket异常", cause); ctx.close(); LOG.info(sessionId + " 注销"); BananaService.logout(sessionId); // 注销 BananaService.notifyDownline(sessionId); // 通知有人下线 } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { LOG.info("WebSocket关闭"); super.close(ctx, promise); LOG.info(sessionId + " 注销"); BananaService.logout(sessionId); // 注销 BananaService.notifyDownline(sessionId); // 通知有人下线 } /** * 处理Http请求,完成WebSocket握手<br/> * 注意:WebSocket连接第一次请求使用的是Http * @param ctx * @param request * @throws Exception */ private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { // 如果HTTP解码失败,返回HHTP异常 if (!request.getDecoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) { sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // 正常WebSocket的Http连接请求,构造握手响应返回 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + request.headers().get(HttpHeaders.Names.HOST), null, false); handshaker = wsFactory.newHandshaker(request); if (handshaker == null) { // 无法处理的websocket版本 WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel()); } else { // 向客户端发送websocket握手,完成握手 handshaker.handshake(ctx.channel(), request); // 记录管道处理上下文,便于服务器推送数据到客户端 this.ctx = ctx; } } /** * 处理Socket请求 * @param ctx * @param frame * @throws Exception */ private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { // 判断是否是关闭链路的指令 if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 判断是否是Ping消息 if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return; } // 当前只支持文本消息,不支持二进制消息 if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException("当前只支持文本消息,不支持二进制消息"); } // 处理来自客户端的WebSocket请求 try { Request request = Request.create(((TextWebSocketFrame)frame).text()); Response response = new Response(); response.setServiceId(request.getServiceId()); if (CODE.online.code.intValue() == request.getServiceId()) { // 客户端注册 String requestId = request.getRequestId(); if (Strings.isNullOrEmpty(requestId)) { response.setIsSucc(false).setMessage("requestId不能为空"); return; } else if (Strings.isNullOrEmpty(request.getName())) { response.setIsSucc(false).setMessage("name不能为空"); return; } else if (BananaService.bananaWatchMap.containsKey(requestId)) { response.setIsSucc(false).setMessage("您已经注册了,不能重复注册"); return; } if (!BananaService.register(requestId, new BananaService(ctx, request.getName()))) { response.setIsSucc(false).setMessage("注册失败"); } else { response.setIsSucc(true).setMessage("注册成功"); BananaService.bananaWatchMap.forEach((reqId, callBack) -> { response.getHadOnline().put(reqId, ((BananaService)callBack).getName()); // 将已经上线的人员返回 if (!reqId.equals(requestId)) { Request serviceRequest = new Request(); serviceRequest.setServiceId(CODE.online.code); serviceRequest.setRequestId(requestId); serviceRequest.setName(request.getName()); try { callBack.send(serviceRequest); // 通知有人上线 } catch (Exception e) { LOG.warn("回调发送消息给客户端异常", e); } } }); } sendWebSocket(response.toJson()); this.sessionId = requestId; // 记录会话id,当页面刷新或浏览器关闭时,注销掉此链路 } else if (CODE.send_message.code.intValue() == request.getServiceId()) { // 客户端发送消息到聊天群 String requestId = request.getRequestId(); if (Strings.isNullOrEmpty(requestId)) { response.setIsSucc(false).setMessage("requestId不能为空"); } else if (Strings.isNullOrEmpty(request.getName())) { response.setIsSucc(false).setMessage("name不能为空"); } else if (Strings.isNullOrEmpty(request.getMessage())) { response.setIsSucc(false).setMessage("message不能为空"); } else { response.setIsSucc(true).setMessage("发送消息成功"); BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 将消息发送到所有机器 Request serviceRequest = new Request(); serviceRequest.setServiceId(CODE.receive_message.code); serviceRequest.setRequestId(requestId); serviceRequest.setName(request.getName()); serviceRequest.setMessage(request.getMessage()); try { callBack.send(serviceRequest); } catch (Exception e) { LOG.warn("回调发送消息给客户端异常", e); } }); } sendWebSocket(response.toJson()); } else if (CODE.downline.code.intValue() == request.getServiceId()) { // 客户端下线 String requestId = request.getRequestId(); if (Strings.isNullOrEmpty(requestId)) { sendWebSocket(response.setIsSucc(false).setMessage("requestId不能为空").toJson()); } else { BananaService.logout(requestId); response.setIsSucc(true).setMessage("下线成功"); BananaService.notifyDownline(requestId); // 通知有人下线 sendWebSocket(response.toJson()); } } else { sendWebSocket(response.setIsSucc(false).setMessage("未知请求").toJson()); } } catch (JsonSyntaxException e1) { LOG.warn("Json解析异常", e1); } catch (Exception e2) { LOG.error("处理Socket请求异常", e2); } } /** * Http返回 * @param ctx * @param request * @param response */ private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { // 返回应答给客户端 if (response.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8); response.content().writeBytes(buf); buf.release(); HttpHeaders.setContentLength(response, response.content().readableBytes()); } // 如果是非Keep-Alive,关闭连接 ChannelFuture f = ctx.channel().writeAndFlush(response); if (!HttpHeaders.isKeepAlive(request) || response.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } /** * WebSocket返回 * @param ctx * @param req * @param res */ public void sendWebSocket(String msg) throws Exception { if (this.handshaker == null || this.ctx == null || this.ctx.isRemoved()) { throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息"); } this.ctx.channel().write(new TextWebSocketFrame(msg)); this.ctx.flush(); } }
聊天服务接口和实现类import com.company.util.Request; public interface BananaCallBack { // 服务端发送消息给客户端 void send(Request request) throws Exception; }import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.log4j.Logger; import com.company.service.BananaCallBack; import com.company.util.CODE; import com.company.util.Request; import com.google.common.base.Strings; public class BananaService implements BananaCallBack { private static final Logger LOG = Logger.getLogger(BananaService.class); public static final Map<String, BananaCallBack> bananaWatchMap = new ConcurrentHashMap<String, BananaCallBack>(); // <requestId, callBack> private ChannelHandlerContext ctx; private String name; public BananaService(ChannelHandlerContext ctx, String name) { this.ctx = ctx; this.name = name; } public static boolean register(String requestId, BananaCallBack callBack) { if (Strings.isNullOrEmpty(requestId) || bananaWatchMap.containsKey(requestId)) { return false; } bananaWatchMap.put(requestId, callBack); return true; } public static boolean logout(String requestId) { if (Strings.isNullOrEmpty(requestId) || !bananaWatchMap.containsKey(requestId)) { return false; } bananaWatchMap.remove(requestId); return true; } @Override public void send(Request request) throws Exception { if (this.ctx == null || this.ctx.isRemoved()) { throw new Exception("尚未握手成功,无法向客户端发送WebSocket消息"); } this.ctx.channel().write(new TextWebSocketFrame(request.toJson())); this.ctx.flush(); } /** * 通知所有机器有机器下线 * @param requestId */ public static void notifyDownline(String requestId) { BananaService.bananaWatchMap.forEach((reqId, callBack) -> { // 通知有人下线 Request serviceRequest = new Request(); serviceRequest.setServiceId(CODE.downline.code); serviceRequest.setRequestId(requestId); try { callBack.send(serviceRequest); } catch (Exception e) { LOG.warn("回调发送消息给客户端异常", e); } }); } public String getName() { return name; } }
前端html5聊天页面及js<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Netty WebSocket 聊天实例</title> </head> <script src="jquery.min.js" type="text/javascript"></script> <script src="map.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function() { var uuid = guid(); // uuid在一个会话唯一 var nameOnline = ''; // 上线姓名 var onlineName = new Map(); // 已上线人员, <requestId, name> $("#name").attr("disabled","disabled"); $("#onlineBtn").attr("disabled","disabled"); $("#downlineBtn").attr("disabled","disabled"); $("#banana").hide(); // 初始化websocket var socket; if (!window.WebSocket) { window.WebSocket = window.MozWebSocket; } if (window.WebSocket) { socket = new WebSocket("ws://localhost:9090/"); socket.onmessage = function(event) { console.log("收到服务器消息:" + event.data); if (event.data.indexOf("isSucc") != -1) {// 这里需要判断是客户端请求服务端返回后的消息(response) var response = JSON.parse(event.data); if (response != undefined && response != null) { if (response.serviceId == 1001) { // 上线 if (response.isSucc) { // 上线成功,初始化已上线人员 onlineName.clear(); $("#showOnlineNames").empty(); for (var reqId in response.hadOnline) { onlineName.put(reqId, response.hadOnline[reqId]); } initOnline(); $("#name").attr("disabled","disabled"); $("#onlineBtn").attr("disabled","disabled"); $("#downlineBtn").removeAttr("disabled"); $("#banana").show(); } else { alert("上线失败"); } } else if (response.serviceId == 1004) { if (response.isSucc) { onlineName.clear(); $("#showBanana").empty(); $("#showOnlineNames").empty(); $("#name").removeAttr("disabled"); $("#onlineBtn").removeAttr("disabled"); $("#downlineBtn").attr("disabled","disabled"); $("#banana").hide(); } else { alert("下线失败"); } } } } else {// 还是服务端向客户端的请求(request) var request = JSON.parse(event.data); if (request != undefined && request != null) { if (request.serviceId == 1001 || request.serviceId == 1004) { // 有人上线/下线 if (request.serviceId == 1001) { onlineName.put(request.requestId, request.name); } if (request.serviceId == 1004) { onlineName.removeByKey(request.requestId); } initOnline(); } else if (request.serviceId == 1003) { // 有人发消息 appendBanana(request.name, request.message); } } } }; socket.onopen = function(event) { $("#name").removeAttr("disabled"); $("#onlineBtn").removeAttr("disabled"); console.log("已连接服务器"); }; socket.onclose = function(event) { // WebSocket 关闭 console.log("WebSocket已经关闭!"); }; socket.onerror = function(event) { console.log("WebSocket异常!"); }; } else { alert("抱歉,您的浏览器不支持WebSocket协议!"); } // WebSocket发送请求 function send(message) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { socket.send(message); } else { console.log("WebSocket连接没有建立成功!"); alert("您还未连接上服务器,请刷新页面重试"); } } // 刷新上线人员 function initOnline() { $("#showOnlineNames").empty(); for (var i=0;i<onlineName.size();i++) { $("#showOnlineNames").append('<tr><td>' + (i+1) + '</td>' + '<td>' + onlineName.element(i).value + '</td>' + '</tr>'); } } // 追加聊天信息 function appendBanana(name, message) { $("#showBanana").append('<tr><td>' + name + ': ' + message + '</td></tr>'); } $("#onlineBtn").bind("click", function() { var name = $("#name").val(); if (name == null || name == '') { alert("请输入您的尊姓大名"); return; } nameOnline = name; // 上线 send(JSON.stringify({"requestId":uuid, "serviceId":1001, "name":name})); }); $("#downlineBtn").bind("click", function() { // 下线 send(JSON.stringify({"requestId":uuid, "serviceId":1004})); }); $("#sendBtn").bind("click", function() { var message = $("#messageInput").val(); if (message == null || message == '') { alert("请输入您的聊天信息"); return; } // 发送聊天消息 send(JSON.stringify({"requestId":uuid, "serviceId":1002, "name":nameOnline, "message":message})); $("#messageInput").val(""); }); }); function guid() { function S4() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); } return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); } </script> <body> <h1>Netty WebSocket 聊天实例</h1> <input type="text" id="name" value="佚名" placeholder="姓名" /> <input type="button" id="onlineBtn" value="上线" /> <input type="button" id="downlineBtn" value="下线" /> <hr/> <table id="banana" border="1" > <tr> <td width="600" align="center">聊天</td> <td width="100" align="center">上线人员</td> </tr> <tr height="200" valign="top"> <td> <table id="showBanana" border="0" width="600"> <!-- <tr> <td>张三: 大家好</td> </tr> <tr> <td>李四: 欢迎加入群聊</td> </tr> --> </table> </td> <td> <table id="showOnlineNames" border="0"> <!-- <tr> <td>1</td> <td>张三</td> <tr/> <tr> <td>2</td> <td>李四</td> <tr/> --> </table> </td> </tr> <tr height="40"> <td></td> <td></td> </tr> <tr> <td> <input type="text" id="messageInput" style="width:590px" placeholder="巴拉巴拉点什么吧" /> </td> <td> <input type="button" id="sendBtn" value="发送" /> </td> </tr> </table> </body> </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 教程
相关文章推荐
- Netty笔记:使用WebSocket协议开发聊天系统
- 使用smack API开发聊天系统
- swift开发笔记28 使用系统自带地图
- 【开发技术】 使用JSP开发WEB应用系统-------笔记
- netty高级篇-Websocket协议开发
- (高级篇 Netty多协议开发和应用)第十一章-WebSocket协议开发
- 使用netty开发私有栈协议
- Netty使用websocket协议实现汽车行驶轨迹追踪demo
- WP7应用开发笔记-豆知识 使用本地值的时候应该多考虑使用系统主题资源
- iOS开发笔记之三十一——日历NSCaledar使用过程中遇到的一个苹果系统bug
- 基于(ssm,websocket,mysql)开发的web聊天系统
- Netty之WebSocket协议开发
- 使用WebSocket开发简单的聊天
- 关于李三影【Unity 游戏开发教程】装备系统 - 01. JSON数据创建与使用要做笔记的地方
- web开发-Windows系统下使用git for Windows软件-学习笔记六
- Netty之WebSocket协议开发
- IOS开发笔记之十四——使用系统相册或相机导致状态栏隐藏的问题(bug总结四)
- 基于udp通信协议开发的简易聊天系统1.0
- Netty学习——基于Netty的WebSocket协议开发
- 使用smack API开发聊天系统