[C# 网络编程系列]专题三:自定义Web服务器

前言:

经过前面的专题中对网络层协议和HTTP协议的简单介绍相信大家对网络中的协议有了大致的了解的, 本专题将针对HTTP协议定义一个Web服务器,我们平常浏览网页通过在浏览器中输入一个网址就可以看到我们想要的网页,这个过程中浏览器只是一个客户端,浏览器(应用层应用程序)通过HTTP协议把用户请求发送到服务端, 服务器接受到发送来的HTTP请求,然后对请求进行处理和响应,最后把响应的内容发送给客户端(浏览器这里充当了用户代理的客户端),浏览器再对接受到的响应内容(一般是HTML文件)进行解释并且显示出来。这就是一次完整的用户请求/响应模型,本专题所讲述的是一个简单的Web服务器,其他一些大型的Web服务器(IIS,Apache)也是这样的一个原理, 本专题只是简单讲述Web服务器的实现原理。

 

一、Socket编程实现一个简单的Web服务器

Socket这个概念是在Unix系统中提出来的。在Unix的时代,为了解决传输层的编程问题,Unix提供了类似于文件操作的网络操作方式——Socket,通过Socket,我们就可以像操作文件一样通过打开、写入、读取、关闭等操作完成网络编程,这样就使得网络编程可以统一到文件操作方面,这样就使我们更容易地编写网络应用程序。需要注意的是,应用层的协议需要网络程序专门处理,Socket不负责应用层协议,仅仅负责传输层的协议。

现在介绍下网络端口号(port)的概念,在同一个网络地址中,为了区分使用相同协议的不同应用程序,为不同的应用程序分配一个数字编号,我们把这个编号就成为网络端口号(就是区分同一个网络地址中不同的进程)。端口号是由一个两个字节的整数,所以取值范围为0~65535,这些端口号又分为三类:

  1. 第一类的范围是0~1023,称为众所周知的端口,这些端口号由特定的网络程序使用,例如,TCP协议使用80端口来完成Http协议的传输。
  2. 第二类的范围是1024~49151,称为登记端口,一般情况下不应该在程序中使用。
  3. 第三类的范围是49152~65535,称为私有端口, 这些端口可以由普通用户程序使用。

在我们用Socket开发网络应用程序中,还有一个就是端点的概念,在网络中,通过IP地址,协议和端口号可以唯一地确定网络上的一个应用程序,其中把IP地址和端口的组合叫做端点(EndPoint)。每个Socket需要绑定到一个端点上与其他端点进行通信。

介绍完基本的一些概念后,下面演示通过Socket编程实现一个简单的Web服务器,此实例中就是简单向浏览器返回一个固定的静态页面,实现代码如下:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace WebServer
{
    /// <summary>
    /// 实现一个简单的Web服务器
    /// 该服务器向请求的浏览器返回一个静态的HTML页面
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            // 获得本机的Ip地址,即127.0.0.1
            IPAddress localaddress =IPAddress.Loopback;
            
            // 创建可以访问的断点,49155表示端口号,如果这里设置为0,表示使用一个由系统分配的空闲的端口号
            IPEndPoint endpoint = new IPEndPoint(localaddress,49155);

            // 创建Socket对象,使用IPv4地址,数据通信类型为数据流,传输控制协议TCP协议.
            Socket socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
            
            //将Socket绑定到断点上
            socket.Bind(endpoint);
            // 设置连接队列的长度
            socket.Listen(10);

            while (true)
            {
                Console.WriteLine("Wait an connect Request...");
                // 开始监听,这个方法会堵塞线程的执行,直到接受到一个客户端的连接请求
                Socket clientsocket =socket.Accept();
                
                // 输出客户端的地址
                Console.WriteLine("Client Address is: {0}", clientsocket.RemoteEndPoint);
                // 把客户端的请求数据读入保存到一个数组中
                byte[] buffer =new byte[2048];

                int receivelength = clientsocket.Receive(buffer, 2048, SocketFlags.None);
                string requeststring = Encoding.UTF8.GetString(buffer, 0, receivelength);
                
                // 在服务器端输出请求的消息
                Console.WriteLine(requeststring);

                // 服务器端做出相应内容
                // 响应的状态行
                string statusLine ="HTTP/1.1 200 OK\r\n";
                byte[] responseStatusLineBytes = Encoding.UTF8.GetBytes(statusLine);
                string responseBody = "<html><head><title>Default Page</title></head><body><p style='font:bold;font-size:24pt'>Welcome you</p></body></html>";
                string responseHeader = 
                    string.Format(
                        "Content-Type: text/html; charset=UTf-8\r\nContent-Length: {0}\r\n",responseBody.Length);

                byte[] responseHeaderBytes = Encoding.UTF8.GetBytes(responseHeader);
                byte[] responseBodyBytes = Encoding.UTF8.GetBytes(responseBody);

                // 向客户端发送状态行
                clientsocket.Send(responseStatusLineBytes);

                // 向客户端发送回应头信息
                clientsocket.Send(responseHeaderBytes);

                // 发送头部和内容的空行
                clientsocket.Send(new byte[] { 13, 10 });

                // 想客户端发送主体部分
                clientsocket.Send(responseBodyBytes);

                // 断开连接
                clientsocket.Close();
                Console.ReadKey();
                break;                                         
            }
            
            // 关闭服务器
            socket.Close();

        }
    }
}

运行结果:

首先运行服务端后的界面:

在浏览器中输入http://localhost:49155/ 则浏览器可以看到如下的所示的结果:

此时在服务器端显示的输出为:

这里只是简单实现了一个web服务器的功能,当然实际的Web服务器通过用户的发来的Http请求中获得请求文件类型,请求文件名以及请求目录等信息,然后Web服务器根据这些请求信息从服务器的物理目录中寻找请求的文件,如果在服务器中找到请求的文件,然后服务器把响应内容发送给客户端。这里只是通过这个简单的Web服务器让大家理解请求/响应模型以及Web服务器的工作原理,一些复杂的Web服务器也是在此基础进行一些其他功能的扩展。

二、基于TcpListener的Web服务器

在.net平台下, 为了简化网络编程,.net对套接字又进行了一次封装,封装后的类是在System.Net.Sockets命名空间下的TcpListener类和TcpClient类,使用TcpListener类用来监听和接收传入的连接请求,在该类的构造函数中只需要传递一组网络端点信息就可以准备好监听参数,而不需要设置使用的网络协议等细节,调用Start方法后,监听工作就开始(间接调用了Socket.Listen方法),AcceptTcpClient方法将阻塞进程,直到一个客户端发来连接请求为止,这个方法返回一个

封装了Socket的TcpClient对象,同时从传入的连接队列中删除该客户端的连接请求。此时通过这个TcpClient对象与客户端进行通信。

下面是基于TcpListener和TcpClient的一个简单的Web服务器的代码:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace TcpWebserver
{
    class Program
    {
        static void Main(string[] args)
        {
            // 获得本机的Ip地址,即127.0.0.1
            IPAddress localaddress =IPAddress.Loopback;
            
            // 创建可以访问的断点,49155表示端口号,如果这里设置为0,表示使用一个由系统分配的空闲的端口号
            IPEndPoint endpoint = new IPEndPoint(localaddress, 49155);

            // 创建Tcp 监听器
            TcpListener tcpListener = new TcpListener(endpoint);
            
            // 启动监听
            tcpListener.Start();
            Console.WriteLine("Wait an connect Request...");
            while (true)
            {
                // 等待客户连接
                TcpClient client =tcpListener.AcceptTcpClient();
                if (client.Connected == true)
                {
                    // 输出已经建立连接
                    Console.WriteLine("Created connection");
                }

                // 获得一个网络流对象
                // 该网络流对象封装了Socket的输入和输出操作
                // 此时通过对网络流对象进行写入来返回响应消息
                // 通过对网络流对象进行读取来获得请求消息
                NetworkStream netstream = client.GetStream();
                // 把客户端的请求数据读入保存到一个数组中
                byte[] buffer = new byte[2048];

                int receivelength = netstream.Read(buffer, 0, 2048);
                string requeststring = Encoding.UTF8.GetString(buffer, 0, receivelength);

                // 在服务器端输出请求的消息
                Console.WriteLine(requeststring);

                // 服务器端做出相应内容
                // 响应的状态行
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] responseStatusLineBytes = Encoding.UTF8.GetBytes(statusLine);
                string responseBody = "<html><head><title>Default Page</title></head><body><p style='font:bold;font-size:24pt'>Welcome you</p></body></html>";
                string responseHeader =
                    string.Format(
                        "Content-Type: text/html; charset=UTf-8\r\nContent-Length: {0}\r\n", responseBody.Length);

                byte[] responseHeaderBytes = Encoding.UTF8.GetBytes(responseHeader);
                byte[] responseBodyBytes = Encoding.UTF8.GetBytes(responseBody);
                
                // 写入状态行信息
                netstream.Write(responseStatusLineBytes, 0, responseStatusLineBytes.Length);
                // 写入回应的头部
                netstream.Write(responseHeaderBytes, 0, responseHeaderBytes.Length);
                // 写入回应头部和内容之间的空行
                netstream.Write(new byte[] { 13, 10 }, 0, 2);

                // 写入回应的内容
                netstream.Write(responseBodyBytes, 0, responseBodyBytes.Length);

                // 关闭与客户端的连接
                client.Close();
                Console.ReadKey();
                break;
            }
            
            // 关闭服务器
            tcpListener.Stop();
        }
    }
}

程序的输出结果和前面的用Socket实现的效果相同,这里就不再贴图了,这里实现的Web服务器都是建立控制台的应用程序来实现的,感兴趣的朋友也可以用Windows窗体进行实现,同时这里也只是简单列出了采用同步的方式进行实现的,同时TcpListener类和TcpClient类同时支持异步操作的方法,下面列出这个两个类中异步操作的方法如下表:

方法

说明

TcpListener

BeginAcceptTcpClient

开始一个异步操作接受一个传入的连接

EndAcceptTcpClient

异步接受传入的连接,并创建新的TcpClient对象来处理客户端的通信

TcpClient

BeginConnect

开始一个对远程主机连接的异步请求

EndConnect

异步接受传入的连接尝试。

如果想了解线程同步和异步的朋友可以参考我的多线程处理系列:http://www.cnblogs.com/zhili/archive/2012/07/21/ThreadsSynchronous.html 

 

三、总结

 到这里这篇文章就差不多介绍到这里了,本专题是介绍如何自定义一个简单Web服务器,通过这个专题希望大家可以对Web服务器的工作过程有一个简单的了解。

另外在这个专题里面我们是用IE浏览器进行发送客户请求的,所以后面专题将介绍自定义一个浏览器,通过我们自定义的浏览器来对Web服务器发送请求,然后在自己自定义的浏览器中把响应消息显示出来。

 

 

posted @ 2012-09-25 16:59  沙耶  阅读(568)  评论(0编辑  收藏  举报