在文章C#实现HTTP服务器:(1)解析HTTP请求头中,我们实现了对HTTP请求头的解析。
同时,我们也提到过,解析是逐字节进行的,会对Socket进行大量的Receive的调用,多了不必要的网络IO消耗。
HttpRequest的代码是这样的:
private static string ReadLine(Stream source, byte[] lineBuffer)
{
int offset = 0;
int chr;
while ((chr = source.ReadByte()) > 0)
{
lineBuffer[offset] = (byte)chr;
if (chr == '\n')
{
//协议要求,每行必须以\r\n结束
if (offset < 1 || lineBuffer[offset - 1] != '\r')
throw new HttpRequestException(HttpRequestError.NotWellFormed);
if (offset == 1)
return "";
//可以使用具体的编码来获取字符串数据,例如Encoding.UTF8
//这里使用ASCII读取
return Encoding.ASCII.GetString(lineBuffer, 0, offset - 1);
}
offset++;
//请求头的每行太长,抛出异常
if (offset >= lineBuffer.Length)
throw new HttpRequestException(HttpRequestError.LineLengthExceedsLimit);
}
//请求头还没解析完就没数据了
throw new HttpRequestException(HttpRequestError.NotWellFormed);
}
查看NetworkStream的源码,发现ReadByte方法并没有重写,直接调用的是Stream的ReadByte,Stream对ReadByte的具体实现如下,可以看到是初始化了1字节的数组,然后调用了Read方法。
public virtual int ReadByte()
{
byte[] buffer = new byte[1];
if (this.Read(buffer, 0, 1) == 0)
{
return -1;
}
return buffer[0];
}
这篇文章我们来解决这个问题,在不修改HttpRequest
任何代码的前提下,不再频繁调用基础设施的读取方法。
我们的解决方法是,实现一个继承NetworkStream
的类BufferedNetworkStream
,重写NetworkStream
的Read
和ReadByte
方法,在我们重写的类中,使用一个大的缓冲区来存储数据,供下游应用使用。
0、BufferedNetworkStream的具体实现
可以看到代码中对Read
和ReadByte
的重写。
ReadByte
方法的重写不是必须的,但很有必要,防止base.ReadByte
频繁创建1字节的缓冲区。
后续会继续实现对异步方法BeginRead/BeginWrite
的重写。
//继承NetworkStream
public class BufferedNetworkStream : NetworkStream
{
/// <summary>
/// 实现NetworkStream的两个构造方法
/// </summary>
/// <param name="baseSocket">基础Socket</param>
public BufferedNetworkStream(Socket baseSocket) : base(baseSocket){}
/// <summary>
/// 实现NetworkStream的两个构造方法
/// </summary>
/// <param name="baseSocket">基础Socket</param>
/// <param name="ownSocket">是否拥有Socket,为true的话,在Stream关闭的同时,关闭Socket</param>
public BufferedNetworkStream(Socket baseSocket, bool ownSocket) : base(baseSocket, ownSocket){}
/// <summary>
/// 定义变量,标识当前流是否在Buffered模式下运行
/// 默认为true
/// 我们可以适时关闭buffered模式
/// 例如在两个流拷贝数据的时候,都是大的数据块,没必要再去缓冲
/// </summary>
private bool _buffered = true;
public bool Buffered {
get => _buffered;
set => _buffered = value;
}
/// <summary>
/// 定义缓冲区,程序会尽可能读满缓冲区
/// 可以根据不同的应用,合理设置这个值
/// </summary>
private byte[] _buffer = new byte[32768];
/// <summary>
/// 缓冲区中数据的读取索引
/// </summary>
private int _offset = 0;
/// <summary>
/// 缓冲区中的可用数据长度,缓冲区没有数据时,尝试读数据到缓冲区
/// </summary>
private int _length = 0;
/// <summary>
/// 重写ReadByte,直接从缓冲区拿数据
/// </summary>
/// <returns></returns>
public override int ReadByte()
{
if(_length > 0)
{
_length--;
return _buffer[_offset++];
}
return base.ReadByte();
}
/// <summary>
/// 重写Read方法,用户从缓冲区中读数据
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="size"></param>
/// <returns></returns>
public override int Read(byte[] buffer, int offset, int size)
{
//缓冲区没有数据,从基础流中读取数据到缓冲区。
if(_length == 0 && _buffered)
{
//索引恢复到起始位置
_offset = 0;
_length = base.Read(_buffer, 0, _buffer.Length);
//没有从基础流读到数据,直接返回,代表流已经读完了所有数据。
if (_length == 0) return 0;
}
//能进入这个分支,说明_buffered为false,并且缓冲区内没有数据了,直接从基础流读数据
//否则会一直从缓冲区内读数据,直到清空缓冲区
if (_length == 0)
{
return base.Read(buffer, offset, size);
}
return CopyFromBuffer(buffer, offset, size);
}
private int CopyFromBuffer(byte[] buffer, int offset, int size)
{
//如果要求读的数据超过了缓冲区内数据的大小,则只返回缓冲区内的可用数据
if (size > _length) size = _length;
//从缓冲区拷贝数据到应用
Array.Copy(_buffer, _offset, buffer, offset, size);
//数据拷贝完成,移动偏移,修改缓冲区的可能数据长度
_offset += size;
_length -= size;
return size;
}
/// <summary>
/// 重写Dispose,释放下缓冲区
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
_buffer = null;
base.Dispose(disposing);
}
}
1、测试下效果
重新实现一个Http服务器,使用重写的类代替NetworkStream。
原代码:Stream stream = new NetworkStream(client, true);
修改为:Stream stream = new BufferedNetworkStream(client, true);
public class HttpServer : TcpIocpServer
{
protected override void NewClient(Socket client)
{
//替换掉原来的NetworkStream
Stream stream = new BufferedNetworkStream(client, true);
//捕获一个HttpRequest
HttpRequest request = HttpRequest.Capture(stream);
//懒得去计算数据长度,直接用Chunked方式发送数据
HttpResponser responser = new ChunkedResponser();
responser.ContentType = "text/html";
responser.KeepAlive = false;
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 />");
responser.End(stream);
stream.Close();
}
}
增加控制台输出用来测试效果,然后运行服务
BufferedNetworkStream
增加控制台输出。
public override int ReadByte()
{
if(_length > 0)
{
//这里加个控制台输出,看看效果
Console.WriteLine($"Data Left: {_length}, Read Offset: {_offset}");
_length--;
return _buffer[_offset++];
}
return base.ReadByte();
}
启动服务
HttpServer server = new HttpServer();
server.Start("0.0.0.0", 4189);
Console.WriteLine("服务器启动成功,监听地址:" + server.LocalEndPoint.ToString());
浏览器访问:http://127.0.0.1:4189
从控制台输出可以看到,第一个字节因为缓冲区为空,使用Read
方法去读取了,之后的数据读取都命中了我们的缓冲区。
这样避免了对底层设备的频繁读取,提高IO性能,同时我们也不需要去修改HttpRequest来优化性能。