【手撸IM】高性能HTTP API服务设计与实现

1. 背景

在互联网应用快速发展的今天,后端服务对 高性能、高并发、低延迟 的需求越来越强烈。传统基于 Servlet 的 HTTP 框架(如 Spring Boot + Tomcat/Jetty)虽然生态丰富,但在性能和灵活性上存在一定的瓶颈。本文将介绍一种基于 Netty 与 ActorRPC 的高性能 Http API 服务架构方案,并结合实际代码实现进行讲解。

2. 方案对比

传统的Spring Boot 方案虽然开发简单,但是效率不高,毕竟需要完整的 Servlet 容器栈,而Servlet 规范决定了很多额外开销,例如:Filter 链、Request/Response 包装、线程池调度等。同时,Spring Boot调用链较长,需要穿过 DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → 序列化层 → Response 包装。

Netty + ActorRPC 则是“定制化高性能”方案,在高并发、低延迟场景下能比 Spring Boot 延迟低、内存开销小,支撑更高连接数。毕竟使用Netty可以直接操作 ChannelHandlerContext,协议解析没有 Servlet 的冗余,同时Netty是一个优秀的NIO框架,基于 事件驱动 + Reactor 模型,通常是 少量 IO 线程 + 任务线程池。而ActorRPC天然异步,内部消息传递直接是对象或轻量级二进制协议,tell 回 sender 时避免了额外的 HTTP 调度(sender 保存了 ChannelHandlerContext ctx,业务逻辑完成后直接 ctx.writeAndFlush() HTTP应答),这样可以做到调用链更短,对象拷贝和方法调用次数更少。

综上,如果是IM、游戏、实时风控、网关等要求高性能的场景下选择后者明显更好,如果是云服务,为了减少服务器成本开销也应当首选后者。当然利弊往往是相生相伴的,使用Netty + ActorRPC方案开发成本高、维护复杂度高,毕竟缺乏缺乏 Spring Boot 的生态支持很多基础工作需要自行实现。

3. 核心代码实现

3.1 流程

在这里插入图片描述

3.2 核心代码实现

3.2.1 HttpApiServerHandler

package cn.bossfriday.im.api.http;

import cn.bossfriday.common.http.HttpProcessorMapper;
import cn.bossfriday.common.http.IHttpProcessor;
import cn.bossfriday.im.api.helper.ApiHelper;
import cn.bossfriday.im.common.api.ApiResponseHelper;
import cn.bossfriday.im.common.entity.result.ResultCode;
import cn.bossfriday.im.common.enums.api.ApiRequestType;
import cn.bossfriday.im.common.helper.AppHelper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;

import java.net.URI;
import java.util.Objects;

import static cn.bossfriday.im.common.constant.ApiConstant.*;

/**
 * HttpApiServerHandler
 *
 * @author chenx
 */
@Slf4j
public class HttpApiServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        FullHttpRequest httpRequest = null;
        try {
            if (msg instanceof FullHttpRequest) {
                httpRequest = (FullHttpRequest) msg;
                this.onMessageReceived(ctx, httpRequest);
            }
        } finally {
            if (httpRequest != null && httpRequest.refCnt() > 0) {
                httpRequest.release();
            }
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("HttpApiServerHandler.exceptionCaught()", cause);
        if (ctx.channel().isActive()) {
            ctx.channel().close();
        }
    }

    /**
     * onMessageReceived
     */
    private void onMessageReceived(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {
        try {
            URI uri = new URI(httpRequest.uri());
            ApiRequestType requestType = ApiRequestType.find(httpRequest.method().name(), uri);
            if (Objects.isNull(requestType)) {
                ApiResponseHelper.sendApiResponse(ctx, ResultCode.API_UNSUPPORTED);
                return;
            }

            ResultCode authResult = ApiHelper.auth(httpRequest);
            if (authResult.getCode() != ResultCode.OK.getCode()) {
                ApiResponseHelper.sendApiResponse(ctx, authResult);
                return;
            }

            String apiVersion = requestType.getUrlParser().parsePath(uri).get(HTTP_URL_ARGS_API_VERSION);
            long appId = AppHelper.getAppId(httpRequest.headers().get(HTTP_HEADER_APP_KEY));
            ctx.channel().attr(ATTRIBUTE_KEY_API_VERSION).set(apiVersion);
            ctx.channel().attr(ATTRIBUTE_KEY_APP_ID).set(appId);

            IHttpProcessor processor = HttpProcessorMapper.getHttpProcessor(requestType.getApiRouteKey());
            processor.process(ctx, httpRequest);
        } catch (Exception ex) {
            log.error("HttpApiServerHandler.onMessageReceived() error!", ex);
            ApiResponseHelper.sendApiResponse(ctx, ResultCode.SYSTEM_ERROR);
        }
    }
}

