Netty学习2.2——客户端

本文介绍Netty客户端的设计与实现过程,重点讲解了如何利用SimpleChannelInboundHandler处理消息,对比了其与ChannelInboundHandlerAdapter的区别。通过具体代码示例展示了客户端的启动流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Echo客户端将会:

(1)连接到服务器;

(2)发送一个或者多个消息;

(3)对于每个消息,等待并接收从服务器发回的相同的消息;

(4)关闭连接。

1、通过ChannelHandler实现客户端逻辑

如同服务器,客户端将拥有一个用来处理数据的ChannelInboundHandler。在这个场景下,你将扩展SimpleChannelInboundHandler类以处理所有必须的任务,这就要求重写以下方法:

channelActive()——在到服务器的连接已经建立后将被调用;

channelRead()——当从服务器接收到一条消息时被调用;

exceptionCaught()——在处理过程中引发异常时被调用。

//标记该类的实例可以被多个Channel共享
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx){
        //当被通知Channel是活跃的时候,发送一条消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        //记录已接收消息的存储
        System.out.println("Client received:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
        //在发生异常时,记录错误并关闭Channel
        cause.printStackTrace();
        ctx.close();
    }
}

(1)重写了channelActive()方法,其将在一个建立连接时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串“Netty rocks!”的字节缓冲区。

(2)重写了channelRead()方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器接收的消息可能会被分块接收。也就是说,如果服务器发送了5字节,那么不能保证这5字节会被一次性接收。即使是对于这么少量的数据,channelRead()方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf(Netty的字节容器),第二次使用一个持有2字节的ByteBuf。作为一个面向流的协议,TCP保证了字节数组将会按照服务器发送他们的顺序被接收。

(3)重写了exceptionCaught()。关闭Channel,终止到服务器的连接,记录Throwable。

SimpleChannelInboundHandler与ChannelInboundHandler

为什么在客户端使用的是SimpleChannelInboundHandler,而在EchoServerHandler中所使用ChannelInboundHandlerAdapter?这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。

在客户端,当channelRead()方法完成时,你已经有了消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。

在EchoServerHandler中,你仍然需要将传入消息回送给发送者,而write()操作时异步地,直到channelRead()方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息,

消息在EchoServerHandler的channelReadComplete()方法中,当writeAndFlush()方法被调用时被释放。

SimpleChannelInboundHandler与ChannelInboundHandler
    为什么在客户端使用的是SimpleChannelInboundHandler,而在EchoServerHandler中所使用ChannelInboundHandlerAdapter?这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。
    在客户端,当channelRead()方法完成时,你已经有了消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。
    在EchoServerHandler中,你仍然需要将传入消息回送给发送者,而write()操作时异步地,直到channelRead()方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息,
    消息在EchoServerHandler的channelReadComplete()方法中,当writeAndFlush()方法被调用时被释放。

2.引导客户端

引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址,也就是这里的Echo服务器地址,而不是绑定要一个一直被监听的端口。

