Socket写的Web服务器——带详细图解

——闲扯:  

    Socket是大家都很熟悉的.NET处理底层硬件通信的类。比如:物联网中的一个器件要与其他器件相通信,那就必须使用到Socket来实现。但是我对Socket的中文翻译很不满意:Socket的中文翻译是“套接字”。我请问一下各位读者朋友,我如果只告诉你“套接字”你会知道这是什么吗? Socket的英文含义是:“插座、开关”,但你能通过“套接字”知道Socket的原意吗?

       Socket就像一根电话线,连接通两端的电话。让电话可以实现通信。我们声明一个Socket对象从实例开始监听的那一刻开始,Socket就像一个电话插座一样,随时监听等待消息的传入,而我们建立连接就像把插头插在这个插座上一样,一插即可通讯。效果和寓意正如英文的原意:插座、开关相符。      

       很多的外国技术文献翻译过来很难让人想象到它原本的意思,这是最失败的地方。而且直接音译的“套接字”也很难跟读音['sɑːkɪt]的Socket联系起来,反而更像读音['tɑːɡɪt]的target.很多晦涩难懂的专业技术名词,你只要查看其英文原意,往往都会恍然大悟、醍醐灌顶。我不知道“前辈”们为何会这样翻译,我以为一个东西的翻译可以有更好的选择,最起码不能翻译的太偏、太晦涩,以至于我们这些后来人很难接受。

       我认为Socket译为“通信插座”更为恰当。我们设置一个Socket对象的实例开始监听,就像设置一个电话插座在那一样,谁拨我这个“IP地址和端口”,我就接通谁。我觉得Socket翻译成“套接字”相对于林语堂大师翻译的“humor:幽默,sofa:沙发”相比,太让人无法接受了。

      总结:我推荐大家尽量去读英文原文的技术资料,去英文编程的技术网站和论坛去看。本人英语6级,虽然没有考过托福、雅思之类的,但是感觉看懂这些英文资料还是比较容易。 这或许受益于本人考研究生时对英语系统的复习,英语几乎每一个单词都有它的来历,‘汉字靠形造词,英语靠音造词’这是导致东西方文化、思想的区别的根源,也是我对学习英语最深的体会。

——正文:

  我们用过了IIS服务器,也了解了IIS服务器的实现原理和机制(读者如果不清楚,可以跟着我写完这个模拟的服务器,相信你就会明白了)。那么我们能不能手写一个类似于IIS的Web服务器呢?注意哦!我们这里写的是web服务器,而服务器有多种:FTP服务器(文件服务器)、POP3服务器(邮箱服务器)等,不过我想底层也应该大同小异.

 开始:

1、首先新建一个空白的解决方案,命名为WebServer.注意图中红色箭头的说明。

 

 2、在解决方案中添加一个WinForm应用程序,命名为“WebServer”,新建一个Winform窗体,并将窗体重命名为:"ServerForm".

3、拖动控件,进行如下布局:

4、对控件进行重命名操作:参考如图中所示。(希望读者养成规范的、良好的重命名的习惯)

5、布局完毕,剩下就是写程序了。写程序之前,我们需要先分析一下我们写Web服务器的思路

我们的思路:
(1)、先建立一个负责监听的“电话插座”——Socket,这个“电话插座”以指定的“IP地址和端口”作为“电话号码”,随时等待接通每一个拨打此“号码”(连接到此IP和端口)的人(在这里是程序进程)的电话。
(2)、因为我们当前的电话插座需要处理很多通信,所以每接通一个"电话"(接收到连接到该IP和端口的请求),我们就复制一个“电话插座”单独为该“电话”服务。(在这里我们会用到多线程的知识。 )
(3)、电话拨通了,但是我们需要懂双方的语言。也就是双方需要说同一门语言,或最起码有一个共同的互相都能懂得的语言约定。这就是HTTP协议。那么我们的浏览器和服务器之间的HTTP协议是什么样子的呢?往下看。

