• 实现一个简单的Http代理服务器


    昨天介绍了下微软的反向代理库YARP,今天准备实现一个简单的Http正向代理服务器玩下。首先还是介绍下背景知识:

    普通代理(Http)

    在Http的时代,大部分是走的RFC 7230中描述的普通代理。这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。它的流程是:

    1. 客户端浏览器将请求原封不动的发送给代理服务器
    2. 代理服务器从HttpHeader中获取目标的主机地址,将请求发送给目标主机
    3. 目标主机将响应回传给代理服务器
    4. 代理服务器将响应回传给客户端浏览器。

    • 对于客户端浏览器来说,代理服务器就是目标web服务器。
    • 对于web服务器来说来说,它会把代理当做客户端,完全察觉不到真正客户端的存在(代理服务器可以通过X-Forwarded-IP这样的自定义头部告诉服务端真正的客户端 IP)。

    这种代理服务器实现是比较简单的,基本上是原封不动的透传,主要是第2步,需要从header中识别目标主机地址。

    隧道代理(Https)

    到了Https时代,这种方式就有问题了,代理服务器是一个web服务器,它是影响了客户端和服务器的TLS加密连接的。此时主要使用RFC中定义的通过 Web 代理服务器用隧道方式传输基于 TCP 的协议的隧道代理方式,它的主要流程为:

    1. 浏览器首先发送Http Connect请求给代理服务器,发送目标主机信息。
    2. 代理服务器建立和目标主机的tcp链接,并向浏览器回应Connection Established应答。
    3. 浏览器将请求发送给代理服务器,代理服务器透传给目标主机。
    4. 目标主机将响应回给代理服务器,代理服务器将响应回给浏览器。

    这种模式下,和Sock5等代理协议非常类似了,代理服务器完全就是一个透传的管道了。只不过是通过http协议协商建立起管道而已。建立连接后,代理服务器只起转发的作用,理论上也适用于转发其它TCP协议。

    功能实现

    两种代理服务器实际上流程是大同小异的,主要是识别目标主机的指令不同,以及交互的方式有所差异,建立连接和完成第一次交互后,后面基本上都是透传。从0开始实现也基本上就几十行代码,实现带调试几个小时差不多可以搞定,如下是我的一个简单的实现,基于.net core 3.1。 

            static void Main(string[] args)
        {
            ProxyServer.Run();
            Thread.Sleep(-1);
        }
    
        class ProxyServer
        {
            public static void Run()
            {
                TcpServer.Run(3000, async tcp =>
                {
                    using var handlder = new ProxyHandler(tcp);
                    await handlder.Process();
                });
            }
        }
    
    
        class ProxyHandler : IDisposable
        {
            TcpClient _tcp;
            TcpClient _remoteTcp;
    
            public ProxyHandler(TcpClient tcp)
            {
                _tcp    = tcp;
                _buffer = MemoryPool<byte>.Shared.Rent(1024 * 3);
            }
    
            IMemoryOwner<byte> _buffer;
            Memory<byte>       _header;
    
            public async Task Process()
            {
                var count = await _tcp.GetStream().ReadAsync(_buffer.Memory);
                _header = _buffer.Memory[..count];
    
                parseHeader(out var method, out var endPoint);
    
                //Console.WriteLine(endPoint);
    
                if (method.Equals("CONNECT", StringComparison.OrdinalIgnoreCase))
                {
                    await createTunnel(endPoint);
                }
                else
                {
                    await createProxy(endPoint);
                }
    
                await pipeStream(_tcp.GetStream(), _remoteTcp.GetStream());
            }
    
    
            static byte[] _tunnelReply = Encoding.UTF8.GetBytes("HTTP/1.0 200 Connection Established
    
    ");
    
            async ValueTask createTunnel(string endPoint)
            {
                var host = endPoint.Split(":");
    
                _remoteTcp = new TcpClient();
                await _remoteTcp.ConnectAsync(host[0], int.Parse(host[1]));
                await _tcp.GetStream().WriteAsync(_tunnelReply);
            }
    
            async ValueTask createProxy(string endPoint)
            {
                var host = new Uri(endPoint);
    
                _remoteTcp = new TcpClient();
                await _remoteTcp.ConnectAsync(host.Host, host.Port);
                await _remoteTcp.GetStream().WriteAsync(_header);
            }
    
    
            void parseHeader(out string method, out string endPoint)
            {
                var reader = new SequenceReader<byte>(new ReadOnlySequence<byte>(_header));
    
                method   = readToSpace(ref reader);
                endPoint = readToSpace(ref reader);
    
                static string readToSpace(ref SequenceReader<byte> r)
                {
                    //读到下一个空格
                    r.TryReadTo(out ReadOnlySpan<byte> buf, (byte)' ');
                    return Encoding.UTF8.GetString(buf);
                }
            }
    
    
            async Task pipeStream(Stream s1, Stream s2)
            {
                await Task.WhenAll(pipe(s1, s2), pipe(s2, s1));
    
                static async Task pipe(Stream source, Stream target)
                {
                    try
                    {
                        await source.CopyToAsync(target);
                    }
                    finally
                    {
                        target.Close();
                        source.Close();
                    }
                }
            }
    
            public void Dispose()
            {
                _buffer?.Dispose();
                _tcp?.Dispose();
                _remoteTcp?.Dispose();
            }
        }
        
        class TcpServer
        {
            public static async void Run(int port, Func<TcpClient, Task> handler)
            {
                var listener = new TcpListener(IPAddress.Loopback, port);
    
                listener.Start();
                while (true)
                {
                    var tcp = await listener.AcceptTcpClientAsync();
                    process(tcp, handler);
                }
            }
    
            static async void process(TcpClient tcp, Func<TcpClient, Task> handler)
            {
                try
                {
                    await Task.Run(() => handler(tcp));
                }
                catch
                {
                    // ignored
                }
            }
        }
    View Code

    简单的运行测试了一下,基本功能应该是完善的,稳定运行貌似没有什么大问题。虽然没有什么额外的功能,但还是考虑了一下性能的,用了内存池,解析http头的时候也尽量较少了内存的分配。考虑带代码的可读性,也没有太必要做到性能的极致。后面有空的话准备用它来写一个SS客户端,使之有更好的扩展性。

    这种模式下,和Sock5等代理协议非常类似了,代理服务器完全就是一个透传的管道了。只不过是通过http协议协商建立起管道而已。建立连接后,代理服务器只起转发的作用,理论上也适用于转发其它TCP协议。

    参考文章

  • 相关阅读:
    一个数组中去除某一部分数组
    关于函数的同步异步
    多维数组转一维数组
    关于Promise的详细总结
    关于ES6(ES2015)的知识点详细总结
    vue实现一个会员卡的组件(可以动态传入图片(分出的一个组件)、背景、文字、卡号等)
    GitHub上常用命令(工作中几乎每天用到的命令)
    gitHub上如何设置或者取消电子邮箱提醒
    React评论展示案例(包含知识点:state、props、ref、React声明周期、localStorage本地存储等)
    感想2-对于组件化的一些思考
  • 原文地址:https://www.cnblogs.com/TianFang/p/12952375.html
Copyright © 2020-2023  润新知