米哈游Java面试被问:gRPC的HTTP/2流控制和消息分帧

一、HTTP/2 核心概念

1.1 HTTP/2 帧结构

java

复制

下载

复制代码
// HTTP/2 帧基础结构(RFC 7540)
public class Http2Frame {
    // 9字节的帧头
    private int length;         // 24位:负载长度 (0-2^24-1)
    private byte type;          // 8位:帧类型
    private byte flags;         // 8位:标志位
    private int streamId;       // 31位:流ID (0-2^31-1)
    
    // 帧负载 (0-16383字节)
    private byte[] payload;
}

// 帧类型枚举
public enum FrameType {
    DATA(0x0),           // 数据帧
    HEADERS(0x1),        // 头部帧
    PRIORITY(0x2),       // 优先级帧
    RST_STREAM(0x3),     // 流终止帧
    SETTINGS(0x4),       // 设置帧
    PUSH_PROMISE(0x5),   // 推送承诺帧
    PING(0x6),           // PING帧
    GOAWAY(0x7),         // GOAWAY帧
    WINDOW_UPDATE(0x8),  // 窗口更新帧
    CONTINUATION(0x9);   // 延续帧
}

1.2 HTTP/2 连接建立

java

复制

下载

复制代码
// HTTP/2 连接建立过程
public class Http2Connection {
    
    // 必须发送的连接前言(24字节)
    private static final byte[] CONNECTION_PREFACE = 
        "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes();
    
    public void establishConnection(Socket socket) throws IOException {
        // 1. TLS握手(如果使用HTTPS)
        SSLSocket sslSocket = (SSLSocket) socket;
        sslSocket.startHandshake();
        
        // 2. 发送连接前言
        socket.getOutputStream().write(CONNECTION_PREFACE);
        
        // 3. 发送SETTINGS帧
        sendSettingsFrame(socket);
        
        // 4. 等待对方SETTINGS帧
        readSettingsFrame(socket);
        
        // 5. 发送SETTINGS ACK
        sendSettingsAck(socket);
    }
    
    private void sendSettingsFrame(Socket socket) throws IOException {
        // 构建SETTINGS帧
        ByteBuffer buffer = ByteBuffer.allocate(9 + 6); // 帧头 + 一个设置项
        
        // 帧头
        buffer.putShort((short) 6);  // 长度
        buffer.put((byte) 0x4);      // 类型: SETTINGS
        buffer.put((byte) 0x0);      // 标志位
        buffer.putInt(0x0);          // 流ID: 0(连接级别)
        
        // 负载:设置初始窗口大小
        buffer.putShort((short) 0x4); // SETTINGS_INITIAL_WINDOW_SIZE
        buffer.putInt(65535);         // 初始窗口大小
        
        socket.getOutputStream().write(buffer.array());
    }
}

二、HTTP/2 流控制实现

2.1 流控制核心算法

java

复制

下载

复制代码
// HTTP/2 流控制实现
public class Http2FlowControl {
    
    // 流控制窗口状态
    private final Map<Integer, StreamWindow> streamWindows = new ConcurrentHashMap<>();
    private final AtomicInteger connectionWindow = new AtomicInteger(65535);
    
    // 流窗口状态
    static class StreamWindow {
        private final int streamId;
        private volatile int windowSize;        // 当前窗口大小
        private volatile int bytesSent;         // 已发送字节数
        private volatile int bytesReceived;     // 已接收字节数
        private volatile boolean blocked;       // 是否阻塞
        
        // 窗口更新等待队列
        private final Queue<Runnable> pendingWrites = new ConcurrentLinkedQueue<>();
        
        public StreamWindow(int streamId, int initialWindowSize) {
            this.streamId = streamId;
            this.windowSize = initialWindowSize;
        }
    }
    
    /**
     * 发送数据前的流控制检查
     */
    public synchronized boolean canSend(int streamId, int dataLength) {
        // 1. 检查连接级别窗口
        if (connectionWindow.get() < dataLength) {
            return false;
        }
        
        // 2. 检查流级别窗口
        StreamWindow window = streamWindows.get(streamId);
        if (window == null) {
            window = new StreamWindow(streamId, 65535);
            streamWindows.put(streamId, window);
        }
        
        if (window.windowSize < dataLength) {
            window.blocked = true;
            return false;
        }
        
        // 3. 更新窗口计数
        connectionWindow.addAndGet(-dataLength);
        window.windowSize -= dataLength;
        window.bytesSent += dataLength;
        
        return true;
    }
    
    /**
     * 接收数据后的窗口更新
     */
    public synchronized void onDataReceived(int streamId, int dataLength) {
        // 1. 更新流窗口
        StreamWindow window = streamWindows.get(streamId);
        if (window != null) {
            window.bytesReceived += dataLength;
            // 当数据被消费后,发送WINDOW_UPDATE
        }
        
        // 2. 更新连接窗口
        connectionWindow.addAndGet(-dataLength);
        
        // 3. 如果窗口太小,发送WINDOW_UPDATE帧
        if (connectionWindow.get() < 16384) { // 低于阈值
            sendWindowUpdate(0, 32768); // 连接级别更新
        }
    }
    
    /**
     * 消费数据后的窗口更新
     */
    public synchronized void onDataConsumed(int streamId, int dataLength) {
        StreamWindow window = streamWindows.get(streamId);
        if (window != null) {
            window.windowSize += dataLength;
            
            // 发送WINDOW_UPDATE帧
            sendWindowUpdate(streamId, dataLength);
            
            // 尝试处理阻塞的写入
            processPendingWrites(window);
        }
    }
    
    /**
     * 发送WINDOW_UPDATE帧
     */
    private void sendWindowUpdate(int streamId, int windowSizeIncrement) {
        ByteBuffer buffer = ByteBuffer.allocate(13); // 9字节帧头 + 4字节增量
        
        // 帧头
        buffer.putInt(4);                // 长度: 4字节
        buffer.put((byte) 0x8);          // 类型: WINDOW_UPDATE
        buffer.put((byte) 0x0);          // 标志位
        buffer.putInt(streamId);         // 流ID
        
        // 负载:窗口增量(必须>0)
        buffer.putInt(windowSizeIncrement);
        
        // 发送帧
        sendFrame(buffer.array());
    }
    