6、HTTP的协议分为:请求报文协议和响应报文协议。而无论是请求报文还是响应报文,其标准格式都是:头(header)、体(content).如:请求头,请求体;响应头,响应体。    

    (1)、下面来看一下我们的请求协议的报文是什么样子的:我们熟知的网页对服务器的请求分为get请求和post请求。

     a、get请求图(没有“请求体”): (那么get请求的请求体到哪里去了呢?请读者思考一下,相信很容易就想出答案)

     

    b、post请求图(请求头和请求体都有):请注意请求头和请求体之间的空行。这是HTTP协议请求报文的约定。

    

    (2)、下面让我们来看一下响应协议的报文是什么样子的

     

7、了解了请求协议的报文和响应协议的报文整体格式之后,我们需要进一步分析里面的“有用”的内容。回顾上面的请求报文图我们发现:

在第一行中包含了,请求方法、请求资源地址。

     

     好了我们拿到对方请求的报文之后,就可以截取这些“有用”的内容(注意:这里并不是说其他内容没有用,我们只是模拟Web服务器的主要功能),将响应的请求资源,以“响应协议报文”的格式,发送过去。这样浏览器也就会自动解读你发送的数据,我们的Web服务器也就实现了!

 

8、源代码开始了:

首先是ServerForm窗体的代码:

//*************************************************************************
//
//File Name:            ServerForm.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace WebServer
{
    public partial class ServerForm : Form
    {
        public ServerForm()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = false;
        }
        private void btnStart_Click(object sender, EventArgs e)
        {
            // 第一步,设置顶级的监听端口的Socket对象
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 准备Socket绑定方法的参数对象IPEndPoint
            IPAddress ipAddress;
 
            if (!IPAddress.TryParse(txtIP.Text.Trim(), out ipAddress)) // 判断当前的IP地址栏数据是否可正常转换为IP地址
            {
                return;
            }
            int port;
            if (!int.TryParse(txtPort.Text.Trim(), out port))// 判断当前的Port是否能转换为数字
            {
                return;
            }

            IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
            // 开始顶级Socket的绑定和监听
            try
            {
                serverSocket.Bind(ipEndPoint);
                serverSocket.Listen(10);
                SetLogText("服务器已经开启...");
                // 设置线程,进行连接Socket对象的处理
                Thread thread=new Thread(Listen);
                thread.IsBackground = true;// 必须设置成为后线程,后台线程在窗体关闭的时候,会自动结束自己线程运行
                thread.Start(serverSocket);// 将监听的的顶级Socket对象作为参数传入线程委托中的函数里面去
            }
            catch (Exception ex)
            {
                // 捕获到异常 
                SetLogText("服务器已经开启,您无需重复开启!");
                SetLogText("  >详细信息:\r\n   "+ex.Message);
            }
        }

        // 设置处理每一次监听到的连接的方法
        private void Listen(object o)
        {
            Socket serverSocket = o as Socket;
            while (true)
            {
                // 将服务监听到的连接,转换成一个Socket对象,后面将使用该连接的Socket进行HTTP请求的接收和响应的处理。
                Socket connSocket = serverSocket.Accept();
                SetLogText(connSocket.RemoteEndPoint+":已建立连接!");
                // 尝试进行HTTP请求的接收和处理
                try
                {
                    // 声明接收HTTP请求的二进制字节数组
                    // 将接收到的二进制字节存放到声明的二进制字节数组中去
                    byte[] buffer=new byte[1024*1024];
                    int realLen = connSocket.Receive(buffer);

                    // 如果接收到的HTTP请求是空的,则关闭当前连接的Socket对象,返回进行下一次连接的监听。
                    if (realLen <= 0)
                    {
                        // 礼貌地关闭该连接Socket对象
                        connSocket.Shutdown(SocketShutdown.Both);
                        connSocket.Close();
                        SetLogText(connSocket.RemoteEndPoint + ":0字节请求,当前连接已关闭!");
                        return;
                    }

                    // 如果接收到的HTTP请求是正常的,则进行HTTP请求报文的分析,并生成HTTP响应报文
                    string content = Encoding.UTF8.GetString(buffer,0,realLen); // 读取HTTP请求报文
                    SetLogText(content);// 将该请求报文记录到服务器日志中
                    // 将有用的报文信息转换成Request(请求)对象;
                    Request request=new Request(content);
                    // 分析请求报文,进行HTTP响应处理
                    RequestStaticOrDynamicPage(request.RawUrl,connSocket);
                }
                catch (Exception)
                { 
                    // 提示异常的发生,并跳出死循环
                    SetLogText("当前连接发生异常,请重启服务!");
                    // 一旦接收异常,关闭此次连接的Socket
                    connSocket.Close();
                   
                    break;
                }
            }
        }

        /// <summary>
        /// 判断请求的是动态页面还是静态页面,并分别针对,进行HTTP响应处理
        /// </summary>
        /// <param name="rawUrl"></param>
        /// <param name="connsocket"></param>
        private void RequestStaticOrDynamicPage(string rawUrl, Socket connsocket)
        {
            // 根据请求文件的后缀名进行判断
            string ext = Path.GetExtension(rawUrl);
            switch (ext)
            {
                case ".aspx":
                case ".asp":
                case ".php":
                case ".jsp":
                    // 动态页面的处理 (挖坑,读者自己来把这里补充完整)
                    break;
                default:
                    // 静态页面的处理
                    ProcessStaticPageRequest(rawUrl,connsocket);
                    break;                    
            }
        }

        /// <summary>
        /// 处理HTTP的静态页面请求
        /// </summary>
        /// <param name="rawUrl"></param>
        /// <param name="connsocket"></param>
        private void ProcessStaticPageRequest(string rawUrl,Socket connsocket)
        {
            // 拼接物理路径的字符串,检测当前物理路径的文件是否存在
            // 注意 Path.Combine()方法中,第二个开始以后的参数,开头的 / 要去掉,否则拼接出来的路径将从后面的
            // 以 / 的字符串开始进行拼接,也就是忽略掉, / 前面的拼接路径字符串
            rawUrl = rawUrl.TrimStart('/');
            string physicalPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"web",rawUrl);

            // 进行检测当前请求的文件是否存在
            if (File.Exists(physicalPath))
            {
               // 文件存在,读取到文件流中,拼接到HTTP响应对象——Response中的“响应报文”中的响应体中。
                using (FileStream fs=new FileStream(physicalPath,FileMode.Open))
                {
                    // 声明存储文件流的二进制字节数组
                    // 将文件流读取到声明好的二进制字节数组中去
                    byte[] buffer=new byte[fs.Length];
                    fs.Read(buffer, 0, buffer.Length);

                    // 准备发送响应报文
                    string ext = Path.GetExtension(rawUrl);
                    Response response=new Response(200,buffer,ext);
                    // 发送响应报文,关闭当前Socket连接,注意在这里体现了HTTP协议的无状态根本原因
                    connsocket.Send(response.GetResponse());
SetLogText(connsocket.RemoteEndPoint+":已关闭连接.");
                    connsocket.Close();
                    
                }
            }
            else
            {
                // 404 页面不存在处理
                // 埋坑,读者可以在这里设置一个专门提示的页面,提示用户当前访问资源不存在
            }
        }

        /// <summary>
        /// 设置日志文本框的记录方法
        /// </summary>
        /// <param name="msg"></param>

        private void SetLogText(string msg)
        {
            txtLog.AppendText(msg + "\r\n");
        }
    }
}

