- 【手撸IM】专题初衷为实现一个分布式、高性能、支持多租户的 IM 云原型,主要目的为开源学习交流。
- 源码地址:https://gitee.com/bossfriday/bossfriday-nubybear
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实现;
运行效果截图: