在前面已经学习了 SOCKET 和 NIO ,从上几章也知道,传统的 NIO 编程,就是一个线程,对应一个selector,客户端的接入、数据读写都在一个线程,这样导致的后果就是没利用好CPU,且当接收客户端阻塞时,数据读写是进行不了的。
另外,NIO 的空转100%cpu占用率的问题,我们也没有解决;
笔者曾经对 NIO 进行了扩展 ,比如单独一个 线程池对应 selector 的 accept 客户端,另外的两个线程池,对应 selector 的READ 和 WRITE 操作;虽然,线程数进行了控制,且对 byteBuffer 也进行了扩展和填充,避免了数据黏包的问题,但是在 文件传输和要进行其他扩展时,总觉得难以进行,故而学习一下 Netty 是很有必要的。
至于 Netty 是什么,相信你已经对它进行过了解了,总之就是叼得一逼,例子和轮胎都不错,可以先看4.x的文档:
Netty 文档 基本跟着敲一遍都有一个很好的了解。
代码工程:https://github.com/LillteZheng/SocketDemo
该教程,后面回去研究一下 Netty 的源码,再根据里面的思想,对以前的项目进行一个扩展。
先看效果:
跟以前的做法一样,就是服务端充当中转站,把客户端的信息接收并传给其他客户端;
接着,来看看Netty 的服务端的配置和 传统的 NIO 有什么不同
public class ChatServer {
public static void main(String[] args) throws InterruptedException {
/**
* NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器
* boss 可以理解是 selector 的 accept 单独一个线程
* worker 可以理解是 selector 的 read 和 write
*/
final EventLoopGroup bossGroup = new NioEventLoopGroup();
final EventLoopGroup workerGroup = new NioEventLoopGroup();
// ServerBootstrap 是一个启动 NIO 服务的辅助启动类
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
//channel 实例化 NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 用来处理 handler ,设置连入服务端的 Client 的 SocketChannel 的处理器
.childHandler(new ChatServerInitializer())
//option 针对NioServerSocketChannel,比如这里 128 个客户端之后,才开始排队
.option(ChannelOption.SO_BACKLOG,128)
// childOption 针对childHandler 的handler
.childOption(ChannelOption.SO_KEEPALIVE,true);
//这里的启动时异步的,阻塞等待
ChannelFuture future = b.bind(Constants.PORT).sync();
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("服务端启动成功");
}
}
});
// 等待服务器 socket 关闭 。
// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
上面的注释已经很清楚了,需要注意几个类,比如 EventLoopGroup 对象,可以理解它为一个线程池,一个用于接收新的客户单,一个专注于数据读写,这样的好处是充分结合多线程和 selector 的模式,如果你想要深入了解,可以搜索 Netty 的 Readctro 模型。
ServerBootstrap 是NIO服务启动的一个辅助类,一般 NIO 的配置都是比较麻烦的, Netty 这里通过 Builder 的模式,可以省略很多步骤。
而 channel 和 childHandler 则是配置服务端和接入的 socketchannel 的属性的。这里用 ChatServerInitializer 来实现,后面看具体实现。
最后通过 bind 绑定端口并阻塞接收客户端的接入。
注意 closeFuture 方法,他是 监听 服务器关闭,不是关闭服务器,而是监听 关闭。
接着,继续看 ChatServerInitializer 的代码:
public class ChatServerInitializer extends ChannelInitializer {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//采用分隔符处理器,处理黏包问题,防止数据过大导致的黏包问题
pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Delimiters.lineDelimiter()));
//编码
pipeline.addLast(new StringDecoder());
//解码
pipeline.addLast(new StringEncoder());
//添加处理器,这里为逻辑的处理
pipeline.addLast(new ChatServerHandler());
}
}
重点看 ChatServerHandler 它为服务端主要的业务代码。这里为 聊天室:
public class ChatServerHandler extends SimpleChannelInboundHandler {
//单例
static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//提示其他客户端,有新客户端加入
group.writeAndFlush("SERVER - "+channel.remoteAddress()+"加入群聊\n");
group.add(channel);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//提示其他客户端,有新客户端加入
System.out.println("handlerRemoved");
group.writeAndFlush("SERVER - "+channel.remoteAddress()+"离开\n");
// group.remove(channel);
// A closed Channel is automatically removed from ChannelGroup,
// so there is no need to do "channels.remove(ctx.channel());"
}
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
Channel clientChannel = channelHandlerContext.channel();
//打印信息
for (Channel channel : group) {
if (channel != clientChannel){
channel.writeAndFlush("[" + clientChannel.remoteAddress() + "]" + s + "\n");
}else{
channel.writeAndFlush("[you]" + s + "\n");
}
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"异常");
cause.printStackTrace();
ctx.close();
}
}
可以看到这里继承的是 SimpleChannelInboundHandler 。当然,它也可以继承 ChannelInboundHandlerAdapter ,区别是 SimpleChannelInboundHandler 可以通过泛型指定数据类型,且在接收到数据之后,会自动 release ,避免 byteBuffer 被占用,而 ChannelInboundHandlerAdapter 则不会自动释放,需要自己 ReferenceCountUtil.release() ;教程都会说,记得回去看官方说明。
这里因为都是字符串类型,所以统一用 SimpleChannelInboundHandler ,当然服务端建议采用 ChannelInboundHandlerAdapter ,因为有多个不同类型的客户端接入,在客户端做区分,并做好释放即可。客户端的话,可以用SimpleChannelInboundHandler ,毕竟这个也比较单一。
这样,服务端的代码就写好了。
接着看 客户端的代码:
很多都是相似的,先看 ChatClient 的代码:
public class ChatClient {
public static void main(String[] args) throws InterruptedException, IOException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(bossGroup)
.channel(NioSocketChannel.class)
.handler(new ChatClientInitializer());
//连接服务器
final ChannelFuture future = bootstrap.connect("localhost", Constants.PORT).sync();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String msg = br.readLine();
if (msg.equals("bye")){
return;
}
future.channel().writeAndFlush(msg+"\n");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
}
}
}
基本与 服务端一直,因为是客户端,所以只要配置 channel 和 handler 即可。其中 ChatClientInitializer与服务端代码基本一直,只是业务逻辑那块,需要换成**ChatClientHandler **:
public class ChatClientHandler extends SimpleChannelInboundHandler {
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
//收到服务端消息
System.out.println(s);
}
}
这样,就完成了。