3.2.2 ApiRequestType

package cn.bossfriday.im.common.enums.api;

import cn.bossfriday.common.exception.ServiceRuntimeException;
import cn.bossfriday.common.http.UrlParser;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.netty.handler.codec.http.HttpMethod;
import lombok.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static cn.bossfriday.im.common.constant.ApiConstant.*;

/**
 * ApiRequestType
 *
 * @author chenx
 */
public enum ApiRequestType {

    /**
     * client api
     */
    CLIENT_NAV(API_ROUTE_KEY_CLIENT_NAV, HttpMethod.POST.name(), new UrlParser(String.format("/api/{%s}/client/nav", HTTP_URL_ARGS_API_VERSION))),

    /**
     * user api
     */
    USER_GET_TOKEN(API_ROUTE_KEY_USER_GET_TOKEN, HttpMethod.POST.name(), new UrlParser(String.format("/api/{%s}/user/getToken", HTTP_URL_ARGS_API_VERSION))),
    ;

    @Getter
    private String apiRouteKey;

    @Getter
    private String httpMethod;

    @Getter
    private UrlParser urlParser;

    ApiRequestType(String apiRouteKey, String httpMethod, UrlParser urlParser) {
        this.apiRouteKey = apiRouteKey;
        this.httpMethod = httpMethod;
        this.urlParser = urlParser;
    }

    private static final Map<String, List<ApiRequestType>> API_REQUEST_TYPE_MAP = Maps.newHashMap();

    static {
        for (ApiRequestType entry : ApiRequestType.values()) {
            List<ApiRequestType> apiRequestTypeList = API_REQUEST_TYPE_MAP.get(entry.httpMethod);
            if (Objects.isNull(apiRequestTypeList)) {
                apiRequestTypeList = Lists.newArrayList();
                API_REQUEST_TYPE_MAP.put(entry.httpMethod, apiRequestTypeList);
            }

            apiRequestTypeList.add(entry);
        }
    }

    /**
     * getByMethod
     *
     * @param httpMethod
     * @return
     */
    public static List<ApiRequestType> getByMethod(String httpMethod) {
        if (StringUtils.isEmpty(httpMethod)) {
            throw new ServiceRuntimeException("httpMethod is empty!");
        }

        return API_REQUEST_TYPE_MAP.get(httpMethod);
    }

    /**
     * find
     * <p>
     * find方法实际上是一个遍历查找,如果ApiRequestType.getByMethod(httpMethod)返回的list条目较多时,效率可能不好。
     * 后续备选优化方案:使用 Trie(前缀树)来提高匹配速度。构建 Trie 结构后,查找 URL 不需要遍历 List,时间复杂度降低为 O(m)(m 为 URL 片段数)。
     *
     * @param httpMethod
     * @param uri
     * @return
     */
    public static ApiRequestType find(String httpMethod, URI uri) {
        List<ApiRequestType> list = ApiRequestType.getByMethod(httpMethod);
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }

        for (ApiRequestType entry : list) {
            if (entry.getUrlParser().isMatch(uri)) {
                return entry;
            }
        }

        return null;
    }
}

3.2.3 HttpProcessorMapper

package cn.bossfriday.common.http;

import cn.bossfriday.common.exception.ServiceRuntimeException;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

/**
 * HttpProcessorMapper
 *
 * @author chenx
 */
public class HttpProcessorMapper {

    private static HashMap<String, Class<? extends IHttpProcessor>> processorMapper = new HashMap<>();

    private HttpProcessorMapper() {
        // do nothing
    }

    /**
     * putHttpProcessor
     *
     * @param apiRouteKey
     * @param httpProcessor
     */
    public static Class<? extends IHttpProcessor> putHttpProcessor(String apiRouteKey, Class<? extends IHttpProcessor> httpProcessor) {
        return processorMapper.putIfAbsent(apiRouteKey, httpProcessor);
    }

    /**
     * getHttpProcessor
     *
     * @param apiRouteKey
     * @return
     */
    public static IHttpProcessor getHttpProcessor(String apiRouteKey) throws InstantiationException,
            IllegalAccessException,
            NoSuchMethodException,
            InvocationTargetException {
        if (!contains(apiRouteKey)) {
            throw new ServiceRuntimeException("IHttpProcessor not existed! apiRouteKey=" + apiRouteKey);
        }

        Class<? extends IHttpProcessor> processor = processorMapper.get(apiRouteKey);

        return processor.getConstructor().newInstance();
    }

    /**
     * contains
     *
     * @param apiRouteKey
     * @return
     */
    public static boolean contains(String apiRouteKey) {
        return processorMapper.containsKey(apiRouteKey);
    }
}

3.2.4 GetTokenProcessor(HttpProcessor)

package cn.bossfriday.im.api.processor.user;

import cn.bossfriday.common.register.HttpApiRoute;
import cn.bossfriday.common.rpc.actor.ActorRef;
import cn.bossfriday.im.api.actor.ApiAckActor;
import cn.bossfriday.im.common.api.BaseHttpProcessor;
import cn.bossfriday.im.common.message.api.user.GetTokenRequest;
import cn.bossfriday.im.common.message.rpc.user.GetTokenInput;
import cn.bossfriday.im.common.rpc.message.ApiMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;

import static cn.bossfriday.im.common.constant.ApiConstant.API_ROUTE_KEY_USER_GET_TOKEN;
import static cn.bossfriday.im.common.constant.ImConstant.ACTOR_USER_GET_TOKEN;

/**
 * GetTokenProcessor
 *
 * @author chenx
 */
@Slf4j
@HttpApiRoute(apiRouteKey = API_ROUTE_KEY_USER_GET_TOKEN)
public class GetTokenProcessor extends BaseHttpProcessor {

    @Override
    protected void doRequest(ChannelHandlerContext ctx, FullHttpRequest httpRequest, String apiVersion, long appId) {
        GetTokenRequest request = this.getRequestPayload(httpRequest, GetTokenRequest.class);
        GetTokenInput input = GetTokenInput.builder()
                .userId(request.getUserId())
                .userName(request.getUserName())
                .deviceId(request.getDeviceId())
                .build();
        ApiMessage apiMessage = this.getApiMessage(apiVersion,
                appId,
                ACTOR_USER_GET_TOKEN,
                request.getUserId(),
                request.getUserId(),
                input);
        
        ActorRef sender = this.getSender(ApiAckActor.class, ctx);
        this.routeMessage(apiMessage, sender);
    }
}

3.2.5 GetTokenActor(ProcessActor)

package cn.bossfriday.im.user.actors;

