前置技术:springboot、netty、websocket的基本概念
在WebSocket概念出来之前,如果页面要不停地显示最新的价格,那么必须不停地刷新页面,或者用一段js代码每隔几秒钟发消息询问服务器数据。
而使用WebSocket技术之后,当服务器有了新的数据,会主动通知浏览器。
每创建一个浏览器会话就创建一个WebServer对象。
里面有四个方法:
OnOpen 表示有浏览器链接过来的时候被调用
OnClose 表示浏览器发出关闭请求的时候被调用
OnMessage 表示浏览器发消息的时候被调用
OnError 表示有错误发生,比如网络断开了等等
前端项目代码:整了一个jsp页面模拟客户端
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Insert title here
当前价格为:¥10000
只贴一下Netty的依赖
io.netty
netty-all
4.1.35.Final
项目结构:
springboot启动类是Main这个类,就不做介绍了。
NettyServer
先来看一下Netty核心逻辑,需要添加四个处理器,三个为自带的,一个为自定义处理器,需要自己去实现。
@Slf4j
public class NettyServer {
private NioEventLoopGroup bossGroup = new NioEventLoopGroup(3);
private NioEventLoopGroup workGroup = new NioEventLoopGroup();
private Channel channel;
private MyNettyServerHandler nettyServerHandler;
public NettyServer (MyNettyServerHandler nettyServerHandler){
this.nettyServerHandler=nettyServerHandler;
}
public ChannelFuture start(int port){
try{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workGroup)
//使用哪种通道实现,NioServerSocketChannel为异步的服务器端 TCP Socket 连接。
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
//设置连接队列长度
.option(ChannelOption.SO_BACKLOG,1024)
//添加拦截处理器
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//websocket协议本身是基于http协议的,所以使用http解编码器
pipeline.addLast(new HttpServerCodec());
//post请求方式
pipeline.addLast(new HttpObjectAggregator(65536));
//websocket协议
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(nettyServerHandler);
}
});
ChannelFuture channelFuture = bootstrap.bind().sync();
channel=channelFuture.channel();
log.info("netty 服务开启成功!");
return channelFuture;
}catch (Exception e){
log.info("netty启动错误!{}",e);
}
return null;
}
public void destory(){
if (channel!=null){
channel.close();
}
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
log.info("netty 服务关闭!");
}
}
MyNettyServerHandler 自定义处理器
@Slf4j
@ChannelHandler.Sharable
public class MyNettyServerHandler extends SimpleChannelInboundHandler {
private ChannelGroup channels;
public MyNettyServerHandler(ChannelGroup channels){
this.channels=channels;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("读取客户端消息!");
if (null==msg){
log.info("数据为空");
return;
}
//第一次读请求是http连接而不是数据
if (msg instanceof FullHttpRequest){
FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
log.info("第一次http请求:"+fullHttpRequest);
}
//正常的数据
else if (msg instanceof TextWebSocketFrame){
TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg;
log.info("客户端消息:"+textWebSocketFrame.text());
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端连接成功!");
channels.add(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端断开连接!");
channels.remove(ctx.channel());
}
}
里面引入了一个ChannelGroup对象,这个对象是其实就是它,用于存放所有channel实例的通道组
public class ChannelPool {
//用于存所有的channel实例
public static final ChannelGroup channels=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
到这里算是单独的Netty服务,怎么将Netty服务和springboot绑定到一起的呢,这时就需要CommandLineRunner接口了。
在springboot项目启动后会自动启动netty服务,netty服务的端口号为springboot服务的端口号加50
public class NettyServerApplication implements CommandLineRunner {
@Value("${server.port}")
private int serverPort;
private NettyServer nettyServer;
public NettyServerApplication(NettyServer nettyServer){
this.nettyServer=nettyServer;
}
@Override
public void run(String... args) throws Exception {
//netty服务为springboot项目端口号加50
ChannelFuture channelFuture = nettyServer.start(serverPort + 50);
//添加钩子函数,jvm关闭时一并将注册的服务也关闭
Runtime.getRuntime().addShutdownHook(new Thread(nettyServer::destory));
//对关闭通道事件进行监听
channelFuture.channel().closeFuture().sync();
}
}
最后通过配置类将这些对象注入进来。
@Configuration
public class WebSocketConfig {
@Bean
public NettyServer nettyServer(){
return new NettyServer(myNettyServerHandler());
}
@Bean
public MyNettyServerHandler myNettyServerHandler(){
return new MyNettyServerHandler(ChannelPool.channels);
}
@Bean
public NettyServerApplication nettyServerApplication(){
return new NettyServerApplication(nettyServer());
}
}
加一个测试入口
@RestController
@RequestMapping("/send")
public class sendController {
@GetMapping("/ws")
public void ws() throws Exception{
while (true){
TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(String.valueOf(new Random().nextInt(10000)));
ChannelPool.channels.writeAndFlush(textWebSocketFrame);
Thread.sleep(5000);
}
}
}
启动服务器端应用(后端项目)和客户端应用(前端项目)。
访问客户端地址,如我的为http://localhost:8081/websocket.jsp
接着访问服务端接口,我的为http://localhost:8082/send/ws
会发现客户端的金额每五秒钟刷新一次,并且发现客户端并没有主动发起请求。
我们再来打开一个新的页面去访问客户端地址,发现这两个客户端页面会同时刷新,到这里整个项目就结束了。