自己动手模拟开发一个简单的Web服务器

开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的Web服务器来体会一下。

一、请求-处理-响应模型

1.1 基本过程介绍

  每一个HTTP请求都会经历三个步凑:请求-处理-响应:每当我们在浏览器中输入一个URL时都会被封装为一个HTTP请求报文发送到Web服务器,而Web服务器则接收并解析HTTP请求报文,然后针对请求进行处理(返回指定的HTML页面、CSS样式表、JS脚本文件亦或是加载动态页面生成HTML并返回)。最后将要返回的内容转为输出流并封装为HTTP响应报文发送回浏览器。

  当然,浏览器接收到响应报文后会加载HTML、CSS与JS并显示在页面中,最后成为我们看到的最终效果。

1.2 通信过程介绍

  Web服务器本质上来说就是一个Socket服务端,在不停地接受着客户端的请求,然后针对每一个客户端的请求进行处理,处理完毕就即时关闭连接。而我们的浏览器则是一个Socket客户端,通过TCP协议向服务端发送HTTP请求报文

About:Socket非常类似于电话插座,以一个电话网为例:电话的通话双方相当于相互通信的2个程序,电话号码就是IP地址。任何用户在通话之前,首先要占有一部电话机,相当于申请一个Socket;同时要知道对方的号码,相当于对方有一个固定的Socket。然后向对方拨号呼叫,相当于发出连接请求。对方假如在场并空闲,拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向Socket发送数据和从Socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤消连接。

1.3 HTTP协议基础

  Internet的基本协议是TCP/IP协议(传输控制协议和网际协议),目前广泛使用的 FTP、HTTP(超文本传输协议)、Archie Gopher都是建立在TCP/IP上面的应用层协议,不同的协议对应不同的应用。而HTTP协议是Web应用所使用的主要协议

  HTTP协议是基于请求响应模式的。客户端向服务器发送一个请求,请求头包含请求的方法、 URI、协议版本、以及包含请求修饰符、客户端信息和内容的类似MIME的消息结果。服务器则以一个状态行作为响应,相应的内容包括消息协议的版本、成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。

  HTTP是无状态协议,依赖于瞬间或者近乎瞬间的请求处理。请求信息被立即发送,理想的情况是 没有延时的进行处理,不过,延时还是客观存在的。HTTP有一种内置的机制,在消息的传递时间上由一定的灵活性:超时机制。一个超时就是客户机等待请求 消息的返回信息的最长时间。

  (1)HTTP请求报文示例

  (2)HTTP响应报文示例

TIP:关于HTTP协议的详细介绍,可以浏览一下小坦克大神的这篇:HTTP协议详解

二、关键设计思路

2.1 要实现的功能

  (1)处理用户的静态文件请求:主要是指html/css/js文件的请求;

  (2)处理用户的动态文件请求:这里只处理ASP.NET请求,即ashx与aspx文件的请求;

2.2 要封装的类

  (1)HttpRequest、HttpResponse与HttpContext类

  根据我们对ASP.NET请求处理机制的分析,我们知道在HttpRuntime的ProcessRequest方法中构造了一个HttpContext对象。在HttpContext的构造函数中,根据HttpWorkerRequest对象创建了HttpContext对象,这是一个重要的Http上下文对象,两个重要类型的字段也随之被初始化:HttpRequest对象和HttpResponse对象。因此,我们在设计时也可以设计一个HttpContext类将HttpRequestHttpResponse两个实例进行封装。

TIP:有关ASP.NET请求处理机制的分析,可以浏览我的另外一篇文章:ASP.NET请求处理机制探索之二-核心

  (2)IHttpHandler接口与实现IHttpHandler接口的HttpApplication类

  针对每个Http请求都有一个抽象的HttpApplication对象来进行处理,而为了考虑扩展性(可以是ashx,也可以是aspx),封装了一个IHttpHandler接口,让不同的处理对象实现这个接口即可。IHttpHandler接口很简单,就声明了一个ProcessRequest方法,每个实现的类只需要实现这个方法即可。

