服务端使用netty的步骤
- 在服务端,需要使用NioEventLoopGroup创建两个 NIO 线程组。NioEventLoopGroup是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。
bossGroup线程组:Boss线程,由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。
workerGroup线程组:Worker线程,Worker线程执行所有的异步I/O,即处理操作。
(在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。) - 创建ServerBootstrap对象,这是一个启动NIO服务的辅助启动类,负责初始化netty服务器,并且开始监听端口的socket请求,用于配置Netty的一系列参数,如接受传出数据的缓存大小等。
- 创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口。
- 绑定端口,执行同步阻塞方法等待服务器端启动即可。
使用netty实现一个简易聊天服务器
该实例参考了这篇文章:Netty 实现聊天功能
1. SimpleChatServer.java
public class SimpleChatServer {
private int port;
public SimpleChatServer(int port) {
this.port = port;
}
public void run() throws InterruptedException {
// 创建两个线程组,Boss和Worker
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap实例,用于配置netty的一系列参数
ServerBootstrap bootstrap = new ServerBootstrap();
// 绑定两个线程组
bootstrap.group(bossGroup, workerGroup)
// 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
.channel(NioServerSocketChannel.class)
// SimpleChatServerInitializer, 继承于ChannelInitializer,目的是配置一个新的 Channel
.childHandler(new SimpleChatServerInitializer())
// option() 是提供给NioServerSocketChannel 用来接收进来的连接。
.option(ChannelOption.SO_BACKLOG, 128)
// childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这里也是NioServerSocketChannel
.childOption(ChannelOption.SO_KEEPALIVE, true);
System.out.println("SimpleChatServer 启动了");
// 绑定端口,开始接收进来的连接
// 可以多次调用 bind() 方法(基于不同绑定地址)
Channel ch = bootstrap.bind(port).sync().channel();
// 等待服务器 socket关闭
// future.channel().closeFuture().sync();
ch.closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
System.out.println("SimpleChatServer 关闭了");
}
}
}
2. SimpleChatServerInitializer.java
public class SimpleChatServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// netty中解决TCP的粘包拆包问题,这里使用自定义分隔符的方法,使用分隔符类DelimiterBasedFrameDecoder
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 编码解码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 自定义的处理实际业务逻辑的handler
pipeline.addLast("headler", new SimpleChatServerHandler());
System.out.println("SimpleChatClient:" + socketChannel.remoteAddress() + "连接上");
}
}
3. SimpleChatServerHandler.java
public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> {
/**
* 一个存储类,本质是一个Set
*/
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 每当从服务端收到新的客户端连接时,客户端的 Channel 存入 ChannelGroup 列表中,并通知列表中的其他客户端 Channel
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
for (Channel channel : channels) {
channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + "加入\n");
}
channels.add(ctx.channel());
}
/**
* 每当从服务端收到客户端断开时,客户端的 Channel 移除 ChannelGroup 列表中,并通知列表中的其他客户端 Channel
* @param ctx
* @throws Exception
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
for (Channel channel : channels) {
channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + "离开\n");
}
channels.remove(ctx.channel());
}
/**
*
* 每当从服务端读到客户端写入信息时,将信息转发给其他客户端的 Channel。
* 其中如果你使用的是 Netty 5.x 以下版本时,需要把 messageReceived() 重命名为 channelRead0()
* 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,也就是服务端接收客户端发来的数据。
* 但是这个数据在不进行解码时它是ByteBuf类型的
* @param ctx
* @param s
* @throws Exception
*/
@Override
protected void messageReceived(ChannelHandlerContext ctx, String s) throws Exception {
Channel incoming = ctx.channel();
for (Channel channel : channels) {
if (channel != incoming) {
channel.writeAndFlush("[" + incoming.remoteAddress() + "]" + s + "\n");
} else {
channel.writeAndFlush("[you]" + s + "\n");
}
}
}
/**
* channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端已经建立了通信通道并且可以传输数据
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "在线");
}
/**
* channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端关闭了通信通道并且不可以传输数据
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "掉线");
}
/**
* 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。
* 在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。
* 然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "异常");
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
最后创建一个Main类,创建一个main方法来运行该服务器:
public class Main {
public static void main(String[] args) throws InterruptedException {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new SimpleChatServer(port).run();
}
}
(注意:这里仅仅是服务器的开发,并没有编写客户端。)