前言
实时通信的分类:
(1)Ajax轮训
通过JS以Ajax异步地让浏览器每隔一段时间(10S)发送请求到后端,去询问服务端是否有新消息、新状态等,如果有则取出并通过前端再渲染。但这很容易造成无限循环,也就是前端Ajax会不停地循环后端的数据 (使用场景:浏览器不需要一直刷新,简单的后台管理系统中的数据更新等)(2)Long Pull
与Ajax轮训类似,也是使用异步请求,只不过它的轮训方式不太友好,阻塞式轮训:当客户端发起请求之后,服务端如果未响应,则Long Pull就不会有响应,直到服务端返回response。过程中不停地建立Http请求,等待服务器端进行处理,被动响应,缺点也是非常明显,也很耗费资源,性能低。(3)webSokect - 推荐
Http本身就不支持长连接,Http1.1支持长连接,WebSokect就是使用了Http1.1协议来完成一小部分的握手,简单来讲就是,客户端发起请求到服务端,服务端会去找一个副助理,找到之后服务器端会和客户端一直保持连接,为客户端进行服务,并且可以主动推送一些消息给客户端。
关于WebSocket
WebSokect有哪些协议,又有什么优点?
1)首先WebSokect相对于Http这种非持久化来讲,是一种持久化的协议,Http的生命周期可以说是通过一个request来进行判定,有一个request请求到后端,后端也会相应的返回一个response给客户端,或者有多个request对应到多个response,两者之间都是一一对应的,有多少个request请求就会有多少个response相应,不会有偏差。此时response其实也是被动的,它不能由服务器端主动发起相应,必须先有request请求。
2)WebSokect由此诞生,它使得资源不会像以前一样浪费,并且它也是非常的主动,只要链接一旦被建立完毕之后,那么服务端就可以不停的主动推送消息给客户端,客户端不需要主动请求服务端也可以达到一样的效果。 也就是说,只要建立一次Http请求就能达到信息的源源不断的传输。类似于在线Online小游戏,一开始建立连接,就可以一直保持在线了。
WebSocket API(最基础也是最常用的几个)
(1)var socket=new WebSocket("ws://[ip]:[port]");
(2)生命周期:onopen() onmessage() onerror() onclose()
(3)主动方法:Socket.send() Socket.close()
参考API:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
导入相关依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pubing</groupId>
<artifactId>helloNetty</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
</dependencies>
</project>
创建服务器启动类 WebSocketServer
package com.phubing.websokect;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* 用于和客户端进行连接
*
* @author phubing
*
*/
public class WebSocketServer {
public static void main(String[] args) throws InterruptedException {
//定义线程组
EventLoopGroup mainGroup = new NioEventLoopGroup();
EventLoopGroup subGroup = new NioEventLoopGroup();
try {
ServerBootstrap server = new ServerBootstrap();
server.group(mainGroup, subGroup)
//channel类型
.channel(NioServerSocketChannel.class)
//针对subGroup做的子处理器,childHandler针对WebSokect的初始化器
.childHandler(new WebSocketinitializer());
//绑定端口并以同步方式进行使用
ChannelFuture channelFuture = server.bind(10086).sync();
//针对channelFuture,进行相应的监听
channelFuture.channel().closeFuture().sync();
}finally {
//针对两个group进行优雅地关闭
mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
}
}
}
创建WebSocket初始化器WebSocketinitializer
package com.phubing.websokect;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebSocketinitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//从Channel中获取对应的pipeline
ChannelPipeline channelPipeline = socketChannel.pipeline();
//添加相应的助手类与处理器
/**
* WebSokect基于Http,所以要有相应的Http编解码器,HttpServerCodec()
*/
channelPipeline.addLast(new HttpServerCodec());
//在Http中有一些数据流的传输,那么数据流有大有小,如果说有一些相应的大数据流处理的话,需要在此添加
//ChunkedWriteHandler:为一些大数据流添加支持
channelPipeline.addLast(new ChunkedWriteHandler());
//UdineHttpMessage进行处理,也就是会用到request以及response
//HttpObjectAggregator:聚合器,聚合了FullHTTPRequest、FullHTTPResponse。。。,当你不想去管一些HttpMessage的时候,直接把这个handler丢到管道中,让Netty自行处理即可
channelPipeline.addLast(new HttpObjectAggregator(2048*64));
//================华丽的分割线:以上是用于支持Http协议================
//================华丽的分割线:以下是用于支持WebSoket==================
// /ws:一开始建立连接的时候会使用到,可自定义
//WebSocketServerProtocolHandler:给客户端指定访问的路由(/ws),是服务器端处理的协议,当前的处理器处理一些繁重的复杂的东西,运行在一个WebSocket服务端
//另外也会管理一些握手的动作:handshaking(close,ping,pong) ping + pong = 心跳,对于WebSocket来讲,是以frames进行传输的,不同的数据类型对应的frames也不同
channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//添加自动handler,读取客户端消息并进行处理,处理完毕之后将相应信息传输给对应客户端
channelPipeline.addLast(new ChatHandler());
}
}
添加自定义助手ChatHandler
package com.phubing.websokect;
import java.time.LocalDate;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
//TextWebSocketFrame:处理消息的handler,在Netty中用于处理文本的对象,frames是消息的载体
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
//用于记录和管理所有客户端的channel,可以把相应的channel保存到一整个组中
//DefaultChannelGroup:用于对应ChannelGroup,进行初始化
private static ChannelGroup channelClient = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//text()获取从客户端发送过来的字符串
String content = msg.text();
System.out.println("客户端传输的数据:"+content);
//针对channel进行发送,客户端对应的是channel
/**
* 方式一
*/
for (Channel channel : channelClient) {
//循环对每一个channel对应输出即可(往缓冲区中写,写完之后再刷到客户端)
//注:writeAndFlush不可以使用String,因为传输的载体是一个TextWebSocketFrame,需要把消息通过载体再刷到客户端
channel.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content));
}
/**
* 方式二
channelClient.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content))
*/
}
//当客户端连接服务端(或者是打开连接之后)
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//获取客户端所对应的channel,添加到一个管理的容器中即可
channelClient.add(ctx.channel());
}
//客户端断开
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//实际上是多余的,只要handler被移除,client会自动的把对应的channel移除掉
channelClient.remove(ctx.channel());
//每一而channel都会有一个长ID与短ID
//一开始channel就有了,系统会自动分配一串很长的字符串作为唯一的ID,如果使用asLongText()获取的ID是唯一的,asShortText()会把当前ID进行精简,精简过后可能会有重复
System.out.println("channel的长ID:"+ctx.channel().id().asLongText());
System.out.println("channel的短ID:"+ctx.channel().id().asShortText());
}
}
此时,服务端已完成,接下来再新建一个前端页面,用于发送文本与显示服务端推送的数据
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Netty+WebSocket案例</title>
</head>
<body>
<div id="">发送消息:</div><br>
<input type="text" name="messageContent" id="messageContent"/>
<input type="button" name="" id="" value="发送" onclick="CHAT.chat()"/>
<hr>
<div id="">接收消息:</div><br>
<div id="receiveNsg" style="background-color: gainsboro;"></div>
<script type="text/javascript">
window.CHAT = {
socket: null,
//初始化
init: function(){
//首先判断浏览器是否支持WebSocket
if (window.WebSocket){
CHAT.socket = new WebSocket("ws://localhost:10086/ws");
CHAT.socket.onopen = function(){
console.log("客户端与服务端建立连接成功");
},
CHAT.socket.onmessage = function(e){
console.log("接收到消息:"+e.data);
var receiveNsg = window.document.getElementById("receiveNsg");
var html = receiveNsg.innerHTML;
receiveNsg.innerHTML = html + "<br>" + e.data;
},
CHAT.socket.onerror = function(){
console.log("发生错误");
},
CHAT.socket.onclose = function(){
console.log("客户端与服务端关闭连接成功");
}
}else{
alert("8102年都过了,升级下浏览器吧");
}
},
chat: function(){
var msg = window.document.getElementById("messageContent");
CHAT.socket.send(msg.value);
}
}
CHAT.init();
</script>
</body>
</html>
注
端口号、IP地址、WebSocket的服务端提供名称一定要与前端相对应,否则会出错
例如:服务端给客户端指定访问的路由为:/ws,IP地址为:192.168.45.96,端口号为:10086
那么前端在建立WebSocket连接时填写为:
CHAT.socket = new WebSocket("ws://localhost:10086/ws"); //ws:// 为固定写法
最后来看看效果图
如有不当之处请指出,虚心接受建议与批评。