2.3 总体设计流程

三、关键代码实现

3.1 开启Socket服务监听浏览器端的HTTP请求

        private void btnStart_Click(object sender, EventArgs e)
        {
            // 创建Socket->绑定IP与端口->设置监听队列的长度->开启监听连接
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socketWatch.Bind(new IPEndPoint(IPAddress.Parse(txtIPAddress.Text), int.Parse(txtPort.Text)));
            socketWatch.Listen(10);
            // 创建Thread->后台执行
            threadWatch = new Thread(ListenClientConnect);
            threadWatch.IsBackground = true;
            threadWatch.Start(socketWatch);

            isEndService = false;
            txtIPAddress.ReadOnly = true;
            txtPort.ReadOnly = true;
            btnStart.Enabled = false;

            ShowMessage("~_~消息:【您已成功启动Web服务!】");
        }

        private void ListenClientConnect(object obj)
        {
            Socket socketListen = obj as Socket;

            while (!isEndService)
            {
                Socket proxSocket = socketListen.Accept();
                byte[] data = new byte[1024 * 1024 * 2];
                int length = proxSocket.Receive(data, 0, data.Length, SocketFlags.None);
                // Step1:接收HTTP请求
                string requestText = Encoding.Default.GetString(data, 0, length);
                HttpContext context = new HttpContext(requestText);
                // Step2:处理HTTP请求
                HttpApplication application = new HttpApplication();
                application.ProcessRequest(context);
                ShowMessage(string.Format("{0} {1} from {2}", context.Request.HttpMethod, context.Request.Url, proxSocket.RemoteEndPoint.ToString()));
                // Step3:响应HTTP请求
                proxSocket.Send(context.Response.GetResponseHeader());
                proxSocket.Send(context.Response.Body);
                // Step4:即时关闭Socket连接
                proxSocket.Shutdown(SocketShutdown.Both);
                proxSocket.Close();
            }
        }

  在监听线程中,通过HttpApplication类对象调用其ProcessRequest方法进行具体的处理。最重要的,处理完毕后立即通过Socket发送响应信息,并及时关闭Socket连接。

