C#实现HTTP服务器:(8)解析查询参数和HTTP请求实体

项目托管地址:https://github.com/hooow-does-it-work/http

0、协议规定

HTTP协议对请求实体内容没有特殊要求,重点在于请求实体内容边界的界定。
请求实体的界定有两种方式。
1、使用Content-Length标头,明确标识出请求实体的内容长度,也是最常用的方式。
2、使用Transfer-Encoding来传输数据,前面介绍过使用Transfer-Encoding来响应数据给客户端,原理上是一样的。

这种方式在客户端发送请求时很少用,在服务器响应客户端时比较常见。

1、实现效果

我们需要实现对HTTP请求的查询字符串和请求实体的解析。
查询字符串是纯文本的,我们可以直接使用相关的解析方法去解析即可。
请求实体我们要作为二进制来处理,取到二进制数据后,再根据不同的Content-Type将二进制数据解析成不同的请求类型。
本文我们我们处理Content-Typeapplication/x-www-form-urlencoded的请求实体,即我们常用的使用form表单请求数据的方式。

解析查询字符串

我们先简单修改下HttpRequest的Ready方法,在Ready方法里面,获取Content-LengthTransfer-Encoding相关的信息,并且对请求的Url分解,取出Path部分和Query部分。
QueryString属性会解析Query文本数据为NameValueCollection对象,方便数据读取。
使用HttpRequest.QueryString["name"]可以方便的取出查询字符串中name的值。
HasEntityBody属性用来判断请求是否包含实体数据。
HttpUtility类的部分代码是参考官方开源的代码。


/// <summary>
/// 这里只是简单的给源请求数据补了新行
/// 可以在Ready方法里面做更多事情
/// 例如解析Host、ContentLength、ContentType、AcceptEncoding、Connection以及Range等请求头
/// </summary>
public HttpRequest Ready()
{
    _originHeaders += "\r\n";

    string header = _headers["content-length"];
    if (!string.IsNullOrEmpty(header))
    {
        if (!long.TryParse(header, out _contentLength))
        {
            _contentLength = -1;
            throw new Exception("Content-Length字段值错误");
        }
    }

    _transferEncoding = _headers["transfer-encoding"];

	//拆解url,取出Path和Query
    int idx = _url.IndexOf('?');
    _path = _url;
    if (idx >= 0)
    {
        _path = _url.Substring(0, idx);
        _query = _url.Substring(idx).TrimStart('?');
    }

    return this;
}

private long _contentLength = -1;
private string _transferEncoding = null;
private string _path = null;
private string _query = null;
private NameValueCollection _queryString = null;

public string Path => _path;
public string Query => _query;
public NameValueCollection QueryString
{
    get
    {
        //在获取属性值时才初始化_queryString的值
        if (_queryString != null) return _queryString;

        return _queryString = HttpUtility.ParseUriComponents(_query);
    }
}

/// <summary>
/// 确认请求是否包含消息
/// </summary>
public bool HasEntityBody => _contentLength > 0
    || !string.IsNullOrEmpty(_transferEncoding);


private Stream _entityReadStream = null;
public Stream OpenRead(Stream baseStream)
{
    if (!HasEntityBody) throw new Exception("请求不包含消息");

    if(_entityReadStream != null)
    {
        return _entityReadStream;
    }

    //如果同时出现transfer-encoding和content-length
    //优先处理transfer-encoding,忽略content-length
    if (!string.IsNullOrEmpty(_transferEncoding))
    {
        //返回一个ChunkedReadStream
        //我们暂不处理这种方式的数据
        throw new NotSupportedException();
    }

    //返回一个ContentedReadStream
    return new ContentedReadStream(_contentLength, baseStream, true);
}
实现ContentedReadStream

由于HTTP的请求实体,数据类型多样,我们先统一使用ContentedReadStream流读出所有二进制数据。
ContentedReadStream实现是的读取固定长度作为边界的请求实体,也是最常用的方式,使用Transfer-Encoding的方式我们暂不考虑,但后面我们会实现一个Transfer-Encoding读取的流。
ContentedReadStream的实现也比较简单,主要目的是,数据读到指定长度后,通知下游应用,请求实体已经读取完毕。
通过下方代码可以看到,我们只是实现了Stream的Read方法。

/// <summary>
/// 实现一个对固定数据长度消息读取的流
/// </summary>
public class ContentedReadStream : Stream
{
    private Stream _innerStream = null;
    private bool _leaveInnerStreamOpen = true;
    private long _contentLength = 0;

    /// <summary>
    /// 使用指定长度、基础流和模式创建实例
    /// </summary>
    /// <param name="contentLength">数据长度</param>
    /// <param name="stream"></param>
    /// <param name="leaveInnerStreamOpen"></param>
    public ContentedReadStream(long contentLength, Stream stream, bool leaveInnerStreamOpen)
    {
        if(contentLength < 0)
        {
            throw new ArgumentOutOfRangeException("contentLength", "contentLength must >= 0");
        }

        _innerStream = stream;
        _leaveInnerStreamOpen = leaveInnerStreamOpen;
        _contentLength = contentLength;
    }

    /// <summary>
    /// 重写Read方法,主要目的是限制数据读取的长度
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="offset"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public override int Read(byte[] buffer, int offset, int count)
    {
        //读完数据,返回0
        if (_contentLength == 0) return 0;

        if(count > _contentLength)
        {
            count = (int)(_contentLength & 0xffffffff);
        }

        count = _innerStream.Read(buffer, offset, count);
        _contentLength -= count;
        return count;
    }