Request对象的代码:

//*************************************************************************
//
//File Name:            Request.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;

namespace WebServer
{
    class Request
    {
        #region 私有属性
        private string _rawUrl;
        private string _method;

        public string RawUrl
        {
            get { return _rawUrl; }
            set { _rawUrl = value; }
        }

        public string Method
        {
            get { return _method; }
            set { _method = value; }
        } 
        #endregion
        #region 构造函数-属性初始化器

        public Request(string content)
        {
            // 按行分解请求报文
            string[] lines = content.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            // 按空格分解请求报文中的第一行,并初始化该对象的两个属性
            this.Method = lines[0].Split(' ')[0];
            this.RawUrl = lines[0].Split(' ')[1];
        }

        #endregion
    }
}

Response对象的代码:

using System.Collections.Generic;
using System.Text;

//*************************************************************************
//
//File Name:            Response.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
namespace WebServer
{
    class Response
    {

        #region 私有字段、属性
        private int _codeStatus;
        private int _contentLength;
        private string _contentType;
        private byte[] _buffer;

        public int CodeStatus
        {
            get { return _codeStatus; }
            set { _codeStatus = value; }
        }

        public int ContentLength
        {
            get { return _contentLength; }
            set { _contentLength = value; }
        }

        public string ContentType
        {
            get { return _contentType; }
            set { _contentType = value; }
        }

