查看原文
其他

Rust实战之使用Nom 解析 Http Response 消息

秋风不度镇南关 码农真经 2023-12-25

相信大多数做业务开发的同学,调用的业务服务大多以Restfull API的形式存在,特别是跨部门调用或公司外部的API服务。此时,一个用于发送请求的http client是必不可少的。好奇心强的你, 一定会尝试自己造个轮子。

这过程中能学到很多东西, 比方说:

  1. 网络编程相关的知识

  2. http协议知识

  3. 编译原理相关的知识

本篇文章将重点聚焦在解析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步

  1. 解析状态行

  2. 响应头

  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很容易就可以轻松写出词法解析代码。


继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存