    protected override void Dispose(bool disposing)
    {
        if (disposing && !_leaveInnerStreamOpen)
        {
            _innerStream?.Close();
        }
        _innerStream = null;
        base.Dispose(disposing);
    }
    public override void Flush() { }
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long length) => throw new NotSupportedException();
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
}
解析请求实体

我们尝试解析由form表单提交的请求实体。
实现一个测试Server,根据不同的请求Path,调用不同的响应方法,输出指定内容到客户端。
关键代码是这段,从请求中打开一个用来读取请求实体的流(HttpRequest实现了OpenRead方法),把读取的内容全部拷贝出来。
然后使用HttpUtility.ParseUriComponents来解析。

//如果浏览器传从数据了
//打开一个读取流,然后把数据拷贝到内存流
//这个数据的获取,可以放在HttpRequest实现
if (request.HasEntityBody)
{
    using(MemoryStream output  = new MemoryStream())
    {
        using(Stream input = request.OpenRead(stream))
        {
            input.CopyTo(output);
        }
        entityContent = output.ToArray();
    }
}

测试Server的全部代码:

public class HttpServer : TcpIocpServer
{
    protected override void NewClient(Socket client)
    {
        Stream stream = new BufferedNetworkStream(client, true);

        //捕获一个HttpRequest
        HttpRequest request = HttpRequest.Capture(stream);

        bool handled = false;

        if (request.Method == "GET")
            handled = ProcessGet(request, stream);

        if (request.Method == "POST")
            handled = ProcessPost(request, stream);

        //请求没被处理,返回个404
        if (!handled)
        {
            HttpResponser responser = new ChunkedResponser(404);

            responser.KeepAlive = false;
            responser.ContentType = "text/html; charset=utf-8";

            responser.Write(stream, "404 页面不存在");

            responser.End(stream);
        }

        stream.Close();
    }

    private bool ProcessGet(HttpRequest request, Stream stream)
    {
        //仅处理路径:/
        //返回个简单的表单给客户端
        if (request.Path == "/")
        {
            HttpResponser responser = new ChunkedResponser();

            responser.KeepAlive = false;
            responser.ContentType = "text/html; charset=utf-8";

            responser.Write(stream,
@"<form method=""POST"" action=""/post?action=save"" >
姓名:<input type=text name=name value=""测试hello world!"" /> <br />
年龄:<input type=text name=age value=31 /> <br />
<input type=submit value=提交 /> 
</form>");

            responser.End(stream);
            return true;
        }
        return false;
    }

    private bool ProcessPost(HttpRequest request, Stream stream)
    {
        //仅处理路径:/post
        if (request.Path == "/post")
        {
            byte[] entityContent = null;

            //如果浏览器传从数据了
            //打开一个读取流,然后把数据拷贝到内存流
            //这个数据的获取,可以放在HttpRequest实现
            if (request.HasEntityBody)
            {
                using(MemoryStream output  = new MemoryStream())
                {
                    using(Stream input = request.OpenRead(stream))
                    {
                        input.CopyTo(output);
                    }
                    entityContent = output.ToArray();
                }
            }


            //展示下客户端请求的一些东西
            HttpResponser responser = new ChunkedResponser();

            responser.KeepAlive = false;
            responser.ContentType = "text/html; charset=utf-8";

            responser.Write(stream, "<style type=\"text/css\">body{font-size:14px;}</style>");
            responser.Write(stream, "<h4>Hello World!</h4>");
            responser.Write(stream, $"Host Name: {request.Headers["host"]} <br />");
            responser.Write(stream, $"Method: {request.Method} <br />");
            responser.Write(stream, $"Request Url: {request.Url} <br />");
            responser.Write(stream, $"HttpPrototol: {request.HttpProtocol} <br />");
            responser.Write(stream, $"Time Now: {DateTime.Now: yyyy-MM-dd HH:mm:ss} <br /><br />");


            responser.Write(stream, $"查询参数:{request.Query}<br />");
            responser.Write(stream, $"查询参数解析结果:<br />");

            var queryString = request.QueryString;
            foreach (string name in queryString.Keys)
            {
                responser.Write(stream, $"&nbsp; &nbsp; {name} = {queryString[name]}<br />");
            }

            if (entityContent != null)
            {
                //请求实体的原文本数据
                string formString = Encoding.UTF8.GetString(entityContent);

                //解析文本为NameValueCollection
                var form = HttpUtility.ParseUriComponents(formString);

                //输出原文
                responser.Write(stream, $"POST原内容:{formString}<br />");

                //输出解析后的数据
                responser.Write(stream, $"POST解析结果:<br />");
                foreach (string name in form.Keys)
                {
                    responser.Write(stream, $"&nbsp; &nbsp; {name} = {form[name]}<br />");
                }

            }


            responser.End(stream);
            return true;
        }
        return false;
    }
}

然后启动我们实现的测试Server,浏览器访问:http://127.0.0.1:4189/查看效果。
在这里插入图片描述
默认会输出一个表单,点击提交按钮。
在这里插入图片描述
可以看到,我们的服务器正确解析了查询字符串和form提交的内容。

2、总结

解析的关键是请求实体的边界界定和实体内容的读取。
个人习惯封装Stream来读取实体内容,封装好的Stream更容易跟其他应用交互。
下游应用不需要关心流的具体实现,只要关心自己的业务,从流里面读取和写入数据即可。

到目前为止,我们所有的实现都是短连接,一个请求结束,连接即关闭,后面我们将实现长链。
即Connection标头的实现和处理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anlige

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值