Web开发之ASP.Net:(2)Http协议及Web服务原理

作者:陆金龙    发表时间:2016-08-21 13:57   


2HTTP协议Web服务

HTTP协议内容:请求报文,响应报文,get和post请求方法等。

HTTP协议将socket原理和ASP.NET内部原理(基础是HttpHandler)串了起来。

承前:基于在socket聊天程序上的拓展,实现遵循HTTP协议的处理请求和进行响应的服务器程序。

启后:引出IHttpHandler接口,ProcessRequest方法,Request,Reponse,静态页面和动态页面处理等ASP.NET的重要概念以及基本原理(Web开发就是HTTP请求 Http响应)。

2.1 HTTP协议基础

2.1.0 HTTPTcp与Socket

SOCKET与Tcp

socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API), 通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的联系。

SOCKET连接与HTTP连接 

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。 

若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;

 

HTTP连接使用的是“请求—响应”的方式,是短连接,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。 很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。 


因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。 

HTTP与TCP的关联

WEB使用HTTP协议作应用层协议,以封装HTTP  文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

2.1.1 HTTP协议

HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是建立在TCP协议之上的一种应用。 HTTP是应用层的协议。

HTTP是无状态的协议。

浏览器和服务器本质上是两个使用socket进行基于HTTP通信的应用程序。通讯需要遵循一定的协议,如服务器不能直接发文件,直接发送的文件,浏览器理解不了。

因此双方都遵循http协议,通讯中要加http协议头。

HTTP连接特点

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器响应,在请求结束后,会主动释放连接。

1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。 
2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。 
由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的做法是即使不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

2.1.2 HTTP请求报文解析

用httpwatch查看访问一个网站的响应情况:敲入一个网址后,浏览器向服务器发出请求。页面中的图片、js、css在单独的请求中。

GET / HTTP/1.1 表示向服务器用GET方式请求首页,使用HTTP/1.1协议

Accept-Encoding gzip, deflate 表示浏览器支持gzip、deflate两种压缩算法

Accept-Language zh-cn 表示浏览器支持的语言,很多进入后自动就是中文界面的国际网站就是通过读取这个头的值实现的。

Connection Keep-Alive 一般情况下,一旦Web服务器向浏览器发送了数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了Connection:keep-alive,则TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

Cookie是浏览器向服务器发送和当前网站关联的Cookie,这样在服务器端也能读取浏览器端的Cookie了。

User-Agent为浏览器的版本信息。通过这个信息可以读取浏览器是IE还是FireFox、支持的插件、.Net版本等。

2.1.3 HTTP响应报文解析

Server: Cassini/3.5.0.5 表示服务器的类型

Content-Type: text/html; charset=utf-8 表示返回数据的类型

服务器通过Content-Type告诉客户端响应的数据的类型,这样浏览器就根据返回数据的类型来进行不同的处理,如果是图片类型就显示图片,如果是文本类型就直接显示内容,如果用html类型就用浏览器显示内容,如果是下载类型就弹出下载工具等。

原则上浏览器会根据Content-Type来决定如何显示返回的消息体内容。

常用Content-Type:text/HTML、image/GIF、image/JPEG、text/plain、text/javascript、application/x-excel 、application/octet-stream(二进制文件)

Content-Length: 19944表示响应报文体的字节长度,报文头只是描述,返回的具体数据(比如HTML文本、图片数据等)在两个回车之后的内容中。

 

状态码:

200段是成功;

300段需要对请求做进一步的处理;

400段表示客户端请求错误;

500段是服务器的错误。

 

浏览器向服务器发出请求,服务器处理可能是成功、可能是失败、可能没有权限访问等原因,服务器会通过响应码来告诉浏览器处理结果。

"200" : OK

"301" : Redirect 重定向: 301 代表永久性转移(Permanently Moved)。

"302" : Redirect 重定向 302 代表暂时性转移(Temporarily Moved )。

"304" :Not Modified  客户端已经执行了GET,但文件未变化。如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个304状态码。有一部分网站的网页内容是静态页面,如图片,htmljs等,这些静态页面往往可能是服务器早已准备好的,用户访问时仅仅是下载而已。那么针对这种静态页面,就可以仅仅通过304状态码来判断,内容是否发生了变化。

"400" : Bad Request 错误请求,发出错误的不符合Http协议的请求.

