HTTP请求parse流程、RequestLineParser、HttpQueryParameterParser
根据标准,HTTP请求消息格式如下:
Request-Line
*(( general-header
| request-header
| entity-header ) CRLF)
CRLF
[ message-body ]
所有如果要将HTTP请求从输入流中parse为Java对象,需要完成3个步骤:
- 解析Request-Line
- 解析headers
- 解析body
让我们首先分析最简单的解析Request-Line。
解析Request-Line
根据标准Request-Line的格式如下:
Method SP Request-URI SP HTTP-Version CRLF
其中Request-URI是客户端使用URLEncode之后的URI,即如果元素URI中包含非ASCII字符,客户端必须将其编码为 %编码值 的形式。
分为4段:
- 第一段是HTTP方法名,即GET、POST、PUT等。
- 第二段是请求路径,如 /index.html
- 第三段是HTTP协议版本,一般是HTTP/1.1。
- 最后是一个CRLF回车换行。
除了CRLF,段与段之间用空格分隔。综上所述,解析步骤为:
- 去掉行尾的CRLF,并trim两端的空格。
- 使用空格将字符串分为大小为3的数组a。
- method是a[0],Request-URI是a[1],HTTP-Version是a[2]。
代码如下:
public class DefaultRequestLineParser implements RequestLineParser {
private static final String SPLITTER = "\\s+";
private static final String CRLF = "\r\n";
@Override
public RequestLine parse(String startLine) {
//去掉末尾的CRLF和空格,转化为Method SP Request-URI SP HTTP-Version
String str = startLine.replaceAll(CRLF, "").trim();
String[] parts = str.split(SPLITTER);
//数组格式{Method,Request-URI,HTTP-Version}
if (parts.length == 3) {
String method = parts[0];
URI uri = URI.create(parts[1]);
String httpVersion = parts[2];
return new RequestLine(method, uri, httpVersion);
}
throw new ParserException("startline format illegal");
}
}
构造QueryParameters
当提交的请求包含查询信息,如 /person?age=20&gender=male,Request-URI将包含这些信息。我们可以将查询信息构造为对象,并保存到HttpMessage接口中。
解析步骤也很简单:
- 调用Request-Line构造而来的URI的getQuery()方法,获取到query字符串。
- 使用&将query字符串拆分为key-value字符串组成的数组。
- 遍历这个数组,将key-value字符串转换为HttpQueryParameter对象。
public class DefaultHttpQueryParameterParser implements HttpQueryParameterParser {
private final HttpQueryParameters httpQueryParameters;
private static final String SPLITTER = "&";
private static final String KV_SPLITTER = "=";
public DefaultHttpQueryParameterParser() {
this.httpQueryParameters = new HttpQueryParameters();
}
@Override
public HttpQueryParameters parse(String queryString) {
if (queryString.contains(SPLITTER)) {
String[] keyValues = queryString.split(SPLITTER);
for (String keyValue : keyValues) {
if (keyValue.contains(KV_SPLITTER)) {
String[] temp = keyValue.split(KV_SPLITTER);
if (temp.length == 2) {
this.httpQueryParameters
.addQueryParameter(new HttpQueryParameter(temp[0], temp[1]));
}
}
}
}
return this.httpQueryParameters;
}
}
public class HttpQueryParameters {
private final LinkedListMultimap<String, HttpQueryParameter> prametersMultiMap;
public HttpQueryParameters() {
this.prametersMultiMap = LinkedListMultimap.create();
}
public List<HttpQueryParameter> getQueryParameter(String name) {
return this.prametersMultiMap.get(name);
}
public HttpQueryParameter getFirstQueryParameter(String name) {
if (this.prametersMultiMap.containsKey(name)) {
return this.prametersMultiMap.get(name).get(0);
}
return null;
}
public List<HttpQueryParameter> getQueryParameters() {
return this.prametersMultiMap.values();
}
public void addQueryParameter(HttpQueryParameter httpQueryParameter) {
this.prametersMultiMap.put(httpQueryParameter.getName(), httpQueryParameter);
}
public void removeQueryParameter(HttpQueryParameter httpQueryParameter) {
this.prametersMultiMap.remove(httpQueryParameter.getName(), httpQueryParameter);
}
public void removeQueryParameter(String httpQueryParameter) {
this.prametersMultiMap.removeAll(httpQueryParameter);
}
public boolean hasRequestParameter(String httpQueryParameter) {
return this.prametersMultiMap.containsKey(httpQueryParameter);
}
public Set<String> getQueryParameterNames() {
return this.prametersMultiMap.keySet();
}
}
HttpQueryParameters中使用guava的LinkedListMultimap保存HttpQueryParameters,以支持同名参数的场景,并保证遍历时访问HttpQueryParameters的顺序,和添加时一致。
单元测试:
测试Request Line的解析
public class TestDefaultRequestLineParser {
private static final Logger
LOGGER = LoggerFactory.getLogger(TestDefaultRequestLineParser.class);
@Test
public void test() {
DefaultRequestLineParser defaultRequestLineParser
= new DefaultRequestLineParser();
RequestLine result = defaultRequestLineParser.parse("GET /hello.txt HTTP/1.1\r\n");
String method = result.getMethod();
assertEquals("GET", method);
final URI requestURI = result.getRequestURI();
assertEquals(URI.create("/hello.txt"), requestURI);
LOGGER.info(requestURI.getQuery());
LOGGER.info(requestURI.getFragment());
assertEquals("HTTP/1.1", result.getHttpVersion());
}
@Test
public void testQuery() {
DefaultRequestLineParser defaultRequestLineParser
= new DefaultRequestLineParser();
RequestLine result = defaultRequestLineParser.parse("GET /test?a=123&a1=1&b=456 HTTP/1.1\r\n");
String method = result.getMethod();
assertEquals("GET", method);
final URI requestURI = result.getRequestURI();
assertEquals(URI.create("/test?a=123&a1=1&b=456"), requestURI);
LOGGER.info(requestURI.getQuery());
assertEquals("HTTP/1.1", result.getHttpVersion());
}
}
测试Query字符串解析
public class TestDefaultHttpQueryParameterParser {
@Test
public void test() {
String queryStr = "a=123&a1=1&b=456&a=321";
DefaultHttpQueryParameterParser httpRequestParameterParser
= new DefaultHttpQueryParameterParser();
HttpQueryParameters result = httpRequestParameterParser.parse(queryStr);
List<HttpQueryParameter> parameters = result.getQueryParameter("a");
assertNotNull(parameters);
assertEquals(2, parameters.size());
assertEquals("123", parameters.get(0).getValue());
assertEquals("321", parameters.get(1).getValue());
}
}
完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step8/
分支step8