相信大多数做业务开发的同学,调用的业务服务大多以Restfull API的形式存在,特别是跨部门调用或公司外部的API服务。此时,一个用于发送请求的http client是必不可少的。好奇心强的你, 一定会尝试自己造个轮子。
这过程中能学到很多东西, 比方说:
网络编程相关的知识
http协议知识
编译原理相关的知识
本篇文章将重点聚焦在解析http 协议上,我将大概介绍下http 协议的response部分合并使用Nom解析http response
Http Response用chrome打开 百度,https://www.baidu.com/,并F12查看, 将看到以下response响应
HTTP/1.1 200 OK
Bdpagetype: 2
Bdqid: 0xc5fcbcd300117410
Cache-Control: private
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Sat, 03 Apr 2021 15:59:39 GMT
Expires: Sat, 03 Apr 2021 15:59:39 GMT
Server: BWS/1.1
Set-Cookie: BDSVRTM=335; path=/
Set-Cookie: BD_HOME=1; path=/
Set-Cookie: H_PS_PSSID=33801_33636_33260_33344_31660_33691_33676_33713; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
Traceid: 1617465579022134426614266485334028153872
X-Ua-Compatible: IE=Edge,chrome=1
Transfer-Encoding: chunked
从上面的消息格式可以看到消息分为2部分 第一部分为 状态行,格式为 HTTP/versionversioncode $msg, 如 HTTP/1.1 200 OK
第二部分为请求头信息(包含多个请求头) 格式为 header_name:headername:header_value, 如 Bdpagetype: 2
另外还有第三部分,没直接显示在chrome devtools的 headers里,该部分就是消息返回的具体内容。总的来说一个完整的http 响应,格式如下 HTTP/versionversioncode msgmsgCRLF header_name1:headername1:header_value1 CRLFCRLFheader_name2: header_value2headervalue2CRLF ... //其他消息头 CRLFCRLFbody
注: 这里以 '$'开头的是变量,如开头的是变量,如CRLF 就是 \r\n
$version 是http的版本号如 1.1, 2
$code 是http状态码,如 200, 400, 500
$msg 是状态信息,如 OK
header_name,headername,header_value是消息头和值,之间由: 隔开
$body是消息体内容,可为空
为了简单起见,我们假定消息头总有Content-Length字段,不包含chunked,transfer-encoding. 这样 body的长度就由 Content-Length指定。
完整的http协议,请参考 https://tools.ietf.org/html/rfc7230
Nom简介Nom是由Rust写的一个解析器组合子,使用Rust可以对数据进行解析,可以做词法分析,语法分析等,完整介绍请参考https://github.com/Geal/nom
基本概念1.解析器
解析器是一个高阶函数,输入通常为匹配的条件(可以是具体的参数如字符串,也可以是一个返回bool的函数), 输出的是一个函数,如tag函数,该函数匹配一个字符串。
let num_fn = tag("1024"); //匹配数字1024,返回一个函数
let num = num_fn("1024ab"); 执行匹配函数,返回匹配结果
2.解析结果
每个解析器执行后,都返回一个解析结果,类型为 IResult,其定义如下
pub type IResult<I, O, E=(I, ErrorKind)> = Result<(I, O), Err<E>>;
pub enum Err<E> {
Incomplete(Needed),
Error(E),
Failure(E),
}
该类型有3个类型参数
I: 表示匹配完成后的剩余输入
O: 表示匹配到的结果
E: 表示匹配失败
需要注意返回的Result里面 I, O是作为一个整体元组的方式返回的
看下例子,就容易理解了
fn main() {
let one_tow_three:IResult<&str, &str> = tag("123")("123abc");
dbg!(one_tow_three);
}
输出:
[src\main.rs:28] one_tow_three = Ok(
(
"abc", // 这里是IResult中的 I,表示匹配完成后的剩余输入
"123",//这里表示IResult中的O, 表示匹配到的结果
),
)
3.复合解析器
复合解析器也是一个解析器,它将多个解析器组合为一个新的解析器。继续看代码
fn main() {
// pair解析器将两个解析器组合起来,这两个解析器将顺序的进行匹配,最后输出结果为元组
let result:IResult<&str, (&str, &str)> = pair(tag("123"), tag("abc"))("123abc9999");
dbg!(result);
}
输出:
[src\main.rs:31] result = Ok(
(
"9999", //剩余输入
( //输出,结果为元组
"123", // tag("123") 匹配到的
"abc", // tag("abc") 匹配到的
),
),
)
协议解析实现我们先定义2个结构体,用来表示http response格式
#[derive(Debug)]
pub struct StatusLine { //状态行
pub status: u16, //状态码
pub msg: String, //状态消息
}
#[derive(Debug)]
pub struct HttpResponse {
pub status_line: StatusLine,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn get_header(&self, name:&str) -> Option<&str> {
self.headers.iter().find(|header| header.0.eq(name)).map(|v| v.1.as_ref())
}
}
定义完结构体,接着再定义解析response的函数,该函数将解析工作分为3步
解析状态行
响应头
解析响应内容
下面看下伪代码
parse_response(input){
parse_status_line(input)
parse_response_header(input)
parse_response_body(input)
}
我们只需要将以上的伪代码实现就可以了,接下来来看下具体实现
1. parse_response的实现//输入参数的类型为字节数组而不是字符串,这是考虑到响应内容可能是二进制,比如图片视频
pub fn parse_response(input: &[u8]) -> Result<HttpResponse, Box<dyn Error + '_>> {
// 解析状态行,返回剩余的输入和状态行实例
let (input, status_line) = parse_status_line(input)?;
// 解析响应头,返回剩余输入和响应头数组
let (input, headers) = parse_response_header(input)?;
// 从响应头中获取content-length, 以用来解析响应体
let content_length = headers.iter().find(|kv| kv.0.eq("Content-Length"))
.map(|kv| kv.1.parse::<usize>().unwrap_or(0)).unwrap_or(0);
// 解析响应内容
let (input, body) = parse_response_body(input, content_length)?;
Ok(HttpResponse {
status_line,
headers,
body: Vec::from(body)
})
}
parse_response是解析的高层抽象,比较简单,主要解析工作是在3个子函数中完成的
2. parse_status_line的实现//匹配状态行,状态行形式为: HTTP/1.1 200 OK $CRLF
pub fn parse_status_line(input: &[u8]) -> Result<(&[u8], StatusLine), Box<dyn Error + '_>> {
let http = tag("HTTP/"); //匹配 HTTP/
//匹配版本号 1.1
let version = tuple((take_while1(is_digit), tag("."), take_while1(is_digit)));
//跳过空格
let space = take_while1(|c| c == b' ');
//匹配状态码
let status = take_while1(is_digit);
//匹配状态消息
let msg = terminated(is_not("\r\n".as_bytes()), tag(b"\r\n"));
//将以上匹配解析器组合为最终解析器,并并解析
let res: IResult<&[u8], (&[u8], (&[u8], &[u8], &[u8]), &[u8], &[u8], &[u8])> = tuple((http, version, space, status, msg))(input);
let res = res?;
let status = res.1.3;
let status = String::from_utf8_lossy(status).to_string();
let status = status.parse::<u16>()?;
Ok((res.0, StatusLine { status, msg: String::from_utf8_lossy(res.1.4).trim().to_string() }))
}
可能有人会对上面Nom的一些函数用法有疑虑,这里大概介绍下
2.1 tag
tag函数,匹配一个字符串,并返回剩余输入和匹配到的结果
2.2. take_while1
该函数接收一个predicate函数,返回满足此predicate的所有输入,并返回剩余输入和匹配到的结果, 比如
take_while1(is_digit)("12345abc")
返回 ("abc", "12345")
2.3 tuple
该函数是给组合子,接收多个解析器,并顺序应用他们,最后返回剩余输入和匹配到的结果,匹配到的结构以元组的方式返回,元组中的每个元素是对应解析器匹配到的结果,比如
tuple(tag("a"), tag("b"), tag("c"))("abc123")
返回 ("123", ("a", "b", "c")) //a, b, c分别是3个解析器匹配到的结果
2.4 terminated
该函数由2个解析器参数,如 terminated(first, second), 这个函数将匹配first, second,并保存first的结果,丢弃second的结果,比如
terminated( tag("123"), tag("abc"))("123abc456")
返回 ("456", "123"), 可以看到 匹配结果中abc被丢弃了
3. parse_response_header的实现//匹配响应头,结果返回响应头数组
fn parse_response_header(input: &[u8]) -> IResult<&[u8], Vec<(String, String)>> {
//匹配响应头的名字
let name = terminated(is_not(":".as_bytes()), tag(":"));
//匹配响应头的值,以CRLF结束
let value = terminated(is_not("\r\n".as_bytes()), tag(b"\r\n"));
//将名字和值组合为新的解析器
let kv = tuple((name, value));
//匹配多个响应头
let headers: IResult<&[u8], Vec<(&[u8], &[u8])>> = many0(kv)(input);
// 将二进制输出转为字符串
match headers {
Ok(hs) => {
let hs2 = hs.1.iter().map(|v| (String::from_utf8_lossy(v.0).trim().to_string(),
String::from_utf8_lossy(v.1).trim().to_string()))
.collect::<Vec<(String, String)>>();
Ok((hs.0, hs2))
}
Err(e) => Err(e)
}
}
再介绍下用到的解析器
3.1 is_not
匹配知道输入满足参数的模式
例子:
is_not("Over")("abcOver123");
输出: ("Over123", "abc")
4. parse_response_body的实现fn parse_response_body(input: &[u8], len: usize) -> IResult<&[u8], &[u8]> {
let body = take(len);
preceded(crlf, body)(input)
}
用到的解析器:
4.1 take take(n): 获取n个输入序列,如
take(5usize)("12345abc")
输出: ("abc", "12345")
4.2 preceded preceded(first, second): 匹配first, second, 忽略first的输出,收集second的输出,如
preceded(tag(",,,"), tag("123"))(",,,123abc")
输出: ("abc", "123")
5. 测试fn main() {
let data = "hello world";
let mut http_resp= String::new();
http_resp.push_str("HTTP/1.1 200 OK\r\n");
http_resp.push_str(format!("Content-Length:{}\r\n", data.len()).as_str());
http_resp.push_str("Content-Type: text/html\r\n");
http_resp.push_str("\r\n");
http_resp.push_str(data);//body
let resp = parse_response(http_resp.as_bytes()).unwrap();
println!("status: {}", resp.status_line.status);
println!("headers: {:?}", &resp.headers);
println!("body: {}", String::from_utf8_lossy(resp.body.as_ref()));
}
总结本篇文章简单介绍了http response的消息格式,并用Nom实现了消息格式的解析。
Nom解析器非常的强大,提供了很多强大的基础解析器和组合解析器,这两个结合起来就可以像搭积木一样定义自己的业务解析器,这样只要明确了词法规则,使用Nom很容易就可以轻松写出词法解析代码。