客户端的主类:

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();
        try{
            //创建Bootstrap
            Bootstrap b = new Bootstrap();
            //指定EventLoopGroup以处理客户端事件;需要使用于NIO的实现
            b.group(group)
                    //适用于NIO传输的Channel类型
                    .channel(NioSocketChannel.class)
                    //设置服务器的InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    //在创建ChannelPipeline中添加一个EchoClientHandler实例
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            channel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            //连接到远程节点,阻塞等待直到连接完成
            ChannelFuture future = b.connect().sync();
            //阻塞,直到Channel关闭
            future.channel().closeFuture().sync();
        } finally {
            //关闭线程池并且释放所有的资源
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        if(args.length != 2){
            System.out.println("Usage:" + EchoClient.class.getSimpleName() + "<host> <port>");
            return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

为初始化客户端,创建了一个Bootstrap实例;

为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;

为服务器连接创建了一个InetSocketAddress实例;

当连接被建立时,一个EchoClientHandler实例会被安装到(该Channel的)ChannelPipeline中;

在一切都设置完成后,调用BootStrap.connect()方法连接到远程节点。

2025-07-15 16:43:42.807 api-gateway [reactor-http-epoll-4] ERROR o.s.b.a.w.r.e.AbstractErrorWebExceptionHandler - [3bfa5d00-4] 500 Server Error for HTTP GET "/" java.lang.NullPointerException: null at com.iflytek.ai.platform.gateway.handler.AbstractApiSecurityHandler.getServiceName(AbstractApiSecurityHandler.java:208) Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): |_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain] |_ checkpoint ⇢ HTTP GET "/" [ExceptionHandlingWebHandler] Stack trace: at com.iflytek.ai.platform.gateway.handler.AbstractApiSecurityHandler.getServiceName(AbstractApiSecurityHandler.java:208) at com.iflytek.ai.platform.gateway.handler.AbstractApiSecurityHandler.handle(AbstractApiSecurityHandler.java:116) at com.iflytek.ai.platform.gateway.mapping.ApiHandlerMapping.getHandler(ApiHandlerMapping.java:45) at org.springframework.web.reactive.DispatcherHandler.lambda$handle$0(DispatcherHandler.java:143) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:367) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:211) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:161) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4110) at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172) at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55) at reactor.netty.http.server.HttpServerHandle.onStateChange(HttpServerHandle.java:64) at reactor.netty.tcp.TcpServerBind$ChildObserver.onStateChange(TcpServerBind.java:228) at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:465) at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:90) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355) at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:170) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355) at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
07-16
<think>嗯,用户遇到了Spring Cloud Gateway中的NullPointerException问题,具体发生在AbstractApiSecurityHandler.getServiceName方法中。这个错误通常与服务名称获取失败有关,需要系统性地排查几个关键点。 首先想到服务注册与发现环节。如果服务没有正确注册到Consul或Nacos等注册中心,Gateway在路由时就无法解析serviceId。用户需要检查三方面:服务实例的spring.application.name配置是否正确,注册中心连接是否正常,以及服务健康状态是否通过检查。比如有时防火墙会阻断注册中心的通信端口。 接着是路由配置问题。用户可能在YAML中配置了类似lb://SERVICE-NAME的URI,但SERVICE-NAME与实际注册名不一致。建议用户用actuator/gateway/routes端点核对路由目标服务名,或者检查Predicate是否匹配了不存在的服务。 第三个常见原因是版本兼容性。从引用[2][3]看,早期Gateway版本确实存在并发导致的NPE问题。如果用户版本低于2.2.1,应该优先升级——特别是NettyRoutingFilter的修复很关键。升级时要注意spring-boot-starter-parent的配套版本,比如Gateway 2.2.1需要Boot 2.2.x。 如果问题依旧,就要深入调试了。开启debug日志可能发现服务发现客户端的异常。另外引用[4]提到的SkyWalking插件也值得注意,它的拦截器可能会干扰上下文,可以尝试暂时移除agent来验证。 最后考虑自定义组件的影响。用户使用的AbstractApiSecurityHandler可能是安全模块的一部分,需要确认是否重写了getServiceName方法。曾见过有开发者误将null作为服务名返回的情况,这种就需要检查自定义逻辑的健壮性。 在补充建议里,用户可能需要检查服务间通信证书是否过期,或者负载均衡器缓存是否残留错误路由。这些细节问题往往被忽略但影响很大。</think>针对 Spring Cloud Gateway 中出现的 `NullPointerException at AbstractApiSecurityHandler.getServiceName` 错误,以下是系统性的排查和解决方案: --- ### 一、核心原因分析 该异常通常发生在以下场景: 1. **服务注册问题** 目标微服务未正确注册到服务发现组件(如 Nacos、Consul),导致 Gateway 无法解析 `serviceId`。 2. **路由配置错误** `uri` 配置的服务名与注册中心记录不一致(例如大小写敏感问题)。 3. **版本兼容性问题** 特定版本的 Spring Cloud Gateway 存在并发缺陷(如引用[2][3]所示)。 4. **自定义组件缺陷** `AbstractApiSecurityHandler` 或其子类未正确处理空服务名场景。 --- ### 二、解决方案 #### ✅ 1. 验证服务注册状态 ```yaml # 检查目标服务的配置 spring: application: name: order-service # 确保此处名称与路由配置一致 cloud: nacos: discovery: server-addr: localhost:8848 ``` - 访问注册中心控制台(如 Nacos `:8848/nacos`),确认目标服务处于 **UP** 状态。 - 若服务未注册,检查网络连接或注册中心配置[^1]。 #### ✅ 2. 修正路由配置 ```yaml spring: cloud: gateway: routes: - id: order_route uri: lb://order-service # lb:// 后必须匹配注册的服务名 predicates: - Path=/api/orders/** ``` - 通过 `/actuator/gateway/routes` 端点确认路由信息是否生效。 - 使用 **Postman** 测试路由:`GET /actuator/gateway/routes`。 #### ✅ 3. 升级 Gateway 版本 > **关键修复**:早期版本(如 2.0.0)存在并发 NPE 缺陷(引用[2][3]) ```xml <!-- 升级至稳定版本 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>2.2.1.RELEASE</version> <!-- 或更高版本 --> </dependency> ``` 升级后观察日志,确认 `NettyRoutingFilter` 相关错误消失(引用[2])。 #### ✅ 4. 检查自定义安全组件 若使用了安全模块(如 `AbstractApiSecurityHandler`): ```java public class CustomSecurityHandler extends AbstractApiSecurityHandler { @Override public String getServiceName(Route route) { // 添加空值保护 return Optional.ofNullable(route) .map(Route::getId) .orElse("default-service"); // 提供兜底值 } } ``` - 重写 `getServiceName()` 方法时,**必须处理 `route` 为 null 的情况**。 #### ✅ 5. 排查第三方插件冲突 - 禁用 SkyWalking 等 APM 插件(引用[4]),验证是否因字节码增强导致上下文丢失: ```bash java -jar your-gateway.jar --skywalking.agent.enable=false ``` - 检查插件版本兼容性,更新至支持当前 Gateway 的版本。 --- ### 三、补充建议 1. **启用详细日志** 在 `application.yml` 中添加: ```yaml logging: level: org.springframework.cloud.gateway: DEBUG org.springframework.cloud.client.discovery: TRACE ``` 2. **检查依赖冲突** 执行 `mvn dependency:tree | findstr "gateway"`,排除重复依赖。 --- ### 📌 总结步骤 1. 升级 Gateway 至 **2.2.1+** 解决已知并发缺陷 2. 确认目标服务在注册中心状态为 **UP** 3. 验证路由配置中 `uri` 的服务名与注册名**完全一致** 4. 在自定义 `getServiceName()` 中添加**空值保护** 5. 临时禁用 APM 插件排除干扰 > 若问题仍存,在 `AbstractApiSecurityHandler.getServiceName()` 入口添加断点,观察 `Route` 对象是否初始化完成。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值