Netty客户端学习使用整理,Netty使用教程

一、客户端如何建立连接并监听与发送数据

下面的代码例子教你如何建立与服务器的连接并发送与接收数据。

1.服务器代码。我这里的服务器代码是基于原生的serverSocket,每建立一个客户端连接会启动一个线程去读取与发送数据。

import java.io.*;
import java.net.*;

public class SocketServer {
    public static final int PORT = 8031;//监听的端口号     
    public static Boolean Open = true;

    public static void main(String[] args) {    
        System.out.println("服务器启动...\n");    
        SocketServer server = new SocketServer();    
        server.init();    
    }    

    public void init() {    
        try {    
            ServerSocket serverSocket = new ServerSocket(PORT);    
            while (Open) {    
                // 一旦有堵塞, 则表示服务器与客户端获得了连接    
                Socket client = serverSocket.accept();    
                // 处理这次连接    
                new HandlerThread(client);    
            }    
            serverSocket.close();
        } catch (Exception e) {    
            System.out.println("服务器异常: " + e.getMessage());    
        }    
    }    

    private class HandlerThread implements Runnable {    
        private Socket socket;    
        public HandlerThread(Socket client) {    
            socket = client;    
            new Thread(this).start();    
        }    

        public void run() {    
              System.out.println("服务器与客户端获得了连接 " );           
            try {                  
                DataInputStream input = new DataInputStream(socket.getInputStream()); 
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());   
                while(Open){                       
                int length = input.available();   
                if(length>0){
                byte[] bs = new byte[length];   
                input.read(bs);  // 读取客户端数据,这里要注意和客户端输出流的写方法对应,客户端发送的是字节,读取的应该也是字节,同时编码方式要一致             
                String clientInputStr = new String(bs,"utf-8");//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException或者读取不到数据 
                System.out.println("客户端发过来的内容:" + clientInputStr);// 处理客户端数据   
                String s = "Data from server:"+clientInputStr+"\n";
                 //如果客户端发送的是close,执行关闭连接         
                if(clientInputStr.equals("close")){
                    Open = false;                
                     s = "close\n";
                }   
                // 向客户端回复信息     ,
                out.write(s.getBytes("utf-8"));    
                out.flush();   
                System.out.println("发送给客户端数据:"+s); 
                }
                }    
                out.close();    
                input.close();    
            } catch (Exception e) {   
                Open = false;
                System.out.println("服务器 run 异常: " + e.getMessage());    
            } 

        }
    }    
}

2.客户端代码。作用是建立一个与客户端的连接,监听键盘输入的数据发送到服务器去,并监听打印服务器返回的数据,不多说,注释写的很清楚了。

  public static Channel channel;
    public static boolean open = true;
    public static EventLoopGroup workerGroup;

    public static void init() {
        String host = "localhost";
        int port = 8031;

        workerGroup = new NioEventLoopGroup();//工作组,用于处理连接通信
        try {
            Bootstrap b = new Bootstrap(); // (1)引导程序,用于设置参数
            b.group(workerGroup); // (2)添加工作组
            b.channel(NioSocketChannel.class); // (3)添加通道,通道用于接收进来的连接
            b.option(ChannelOption.TCP_NODELAY, true); // (4)设置通道连接参数
            b.handler(new ChannelInitializer<SocketChannel>() {//ChannelInitializer是用于配置通道
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    //主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
                    //在这里实现添加handlers到指定位置实现数据逻辑处理而已
                    ch.pipeline().addLast(new ClientDataHandler());
                }
            });
            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)
            f.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    channel = channelFuture.channel();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            //启动线程进行数据输入发送,必须是新的线程,否则将会阻塞显示导致无法接收数据
                            getData();
                        }
                    }).start();

                }

                private void getData() {
                    while (open) {
                        try {
                            System.out.print("请输入: \t");
                            //获取键盘输入的数据发送给服务器
                            String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
                            if (channel != null) {
                                channel.writeAndFlush(Unpooled.copiedBuffer(str.getBytes("utf-8")));
                            }
                            //如果输入为close,就关闭连接
                            if (str.equals("close")) {
                                closed();
                            }
                            Thread.sleep(1000);
                        } catch (Exception e) {
                            open = false;
                            System.out.println("客户端异常:" + e.getMessage());
                        } finally {

                        }
                    }
                }
            });

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            closed();
            System.out.println("Exception:" + e.toString());
        }
    }

    public static void closed() {
        System.out.println("closed" );
        open = false;
        workerGroup.shutdownGracefully();
    }

