65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 创建自己的 Web 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (44投票s)

2001年10月28日

5分钟阅读

viewsIcon

512309

downloadIcon

11976

逐步介绍如何使用 C# 编写 Web 服务器。

摘要

本文解释了如何使用 C# 编写一个简单的 Web 服务器应用程序。虽然可以使用任何支持 .NET 的语言进行开发,但在此示例中我选择了 C#。代码使用 beta2 编译。Microsoft (R) Visual C# Compiler Version 7.00.9254 [CLR version v1.0.2914]。稍作修改即可与 Beta1 一起使用。此应用程序可以与 IIS 或任何 Web 服务器共存,关键是选择任何可用端口。我假设用户对 .NET 和 C# 或 VB.Net 有一些基本了解。此 Web 服务器仅返回 HTML 格式的文件,并且还支持图像。它不支持任何类型的脚本。为了简单起见,我开发了一个基于控制台的应用程序。

Web 服务器

首先,我们将为我们的 Web 服务器定义根文件夹。例如:C:\MyPersonalwebServer,并在其下创建一个 Data 目录,例如:C:\MyPersonalwebServer\Data。我们将在 data 目录中创建三个文件,即:

Mimes.Dat
Vdirs.Dat
Default.Dat

Mime.Dat 将包含我们 Web 服务器支持的 MIME 类型,语法:  <扩展名>; <MIME 类型>
例如:

.html; text/html
.htm; text/html
.gif; image/gif
.bmp; image/bmp

VDirs.Dat 将包含虚拟目录信息。 语法:<虚拟目录>; <物理目录>  
例如:

/; C:\myWebServerRoot/
/test/; C:\myWebServerRoot\Imtiaz\

注意:我们必须包含 Web 服务器使用的所有目录。例如,如果 HTML 页面包含对图像的引用,并且我们希望显示图像,那么我们也需要包含它。例如:
/images/; c:myWebServerRoot\Images\

Default.Dat 将包含虚拟目录信息;
例如:

default.html
default.htm
Index.html
Index.htm;

为了简单起见,我们将所有信息存储在纯文本文件中,我们可以使用 XML、注册表甚至硬编码。在继续编写代码之前,让我们先看看浏览器在请求我们的网站时会传递的标头信息。

假设我们请求 test.html。我们键入 https://:5050/test.html (请记住在 URL 中包含端口)。Web 服务器接收到的信息如下。

