netty实战(一)聊天服务

经典例子,netty聊天服务,用来作为第一个实战来理解netty流程适合不过了。

netty出现是为了方便大家使用NIO来进行编程,特别是socket编程。而很多的socket编程都会有很多的协议,我们此次只是为了体验netty,所以不设置协议,直接裸体读写。

聊天消息结构体

public class MsgData {
    String name;
    String msg;
    String data;
    int device;
    //省略
    }

其实搞什么都无所谓,毕竟目前走通pipeline,msg最重要。

handler

netty中handler可以说是我们编码著需要注意的,对于对称消息来说,无非就解码和编码。所以此处我们使用序列化将对象转化为byte,同理在返回,轻快简介。

关于理论篇可以去看netty理论部分

以下是主要的handler处理部分,使用的是Kryo的序列化。功能就是把消息转化为byte,byte转化为消息。然后把byte写入btyebuff将bytebuff转化为消息。由于消息是相同的类型,我们只用了一套编码器与解码器。

public class MsgDataDecoder extends ReplayingDecoder<MsgData> {
    private Kryo kryo = new Kryo();
    public static final int HEAD_LENGTH = 4;


    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < HEAD_LENGTH) {  //这个HEAD_LENGTH是我们用于表示头长度的字节数。  由于Encoder中我们传的是一个int类型的值,所以这里HEAD_LENGTH的值为4.
            return;
        }
        in.markReaderIndex();                  //我们标记一下当前的readIndex的位置
        int dataLength = in.readInt();       // 读取传送过来的消息的长度。ByteBuf 的readInt()方法会让他的readIndex增加4
        if (dataLength < 0) { // 我们读到的消息体长度为0,这是不应该出现的情况,这里出现这情况,关闭连接。
            ctx.close();
        }

        if (in.readableBytes() < dataLength) { //读到的消息体长度如果小于我们传送过来的消息长度,则resetReaderIndex. 这个配合markReaderIndex使用的。把readIndex重置到mark的地方
            in.resetReaderIndex();
            return;
        }

        byte[] body = new byte[dataLength];  //传输正常
        in.readBytes(body);
        Object o = convertToObject(body);  //将byte数据转化为我们需要的对象
        out.add(o);
    }
    private Object convertToObject(byte[] body) {

        Input input = null;
        ByteArrayInputStream bais = null;
        try {
            bais = new ByteArrayInputStream(body);
            input = new Input(bais);

            MsgData obj= kryo.readObject(input, MsgData.class);
            System.out.println(obj);
            return obj;
        } catch (KryoException e) {
            e.printStackTrace();
        }finally{
            IOUtils.closeQuietly(input);
            IOUtils.closeQuietly(bais);
        }

        return null;
    }
}
public class MsgDataEncoder extends MessageToByteEncoder<MsgData> {
    private final Charset charset = Charset.forName("UTF-8");
    private Kryo kryo = new Kryo();
    private byte[] convertToBytes(MsgData car) {

        ByteArrayOutputStream bos = null;
        Output output = null;
        try {
            bos = new ByteArrayOutputStream();
            output = new Output(bos);
            kryo.writeObject(output, car);
            output.flush();

            return bos.toByteArray();
        } catch (KryoException e) {
            e.printStackTrace();
        }finally{
            IOUtils.closeQuietly(output);
            IOUtils.closeQuietly(bos);
        }
        return null;
    }


    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MsgData msgData, ByteBuf byteBuf) throws Exception {
        byte[]body=convertToBytes(msgData);
        int dataLen=body.length;
        byteBuf.writeInt(dataLen);
        byteBuf.writeBytes(body);
    }
}

