转载请表明出处 https://blog.csdn.net/Amor_Leo/article/details/107089890 谢谢
简介
Netty 是一个基于NIO(Nonblocking I/O,非阻塞IO)的客户、服务器端的异步事件驱动的网络应用程序框架。
pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.50.Final</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-android</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
yml
server:
port: 8080
webSocket:
netty:
port: 8000
path: /ws
实体类
/**
* @author LHL
*/
@Data
public class MessageRequest {
/**
* 多个handle 通过编写路由handler 通过该属性判断是哪一个handler执行
* */
private String channel;
/**
* 消息时间
*/
protected String dateTime;
/**
* 发送者id
*/
private String senderId;
/**
* 消息接收方id
*/
private String receiverId;
/**
* 发送方头像
*/
private String sendPortrait;
/**
* 接收方头像
*/
private String receiverPortrait;
/**
* 消息接收方昵称
*/
private String nickName;
/**
* 发送者昵称
*/
private String senderName;
/**
* 发送时间
* */
private String sendTime;
/**
* 消息id,用于撤回
*/
private String msgId;
/**
* 消息实体json字符串
*/
private String msgEntity;
}
/**
* @author LHL
*/
@Data
public class MessageResponse {
/**
* 管道类型
*/
private String channel;
/**
* 消息id
*/
private String msgId;
/**
* 发送者Id
*/
private String senderId;
/**
* 接受者id
*/
private String receiverId;
/**
* 发送者对象
*/
private Object sender;
/**
* 接受者对象
*/
private Object receiver;
/**
* 响应消息
*/
private String msgEntity;
/**
* 消息时间
*/
private String dateTime;
/**
* 200成功
*/
private Integer code;
/**
* 0不是回执,1是回执
*/
private Integer isReceipt;
public MessageResponse(){}
public MessageResponse(MessageRequest messageRequest){
this.isReceipt = 1;
this.code = 500;
this.channel = messageRequest.getChannel();
this.senderId = messageRequest.getSenderId();
this.receiverId = messageRequest.getReceiverId();
this.msgEntity = messageRequest.getMsgEntity();
}
}
整合
config
/**
* @author LHL
*/
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;
}
}
netty server
/**
* @author LHL
*/
@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}")
private int port;
/**
* webSocket路径
*/
@Value("${webSocket.netty.path}")
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 NIO双向通道
bootstrap.channel(NioServerSocketChannel.class);
// 设置监听端口
bootstrap.localAddress(new InetSocketAddress(port));
/*
* option是设置 bossGroup,childOption是设置workerGroup
* netty 默认数据包传输大小为1024字节, 设置它可以自动调整下一次缓冲区建立时分配的空间大小,避免内存的浪费 最小 初始化 最大 (根据生产环境实际情况来定)
* 使用对象池,重用缓冲区
*/
bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576));
bootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576));
// 连接到达时会创建一个通道 初始化器,chanel注册后会执行里面相应的初始化方法
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 StringDecoder(CharsetUtil.UTF_8));
//ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ObjectEncoder());
// 以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
/*
请求分段,聚合请求进行完整的请求或响应
说明:
1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
*/
ch.pipeline().addLast(new HttpObjectAggregator(8192));
//对httpMessage进行聚合,聚合成FullHttpReq 或者 FullHttpRes
ch.pipeline().addLast(new HttpObjectAggregator(1024*64));
//websocket服务器协议,用于指定给客户端链接路由:ws,会帮忙处理握手动作,以及心跳
//处理心跳检测
ch.pipeline().addLast(new IdleStateHandler(6*10,0,0));
/*
说明:
1、对应webSocket,它的数据是以帧(frame)的形式传递
2、浏览器请求时 ws://localhost:8000/xxx 表示请求的uri
3、核心功能是将http协议升级为ws协议,保持长连接
*/
ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
// 自定义的handler,处理业务逻辑
ch.pipeline().addLast(webSocketHandler);
}
});
//启动
//绑定端口,并设置为同步方式,是一个异步的chanel
//配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
/*
* 关闭
* 获取某个客户端所对应的chanel,关闭并设置同步方式
* 对关闭通道进行监听
*/
channelFuture.channel().closeFuture().sync();
}
/**
* 在程序关闭前
* 释放资源
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workGroup != null) {
workGroup.shutdownGracefully().sync();
}
}
/**
* 在创建Bean时运行
* 需要开启一个新的线程来执行netty server 服务器
*/
@PostConstruct()
public void init() {
new Thread(() -> {
try {
start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
handler
/**
* @author LHL
* @Description: @Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用
*/
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
private WebSocketServerHandshaker handshaker;
/**
* webSocket路径
*/
@Value("${webSocket.netty.path}")
private String webSocketPath;
/**
* channel注册
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered channel注册");
super.channelRegistered(ctx);
}
/**
* channel注册
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelUnregistered channel注册");
super.channelUnregistered(ctx);
}
/**
* 客户端与服务端第一次建立连接时 执行
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(date + " " + ctx.channel().remoteAddress() + " 客户端连接成功!");
ctx.writeAndFlush("连接成功!");
}
/**
* 一旦连接,第一个被执行 客户端连接成功后执行的回调方法
*
* @param ctx ChannelHandlerContext
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
log.info("handlerAdded 被调用" + ctx.channel().id().asLongText());
// 添加到channelGroup 通道组
NettyConfig.getChannelGroup().add(ctx.channel());
}
/**
* 目前走这个方法
* 接收到消息执行的回调方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//http请求和tcp请求分开处理
if (msg instanceof HttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
/**
* 如果不重写channelRead
* 则走这个方法
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
// //接收到的消息
//log.info("服务器 收到[" + ctx.channel().remoteAddress() + "]消息:" + frame.text());
// //获取请求消息对象
//MessageRequest messageReq = JSONObject.parseObject(frame.text(), MessageRequest.class);
//MessageResponse messageResp = new MessageResponse(messageReq);
//String uid = messageReq.getSenderId();
// //发送者id为空
//if(StringUtils.isBlank(messageReq.getSenderId())){
// messageResp.setMsgEntity("发送用户不存在!");
// ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
// return;
//}
// // 获取用户ID,关联channel
//bind(ctx, uid);
// //回复消息
//messageResp.setIsReceipt(1);
//messageResp.setCode(200);
//ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
}
/**
* 第一次请求是http请求,请求头包括ws的信息
*/
public void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request)
throws Exception {
// 如果HTTP解码失败,返回HTTP异常
if (null != request) {
if (request instanceof HttpRequest) {
HttpMethod method = request.method();
// 如果是websocket请求就握手升级
if (webSocketPath.equalsIgnoreCase(request.uri())) {
log.info(" req instanceof HttpRequest");
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
webSocketPath, null, false);
handshaker = wsFactory.newHandshaker(request);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
}
handshaker.handshake(ctx.channel(), request);
}
}
}
}
/**
* websocket消息处理
* (只支持文本)
*/
public void handlerWebSocketFrame(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) {
//接收到的消息
log.info("服务器 收到[" + ctx.channel().remoteAddress() + "]消息:" + ((TextWebSocketFrame) frame).text());
//获取请求消息对象
MessageRequest messageReq = JSONObject.parseObject(((TextWebSocketFrame) frame).text(), MessageRequest.class);
MessageResponse messageResp = new MessageResponse(messageReq);
String uid = messageReq.getSenderId();
//发送者id为空
if (StringUtils.isBlank(messageReq.getSenderId())) {
messageResp.setMsgEntity("发送用户不存在!");
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
return;
}
// 获取用户ID,关联channel
bind(ctx, uid);
// 回复消息
messageResp.setIsReceipt(1);
messageResp.setCode(200);
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
}
}
/**
* channel读取数据完毕
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("channel读取数据完毕");
super.channelReadComplete(ctx);
}
/**
* channel可写事件更改
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
log.info("channel可写事件更改");
super.channelWritabilityChanged(ctx);
}
/**
* 这里是保持服务器与客户端长连接 进行心跳检测 避免连接断开
*
* @param ctx
* @param evt
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent stateEvent = (IdleStateEvent) evt;
switch (stateEvent.state()) {
//读空闲(服务器端)
case READER_IDLE:
String date1 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(date1 + "【" + ctx.channel().remoteAddress() + "】读空闲(服务器端)");
break;
//写空闲(客户端)
case WRITER_IDLE:
String date2 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(date2 + "【" + ctx.channel().remoteAddress() + "】写空闲(客户端)");
break;
case ALL_IDLE:
String date3 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(date3 + "【" + ctx.channel().remoteAddress() + "】读写空闲");
break;
default:
break;
}
}
}
/**
* 客户端与服务端 断连时 执行
* 客户端下线
*
* @param ctx ChannelHandlerContext
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(date + " " + ctx.channel().remoteAddress() + " 客户端下线!");
}
/**
* 断开连接
*
* @param ctx ChannelHandlerContext
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
log.info("handlerRemoved 被调用" + ctx.channel().id().asLongText());
// 删除通道
NettyConfig.getChannelGroup().remove(ctx.channel());
removeUserId(ctx);
}
/**
* 抛出异常
*
* @param ctx ChannelHandlerContext
* @param cause 异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("异常:{}", cause.getMessage());
// 删除通道
NettyConfig.getChannelGroup().remove(ctx.channel());
removeUserId(ctx);
ctx.close();
}
/**
* 删除用户与channel的对应关系
*
* @param ctx ChannelHandlerContext
*/
private void removeUserId(ChannelHandlerContext ctx) {
AttributeKey<String> key = AttributeKey.valueOf("userId");
String userId = ctx.channel().attr(key).get();
NettyConfig.getUserChannelMap().remove(userId);
}
/**
* 根据用户id判断是否存在用户
*
* @param userId 用户id
* @return Channel
*/
public static boolean exist(String userId) {
return NettyConfig.getUserChannelMap().containsKey(userId);
}
/**
* 根据用户id获取对应的channel
*
* @param userId 用户id
* @return Channel
*/
public static Channel getChannel(String userId) {
return NettyConfig.getUserChannelMap().get(userId);
}
/**
* 用户绑定channel
*
* @param ctx ChannelHandlerContext
* @param uid 用户id
*/
public void bind(ChannelHandlerContext ctx, String uid) {
if (!exist(uid)) {
NettyConfig.getUserChannelMap().put(uid, ctx.channel());
// 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
AttributeKey<String> key = AttributeKey.valueOf("userId");
ctx.channel().attr(key).setIfAbsent(uid);
}
}
}
页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Netty-Websocket</title>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:8000/ws");
socket.onmessage = function (event) {
var ta = document.getElementById('responseText');
let resData = JSON.parse(event.data);
console.log(resData);
ta.value += "用户: " + resData.senderId + "\t\t消息: " + resData.msgEntity + "\r\n";
};
socket.onopen = function (event) {
var ta = document.getElementById('responseText');
ta.value = "Netty-WebSocket服务器。。。。。。连接 \r\n";
};
socket.onclose = function (event) {
var ta = document.getElementById('responseText');
ta.value = "Netty-WebSocket服务器。。。。。。关闭 \r\n";
};
} else {
alert("您的浏览器不支持WebSocket协议!");
}
function send(uid, message) {
if (!window.WebSocket) {
return;
}
let param={
senderId: uid,
msgEntity: message
}
if (socket.readyState == WebSocket.OPEN) {
let resData = JSON.stringify(param);
console.log(resData);
socket.send(resData);
} else {
alert("WebSocket 连接没有建立成功!");
}
}
</script>
</head>
<body>
<form onSubmit="return false;">
<div>
<label>ID</label><input type="text" name="uid" value="${uid!!}"/> <br/>
<label>TEXT</label><input type="text" name="message" value="这里输入消息"/> <br/>
<br/> <input type="button" value="发送ws消息"
onClick="send(this.form.uid.value, this.form.message.value)"/>
</div>
<hr color="black"/>
<h3>服务端返回的应答消息</h3>
<textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>
接口
controller
/**
* @author LHL
*/
@RestController
@RequestMapping("/push")
public class PushController {
@Autowired
private IPushService pushService;
/**
* 推送给所有用户
*
* @param msg 消息
*/
@PostMapping("/pushAll")
public void pushToAll(@RequestParam("msg") String msg) {
pushService.pushMsgToAll(msg);
}
/**
* 推送给指定用户
*
* @param request 消息信息
*/
@PostMapping("/pushOne")
public void pushMsgToOne(@RequestBody MessageRequest request) {
pushService.pushMsgToOne(request);
}
}
service
/**
* @author LHL
*/
public interface IPushService {
/**
* 推送给指定用户
*
* @param userId
* @param msg
*/
void pushMsgToOne(MessageRequest request);
/**
* 推送给所有用户
*
* @param msg
*/
void pushMsgToAll(String msg);
}
/**
* @author LHL
*/
@Service
public class PushServiceImpl implements IPushService {
@Override
public void pushMsgToOne(MessageRequest request) {
Channel channel = null;
if (exist(request.getSenderId())) {
channel = getChannel(request.getSenderId());
MessageResponse response = new MessageResponse(request);
response.setIsReceipt(1);
response.setCode(200);
channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(response)));
}
}
@Override
public void pushMsgToAll(String msg) {
NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
}
/**
* 根据用户id判断是否存在用户
*
* @param userId 用户id
* @return Channel
*/
public static boolean exist(String userId) {
return NettyConfig.getUserChannelMap().containsKey(userId);
}
/**
* 根据用户id获取对应的channel
*
* @param userId 用户id
* @return Channel
*/
public static Channel getChannel(String userId) {
return NettyConfig.getUserChannelMap().get(userId);
}
/**
* 用户绑定channel
*
* @param ctx ChannelHandlerContext
* @param uid 用户id
*/
public void bind(ChannelHandlerContext ctx, String uid) {
NettyConfig.getUserChannelMap().put(uid, ctx.channel());
// 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
AttributeKey<String> key = AttributeKey.valueOf("userId");
ctx.channel().attr(key).setIfAbsent(uid);
}
}
实践
打开页面 http://localhost:8080/index.html