3.2 设计HttpConext类封装HttpRequest与HttpResponse

  (1)HttpContext

    public class HttpContext
    {
        public HttpRequest Request { get; set; }
        public HttpResponse Response { get; set; }

        public HttpContext(string requestText)
        {
            Request = new HttpRequest(requestText);
            Response = new HttpResponse();
        }
    }

  (2)HttpRequest

    public class HttpRequest
    {
        public HttpRequest(string requestText)
        {
            string[] lines = requestText.Replace("\r\n", "\r").Split(‘\r‘);
            string[] requestLines = lines[0].Split(‘ ‘);
            // 获取HTTP请求方式、请求的URL地址、HTTP协议版本
            HttpMethod = requestLines[0];
            Url = requestLines[1];
            HttpVersion = requestLines[2];
        }
        // 请求方式:GET or POST?
        public string HttpMethod { get; set; }
        // 请求URL
        public string Url { get; set; }
        // Http协议版本
        public string HttpVersion { get; set; }
        // 请求头
        public Dictionary<string, string> HeaderDictionary { get; set; }
        // 请求体
        public Dictionary<string, string> BodyDictionary { get; set; }
    }

  (3)HttpResponse

    public class HttpResponse
    {
        // 响应状态码
        public string StateCode { get; set; }
        // 响应状态描述
        public string StateDescription { get; set; }
        // 响应内容类型
        public string ContentType { get; set; }
        //响应报文的正文内容
        public byte[] Body { get; set; }

        // 生成响应头信息
        public byte[] GetResponseHeader()
        {
            string strRequestHeader = string.Format(@"HTTP/1.1 {0} {1}
Content-Type: {2}
Accept-Ranges: bytes
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET
Date: {3}
Content-Length: {4}

", StateCode, StateDescription, ContentType, string.Format("{0:R}", DateTime.Now), Body.Length);

            return Encoding.UTF8.GetBytes(strRequestHeader);
        }
    }

  这里需要注意的是在HttpResponse类中,为了生成响应头信息,需要格式化一个固定格式的信息,并且在最后保留两个CRLF(即换行符)作为头部结束标志,可以看看下面的格式说明:

3.3 设计IHttpHandler接口

    public interface IHttpHandler
    {
        void ProcessRequest(HttpContext context);
    }

  仿照ASP.NET内部实现,我们也设计一个IHttpHandler接口,只定义了一个方法:ProcessRequest;

3.4 设计实现IHttpHandler接口的HttpApplication类

    public class HttpApplication : IHttpHandler
    {
        // 对请求上下文进行处理
        public void ProcessRequest(HttpContext context)
        {
            // 1.获取网站根路径
            string bastPath = AppDomain.CurrentDomain.BaseDirectory;
            string fileName = Path.Combine(bastPath+"\\MyWebSite", context.Request.Url.TrimStart(‘/‘));
            string fileExtension = Path.GetExtension(context.Request.Url);
            // 2.处理动态文件请求
            if(fileExtension.Equals(".aspx") || fileExtension.Equals(".ashx"))
            {
                string className = Path.GetFileNameWithoutExtension(context.Request.Url);
                IHttpHandler handler = Assembly.Load("MyWebServer").CreateInstance("MyWebServer.Page." + className) as IHttpHandler;
                handler.ProcessRequest(context);

                return;
            }
            // 3.处理静态文件请求
            if (!File.Exists(fileName))
            {
                context.Response.StateCode = "404";
                context.Response.StateDescription = "Not Found";
                context.Response.ContentType = "text/html";
                string notExistHtml = Path.Combine(bastPath, @"MyWebSite\notfound.html");
                context.Response.Body = File.ReadAllBytes(notExistHtml);
            }
            else
            {
                context.Response.StateCode = "200";
                context.Response.StateDescription = "OK";
                context.Response.ContentType = GetContenType(Path.GetExtension(context.Request.Url));
                context.Response.Body = File.ReadAllBytes(fileName);
            }
        }

        // 根据文件扩展名获取内容类型
        public string GetContenType(string fileExtension)
        {
            string type = "text/html; charset=UTF-8";
            switch (fileExtension)
            {
                case ".aspx":
                case ".html":
                case ".htm":
                    type = "text/html; charset=UTF-8";
                    break;
                case ".png":
                    type = "image/png";
                    break;
                case ".gif":
                    type = "image/gif";
                    break;
                case ".jpg":
                case ".jpeg":
                    type = "image/jpeg";
                    break;
                case ".css":
                    type = "text/css";
                    break;
                case ".js":
                    type = "application/x-javascript";
                    break;
                default:
                    type = "text/plain; charset=gbk";
                    break;
            }
            return type;
        }
    }

  这里,我们封装一个抽象的HttpApplication类,它实现了IHttpHandler接口,对一般的请求做一个通用的处理操作。如果是静态文件请求,则直接读取文件并生成响应流,如果是动态文件请求,则通过反射方式生成对应的Page对象实例,将HttpContext对象传入其ProcessRequest方法中进行处理,最后的响应内容都封装到了HttpConext中的HttpResponse对象的Body属性中。

3.5 设计实现IHttpHandler接口的模拟Page类

    public class DemoPage : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            StringBuilder sbText = new StringBuilder();
            sbText.Append("<html>");
            sbText.Append("<head><meta http-equiv=‘Content-Type‘ content=‘text/html; charset=utf-8‘/><title>DemoPage</title></head>");
            sbText.Append("<body style=‘margin:10px auto;text-align:center;‘>");
            sbText.Append("<h1>用户信息列表</h1>");
            sbText.Append("<table align=‘center‘ cellpadding=‘1‘ cellspacing=‘1‘><thead><tr><td>ID</td><td>用户名</td></tr></thead>");
            sbText.Append("<tbody>");
            sbText.Append(GetDataList());
            sbText.Append("</tbody></table>");
            sbText.Append(string.Format("<h3>更新时间:{0}</h3>", DateTime.Now.ToString()));
            sbText.Append("</body>");
            sbText.Append("</html>");

            context.Response.Body = Encoding.UTF8.GetBytes(sbText.ToString());
            context.Response.StateCode = "200";
            context.Response.ContentType = "text/html";
            context.Response.StateDescription = "OK";
        }

        private string GetDataList()
        {
            StringBuilder sbData = new StringBuilder();
            string strConn = System.Configuration.ConfigurationManager.ConnectionStrings["MyConn"].ToString();
            using (SqlConnection conn = new SqlConnection(strConn))
            {
                conn.Open();
                using (SqlCommand cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT * FROM UserInfo";
                    using(SqlDataAdapter adapter = new SqlDataAdapter(cmd))
                    {
                        DataTable dt = new DataTable();
                        adapter.Fill(dt);

                        if(dt != null)
                        {
                            foreach (DataRow row in dt.Rows)
                            {
                                sbData.Append("<tr>");
                                sbData.Append(string.Format("<td>{0}</td>",row["ID"].ToString()));
                                sbData.Append(string.Format("<td>{0}</td>", row["UserName"].ToString()));
                                sbData.Append("</tr>");
                            }
                        }
                    }
                }
            }

            return sbData.ToString();
        }
    }

  这里我们模拟一个Page页面类,它也实现了IHttpHandler接口,在ProcessRequest方法通过ADO.NET访问了数据库并读取数据作为输出内容。这里,我们主要是通过分析ASP.NET WebForm中的aspx对象,它虽然直接继承的类是Page类,但是Page类却是实现了IHttpHandler接口的。在具体的处理方法中,都是通过调用这个接口的ProcessRequest方法进行处理的。

