Netty应用实例 - 群聊系统

需求


  1. 编写一个Netty群聊系统, 实现服务器端和客户端之间的数据简单通讯(非阻塞)
  2. 实现多人群聊


  3. 服务器端: 可以监测用户上下, 离线, 并实现消息转发功能
  4. 客户端: 通过channel可以无阻塞发送消息给其他所有用户同时可以接收其他用户发送的消息(由服务器转发得到)


  5. 目的: 进一步理解Netty 非阻塞网络编程机制
  6. 看老师代码演示

14-Netty 应用实例 - 多人群聊系统_客户端

NettyServer

package com.dance.netty.netty.groupchar;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.nio.charset.StandardCharsets;

public class NettyServer {

private final int port;

public NettyServer(int port) {
this.port = port;
}

public void run() throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.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 {
ch.pipeline()
.addLast("decoder", new StringDecoder(StandardCharsets.UTF_8))
.addLast("encoder", new StringEncoder(StandardCharsets.UTF_8))
.addLast("myHandler", new NettyServerHandler());
}
});
System.out.println("netty server is starter......");
ChannelFuture sync = serverBootstrap.bind(port).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}

}

public static void main(String[] args) throws InterruptedException {
new NettyServer(7000).run();
}
}


NettyServerhandler

package com.dance.netty.netty.groupchar;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NettyServerHandler extends SimpleChannelInboundHandler<String> {

/**
* 定义一个Channel组, 用于管理所有的channel
* GlobalEventExecutor.INSTANCE : 是一个全局的事件执行器, 是一个单例
*/
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

/**
* 表示建立连接, 一旦连接, 第一个执行
* @param ctx 上下文对象
* @throws Exception 异常
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
/*
* 将该用户加入聊天的信息提送给其他客户端
* 该方法会将 channelGroup 中 所有的Channel遍历 并发送
*/
channelGroup.writeAndFlush("[客户端]:" + channel.remoteAddress() +" " + dateTimeFormatter.format(LocalDateTime.now()) + " 加入聊天\n" );
// 加入到组中 因为先发送后加入, 就不会发送给自己了
channelGroup.add(channel);
}

/**
* 表示Channel处于活跃的状态
* @param ctx 上下文对象
* @throws Exception 异常
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("[客户端]: " +ctx.channel().remoteAddress() + " 上线了~" +" " + dateTimeFormatter.format(LocalDateTime.now()));
}

/**
* 表示Channel处于非活跃状态
* @param ctx 上下文对象
* @throws Exception 异常
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("[客户端]: " + ctx.channel().remoteAddress() + " 离线了~" +" " + dateTimeFormatter.format(LocalDateTime.now()));
}

/**
* 表示断开连接
* @param ctx 上下文对象
* @throws Exception 异常
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 会自动将channel从ChannelGroup中移除, 不需要手动
channelGroup.writeAndFlush(ctx.channel().remoteAddress() + " 离开了~" +" " + dateTimeFormatter.format(LocalDateTime.now()));
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
Channel channel = ctx.channel();
channelGroup.forEach(x -> {
// 排除自己
if(channel != x){
x.writeAndFlush("[客户] : " + channel.remoteAddress() +" " + dateTimeFormatter.format(LocalDateTime.now()) + " 发送了 -> " +msg + "\n");
}else{
// 回显自己
x.writeAndFlush("[自己] : " + channel.remoteAddress()+" " + dateTimeFormatter.format(LocalDateTime.now()) + " 发送了 -> " +msg + "\n");
}
});
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}


NettyClient

package com.dance.netty.netty.groupchar;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class NettyClient {

private final int port;

private final String ip;

public NettyClient(int port, String ip) {
this.port = port;
this.ip = ip;
}

public void run() throws InterruptedException {
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("decoder", new StringDecoder())
.addLast("encoder", new StringEncoder())
.addLast("myHandler", new NettyClientHandler());
}
});
ChannelFuture sync = bootstrap.connect(ip, port).sync();
System.out.println("----------"+sync.channel().localAddress()+"----------");
Scanner scanner = new Scanner(System.in);
Channel channel = sync.channel();
while (scanner.hasNextLine()){
String s = scanner.nextLine();
channel.writeAndFlush(s+ "\r\n");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
eventExecutors.shutdownGracefully();
}
}

public static void main(String[] args) throws InterruptedException {
new NettyClient(7000, "127.0.0.1").run();
}
}


NettyClientHandler

package com.dance.netty.netty.groupchar;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}


执行结果

NettyServer

netty server is starter......
[客户端]: /127.0.0.1:61298 上线了~ 2022-01-16 18:59:32
[客户端]: /127.0.0.1:61342 上线了~ 2022-01-16 18:59:35
[客户端]: /127.0.0.1:61402 上线了~ 2022-01-16 18:59:49
[客户端]: /127.0.0.1:61402 离线了~ 2022-01-16 19:03:34


NettyClient1

----------/127.0.0.1:61298----------
[客户端]:/127.0.0.1:61342 2022-01-16 18:59:35 加入聊天
[客户端]:/127.0.0.1:61402 2022-01-16 18:59:49 加入聊天
hi flower
[自己] : /127.0.0.1:61298 2022-01-16 19:00:16 发送了 -> hi flower
[客户] : /127.0.0.1:61342 2022-01-16 19:00:37 发送了 -> hi dance
/127.0.0.1:61402 离开了~ 2022-01-16 19:03:34


NettyClient2

----------/127.0.0.1:61342----------
[客户端]:/127.0.0.1:61402 2022-01-16 18:59:49 加入聊天
[客户] : /127.0.0.1:61298 2022-01-16 19:00:16 发送了 -> hi flower
hi dance
[自己] : /127.0.0.1:61342 2022-01-16 19:00:37 发送了 -> hi dance
/127.0.0.1:61402 离开了~ 2022-01-16 19:03:34


NettyClient3

----------/127.0.0.1:61402----------
[客户] : /127.0.0.1:61298 2022-01-16 19:00:16 发送了 -> hi flower
[客户] : /127.0.0.1:61342 2022-01-16 19:00:37 发送了 -> hi dance