    /**
     * 处理被阻塞的写入
     */
    private void processPendingWrites(StreamWindow window) {
        while (!window.pendingWrites.isEmpty()) {
            Runnable write = window.pendingWrites.peek();
            
            // 重新尝试执行
            try {
                write.run();
                window.pendingWrites.poll();
            } catch (WindowBlockedException e) {
                // 仍然阻塞,等待下次窗口更新
                break;
            }
        }
        
        if (window.pendingWrites.isEmpty()) {
            window.blocked = false;
        }
    }
}

2.2 零窗口与阻塞恢复

java

复制

下载

复制代码
// 零窗口探测与恢复
public class ZeroWindowProbe {
    
    private final ScheduledExecutorService scheduler;
    private final Map<Integer, ProbeState> probeStates;
    
    public ZeroWindowProbe() {
        this.scheduler = Executors.newSingleThreadScheduledExecutor();
        this.probeStates = new ConcurrentHashMap<>();
    }
    
    /**
     * 当窗口变为0时,启动探测机制
     */
    public void onZeroWindow(int streamId) {
        ProbeState state = new ProbeState(streamId);
        probeStates.put(streamId, state);
        
        // 启动探测定时任务
        scheduler.scheduleAtFixedRate(
            () -> probeZeroWindow(streamId),
            1000,  // 初始延迟1秒
            1000,  // 间隔1秒
            TimeUnit.MILLISECONDS
        );
    }
    
    /**
     * 探测零窗口
     */
    private void probeZeroWindow(int streamId) {
        // 发送1字节的空DATA帧(标志位: END_STREAM = false)
        byte[] probeFrame = createDataFrame(streamId, new byte[1], false);
        sendFrame(probeFrame);
        
        ProbeState state = probeStates.get(streamId);
        if (state != null) {
            state.probeCount++;
            
            // 如果探测超过阈值,关闭流
            if (state.probeCount > 30) { // 30次探测失败
                resetStream(streamId, ErrorCode.FLOW_CONTROL_ERROR);
                probeStates.remove(streamId);
            }
        }
    }
    
    /**
     * 窗口恢复,停止探测
     */
    public void onWindowUpdate(int streamId, int increment) {
        ProbeState state = probeStates.get(streamId);
        if (state != null && increment > 0) {
            state.probeTask.cancel(false);
            probeStates.remove(streamId);
        }
    }
    
    static class ProbeState {
        final int streamId;
        int probeCount;
        ScheduledFuture<?> probeTask;
        
        ProbeState(int streamId) {
            this.streamId = streamId;
            this.probeCount = 0;
        }
    }
    
    /**
     * 创建DATA帧
     */
    private byte[] createDataFrame(int streamId, byte[] data, boolean endStream) {
        ByteBuffer buffer = ByteBuffer.allocate(9 + data.length);
        
        // 帧头
        buffer.putShort((short) data.length);  // 长度
        buffer.put((byte) 0x0);                // 类型: DATA
        buffer.put((byte) (endStream ? 0x1 : 0x0)); // END_STREAM标志位
        buffer.putInt(streamId);               // 流ID
        
        // 负载
        buffer.put(data);
        
        return buffer.array();
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

三、gRPC 消息分帧实现

3.1 Protobuf 消息序列化与分帧

java

复制

下载

复制代码
// gRPC消息分帧处理器
public class GrpcMessageFramer {
    
    // gRPC帧头:5字节
    static class FrameHeader {
        byte compressedFlag;  // 压缩标志 (1=压缩,0=未压缩)
        int length;           // 消息长度 (4字节,大端序)
    }
    
    // 消息分帧
    public List<byte[]> frameMessage(byte[] protobufData, boolean compressed) {
        List<byte[]> frames = new ArrayList<>();
        
        // 如果消息太大,需要分片
        int maxFrameSize = 16384; // 16KB,HTTP/2默认最大帧大小
        int offset = 0;
        
        while (offset < protobufData.length) {
            int chunkSize = Math.min(maxFrameSize, protobufData.length - offset);
            byte[] chunk = new byte[chunkSize];
            System.arraycopy(protobufData, offset, chunk, 0, chunkSize);
            
            // 创建帧
            byte[] frame = createFrame(chunk, compressed, offset == 0);
            frames.add(frame);
            
            offset += chunkSize;
        }
        
        return frames;
    }
    
    // 创建单个帧
    private byte[] createFrame(byte[] data, boolean compressed, boolean isFirst) {
        ByteBuffer buffer = ByteBuffer.allocate(5 + data.length);
        
        // 帧头
        buffer.put(compressed ? (byte) 1 : (byte) 0); // 压缩标志
        buffer.putInt(data.length);                   // 数据长度
        
        // 数据负载
        buffer.put(data);
        
        return buffer.array();
    }
    
    // 消息重组
    public byte[] deframeMessages(List<byte[]> frames) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        
        for (byte[] frame : frames) {
            ByteBuffer buffer = ByteBuffer.wrap(frame);
            
            // 解析帧头
            byte compressedFlag = buffer.get();
            int length = buffer.getInt();
            
            // 读取数据
            byte[] data = new byte[length];
            buffer.get(data);
            
            // 处理压缩
            if (compressedFlag == 1) {
                data = decompress(data);
            }
            
            output.write(data);
        }
        
        return output.toByteArray();
    }
    
    // Protobuf消息包装器
    public static class GrpcMessage<T> {
        private final T protobufMessage;
        private final Metadata headers;
        
        public GrpcMessage(T message, Metadata headers) {
            this.protobufMessage = message;
            this.headers = headers;
        }
        
        public byte[] serialize() throws IOException {
            // 1. 序列化Protobuf消息
            ByteArrayOutputStream protoStream = new ByteArrayOutputStream();
            protobufMessage.writeTo(protoStream);
            byte[] protoData = protoStream.toByteArray();
            
            // 2. 添加gRPC帧头
            ByteBuffer buffer = ByteBuffer.allocate(5 + protoData.length);
            buffer.put((byte) 0); // 未压缩
            buffer.putInt(protoData.length);
            buffer.put(protoData);
            
            return buffer.array();
        }
        
        public static <T extends Message> GrpcMessage<T> deserialize(
            byte[] framedData, 
            Class<T> clazz) throws IOException {
            
            ByteBuffer buffer = ByteBuffer.wrap(framedData);
            
            // 解析帧头
            byte compressedFlag = buffer.get();
            int length = buffer.getInt();
            
            // 读取数据
            byte[] protoData = new byte[length];
            buffer.get(protoData);
            
            // 反序列化Protobuf
            T message = parseProtobuf(protoData, clazz);
            
            return new GrpcMessage<>(message, new Metadata());
        }
        
        private static <T extends Message> T parseProtobuf(
            byte[] data, 
            Class<T> clazz) throws IOException {
            
            Method parseMethod = clazz.getMethod("parseFrom", byte[].class);
            return (T) parseMethod.invoke(null, data);
        }
    }
}

3.2 HTTP/2 头部帧与数据帧组装

java

复制

下载

复制代码
// gRPC over HTTP/2 帧组装器
public class GrpcHttp2Framer {
    
