项目托管地址:https://github.com/hooow-does-it-work/http
0、协议规定
HTTP协议对请求实体内容没有特殊要求,重点在于请求实体内容边界的界定。
请求实体的界定有两种方式。
1、使用Content-Length
标头,明确标识出请求实体的内容长度,也是最常用的方式。
2、使用Transfer-Encoding
来传输数据,前面介绍过使用Transfer-Encoding
来响应数据给客户端,原理上是一样的。
这种方式在客户端发送请求时很少用,在服务器响应客户端时比较常见。
1、实现效果
我们需要实现对HTTP请求的查询字符串和请求实体的解析。
查询字符串是纯文本的,我们可以直接使用相关的解析方法去解析即可。
请求实体我们要作为二进制来处理,取到二进制数据后,再根据不同的Content-Type
将二进制数据解析成不同的请求类型。
本文我们我们处理Content-Type
为application/x-www-form-urlencoded
的请求实体,即我们常用的使用form
表单请求数据的方式。
解析查询字符串
我们先简单修改下HttpRequest的Ready
方法,在Ready
方法里面,获取Content-Length
和Transfer-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, $" {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, $" {name} = {form[name]}<br />");
}
}
responser.End(stream);
return true;
}
return false;
}
}
然后启动我们实现的测试Server,浏览器访问:http://127.0.0.1:4189/
查看效果。
默认会输出一个表单,点击提交按钮。
可以看到,我们的服务器正确解析了查询字符串和form提交的内容。
2、总结
解析的关键是请求实体的边界界定和实体内容的读取。
个人习惯封装Stream来读取实体内容,封装好的Stream更容易跟其他应用交互。
下游应用不需要关心流的具体实现,只要关心自己的业务,从流里面读取和写入数据即可。
到目前为止,我们所有的实现都是短连接,一个请求结束,连接即关闭,后面我们将实现长链。
即Connection标头的实现和处理。