四、个人开发小结

4.1 开发效果展示

  (1)开启监听服务

  (2)请求静态页面

  (3)请求动态页面

4.2 开发实战总结

  本次模拟的一个超级简单的Web服务器软件,实现了静态文件和动态文件(通过模拟aspx页面对象)的处理和响应。但是,还有很多的功能并未实现,因为一个真正的Web服务器需要考虑的东西很多很多,例如:多线程的处理优化、高效的IO模型等。不过,对于一个最基本的Web服务器所需要了解的最基本的原理:Socket的监听和连接、基于TCP协议的HTTP协议、动态文件类的反射与调用等,模拟开发本次的DEMO的过程是可以达到的。

附件下载

  MyWebServer v1.0http://pan.baidu.com/s/1mgtC1HA

作者:周旭龙

出处:http://edisonchou.cnblogs.com/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

时间: 2024-10-17 12:12:20

自己动手模拟开发一个简单的Web服务器的相关文章

自己模拟的一个简单的web服务器

首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器是如何工作的 servlet容器是一个复杂的系统.不过,一个servlet容器要为一个servlet的请求提供服务,基本上有三件事要做: 1,创建一个request对象并填充那些有可能被所引用的servlet使用的信息,如参数.头部. cookies.查询字符串. URI 等等.一个 request

使用 Nodejs 搭建一个简单的Web服务器

使用Nodejs搭建Web服务器是学习Node.js比较全面的入门教程,因为要完成一个简单的Web服务器,你需要学习Nodejs中几个比较重要的模块,比如:http协议模块.文件系统.url解析模块.路径解析模块.以及301重定向问题,下面我们就简单讲一下如何来搭建一个简单的Web服务器. 作为一个Web服务器应具备以下几个功能: 1.能显示以.html/.htm结尾的Web页面 2.能直接打开以.js/.css/.json/.text结尾的文件内容 3.显示图片资源 4.自动下载以.apk/.

