8,客户端开发面试题目

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

            byteBuf.resetReaderIndex();
            return;
        }
        // 读取消息体并进行反序列化
        byte[] payload = new byte[size];
        byteBuf.readBytes(payload);
        // 这里根据消息头中的extraInfo部分选择相应的序列化和压缩方式
        Serialization serialization = SerializationFactory.get(extraInfo);
        Compressor compressor = CompressorFactory.get(extraInfo);
        if (Constants.isRequest(extraInfo)) {
            // 得到消息体
            body = serialization.deserialize(compressor.unCompress(payload),
                    Request.class);
        } else {
            // 得到消息体
            body = serialization.deserialize(compressor.unCompress(payload),
                    Response.class);
        }
    }
    // 将上面读取到的消息头和消息体拼装成完整的Message并向后传递
    Header header = new Header(magic, version, extraInfo, messageId, size);
    Message message = new Message(header, body);
    out.add(message);
}

}



public class DemoRpcEncoder extends MessageToByteEncoder {

@Override
protected void encode(ChannelHandlerContext ctx,
                      Message message, ByteBuf byteBuf) throws Exception {
    Header header = message.getHeader();
    // 依次序列化消息头中的魔数、版本、附加信息以及消息ID
    byteBuf.writeShort(header.getMagic());
    byteBuf.writeByte(header.getVersion());
    byteBuf.writeByte(header.getExtraInfo());
    byteBuf.writeLong(header.getMessageId());
    Object content = message.getContent();
    if (Constants.isHeartBeat(header.getExtraInfo())) {
        byteBuf.writeInt(0); // 心跳消息,没有消息体,这里写入0
        return;
    }
    // 按照extraInfo部分指定的序列化方式和压缩方式进行处理
    Serialization serialization = SerializationFactory.get(header.getExtraInfo());
    Compressor compressor = CompressorFactory.get(header.getExtraInfo());
    byte[] payload = compressor.compress(serialization.serialize(content));
    byteBuf.writeInt(payload.length); // 写入消息体长度
    byteBuf.writeBytes(payload); // 写入消息体
}

}


## transport 相关实现


正如前文介绍 Netty 线程模型的时候提到,我们不能在 Netty 的 I/O 线程中执行耗时的业务逻辑。在 Demo RPC 框架的 Server 端接收到请求时,首先会通过上面介绍的 DemoRpcDecoder 反序列化得到请求消息,之后我们会通过一个自定义的 ChannelHandler(DemoRpcServerHandler)将请求提交给业务线程池进行处理。


在 Demo RPC 框架的 Client 端接收到响应消息的时候,也是先通过 DemoRpcDecoder 反序列化得到响应消息,之后通过一个自定义的 ChannelHandler(DemoRpcClientHandler)将响应返回给上层业务。


DemoRpcServerHandler 和 DemoRpcClientHandler 都继承自 SimpleChannelInboundHandler,如下图所示:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/8afba4614f4a4e7a9eea3a0cf438c412.png)  
 下面我们就来看一下这两个自定义的 ChannelHandler 实现:



public class DemoRpcServerHandler extends SimpleChannelInboundHandler<Message> {

// 业务线程池
private static Executor executor = Executors.newCachedThreadPool();

@Override
protected void channelRead0(final ChannelHandlerContext channelHandlerContext, Message<Request> message) throws Exception {
    byte extraInfo = message.getHeader().getExtraInfo();
    if (Constants.isHeartBeat(extraInfo)) { // 心跳消息,直接返回即可
        channelHandlerContext.writeAndFlush(message);
        return;
    }
    // 非心跳消息,直接封装成Runnable提交到业务线程池
    executor.execute(new InvokeRunnable(message, channelHandlerContext));
}

}



public class DemoRpcClientHandler extends SimpleChannelInboundHandler<Message> {

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Message<Response> message) throws Exception {
    NettyResponseFuture responseFuture =
            Connection.IN\_FLIGHT\_REQUEST\_MAP.remove(message.getHeader().getMessageId());
    Response response = message.getContent();
    // 心跳消息特殊处理
    if (response == null && Constants.isHeartBeat(message.getHeader().getExtraInfo())) {
        response = new Response();
        response.setCode(Constants.HEARTBEAT\_CODE);
    }
    responseFuture.getPromise().setSuccess(response.getResult());
}

}


注意,这里有两个点需要特别说明一下。一个点是 Server 端的 InvokeRunnable,在这个 Runnable 任务中会根据请求的 serviceName、methodName 以及参数信息,调用相应的方法:



class InvokeRunnable implements Runnable {

private ChannelHandlerContext ctx;
private Message<Request> message;

public InvokeRunnable(Message<Request> message, ChannelHandlerContext ctx) {
    this.message = message;
    this.ctx = ctx;
}

@Override
public void run() {
    Response response = new Response();
    Object result = null;
    try {
        Request request = message.getContent();
        String serviceName = request.getServiceName();
        // 这里提供BeanManager对所有业务Bean进行管理,其底层在内存中维护了
        // 一个业务Bean实例的集合。感兴趣的同学可以尝试接入Spring等容器管
        // 理业务Bean
        Object bean = BeanManager.getBean(serviceName);
        // 下面通过反射调用Bean中的相应方法
        Method method = bean.getClass().getMethod(request.getMethodName(), request.getArgTypes());
        result = method.invoke(bean, request.getArgs());
    } catch (Exception e) {
        // 省略异常处理
    } finally {
    }
    Header header = message.getHeader();
    header.setExtraInfo((byte) 1);
    response.setResult(result); // 设置响应结果
    // 将响应消息返回给客户端
    ctx.writeAndFlush(new Message(header, response));
}

}


另一个点是 Client 端的 Connection,它是用来暂存已发送出去但未得到响应的请求,这样,在响应返回时,就可以查找到相应的请求以及 Future,从而将响应结果返回给上层业务逻辑,具体实现如下:



public class Connection implements Closeable {
private static AtomicLong ID_GENERATOR = new AtomicLong(0);
public static Map<Long, NettyResponseFuture>
IN_FLIGHT_REQUEST_MAP = new ConcurrentHashMap<>();
private ChannelFuture future;
private AtomicBoolean isConnected = new AtomicBoolean();
public Connection(ChannelFuture future, boolean isConnected) {
this.future = future;
this.isConnected.set(isConnected);
}
public NettyResponseFuture request(Message message, long timeOut) {
// 生成并设置消息ID
long messageId = ID_GENERATOR.incrementAndGet();
message.getHeader().setMessageId(messageId);
// 创建消息关联的Future
NettyResponseFuture responseFuture = new NettyResponseFuture(System.currentTimeMillis(),
timeOut, message, future.channel(), new DefaultPromise(new DefaultEventLoop()));
// 将消息ID和关联的Future记录到IN_FLIGHT_REQUEST_MAP集合中
IN_FLIGHT_REQUEST_MAP.put(messageId, responseFuture);
try {
future.channel().writeAndFlush(message); // 发送请求
} catch (Exception e) {
// 发送请求异常时,删除对应的Future
IN_FLIGHT_REQUEST_MAP.remove(messageId);
throw e;
}
return responseFuture;
}
// 省略getter/setter以及close()方法
}


我们可以看到,Connection 中没有定时清理 IN\_FLIGHT\_REQUEST\_MAP 集合的操作,在无法正常获取响应的时候,就会导致 IN\_FLIGHT\_REQUEST\_MAP 不断膨胀,最终 OOM。你也可以添加一个时间轮定时器,定时清理过期的请求消息,这里我们就不再展开讲述了。


完成自定义 ChannelHandler 的编写之后,我们需要再定义两个类—— DemoRpcClient 和 DemoRpcServer,分别作为 Client 和 Server 的启动入口。DemoRpcClient 的实现如下:



public class DemoRpcClient implements Closeable {

protected Bootstrap clientBootstrap;
protected EventLoopGroup group;
private String host;
private int port;

public DemoRpcClient(String host, int port) {
    this.host = host;
    this.port = port;
    // 创建并配置客户端Bootstrap
    clientBootstrap = new Bootstrap();
    group = NettyEventLoopFactory.eventLoopGroup(Constants.DEFAULT\_IO\_THREADS, "NettyClientWorker");
    clientBootstrap.group(group)
            .option(ChannelOption.TCP\_NODELAY, true)
            .option(ChannelOption.SO\_KEEPALIVE, true)
            .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
            .channel(NioSocketChannel.class) // 创建的Channel类型
            // 指定ChannelHandler的顺序
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("demo-rpc-encoder", new DemoRpcEncoder());
                    ch.pipeline().addLast("demo-rpc-decoder", new DemoRpcDecoder());
                    ch.pipeline().addLast("client-handler", new DemoRpcClientHandler());
                }
            });
}


public ChannelFuture connect() {
    // 连接指定的地址和端口
    ChannelFuture connect = clientBootstrap.connect(host, port);
    connect.awaitUninterruptibly();
    return connect;
}

@Override
public void close() {
    group.shutdownGracefully();
}

}


