【手撸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

java 复制代码
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

java 复制代码
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

java 复制代码
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)

java 复制代码
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)

java 复制代码
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

java 复制代码
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实现;

运行效果截图:

相关推荐
北京耐用通信3 小时前
耐达讯自动化Modbus RTU转Profibus,让电磁阀连接从此与众不同!
网络·人工智能·网络协议·网络安全·自动化
漫谈网络3 小时前
什么是RDMA?—— 一场网络通信的范式革命
运维·服务器·网络
GISer_Jing5 小时前
Windows如何查看端口是否占用,并结束端口进程
网络·windows
爱吃KFC的大肥羊5 小时前
应用层协议——HTTP
网络·网络协议·http
计算机小手5 小时前
内网穿透系列十四:基于Websocket传输协议实现的异地组网工具 candy,简单安全稳定
经验分享·网络协议·docker·开源软件
独行soc5 小时前
2025年渗透测试面试题总结-100(题目+回答)
网络·安全·web安全·网络安全·adb·渗透测试·安全狮
boy快快长大6 小时前
【面试题】HTTP与HTTPS的区别
网络协议·http·https
wangjialelele8 小时前
OSI模型、网络地址、与协议
linux·服务器·网络·tcp/ip
哈乐9 小时前
VRRP 和堆叠
网络