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

原创
2021/04/04 09:52
阅读数 250

相信大多数做业务开发的同学,调用的业务服务大多以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/$version $code $msg, 如 HTTP/1.1 200 OK

第二部分为请求头信息(包含多个请求头) 格式为 $header_name: $header_value, 如 Bdpagetype: 2

另外还有第三部分,没直接显示在chrome devtools的 headers里,该部分就是消息返回的具体内容。 总的来说一个完整的http 响应,格式如下 HTTP/$version $code $msg $CRLF $header_name1: $header_value1 $CRLF $header_name2: $header_value2 $CRLF ... //其他消息头 $CRLF $body

注: 这里以$开头的是变量,如$CRLF 就是 \r\n

  • $version 是http的版本号如 1.1, 2
  • $code 是http状态码,如 200, 400, 500
  • $msg 是状态信息,如 OK
  • $header_name, $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"); 执行匹配函数,返回匹配结果
  1. 解析结果

每个解析器执行后,都返回一个解析结果,类型为 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, 表示匹配到的结果
    ),
)
  1. 复合解析器

复合解析器也是一个解析器,它将多个解析器组合为一个新的解析器。继续看代码

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

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部