    // gRPC特定的HTTP/2头部
    private static final List<Header> GRPC_HEADERS = Arrays.asList(
        new Header("content-type", "application/grpc"),
        new Header("te", "trailers")  // 必须的,用于支持尾部头部
    );
    
    /**
     * 构建gRPC请求帧序列
     */
    public List<byte[]> buildRequestFrames(
        int streamId, 
        String methodPath,
        byte[] messageData,
        Metadata headers) {
        
        List<byte[]> frames = new ArrayList<>();
        
        // 1. 构建HEADERS帧(包含gRPC特定头部)
        byte[] headersFrame = buildHeadersFrame(streamId, methodPath, headers);
        frames.add(headersFrame);
        
        // 2. 构建DATA帧序列
        List<byte[]> dataFrames = buildDataFrames(streamId, messageData);
        frames.addAll(dataFrames);
        
        return frames;
    }
    
    /**
     * 构建HEADERS帧
     */
    private byte[] buildHeadersFrame(
        int streamId, 
        String path, 
        Metadata metadata) {
        
        List<Header> allHeaders = new ArrayList<>();
        
        // 必需头部
        allHeaders.add(new Header(":method", "POST"));
        allHeaders.add(new Header(":scheme", "http"));
        allHeaders.add(new Header(":path", path));
        allHeaders.add(new Header(":authority", "api.example.com"));
        
        // gRPC特定头部
        allHeaders.addAll(GRPC_HEADERS);
        
        // 自定义元数据头部(需要编码)
        for (String key : metadata.keys()) {
            String value = metadata.get(key);
            allHeaders.add(new Header(key, value));
        }
        
        // HPACK压缩头部
        byte[] compressedHeaders = compressHeaders(allHeaders);
        
        // 构建帧
        return buildFrame(FrameType.HEADERS, streamId, 
            (byte) 0x4, // END_HEADERS标志
            compressedHeaders);
    }
    
    /**
     * 构建DATA帧序列
     */
    private List<byte[]> buildDataFrames(int streamId, byte[] messageData) {
        List<byte[]> frames = new ArrayList<>();
        
        // gRPC帧头
        byte[] grpcFrameHeader = new byte[5];
        ByteBuffer headerBuffer = ByteBuffer.wrap(grpcFrameHeader);
        headerBuffer.put((byte) 0); // 未压缩
        headerBuffer.putInt(messageData.length);
        
        // 组合完整消息
        byte[] fullMessage = new byte[grpcFrameHeader.length + messageData.length];
        System.arraycopy(grpcFrameHeader, 0, fullMessage, 0, grpcFrameHeader.length);
        System.arraycopy(messageData, 0, fullMessage, grpcFrameHeader.length, messageData.length);
        
        // 分片(如果需要)
        int maxFrameSize = 16384; // HTTP/2默认最大帧
        int offset = 0;
        
        while (offset < fullMessage.length) {
            int chunkSize = Math.min(maxFrameSize, fullMessage.length - offset);
            byte[] chunk = new byte[chunkSize];
            System.arraycopy(fullMessage, offset, chunk, 0, chunkSize);
            
            byte flags = 0;
            if (offset + chunkSize == fullMessage.length) {
                flags = 0x1; // END_STREAM标志
            }
            
            byte[] frame = buildFrame(FrameType.DATA, streamId, flags, chunk);
            frames.add(frame);
            
            offset += chunkSize;
        }
        
        return frames;
    }
    
    /**
     * 构建通用HTTP/2帧
     */
    private byte[] buildFrame(FrameType type, int streamId, byte flags, byte[] payload) {
        ByteBuffer buffer = ByteBuffer.allocate(9 + payload.length);
        
        // 帧头
        buffer.putShort((short) payload.length); // 24位长度的高16位
        buffer.put((byte) 0);                     // 24位长度的低8位
        buffer.put((byte) type.getValue());
        buffer.put(flags);
        buffer.putInt(streamId);
        
        // 负载
        buffer.put(payload);
        
        return buffer.array();
    }
    
    /**
     * HPACK头部压缩
     */
    private byte[] compressHeaders(List<Header> headers) {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        
        for (Header header : headers) {
            // 简化的HPACK编码
            if (isIndexedHeader(header)) {
                // 使用索引表示
                int index = getHeaderIndex(header);
                output.write(0x80 | index); // 最高位为1表示索引
            } else {
                // 字面表示
                output.write(0x40); // 字面头部,不索引
                encodeString(output, header.name);
                encodeString(output, header.value);
            }
        }
        
        return output.toByteArray();
    }
    
    private void encodeString(ByteArrayOutputStream output, String str) {
        byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
        if (bytes.length < 127) {
            output.write(bytes.length);
        } else {
            output.write(0x80 | (bytes.length & 0x7F));
            output.write(bytes.length >> 7);
        }
        output.write(bytes);
    }
}

四、gRPC 流式通信实现

4.1 服务器端流实现

java

复制

下载

复制代码
// 服务器端流处理器
public class ServerStreamHandler {
    
