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






4.67/5 (44投票s)
2001年10月28日
5分钟阅读

512309

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();
编译和执行
从命令行编译程序:
在我使用的 .NET 版本中,我不需要指定任何库名,可能对于旧版本,我们需要使用 /r 参数添加对 dll 的引用。
要运行应用程序,只需键入应用程序名称并按 Enter 键。
现在,假设用户发送了请求。我们的 Web 服务器将识别默认文件名并将其发送给浏览器。
用户也可以请求图像文件。
可能的改进
WebServer 应用程序可以进行许多改进。目前它不支持嵌入式图像,也没有脚本支持。我们可以编写自己的 ISAPI 过滤器,或者为了学习目的,我们也可以使用 IIS ISAPI 过滤器。编写基本 ISAPI 过滤器的代码在 ISAPI Filters: Designing SiteSentry, an Anti-Scraping Filter for IIS 中得到了很好的解释。
结论
本文提供了编写 Web 服务器应用程序的非常基础的思路,还有很多可以改进的地方。如果我能收到任何关于改进它的评论,我将不胜感激。我也期待着为这个应用程序添加调用 Microsoft ISAPI Filter 的功能。