解析 HTTP 请求,包括 GET/POST ,以及应对不同 Content-Type 的参数处理。
设计
从网络请求到会话,需要对 GET/POST 的请求,以及请求的参数类型 Content-Type 做细化的参数解析操作。同时按照 RPC 泛化调用的入参方式,将解析的参数封装处理。
实现 Mapping部分:
IGenericReference:invoke 方法参数变化,现在的参数变成了一个 Map,就是一个参数名和参数值的映射
MappedMethod:加入对于 POST 请求类型的处理逻辑
DataSource部分:
Session部分:
GatewaySession新增 post 方法 ,暂时还是用 get 方法实现
Socket部分:
新增了请求解析器,根据 GET 和 POST 进行参数解析,先解析 Header中的 Content-Type ,然后对应 POST 中不同的 Content-Type 进行不同的解析操作
Type部分:
新增 SimpleTypeRegistry 类型注册器:用于判断 parameterType, 如果是注册器中的基本数据类型,则将入参转为数组,如果是其它类型则用Object数组包装一下
核心在于 GatewayServerHandler 在接收到 HTTP 请求后,对参数信息的封装处理。
请求解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package cn.ray.gateway.socket.agreement;import com.alibaba.fastjson.JSON;import io.netty.buffer.ByteBuf;import io.netty.handler.codec.http.FullHttpRequest;import io.netty.handler.codec.http.HttpMethod;import io.netty.handler.codec.http.QueryStringDecoder;import io.netty.handler.codec.http.multipart.Attribute;import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.util.HashMap;import java.util.Map;import java.util.Optional;public class RequestParser { private final FullHttpRequest request; public RequestParser (FullHttpRequest request) { this .request = request; } public Map<String,Object> parse () { String contentType = getContentType(); HttpMethod method = request.method(); if (HttpMethod.GET == method) { Map<String, Object> parameterMap = new HashMap <>(); QueryStringDecoder decoder = new QueryStringDecoder (request.uri()); decoder.parameters().forEach( (key, value) -> parameterMap.put(key,value.get(0 ))); return parameterMap; } else if (HttpMethod.POST == method) { switch (contentType) { case "multipart/form-data" : Map<String, Object> parameterMap = new HashMap <>(); HttpPostRequestDecoder decoder = new HttpPostRequestDecoder (request); decoder.offer(request); decoder.getBodyHttpDatas().forEach( data -> { Attribute attribute = (Attribute) data; try { parameterMap.put(data.getName(), attribute.getValue()); } catch (IOException e) { e.printStackTrace(); } } ); return parameterMap; case "application/json" : ByteBuf byteBuf = request.content().copy(); if (byteBuf.isReadable()) { String content = byteBuf.toString(StandardCharsets.UTF_8); return JSON.parseObject(content); } break ; default : throw new RuntimeException ("未实现的协议类型 Content-Type : " + contentType); } } throw new RuntimeException ("未实现的请求类型 HttpMethod : " + method); } private String getContentType () { Optional<Map.Entry<String, String>> header = request.headers().entries().stream().filter( val -> val.getKey().equals("Content-Type" ) ).findAny(); Map.Entry<String, String> entry = header.orElse(null ); assert entry!=null ; String contentType = entry.getValue(); int indexOf = contentType.indexOf(";" ); if (indexOf>0 ){ return contentType.substring(0 ,indexOf); } else { return contentType; } } }
按照不同类型的请求和 Content-Type 做对应的流程处理,
GET 请求只需要用到 QueryStringDecoder 这个 Netty 提供的解析操作即可。POST 请求中有不同 Content-Type,一种是类似 FORM 表单的 multipart/form-data 类型,还有一种是大家非常常用的 application/json 类型,分别进行处理
首先通过 FullHttpRequest 获取请求的方法类型 GET/POST ,当然还有其他的这里暂时不需要添加。
获取类型后,按照 GET/POST 分别做解析处理。其实在网关中最常用的就是 POST 请求+application/json 的方式。后面加上 token 信息一并传递,避免网关接口被外部滥用的情况。
RPC 泛化调用 RPC 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package cn.ray.gateway.interfaces;import cn.ray.gateway.rpc.IActivityBooth;import cn.ray.gateway.rpc.dto.XReq;import com.alibaba.fastjson.JSON;import org.apache.dubbo.config.annotation.Service;@Service(version = "1.0.0") public class ActivityBooth implements IActivityBooth { @Override public String sayHi (String str) { return "hi " + str + " by api-gateway-test" ; } @Override public String insert (XReq req) { return "hi " + JSON.toJSONString(req) + " by api-gateway-test" ; } @Override public String test (String str, XReq req) { return "hi " + str + JSON.toJSONString(req) + " by api-gateway-test" ; } }
基本类型 + 单参 1 Object user = genericService.$invoke("sayHi" , new String []{"java.lang.String" }, new Object []{"world" });
对象类型 + 单参 1 2 3 4 Map<String, Object> params = new HashMap <>(); params.put("uid" , "10001" ); params.put("name" , "Ray" ); Object user = genericService.$invoke("insert" , new String []{"java.lang.String" , "cn.ray.gateway.rpc.dto.XReq" }, new Object []{params});
对象类型 + 多参 1 2 3 4 5 6 7 Map<String, Object> params01 = new HashMap <>(); params01.put("str" , "10001" ); Map<String, Object> params02 = new HashMap <>(); params02.put("uid" , "10001" ); params02.put("name" , "Ray" ); Object user = genericService.$invoke("test" , new String []{"java.lang.String" , "cn.ray.gateway.rpc.dto.XReq" }, new Object []{params01.values().toArray()[0 ], params02});
多参数请求(如 multipart/form-data)一般不在网关使用的原因有以下几点:
复杂性:多参数请求通常需要处理包含文件上传等复杂数据类型的请求体。网关的主要职责是路由和负载均衡 ,处理这些复杂请求需要额外的处理逻辑和资源。将这部分逻辑放在网关会增加网关的复杂性和开发成本 。
性能:处理多参数请求可能需要解析和处理大量数据,对网关的性能会产生负面影响 。网关应该专注于快速的请求转发和处理,而不是耗费大量资源来处理请求体中的每个参数。
横向扩展:多参数请求的处理可能需要更多的计算资源和存储资源。将这部分处理逻辑放在网关中,会限制网关的横向扩展能力。如果请求量增加,需要增加网关实例来处理更多的请求和负载,而这些实例需要额外的资源来处理多参数请求。
耦合性:将多参数请求的处理逻辑放在网关中,会将网关与具体的应用程序耦合在一起。这样会使得网关的代码更复杂,并且可能需要在网关代码中处理应用程序特定的逻辑。 这违反了网关的职责,应该将应用程序特定的逻辑留给应用程序本身来处理。
因此,一般建议将多参数请求的处理逻辑放在后端服务中,让网关专注于路由和负载均衡等核心功能。后端服务可以根据自身需要,使用专门的框架或库来处理多参数请求,以更好地管理和处理复杂的请求体数据。
会话中消息封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package cn.ray.gateway.session.defaults;import cn.ray.gateway.bind.IGenericReference;import cn.ray.gateway.datasource.Connection;import cn.ray.gateway.datasource.DataSource;import cn.ray.gateway.mapping.HttpStatement;import cn.ray.gateway.session.Configuration;import cn.ray.gateway.session.GatewaySession;import cn.ray.gateway.type.SimpleTypeRegistry;import org.apache.dubbo.config.ApplicationConfig;import org.apache.dubbo.config.ReferenceConfig;import org.apache.dubbo.config.RegistryConfig;import org.apache.dubbo.config.bootstrap.DubboBootstrap;import org.apache.dubbo.config.utils.ReferenceConfigCache;import org.apache.dubbo.rpc.service.GenericService;import java.util.Map;public class DefaultGatewaySession implements GatewaySession { private Configuration configuration; private String uri; private DataSource dataSource; public DefaultGatewaySession (Configuration configuration, String uri, DataSource dataSource) { this .configuration = configuration; this .uri = uri; this .dataSource = dataSource; } @Override public Object get (String methodName, Map<String,Object> parameters) { Connection connection = dataSource.getConnection(); HttpStatement httpStatement = configuration.getHttpStatement(uri); String parameterType = httpStatement.getParameterType(); return connection.execute( methodName, new String []{parameterType}, new String []{"ignore" }, SimpleTypeRegistry.isSimpleType(parameterType) ? parameters.values().toArray() : new Object []{parameters}); } @Override public Object post (String methodName, Map<String, Object> parameters) { return get(methodName, parameters); } @Override public IGenericReference getMapper () { return configuration.getMapper(uri, this ); } @Override public Configuration getConfiguration () { return configuration; } }
在网关会话的调用中,按照参数的类型封装请求信息。这里 post 请求暂时不做过多处理,只是调用get请求即可
测试 RPC 服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package cn.ray.gateway.interfaces;import cn.ray.gateway.rpc.IActivityBooth;import cn.ray.gateway.rpc.dto.XReq;import com.alibaba.fastjson.JSON;import org.apache.dubbo.config.annotation.Service;@Service(version = "1.0.0") public class ActivityBooth implements IActivityBooth { @Override public String sayHi (String str) { return "hi " + str + " by api-gateway-test" ; } @Override public String insert (XReq req) { return "hi " + JSON.toJSONString(req) + " by api-gateway-test" ; } @Override public String test (String str, XReq req) { return "hi " + str + JSON.toJSONString(req) + " by api-gateway-test" ; } }
启动网关 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package cn.ray.gateway.test;import cn.ray.gateway.mapping.HttpRequestType;import cn.ray.gateway.mapping.HttpStatement;import cn.ray.gateway.session.Configuration;import cn.ray.gateway.session.defaults.DefaultGatewaySessionFactory;import cn.ray.gateway.socket.GatewaySocketServer;import io.netty.channel.Channel;import org.junit.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.concurrent.ExecutionException;import java.util.concurrent.Executors;import java.util.concurrent.Future;public class ApiTest { private final Logger logger = LoggerFactory.getLogger(ApiTest.class); @Test public void test_GenericReference () throws InterruptedException, ExecutionException { Configuration configuration = new Configuration (); HttpStatement httpStatement01 = new HttpStatement ( "api-gateway-test" , "cn.ray.gateway.rpc.IActivityBooth" , "sayHi" , "/wg/activity/sayHi" , HttpRequestType.GET, "java.lang.String" ); HttpStatement httpStatement02 = new HttpStatement ( "api-gateway-test" , "cn.ray.gateway.rpc.IActivityBooth" , "insert" , "/wg/activity/insert" , HttpRequestType.POST, "cn.ray.gateway.rpc.dto.XReq" ); configuration.addMapper(httpStatement01); configuration.addMapper(httpStatement02); DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory (configuration); GatewaySocketServer server = new GatewaySocketServer (gatewaySessionFactory); Future<Channel> future = Executors.newFixedThreadPool(2 ).submit(server); Channel channel = future.get(); if (null == channel) throw new RuntimeException ("netty server start error channel is null" ); while (!channel.isActive()) { logger.info("netty server gateway start Ing ..." ); Thread.sleep(500 ); } logger.info("netty server gateway start Done! {}" , channel.localAddress()); Thread.sleep(Long.MAX_VALUE); } }
测试 GET 请求:
将请求参数封装至 Params 中,并要在Headers中添加Content-Type。 米
其实 Content-Type 对于 GET 请求是无用的,但在代码中井未做区分。
所以测试验证时,要么改 RequestParser#getContentType 中的逻辑,要么在请求的Headers加上Content-Type,请求网关
测试POST请求:
要在Headers中添加 Content-Type = application/json,并在 Body 中以JSON的方式添加参数,请求网关
测试结果 GET
POST
思考 因为 GET 请求其实用不到 Content-Type,我们也不会去处理它,只会去解析 uri 里的内容,然后在此处的断言逻辑是,Content-type不为空,其实就有问题,在网页里直接GET请求会报错,所以应该把它迁移到POST请求里,只在 POST 请求去区分请求类型。
感觉可以考虑用一下策略模式,用个 Adaptor 去中转,把 POST 和 GET 的 Parser 区分成两个类,然后在 Adaptor 里面去判断是什么类型之后用哪一种 Parser。