在讲功能实现之前,我们先来捋一下底层的原理,后面附上工程结构及代码
NIO主要包含三大核心部分:缓冲区(Buffer)、通道(Channel)、选择器(Selector)。
下图是NIO通信的结构图。
NIO之所以是NIO,背后有Selector
Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
一个用于特定基本数据类 型的容器。由 java.nio 包定义的,所有缓冲区 都是 Buffer 抽象类的子类.。Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中。
这部分内容较为枯燥,运用netty框架后直接调API,可以暂时跳过
Buffer 中的重要概念:
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark
使用Buffer读写数据一般遵循以下四个步骤:
通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
1、 NIO 的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲:
2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)
是双向的,可以读操作,也可以写操作。
3、Channel 在 NIO 中是一个接口
int read(ByteBuffer dst) 从 Channel 到 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) 将 Channel 到 中的数据“分散”到 ByteBuffer[]
int write(ByteBuffer src) 将 ByteBuffer 到 中的数据写入到 Channel
long write(ByteBuffer[] srcs) 将 ByteBuffer[] 到 中的数据“聚集”到 Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
NIO精华(必看)
看完上面的NIO基本概述,那么你已经对NIO的通信框架有一定的了解了,下面介绍一下Netty。
Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。摘自百度百科
参考blog:Netty入门教程——认识Netty
websocket实现服务器中消息极速推送到前端。
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。摘自百度百科
Quartz介绍及入门案例
上面这个是之前用SSM框架实现的quartz定时任务demo,大家感兴趣可以了解一下,本次在SpringBoot项目中用了轻量级的定时框架SpringTask。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.32.Finalversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.51version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>RELEASEversion>
<scope>compilescope>
dependency>
dependencies>
server:
port: 8888
spring:
application:
name: message-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
# redis:
# host: 127.0.0.1
main:
allow-bean-definition-overriding: true
mybatis-plus:
type-aliases-package: com.heima.message.pojo
configuration:
map-underscore-to-camel-case: true
# mapper-locations: mappers/*.xml
logging:
level:
com.heima: debug
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class CfMessageApplication {
public static void main(String[] args) {
SpringApplication.run(CfMessageApplication.class,args);
}
}
package com.heima.message.controller;
import com.heima.message.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/message")
public class MessageController {
@Autowired
private MessageService messageService;
/**
* 推送给所有用户
* @param msg
*/
@PostMapping("/pushAll")
public void pushToAll(@RequestParam("msg") String msg){
messageService.pushMsgToAll(msg);
}
/**
* 推送给指定用户
* @param userId
* @param msg
*/
@PostMapping("/pushOne")
public void pushMsgToOne(@RequestParam("userId") String userId,@RequestParam("msg") String msg){
messageService.pushMsgToOne(userId,msg);
}
/**
* 一对多聊天
*/
@PostMapping("/chatAll")
public void chatToAll(@RequestParam("senderId")String senderId,@RequestParam("msg") String msg){
messageService.chatToAll(senderId,msg);
}
/**
* 一对一聊天
*/
@PostMapping("/chatOne")
public void chatToOne(@RequestParam("senderId")String senderId,
@RequestParam("receiverId") String receiverId,
@RequestParam("msg") String msg
){
messageService.chatToOne(senderId,receiverId,msg);
}
}
package com.heima.message.service;
public interface MessageService {
/**
* 推送给指定用户
*
* @param userId
* @param msg
*/
void pushMsgToOne(String userId, String msg);
/**
* 推送给所有用户
*
* @param msg
*/
void pushMsgToAll(String msg);
/**
* 一对多聊天
* @param senderId
* @param msg
*/
void chatToAll(String senderId, String msg);
/**
* 一对一聊天
* @param senderId
* @param receiverId
* @param msg
*/
void chatToOne(String senderId, String receiverId, String msg);
}
package com.heima.message.service.impl;
import com.heima.message.nettywebsocket.NettyConfig;
import com.heima.message.service.MessageService;
import org.springframework.stereotype.Service;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class MessageServiceImpl implements MessageService {
@Override
public void pushMsgToOne(String userId, String msg){
ConcurrentHashMap<String, Channel> userChannelMap = NettyConfig.getUserChannelMap();
Channel channel = userChannelMap.get(userId);
channel.writeAndFlush(new TextWebSocketFrame(msg));
}
@Override
public void pushMsgToAll(String msg){
NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
}
@Override
public void chatToAll(String senderId, String msg) {
msg = senderId+"对所有人说: "+msg;
NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
}
@Override
public void chatToOne(String senderId, String receiverId, String msg) {
msg = senderId+"对"+receiverId+"说: "+msg;
ConcurrentHashMap<String, Channel> userChannelMap = NettyConfig.getUserChannelMap();
Channel channel = userChannelMap.get(receiverId);
channel.writeAndFlush(new TextWebSocketFrame(msg));
}
}
package com.heima.message.nettywebsocket;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.util.concurrent.ConcurrentHashMap;
public class NettyConfig {
/**
* 定义一个channel组,管理所有的channel
* GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
*/
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 存放用户与Chanel的对应信息,用于给指定用户发送消息
*/
private static ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap<>();
private NettyConfig() {
}
/**
* 获取channel组
*
* @return
*/
public static ChannelGroup getChannelGroup() {
return channelGroup;
}
/**
* 获取用户channel map
*
* @return
*/
public static ConcurrentHashMap<String, Channel> getUserChannelMap() {
return userChannelMap;
}
}
package com.heima.message.nettywebsocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
@Component
public class NettyServer {
private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
/**
* webSocket协议名
*/
private static final String WEBSOCKET_PROTOCOL = "WebSocket";
/**
* 端口号
*/
@Value("${webSocket.netty.port:58080}")
private int port;
/**
* webSocket路径
*/
@Value("${webSocket.netty.path:/webSocket}")
private String webSocketPath;
@Autowired
private WebSocketHandler webSocketHandler;
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
/**
* 启动
*
* @throws InterruptedException
*/
private void start() throws InterruptedException {
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
// bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
bootstrap.group(bossGroup, workGroup);
// 设置NIO类型的channel
bootstrap.channel(NioServerSocketChannel.class);
// 设置监听端口
bootstrap.localAddress(new InetSocketAddress(port));
// 连接到达时会创建一个通道
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 流水线管理通道中的处理程序(Handler),用来处理业务
// webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ObjectEncoder());
// 以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
/*
说明:
1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
*/
ch.pipeline().addLast(new HttpObjectAggregator(8192));
/*
说明:
1、对应webSocket,它的数据是以帧(frame)的形式传递
2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
3、核心功能是将http协议升级为ws协议,保持长连接
*/
ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
// 自定义的handler,处理业务逻辑
ch.pipeline().addLast(webSocketHandler);
}
});
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}
/**
* 释放资源
*
* @throws InterruptedException
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workGroup != null) {
workGroup.shutdownGracefully().sync();
}
}
@PostConstruct()
public void init() {
//需要开启一个新的线程来执行netty server 服务器
new Thread(() -> {
try {
start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
package com.heima.message.nettywebsocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
@Component
public class NettyServer {
private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
/**
* webSocket协议名
*/
private static final String WEBSOCKET_PROTOCOL = "WebSocket";
/**
* 端口号
*/
@Value("${webSocket.netty.port:58080}")
private int port;
/**
* webSocket路径
*/
@Value("${webSocket.netty.path:/webSocket}")
private String webSocketPath;
@Autowired
private WebSocketHandler webSocketHandler;
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
/**
* 启动
*
* @throws InterruptedException
*/
private void start() throws InterruptedException {
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
// bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
bootstrap.group(bossGroup, workGroup);
// 设置NIO类型的channel
bootstrap.channel(NioServerSocketChannel.class);
// 设置监听端口
bootstrap.localAddress(new InetSocketAddress(port));
// 连接到达时会创建一个通道
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 流水线管理通道中的处理程序(Handler),用来处理业务
// webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ObjectEncoder());
// 以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
/*
说明:
1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
*/
ch.pipeline().addLast(new HttpObjectAggregator(8192));
/*
说明:
1、对应webSocket,它的数据是以帧(frame)的形式传递
2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
3、核心功能是将http协议升级为ws协议,保持长连接
*/
ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
// 自定义的handler,处理业务逻辑
ch.pipeline().addLast(webSocketHandler);
}
});
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}
/**
* 释放资源
*
* @throws InterruptedException
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workGroup != null) {
workGroup.shutdownGracefully().sync();
}
}
@PostConstruct()
public void init() {
//需要开启一个新的线程来执行netty server 服务器
new Thread(() -> {
try {
start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
package com.heima.message.nettywebsocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
@Component
public class NettyServer {
private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
/**
* webSocket协议名
*/
private static final String WEBSOCKET_PROTOCOL = "WebSocket";
/**
* 端口号
*/
@Value("${webSocket.netty.port:58080}")
private int port;
/**
* webSocket路径
*/
@Value("${webSocket.netty.path:/webSocket}")
private String webSocketPath;
@Autowired
private WebSocketHandler webSocketHandler;
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
/**
* 启动
*
* @throws InterruptedException
*/
private void start() throws InterruptedException {
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
// bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
bootstrap.group(bossGroup, workGroup);
// 设置NIO类型的channel
bootstrap.channel(NioServerSocketChannel.class);
// 设置监听端口
bootstrap.localAddress(new InetSocketAddress(port));
// 连接到达时会创建一个通道
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 流水线管理通道中的处理程序(Handler),用来处理业务
// webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ObjectEncoder());
// 以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
/*
说明:
1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
*/
ch.pipeline().addLast(new HttpObjectAggregator(8192));
/*
说明:
1、对应webSocket,它的数据是以帧(frame)的形式传递
2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
3、核心功能是将http协议升级为ws协议,保持长连接
*/
ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
// 自定义的handler,处理业务逻辑
ch.pipeline().addLast(webSocketHandler);
}
});
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}
/**
* 释放资源
*
* @throws InterruptedException
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workGroup != null) {
workGroup.shutdownGracefully().sync();
}
}
@PostConstruct()
public void init() {
//需要开启一个新的线程来执行netty server 服务器
new Thread(() -> {
try {
start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cheung的账号title>
<link rel="stylesheet" href="./index.css">
head>
<body>
<div class="websocket">
<div class="receive">
<p>消息列表p>
<div id="receive-box">div>
div>
<div class="send">
<textarea type="text" id="msg-need-send">textarea>
<p>
<select>
Memorial Day is 553 days |