C#实现HTTP服务器:(6)将一个JS文件Gzip压缩后,使用Transfer-Encoding标头发送到客户端

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

准备一个JS文件,直接去尤大大那拿了个。
https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js
跟你编译好的程序放在一起,只要能读到的位置即可。

0、实现一个用于写入Chunked数据的流ChunkedWriteStream

很简单的一个实现,只保留了写入的功能,所有读取的功能均禁用掉了。

/// <summary>
/// 实现同步写的Stream,不支持读取操作
/// </summary>
public class ChunkedWriteStream : Stream
{
    private static byte[] _crlf = new byte[] { 13, 10};
    private static byte[] _ending = Encoding.ASCII.GetBytes("0\r\n\r\n");

    private Stream _innerStream = null;
    private bool _leaveInnerStreamOpen = true;

    /// <summary>
    /// 使用指定基础流和模式创建实例
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="leaveInnerStreamOpen"></param>
    public ChunkedWriteStream(Stream stream, bool leaveInnerStreamOpen)
    {
        _innerStream = stream;
        _leaveInnerStreamOpen = leaveInnerStreamOpen;
    }

    /// <summary>
    /// 写入数据块到基础流
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="offset"></param>
    /// <param name="count"></param>
    public override void Write(byte[] buffer, int offset, int count)
    {
        ///组装包头,包含长度数据
        ///数据长度的16进制表示形式+\r\n
        ///举例:
        ///如果数据长度是10,那么包头是:a\r\n
        ///如果数据长度是20,那么包头是:14\r\n
        ///如果数据长度是256,那么包头是:100\r\n
        string length = count.ToString("x") + "\r\n";
        byte[] lengthBuffer = Encoding.ASCII.GetBytes(length);

        ///写入长度数据
        _innerStream.Write(lengthBuffer, 0, lengthBuffer.Length);
        ///写入包数据
        _innerStream.Write(buffer, offset, count);
        ///写入包尾,固定的回车换行
        _innerStream.Write(_crlf, 0, 2);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && !_leaveInnerStreamOpen)
        {
            _innerStream?.Close();
        }
        _innerStream = null;
        base.Dispose(disposing);
    }
    public override void Flush() { }
    public override int Read(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 => false;
    public override bool CanSeek => false;
    public override bool CanWrite => true;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
}
1、为HttpResponser增加一个方法OpenWrite

一个虚方法,主要是要在子类实现具体的逻辑。
在这里可以查看HttpResponser类的实现

public virtual Stream OpenWrite(Stream baseStream)
{
    WriteHeader(baseStream);
    return baseStream;
}
2、为ChunkedResponser增加一个方法,重写HttpResponser的OpenWrite

重写的目的是在子类中把响应标头写入客户端,并返回一个基于基础流的ChunkedWriteStream
在这里可以查看ChunkedResponser类的实现

/// <summary>
/// 打开一个流,用于写入数据
/// </summary>
/// <param name="baseStream"></param>
/// <returns>ChunkedWriteStream流</returns>
public override Stream OpenWrite(Stream baseStream)
{
	//基类的OpenWrite会写入响应标头,并把baseStream返回,跟下面两行等价
    //WriteHeader(baseStream);
    //return new ChunkedWriteStream(baseStream, true);
    
    return new ChunkedWriteStream(base.OpenWrite(baseStream), true);
}
3、把JS文件压缩后,写入客户端

这里顺便做了个文件是否存在的判断,文件不存在就响应404给客户端。
代码中的三层using也不难理解:
1、基于基础流,创建一个Chunked写入流:ChunkedWriteStream
2、创建GzipStream,压缩模式,压缩后写入目标是ChunkedWriteStream
3、打开文件,将未压缩的数据写入GzipStream
4、GzipStream内部将压缩后的数据写入ChunkedWriteStream
5、ChunkedWriteStream封包后,将封包的数据写入基础流

public class HttpServer : TcpIocpServer
{
    protected override void NewClient(Socket client)
    {
        Stream stream = new NetworkStream(client, true);
        //捕获一个HttpRequest
        HttpRequest request = HttpRequest.Capture(stream);

        //从request拿到Url
        string url = request.Url;

        //url是以'/'开头的,这里简单处理下,合成新路径
        //实际应用中,为了安全我会对url进行一个判断,判断有没有危险字符,例如'..',这两个点可能会导致全盘读取任意文件
        string vueAt = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "." + url));

        if (!File.Exists(vueAt))
        {
            //404这里直接关闭了基础流,所以既没有写Content-Length,也没有用Chunked传输
            //用完关闭,让客户端读取完所有数据即可。
            HttpResponser notFound = new HttpResponser(404);
            notFound.ContentType = "text/html; charset=utf-8";
            notFound.KeepAlive = false;
            notFound.Write(stream, $"文件'{vueAt}'未找到。");
            stream.Close();
            return;
        }

        //实例化ChunkedResponser类
        //ChunkedResponser会自动写入Transfer-Encoding: chunked头
        HttpResponser responser = new ChunkedResponser();

        //使用Content-Encoding标头,告诉客户端发送的是经过Gzip压缩的数据。 
        responser["Content-Encoding"] = "gzip";

        //发送的是JS文件,设置Content-Type
        responser.ContentType = "text/javascript";
        responser.KeepAlive = false;

        //使用ChunkedResponser打开一个写入流ChunkedWriteStream
        //GzipStream压缩后,直接写入ChunkedWriteStream
        //介意套娃的同学可以使用简单写法。
        using (Stream output = responser.OpenWrite(stream))
        {
            //实例化Gzip压缩流,压缩后写入output
            using (GZipStream input = new GZipStream(output, CompressionMode.Compress))
            {
                //打开文件,读取数据
                using (FileStream source = File.OpenRead(vueAt))
                {
                    //使用原生方法,把文件数据拷贝到Gzip压缩流
                    source.CopyTo(input);
                }
            }
        }

        //这一句不能忘记,写入结束包
        responser.End(stream);
        
        stream.Close();
    }
}

浏览器输入:http://127.0.0.1:4189/vue.js
在这里插入图片描述
可以看到浏览器正确接收到了压缩后的JS文件,传输大小只有90.9KB,原文件是336KB。
在这里插入图片描述

4、后话

其实,文章写到这里,你已经完全可以自己实现一个轻量级的静态文件服务器了。
当然,由于发文都是在讲原理和具体实现方法,代码细节都没有优化。
中除了TcpIocpServer类使用了IOCP来接受连接,使用ThreadPool.UnsafeQueueUserWorkItem去在新线程处理业务逻辑外,其他的代码都是同步的,包括解析请求,写入响应等。
示例中的各种文件IO、网络IO操作都可以使用异步方法实现。
同时,对请求行解析时单个字节读取(ReadByte)也存在问题,对基础设备读取太多,都有待优化。

后续,会继续实现POST请求的处理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Anlige

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

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

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

打赏作者

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

抵扣说明:

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

余额充值