第五章 HTTP请求参数解析

解析 HTTP 请求,包括 GET/POST ,以及应对不同 Content-Type 的参数处理。

设计

5-设计

从网络请求到会话,需要对 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数组包装一下

5-实现

核心在于 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;

/**
* @author Ray
* @date 2023/5/16 15:29
* @description 请求解析器,解析 HTTP 请求,GET/POST, form-data/raw-json
*/
public class RequestParser {

private final FullHttpRequest request;

public RequestParser(FullHttpRequest request) {
this.request = request;
}

public Map<String,Object> parse() {
// 获取 Content-Type
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`类型是Java 8中引入的用于表示可能为空的值的容器类型。
// 通过使用`Optional`,可以更好地处理可能出现`null`的情况,避免空指针异常。
//1. `request.headers()`:`request`是一个请求对象,这里调用了`headers()`方法来获取请求头。
//2. `.entries()`:这个方法用于将请求头转换为一个包含键值对的集合,每个键值对表示一个请求头的键和值。
//3. `.stream()`:将集合转换为一个流,以便进行后续的操作。
//4. `.filter(...)`:这里使用`filter`方法对流中的元素进行筛选。筛选条件是键名等于"Content-Type"的元素。
//5. `val -> val.getKey().equals("Content-Type")`:这是一个Lambda表达式,它表示筛选条件。`val`代表流中的元素,这里指的是请求头的一个键值对。`val.getKey()`返回键的值,然后与"Content-Type"进行比较。
//6. `.findAny()`:找到任意一个满足筛选条件的元素。如果存在符合条件的元素,则返回一个`Optional`对象,否则返回空的`Optional`对象。
//最终,`header`变量将包含满足条件的键值对,如果存在的话。
Optional<Map.Entry<String, String>> header = request.headers().entries().stream().filter(
val -> val.getKey().equals("Content-Type")
).findAny();

// 如果 header 存在 Content-Type ,那么该值将被返回;如果header为空,那么null将被返回。
Map.Entry<String, String> entry = header.orElse(null);
assert entry!=null;
// application/json、multipart/form-data;
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;

/**
* @author Ray
* @date 2023/5/12 22:36
* @description
*/
@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. 耦合性:将多参数请求的处理逻辑放在网关中,会将网关与具体的应用程序耦合在一起。这样会使得网关的代码更复杂,并且可能需要在网关代码中处理应用程序特定的逻辑。这违反了网关的职责,应该将应用程序特定的逻辑留给应用程序本身来处理。

因此,一般建议将多参数请求的处理逻辑放在后端服务中,让网关专注于路由和负载均衡等核心功能。后端服务可以根据自身需要,使用专门的框架或库来处理多参数请求,以更好地管理和处理复杂的请求体数据。

会话中消息封装

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;

/**
* @author Ray
* @date 2023/5/13 17:56
* @description 默认网关会话实现类
*/
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;

/**
* @author Ray
* @date 2023/5/12 22:36
* @description
*/
@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;

/**
* @author Ray
* @date 2023/5/11 16:26
* @description
*/
public class ApiTest {

private final Logger logger = LoggerFactory.getLogger(ApiTest.class);

@Test
public void test_GenericReference() throws InterruptedException, ExecutionException {
// 1. 创建配置信息加载注册
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);

// 2. 基于配置构建会话工厂
DefaultGatewaySessionFactory gatewaySessionFactory = new DefaultGatewaySessionFactory(configuration);

// 3. 创建启动网关网络服务
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

5-测试-1

POST

5-测试-2

思考

因为 GET 请求其实用不到 Content-Type,我们也不会去处理它,只会去解析 uri 里的内容,然后在此处的断言逻辑是,Content-type不为空,其实就有问题,在网页里直接GET请求会报错,所以应该把它迁移到POST请求里,只在 POST 请求去区分请求类型。

感觉可以考虑用一下策略模式,用个 Adaptor 去中转,把 POST 和 GET 的 Parser 区分成两个类,然后在 Adaptor 里面去判断是什么类型之后用哪一种 Parser。