import cn.bossfriday.common.plugin.PluginSpringContext;
import cn.bossfriday.common.register.ActorRoute;
import cn.bossfriday.common.rpc.actor.ActorRef;
import cn.bossfriday.im.common.codec.ImTokenCodec;
import cn.bossfriday.im.common.db.entity.AppInfo;
import cn.bossfriday.im.common.entity.ImToken;
import cn.bossfriday.im.common.entity.result.Result;
import cn.bossfriday.im.common.helper.AppHelper;
import cn.bossfriday.im.common.message.rpc.user.GetTokenInput;
import cn.bossfriday.im.common.message.rpc.user.GetTokenOutput;
import cn.bossfriday.im.common.rpc.BaseActor;
import cn.bossfriday.im.common.service.UserInfoService;
import lombok.extern.slf4j.Slf4j;

import static cn.bossfriday.im.common.constant.ImConstant.ACTOR_USER_GET_TOKEN;
import static cn.bossfriday.im.common.entity.result.ResultCode.SYSTEM_ERROR;

/**
 * GetTokenActor
 *
 * @author chenx
 */
@Slf4j
@ActorRoute(methods = ACTOR_USER_GET_TOKEN)
public class GetTokenActor extends BaseActor<GetTokenInput> {

    @Override
    public void onMessageReceived(GetTokenInput msg) {
        try {
            long appId = this.getContext().getAppId();
            String uid = msg.getUserId();
            String deviceId = msg.getDeviceId();
            long time = System.currentTimeMillis();

            // create token
            AppInfo appInfo = AppHelper.getAppInfo(appId);
            ImToken imToken = new ImToken(appId, appInfo.getAppSecret(), uid, deviceId, time);
            String token = ImTokenCodec.encode(imToken);

            // register user
            UserInfoService userInfoService = PluginSpringContext.getBean(UserInfoService.class);
            userInfoService.register(appId, uid, msg.getUserName());

            GetTokenOutput output = GetTokenOutput.builder()
                    .token(token)
                    .userId(msg.getUserId())
                    .build();
            this.getSender().tell(Result.ok(output), ActorRef.noSender());
        } catch (Exception ex) {
            log.error("GetTokenActor.onMessageReceived() error!", ex);
            this.getSender().tell(Result.error(SYSTEM_ERROR), ActorRef.noSender());
        }
    }
}

3.2.6 ApiAckActor

package cn.bossfriday.im.api.actor;

import cn.bossfriday.common.register.ActorRoute;
import cn.bossfriday.common.rpc.actor.BaseUntypedActor;
import cn.bossfriday.im.common.api.ApiResponseHelper;
import cn.bossfriday.im.common.entity.result.Result;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;

import static cn.bossfriday.im.common.constant.ImConstant.ACTOR_API_ACK;
import static cn.bossfriday.im.common.entity.result.ResultCode.API_UNSUPPORTED_API_ACK_MESSAGE_TYPE;
import static cn.bossfriday.im.common.entity.result.ResultCode.SYSTEM_ERROR;

/**
 * ApiAckActor:公共API回调Actor
 *
 * @author chenx
 */
@Slf4j
@ActorRoute(methods = ACTOR_API_ACK)
public class ApiAckActor extends BaseUntypedActor {

    private ChannelHandlerContext ctx;

    public ApiAckActor(ChannelHandlerContext ctx) {
        this.ctx = ctx;
    }

    @Override
    public void onMsgReceive(Object msg) {
        try {
            if (msg instanceof Result) {
                ApiResponseHelper.sendApiResponse(this.ctx, (Result<?>) msg);
                return;
            }

            ApiResponseHelper.sendApiResponse(this.ctx, API_UNSUPPORTED_API_ACK_MESSAGE_TYPE);
        } catch (Exception ex) {
            log.error("ApiAckActor.onMsgReceive() error!", ex);
            ApiResponseHelper.sendApiResponse(this.ctx, SYSTEM_ERROR);
        }
    }
}

4. 总结

从上面的主要代码可以看出,虽然缺乏 Spring Boot 的生态支持很多基础工作需要自行实现,但是一些基础代码实现之后如果要新加一个接口那么只需要如下3步,想想其实跟Spring Boot也差不多是吧,但是性能上却能获得极大收益。
1、ApiRequestType扩展一个枚举值; --类比SpringBoot中写RequestMapping注解;
2、新增一个BaseHttpProcessor实现; --类比加Controller中的方法;
3、新增一个BaseActor实现; --类比常规的Service实现;

运行效果截图:
在这里插入图片描述

5. 后续优化

之前提到了ApiRequestType.find()方法是一个对所有枚举值的遍历查找,同时还需要执行一个UrlParser.isMatch()方法,当API较多时这种处理效率不高可以用前缀树的方式进行优化,现补充主要优化代码(完整代码请查看gitee上的源码):

package cn.bossfriday.im.common.enums.api;

import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * ApiTrieTree
 *
 * @author chenx
 */
public class ApiTrieTree {

    /**
     * TrieNode
     */
    static class TrieNode {

        Map<String, TrieNode> fixedChildren = new HashMap<>();

        Map<String, TrieNode> variableChildrenMap = new HashMap<>();

        ApiRequestType apiRequestType;

        String variableName;
    }

    private final Map<String, TrieNode> methodTrieNodeMap = new HashMap<>();

    /**
     * insert
     *
     * @param type
     */
    public void insert(ApiRequestType type) {
        TrieNode root = this.methodTrieNodeMap.computeIfAbsent(type.getHttpMethod(), k -> new TrieNode());

        // 去掉首空字符串 ""
        String[] segments = Arrays.stream(type.getUrlParser().getPatternSegments())
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);

        TrieNode node = root;
        for (String seg : segments) {
            if (this.isVariable(seg)) {

                // 如果同一级变量节点已存在,直接复用
                node = node.variableChildrenMap.computeIfAbsent(seg, k -> {
                    TrieNode newNode = new TrieNode();
                    newNode.variableName = seg;

                    return newNode;
                });
            } else {
                node = node.fixedChildren.computeIfAbsent(seg, k -> new TrieNode());
            }
        }

        node.apiRequestType = type;
    }

    /**
     * find
     *
     * @param method
     * @param uri
     * @return
     */
    public ApiRequestType find(String method, URI uri) {
        TrieNode root = this.methodTrieNodeMap.get(method);
        if (Objects.isNull(root)) {
            return null;
        }

        // 去掉首空字符串 ""
        String[] segments = Arrays.stream(uri.getPath().split("/"))
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);

        return this.match(root, segments, 0);
    }

    /**
     * match(递归匹配)
     */
    private ApiRequestType match(TrieNode node, String[] segments, int index) {
        if (Objects.isNull(node)) {
            return null;
        }
        if (index == segments.length) {
            return node.apiRequestType;
        }

        String seg = segments[index];

        // 优先匹配固定路径
        TrieNode next = node.fixedChildren.get(seg);
        if (!Objects.isNull(next)) {
            ApiRequestType result = this.match(next, segments, index + 1);
            if (!Objects.isNull(result)) {
                return result;
            }
        }

        /**
         * 尝试变量节点: 循环次数极少(通常 = 1)
         * POST
         *  └─ api
         *      └─ {apiVersion}  <-- 唯一节点
         *          ├─ client/nav
         *          └─ user/getToken
         */
        for (TrieNode varNode : node.variableChildrenMap.values()) {
            ApiRequestType result = this.match(varNode, segments, index + 1);
            if (!Objects.isNull(result)) {
                return result;
            }
        }

        return null;
    }

    /**
     * isVariable
     */
    private boolean isVariable(String seg) {
        return seg.startsWith("{") && seg.endsWith("}");
    }
}

在这里插入图片描述

基于Open-IM-Server、使用go-zero框架编写的IM服务Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

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

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

打赏作者

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

抵扣说明:

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

余额充值