"401" : Unauthorized 未授权。一般该错误消息表明您首先需要登录(输入有效的用户名和密码)

"403" : Forbidden 禁止。

"404" : Not Found 未找到。演示访问一个不存在的页面看报文

"500" : Internal Server Error 服务器内部错误。演示页面抛出异常。

"503" : Service Unavailable。一般是访问人数过多。

 

http是无状态的,不会记得“上个请求***”,所以哪怕是同一个页面中的js、css、jpg也都要重复的提交Accept-Language、Accept-Encoding、Cookie等。

网页中如果有图片、css、js等外部文件的话,图片、css、js都在单独的请求中,也就是并不是页面的所有内容都在一个请求中完成,而是每个资源一个请求。

一般情况下,只有浏览器请求服务器端,服务器端才有给浏览器响应数据,服务器不会主动向浏览器推送数据,这样是安全考虑,也是提高服务器的性能考虑。如果要服务器向浏览器推送数据,则需要使用ServerPush(ajax隔一段时间到服务器请求最新的数据)等额外的技术。

 

Http是“请求—响应”的工作方式。

2.2 HTTP协议的Web服务器

该Web服务器遵循HTTP协议,可以处理各种浏览器客户端的请求。这是与前面socket聊天程序的服务端最大的不同。

2.2.1实现简单Web服务

请求、响应一次结束,连接就关闭了。所以每个通信的socket只Receive一次,不用循环。浏览器发送请求之后,处于等待状态,服务器响应之后,将连接关闭,浏览器才结束等待状态。

服务器端处理html仅仅是将html文件找到,发给客户端,在客户端由浏览器解释执行。

静态页面不会执行任何服务器端代码的页面,动态页面就是要在服务器上执行服务器端代码的页面。

2.2.2 Http协议通讯过程

(A) 三次握手建立连接

(B) 浏览器发送Http约定好的请求,服务器发送http约定好的响应(http格式的超文本信息)

(C) 连接关闭

2.2.3浏览器和服务器

浏览器客户端工具软件。

服务器就是为浏览器提供服务的计算机软件。

3、Web服务器模拟IIS服务

3.1请求-响应过程

服务器遵循http协议,处理静态页面或处理动态页面。

(1)、接收浏览器发送的请求(socket)

(2)、分析请求报文,请求的路径--根据后缀名去判断请求的文件是动态还是静态。

(3)、生成响应报文头,响应体。

(4)、发送响应内容。

3.2 服务器详细步骤

3.2.1 服务器处理和响应流程

(1)、创建负责监听的socket,接收连接创建负责通信的socket。

(2)、创建DataConn实例,构造方法中接收请求报文,输出响应报文。

(3)、DataCon接收请求报文,交由Request类解析和封装,Judge()进行判断和进一步处理。

(4)、DataConn的Judge()根据请求路径的后缀判断,静态页面交给ProcessStaticPage处理和响应,动态页面交给ProcessDyPage处理和响应。

3.2.2 服务器处理和响应业务逻辑

(1)、根据请求文件的后缀名,判断是静态还是动态页面-生成响应头

(2)、静态页面的处理和响应

读取静态页面,发送回浏览器Response(发送响应报文)

(3)、动态页面的处理和响应

A、创建接口IHttpHandler,保证所有能处理请求的类都具有ProcessRequest

B、写具体的负责处理请求的类。

C、通过反射创建负责请求类的对象,并且放到接口中。

D、通过接口调用类的ProcessRequest方法。

E、发送响应消息。

4、Web服务器代码清单

4.1 Blowser:浏览器

using System.Net;

using System.Net.Sockets;

using System.Threading;

using System.IO;

namespace chatClient

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        

        private void btnStart_Click(object sender, EventArgs e)

        {

            HttpWebRequest request = WebRequest.Create(txtUrl.Text) as HttpWebRequest;

            request.Method = "Get";

            

            HttpWebResponse response = request.GetResponse() as HttpWebResponse;

            

            using (Stream stream = response.GetResponseStream())

        {

StreamReader srd = new StreamReader(stream);

txtMsg.Text = srd.ReadToEnd();

        }

        }

    }

}

4.2 Server:Web服务器

这里服务器程序与前面聊天程序不同之处,在RecMsg方法中是一次性操作,接收一次客户端的消息,进行处理和响应,然后连接就关闭了,因此不用循环。

