C#实现HTTP服务器:(7)使用缓冲区优化对HTTP请求头的解析

在文章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,重写NetworkStreamReadReadByte方法,在我们重写的类中,使用一个大的缓冲区来存储数据,供下游应用使用。

0、BufferedNetworkStream的具体实现

可以看到代码中对ReadReadByte的重写。
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来优化性能。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Anlige

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

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

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

打赏作者

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

抵扣说明:

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

余额充值