GoAgent原理与实现(二):本地代理的C#试验

今天谈谈GoAgent中本地代理的部分实现。不过这个试验和GoAgent没有任何关系。     


      实现复杂的程序之前,我习惯做一些可行性试验。这样可以让自己对实现它的流程有个明晰的认识,对它的实现更有把握。由于自己对C#比较熟悉,于是今天用C#写了一个可以大概实现本地代理的小程序。该程序的基本思想是这样的:


GoAgent原理与实现(二):本地代理的C#试验_第1张图片

图一:本地代理原理描述


      当把浏览器代理设置成:127.0.0.1:8087之后,所有的请求将被重置到本地计算机的8087端口。程序可以监听本地8087端口,当有请求时,绑定一个Socket,本文称之为LocalSocket。LocalSocket接收请求内容,根据该内容实例化特定的外部服务器Socket,本文称之为ServerSocket,并将请求通过ServerSocket发出。ServerSocket接收到的数据后,直接发给LocalSocket,LocalSocket会把数据返回给浏览器。


      由于页面中会有很多http请求,可以将每个请求的上述操作放在独立的线程中执行,这也是对类似操作的一般做法。


      具体试验是通过C#的Console程序实现的。由于仅仅是可行性试验,对过多的情况没有做具体处理。程序分由两个文件组成:program.cs和Proxy.cs。program.cs包括程序的入口main函数,Proxy.cs以类的形式组织了具体代理的实现过程,但和面向对象好像没有什么太大的关系。具体分析如下:


 static void Main(string[] args)

        {

            const int port = 8087;  //定义端口号

            TcpListener tcplistener = new TcpListener(port); //定义监听

            Console.WriteLine("Listenning: " + port);

            tcplistener.Start();  //监听打开

            while (true)

            {

                Socket localSocket = tcplistener.AcceptSocket();

                //获取localSocket实例

                Proxy myproxy = new Proxy(localSocket);

                //实例化Proxy对象

                Thread thread = new Thread(new ThreadStart(myproxy.Run));

                //创建线程

                thread.Start();  //开启线程

            }

        }


      Proxy.cs 中有三个处理函数:ReadMessage(...)来读取LocalSocket的请求,ForwardTcpData(...)用了交换两个Socket中的数据,Run()函数用来实现具体流程。



 private int ReadMessage(byte[] ByteArray, ref Socket s, ref String clientMessage)

        {

            int bytes = s.Receive(ByteArray, 1024, 0); //接收请求数据

            String message= Encoding.ASCII.GetString(ByteArray); 

            clientMessage = (String)message;

            return bytes;

        }


 private void ForwardTcpData(Socket client, Socket server)

        {

            ArrayList ReadList = new ArrayList(2);

            while (true)

            {

                ReadList.Clear();

                ReadList.Add(client);

                ReadList.Add(server);

                try

                {

                    //测试ReadList中的Socket是否可用

                    Socket.Select(ReadList, null, null, 1 * 1000 * 1000);

                }

               catch (SocketException e)

                {

                   Console.WriteLine("Select error: " + e.Message);

                   break;

               }


                // 若ServerSocket可读

                if (ReadList.Contains(server))

                {

                    byte[] Recv = new byte[1024 * 10];

                    int Length = 0;

                    try

                    {

                        //从ServerSocket端接收数据

                        Length = server.Receive(Recv, Recv.Length, 0);

                        if (Length == 0)

                        {

                            Console.WriteLine("Server is disconnect");

                            break;

                        }

                        Console.WriteLine("Recv bytes from server", Length);

                    }

                    catch (Exception e)

                    {

                        Console.WriteLine("Read from server error: " + e.Message);

                        break;

                    }

                    try

                    {

                        //向LocalSocket端写入数据

                        Length = client.Send(Recv, Length, 0);

                        Console.WriteLine(" Write bytes to client", Length);

                    }

                    catch (Exception e)

                    {

                        Console.WriteLine("Write data to client error: " + e.Message);

                        break;

                    }

                }

            }

           //完成后关闭Socket

           client.Shutdown(SocketShutdown.Both);

           server.Shutdown(SocketShutdown.Both);

           client.Close();

           server.Close();

           Console.WriteLine("Transmit complete !");

        }


        public void Run()

        {

            String clientmessage = " ";

            //存放来自客户端的HTTP请求字符串

            int bytes = ReadMessage(read,ref clientSocket,ref clientmessage);

            if(bytes == 0) return ;

            String[] strs = clientmessage.Split(' ');

            Uri myUri = new Uri(strs[1]);

            try

            {

                IPHostEntry IPHost;

                if (strs[0] == "CONNECT") 

                {

                    String[] temp = strs[3].Split('\r');

                    IPHost = Dns.Resolve(temp[0]);

                }

                else IPHost = Dns.GetHostEntry(myUri.Host);

                Console.WriteLine("Remote Server:" + IPHost.HostName);

                String[] aliases = IPHost.Aliases;

                IPAddress[] address = IPHost.AddressList;

                Console.WriteLine("web Server IP Address:" + address[0]);

                //以上先解析出要访问的服务器地址

                IPEndPoint ipEndpoint = new IPEndPoint(address[0], 80);

                Socket  serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                //以上创建连接Web服务器端的Socket对象

                serverSocket.Connect(ipEndpoint);  //Socket连接服务器

                if ( serverSocket.Connected) Console.WriteLine("Socket connected ...");

                String GET = clientmessage;

                Byte[] ByteGet =UTF8.GetBytes(GET);

                serverSocket.Send(ByteGet, ByteGet.Length, 0);

                ForwardTcpData(clientSocket,  serverSocket );

            }

            catch (Exception exc2)

            {

                Console.WriteLine(exc2.StackTrace);

            }

        }


      Run函数的流程是,先从LocalSocket中接收请求数据,并分析出请求的host name。利用host name解析出IPAddress。建立一个新的ServerSocket(这里默认Web访问80端口)。


      需要注意对http头数据的处理。请求baidu.com的头数据是Get方式:


GoAgent原理与实现(二):本地代理的C#试验_第2张图片

图二 :Http Header-Get方式



而一些插件的请求数据头是CONNECT方式:



图三: Http Header-CONNECT方式



      因此,需要在处理时区别对待。


      当设置好了浏览器代理,并开启该程序后,一般的网站可以通过它正常访问,但Weibo.com中别人发布的信息无法看到,没有仔细研究。


      该程序对gzip页面没有解压功能,对一些需要证书的网站应该无法代理。运行时还会出现一些Exception的调试信息没有处理(但不影响程序的运行)。总之,这是一个十分粗糙的程序,对于代理需要的情况并没有面面俱到,但对于一个试验性的程序来说,已经满足要求了。


     接下来的工作是,将其改写成python版。但考虑到它们的实现也许会很不一样,而且python还没有学会,也许这将是一个比较漫长的工作。



参考资料:


[1] 粗糙的C#版HTTP代理;


[2] 浅谈C#实现Web代理服务器的几大步骤;


[3] 超文本传输协议;


你可能感兴趣的:(Engine,Google,APP,防火墙,GAE,goagent)