|
<p ><ccid_nobr><b>摘要</b></ccid_nobr><p >这篇文章讨论了如何使用C#开发一个简单的web服务器应用程序。尽管我们可以使用任何一种支持.NET的编程语言开发,但我选择了C#。本篇文章中的代码是使用微软的β2版的Visual C# Compiler Version 7.00.9254 [CLR version v1.0.2914]编译通过的,对代码作一些小的改动后,使用β1版也可能编译通过。该web服务器应用程序能够与IIS或其他任何web服务器软件同时在一台服务器上运行,只要为它指定一个空闲的端口即可。在本篇文章中,我还假定读者对.NET、C#或Visual Basic .Net有一定的了解。<p >该web服务器应用程序能够向浏览器返回HTML格式的文件,而且支持图像,它不加载嵌入式图像或支持任何一种脚本语言。为了简单起见,我将它开发成一个命令行应用程序。<p ><ccid_nobr><b>准备工作</b></ccid_nobr><p >首先,我们需要为这个web服务器应用程序定义一个根文件夹,例如,C:\MyPersonalwebServer,然后在该要根目录下创建一个数据目录,例如,C:\MyPersonalwebServer\Data;最后在数据目录下创建三个文件,例如:<p ><ccid_nobr><ul><li>Mimes.Dat</li><li>Vdirs.Dat</li><li>Default.Dat</li></ul></ccid_nobr><p >Mime.Dat中将包含该web服务器支持的MIME类型,其格式为<扩展名>; <MIME类型>,例如:<p ><ccid_nobr><ul><li>.html;text/html</li><li>.htm;text/html</li><li>.bmp;image/bmp</li></ul></ccid_nobr><p >VDirs.Dat中包含有虚拟目录的信息,格式为</虚拟目录 />; <物理目录>,例如:<p ><DRIVE:\PHYSICALDIR \>/; C:\myWebServerRoot/<p > test/; C:\myWebServerRoot\Imtiaz\ <p >Default.Dat中包含有虚拟目录中文件的信息,例如:<p ><ccid_nobr><ul><li>default.html</li><li>default.htm</li><li>Index.html</li><li>Index.htm</li></ul></ccid_nobr><p >为简单起见,我们将使用文本文件存储所有的信息,但我们也可以使用XML等其他的格式。在开始研究代码之前,我们先来看一下在登录网站时浏览器需要传递的头部信息。<p >我们以请求test.html为例进行说明。在浏览器的地址栏输入http://localhost:5050/test.html(记住,需要在URL中包括端口号),服务器将得到下面的信息:<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code"></DRIVE:\PHYSICALDIR> <br>GET /test.html HTTP/1.1<br>Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*<br>Accept-Language: en-usAccept-Encoding: gzip, deflate<br>User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0; .NET CLR 1.0.2914)<br>Host: localhost:5050Connection: Keep-Alive <br><br><ccid_nobr><b>开始编程</b></ccid_nobr><br><p >namespace Imtiaz <br><p >{<br><br> using System;<br><p > using System.IO;<br><p > using System.Net;<br><p > using System.Net.Sockets;<br><p > using System.Text;<br><p > using System.Threading ;<br><br><p > class MyWebServer <br><p > {<br><br><p > private TcpListener myListener ;<br><p > private int port = 5050 ; // 可以任意选择空闲的端口<br><br><p > //生成TcpListener的构建器开始监听给定的端口,它还启动调用StartListen()方法的一个线程<br><p > public MyWebServer()<br><p > {<br><p > try<br><p > {<br><p > //开始监听给定的端口<br><p > myListener = new TcpListener(port) ;<br><p > myListener.Start();<br><p > Console.WriteLine("Web Server Running... Press ^C to Stop...");<br><p ><br><p > //启动调用StartListen方法的线程<br><p > Thread th = new Thread(new ThreadStart(StartListen));<br><p > th.Start() ;<p ><br><br> }<br><p > catch(Exception e)<br><p > {<br><p > Console.WriteLine("An Exception Occurred while Listening :" +e.ToString());<br><p > }<br><p > } <p ></td><p ></tr><p ></table><p ></ccid_nobr><p >我们定义了名字空间,包括应用程序必需的引用,初始化了构建器中的端口,启动了端口监听进程,创建了一个新的线程调用startlisten函数。<p >我们假设用户没有在URL中提供文件名,在这种情况下我们必须自己确定缺省的文件名,并将它返回给浏览器,就象在IIS中的文档标签中定义缺省的文档那样。<p >我们已经在default.dat中存储了缺省的文件名,并将文件存储在了数据目录中。GetTheDefaultFileName函数将目录路径作为输入参数,打开default.dat文件,在目录中查找文件,根据是否找到了文件返回文件名或一个空格。<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code">public string GetTheDefaultFileName(string sLocalDirectory)<br>{<br>StreamReader sr;<br>String sLine = "";<br><br> try<br> {<br> //打开default.dat,获得缺省清单<br> sr = new StreamReader("data\\Default.Dat");<br><br> while ((sLine = sr.ReadLine()) != null)<br> {<br> //在web服务器的根目录下查找缺少文件<br> if (File.Exists( sLocalDirectory + sLine) == true)<br> break;<br> }<br> }<br> catch(Exception e)<br> {<br> Console.WriteLine("An Exception Occurred : " + e.ToString());<br> }<br> if (File.Exists( sLocalDirectory + sLine) == true)<br> return sLine;<br> else<br> return "";<br> } </td></tr></table></ccid_nobr><p >象在IIS中那样,我们必须将虚拟目录解析为物理目录。在Vdir.Dat中,我们已经存储了实际的物理目录和虚拟目录之间的映像关系。需要记住的是,在任何情况下,文件的格式都是重要的。<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code"> public string GetLocalPath(string sMyWebServerRoot, string sDirName)<br>{<br><br>treamReader sr;<br> String sLine = "";<br> String sVirtualDir = ""; <br> String sRealDir = "";<br> int iStartPos = 0;<br><br> //删除多余的空格<br> sDirName.Trim();<br><br> // 转换成小写<br> sMyWebServerRoot = sMyWebServerRoot.ToLower();<br><br> // 转换成小写<br> sDirName = sDirName.ToLower();<br><br> try<br> {<br> //打开Vdirs.dat文件,获得虚拟目录<br> sr = new StreamReader("data\\VDirs.Dat");<br><br> while ((sLine = sr.ReadLine()) != null)<br> {<br> //删除多余的空格<br> sLine.Trim();<br><br> if (sLine.Length > 0)<br> {<br> //找到分割符<br> iStartPos = sLine.IndexOf(";");<br><br>// 转换成小写<br> sLine = sLine.ToLower();<br><br> sVirtualDir = sLine.Substring(0,iStartPos);<br> sRealDir = sLine.Substring(iStartPos + 1);<br><br> if (sVirtualDir == sDirName)<br> {<br> break;<br> }<br> }<br> }<br> }<br> catch(Exception e)<br> {<br> Console.WriteLine("An Exception Occurred : " + e.ToString());<br> }<br><br> if (sVirtualDir == sDirName)<br> return sRealDir;<br> else<br> return "";<br> }<br><br> 我们还必须使用用户提供的文件扩展名确定Mime类型。<br> public string GetMimeType(string sRequestedFile)<br> {<br><br> StreamReader sr;<br> String sLine = "";<br> String sMimeType = "";<br> String sFileExt = "";<br> String sMimeExt = "";<br><br> // 转换成小写<br> sRequestedFile = sRequestedFile.ToLower();<br><br> int iStartPos = sRequestedFile.IndexOf(".");<br><br> sFileExt = sRequestedFile.Substring(iStartPos);<br><br> try<br> {<br> //打开Vdirs.dat文件,获得虚拟目录<br> sr = new StreamReader("data\\Mime.Dat");<br><br> while ((sLine = sr.ReadLine()) != null)<br> {<br><br> sLine.Trim();<br><br> if (sLine.Length > 0)<br> {<br> //找到分割符<br> iStartPos = sLine.IndexOf(";");<br><br> // 转换成小写<br> sLine = sLine.ToLower();<br><br>sMimeExt = sLine.Substring(0,iStartPos);<br> sMimeType = sLine.Substring(iStartPos + 1);<br><br>if (sMimeExt == sFileExt)<br> break;<br> }<br> }<br> }<br> catch (Exception e)<br> {<br> Console.WriteLine("An Exception Occurred : " + e.ToString());<br> }<br><br> if (sMimeExt == sFileExt)<br> return sMimeType; <br> else<br> return "";<br> } </td></tr></table></ccid_nobr><p >下面我们来编写建立和向浏览器(客户端)发送头部信息的函数。<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code">public void SendHeader( string sHttpVersion, <br>string sMIMEHeader, <br>int iTotBytes, <br>string sStatusCode,<br>ref Socket mySocket)<br>{<br><br> String sBuffer = "";<br><br> //如果用户没有提供Mime类型,则将其缺省地设置为text/html<br> if (sMIMEHeader.Length == 0 )<br> {<br> sMIMEHeader = "text/html"; // Default Mime Type is text/html<br> }<br><br> sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n";<br> sBuffer = sBuffer + "Server: cx1193719-b\r\n";<br> sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";<br> sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";<br> sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n";<br><br> Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer); <br><br> SendToBrowser( bSendData, ref mySocket);<br><br> Console.WriteLine("Total Bytes : " + iTotBytes.ToString());<br><br>}<br><br> SendToBrowser函数向浏览器发送信息,这是一个工作量比较大的函数。<br> public void SendToBrowser(String sData, ref Socket mySocket)<br> {<br> SendToBrowser (Encoding.ASCII.GetBytes(sData), ref mySocket);<br> }<br><br>public void SendToBrowser(Byte[] bSendData, ref Socket mySocket)<br> {<br> int numBytes = 0;<br> try<br> {<br> if (mySocket.Connected)<br> {<br> if (( numBytes = mySocket.Send(bSendData, bSendData.Length,0)) == -1)<br> Console.WriteLine("Socket Error cannot Send Packet");<br> else<br> {<br> Console.WriteLine("No. of bytes send {0}" , numBytes);<br> }<br> }<br> else<br> Console.WriteLine("Connection Dropped....");<br> }<br> catch (Exception e)<br> {<br> Console.WriteLine("Error Occurred : {0} ", e );<br> }<br> }<br> 我们已经有了编写一个互联网服务器应用程序的一些部件,下面我们将讨论互联网服务器应用程序中的关健函数。<br> public void StartListen()<br> {<br><br> int iStartPos = 0;<br> String sRequest;<br> String sDirName;<br> String sRequestedFile;<br> String sErrorMessage;<br> String sLocalDir;<br> String sMyWebServerRoot = "C:\\MyWebServerRoot\\";<br> String sPhysicalFilePath = "";<br> String sFormattedMessage = "";<br> String sResponse = "";<br><br> while(true)<br> {<br> //接受一个新的连接<br> Socket mySocket = myListener.AcceptSocket() ;<br><br> Console.WriteLine ("Socket Type " +mySocket.SocketType ); <br> if(mySocket.Connected)<br> {<br> Console.WriteLine("\nClient Connected!!\n==================\n<br> CLient IP {0}\n", mySocket.RemoteEndPoint) ;<br><br><br> //生成一个字节数组,从客户端接收数据<br> Byte[] bReceive = new Byte[1024] ;<br> int i = mySocket.Receive(bReceive,bReceive.Length,0) ;<br><br> //将字节型数据转换为字符串<br> string sBuffer = Encoding.ASCII.GetString(bReceive);<br><br> //上前我们将只处理GET类型<br> if (sBuffer.Substring(0,3) != "GET" )<br> {<br> Console.WriteLine("Only Get Method is supported..");<br> mySocket.Close();<br> return;<br> }<br><br> // 查找HTTP请求<br> iStartPos = sBuffer.IndexOf("HTTP",1);<br><br> // 获取“HTTP”文本和版本号,例如,它会返回“HTTP/1.1”<br> string sHttpVersion = sBuffer.Substring(iStartPos,8);<br><br> //解析请求的类型和目录/文件<br> sRequest = sBuffer.Substring(0,iStartPos - 1);<br><br> //如果存在\符号,则使用/替换<br> sRequest.Replace("\\","/");<br><br> //如果提供的文件名中没有/,表明这是一个目录,我们解危需要查找缺省的文件名<br> if ((sRequest.IndexOf(".") <1) && (!sRequest.EndsWith("/")))<br> {<br> sRequest = sRequest + "/"; <br> }<br> //解析请求的文件名<br> iStartPos = sRequest.LastIndexOf("/") + 1;<br> sRequestedFile = sRequest.Substring(iStartPos);<br><br> //解析目录名<br> sDirName = sRequest.Substring(sRequest.IndexOf("/"), sRequest.LastIndexOf("/")-3);<br><br>上面的代码无须多加解释,它接收用户的请求,将用户的请求由字节型数据转换为字符串型数据,然后查找请求的类型,解析HTTP的版本号、文件和目录信息。<br> // 确定物理目录<br> if ( sDirName == "/")<br> sLocalDir = sMyWebServerRoot;<br> else<br> {<br> //获得虚拟目录<br> sLocalDir = GetLocalPath(sMyWebServerRoot, sDirName);<br> }<br><br>Console.WriteLine("Directory Requested : " + sLocalDir);<br><br>//如果物理目录不存在,则显示出错信息<br> if (sLocalDir.Length == 0 )<br> {<br> sErrorMessage = "<H2>Error!! Requested Directory does not exists</H2><Br>";<br> //sErrorMessage = sErrorMessage + " lease check data\\Vdirs.Dat";<br><br> //对信息进行格式化<br> SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);<br><br> //向浏览器发送信息<br> SendToBrowser(sErrorMessage, ref mySocket);<br><br> mySocket.Close();<br><br> continue;<br> } </td></tr></table></ccid_nobr><p >提示:微软的IE浏览器一般情况下总会显示一个比较“友好”一点的HTTP错误网页,如果要显示我们的Web服务器应用程序的错误信息,需要禁用IE中“显示友好HTTP错误信息”的功能,方法是依次点击“工具”->“互联网工具”,然后在其中的“高级”标签中即可以看到该选项。<p >如果用户没有提供目录名,Web服务器应用程序会使用GetLocalPath函数获取物理目录的信息,如果目录不存在(或者没有映射为Vdir.Dat中的条目),就会向浏览器发送错误信息。接下来Web服务器应用程序会确定文件名,如果用户没有提供文件名,Web服务器应用程序可以调用GetTheDefaultFileName函数获取文件名,如果有错误发生,则会将错误信息发送到浏览器。<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code"> //如果文件名不存在,则查找缺省文件列表<br>if (sRequestedFile.Length == 0 )<br>{<br>// 获取缺省的文件名<br>sRequestedFile = GetTheDefaultFileName(sLocalDir);<br><br> if (sRequestedFile == "")<br> {<br> sErrorMessage = "<H2>Error!! No Default File Name Specified</H2>";<br> SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", <br> ref mySocket);<br> SendToBrowser ( sErrorMessage, ref mySocket);<br><br> mySocket.Close();<br><br> return;<br><br> }<br> }<br><br></td></tr></table></ccid_nobr><p >下面我们来识别Mime类型:<p ><ccid_nobr><table width="550" border="1" cellspacing="0" cellpadding="0" bordercolorlight = "black" bordercolordark = "#FFFFFF"><tr><td bgcolor="e6e6e6" class="code">String sMimeType = GetMimeType(sRequestedFile);<br><br>//构建物理路径<br> sPhysicalFilePath = sLocalDir + sRequestedFile;<br> Console.WriteLine("File Requested : " + sPhysicalFilePath);<br><br><br> 最后一个步骤是打开被请求的文件,并将它发送给浏览器。<br><br>if (File.Exists(sPhysicalFilePath) == false)<br> {<br><br> sErrorMessage = "<H2>404 Error! File Does Not Exists...</H2>";<br> SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);<br> SendToBrowser( sErrorMessage, ref mySocket);<br><br> Console.WriteLine(sFormattedMessage);<br> }<br><br>else<br> {<br> int iTotBytes=0;<br><br> sResponse ="";<br><br> FileStream fs = new FileStream(sPhysicalFilePath, FileMode.Open,FileAccess.Read,<br> FileShare.Read);<br> // 创建一个能够从FileStream中读取字节数据的reader<br><br> BinaryReader reader = new BinaryReader(fs);<br> byte[] bytes = new byte[fs.Length];<br> int read;<br> while((read = reader.Read(bytes, 0, bytes.Length)) != 0) <br> {<br> // 从文件中读取数据,并将数据发送到网络上<br> sResponse = sResponse + Encoding.ASCII.GetString(bytes,0,read);<br><br> iTotBytes = iTotBytes + read;<br><br> }<br> reader.Close(); <br> fs.Close();<br><br> SendHeader(sHttpVersion, sMimeType, iTotBytes, " 200 OK", ref mySocket);<br> SendToBrowser(bytes, ref mySocket);<br> //mySocket.Send(bytes, bytes.Length,0);<br><br>}<br> mySocket.Close(); <br><br>}<br> }<br> }<br> }<br> }<br><br></td></tr></table></ccid_nobr><p ><ccid_nobr><b>编译和执行</b></ccid_nobr><p >可以使用下图所示的命令编译我们的Web服务器应用程序:<p ><ccid_nobr><center><img src="http://www.hh010.com/upload_files/article/244/9_c92shq10817.gif"></center></ccid_nobr><p >在我使用的.NET开发工具中,无须指定任何库的名字,在较老版本的.NET开发工具中,可能会需要使用/r参数添加对dll库文件的引用。<p >要运行该Web服务器应用程序,只要如下图那样输入程序的名字,并按回车键即可。<p ><ccid_nobr><center><img src="http://www.hh010.com/upload_files/article/244/9_txvepw10818.gif"></center></ccid_nobr><p >Now, let say user send the request, our web server will identify the default file name and sends to the browser. <p >现在,我们假设用户发送了请求,我们的Web服务器应用程序将会决定使用缺省的文件,并将它返回给浏览器。如下图所示:<p ><ccid_nobr><center><img src="http://www.hh010.com/upload_files/article/244/9_fru5l710819.gif"></center></ccid_nobr><p >当然了,用户也可以请求图像文件<p ><ccid_nobr><center><img src="http://www.hh010.com/upload_files/article/244/9_ff4jtj10820.gif"></center></ccid_nobr><p ><ccid_nobr><b>可能的改进</b></ccid_nobr><p >WebServer仍然有许多地方可以加以改进。它不支持嵌入式图像和脚本,读者可以自己编写ISAPI过滤器,也可以使用IIS ISAPI过滤器。<p ><ccid_nobr><b>结束语</b></ccid_nobr><p >本篇文章展示了开发Web服务器的基本原理,我们仍然可以对文章中的Web服务器应用程序进行许多改进,希望它能够起到抛砖引玉的作用,对读者有所启迪。 <p align="center"></p></p> |
|