通过 DemoRpcClient 的代码我们可以看到其 ChannelHandler 的执行顺序如下:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d75908f870364401afbd15c63eaf9bee.png)  
 客户端 ChannelHandler 结构图


另外,在创建EventLoopGroup时并没有直接使用NioEventLoopGroup,而是在 NettyEventLoopFactory 中根据当前操作系统进行选择,对于 Linux 系统,会使用 EpollEventLoopGroup,其他系统则使用 NioEventLoopGroup。


接下来我们再看DemoRpcServer 的具体实现:



public class DemoRpcServer {

private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ServerBootstrap serverBootstrap;
private Channel channel;
protected int port;

public DemoRpcServer(int port) throws InterruptedException {
    this.port = port;
    // 创建boss和worker两个EventLoopGroup,注意一些小细节,
    // workerGroup 是按照中的线程数是按照 CPU 核数计算得到的
    bossGroup = NettyEventLoopFactory.eventLoopGroup(1,
            "NettyServerBoss");
    workerGroup = NettyEventLoopFactory.eventLoopGroup(
            Math.min(Runtime.getRuntime().availableProcessors() + 1, 32),
            "NettyServerWorker");
    serverBootstrap = new ServerBootstrap().group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .option(ChannelOption.SO\_REUSEADDR, Boolean.TRUE)
            .childOption(ChannelOption.TCP\_NODELAY, Boolean.TRUE)
            .childOption(ChannelOption.SO\_KEEPALIVE, true) //设置保持活动连接状态
            .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
            // 指定每个Channel上注册的ChannelHandler以及顺序
            .handler(new LoggingHandler(LogLevel.INFO))
            .childHandler(
                    new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast("demp-rpc-decoder", new DemoRpcDecoder());
                            ch.pipeline().addLast("demo-rpc-encoder", new DemoRpcEncoder());
                            ch.pipeline().addLast("server-handler", new DemoRpcServerHandler());
                        }
                    });
}

public ChannelFuture start() throws InterruptedException {
    // 监听指定的端口
    ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
    channelFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (channelFuture.isSuccess()) {
                System.out.println("监听端口 6668 成功");
            } else {
                System.out.println("监听端口 6668 失败");
            }
        }
    });

    channel = channelFuture.channel();
    channel.closeFuture().sync();
    return channelFuture;
}


public void startAndWait() throws InterruptedException {
    try {
        channel.closeFuture().await();
    } catch (InterruptedException e) {
        Thread.interrupted();
    }
}


public void shutdown() throws InterruptedException {
    channel.close().sync();
    if (bossGroup != null)
        bossGroup.shutdownGracefully().awaitUninterruptibly(15000);
    if (workerGroup != null)
        workerGroup.shutdownGracefully().awaitUninterruptibly(15000);
}

}


通过对 DemoRpcServer 实现的分析,我们可以知道每个 Channel 上的 ChannelHandler 顺序如下:


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/dd077e76d3044b68bc8640a4b64d564f.png)  
 服务端 ChannelHandler 结构图


## registry 相关实现


介绍完客户端和服务端的通信之后,我们再来看简易 RPC 框架的另一个基础能力——服务注册与服务发现能力,对应 demo-rpc 项目源码中的 registry 包。


registry 包主要是依赖 Apache Curator 实现了一个简易版本的 ZooKeeper 客户端,并基于 ZooKeeper 实现了注册中心最基本的两个功能:Provider 注册以及 Consumer 订阅。


这里我们先定义一个 Registry 接口,其中提供了注册以及查询服务实例的方法,如下图所示:



public interface Registry {

void registerService(ServiceInstance<T> service) throws Exception;

void unregisterService(ServiceInstance<T> service) throws Exception;

List<ServiceInstance<T>> queryForInstances(String name) throws Exception;

}


ZooKeeperRegistry 是基于 curator-x-discovery 对 Registry 接口的实现类型,其中封装了之前课时介绍的 ServiceDiscovery,并在其上添加了 ServiceCache 缓存提高查询效率。ZooKeeperRegistry 的具体实现如下:



public class ZookeeperRegistry implements Registry {

private Map<String, List<ServiceInstanceListener<T>>> listeners = Maps.newConcurrentMap();

private InstanceSerializer serializer = new JsonInstanceSerializer<>(ServerInfo.class);

private ServiceDiscovery<T> serviceDiscovery;

private ServiceCache<T> serviceCache;

private String address = "localhost:2181";

public void start() throws Exception {
    String root = "/demo/rpc";
    // 初始化CuratorFramework
    CuratorFramework client = CuratorFrameworkFactory.newClient(address, new ExponentialBackoffRetry(1000, 3));
    client.start();  // 启动Curator客户端
    // client.createContainers(root);

    // 初始化ServiceDiscovery
    serviceDiscovery = ServiceDiscoveryBuilder.builder(ServerInfo.class)
            .client(client).basePath(root)
            .serializer(serializer)
            .build();
    serviceDiscovery.start(); // 启动ServiceDiscovery

    // 创建ServiceCache,监Zookeeper相应节点的变化,也方便后续的读取
    serviceCache = serviceDiscovery.serviceCacheBuilder()
            .name("/demoService")
            .build();

// client.start(); // 启动Curator客户端
client.blockUntilConnected(); // 阻塞当前线程,等待连接成功
serviceDiscovery.start(); // 启动ServiceDiscovery
serviceCache.start(); // 启动ServiceCache
}

@Override
public void registerService(ServiceInstance<T> service) throws Exception {
    serviceDiscovery.registerService(service);
}

@Override
public void unregisterService(ServiceInstance service) throws Exception {
    serviceDiscovery.unregisterService(service);
}

@Override
public List<ServiceInstance<T>> queryForInstances(String name) throws Exception {
    // 直接根据name进行过滤ServiceCache中的缓存数据
    return serviceCache.getInstances().stream()
            .filter(s -> s.getName().equals(name))
            .collect(Collectors.toList());
}

}


通过对 ZooKeeperRegistry的分析可以得知,它是基于 Curator 中的 ServiceDiscovery 组件与 ZooKeeper 进行交互的,并且对 Registry 接口的实现也是通过直接调用 ServiceDiscovery 的相关方法实现的。在查询时,直接读取 ServiceCache 中的缓存数据,ServiceCache 底层在本地维护了一个 ConcurrentHashMap 缓存,通过 PathChildrenCache 监听 ZooKeeper 中各个子节点的变化,同步更新本地缓存。这里我们简单看一下 ServiceCache 的核心实现:



public class ServiceCacheImpl implements ServiceCache,
PathChildrenCacheListener{//实现PathChildrenCacheListener接口
// 关联的ServiceDiscovery实例
private final ServiceDiscoveryImpl discovery;
// 底层的PathChildrenCache,用于监听子节点的变化
private final PathChildrenCache cache;
// 本地缓存
private final ConcurrentMap<String, ServiceInstance> instances
= Maps.newConcurrentMap();
public List<ServiceInstance> getInstances(){ // 返回本地缓存内容
return Lists.newArrayList(instances.values());
}
public void childEvent(CuratorFramework client,
PathChildrenCacheEvent event) throws Exception{
switch(event.getType()){
case CHILD_ADDED:
case CHILD_UPDATED:{
addInstance(event.getData(), false); // 更新本地缓存
notifyListeners = true;
break;
}
case CHILD_REMOVED:{ // 更新本地缓存
instances.remove(instanceIdFromData(event.getData()));
notifyListeners = true;
break;
}
}
… // 通知ServiceCache上注册的监听器
}
}


## proxy 相关实现


在简易版 Demo RPC 框架中,Proxy 主要是为 Client 端创建一个代理,帮助客户端程序屏蔽底层的网络操作以及与注册中心之间的交互。


简易版 Demo RPC 使用 JDK 动态代理的方式生成代理,这里需要编写一个 InvocationHandler 接口的实现,即下面的 DemoRpcProxy。其中有两个核心方法:一个是 newInstance() 方法,用于生成代理对象;另一个是 invoke() 方法,当调用目标对象的时候,会执行 invoke() 方法中的代理逻辑。


下面是 DemoRpcProxy 的具体实现:



public class DemoRpcProxy implements InvocationHandler {

private String serviceName; // 需要代理的服务(接口)名称

public Map<Method, Header> headerCache = new ConcurrentHashMap<>();

// 用于与Zookeeper交互,其中自带缓存
private Registry<ServerInfo> registry;

public DemoRpcProxy(String serviceName,
                    Registry<ServerInfo> registry) throws Exception {
    this.serviceName = serviceName;
    this.registry = registry;
}

public static <T> T newInstance(Class<T> clazz, Registry<ServerInfo> registry) throws Exception {
    // 创建代理对象
    return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[]{clazz},
            new DemoRpcProxy("demoService", registry));
}


@Override

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

erverInfo> registry) throws Exception {
// 创建代理对象
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{clazz},
new DemoRpcProxy(“demoService”, registry));
}

@Override

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-ypLjST4T-1713270741765)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值