        public byte[] Buffer
        {
            get { return _buffer; }
            set { _buffer = value; }
        }
        #endregion

        #region 构造函数——属性初始化器

        public Response(int codeStatus,byte[] buffer,string ext)
        {
            FillCodeStaDic();
            this.Buffer = buffer;
            this.CodeStatus = codeStatus;
            this.ContentLength = buffer.Length;
            GetContentType(ext);
        }
        Dictionary<int,string> codeStatusDic=new Dictionary<int, string>();

        /// <summary>
        /// 填充状态码 字典 
        /// </summary>
        private void FillCodeStaDic()
        {
            codeStatusDic[200] = "OK";
            codeStatusDic[404] = "请求页面不存在!";
            //...挖坑,读者可以在这里进行详细的补充
        }

        /// <summary>
        /// 根据请求文件的后缀名,确定响应体的类型
        /// </summary>
        /// <param name="ext"></param>
        void GetContentType(string ext)
        {
            switch (ext)
            {
                case ".css":
                    this.ContentType = "text/css";
                    break;
                case ".gif":
                    this.ContentType = "image/gif";
                    break;
                case ".ico":
                    this.ContentType = "image/x-icon";
                    break;
                case ".jpe":
                case ".jpeg":
                case ".jpg":
                    this.ContentType = "image/jpeg";
                    break;
                case "bmp":
                    this.ContentType = "image/bmp";
                    break;
                case ".js":
                    this.ContentType = "application/x-javascript";
                    break;
                case "stm":
                case ".htm":
                case ".html":
                    this.ContentType = "text/html";
                    break;
                // ...挖坑,读者可以在这里进行详细的补充
            }
        }

        /// <summary>
        /// 拼接响应报文
        /// </summary>
        public byte[] GetResponse()
        {
            // 拼接响应报文头
            StringBuilder sb=new StringBuilder();
            sb.Append("HTTP/1.0 "+this.CodeStatus+" "+codeStatusDic[this.CodeStatus]+"\r\n");
            sb.Append("Content-Type: "+this.ContentType+"\r\n");
            sb.Append("Content-Length: "+this.ContentLength+"\r\n");
            sb.Append("Server: ghhSever/1.0\r\n");
            sb.Append("X-Powered-By: MannyGuo\r\n");// 大家可以模拟下面的响应报文进行添加,注意格式必须要一致(末尾换行)
            sb.Append("\r\n");
            // 构建响应报文头
            byte[] header = Encoding.UTF8.GetBytes(sb.ToString());
            // 构建响应报文体
            byte[] content = this.Buffer;
            // 装载响应报文
            List<byte>bList=new List<byte>();
            bList.AddRange(header);
            bList.AddRange(content);

            return bList.ToArray();
        }

        #region 响应报文分析
        /*
        HTTP/1.0 200 OK
        Content-Type: text/html
        Content-Length: 337
        Connection: keep-alive
        Date: Sun, 09 Jun 2013 04:50:44 GMT
        Server: Apache
        X-Powered-By: PHP/5.2.5
        Content-Encoding: gzip
        Vary: Accept-Encoding
        Age: 37928
        Via: 1.0 fe91fd60a17845818d57d903e10536ce.cloudfront.net (CloudFront)
        X-Cache: Hit from cloudfront
        X-Amz-Cf-Id: WKYiDsukwM6go6_K9lF207F72tlhGB6Wv1wgRutHWslDdd_7MoUpdw==
        
        50 
         */
        
        #endregion
        #endregion
    }
}

9、演示效果:

为了演示效果,我们需要在程序的debug目录下新建一个Web文件夹,里面放一个测试用的1.html

  

 运行我们自己手写的Web服务器,启动服务。在浏览器地址中输入“IP地址:端口号/页面(或者资源)”,就可以看到效果了。

10、上一篇文章,短短3天内浏览量超过了1000。小郭在此感谢大家的支持!我会一如既往的为大家奉献更多的东西。

 

 

posted @ 2013-06-11 21:06  典语  阅读(6517)  评论(23编辑  收藏  举报