    private final Http2Connection connection;
    private final Map<Integer, StreamContext> activeStreams;
    
    public ServerStreamHandler(Http2Connection connection) {
        this.connection = connection;
        this.activeStreams = new ConcurrentHashMap<>();
    }
    
    /**
     * 处理客户端请求并返回服务器端流
     */
    public void handleServerStream(int streamId, 
                                  MethodDescriptor method,
                                  StreamObserver<Message> responseObserver) {
        
        StreamContext context = new StreamContext(streamId, method, responseObserver);
        activeStreams.put(streamId, context);
        
        // 开始流式响应
        sendInitialHeaders(streamId);
        
        // 在后台生成流数据
        Executors.newSingleThreadExecutor().submit(() -> {
            try {
                generateStreamData(context);
            } catch (Exception e) {
                sendError(streamId, e);
            } finally {
                completeStream(streamId);
            }
        });
    }
    
    /**
     * 生成流数据
     */
    private void generateStreamData(StreamContext context) {
        int count = 0;
        while (count < 100 && !context.isCancelled()) {
            // 生成响应消息
            Message response = context.method.invoke(context.request);
            
            // 发送消息帧
            sendMessageFrame(context.streamId, response, false);
            
            count++;
            
            // 流控制检查
            if (!checkFlowControl(context.streamId)) {
                // 等待窗口更新
                waitForWindowUpdate(context.streamId);
            }
            
            // 模拟处理延迟
            Thread.sleep(100);
        }
        
        // 发送结束标志
        sendTrailers(context.streamId, Status.OK);
    }
    
    /**
     * 发送消息帧
     */
    private void sendMessageFrame(int streamId, Message message, boolean endStream) {
        try {
            // 序列化消息
            byte[] serialized = message.toByteArray();
            
            // 构建gRPC帧
            byte[] grpcFrame = buildGrpcFrame(serialized, false);
            
            // 构建HTTP/2 DATA帧
            byte[] http2Frame = buildDataFrame(streamId, grpcFrame, endStream);
            
            // 发送帧
            connection.sendFrame(http2Frame);
            
            // 更新流控制状态
            updateFlowControl(streamId, grpcFrame.length);
            
        } catch (IOException e) {
            throw new RuntimeException("Failed to send message", e);
        }
    }
    
    /**
     * 流控制检查
     */
    private boolean checkFlowControl(int streamId) {
        StreamContext context = activeStreams.get(streamId);
        if (context == null) {
            return false;
        }
        
        // 检查窗口大小
        return connection.getStreamWindowSize(streamId) > 0;
    }
    
    /**
     * 等待窗口更新
     */
    private void waitForWindowUpdate(int streamId) {
        StreamContext context = activeStreams.get(streamId);
        if (context == null) {
            return;
        }
        
        synchronized (context) {
            try {
                // 等待窗口更新通知
                context.wait(5000); // 5秒超时
                
                // 检查是否被取消
                if (context.isCancelled()) {
                    throw new CancellationException();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Stream interrupted", e);
            }
        }
    }
    
    /**
     * 发送尾部头部(包含gRPC状态)
     */
    private void sendTrailers(int streamId, Status status) {
        Metadata trailers = new Metadata();
        trailers.put(GRPC_STATUS_KEY, status.getCode().value());
        trailers.put(GRPC_MESSAGE_KEY, status.getDescription());
        
        byte[] trailersFrame = buildHeadersFrame(streamId, trailers, true);
        connection.sendFrame(trailersFrame);
        
        // 标记流结束
        activeStreams.remove(streamId);
    }
    
    static class StreamContext {
        final int streamId;
        final MethodDescriptor method;
        final StreamObserver<Message> responseObserver;
        volatile boolean cancelled = false;
        volatile Message request;
        
        StreamContext(int streamId, MethodDescriptor method, 
                     StreamObserver<Message> responseObserver) {
            this.streamId = streamId;
            this.method = method;
            this.responseObserver = responseObserver;
        }
        
        synchronized void cancel() {
            cancelled = true;
            notifyAll();
        }
        
        boolean isCancelled() {
            return cancelled;
        }
    }
}

4.2 客户端流实现

java

复制

下载

复制代码
// 客户端流处理器
public class ClientStreamHandler {
    
    private final Http2Connection connection;
    private final int streamId;
    private final StreamObserver<Message> requestObserver;
    private final StreamObserver<Message> responseObserver;
    private volatile boolean completed = false;
    
    public ClientStreamHandler(Http2Connection connection, 
                              MethodDescriptor method,
                              StreamObserver<Message> responseObserver) {
        this.connection = connection;
        this.streamId = connection.createStream();
        this.responseObserver = responseObserver;
        
        // 发送初始头部
        sendInitialHeaders(method);
        
        // 创建请求观察者
        this.requestObserver = new StreamObserver<Message>() {
            @Override
            public void onNext(Message message) {
                sendMessage(message, false);
            }
            
            @Override
            public void onError(Throwable t) {
                sendError(t);
                completed = true;
            }
            
            @Override
            public void onCompleted() {
                sendComplete();
                completed = true;
            }
        };
    }
    
    /**
     * 发送消息到服务器
     */
    private void sendMessage(Message message, boolean endStream) {
        if (completed) {
            throw new IllegalStateException("Stream already completed");
        }
        
        try {
            // 检查流控制
            while (!canSendMessage()) {
                Thread.sleep(10);
            }
            
            // 序列化消息
            byte[] serialized = message.toByteArray();
            
            // 构建并发送帧
            byte[] grpcFrame = buildGrpcFrame(serialized, false);
            byte[] http2Frame = buildDataFrame(streamId, grpcFrame, endStream);
            
            connection.sendFrame(http2Frame);
            
            // 更新流控制
            connection.updateSentBytes(streamId, grpcFrame.length);
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to send message", e);
        }
    }
    