如何用PHP/MySQL为 iOS App 写一个简单的web服务器(译) PART1

原文:http://www.raywenderlich.com/2941/how-to-write-a-simple-phpmysql-web-service-for-an-ios-app 作为一个iPhone/iPad开发者,能够自己写一个简单的web服务器将是很有用的. 例如,你可能希望在软件启动时显示一些来自服务器的更新,或者在服务器端保存一些用户数据.除了你的想象力,没有什么能限制你了. 在第一篇中,我们将会一步一步的建立一个web服务器,基于promo code system(促销码系

一个简单的web服务器

static void Main(string[] args) { IPAddress localAddress = IPAddress.Loopback;//获取本机的ip地址 IPEndPoint endPoint =new IPEndPoint(localAddress, 49155); Socket socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); socket.Bind

一个简单的Web服务器-支持Servlet请求

上接 一个简单的Web服务器-支持静态资源请求,这个服务器可以处理静态资源的请求,那么如何处理Servlet请求的呢? 判断是否是Servlet请求 首先Web服务器需要判断当前请求是否是Servlet请求. 像Tomcat,通过解析HTTP报文拿到请求url后,就可以根据web.xml来查找是否有匹配的Servlet,如果有匹配则认定为是一个有效的Servlet请求,然后将request,response传给对应的servlet的service()方法. 这里既然要实现一个简单的Web服务器,

一个简单的web服务器例子

一个简单的web容器小例子,功能十分简单,只能访问静态资源,对于新手来说还是有一定的意义.主要分三个类 1.server类:主要功能开启socketServer,阻塞server,接收socket访问,解析request,创建request,作出响应 public class TestServer1 { private boolean shutdown = false; // web目录webroot public static final String WEB_ROOT = System.ge

(一)一个简单的Web服务器

万丈高楼平地起,首先我们必须了解 超文本传输协议(HTTP) 以后才能够比较清晰的明白web服务器是怎么回事. 1. 浅析Http协议 HTTP是一种协议,允许web服务器和浏览器通过互联网进行来发送和接受数据.它是一种请求和响应协议.客户端请求一个文件而服务器响应请求.HTTP使用可靠的TCP连接--TCP默认使用80端口.第一个HTTP版是HTTP/0.9,然后被HTTP/1.0所替代.正在取代HTTP/1.0的是当前版本HTTP/1.1,它定义于征求意见文档(RFC) 2616,可以从ht

学习用node.js建立一个简单的web服务器

一.建立简单的Web服务器涉及到Node.js的一些基本知识点: 1.请求模块 在Node.js中,系统提供了许多有用的模块(当然你也可以用JavaScript编写自己的模块,以后的章节我们将详细讲解),如http.url等.模块封装特定的功能,提供相应的方法或属性,要使用这些模块,需要先请求模块获得其操作对象. 例如要使用系统的http模块,可以这样写: var libHttp = require('http'); //请求HTTP协议模块 这样,以后的程序将可以通过变量libHttp访问ht

Tomcat学习笔记(一)一个简单的Web服务器

内容为<深入剖析Tomcat>第一章重点,以及自己的总结,如有描述不清的,可查看原书. 一.HTTP协议: 1.定义:用于服务器与客户端的通讯的协议,允许web服务器和浏览器通过互联网进行发送和接收数据.是一种请求和响应协议,使用可靠的TCP协议,TCP协议的端口为80,是一种面向连接的协议. 2.HTTP协议请求的三个组成部分:这三部分之间用回车换行符(CRLF)隔开 请求部分:方法(GET/POST等7种,其他的很少用,书上有介绍)[空格,该部分内容以空格隔开] 统一资源标识符URI[空格