Netty学习(1)-基于socket的在线聊天demo

175 阅读4分钟

概述

几天前偶然看到手搓RPC(远程过程调用)框架的帖子,于是想着重新了解Netty-网络编程框架的想法,写下了关于Netty实现的socket协议实时聊天室demo

Socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

简单来说,socket就是服务器-客户端点对点通讯的TCP/IP协议的抽象实现。

Netty

Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能可扩展协议的服务器和客户端。

它主要简化了网络应用程序的开发,例如简化了TCPUDPsocket服务的开发,接下来需要着手的demo就是由Netty框架开发。

正文

需要实现的功能是:仿造在线聊天室实现即时通讯,即需要一个服务端和多个客户端的模型。

依赖版本:Netty 4.x

服务端

服务端需要做的:客户端连接/离开时、客户端发送信息时广播对应内容。

从处理器着手,我们需要收发的信息为字符串类型,所以ChatServerHandler继承 SimpleChannelInboundHandler泛型为String类型,它继承于ChannelInboundHandlerAdapter,提供了信息进入处理器大部分方法,后面只需要按场景重写对应方法即可。

ChatServerHandler

@Slf4j
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
    // 重写channelRead0方法,在读取到客户端发送的信息时,向聊天室广播信息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        Channel say = ctx.channel();
        for (Channel member : channels) {
            if (say != member) {
                member.writeAndFlush(String.format("[%s say]: %s.\n", say.remoteAddress(), s));
            } else {
                member.writeAndFlush(String.format("[You say]: %s.\n", s));
            }
        }
    }
    
    // 服务端异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("服务器异常:{}", cause.getMessage(), cause);
    }

    // 客户端链接时触发
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel in = ctx.channel();
        channels.writeAndFlush(String.format("[Chat Room]: %s,join in chat.\n", in.remoteAddress()));
        channels.add(in);
    }

    // 客户端断开时触发
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel left = ctx.channel();
        channels.writeAndFlush(String.format("[Chat Room]: %s,has left.\n", left.remoteAddress()));
        channels.remove(left);
    }

    // 客户端活动时触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("{}上线\n", ctx.channel().remoteAddress());
    }

    // 客户端不活动时触发
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("{}离开\n", ctx.channel().remoteAddress());
    }
}

ChatServerChannelInitializer

继承于ChannelInitializer<SocketChannel> ,用来给SocketChannel pipeline中添加多个信息处理类:解码、编码等。

public class ChatServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel sc) throws Exception {
        ChannelPipeline pipeline = sc.pipeline();
        // 信息分割,处理netty信息粘包,Delimiters.lineDelimiter使用\n进行帧分割
        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        // 解码
        pipeline.addLast("decoder", new StringDecoder());
        // 编码
        pipeline.addLast("encoder", new StringEncoder());
        // 自定义的服务端处理器
        pipeline.addLast("handler", new ChatServerHandler());
    }
}

ChatServer

public class ChatServer {

    private Integer port;

    private NioEventLoopGroup boss;

    private NioEventLoopGroup worker;

    public ChatServer(Integer port) {
        this.port = port;
    }
    
    // 启动方法
    public void run() throws Exception{
        // 多线程循环处理器
        boss = new NioEventLoopGroup();
        worker = new NioEventLoopGroup();
        // 服务端启动辅助类,定义配置
        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            bootstrap.group(boss,worker) // boss收到信息后,会转发到worker进行处理
                    .channel(NioServerSocketChannel.class) // 服务端通道
                    .option(ChannelOption.SO_BACKLOG,128) // boss线程组选项,这里是服务器的队列长度
                    .childOption(ChannelOption.SO_KEEPALIVE,true) // worker线程组选项
                    .childHandler(new ChatServerChannelInitializer()); // 添加自定义的ChannelInitializer
            ChannelFuture future = bootstrap.bind(port).sync(); // 绑定端口
            //future.channel().closeFuture().sync(); // 接收服务端关闭

        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

客户端

客户端要做的只是收发信息,收到信息时打印,或在控制台中发送信息。

ChatClientHandler

信息处理器只需打印内容

@Slf4j
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
    // 只需要打印读到的内容
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
          log.info(s);
          log.info("\n");
    }
}

ChatClient

@Data
public class ChatClient {

    private String host;

    private Integer port;

    private NioEventLoopGroup boss;

    private Channel channel;

    public ChatClient(String host, Integer port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception{
        boss = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(boss) // 客户端只需一个线程组
                    .channel(NioSocketChannel.class) // 绑定NioSocketChannel而不是 NioServerSocketChannel
                    // 使用匿名内部类新建ChannelInitializer
                    .handler(new ChannelInitializer<SocketChannel>() { 
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            ChannelPipeline pipeline = sc.pipeline();
                            pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("handler",new ChatClientHandler());
                        }
                    })
                    .option(ChannelOption.SO_KEEPALIVE,true);
             // 连接
            ChannelFuture future = bootstrap.connect(host, port).sync();
            this.channel = future.channel(); // 获取连接的channel
            
            // 控制台输出信息
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            while(true){
                channel.writeAndFlush(in.readLine() + "\r\n");
            }

        } finally {
            boss.shutdownGracefully(); // 关闭客户端
        }
    }
}

运行

分别编写主方法运行服务端、2个客户端,测试收发信息。

服务端输出

客户端01发送信息

客户端00接收信息

Demo已经上传代码仓库:netty-learning,后续有机会编写基于Netty的其他demo(远程调用RPC)会在此仓库同步更新,感兴趣可以看看。