using System.Net;

using System.Net.Sockets;

using System.Threading;

using System.IO;

namespace Server

{

    //主窗体

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

            Control.CheckForIllegalCrossThreadCalls = false;

        }

        

        //创建socket对象,开启线程,执行Listen方法监听连接

        private void btnStart_Click(object sender, EventArgs e)

        {

            IPAddress ip = IPAddress.Parse(txtServer.Text.Trim());

            IPEndPoint point = new IPEndPoint(ip,int.Parse(txtPort.Text));        

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

            try

            {

                WatchSoc.Bind(point);

                WatchSoc.Listen(11);

                btnStart.Enabled = false;

                ShowMsg("服务器运行中...");

                

                //开启一个线程,执行AcceptConn接收客户端连接、请求

                Thread thWatch = new Thread(AcceptConn);

                thWatch.IsBackground = true;

                thWatch.Start(WatchSoc);

            }

            catch(Exception ex)

            {

                MessageBox.Show(ex.Message);

                btnStart.Enabled = true;

            }

        }

        

        //AcceptConn方法

        void AcceptConn(object socket)

        {

            while (true)

            {

                //使用Accept方法接受连接请求,返回一个新的socket对象,进行通讯

                Socket watchSocket = socket as Socket;

                Socket connSocket = watchSocket.Accept();

                ShowMsg(connSocket.RemoteEndPoint.ToString() "连接成功!");

                

                //接收客户端消息(只接收和响应一次然后关闭)

//定义数据通信类:构造方法中接收浏览器的http请求,并向浏览器发送http响应

                DataConn dataconn = new DataConn(connSocket, ShowMsg);

            }

        }

        

        //ShowMs方法

        void ShowMsg(string msg)

        {

            txtLog.AppendText(msg "\r\n");

        }

    }

}

 

    //委托在这里的作用,就是执行方法ShowMsg中的代码

    delegate void DelShowMsg(string msg);

    

    //dataconn类处理请求响应

    /// <summary>

    /// 数据通信(接收浏览器的http请求,并向浏览器发送http响应)

    /// </summary>

    class DataConn

    {

        private Socket socket;

        private DelShowMsg dele;

        public DataConn(Socket socket, DelShowMsg del)

        {

            this.socket = socket;

            this.del = del;

           

      //1 接收http请求报文

            string msg=RecMsg();

            // 显示到文本框中

            dele(msg);

            

      //2 解析请求报文

//用Request类的构造方法得到Url等一些报文中的数据项

         Request req = new Request(msg);

         //可以用req.Url和req.method 得到想要的数据

            

      //3 根据后缀名判断请求的文件是静态还是动态的,进行相应的响应

            Judge(req.Url);

        }

        

    //接收请求报文msg

        string RecMsg() //此处只接收一次没有循环处理调用该方法时不考虑多线程

        {

            byte[] buffer = new byte[1024 * 1024 * 2];

            int length = socket.Receive(buffer);

            string msg = Encoding.UTF8.GetString(buffer, 0, length);

            

            return msg;

        }

        

        //判断页面并调用对应方法处理

        void Judge(string url)

        {

            string ext = Path.GetExtension(url).ToLower();

            switch (ext.ToLower())

            {

                case ".html":

                case ".htm":

                case ".jpg":

                case ".js":

                case ".css":

                case ".gif":

                    //静态文件

                    ProcessStaticPage(url);

                    break;

                case ".aspx":

                    //动态文件

                    ProcessDynamicPage(url);

                    break;

                default:

                    break;

            }

        }

        

      //4 处理静态页面的方法

        void ProcessStaticPage(string url)

        {

            //得到绝对路径(启动项目的路径 请求的路径url)

            string path = Application.StartupPath url;

            

            //生成和发送响应报文

            using (FileStream fs = new FileStream(path, FileMode.Open))

            {

                //(1)获取响应头

                Response res = new Response(200, buffer.Length, Path.GetExtension(url));

                

                //(2)读取文件流得到字节数组--响应体

                byte[] buffer = new byte[fs.Length];

                fs.Read(buffer, 0, buffer.Length);

                

                //(3)发送响应报文关闭连接

                socket.Send(res.GetResponseHeader());

                socket.Send(buffer);

                socket.Close();

            }

        }

        

      //5 处理动态页面的方法

        void ProcessDynamicPage(string url)

        {

            string fileName = Path.GetFileNameWithoutExtension(url); 

            IHttpHandler handler = null;  

            //通过反射创建处理浏览器请求的类的对象

            //(1)获得要创建实例的类的命名空间

            string nameSpace = System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace;         

//(2)获得要创建实例的完整类名

string className = nameSpace "." fileName;                                                               //(3)创建实例

            handler = System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(className, true) as IHttpHandler;

            

            //调用实例的ProcessRequest()方法处理请求(本示例中的作用是生成响应体)

            byte[] buffer = handler.ProcessRequest();

            

            //得到包含响应头的对象

            Response res = new Response(200, buffer.Length, Path.GetExtension(url));

            

            //发送响应报文到浏览器关闭连接

            socket.Send(res.GetResponseHeader());

            socket.Send(buffer);

            

            socket.Close();

        }

}