    /**
     * 检查是否可以发送消息
     */
    private boolean canSendMessage() {
        // 检查连接级别窗口
        int connectionWindow = connection.getConnectionWindowSize();
        if (connectionWindow <= 0) {
            return false;
        }
        
        // 检查流级别窗口
        int streamWindow = connection.getStreamWindowSize(streamId);
        return streamWindow > 0;
    }
    
    /**
     * 处理服务器响应
     */
    public void onHeadersReceived(Map<String, String> headers) {
        // 处理响应头部
        String status = headers.get(":status");
        if (!"200".equals(status)) {
            responseObserver.onError(new RuntimeException("HTTP error: " + status));
        }
    }
    
    public void onDataReceived(byte[] data) {
        try {
            // 解析gRPC帧
            GrpcFrame frame = parseGrpcFrame(data);
            
            // 如果是压缩的,解压缩
            byte[] messageData = frame.compressed ? 
                decompress(frame.payload) : frame.payload;
            
            // 反序列化消息
            Message message = parseMessage(messageData);
            
            // 传递给观察者
            responseObserver.onNext(message);
            
            // 发送窗口更新
            connection.sendWindowUpdate(streamId, data.length);
            
        } catch (Exception e) {
            responseObserver.onError(e);
        }
    }
    
    public void onTrailersReceived(Map<String, String> trailers) {
        // 检查gRPC状态
        String grpcStatus = trailers.get("grpc-status");
        if (!"0".equals(grpcStatus)) {
            String grpcMessage = trailers.get("grpc-message");
            responseObserver.onError(new RuntimeException(
                "gRPC error: " + grpcStatus + " - " + grpcMessage));
        } else {
            responseObserver.onCompleted();
        }
        
        completed = true;
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

五、流量整形与优先级

5.1 HTTP/2 优先级处理

java

复制

下载

复制代码
// HTTP/2 优先级调度器
public class Http2PriorityScheduler {
    
    // 优先级树节点
    static class PriorityNode {
        final int streamId;
        final int weight;           // 权重 (1-256)
        int dependency;             // 依赖的流ID
        boolean exclusive;          // 是否独占依赖
        int allocatedBytes;         // 已分配字节数
        PriorityNode parent;
        List<PriorityNode> children = new ArrayList<>();
        
        PriorityNode(int streamId, int weight, int dependency, boolean exclusive) {
            this.streamId = streamId;
            this.weight = weight;
            this.dependency = dependency;
            this.exclusive = exclusive;
        }
        
        int getEffectiveWeight() {
            if (parent == null) {
                return weight;
            }
            return weight * parent.getEffectiveWeight() / 100;
        }
    }
    
    private final Map<Integer, PriorityNode> nodes = new HashMap<>();
    private final PriorityQueue<PriorityNode> readyQueue;
    
    public Http2PriorityScheduler() {
        // 初始化根节点 (stream 0)
        PriorityNode root = new PriorityNode(0, 256, 0, false);
        nodes.put(0, root);
        
        // 基于有效权重的优先级队列
        readyQueue = new PriorityQueue<>(
            Comparator.comparingInt(PriorityNode::getEffectiveWeight).reversed()
        );
    }
    
    /**
     * 处理PRIORITY帧
     */
    public void processPriorityFrame(int streamId, int dependency, 
                                    int weight, boolean exclusive) {
        
        PriorityNode node = nodes.get(streamId);
        if (node == null) {
            node = new PriorityNode(streamId, weight, dependency, exclusive);
            nodes.put(streamId, node);
        } else {
            node.weight = weight;
            node.dependency = dependency;
            node.exclusive = exclusive;
        }
        
        // 更新依赖关系
        updateDependency(node);
        
        // 重新计算优先级
        recalculatePriorities();
    }
    
    /**
     * 更新依赖关系
     */
    private void updateDependency(PriorityNode node) {
        // 从原父节点移除
        if (node.parent != null) {
            node.parent.children.remove(node);
        }
        
        // 设置新父节点
        PriorityNode parent = nodes.get(node.dependency);
        if (parent == null) {
            parent = nodes.get(0); // 默认依赖根节点
        }
        
        if (node.exclusive) {
            // 独占依赖:新节点成为父节点的唯一子节点
            List<PriorityNode> oldChildren = new ArrayList<>(parent.children);
            parent.children.clear();
            parent.children.add(node);
            node.children.addAll(oldChildren);
        } else {
            // 共享依赖:添加到子节点列表
            parent.children.add(node);
        }
        
        node.parent = parent;
    }
    
    /**
     * 调度发送数据
     */
    public List<Integer> scheduleSend(int availableWindow) {
        List<Integer> scheduledStreams = new ArrayList<>();
        int remaining = availableWindow;
        
        // 填充就绪队列
        fillReadyQueue();
        
        while (remaining > 0 && !readyQueue.isEmpty()) {
            PriorityNode node = readyQueue.poll();
            
            // 计算分配量(基于权重)
            int allocation = calculateAllocation(node, remaining);
            
            if (allocation > 0) {
                node.allocatedBytes += allocation;
                scheduledStreams.add(node.streamId);
                remaining -= allocation;
                
                // 如果还有剩余额度,重新加入队列
                if (allocation < remaining) {
                    readyQueue.add(node);
                }
            }
        }
        
        return scheduledStreams;
    }
    
    /**
     * 计算分配量
     */
    private int calculateAllocation(PriorityNode node, int totalAvailable) {
        // 基于权重和父节点已分配量的比例
        double weightRatio = (double) node.weight / 
            node.parent.children.stream().mapToInt(n -> n.weight).sum();
        
        int parentAllocated = node.parent.allocatedBytes;
        int fairShare = (int) (parentAllocated * weightRatio);
        
        // 减去已经分配的量
        int allocation = Math.max(0, fairShare - node.allocatedBytes);
        
        // 不超过可用窗口
        allocation = Math.min(allocation, totalAvailable);
        
        return allocation;
    }
    
    /**
     * 填充就绪队列
     */
    private void fillReadyQueue() {
        readyQueue.clear();
        
        // 遍历所有有数据的流
        for (PriorityNode node : nodes.values()) {
            if (node.streamId != 0 && hasDataToSend(node.streamId)) {
                readyQueue.add(node);
            }
        }
    }
}

5.2 gRPC 特定流量控制

java

复制

下载

复制代码
// gRPC特定的流量控制器
public class GrpcFlowController {
    
    // gRPC流控制设置
    private static final int DEFAULT_INITIAL_WINDOW_SIZE = 65535;
    private static final int DEFAULT_MAX_CONCURRENT_STREAMS = 100;
    private static final int DEFAULT_MAX_FRAME_SIZE = 16384;
    
    private final Http2FlowControl http2FlowControl;
    private final Map<Integer, GrpcStreamContext> grpcStreams;
    private final int initialWindowSize;
    
    public GrpcFlowController() {
        this(DEFAULT_INITIAL_WINDOW_SIZE);
    }
    
    public GrpcFlowController(int initialWindowSize) {
        this.initialWindowSize = initialWindowSize;
        this.http2FlowControl = new Http2FlowControl();
        this.grpcStreams = new ConcurrentHashMap<>();
    }
    
    /**
     * gRPC特定的流创建
     */
    public int createGrpcStream(String methodName, Metadata headers) {
        // 创建HTTP/2流
        int streamId = http2FlowControl.createStream();
        
        // 设置gRPC特定的初始窗口
        http2FlowControl.setInitialWindowSize(streamId, initialWindowSize);
        
        // 创建gRPC流上下文
        GrpcStreamContext context = new GrpcStreamContext(streamId, methodName);
        grpcStreams.put(streamId, context);
        
        // 应用方法特定的流控制策略
        applyMethodSpecificFlowControl(context, methodName);
        
        return streamId;
    }
    
    /**
     * 发送gRPC消息(考虑消息分帧)
     */
    public void sendGrpcMessage(int streamId, byte[] messageData) 
        throws FlowControlException {
        
        GrpcStreamContext context = grpcStreams.get(streamId);
        if (context == null) {
            throw new IllegalArgumentException("Stream not found: " + streamId);
        }
        
        // 1. 添加gRPC帧头
        byte[] framedMessage = frameGrpcMessage(messageData, false);
        
        // 2. 检查流控制
        if (!canSendGrpcMessage(streamId, framedMessage.length)) {
            throw new FlowControlException("Flow control limit exceeded");
        }
        
        // 3. 分帧发送(如果超过HTTP/2最大帧大小)
        List<byte[]> frames = splitIntoFrames(framedMessage);
        
        for (byte[] frame : frames) {
            sendDataFrame(streamId, frame, false);
        }
        
        // 4. 更新统计
        context.sentMessages++;
        context.sentBytes += framedMessage.length;
    }
    
    /**
     * 接收gRPC消息
     */
    public byte[] receiveGrpcMessage(int streamId, List<byte[]> frames) 
        throws IOException {
        
        // 1. 重组帧
        byte[] completeMessage = reassembleFrames(frames);
        
        // 2. 解析gRPC帧头
        GrpcFrameHeader header = parseGrpcFrameHeader(completeMessage);
        
        // 3. 提取消息数据
        byte[] messageData = extractMessageData(completeMessage, header);
        
        // 4. 如果是压缩的,解压缩
        if (header.isCompressed()) {
            messageData = decompress(messageData);
        }
        
        // 5. 更新流控制窗口
        updateWindowAfterReceiving(streamId, completeMessage.length);
        
        // 6. 更新统计
        GrpcStreamContext context = grpcStreams.get(streamId);
        if (context != null) {
            context.receivedMessages++;
            context.receivedBytes += completeMessage.length;
        }
        
        return messageData;
    }
    
    /**
     * 动态调整窗口大小
     */
    public void adjustWindowDynamically(int streamId) {
        GrpcStreamContext context = grpcStreams.get(streamId);
        if (context == null) {
            return;
        }
        
        // 基于历史流量模式调整窗口
        double messagesPerSecond = calculateMessageRate(context);
        int avgMessageSize = calculateAverageMessageSize(context);
        
        // 计算理想窗口大小(2秒的流量)
        int idealWindowSize = (int) (messagesPerSecond * avgMessageSize * 2);
        
        // 限制在合理范围内
        idealWindowSize = Math.max(initialWindowSize, 
            Math.min(idealWindowSize, initialWindowSize * 10));
        
        // 如果调整幅度超过10%,发送窗口更新
        int currentWindow = http2FlowControl.getStreamWindowSize(streamId);
        double changeRatio = (double) Math.abs(idealWindowSize - currentWindow) / currentWindow;
        
        if (changeRatio > 0.1) {
            int increment = idealWindowSize - currentWindow;
            http2FlowControl.sendWindowUpdate(streamId, increment);
        }
    }
    
    /**
     * 应用方法特定的流控制策略
     */
    private void applyMethodSpecificFlowControl(GrpcStreamContext context, 
                                               String methodName) {
        
        // 根据方法类型设置不同的流控制策略
        if (methodName.contains("Upload") || methodName.contains("Stream")) {
            // 上传或流式方法:使用较小的初始窗口,逐步增加
            context.flowControlStrategy = FlowControlStrategy.GRADUAL;
            context.maxWindowSize = initialWindowSize * 2;
        } else if (methodName.contains("Download") || methodName.contains("Get")) {
            // 下载方法:使用较大的窗口
            context.flowControlStrategy = FlowControlStrategy.AGGRESSIVE;
            context.maxWindowSize = initialWindowSize * 5;
        } else {
            // 普通RPC:使用默认设置
            context.flowControlStrategy = FlowControlStrategy.CONSERVATIVE;
            context.maxWindowSize = initialWindowSize;
        }
    }
    
    static class GrpcStreamContext {
        final int streamId;
        final String methodName;
        FlowControlStrategy flowControlStrategy;
        int maxWindowSize;
        int sentMessages;
        int sentBytes;
        int receivedMessages;
        int receivedBytes;
        long createdTime;
        
        GrpcStreamContext(int streamId, String methodName) {
            this.streamId = streamId;
            this.methodName = methodName;
            this.createdTime = System.currentTimeMillis();
        }
    }
    
    enum FlowControlStrategy {
        CONSERVATIVE,   // 保守:小窗口,慢启动
        GRADUAL,        // 渐进:中等窗口,线性增长
        AGGRESSIVE      // 激进:大窗口,快速响应
    }
}

六、错误处理与重试

6.1 gRPC 错误码映射

java

复制

下载

复制代码
// gRPC错误处理
public class GrpcErrorHandler {
    
    // gRPC状态码到HTTP/2错误码的映射
    private static final Map<Status.Code, ErrorCode> GRPC_TO_HTTP2_ERROR_MAP = 
        new EnumMap<>(Status.Code.class);
    
    static {
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.CANCELLED, ErrorCode.CANCEL);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.DEADLINE_EXCEEDED, ErrorCode.CANCEL);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.INVALID_ARGUMENT, ErrorCode.PROTOCOL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.NOT_FOUND, ErrorCode.REFUSED_STREAM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.ALREADY_EXISTS, ErrorCode.REFUSED_STREAM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.PERMISSION_DENIED, ErrorCode.REFUSED_STREAM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.RESOURCE_EXHAUSTED, ErrorCode.ENHANCE_YOUR_CALM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.FAILED_PRECONDITION, ErrorCode.REFUSED_STREAM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.ABORTED, ErrorCode.INTERNAL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.OUT_OF_RANGE, ErrorCode.PROTOCOL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.UNIMPLEMENTED, ErrorCode.PROTOCOL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.INTERNAL, ErrorCode.INTERNAL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.UNAVAILABLE, ErrorCode.REFUSED_STREAM);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.DATA_LOSS, ErrorCode.INTERNAL_ERROR);
        GRPC_TO_HTTP2_ERROR_MAP.put(Status.Code.UNAUTHENTICATED, ErrorCode.REFUSED_STREAM);
    }
    
