Netty 客户端与服务端收发消息demo

客户端与服务端收发消息,要实现的具体功能是:在控制台输入一条消息之后按回车键,校验完客户端的登录状态之后,把消息发送到服务端;服务端收到消息之后打印,并向客户端发送一条消息,客户端收到消息之后打印。

 

客户端NettyClient

import com.xc.xcspringboot.x_netty.client.handler.*;
import com.xc.xcspringboot.x_netty.protocol.PacketCodec;
import com.xc.xcspringboot.x_netty.protocol.request.MessageRequestPacket;
import com.xc.xcspringboot.x_netty.util.LoginUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.util.Date;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class NettyClient {

    private static final int MAX_RETRY = 5;
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8000;

    public static void main(String[] args) {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        // 引导器引导启动
        bootstrap
                .group(workerGroup) // 指定线程模型,驱动连接的数据读写
                .channel(NioSocketChannel.class) // 指定IO模型为NioSocketChannel,表示IO模型为NIO
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 表示连接的超时时间,超过这个时间,如果仍未连接到服务端,则表示连接失败。
                .option(ChannelOption.SO_KEEPALIVE, true) // 表示是否开启TCP底层心跳机制,true表示开启。
                .option(ChannelOption.TCP_NODELAY, true) // 表示是否开始Nagle算法,true表示关闭,false表示开启。
                .handler(new ChannelInitializer<Channel>() { // 给引导类指定一个Handler,主要定义连接的业务处理逻辑
                    @Override
                    protected void initChannel(Channel channel) {
//                        channel.pipeline().addLast(new StringEncoder());
//                        channel.pipeline().addLast(new FirstClientHandler());
                        channel.pipeline().addLast(new ClientHandler());
                    }
                });

        // 建立通道
        /*
         * 配置完线程模型、IO模型、业务处理逻辑之后,调用connect方法进行连接,
         * 可以看到connect方法有两个参数,第一个参数可以填写IP或者域名,第二个参数填写端口号。
         * 由于connect方法返回的是一个Future,也就是说这个方法是异步的,通过addListener方法可以监听连接是否成功,进而打印连接信息。
         */
//        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

        connect(bootstrap, HOST, PORT, MAX_RETRY);

    }

    private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
        bootstrap.connect(host, port).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 连接成功,启动控制台线程……");
                Channel channel = ((ChannelFuture) future).channel();
                startConsoleThread(channel);
            } else if (retry == 0) {
                System.err.println("重试次数已用完,放弃连接!");
            } else {
                // 第几次重连
                int order = (MAX_RETRY - retry) + 1;
                // 本次重连的间隔
                int delay = 1 << order;
                System.err.println(new Date() + ": 连接失败,第" + order + "次重连…… delay:" + delay);
                /*
                 * 定时任务调用的是bootstrap.config().group().schedule(),
                 * 其中bootstrap.config()这个方法返回的是BootstrapConfig,它是对Bootstrap配置参数的抽象,
                 * 然后bootstrap.config().group()返回的就是我们在一开始配置的线程模型workerGroup,
                 * 调用workerGroup的schedule方法即可实现定时任务逻辑。
                 */
                bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
            }
        });
    }

    /*private static void startConsoleThread(Channel channel) throws InterruptedException {
        while (true) {
            channel.writeAndFlush(new Date() + " Hello world");
            Thread.sleep(2000);
        }
    }*/

    private static void startConsoleThread(Channel channel) {
        new Thread(() -> {
            while (!Thread.interrupted()) {
                if (LoginUtil.hasLogin(channel)) { // 在判断是否登录成功的时候取出这个标志位
                    System.out.println("输入消息发送至服务端: ");
                    Scanner SC = new Scanner(System.in);
                    String line = SC.nextLine();
                    MessageRequestPacket packet = new MessageRequestPacket();
                    packet.setMessage(line);
                    ByteBuf buffer = channel.alloc().buffer();
                    PacketCodec.INSTANCE.encode(buffer, packet);
                    channel.writeAndFlush(buffer);
                }
            }
        }).start();
    }

}

 

ClientHandler

import com.xc.xcspringboot.x_netty.protocol.Packet;
import com.xc.xcspringboot.x_netty.protocol.PacketCodec;
import com.xc.xcspringboot.x_netty.protocol.request.LoginRequestPacket;
import com.xc.xcspringboot.x_netty.protocol.response.LoginResponsePacket;
import com.xc.xcspringboot.x_netty.protocol.response.MessageResponsePacket;
import com.xc.xcspringboot.x_netty.util.LoginUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class ClientHandler extends ChannelInboundHandlerAdapter {

    // 当 Channel 已经连接/绑定并且已经就绪时调用
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println(new Date() + ":客户端开始登录");
        //创建登录对象
        LoginRequestPacket loginRequestPacket = new LoginRequestPacket();
//        loginRequestPacket.setUserId(UUID.randomUUID().toString());
        loginRequestPacket.setUserName("flash");
        loginRequestPacket.setPassword("pwd");
        //编码
        ByteBuf buffer = ctx.alloc().buffer();
        PacketCodec.INSTANCE.encode(buffer, loginRequestPacket);
        // 写数据
        ctx.channel().writeAndFlush(buffer);
    }

    // 当从 Channel 读取数据时被调用
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;
        Packet packet = PacketCodec.INSTANCE.decode(byteBuf);
        if (packet instanceof LoginResponsePacket) { // 登录逻辑
            LoginResponsePacket loginResponsePacket = (LoginResponsePacket) packet;
            if (loginResponsePacket.isSuccess()) {
                LoginUtil.markAsLogin(ctx.channel()); // 在登录成功之后,给Channel绑定一个登录成功的标志位
                System.out.println(new Date() + ":客户端登录成功");
            } else {
                System.out.println(new Date() + ":客户端登录失败,原因:" + loginResponsePacket.getReason());
            }
        } else if (packet instanceof MessageResponsePacket) { // 消息逻辑
            MessageResponsePacket messageResponsePacket = (MessageResponsePacket) packet;
            System.out.println(new Date() + ":收到服务端的消息:" + messageResponsePacket.getMessage());
        }
    }

}

  