GET /test.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, 
        application/vnd.ms-powerpoint, application/vnd.ms-excel, 
        application/msword, */*
Accept-Language: en-usAccept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0; 
                         .NET CLR 1.0.2914)
Host: localhost:5050
Connection: Keep-Alive

让我们深入代码。

// MyWebServer Written by Imtiaz Alam
namespace Imtiaz 
{

    using System;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading ;

class MyWebServer 
{

    private TcpListener myListener ;
    private int port = 5050 ;  // Select any free port you wish 

     //The constructor which make the TcpListener start listening on th
     //given port. It also calls a Thread on the method StartListen(). 
    public MyWebServer()
    {
        try
        {
             //start listing on the given port
            myListener = new TcpListener(port) ;
            myListener.Start();
            Console.WriteLine("Web Server Running... Press ^C to Stop...");
            
             //start the thread which calls the method 'StartListen'
            Thread th = new Thread(new ThreadStart(StartListen));
            th.Start() ;

        }
        catch(Exception e)
        {
            Console.WriteLine("An Exception Occurred while Listening :" 
                               + e.ToString());
        }
    }

我们定义了一个命名空间,包含了应用程序所需的引用,并在构造函数中初始化了端口,启动了监听器,创建了一个新线程并调用了 `startlisten` 函数。

现在,假设用户没有提供文件名,在这种情况下,我们需要识别默认文件名并将其发送给浏览器。就像在 IIS 中一样,我们在“文档”选项卡下定义默认文档。

我们已经将默认文件名存储在 default.dat 中,并将其存储在 data 目录中。`GetTheDefaultFileName` 函数以目录路径为输入,打开 default.dat 文件,在提供的目录中查找文件,并根据情况返回文件名或空白。

public string GetTheDefaultFileName(string sLocalDirectory)
{
    StreamReader sr;
    String sLine = "";

    try
    {
        //Open the default.dat to find out the list
        // of default file
        sr = new StreamReader("data\\Default.Dat");

        while ((sLine = sr.ReadLine()) != null)
        {
            //Look for the default file in the web server root folder
            if (File.Exists( sLocalDirectory + sLine) == true)
                break;
        }
    }
    catch(Exception e)
    {
        Console.WriteLine("An Exception Occurred : " + e.ToString());
    }
    if (File.Exists( sLocalDirectory + sLine) == true)
        return sLine;
    else
        return "";
}

我们还需要将虚拟目录解析为实际的物理目录,就像我们在 IIS 中所做的那样。我们已经将实际目录和虚拟目录之间的映射存储在 Vdir.Dat 中。请记住,在所有情况下,文件格式都非常重要。

public string GetLocalPath(string sMyWebServerRoot, string sDirName)
{

    StreamReader sr;
    String sLine = "";
    String sVirtualDir = ""; 
    String sRealDir = "";
    int iStartPos = 0;


    //Remove extra spaces
    sDirName.Trim();



    // Convert to lowercase
    sMyWebServerRoot = sMyWebServerRoot.ToLower();

    // Convert to lowercase
    sDirName = sDirName.ToLower();

    
    try
    {
        //Open the Vdirs.dat to find out the list virtual directories
        sr = new StreamReader("data\\VDirs.Dat");

        while ((sLine = sr.ReadLine()) != null)
        {
            //Remove extra Spaces
            sLine.Trim();

            if (sLine.Length > 0)
            {
                //find the separator
                iStartPos = sLine.IndexOf(";");

                // Convert to lowercase
                sLine = sLine.ToLower();

                sVirtualDir = sLine.Substring(0,iStartPos);
                sRealDir = sLine.Substring(iStartPos + 1);

                if (sVirtualDir == sDirName)
                {
                    break;
                }
            }
        }
    }
    catch(Exception e)
    {
        Console.WriteLine("An Exception Occurred : " + e.ToString());
    }


    if (sVirtualDir == sDirName)
        return sRealDir;
    else
        return "";
}

我们还需要使用用户提供的文件扩展名来识别 MIME 类型。

public string GetMimeType(string sRequestedFile)
{


    StreamReader sr;
    String sLine = "";
    String sMimeType = "";
    String sFileExt = "";
    String sMimeExt = "";

    // Convert to lowercase
    sRequestedFile = sRequestedFile.ToLower();

    int iStartPos = sRequestedFile.IndexOf(".");

    sFileExt = sRequestedFile.Substring(iStartPos);

    try
    {
        //Open the Vdirs.dat to find out the list virtual directories
        sr = new StreamReader("data\\Mime.Dat");

        while ((sLine = sr.ReadLine()) != null)
        {

            sLine.Trim();

            if (sLine.Length > 0)
            {
                //find the separator
                iStartPos = sLine.IndexOf(";");

                // Convert to lower case
                sLine = sLine.ToLower();

                sMimeExt = sLine.Substring(0,iStartPos);
                sMimeType = sLine.Substring(iStartPos + 1);

                if (sMimeExt == sFileExt)
                    break;
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("An Exception Occurred : " + e.ToString());
    }

    if (sMimeExt == sFileExt)
        return sMimeType; 
    else
        return "";
}

现在我们将编写一个函数来构建并发送标头信息给浏览器(客户端)。

public void SendHeader(string sHttpVersion, string sMIMEHeader, 
            int iTotBytes, string sStatusCode, ref Socket mySocket)
{

    String sBuffer = "";
    
    // if Mime type is not provided set default to text/html
    if (sMIMEHeader.Length == 0 )
    {
        sMIMEHeader = "text/html";  // Default Mime Type is text/html
    }

    sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n";
    sBuffer = sBuffer + "Server: cx1193719-b\r\n";
    sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";
    sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";
    sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n";
    
    Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer); 

    SendToBrowser( bSendData, ref mySocket);

    Console.WriteLine("Total Bytes : " + iTotBytes.ToString());

}

`SendToBrowser` 函数将信息发送给浏览器。这是一个重载函数。

public void SendToBrowser(String sData, ref Socket mySocket)
{
    SendToBrowser (Encoding.ASCII.GetBytes(sData), ref mySocket);
}


public void SendToBrowser(Byte[] bSendData, ref Socket mySocket)
{
    int numBytes = 0;
    try
    {
        if (mySocket.Connected)
        {
            if (( numBytes = mySocket.Send(bSendData, 
                  bSendData.Length,0)) == -1)
                Console.WriteLine("Socket Error cannot Send Packet");
            else
            {
                Console.WriteLine("No. of bytes send {0}" , numBytes);
            }
        }
        else
            Console.WriteLine("Connection Dropped....");
    }
    catch (Exception  e)
    {
        Console.WriteLine("Error Occurred : {0} ", e );
    }
}

现在我们已经准备好所有构建块,现在我们将深入研究我们应用程序的关键函数。

public void StartListen()
{

    int iStartPos = 0;
    String sRequest;
    String sDirName;
    String sRequestedFile;
    String sErrorMessage;
    String sLocalDir;
    String sMyWebServerRoot = "C:\\MyWebServerRoot\\";
    String sPhysicalFilePath = "";
    String sFormattedMessage = "";
    String sResponse = "";


    while(true)
    {
        //Accept a new connection
        Socket mySocket = myListener.AcceptSocket() ;

        Console.WriteLine ("Socket Type " + mySocket.SocketType ); 
        if(mySocket.Connected)
        {
            Console.WriteLine("\nClient Connected!!\n==================\n
             CLient IP {0}\n", mySocket.RemoteEndPoint) ;


            //make a byte array and receive data from the client 
            Byte[] bReceive = new Byte[1024] ;
            int i = mySocket.Receive(bReceive,bReceive.Length,0) ;


            //Convert Byte to String
            string sBuffer = Encoding.ASCII.GetString(bReceive);


            //At present we will only deal with GET type
            if (sBuffer.Substring(0,3) != "GET" )
            {
                Console.WriteLine("Only Get Method is supported..");
                mySocket.Close();
                return;
            }


            // Look for HTTP request
            iStartPos = sBuffer.IndexOf("HTTP",1);


            // Get the HTTP text and version e.g. it will return "HTTP/1.1"
            string sHttpVersion = sBuffer.Substring(iStartPos,8);


            // Extract the Requested Type and Requested file/directory
            sRequest = sBuffer.Substring(0,iStartPos - 1);


            //Replace backslash with Forward Slash, if Any
            sRequest.Replace("\\","/");


            //If file name is not supplied add forward slash to indicate 
            //that it is a directory and then we will look for the 
            //default file name..
            if ((sRequest.IndexOf(".") <1) && (!sRequest.EndsWith("/")))
            {
                sRequest = sRequest + "/"; 
            }
            //Extract the requested file name
            iStartPos = sRequest.LastIndexOf("/") + 1;
            sRequestedFile = sRequest.Substring(iStartPos);


            //Extract The directory Name
            sDirName = sRequest.Substring(sRequest.IndexOf("/"), 
                       sRequest.LastIndexOf("/")-3);

代码是自解释的。它接收请求,将其从字节转换为字符串,然后查找请求类型,提取 HTTP 版本、文件和目录信息。

/////////////////////////////////////////////////////////////////////
// Identify the Physical Directory
/////////////////////////////////////////////////////////////////////
if ( sDirName == "/")
    sLocalDir = sMyWebServerRoot;
else
{
    //Get the Virtual Directory
    sLocalDir = GetLocalPath(sMyWebServerRoot, sDirName);
}


Console.WriteLine("Directory Requested : " +  sLocalDir);

//If the physical directory does not exists then
// dispaly the error message
if (sLocalDir.Length == 0 )
{
    sErrorMessage = "<H2>Error!! Requested Directory does not exists</H2><Br>";
    //sErrorMessage = sErrorMessage + "Please check data\\Vdirs.Dat";

    //Format The Message
    SendHeader(sHttpVersion,  "", sErrorMessage.Length, 
               " 404 Not Found", ref mySocket);

    //Send to the browser
    SendToBrowser(sErrorMessage, ref mySocket);

    mySocket.Close();

    continue;
}

注意:Microsoft Internet Explorer 通常会显示“友好的”HTTP 错误页面。如果您想显示我们的错误消息,则需要在“工具”->“Internet 选项”->“高级”选项卡下的“显示友好的 HTTP 错误消息”选项中禁用它。接下来,我们检查是否提供了目录名,我们调用 `GetLocalPath` 函数来获取物理目录信息。如果目录未找到(或未在 Vdir.Dat 中映射),则将错误消息发送给浏览器。接下来,我们将识别文件名。如果用户未提供文件名,我们将调用 `GetTheDefaultFileName` 函数来检索文件名。如果发生错误,它将被抛出给浏览器。

/////////////////////////////////////////////////////////////////////
// Identify the File Name
/////////////////////////////////////////////////////////////////////

//If The file name is not supplied then look in the default file list
if (sRequestedFile.Length == 0 )
{
    // Get the default filename
    sRequestedFile = GetTheDefaultFileName(sLocalDir);

    if (sRequestedFile == "")
    {
        sErrorMessage = "<H2>Error!! No Default File Name Specified</H2>";
        SendHeader(sHttpVersion,  "", sErrorMessage.Length, 
                   " 404 Not Found", ref mySocket);
        SendToBrowser ( sErrorMessage, ref mySocket);

        mySocket.Close();

        return;

    }
}

然后我们需要识别 MIME 类型。

//////////////////////////////////////////////////
// Get TheMime Type
//////////////////////////////////////////////////

String sMimeType = GetMimeType(sRequestedFile);


//Build the physical path
sPhysicalFilePath = sLocalDir + sRequestedFile;
Console.WriteLine("File Requested : " +  sPhysicalFilePath);

现在是打开请求文件并将其发送给浏览器的最后步骤。

if (File.Exists(sPhysicalFilePath) == false)
{

    sErrorMessage = "<H2>404 Error! File Does Not Exists...</H2>";
    SendHeader(sHttpVersion, "", sErrorMessage.Length, 
               " 404 Not Found", ref mySocket);
    SendToBrowser( sErrorMessage, ref mySocket);

    Console.WriteLine(sFormattedMessage);
}
else
{
    int iTotBytes=0;

    sResponse ="";

    FileStream fs = new FileStream(sPhysicalFilePath, 
                    FileMode.Open, FileAccess.Read,
      FileShare.Read);
    // Create a reader that can read bytes from the FileStream.

    
    BinaryReader reader = new BinaryReader(fs);
    byte[] bytes = new byte[fs.Length];
    int read;
    while((read = reader.Read(bytes, 0, bytes.Length)) != 0) 
    {
        // Read from the file and write the data to the network
        sResponse = sResponse + Encoding.ASCII.GetString(bytes,0,read);

        iTotBytes = iTotBytes + read;

    }
    reader.Close(); 
    fs.Close();

    SendHeader(sHttpVersion,  sMimeType, iTotBytes, " 200 OK", ref mySocket);
    SendToBrowser(bytes, ref mySocket);
    //mySocket.Send(bytes, bytes.Length,0);

}
mySocket.Close();    

编译和执行

从命令行编译程序: 

Compile.gif

在我使用的 .NET 版本中,我不需要指定任何库名,可能对于旧版本,我们需要使用 /r 参数添加对 dll 的引用。

要运行应用程序,只需键入应用程序名称并按 Enter 键。

RunApp.gif

现在,假设用户发送了请求。我们的 Web 服务器将识别默认文件名并将其发送给浏览器。 

HtmlOutput.gif

用户也可以请求图像文件。 

ImgOutput.gif

可能的改进

WebServer 应用程序可以进行许多改进。目前它不支持嵌入式图像,也没有脚本支持。我们可以编写自己的 ISAPI 过滤器,或者为了学习目的,我们也可以使用 IIS ISAPI 过滤器。编写基本 ISAPI 过滤器的代码在 ISAPI Filters: Designing SiteSentry, an Anti-Scraping Filter for IIS 中得到了很好的解释。

结论

本文提供了编写 Web 服务器应用程序的非常基础的思路,还有很多可以改进的地方。如果我能收到任何关于改进它的评论,我将不胜感激。我也期待着为这个应用程序添加调用 Microsoft ISAPI Filter 的功能。

© . All rights reserved.