设为首页收藏本站language 语言切换
查看: 2024|回复: 0
收起左侧

使用C#开发自己的web服务器

[复制链接]
发表于 2010-2-25 10:21:55 | 显示全部楼层 |阅读模式
<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">&lt;/DRIVE:\PHYSICALDIR&gt; <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(&quot;Web Server Running... Press ^C to Stop...&quot;);<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(&quot;An Exception Occurred while Listening :&quot; +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 = &quot;&quot;;<br><br> try<br>  {<br>  //打开default.dat,获得缺省清单<br>  sr = new StreamReader(&quot;data\\Default.Dat&quot;);<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(&quot;An Exception Occurred : &quot; + e.ToString());<br>  }<br>  if (File.Exists( sLocalDirectory + sLine) == true)<br>  return sLine;<br>  else<br>  return &quot;&quot;;<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 = &quot;&quot;;<br>  String sVirtualDir = &quot;&quot;; <br>  String sRealDir = &quot;&quot;;<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(&quot;data\\VDirs.Dat&quot;);<br><br> while ((sLine = sr.ReadLine()) != null)<br>  {<br>  //删除多余的空格<br>  sLine.Trim();<br><br> if (sLine.Length &gt; 0)<br>  {<br>  //找到分割符<br>  iStartPos = sLine.IndexOf(&quot;;&quot;);<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(&quot;An Exception Occurred : &quot; + e.ToString());<br>  }<br><br>  if (sVirtualDir == sDirName)<br>  return sRealDir;<br>  else<br>  return &quot;&quot;;<br>  }<br><br> 我们还必须使用用户提供的文件扩展名确定Mime类型。<br>  public string GetMimeType(string sRequestedFile)<br>  {<br><br>  StreamReader sr;<br>  String sLine = &quot;&quot;;<br>  String sMimeType = &quot;&quot;;<br>  String sFileExt = &quot;&quot;;<br>  String sMimeExt = &quot;&quot;;<br><br>  // 转换成小写<br>  sRequestedFile = sRequestedFile.ToLower();<br><br>  int iStartPos = sRequestedFile.IndexOf(&quot;.&quot;);<br><br>  sFileExt = sRequestedFile.Substring(iStartPos);<br><br>  try<br>  {<br>  //打开Vdirs.dat文件,获得虚拟目录<br>  sr = new StreamReader(&quot;data\\Mime.Dat&quot;);<br><br>  while ((sLine = sr.ReadLine()) != null)<br>  {<br><br>  sLine.Trim();<br><br>  if (sLine.Length &gt; 0)<br>  {<br>  //找到分割符<br>  iStartPos = sLine.IndexOf(&quot;;&quot;);<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(&quot;An Exception Occurred : &quot; + e.ToString());<br>  }<br><br>  if (sMimeExt == sFileExt)<br>  return sMimeType; <br>  else<br>  return &quot;&quot;;<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 = &quot;&quot;;<br><br>  //如果用户没有提供Mime类型,则将其缺省地设置为text/html<br>  if (sMIMEHeader.Length == 0 )<br>  {<br>  sMIMEHeader = &quot;text/html&quot;; // Default Mime Type is text/html<br>  }<br><br> sBuffer = sBuffer + sHttpVersion + sStatusCode + &quot;\r\n&quot;;<br>  sBuffer = sBuffer + &quot;Server: cx1193719-b\r\n&quot;;<br>  sBuffer = sBuffer + &quot;Content-Type: &quot; + sMIMEHeader + &quot;\r\n&quot;;<br>  sBuffer = sBuffer + &quot;Accept-Ranges: bytes\r\n&quot;;<br>  sBuffer = sBuffer + &quot;Content-Length: &quot; + iTotBytes + &quot;\r\n\r\n&quot;;<br><br>  Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer); <br><br> SendToBrowser( bSendData, ref mySocket);<br><br> Console.WriteLine(&quot;Total Bytes : &quot; + 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(&quot;Socket Error cannot Send Packet&quot;);<br>  else<br>  {<br>  Console.WriteLine(&quot;No. of bytes send {0}&quot; , numBytes);<br>  }<br>  }<br>  else<br>  Console.WriteLine(&quot;Connection Dropped....&quot;);<br>  }<br>  catch (Exception e)<br>  {<br>  Console.WriteLine(&quot;Error Occurred : {0} &quot;, 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 = &quot;C:\\MyWebServerRoot\\&quot;;<br>  String sPhysicalFilePath = &quot;&quot;;<br>  String sFormattedMessage = &quot;&quot;;<br>  String sResponse = &quot;&quot;;<br><br> while(true)<br>  {<br>  //接受一个新的连接<br>  Socket mySocket = myListener.AcceptSocket() ;<br><br> Console.WriteLine (&quot;Socket Type &quot; +mySocket.SocketType ); <br>  if(mySocket.Connected)<br>  {<br>  Console.WriteLine(&quot;\nClient Connected!!\n==================\n<br>  CLient IP {0}\n&quot;, 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) != &quot;GET&quot; )<br>  {<br>  Console.WriteLine(&quot;Only Get Method is supported..&quot;);<br>  mySocket.Close();<br>  return;<br>  }<br><br> // 查找HTTP请求<br>  iStartPos = sBuffer.IndexOf(&quot;HTTP&quot;,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(&quot;\\&quot;,&quot;/&quot;);<br><br> //如果提供的文件名中没有/,表明这是一个目录,我们解危需要查找缺省的文件名<br>  if ((sRequest.IndexOf(&quot;.&quot;) &lt;1) &amp;&amp; (!sRequest.EndsWith(&quot;/&quot;)))<br>  {<br>  sRequest = sRequest + &quot;/&quot;; <br>  }<br>  //解析请求的文件名<br>  iStartPos = sRequest.LastIndexOf(&quot;/&quot;) + 1;<br>  sRequestedFile = sRequest.Substring(iStartPos);<br><br> //解析目录名<br>  sDirName = sRequest.Substring(sRequest.IndexOf(&quot;/&quot;), sRequest.LastIndexOf(&quot;/&quot;)-3);<br><br>上面的代码无须多加解释,它接收用户的请求,将用户的请求由字节型数据转换为字符串型数据,然后查找请求的类型,解析HTTP的版本号、文件和目录信息。<br>  // 确定物理目录<br>  if ( sDirName == &quot;/&quot;)<br>  sLocalDir = sMyWebServerRoot;<br>  else<br>  {<br>  //获得虚拟目录<br>  sLocalDir = GetLocalPath(sMyWebServerRoot, sDirName);<br>  }<br><br>Console.WriteLine(&quot;Directory Requested : &quot; + sLocalDir);<br><br>//如果物理目录不存在,则显示出错信息<br>  if (sLocalDir.Length == 0 )<br>  {<br>  sErrorMessage = &quot;&lt;H2&gt;Error!! Requested Directory does not exists&lt;/H2&gt;&lt;Br&gt;&quot;;<br>  //sErrorMessage = sErrorMessage + &quotlease check data\\Vdirs.Dat&quot;;<br><br> //对信息进行格式化<br>  SendHeader(sHttpVersion, &quot;&quot;, sErrorMessage.Length, &quot; 404 Not   Found&quot;, 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 == &quot;&quot;)<br>  {<br>  sErrorMessage = &quot;&lt;H2&gt;Error!! No Default File Name Specified&lt;/H2&gt;&quot;;<br>  SendHeader(sHttpVersion, &quot;&quot;, sErrorMessage.Length, &quot; 404 Not   Found&quot;, <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(&quot;File Requested : &quot; + sPhysicalFilePath);<br><br><br>  最后一个步骤是打开被请求的文件,并将它发送给浏览器。<br><br>if (File.Exists(sPhysicalFilePath) == false)<br>  {<br><br> sErrorMessage = &quot;&lt;H2&gt;404 Error! File Does Not Exists...&lt;/H2&gt;&quot;;<br>  SendHeader(sHttpVersion, &quot;&quot;, sErrorMessage.Length, &quot; 404 Not   Found&quot;, ref mySocket);<br>  SendToBrowser( sErrorMessage, ref mySocket);<br><br> Console.WriteLine(sFormattedMessage);<br>  }<br><br>else<br>  {<br>  int iTotBytes=0;<br><br> sResponse =&quot;&quot;;<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, &quot; 200 OK&quot;, 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>
您需要登录后才可以回帖 登录 | 论坛注册

本版积分规则

QQ|Archiver|手机版|小黑屋|sitemap|鸿鹄论坛 ( 京ICP备14027439号 )  

GMT+8, 2025-4-10 15:49 , Processed in 0.059068 second(s), 24 queries , Redis On.  

  Powered by Discuz!

  © 2001-2025 HH010.COM

快速回复 返回顶部 返回列表