3.数据监听。在ClientDataHandler进行监听:

public class ClientDataHandler extends ChannelInboundHandlerAdapter{
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte [] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String message = new String(req,"UTF-8");
        System.out.println("来自服务器的数据:"+ message);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //释放资源
        ctx.close();
    }
}

4.打印结果:

客户端:
请输入:    hello
来自服务器的数据:Data from server:hello

请输入:    i am bifan
来自服务器的数据:Data from server:i am bifan

请输入:    



服务器端:
服务器启动...

服务器与客户端获得了连接 
客户端发过来的内容:hello
发送给客户端数据:Data from server:hello

客户端发过来的内容:i am bifan
发送给客户端数据:Data from server:i am bifan

5.添加编码器与解码器。上面我们发送与接收的是字节数据,但是我们处理的是字符串,所以对发送与接收的字符串进行了编码与解码,如果添加了编码器与解码器,那么就不用字节处理了。

添加发送编码器与接收解码器:

@Override
                public void initChannel(SocketChannel ch) throws Exception {
                    //主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
                    //在这里实现添加handlers到指定位置实现数据逻辑处理而已
                    //添加发送数据编码器
                    ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                    //添加接收数据解码器
                    ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                    ch.pipeline().addLast(new ClientDataHandler());
                }

发送数据时就可以直接发送字符串了:

                            //获取键盘输入的数据发送给服务器
                            String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
                            if (channel != null) {
                                channel.writeAndFlush(str);//直接发送字符串数据
                            }

接收数据处理也可以直接处理字符串:

public class ClientDataHandler extends ChannelInboundHandlerAdapter{
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String message = (String) msg;
        System.out.println("来自服务器的数据:"+ message);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //释放资源
        ctx.close();
    }
}

也可以使用自己自定义的编码器与解码器,只需要继承MessageToMessageDecoder与MessageToMessageEncoder就可以,可以实现发送与接收对象等自定义的格式。


二、使用IdleStateHandler发送心跳包

IdleStateHandler是空闲状态处理,顾明思意就是在不跟服务交流的时间,如果处于某一状态,可以做相应的处理,那就可以利用这个去发送心跳包。
添加方式一样:

 ch.pipeline().addLast(new IdleStateHandler(0,0,5));
 三个参数:
 int readerIdleTimeSeconds:读取空闲,超过这个时间没有读取来自服务器的数据,将触发
 int writerIdleTimeSeconds:写出空闲,超过这个时间没有发送数据,将触发
 int allIdleTimeSeconds:整体空闲将触发
 触发将在userEventTriggered中触发。

下面我们先创建一个包括处理心跳包的HeartBeatHandler,这个是集成了SimpleChannelInboundHandler,因为上面我们的例子最终处理的是字符串,具体看注释,挺详细的了:

public class HeartBeatHandler extends SimpleChannelInboundHandler<String> {
    public static final String HearBeatSendToServer = "HearBeatSendToServer";//发送的心跳消息
    public static final String HearBeatSendFromServer = "HearBeatSendFromServer";//服务器心跳的回复
    /**
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    //在这个方法里,如果空闲触发,我们将发送心跳包
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case READER_IDLE://读取空闲
                    handleReaderIdle(ctx);
                    break;
                case WRITER_IDLE://写出空闲
                    handleWriterIdle(ctx);
                    break;
                case ALL_IDLE://整体空闲
                    handleAllIdle(ctx);
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * @param ctx 读取超时处理
     */
    protected void handleReaderIdle(ChannelHandlerContext ctx) {
    }

    /**
     * @param ctx 写超时处理
     */
    protected void handleWriterIdle(ChannelHandlerContext ctx) {
    }

