WebSocket是一种规范,是Html5规范的一部分,websocket解决什么问题呢?解决http协议的一些不足。我们知道,http协议是一种无状态的,基于请求响应模式的协议。
网页聊天的程序(基于http协议的),浏览器客户端发送一个数据,服务器接收到这个浏览器数据之后,如何将数据推送给其他的浏览器客户端呢?
这就涉及到服务器的推技术。早年为了实现这种服务器也可以像浏览器客户端推送消息的长连接需求,有很多方案,比如说最常用的采用一种轮询技术,就是客户端每隔一段时间,比如说2s或者3s向服务器发送请求,去请求服务器端是否还有信息没有响应给客户端,有就响应给客户端,当然没有响应就只是一种无用的请求。
这种长轮询技术的缺点有:
1)响应数据不是实时的,在下一次轮询请求的时候才会得到这个响应信息,只能说是准实时,而不是严格意义的实时。
2)大多数轮询请求的空轮询,造成大量的资源带宽的浪费,每次http请求携带了大量无用的头信息,而服务器端其实大多数都不关注这些头信息,而实际大多数情况下这些头信息都远远大于body信息,造成了资源的消耗。
拓展
比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
websocket的出现就是解决了客户端与服务端的这种长连接问题,这种长连接是真正意义上的长连接。客户端与服务器一旦连接建立双方就是对等的实体,不再区分严格意义的客户端和服务端。长连接只有在初次建立的时候,客户端才会向服务端发送一些请求,这些请求包括请求头和请求体,一旦建立好连接之后,客户端和服务器只会发送数据本身而不需要再去发送请求头信息,这样大量减少了
网络带宽。websocket协议本身是构建在http协议之上的升级协议,客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。websocket协议也可以应用在非浏览器应用,只需要引入相关的websocket库就可以了。
HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket使用ws或wss的统一资源标志符,类似于HTTPS,其中wss表示在TLS之上的Websocket。如:
优点
- 较少的控制开销:相对与http请求的头部信息,websocket信息明显减少。
- 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- 保持连接状态。于HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率
转载 出处 : https://www.jianshu.com/p/9a97e667cf84
消息风暴扩散系数
一个IM系统,假设平均每个用户加了20个群,平均每个群有200个用户,假设20用户在线,那么为了保证群友状态的实时性,每个用户登录,就要将自己的状态改变通知发送给20*20*200=800个群友,N=800,意味着,任何一个状态变化会变成800个推送请求。
IM 系统 的解决方案:
轮询拉取
发送方发送了消息,先入队列
网页端起一个timer,每隔一段时间(例如10秒),发起一个轮询请求,拉取队列里的消息
如果队列里有消息,就返回消息
如果队列里无消息,就10秒后再次轮询
优势是:实现简单,直观且,容易理解
缺点也很明显:
- 实时性差:最坏的情况下,1条消息进入队列后,10s之后才会收到
- 效率低下:发消息是一个低频动作,如果10次轮询才收到1条消息,请求有效性只有10%,浪费了大量服务器资源
建立长连接
websocket
flashsocket
HTTP长轮询
HTTP长轮询的核心在于,浏览器与服务端之间建立了一条“通知连接”,它的特点是:
- 这是一条browser发往web-server的HTTP连接
- 这条连接只用来收取推送通知
- 不像普通的“请求-响应”式HTTP请求,这个HTTP会被服务端夯住,直到有推送通知到达,或者超过约定的时间
发起通知时如果队列
正好有消息,则实时把队列里的消息带回,立马再发起通知连接
否则一直等待,直到触发“时间阀值”,返回无消息,立马再发起通知连接
以上概念摘录于 58沈剑 架构师之路
简单群聊demo
服务类 |
package cn.shendu.netty;
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.SocketChannel; 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;
public class WebSocketServer { 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<SocketChannel>() {
@Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("http-codec", new HttpServerCodec()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); pipeline.addLast("handler", new WebSocketServerHandler()); } });
Channel ch = b.bind(port).sync().channel(); System.out.println("Web socket server started at port " + port + '.'); System.out.println("Open your browser and navigate to http://localhost:" + port + '/');
ch.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
public static void main(String[] args) throws Exception {
new WebSocketServer().run(8765); } } |
消息处理类 |
package cn.shendu.netty;
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; 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.HttpHeaderUtil; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.CharsetUtil;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());
private WebSocketServerHandshaker handshaker; private static ConcurrentHashMap<ChannelHandlerContext,String> concurrentHashMap = new ConcurrentHashMap<>();
private void sendText(ChannelHandlerContext ctx,String msg){ ctx.channel().writeAndFlush(new TextWebSocketFrame(msg)); }
private void sendAllText(String msg){ concurrentHashMap.forEach((k,v)->{ sendText(k,msg); }); }
@Override public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
// 传统的HTTP接入 if (msg instanceof FullHttpRequest) { FullHttpRequest request = (FullHttpRequest)msg; System.out.println(request.uri()); handleHttpRequest(ctx, (FullHttpRequest) msg); } // WebSocket接入 else if (msg instanceof WebSocketFrame) { handleWebSocketFrame(ctx, (WebSocketFrame) msg); } }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); }
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // 如果HTTP解码失败,返回HHTP异常 if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); return; } // 构造握手响应返回,本机测试 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( "ws://localhost:8080/websocket", null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req);
//接收请求 String uri = req.uri(); String username = uri.substring(uri.lastIndexOf("/")+1);
concurrentHashMap.put(ctx,username);
sendAllText(" 我加入了聊天室............."); } }
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
// 判断是否是关闭链路的指令 if (frame instanceof CloseWebSocketFrame) { String user = concurrentHashMap.get(ctx); concurrentHashMap.remove(ctx); sendAllText(user+" 离开了聊天室");
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(String.format("%s frame types not supported", frame.getClass().getName())); }
// 返回应答消息 String request = ((TextWebSocketFrame) frame).text(); if (logger.isLoggable(Level.FINE)) { logger.fine(String.format("%s 服务端收到 %s", ctx.channel(), request)); } String user = concurrentHashMap.get(ctx); sendAllText(user+":"+request+"\n"); }
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // 返回应答给客户端 if (res.status().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); HttpHeaderUtil.setContentLength(res, res.content().readableBytes()); } // 如果是非Keep-Alive,关闭连接 ChannelFuture f = ctx.channel().writeAndFlush(res); if (!HttpHeaderUtil.isKeepAlive(req) || res.status().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } |
html |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8">
<title>聊天室</title> <script src="https://code.jquery.com/jquery-3.2.1.min.js" ></script>
</head>
<body> 聊天消息内容: <br/> <textarea id="text_chat_content" readonly="readonly" cols="100" rows="9">
</textarea>
<br/>
用户:<input id="in_user_name" value=""/> <button id="btn_join">加入聊天室</button> <button id="btn_exit">离开聊天室</button>
<br/>
输入框:<input id="in_msg" value=""/><button id="btn_send">发送消息</button>
<script type="text/javascript"> $(document).ready(function(){
//var urlPrefix ='ws://127.0.0.1:8765/chat-room/'; var urlPrefix ='ws://127.0.0.1:8765/websocket/'; var ws = null;
$('#btn_join').click(function(){
var username = $('#in_user_name').val();
// var url = urlPrefix+username;
var url = urlPrefix+username; ws = new WebSocket(url);
ws.onmessage = function(event){ //服务端发送的消息 $('#text_chat_content').append(event.data+'\n'); }
ws.onclose = function(event){ $('#text_chat_content').append('用户['+username+'] 已经离开聊天室!'); }
});
//客户端发送消息到服务器 $('#btn_send').click(function(){
var msg = $('#in_msg').val();
if(ws){
ws.send(msg); }
});
//离开聊天室 $('#btn_exit').click(function(){
if(ws){ ws.close(); }
});
}) </script>
</body>
</html> |
maven 依赖 |
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>5.0.0.Alpha2</version> </dependency> |