目录
一、群聊
需求
- 编写一个 Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(由服务器转发得到)
- 目的:进一步理解Netty非阻塞网络编程机制
服务端代码
ChatGroupServerHandler.java
public class ChatGroupServerHandler extends SimpleChannelInboundHandler<String> {
/**
* 定义一个 channelGroup ,管理所有的channel
* GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
*/
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 表示Channel 处于活动状态,提示 XX 上线
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + "-上线了~");
}
/**
* 表示Channel 处于不活动状态,提示 XX 离线
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + "-离线了~");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 关闭通道
ctx.close();
}
/**
* 表示连接建立,一旦建立连接,第一个执行该方法
* 并且将当前 channel 加入到channelGroup
*
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
// 将该客户端加入聊天的信息,发送给其他客户端
/**
* 该方法将会 循环遍历ChannelGroup中所有的Channel,并发送消息,我们不需要自己遍历
*/
channelGroup.writeAndFlush(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm:ss")
.format(LocalDateTime.now()) + ": 【" + channel.remoteAddress() + "】-加入聊天");
channelGroup.add(channel);
}
// 断开连接,将XX客户端离开的消息推送给当前在线的客户端
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
channelGroup.writeAndFlush(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm:ss")
.format(LocalDateTime.now()) + ": 【" + channel.remoteAddress() + "】-离开聊天");
System.out.println("GroupChannel-Size:" + channelGroup.size());
}
// 读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 读取道当前的Channel
Channel channel = ctx.channel();
// 这时我们遍历ChannelGroup,根据不同的情况,回送不同的消息
channelGroup.forEach(ch -> {
if (channel != ch) {
// 不是自己
ch.writeAndFlush("【" + channel.remoteAddress() + "】发送消息:" + msg + "\n");
} else {
// 回显
ch.writeAndFlush("【自己】:" + msg + "\n");
}
});
}
}
ChatGroupServer.java
/**
* 群聊服务端
*/
public class ChatGroupServer {
private final int PORT;
public ChatGroupServer(int PORT) {
this.PORT = PORT;
}
public void run() {
// 创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
// 向pipeline 加入解码器
pipeline.addLast("decoder", new StringDecoder());
// 向pipeline 加入编码器
pipeline.addLast("encoder", new StringEncoder());
// 加入自己的业务处理的handler
pipeline.addLast(new ChatGroupServerHandler());
}
});
System.out.println("群聊服务器启动~~~");
ChannelFuture channelFuture = serverBootstrap.bind(PORT).sync();
// 监听关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new ChatGroupServer(7000).run();
}
}
客户端代码
ChatGroupClientHandler.java
public class ChatGroupClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}
ChatGroupClient.java
public class ChatGroupClient {
private final int PORT;
private final String HOST;
public ChatGroupClient(String host, int port) {
PORT = port;
HOST = host;
}
private void run() {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
// 向pipeline 加入解码器
pipeline.addLast("decoder", new StringDecoder());
// 向pipeline 加入编码器
pipeline.addLast("encoder", new StringEncoder());
// 加入自己的业务处理的handler
pipeline.addLast(new ChatGroupClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(HOST, PORT).sync();
// 得到Channel
Channel channel = channelFuture.channel();
System.out.println("-----" + channel.localAddress() + "-----");
// 客户端需要输入信息,
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
// 通过channel 发送到服务端
channel.writeAndFlush(msg + "\r\n");
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
new ChatGroupClient("127.0.0.1", 7000).run();
}
}
测试结果
服务端 客户端1
客户端2 客户端3
二、心跳检测
需求:
- 编写一个 Netty心跳检测机制案例, 当服务器超过3秒没有读时,就提示读空闲
- 当服务器超过5秒没有写操作时,就提示写空闲
- 实现当服务器超过7秒没有读或者写操作时,就提示读写空闲
代码
HeartBeatHandler.java
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
/**
* @param ctx 上下文
* @param evt 事件
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 将evt 向下转型,
IdleStateEvent event = (IdleStateEvent) evt;
switch (event.state()) {
case READER_IDLE:
System.out.println(System.currentTimeMillis()+":读空闲");
break;
case WRITER_IDLE:
System.out.println(System.currentTimeMillis()+":写空闲");
break;
case ALL_IDLE:
System.out.println(System.currentTimeMillis()+":读写空闲");
break;
}
// 关闭通道
// ctx.close();
}
}
HeartBeatServer.java
public class HeartBeatServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/**
* 加入一个netty提供的IdleStateHandler
* 说明:
* 1. IdleStateHandler 是 netty 提供的处理空闲状态的处理器
* 2. long readerIdleTime: 表示经过多长时间没有读,就会发送一个心跳检测包进行连接检查
* 3. long writerIdleTime: 表示经过多长时间没有写,就会发送一个心跳检测包进行连接检查
* 4. long allIdleTime: 表示经过多长时间没有读写,就会发送一个心跳检测包进行连接检查
* 官方解释:
* Triggers an IdleStateEvent when a Channel has not performed read, write, or both operation for a while.
* 5. 当 IdleStateEvent 触发后,就会传递给管道的下一个handler去处理,
* 通过调用下一个Handler的 userEventTiggered,在该方法中处理IdleStateEvent(读空闲,写空闲,读写空闲)
*/
pipeline.addLast(new IdleStateHandler(7, 10, 5, TimeUnit.SECONDS));
pipeline.addLast(new HeartBeatHandler());
}
});
System.out.println(System.currentTimeMillis()+"服务器启动");
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
结果
三、通过WebSocket实现长连接
要求: 实现基于webSocket的长连接的全双工的交互
- Http协议是无状态的, 浏览器和服务器间的请求响应一次,下一次会重新创建连接.
- 需要改变Http协议多次请求的约束,实现长连接, 服务器可以发送消息给浏览器
- 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知
服务端代码
- WebSocketServerHandler.java
/**
* TextWebSocketFrame 表示一个文本帧
*/
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务端收到消息:"+msg.text());
// 回复消息
ctx.writeAndFlush(new TextWebSocketFrame("当前时间:"+ LocalDateTime.now()+"--"+msg.text()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生:" + cause.getMessage());
ctx.close();
}
/**
* 当 web 客户端连接后,触发该方法
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// id 是唯一值,short 不唯一,Long 是唯一的
System.out.println("handlerAdded:"+ctx.channel().id().asShortText());
System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
}
}
WebSocketServer.java
/**
* websocket 服务端
*/
public class WebSocketServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.ERROR)) // 打印 netty 日志
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 因为基于Http协议,所以使用http的编解码器
pipeline.addLast(new HttpServerCodec());
// 以块方式写,添加 ChunkedWriteHandler 处理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* 说明
* 1. Http再传输过程中是分段,HttpObjectAggregator 就是可以将多个段聚合,
* 2. 这就是为什么,当浏览器发送大数据时,会发生多次 http 请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/**
* 说明:
* 1. websocket 协议,数据是以帧(frame)形式传递
* 2. WebSocketFrame 下面有6个子类
* 3. 浏览器请求时:ws:127.0.0.1/7998/hello 表示请求的uri
* 4. WebSocketServerProtocolHandler 核心功能是将http协议升级为ws协议,保持长连接,状态码101
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
// 自定义的handler,处理业务逻辑
pipeline.addLast(new WebSocketServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7998).sync();
System.out.println("~~~websocket 服务器已经启动~~~");
channelFuture.channel().closeFuture().sync();
} catch (Exception exception){
exception.printStackTrace();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>netty-websocket-client</title>
</head>
<body>
<script>
let socket;
// 判断当前浏览器是否支持websocket
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:7998/hello");
// 相当于 channelRead0,读取消息
socket.onmessage = function (ev) {
const showText = document.getElementById('responseText');
showText.value += ev.data + "\r\n";
}
// 连接开启
socket.onopen = function (ev) {
const showText = document.getElementById('responseText');
showText.value = "已建立连接"+"\r\n";
}
// 连接关闭
socket.onclose = function (ev) {
const showText = document.getElementById('responseText');
showText.value += "已关闭连接" + "\r\n";
}
// 发送消息到服务器
function send(msg) {
// 先判断socket是否创建好
if (!socket) {
return;
}
if (socket.readyState === socket.OPEN) { // 判断websocket是否以建立连接
socket.send(msg);
const sendBox = document.getElementById('sendBox');
sendBox.value = '';
} else {
alert("未开启连接")
}
}
} else {
alert("当前浏览器不支持websocket!!!");
}
</script>
<form onsubmit="return false">
<textarea id = 'sendBox' name="message" style="width: 150px;height: 200px"></textarea>
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="width: 150px;height: 200px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value = ''">
</form>
</body>
</html>
结果