    /**
     * @param ctx 整体超时处理
     */
    protected void handleAllIdle(ChannelHandlerContext ctx) {
       //超时了,发送一个心跳
        sendHearBeatToServer(ctx);
    }
   /**
     * @param channelHandlerContext
     * @param s
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        if (s.equals(HearBeatSendToServer)) {//这个是发送心跳的消息,直接发送心跳包
            sendHearBeatToServer(channelHandlerContext);
        } else if (s.equals(HearBeatSendFromServer)) {//这个说明收到了服务器对心跳包的响应
            onReceiveHearBeatResponseFromServer(channelHandlerContext);
        } else {//这个是收到了其他数据
            handleData(channelHandlerContext, s);
        }

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //释放资源
        ctx.close();
    }

    /**
     * 消息业务逻辑处理
     *
     * @param channelHandlerContext
     * @param s
     */
    protected void handleData(ChannelHandlerContext             channelHandlerContext, String data) {

    }

    /**
     * @param channelHandlerContext 收到来自服务器的心跳响应
     */
    protected void onReceiveHearBeatResponseFromServer(ChannelHandlerContext channelHandlerContext) {
        System.out.println("收到来自服务器的心跳响应:" + HearBeatSendFromServer);
    }

    /**
     * @param channelHandlerContext 发送心跳到服务器
     */
    protected void sendHearBeatToServer(ChannelHandlerContext channelHandlerContext) {
        channelHandlerContext.writeAndFlush(HearBeatSendToServer);
    }


}

我们的ClientDataHandler只需要继承HeartBeatHandler处理我们的业务数据即可:

ublic class ClientDataHandler extends HeartBeatHandler{
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    }

    @Override
    protected void handleData(ChannelHandlerContext channelHandlerContext, String s) {
        super.handleData(channelHandlerContext, s);
        System.out.println("收到来自服务器数据:"+s);
    }
}

最终,我们的客户端带心跳处理的完整代码如下:

public static Channel channel;
    public static boolean open = true;
    public static EventLoopGroup workerGroup;

    public static void init() {
        String host = "localhost";
        int port = 8032;
        workerGroup = new NioEventLoopGroup();//工作组,用于处理连接通信
        try {
            Bootstrap b = new Bootstrap(); // (1)引导程序,用于设置参数
            b.group(workerGroup); // (2)添加工作组
            b.channel(NioSocketChannel.class); // (3)添加通道,通道用于接收进来的连接
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)设置通道连接参数
            b.handler(new ChannelInitializer<SocketChannel>() {//ChannelInitializer是用于配置通道
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    //主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
                    //在这里实现添加handlers到指定位置实现数据逻辑处理而已
                    //添加发送数据编码器
                    ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                    //添加接收数据解码器
                    ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                    //添加空闲处理
                    ch.pipeline().addLast(new IdleStateHandler(0,0,5));
                    //添加到空闲发送心跳包的数据处理Handler
                    ch.pipeline().addLast(new ClientDataHandler());
                }
            });
            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)
            f.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    channel = channelFuture.channel();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            //启动线程进行数据输入发送,必须是新的线程,否则将会阻塞显示导致无法接收数据
                            getData();
                        }
                    }).start();
                }
                private void getData() {
                    while (open) {
                        try {
                            System.out.print("请输入: \t");
                            //获取键盘输入的数据发送给服务器
                            String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
                            if (channel != null) {
                                channel.writeAndFlush(str);
                            }
                            //如果输入为close,就关闭连接
                            if (str.equals("close")) {
                                closed();
                            }
                            Thread.sleep(1000);
                        } catch (Exception e) {
                            open = false;
                            System.out.println("客户端异常:" + e.getMessage());
                        } finally {

                        }
                    }
                }
            });
            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            closed();
            System.out.println("Exception:" + e.toString());
        }
    }

    public static void closed() {
        System.out.println("closed" );
        open = false;
        workerGroup.shutdownGracefully();
    }

服务器也需要修改下,需要响应客户端发送的心跳包,整体代码如下:

import java.io.*;
import java.net.*;

public class SocketServer {
    public static final int PORT = 8032;//监听的端口号     
    public static Boolean Open = true;

    public static void main(String[] args) {    
        System.out.println("服务器启动...\n");    
        SocketServer server = new SocketServer();    
        server.init();    
    }    