4.3 服务端重要的类和方法

4.3.1 Request类:获取和封装请求信息

    //通过接收到的消息用构造函数获取请求信息:路径和请求方法

    class Request

    {

        string url;

        public string Url

        {

            get { return url; }

            set { url = value; }

        }

        

        string method;

        public string Method

        {

          get { return method; }

          set { method = value; }

        }

        

        public Request(string msg)

        {      

            string[] lines = msg.Split(new string[] {"\r\n"},StringSplitOptions.RemoveEmptyEntries);

            

            this.method = lines[0].Split(' ')[0];

            this.url = lines[0].Split(' ')[1];

        }

    }

 

4.3.2 ProcessRequest方法:处理请求

    //IHttpHandler接口--ProcessRequest方法处理请求生成响应体

    interface IHttpHandler

    {

        byte[] ProcessRequest();

    }

    

    //实现IHttpHandler接口的类

    class NewsList : IHttpHandler

    {

        /// <summary>

        /// 处理请求,生成响应体

        /// </summary>

        /// <returns></returns>

        public byte[] ProcessRequest()

        {

            StringBuilder sb = new StringBuilder();

            sb.Append("<html>");

            sb.Append("<body>");

            sb.Append("<h3>新闻列表</h3>");

            sb.Append("<b><a href='newsdetails.aspx'>全面开放二孩了!</a></b>");

            sb.Append("<br />时间:" DateTime.Now);

            sb.Append("</body>");

            sb.Append("</html>");

            return Encoding.UTF8.GetBytes(sb.ToString());

        }

}

 

    //实现IHttpHandler接口的类

    class NewsDetails:IHttpHandler

    {

        /// <summary>

        /// 处理请求,生成响应体

        /// </summary>

        /// <returns></returns>

        public byte[] ProcessRequest()

        {

            StringBuilder sb = new StringBuilder();

            sb.Append("<html>");

            sb.Append("<body>");

            sb.Append("<h3>新闻详细页面</h3>");

            sb.Append("<b>全面开放二孩了!</b>");

            sb.Append("<br />将取消准生证,取消晚婚晚育奖励政策");

            sb.Append("<br />时间:" DateTime.Now);

            sb.Append("</body>");

            sb.Append("</html>");

            

            return Encoding.UTF8.GetBytes(sb.ToString());

        }

    }