服务端

import com.xc.xcspringboot.x_netty.server.handler.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.util.Date;

public class NettyServer {

    private static final int PORT = 8000;

    public static void main(String[] args) {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(); // bossGroup表示监听端口,接收新连接的线程组
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(); // workerGroup表示处理每一个连接的数据读写的线程组

        ServerBootstrap serverBootstrap = new ServerBootstrap(); // 引导类ServerBootstrap,这个类将引导服务端的启动工作
        serverBootstrap
                .group(boosGroup, workerGroup) // .group(bossGroup,workerGroup)给引导类配置两大线程组
                .channel(NioServerSocketChannel.class) // 指定服务端的IO模型为NIO NioServerSocketChannel是对NIO类型连接的抽象
                .option(ChannelOption.SO_BACKLOG, 1024) // 表示系统用于临时存放已完成三次握手的请求的队列的最大长度
                .childOption(ChannelOption.SO_KEEPALIVE, true) // 表示是否开启TCP底层心跳机制,true表示开启。
                .childOption(ChannelOption.TCP_NODELAY, true) // 表示是否开启Nagle算法,true表示关闭,false表示开启
                .handler(new ChannelInitializer<NioServerSocketChannel>() { // handler()方法用于指定在服务端启动过程中的一些逻辑
                    @Override
                    protected void initChannel(NioServerSocketChannel ch) {
                        System.out.println("服务端启动过程中...");
                    }
                })
                .childHandler(new ChannelInitializer<NioSocketChannel>() { // childHandler()方法,给这个引导类创建一个ChannelInitializer,主要是定义后续每个连接的数据读写
                    protected void initChannel(NioSocketChannel ch) { // 泛型参数NioSocketChannel,这个类就是Netty对NIO类型连接的抽象
                        /*ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });*/
//                        ch.pipeline().addLast(new FirstServerHandler());
                        ch.pipeline().addLast(new ServerHandler());
                    }
                });
        /*
         * 要启动一个Netty服务端,必须要指定三类属性,分别是线程模型、IO模型、连接读写处理逻辑。
         * 有了这三者,之后再调用bind(8000),就可以在本地绑定一个8000端口启动服务端。
         */
//                .bind(8000);

        // 给这个ChannelFuture添加一个监听器GenericFutureListener
        serverBootstrap.bind(PORT).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 端口[" + PORT + "]绑定成功!");
            } else {
                System.err.println("端口[" + PORT + "]绑定失败!");
            }
        });
    }

}

  

ServerHandler

import com.xc.xcspringboot.x_netty.protocol.Packet;
import com.xc.xcspringboot.x_netty.protocol.PacketCodec;
import com.xc.xcspringboot.x_netty.protocol.request.LoginRequestPacket;
import com.xc.xcspringboot.x_netty.protocol.request.MessageRequestPacket;
import com.xc.xcspringboot.x_netty.protocol.response.LoginResponsePacket;
import com.xc.xcspringboot.x_netty.protocol.response.MessageResponsePacket;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    // 当从 Channel 读取数据时被调用
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf requestByteBuf = (ByteBuf) msg;
        Packet packet = PacketCodec.INSTANCE.decode(requestByteBuf); // 解码
        ByteBuf buffer = ctx.alloc().buffer();
        if (packet instanceof LoginRequestPacket) { // 处理登录
            LoginRequestPacket loginRequestPacket = (LoginRequestPacket) packet;
            LoginResponsePacket loginResponsePacket = new LoginResponsePacket();
            loginResponsePacket.setVersion(packet.getVersion());
            //登录校验
            if (valid(loginRequestPacket)) {
                //校验成功
                loginResponsePacket.setSuccess(true);
            } else {
                //校验失败
                loginResponsePacket.setReason("账号密码校验失败");
                loginResponsePacket.setSuccess(false);
            }
            //编码
            PacketCodec.INSTANCE.encode(buffer, loginResponsePacket);
            ctx.channel().writeAndFlush(buffer);
        } else if (packet instanceof MessageRequestPacket) { //处理消息
            MessageRequestPacket messageRequestPacket = ((MessageRequestPacket) packet);
            System.out.println(new Date() + ":收到客户端消息: " + messageRequestPacket.getMessage());
            MessageResponsePacket messageResponsePacket = new MessageResponsePacket();
            messageResponsePacket.setMessage("服务端回复[" + messageRequestPacket.getMessage() + "] ");
            PacketCodec.INSTANCE.encode(buffer, messageResponsePacket);
            ctx.channel().writeAndFlush(buffer);
        }
    }


    private boolean valid(LoginRequestPacket loginRequestPacket) {
        return true;
    }

}

  

 

posted @ 2024-05-22 08:50  草木物语  阅读(113)  评论(0编辑  收藏  举报