    public void init() {    
        try {    
            ServerSocket serverSocket = new ServerSocket(PORT);    
            while (Open) {    
                // 一旦有堵塞, 则表示服务器与客户端获得了连接    
                Socket client = serverSocket.accept();    
                // 处理这次连接    
                new HandlerThread(client);    
            }    
            serverSocket.close();
        } catch (Exception e) {    
            System.out.println("服务器异常: " + e.getMessage());    
        }    
    }    

    private class HandlerThread implements Runnable {    
        private Socket socket;    
        public HandlerThread(Socket client) {    
            socket = client;    
            new Thread(this).start();    
        }    

        public void run() {    
              System.out.println("服务器与客户端获得了连接 " );           
            try {                  
                DataInputStream input = new DataInputStream(socket.getInputStream()); 
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());   
                while(Open){                       
                int length = input.available();   
                if(length>0){
                byte[] bs = new byte[length];   
                input.read(bs);  // 读取客户端数据,这里要注意和客户端输出流的写方法对应,客户端发送的是字节,读取的应该也是字节,同时编码方式要一致             
                String clientInputStr = new String(bs,"utf-8");//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException或者读取不到数据 
                System.out.println("客户端发过来的内容:" + clientInputStr);// 处理客户端数据   
                String s = "Data from server:"+clientInputStr+"\n";
                 //如果客户端发送的是close,执行关闭连接         
                if(clientInputStr.equals("close")){
                    Open = false;                
                     s = "close\n";
                }  

                if(clientInputStr.equals("HearBeatSendToServer")){                           
                     s = "HearBeatSendFromServer";//如果是客户端的心跳消息,发送响应
                }
                // 向客户端回复信息     ,
                out.write(s.getBytes("utf-8"));    
                out.flush();   
                System.out.println("发送给客户端数据:"+s); 
                }
                }    
                out.close();    
                input.close();    
            } catch (Exception e) {   
                Open = false;
                System.out.println("服务器 run 异常: " + e.getMessage());    
            } 

        }
    }    
}

运行代码,效果如下:

客户端:
请输入:    收到来自服务器的心跳响应:HearBeatSendFromServer
gg收到来自服务器的心跳响应:HearBeatSendFromServer

收到来自服务器数据:Data from server:gg

请输入:    dd
收到来自服务器数据:Data from server:dd

请输入:    gg
收到来自服务器数据:Data from server:gg
//空闲后会发送心跳包并收到响应
请输入:    收到来自服务器的心跳响应:HearBeatSendFromServer



服务器端:
服务器与客户端获得了连接 
客户端发过来的内容:HearBeatSendToServer
发送给客户端数据:HearBeatSendFromServer
客户端发过来的内容:HearBeatSendToServer
发送给客户端数据:HearBeatSendFromServer
客户端发过来的内容:gg
发送给客户端数据:Data from server:gg

客户端发过来的内容:dd
发送给客户端数据:Data from server:dd

客户端发过来的内容:gg
发送给客户端数据:Data from server:gg

客户端发过来的内容:HearBeatSendToServer

如果断线了,如何自动重连?在ClientDataHandler的channelInactive会触发断线事件:

 @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        NettyClient.doReConnect();//执行重连
    }
 public static void doReConnect(){
        if (channel != null && channel.isActive()) {
            return;
        }
        ChannelFuture future = b.connect(host, port);//b是Bootstrap
        future.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture futureListener) throws Exception {
                if (futureListener.isSuccess()) {
                    channel = futureListener.channel();//重连成功               
                } else {                    
                  futureListener.channel().eventLoop().schedule(new Runnable() {//十秒后重新连接
                        @Override
                        public void run() {
                            doReConnet();
                        }
                    }, 10, TimeUnit.SECONDS);
                }
            }
        });

    }

三、一些心得笔记

Bootstrap:用于指导客户端使用channel的引导程序,TCP连接可以使用提供的connect执行连接。
EventLoopGroup: 工作组,用于调度对应的处理channel到对应的事件去。


1.感谢https://segmentfault.com/a/1190000006931568#articleHeader6,学习了不少。
2.api文档很完善,http://netty.io/4.1/api/index.html
3.转载注明:https://blog.csdn.net/u014614038/article/details/80263713

猜你喜欢

转载自blog.csdn.net/u014614038/article/details/80263713