    /**
     * 处理gRPC错误
     */
    public void handleGrpcError(int streamId, Status status, Metadata trailers) {
        // 1. 发送包含错误状态的尾部头部
        sendErrorTrailers(streamId, status, trailers);
        
        // 2. 如果错误严重,发送RST_STREAM帧
        if (shouldResetStream(status)) {
            ErrorCode errorCode = mapGrpcToHttp2Error(status.getCode());
            sendRstStream(streamId, errorCode);
        }
        
        // 3. 记录错误统计
        recordErrorStatistics(status);
    }
    
    /**
     * 发送错误尾部头部
     */
    private void sendErrorTrailers(int streamId, Status status, Metadata trailers) {
        // 添加gRPC状态头部
        Metadata errorTrailers = new Metadata();
        errorTrailers.merge(trailers);
        errorTrailers.put(GrpcUtil.STATUS_KEY, status.getCode().value());
        errorTrailers.put(GrpcUtil.MESSAGE_KEY, status.getDescription());
        
        // 发送HEADERS帧(END_STREAM标志)
        sendHeadersFrame(streamId, errorTrailers, true);
    }
    
    /**
     * 发送RST_STREAM帧
     */
    private void sendRstStream(int streamId, ErrorCode errorCode) {
        ByteBuffer buffer = ByteBuffer.allocate(13); // 9字节帧头 + 4字节错误码
        
        // 帧头
        buffer.putInt(4);                // 长度: 4字节
        buffer.put((byte) 0x3);          // 类型: RST_STREAM
        buffer.put((byte) 0x0);          // 标志位
        buffer.putInt(streamId);         // 流ID
        
        // 错误码
        buffer.putInt(errorCode.getCode());
        
        sendFrame(buffer.array());
    }
    