在服务端有需要额外处理部分,比如每个人的上限与下限,在netty眼中,每个人就是个channel罢了,所以active与unactive代表着上限与下限。同理每个人的消息都是如此,这里没写很复杂的功能,想写,可以加handler,但是得注意handler得顺序与用法,具体还是看上篇文章关于handler责任链得部分。netty理论部分

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    //内部会用ConcurrentMap来维护,线程安全
    private static ChannelGroup channelGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * channel处于就绪状态,客户端刚上线
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        String remoteAddress = channel.remoteAddress().toString();
        //加入全局变量中
        //channelGroup.writeAndFlush(Unpooled.copiedBuffer("【客户端】"+remoteAddress+"上线啦 "+format.format(new Date()),CharsetUtil.UTF_8));
        channelGroup.writeAndFlush("【客户端】"+remoteAddress+"上线啦 "+format.format(new Date()));
        channelGroup.add(channel);
        //将当前channel加入到ChannelGroup
        System.out.println("【客户端】"+remoteAddress+"上线啦");
    }
    /**
     * channel离线
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        String remoteAddress = channel.remoteAddress().toString();
        channelGroup.remove(channel);
        channelGroup.writeAndFlush("【客户端】"+remoteAddress+"已下线 "+format.format(new Date()));
        System.out.println("【客户端】"+remoteAddress+"已下线");
    }
    //读取数据事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        String remoteAddress = channel.remoteAddress().toString();
        System.out.println("【客户端】"+remoteAddress+":"+msg);
//        msg= (MsgData)msg;
        channelGroup.forEach(ch -> {
                    if(ch==channel) {
                        ((MsgData) msg).setName("self");
                        ch.writeAndFlush(msg);
                    }else {
                        ch.writeAndFlush("【客户端】"+remoteAddress+":"+msg);
                    }
                }
        );
    }
    //异常发生事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //日志:远程主机强迫关闭了一个现有的连接。
        //System.out.println(cause.getMessage());
        ctx.close();
    }
}

server

服务端代码

public class ServerNetty {
    private int port=8080;

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

    public static void main(String[] args) throws Exception {

        int port = args.length > 0
                ? Integer.parseInt(args[0])
                : 9999;

        new ServerNetty(port).run();
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline().addLast(new MsgDataEncoder(),new MsgDataDecoder(),new NettyServerHandler());
                        }
                    }).option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

client

每一次channel得write和read如果抵达了NIO的缓冲区,pipeline的handler都会触发,所以在主线程,可以通过channel进行写操作触发到服务器。

public class ClientNetty {
    public static void main(String[] args)throws Exception {
        //1、创建一个线程组
        EventLoopGroup group = new NioEventLoopGroup();
        //2、创建客户端启动助手,完成相关配置
        Bootstrap b = new Bootstrap();
        b.group(group)//3、设置线程组
                .channel(NioSocketChannel.class)//4、设置客户端通道的实现类
                .handler(new ChannelInitializer<SocketChannel>() {//创建一个初始化通道对象
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new MsgDataEncoder());//对 String 对象自动编码,属于出站站处理器
                        sc.pipeline().addLast(new MsgDataDecoder());//把网络字节流自动解码为 String 对象,属于入站处理器
                        //6、在pipline中添加自定义的handler
                        sc.pipeline().addLast(new NettyClientHandler());
                    }
                });
        System.out.println("【客户端已启动】");
        //7、启动客户端去连接服务器端 connect方法是异步的,sync方法是同步阻塞的
        ChannelFuture cf =b.connect("127.0.0.1", 9999).sync();
        System.out.println("---"+cf.channel().remoteAddress()+"------");
        //循环监听用户键盘输入
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            System.out.println("用户输入:"+msg);
            MsgData msgData=new MsgData();
            msgData.setMsg(msg);
            //通过channel发送到服务器端
            cf.channel().writeAndFlush(msgData);
        }
        //8、关闭连接(异步非阻塞)
        cf.channel().closeFuture().sync();
        System.out.print("Client is end.....");
    }
}

总结

目前写的部分很简单,只是基础的实现部分,下一步可能会实现一些基本功能,如群发,私发,缓冲等待发等等。功能的实现可能有很多不同的细节,但思路都大差不差。所以走通pipeline是很重要。