【三、使用Netty编写客户端】

本文介绍了如何使用Netty构建TCP客户端,包括设置Bootstrap、处理TCP拆包粘包问题、使用解码器和POJO对象传输数据。通过TimeClientHandler和自定义解码器确保数据完整,使数据以UnixTime对象的形式进行传输。
摘要由CSDN通过智能技术生成

目标

上一章通过时间服务器我们了解了如何从服务端写数据到客户端,本章节我们学习如何构建客户端,并接收服务端的数据

构建客户端

public class TimeClient {


    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            // 连接服务端.
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

}

构建客户端使用BootStrap类,与服务端不同,客户端事件循环只需要work group,其它设置与服务端基本类似。设置好handler后通过connect连接服务端的IP和端口

Handler代码实现

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg; // (1)
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

示例中读取数据为时间戳,为什么要减去2208988800L这个数值呢?(因为服务端写的时候添加了这个数值,废话);因为我们平时用的时间戳为Unix时间戳从1970/1/1开始,而示例中返回的其实是GPS时间戳时间从1980/1/6开始。而他们之间相差的秒数就是2208988800L这个数值

上面的示例中我们是通过TCP传输数据,由于TCP是基于流式的,所以我们很难界定一次传输的数据是否是完整的一次数据包。当读写数据时,handler中read函数接收到的数据可能是不完整或多个数据混在一起的(TCP的数据拆包,粘包问题)。因此我们要通过约定一个格式来分开数据,确保读取的数据都是正常的。

在handler中确保数据接收完整

修改上面的TimeClientHandler

	@Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        buf = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release();
        buf = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m);
        m.release();

        if (buf.readableBytes() >= 4) {
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }
  1. 连接创建时申请缓冲区,移除连接时要记得释放掉
  2. 在channelRead中把数据读取到buf中,并判断数据是否完整(时间戳为4个字节)
  3. 读取缓冲区完整的数据

通过解码器处理TCP拆包粘包

解码器的作用是收到数据后会根据decode方法中的代码,把数据拆分或组合成多个或单个消息

public class TimeDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            return;
        }
        out.add(in.readBytes(4));
    }
}

添加handler

ch.pipeline().addLast(new TimeDecoder(),new TimeClientHandler());
  1. 继承ByteToMessageDecoder类,将Byte数据转换成其它消息类型
  2. 重写decode方法,方法接收3个参数。ctx是上下文对象。in是收到的消息,out用于存放解码后的消息。
  3. 如果收到的消息可用数据小于4字节则不处理,继续读取。如果大于,则读取4个字节并添加到out中。
  4. 将TimeDecoder添加到Pipeline中,注意要在TimeClientHandler前添加,这样TimeClientHandler接收到的才是解码后的数据

使用POJO对象传输数据

前面的示例中我们都是通过字节来操作数据,这样很不直观,对于一些复杂的数据格式也非常麻烦,所以我们希望通过JAVA 的POJO对象来传输数据。
因为我们传输时使用的都是二进制来传输数据,要将字节数据转换成对象,自然而然我们会想到通过上面学习到的解码器来实现。

  1. 首先我们先创建一个POJO对象,以TimeServer的示例作为修改
public class UnixTime {

    private final long value;

    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }

    public UnixTime(long value) {
        this.value = value;
    }

    public long value() {
        return value;
    }

    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}
  1. 修改解码器,向out中添加UnixTime对象
public class TimeDecoderPojo extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            return;
        }
        out.add(new UnixTime(in.readUnsignedInt()));
    }
}

  1. 修改TimeClientHandler,接收到的数据不再是ByteBuf而是UnixTime对象
public class TimeClientHandlerPojo extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        UnixTime m = (UnixTime) msg;
        System.out.println(m);
        ctx.close();
    }
}

这里我们已经处理了接收端,那我们发送数据时也想用POJO对象怎么修改呢,下面是修改TimeServer的代码

  1. 添加编码器(处理读数据使用解码器,处理写数据当然用编码器)在写入前对数据进行编码
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int) m.value());
        ctx.write(encoded, promise);
    }
}

ChannelOutboundHandlerAdapter 是一个基础实现类,用于处理通道的写入
另外也可以通过MessageToByteEncoder 实现

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}
  1. 添加handler
ch.pipeline().addLast(new TimeServerHandler(),new TimeEncoder());

注意编码器应该放在业务处理handler,TimeServerHandler 之后
3. 发送数据

public class TimeServerHandlerPojo extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ChannelFuture f = ctx.writeAndFlush(new UnixTime());
        f.addListener(ChannelFutureListener.CLOSE);
    }
}

下一章节,我们学习通过Netty构建UDP应用

好的,我可以回答这个问题。以下是一个简单的使用JavaNetty编写TCP客户端代码的示例: ``` import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; public class TcpClient { private static final String HOST = "127.0.0.1"; private static final int PORT = 8080; public static void main(String[] args) throws Exception { Bootstrap b = new Bootstrap(); b.group(new NioEventLoopGroup()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new StringEncoder(), new TcpClientHandler()); } }); Channel ch = b.connect(HOST, PORT).sync().channel(); ch.writeAndFlush("Hello World!\r\n"); ch.closeFuture().sync(); } private static class TcpClientHandler extends SimpleChannelInboundHandler<String> { @Override public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println("Received message: " + msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } } ``` 该代码使用Netty的Bootstrap创建一个TCP客户端,并在连接后发送一条消息。接收到来自服务器的消息时,客户端会将其打印到控制台上。在客户端发生异常时,它将关闭连接并打印异常堆栈跟踪。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ThinkLess404

有问题可以私信交流

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值