4.3.3 Response类:生成响应数据

    class Response

    {

        private int statusCode;

        /// <summary>

        /// 响应码

        /// </summary>

        public int StatusCode

        {

            get { return statusCode; }

            set { statusCode = value; }

        }

        

        private string contentType;

        /// <summary>

        /// 内容类型

        /// </summary>

        public string ContentType

        {

            get { return contentType; }

            set { contentType = value; }

        }

        

        private int contentLength;

        /// <summary>

        /// 内容的长度

        /// </summary>

        public int ContentLength

        {

            get { return contentLength; }

            set { contentLength = value; }

        }

        

        /// <summary>

        /// 初始化字段和参数

        /// </summary>

        public Response(int statusCode, int contentLength,string ext)

        {

            this.statusCode = statusCode;          //初始化statusCode

            FillDic();                          //初始化statusCode对应的描述

            this.contentLength = contentLength;     //初始化contentLength

            SetConentType(ext);                 //初始化contentType

        }

        

        void FillDic()

        {

            dic.Add(200,"OK");

            dic.Add(404,"Not Found");

            dic.Add(403, "Forbidden");

            dic.Add(503, "Service Unavailable");

        }

        

   Dictionary<int, string> dic = new Dictionary<int, string>();

       

       //对post请求,根据url信息中文件的后缀名,设置contentType

//get请求没有必要,没有contentType

    void SetConentType(string ext)

         {

            switch (ext)

        {

        case ".htm":

                case ".html":

                    contentType = "text/html";

                    break;

                case ".css":

                    contentType = "text/css";

                    break;

                case ".js":

                    contentType = "text/javascript";

                    break;

                case ".jpg":

                    contentType = "image/jpeg";

                    break;

                case ".png":

                    contentType = "image/png";

                    break;

                case ".aspx":

                    contentType = "text/html";

                    break;

                default:

                    break;

        }

         }

        

        /// <summary>

        /// 生成响应头

        /// </summary>

        public byte[] GetResponseHeader()

        {

            StringBuilder builder = new StringBuilder();

            builder.Append("HTTP/1.1 " statusCode " " dic[statusCode] "\r\n");

            builder.Append("Content-Type:" contentType ";charset=utf-8\r\n");

            builder.Append("Content-Length:" contentLength "\r\n\r\n");

            return Encoding.UTF8.GetBytes(builder.ToString());

        }

}

5、Http请求

5.1 Http请求方法

GET 通过请求URI得到资源

POST 用于添加新的内容

PUT用于修改某个内容

DELETE 删除某个内容

CONNECT 用于代理进行传输,如使用SSL

OPTIONS 询问可以执行哪些方法

PATCH 部分文档更改

TRACE 用于远程诊断服务器

HEAD 类似于GET, 但是不返回body信息,用于检查对象是否存在,以及得到对象的元数据

PROPFIND, (wedav) 查看属性

PROPPATCH, (wedav) 设置属性

MKCOL, (wedav) 创建集合(文件夹)

COPY, (wedav) 拷贝

MOVE, (wedav) 移动

LOCK, (wedav) 加锁

UNLOCK (wedav) 解锁

5.1.1 post和get的区别

    (A)数据量小 通过url传参更方便处理 这时用get;数据量大,通过json对象传数据(如Ajax无刷新功能的post提交),以及传送大文件等用post请求

    (B)通常非表单元素的值通过get请求中url参数的形式提交到服务器端,表单元素的值通过post请求提交到服务器端

    (C)get请求 没有contentType,post请求必须有contentType,并且有Cache-Control: no-cache的特点 (post不缓存)    

    表单提交方面:

    (D)还可以设定<form>的method属性指定表单提交方式不写method时,默认为get,但是vs中form自动带出的是post

    (E)get是通过URL传递表单值,post通过url看不到表单域传值,表单值是隐藏到http报文体中,url中看到不。

    (F)get传递的数据量是有限的,如果要传递大数据量不能用get。比如type=“file”上传文章、type=“password”传递密码或者<textarea>发表大段文章,post则没有这个限制;

    (G)页面刷新时(例如F5),post会有浏览器提示重新提交表单的问题,get则没有。

5.1.2 post请求注意

(A)只能post表单元素,只能为input(text、CheckBox、RadioButton等)、textarea、select三种类型的标签。

      RadioButton,同name的为一组,选中的RadioButton的value被提交到服务器。

(B)表单域只有设定了name的才会被提交给服务器(用get方式看的清楚)。如果给submit按钮设定name,那么按钮的value也会被提交给服务器

(C)如果多个name=test 的元素,服务器输出多个值,如abc,123,zxc,将value的值用逗号分开。

(D)disabled的控件没办法提交给服务器

    (E)post方式的正确的地址很难直接发给别人。

5.1.3 get请求注意

URL数据格式,服务端文件名后跟着“?”,由于客户端可能向服务器端提交多个键值对,键值对之间用“&”进行分割,如果URL中有汉字、特殊符号等,则需要对URL进行编码。

5.1.4 刷新时的重复提交方式

get:如果访问Hello.ashx,多次点击刷新(重复上一次操作),都是“直接进入”

post:如果刚提交过表单,再多次点击刷新,都是“提交进入”

对于包含表单的页面,重新敲地址栏再刷新就不会提示重新提交了,因为重新敲地址时get请求,没有进行过post提交。