    /**
     * 判断是否需要重置流
     */
    private boolean shouldResetStream(Status status) {
        switch (status.getCode()) {
            case CANCELLED:
            case DEADLINE_EXCEEDED:
            case RESOURCE_EXHAUSTED:
            case UNAVAILABLE:
                return true;
            default:
                return false;
        }
    }
    
    /**
     * gRPC错误码到HTTP/2错误码的映射
     */
    private ErrorCode mapGrpcToHttp2Error(Status.Code grpcCode) {
        return GRPC_TO_HTTP2_ERROR_MAP.getOrDefault(grpcCode, ErrorCode.INTERNAL_ERROR);
    }
    
    /**
     * 流控制错误处理
     */
    public void handleFlowControlError(int streamId, FlowControlException e) {
        // 1. 发送WINDOW_UPDATE帧来恢复
        sendWindowUpdate(streamId, 65536); // 增加64KB窗口
        
        // 2. 如果是持续错误,减少窗口
        if (isPersistentFlowControlError(streamId)) {
            adjustFlowControlWindow(streamId, -0.5); // 减少50%
        }
        
        // 3. 记录日志
        logFlowControlError(streamId, e);
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​

七、性能监控与调优

7.1 gRPC 性能监控

java

复制

下载

复制代码
// gRPC性能监控器
@RestController
public class GrpcPerformanceMonitor {
    
    @Autowired
    private GrpcMetricsCollector metricsCollector;
    
    @GetMapping("/api/grpc/metrics")
    public GrpcMetrics getMetrics() {
        return metricsCollector.collectAllMetrics();
    }
    
    @GetMapping("/api/grpc/streams/{id}")
    public StreamMetrics getStreamMetrics(@PathVariable int streamId) {
        return metricsCollector.getStreamMetrics(streamId);
    }
    
    @GetMapping("/api/grpc/flow-control")
    public FlowControlMetrics getFlowControlMetrics() {
        return metricsCollector.getFlowControlMetrics();
    }
}

// gRPC指标收集器
@Component
public class GrpcMetricsCollector {
    
    // 连接级别指标
    private final AtomicInteger activeStreams = new AtomicInteger(0);
    private final AtomicLong totalRequests = new AtomicLong(0);
    private final AtomicLong totalBytesSent = new AtomicLong(0);
    private final AtomicLong totalBytesReceived = new AtomicLong(0);
    
    // 流级别指标
    private final Map<Integer, StreamMetrics> streamMetrics = 
        new ConcurrentHashMap<>();
    
    // 延迟直方图
    private final Histogram latencyHistogram = new Histogram(
        TimeUnit.MILLISECONDS.toNanos(1),  // 1ms
        TimeUnit.SECONDS.toNanos(10),      // 10s
        5                                   // 精度
    );
    
    /**
     * 记录请求开始
     */
    public void recordRequestStart(int streamId, String method) {
        activeStreams.incrementAndGet();
        totalRequests.incrementAndGet();
        
        StreamMetrics metrics = new StreamMetrics(streamId, method);
        metrics.startTime = System.nanoTime();
        streamMetrics.put(streamId, metrics);
    }
    
    /**
     * 记录请求完成
     */
    public void recordRequestEnd(int streamId, Status status) {
        activeStreams.decrementAndGet();
        
        StreamMetrics metrics = streamMetrics.get(streamId);
        if (metrics != null) {
            metrics.endTime = System.nanoTime();
            metrics.duration = metrics.endTime - metrics.startTime;
            metrics.status = status;
            
            // 记录延迟
            latencyHistogram.record(metrics.duration);
            
            // 保留一段时间后清理
            scheduleCleanup(streamId);
        }
    }
    
    /**
     * 记录流量数据
     */
    public void recordTraffic(int streamId, int bytesSent, int bytesReceived) {
        totalBytesSent.addAndGet(bytesSent);
        totalBytesReceived.addAndGet(bytesReceived);
        
        StreamMetrics metrics = streamMetrics.get(streamId);
        if (metrics != null) {
            metrics.bytesSent += bytesSent;
            metrics.bytesReceived += bytesReceived;
        }
    }
    
    /**
     * 收集所有指标
     */
    public GrpcMetrics collectAllMetrics() {
        GrpcMetrics metrics = new GrpcMetrics();
        
        metrics.setActiveStreams(activeStreams.get());
        metrics.setTotalRequests(totalRequests.get());
        metrics.setTotalBytesSent(totalBytesSent.get());
        metrics.setTotalBytesReceived(totalBytesReceived.get());
        
        metrics.setAverageLatency(latencyHistogram.getMean());
        metrics.setP50Latency(latencyHistogram.getValue(0.5));
        metrics.setP95Latency(latencyHistogram.getValue(0.95));
        metrics.setP99Latency(latencyHistogram.getValue(0.99));
        
        // 计算成功率
        long successfulStreams = streamMetrics.values().stream()
            .filter(m -> m.status != null && m.status.isOk())
            .count();
        double successRate = streamMetrics.isEmpty() ? 0 : 
            (double) successfulStreams / streamMetrics.size();
        metrics.setSuccessRate(successRate);
        
        return metrics;
    }
    
    /**
     * 流控制指标
     */
    public FlowControlMetrics getFlowControlMetrics() {
        FlowControlMetrics metrics = new FlowControlMetrics();
        
        // 计算平均窗口使用率
        double avgWindowUsage = streamMetrics.values().stream()
            .mapToDouble(m -> m.getWindowUsage())
            .average()
            .orElse(0);
        
        metrics.setAverageWindowUsage(avgWindowUsage);
        
        // 识别热点流
        List<Integer> hotStreams = streamMetrics.entrySet().stream()
            .filter(e -> e.getValue().bytesSent > 1024 * 1024) // 超过1MB
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        metrics.setHotStreams(hotStreams);
        
        return metrics;
    }
    
    static class StreamMetrics {
        final int streamId;
        final String method;
        long startTime;
        long endTime;
        long duration;
        Status status;
        long bytesSent;
        long bytesReceived;
        int windowSize;
        
        StreamMetrics(int streamId, String method) {
            this.streamId = streamId;
            this.method = method;
        }
        
        double getWindowUsage() {
            return windowSize == 0 ? 0 : (double) bytesSent / windowSize;
        }
    }
}

八、总结

8.1 核心要点总结

  1. HTTP/2 流控制

    • 基于信用的流控制机制

    • 连接级别和流级别窗口控制

    • WINDOW_UPDATE帧动态调整窗口

  2. gRPC 消息分帧

    • 5字节gRPC帧头(压缩标志 + 长度)

    • 支持消息压缩(gzip)

    • 自动分片以适应HTTP/2帧大小限制

  3. 性能优化

    • 优先级调度确保关键流量

    • 动态窗口调整适应网络条件

    • 连接复用减少握手开销

  4. 错误处理

    • gRPC状态码到HTTP/2错误码的映射

    • 优雅的连接终止(GOAWAY)

    • 智能重试机制

8.2 最佳实践

  1. 窗口大小调优

    java

    复制

    下载

    复制代码
    // 根据网络延迟调整初始窗口
    int rtt = measureRTT();
    int initialWindow = Math.max(65535, rtt * bandwidth / 8);
  2. 消息大小控制

    java

    复制

    下载

    复制代码
    // 大消息自动分块
    if (messageSize > 1_000_000) { // 1MB
        enableCompression();
        useStreamingRPC();
    }
  3. 监控告警

    java

    复制

    下载

    复制代码
    // 关键指标监控
    if (windowUsage > 0.9) {
        alert("Flow control window nearly exhausted");
    }
    if (errorRate > 0.01) {
        alert("High error rate detected");
    }

gRPC的HTTP/2实现提供了高效的流控制和消息分帧机制,通过理解这些底层原理,可以更好地优化gRPC应用的性能和可靠性。

相关推荐
UrbanJazzerati7 小时前
Vue3 父子组件通信完全指南
前端·面试
UrbanJazzerati7 小时前
Vue 3 纯小白快速入门指南
前端·面试
华仔啊9 小时前
挖到了 1 个 Java 小特性:var,用完就回不去了
java·后端
JaguarJack9 小时前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo9 小时前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
SimonKing9 小时前
SpringBoot整合秘笈:让Mybatis用上Calcite,实现统一SQL查询
java·后端·程序员
NAGNIP19 小时前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP19 小时前
一文搞懂激活函数!
算法·面试
前端Hardy1 天前
面试官:JS数组的常用方法有哪些?这篇总结让你面试稳了!
javascript·面试
日月云棠1 天前
各版本JDK对比:JDK 25 特性详解
java