参考连接:html
http://www.cnblogs.com/jivi/archive/2013/03/10/2952860.html数组
https://www.2cto.com/kf/201405/297926.html浏览器
http://www.jb51.net/article/116591.htm缓存
http://www.jb51.net/article/79893.htm服务器
http://blog.csdn.net/u012734441/article/details/44801523session
http://blog.csdn.net/turkeyzhou/article/details/5512348dom
http://blog.csdn.net/u013087513/article/details/53560827异步
参照上图设置,HTTPS和FTP等不要设置(本代理服务器未对这两种协议进行处理), IP是 127.0.0.1 表示代理服务器是在本机,监听端口设成8888。 post
在IE里设置完后,咱们会发现其它浏览器也自动开始使用代理服务器了,这是由于设置代理服务器是系统的功能,每一个浏览器打开的都是同一个设置代理服务器的程序。网站
其实,咱们能够实现一个自动设置代理服务器的功能,这样,当咱们的代理服务器启动的时候,就自动将本机的代理服务器设置成本身,退出的时候,再恢复成原样,这样就再也不须要向上面同样手动设置了。
配置完毕后,在浏览器里输入网址: http://www.baidu.com
咱们能够清楚看到,全部的请求和响应信息都已经被监听到了。
客户端先创建一个和服务端的TCP链接,而后利用这个TCP链接将一份象上面同样的HTTP请求报文发到服务端,
服务端监听到这个请求,而后利用Accept创建一条和这个客户端的专门链接,而后利用这个专门链接读取这一段请求报文,而后再分析这段报文,当他看到有connection:keep-alive的首部时,服务端就知道,客户端要求创建持久链接,服务端根据实际状况对这个请求进行处理。
a. 若是服务端不一样意创建持久链接,那么会在响应报文里加上一个首部 connection:close 。而后再利用这个专门链接将这个响应报文发回给客户端,接着服务端就会关闭这条链接,
客户端会收到服务器刚才的应答信息,看到了connection:close,这时候客户端就知道服务端拒绝了他的持久链接,那么,客户端在完成此次响应报文的解析后会关闭这条链接,当下次再有请求发送到这个服务器的时候,会从新建一个链接。
b. 若是服务端赞成创建持久链接,那么会在响应报文里加上一个首部connection:keep-alive。而后利用这个专门链接,将这个响应报文发回给客户端, 但不关闭这条链接,而是阻塞在那里,直到监视到有新的请求从这个链接传来,再接着处理。
客户端收到刚才的响应报名,看到了connection:keep-alive,因而客户端知道服务端赞成了他的持久链接请求,那么客户端也不会关闭这个链接,当有新的向此服务器发送的请求时,客户端就会经过这个已经打开的链接进行传输,这样就能够节省不少时间(链接创建的时间是很耗时的)。
http请求报文包含以下三部分:
http请求报文例子分析:
关于请求头host补充一下:请求头host冒号后面的部分除了采用 域名:端口 的方法,还能够采用 IP:端口的方法。
例如 host:192.168.1.12:8080 。这种状况下,第二个冒号前面的就是IP了。
若是是域名,host:www.domain.com 的状况。这种状况,就须要从域名获得IP了。那么从域名--IP,须要什么技术呢,天然就是DNS(Domain Name System)了。
在本地的DNS缓存里找域名对应的IP,而后封装成IPAddress类型的数组进行返回。为何返回的是数组?
由于有IPV4和IPV6两种类型的地址,但在一些不支持IPV6的机器上,IPV6项会被筛选掉,因此这种状况下,这个数组的大小就是1。
当没有设置代理的时候,正常的HTTP请求报文格式应该是这样的
可是若是设置了代理,那么浏览器的请求报文都会被发送到代理服务器,在这种状况下,浏览器会对请求报文作些简单的变化,主要在两个方面。
咱们看到在直接发送到目标服务器的状况下,通常都是相对的地址(前面的例子默认都是这种状况下的):
post / http/1.1
而在发送到代理服务器的状况下,就会变成:
post http://www.domain.com/ http/1.1
<request-url> 部分被替换成了完整的URL地址。为何要这样作呢,是由于早期的HTTP协议,是没有HOST首部的,这样当直接链接到服务器时,是没有问题的,由于服务器IP在发送报文前确定是已知的,因此这种状况只要传个相对路径给服务器,服务器就能够经过这个相对路径找到资源并返回了。
可是若是是发送到代理的话,这个请求报文就出问题了,由于代理没法从这个请求报文里分析出来目标服务器的地址,地址都不知道代理又如何将这个请求转发呢。
因此,在HTTP协议里将发送到代理服务器状况下的请求报文里的<request-url>设计成了完整地址,这样代理服务器就能够经过分析这个完整的址址的主机部分获取目标服务器的地址,如此就可能顺利的创建到目标服务器的链接并实现转发了。
可是后来随着虚拟主机的出现,同一个服务器能够映射多个站点,没有HOST首部的HTTP协议,已经没办法处理这种状况了,因此后来HTTP协议里引入了HOST这个首部,
理论上在引入了HOST首部后,不只虚拟主机的状况能够解决,就连代理服务器也能够一并解决了,就不用再在<request-url>里写完整地址了,可是为了保持协议的兼容性,当发送到代理服务器时<request-url>为完整地址的规则仍是被继承了下来。
那就是当直接发送到目标服务器时,有个首部是
connection:keep-alive/close
当发送到代理服务器时,这个首部会被替换成
proxy-conection:keep-alive/close
这段代码在 ServerChatter 类里的 ResendRequest方法体里。这里又出现了this._bWasForwarded. 意思是 若是继续转发请求给代理服务器或网关,并且不是HTTPS的状况下,就把proxy-connection 替换成 connection .
然而程序自己是没有办法判断出要转发到的目标服务器到底是不是代理服务器或者网关的,因此这个变量,不在人工干预的状况下,百分之百是FALSE的 。在咱们的代码里,目前并无实现人工干预的功能,因此这个变量永远都是FALSE。保留他下来只是为了之后的扩展。
事实上代理服务器确实不必定会把请求直接转发到最终的目标服务器,而是有可能先转发到另一个代理服务器或者网关上,而后再由他们转发到最终的目标服务器或者他也是再转发至下一个代理服务器或者网关。
由于咱们颇有可能会遇到下面的这种状况, 在本身的内网建了一个过滤代理,这个过滤代理的功能就是把除了公司想让咱们访问的网址之外的全部网址所有屏蔽,可是如今出问题了,公司想让咱们访问的网站,所有都是被天朝封锁的国外网站,这可怎么办呢?
其实不难办,这时候只要过滤代理再把这个请求转发给另一个代理服务器(这个服务器要求在国内能访问,另外它也能访问国外的网站就能够了),而后由它来获取网页后响应给咱们的过滤代理,再由咱们的过滤代理发回给客户端 。
代理服务器程序启动时,new 一个代理(Proxy)类的实例,而后调用这个实例的Start方法,在Start方法里不停的异步监听代理服务器8888端口,
若是监听到了,就从线程池里取出来一个线程,并在这个线程里构造一个Session对象。一个Session对象表明客户端与服务器的一次会话,在有代理服务器状况下的一次会话(Session) 过程以下:
故而在Session类里,封装一个ClientChatter类型的名为Request的对象,用来实现和客户端的通信, 另外又封装了一个ServiceChatter类型的名为Response的对象,用来实现和目标服务器的通信。
这里就是不停的读取请求信息,直到读取完成为止。读取的同时将这些请求信息存在this.m_requestData(MemoryStream类型)这个全局变量里。有一点要注意一下,那就是判断接收结束的方法。也就是while里面的那三个条件:
iMaxByteCount = 0 了,不就表明已经读取完客户端发过来的请求数据了吗,固然不是,iMaxByteCount实际上是客户端关闭了连接。
这个iMaxByteCount 实际上是Socket.Receive的返回值, Socket.Receive(byte[] buffer) 从绑定的 Socket 套接字接收数据,将数据存入接收缓冲区。
当读不到数据的时候,Receive方法会阻塞在那里,直到有数据到达,或者超时为止,而不是象咱们想象的那样返回0,返回0只有一种状况,就是Socket.Shutdown(),也就是链接的那个Socket关闭了他的链接,在这里也就是客户端关闭了链接。
因此说通常状况下,咱们是不可能经过iMaxByteCount=0(iMaxByteCount= Socket.receive())来判断是否已经读取完了客户端的请求报文(用户在请求过程上,关闭了浏览器可能会发生这种状况)。
请求接收结束时机 isRequestComplete()->content-length方式结束
当客户端将请求报文发送到服务器后,链接是不会关闭的,客户端是否关闭链接,要等到服务器响应后才决定。
也就是说通常状况下,咱们是不可能经过iMaxByteCount=0(iMaxByteCount= Socket.receive())来判断是否已经读取完了客户端的请求报文(用户在请求过程上,关闭了浏览器可能会发生这种状况)。 那么咱们又怎么来判断请求报文已经所有接收完成了呢?
请求头:content-length:8, 表示实体 <entity-body>(在上面的例子里就是a=b&b=cd)的长度,那么<head>头部解析完后再读取content-length个字符,不就表示这次的请求已经所有读取完成了吗?
实际的处理以下:
请求接收结束时机 isRequestComplete()->transfer-encoding:chunked方式结束
固然content-length并不能判断全部的状况,只有确切的知道entity-body长度的状况下,content-length才是有意义的。
可是事实上entity-body的长度并不老是能够预知的,尤为在传一些大文件的时候,为了节省资源和时间,通常会采用分块传输的方式,采用分块传输的时候,会在报文里增长一个首部transfer-encoding:chunked,另外在entity-body里也要遵循必定的格式,
这种状况在请求报文里不多见,由于请求报文在不选择文件进行提交的时候,通常报文都很小,这种状况主要出如今响应报文里。这种状况通常会有以下处理方式:
同时,isRequestComplete()解析了请求报文头,ParseRequestForHeaders()
if (!this.ParseRequestForHeaders())
这个就是分析报头的代码了,前面提到过,会将原始报头映射到一个HTTPRequestHeaders类型的对象里,那么这个方法就是作那个的了,
此方法执行完成后,会把原始的请求报文流中的报头部分(除entity-body之外的部分)分析到一个HTTPRequestHeaders类型的私有属性(m_headers)里。
而后在ClientChatter里又暴露了一个Public的属性Headers来访问这个属性。固然这个方法里还会记录entity-body的起始位置,这样,在后面的TakeEntity方法就能够经过这个位置读取entity-body的内容了。
而TakeEntity会在 Session类的ObtainRequest里被调用this.RequestBodyBytes = this.Request.TakeEntity();
Session类的ObtainRequest方法终于分析完成!
调用完Session的ObtainRequest方法后,程序会变成什么样呢,通过刚才的分析其实已经很清楚了。
这时在Session类里,只要使用this.Request.Headers就能够得到全部的报头信息了。报体部分entity-body 则是经过this.RequestBodyBytes 进行调用 。
核心逻辑分析以下:
其中,
a、this.ConnectToHost();
// 构造一个ServerPipe,并在里面封装一个和服务端通信的Socket
// 获取ip(能够经过dns解析域名,从在本地的DNS缓存里找域名对应的IP获得)
// 获取端口
// 建立连接
this.ConnectToHost() 展开以下:
b、this.ServerPipe.Send( this.m_session.Request.Headers.ToByteArray( true, true, this._bWasForwarded && !this.m_session.IsHTTPS ) );
// 经过这个ServerPipe的Send方法将使用this.m_session.Request.Headers.ToByteArray方法从新包装的请求报头发送给服务器 。
c、this.ServerPipe.Send(this.m_session.RequestBodyBytes);
// 发送原始请求报文的报体到服务器
响应报文:
ReadResponse(ServerChatter.cs里)方法和读取请求报文时(ClientChatter的ReadRequest方法)基本上是同样的,都是不停的使用Receive方法接收数据,直到已经接收完成为止(也要判断三种状况)。
而后再将接收到的响应信息映射到一个HTTPResponseHeaders 类型的对象里,这个对象名叫_inHeaders。一样的也有一个public的Headers属性来访问它。
和读取请求报文时同样,这个方法里,也会记录<entity-body>的偏移位置,以备 TakeEntity方法调用时,用来读取entito-body 部分。