基于C#动手实现网络服务器Web Server

前言

最近在学习网络原理,突然萌发出自己实现一个网络服务器的想法,并且由于第三代小白机器人的开发需要,我把之前使用python、PHP写的那部分代码都迁移到了C#(别问我为什么这么喜欢C#),之前使用PHP就是用来处理网络请求的,现在迁移到C#了,而Linux系统上并没有IIS服务器,自然不能使用ASP.Net,所以这个时候自己实现一个功能简单的网络服务器就恰到好处地解决这些问题了。

基本原理

Web Server在一个B/S架构系统中起到的作用不仅多而且相当重要,Web开发者大部分时候并不需要了解它的详细工作机制。虽然不同的Web Server可能功能并不完全一样,但是以下三个功能几乎是所有Web Server必须具备的:

接收来自浏览器端的HTTP请求
将请求转发给指定Web站点程序(后者由Web开发者编写,负责处理请求)
向浏览器发送请求处理结果

下图显示Web Server在整个Web架构系统中所处的重要位置:

如上图,Web Server起到了一个“承上启下”的作用(虽然并没有“上下”之分),它负责连接用户和Web站点。

每个网站就像一个个“插件”,只要网站开发过程中遵循了Web Server提出的规则,那么该网站就可以“插”在Web Server上,我们便可以通过浏览器访问网站。

太长不看版原理

浏览器想要拿到哪个文件(html、css、js、image)就和服务器发请求信息说我要这个文件,然后服务器检查请求合不合法,如果合法就把文件数据传回给浏览器,这样浏览器就可以把网站显示出来了。(一个网站一般会包含n多个文件)

话不多说,直接上代码

在C#中有两种方法可以简单实现Web服务器,分别是直接使用Socket和使用封装好的HttpListener。

因为后者比较方便一些,所以我选择使用后者。

这是最简单的实现一个网络服务器,可以处理浏览器发过来的请求,然后将指定的字符串内容返回。

class Program
{
  static void Main(string[] args)
  {
    string port = "8080";
    HttpListener httpListener = new HttpListener();
    httpListener.Prefixes.Add(string.Format("http://+:{0}/", port));
    httpListener.Start();
    httpListener.BeginGetContext(new AsyncCallback(GetContext), httpListener); //开始异步接收request请求
    Console.WriteLine("监听端口:" + port);
    Console.Read();
  }

  static void GetContext(IAsyncResult ar)
  {
    HttpListener httpListener = ar.AsyncState as HttpListener;
    HttpListenerContext context = httpListener.EndGetContext(ar); //接收到的请求context(一个环境封装体)

    httpListener.BeginGetContext(new AsyncCallback(GetContext), httpListener); //开始 第二次 异步接收request请求

    HttpListenerRequest request = context.Request; //接收的request数据
    HttpListenerResponse response = context.Response; //用来向客户端发送回复

    response.ContentType = "html";
    response.ContentEncoding = Encoding.UTF8;

    using (Stream output = response.OutputStream) //发送回复
    {
      byte[] buffer = Encoding.UTF8.GetBytes("要返回的内容");
      output.Write(buffer, 0, buffer.Length);
    }
  }
}

这个简单的代码已经可以实现用于小白机器人的网络请求处理了,因为大致只用到GET和POST两种HTTP方法,只需要在GetContext方法里判断GET、POST方法,然后分别给出响应就可以了。

但是我们的目的是开发一个真正的网络服务器,当然不能只满足于这样一个专用的服务器,我们要的是可以提供网页服务的服务器。

那就继续吧。

根据我的研究,提供网页访问服务的服务器做起来确实有一点麻烦,因为需要处理的东西很多。需要根据浏览器请求的不同文件给出不同响应,处理Cookies,还要处理编码,还有各种出错的处理。

首先我们要确定一下我们的服务器要提供哪些文件的访问服务。

这里我用一个字典结构来保存。

/// <summary>
/// MIME类型
/// </summary>
public Dictionary<string, string> MIME_Type = new Dictionary<string, string>()
{
  { "htm", "text/html" },
  { "html", "text/html" },
  { "php", "text/html" },
  { "xml", "text/xml" },
  { "json", "application/json" },
  { "txt", "text/plain" },
  { "js", "application/x-javascript" },
  { "css", "text/css" },
  { "bmp", "image/bmp" },
  { "ico", "image/ico" },
  { "png", "image/png" },
  { "gif", "image/gif" },
  { "jpg", "image/jpeg" },
  { "jpeg", "image/jpeg" },
  { "webp", "image/webp" },
  { "zip", "application/zip"},
  { "*", "*/*" }
};

剧透一下:其中有PHP类型是我们后面要使用CGI接入的方式使我们的服务器支持PHP。

我在QFramework中封装了一个QHttpWebServer模块,这是其中的启动代码。

/// <summary>
/// 启动本地网页服务器
/// </summary>
/// <param name="webroot">网站根目录</param>
/// <returns></returns>
public bool Start(string webroot)
{
  //触发事件
  if (OnServerStart != null)
  OnServerStart(httpListener);

  WebRoot = webroot;
  try
  {
    //监听端口
    httpListener.Prefixes.Add("http://+:" + port.ToString() + "/");
    httpListener.Start();
    httpListener.BeginGetContext(new AsyncCallback(onWebResponse), httpListener); //开始异步接收request请求
  }
  catch (Exception ex)
  {
    Qdb.Error(ex.Message, QDebugErrorType.Error, "Start");
    return false;
  }
  return true;
}

现在把网页服务器的核心处理代码贴出来。

这个代码只是做了基本的处理,对于网站的主页只做了html后缀的识别。

后来我在QFramework中封装的模块做了更多的细节处理。

/// <summary>
/// 网页服务器相应处理
/// </summary>
/// <param name="ar"></param>
private void onWebResponse(IAsyncResult ar)
{
  byte[] responseByte = null;  //响应数据

  HttpListener httpListener = ar.AsyncState as HttpListener;
  HttpListenerContext context = httpListener.EndGetContext(ar); //接收到的请求context(一个环境封装体)      

  httpListener.BeginGetContext(new AsyncCallback(onWebResponse), httpListener); //开始 第二次 异步接收request请求

  //触发事件
  if (OnGetRawContext != null)
    OnGetRawContext(context);

  HttpListenerRequest request = context.Request; //接收的request数据
  HttpListenerResponse response = context.Response; //用来向客户端发送回复

  //触发事件
  if (OnGetRequest != null)
    OnGetRequest(request, response);

  if (rawUrl == "" || rawUrl == "/") //单纯输入域名或主机IP地址
    fileName = WebRoot + @"\index.html";
  else if (rawUrl.IndexOf('.') == -1) //不带扩展名,理解为文件夹
    fileName = WebRoot + @"\" + rawUrl.SubString(1) + @"\index.html";
  else
  {
    int fileNameEnd = rawUrl.IndexOf('?');
    if (fileNameEnd > -1)
      fileName = rawUrl.Substring(1, fileNameEnd - 1);
    fileName = WebRoot + @"\" + rawUrl.Substring(1);
  }

  //处理请求文件名的后缀
  string fileExt = Path.GetExtension(fileName).Substring(1);

  if (!File.Exists(fileName))
  {
    responseByte = Encoding.UTF8.GetBytes("404 Not Found!");
    response.StatusCode = (int)HttpStatusCode.NotFound;
  }
  else
  {
    try
    {
      responseByte = File.ReadAllBytes(fileName);
      response.StatusCode = (int)HttpStatusCode.OK;
    }
    catch (Exception ex)
    {
      Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse");
      response.StatusCode = (int)HttpStatusCode.InternalServerError;
    }
  }

  if (MIME_Type.ContainsKey(fileExt))
    response.ContentType = MIME_Type[fileExt];
  else
    response.ContentType = MIME_Type["*"];

  response.Cookies = request.Cookies; //处理Cookies

  response.ContentEncoding = Encoding.UTF8;

  using (Stream output = response.OutputStream) //发送回复
  {
    try
    {
      output.Write(responseByte, 0, responseByte.Length);
    }
    catch (Exception ex)
    {
      Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse");
      response.StatusCode = (int)HttpStatusCode.InternalServerError;
    }
  }
}

这样就可以提供基本的网页访问了,经过测试,使用Bootstrap,Pure等前端框架的网页都可以完美访问,性能方面一般般。(在QFramework的封装中我做了一点性能优化,有一点提升)我觉得要在性能方面做提升还是要在多线程处理这方面做优化,由于篇幅关系,就不把多线程版本的代码贴出来了。

接下来我们还要实现服务器的PHP支持。

首先定义两个字段。

/// <summary>
/// 是否开启PHP功能
/// </summary>
public bool PHP_CGI_Enabled = true;

/// <summary>
/// PHP执行文件路径
/// </summary>
public string PHP_CGI_Path = "php-cgi";
接下来在网页服务的核心代码里做PHP支持的处理。

//PHP处理
string phpCgiOutput = "";
Action phpProc = new Action(() =>
{
  try
  {
    string argStr = "";

    if (request.HttpMethod == "GET")
    {
      if (rawUrl.IndexOf('?') > -1)
        argStr = rawUrl.Substring(rawUrl.IndexOf('?'));
    }
    else if (request.HttpMethod == "POST")
    {
      using (StreamReader reader = new StreamReader(request.InputStream))
      {
        argStr = reader.ReadToEnd();
      }
    }

    Process p = new Process();
    p.StartInfo.CreateNoWindow = false; //不显示窗口
    p.StartInfo.RedirectStandardOutput = true; //重定向输出
    p.StartInfo.RedirectStandardInput = false; //重定向输入
    p.StartInfo.UseShellExecute = false; //是否指定操作系统外壳进程启动程序
    p.StartInfo.FileName = PHP_CGI_Path;
    p.StartInfo.Arguments = string.Format("-q -f {0} {1}", fileName, argStr);
    p.Start();

    StreamReader sr = p.StandardOutput;
    while (!sr.EndOfStream)
    {
      phpCgiOutput += sr.ReadLine() + Environment.NewLine;
    }

    responseByte = sr.CurrentEncoding.GetBytes(phpCgiOutput);
  }
  catch (Exception ex)
  {
    Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse->phpProc");
    response.StatusCode = (int)HttpStatusCode.InternalServerError;
  }
});

if (fileExt == "php" && PHP_CGI_Enabled)
{
  phpProc();
}
else
{
  if (!File.Exists(fileName))
  {
    responseByte = Encoding.UTF8.GetBytes("404 Not Found!");
    response.StatusCode = (int)HttpStatusCode.NotFound;
  }
  else
  {
    try
    {
      responseByte = File.ReadAllBytes(fileName);
      response.StatusCode = (int)HttpStatusCode.OK;
    }
    catch (Exception ex)
    {
      Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse");
      response.StatusCode = (int)HttpStatusCode.InternalServerError;
    }
  }
}

这样就实现了基于PHP-CGI的PHP支持了,经过测试,基本的php页面都可以支持,但是需要使用curl和xml这类扩展的暂时还没办法。需要做更多的工作。

接下来我会给服务器做一个GUI界面,供大家测试。

同时也会把QFramework框架发布,有兴趣的可以使用基于QFramework的服务器封装。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#创建一个小型Web Server(Socket实现)

    要实现了Web Server,通过以下几句代码浏览器访问就可以获得访问的数据. Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socketWatch.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 81)); socketWatch.Listen(20); // 参数表示最多可容纳的等待

  • 基于C#动手实现网络服务器Web Server

    前言 最近在学习网络原理,突然萌发出自己实现一个网络服务器的想法,并且由于第三代小白机器人的开发需要,我把之前使用python.PHP写的那部分代码都迁移到了C#(别问我为什么这么喜欢C#),之前使用PHP就是用来处理网络请求的,现在迁移到C#了,而Linux系统上并没有IIS服务器,自然不能使用ASP.Net,所以这个时候自己实现一个功能简单的网络服务器就恰到好处地解决这些问题了. 基本原理 Web Server在一个B/S架构系统中起到的作用不仅多而且相当重要,Web开发者大部分时候并不需要

  • Caddy 一个用Go实现的Web Server

    这是一个Web Server的时代,apache2与nginx共舞,在追求极致性能的路上,没有最高,只有更高.但这又是一个追求个性化的时代,有些Web Server并没有去挤"Performance提升"这一独木桥,而是有着自己的定位,Caddy就是这样一个开源Web Server. Caddy的作者Matt Holt在caddy官网以及FAQ中对caddy的目标阐释如下: 其他Web Server为Web而设计,Caddy为human设计.功能定位上,与经常充当最前端反向代理的ngi

  • 基于HTTP协议实现的小型web服务器的方法

    我们先了解一下这个项目最终能达到的一个目标,然后以这个来进行项目的分析: 1.实现最基本的HTTP/1.0版本的web服务器,客户端能够使用GET.POST方法请求资源 2.服务器将客户请求的资源以html页面的形似呈现,并能够进行差错处理(如:客户请求的资源不存在时,服务器能够返回一个404的页面) 3.服务器能进行简单的cgi运行.比如当客户在表单中输入数据后,服务器能够将运行结果返回个客户 4.能够通过页面对数据库进行操作,如增删查改等操作 一.http服务器实现的基本框架 关于HTTP协

  • Tornado Web Server框架编写简易Python服务器

    我们都知道在Web开发中,都需要服务器,比如Java Web开发的Tomcat,WebLogic,WebSphere,现在来看利用Tornado Web Server框架如何写一个简易的Python服务器. 一般来说只需要实现get和post方法就可以了.以上次使用redis数据库的例子说明,数据库插入代码如下: import redis import datetime class Database: def __init__(self): self.host = 'localhost' sel

  • 构建基于虚拟用户的vsftpd服务器应用

    安装: [root@server ~]# yum install -y vsftpd [root@server ~]# rpm -ql vsftpd /etc/logrotate.d/vsftpd /etc/pam.d/vsftpd /etc/rc.d/init.d/vsftpd /etc/vsftpd /etc/vsftpd/ftpusers /etc/vsftpd/user_list /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd_conf_migrat

  • 碎片拼接技术恢复XenServer服务器SQL Server数据库数据

    目录 1.数据恢复方案一 2.数据恢复方案二 ​环境:​ Dell PowerEdge服务器: XenServer虚拟化平台: 4块希捷2T STAT硬盘用RAID卡组成的RAID10: XenServer虚拟机操作系统:Windows Server系统: 虚拟机磁盘:1个10G系统盘和1个5G数据盘,部署的Web服务器(ASP +SQL). ​故障:​ 服务器突然断电导致服务器中一台XenServer虚拟机不可用,虚拟磁盘文件丢失,服务器管理员联系北亚数据恢复中心寻求帮助. ​故障检测和分析:

  • ftp服务器FileZilla Server详细配置教程

    FileZilla Server下载安装完成后,必须启动软件进行设置,由于此软件是英文,本来就是一款陌生的软件,再加上英文,配置难度可想而知,小编从网上找到一篇非常详细的教程进行整理了一番,确保读到这篇教程的同学都能够进行免费ftp服务器FileZilla Server配置. 运行FileZilla Server Interface.exe,得到以上界面,如果是第一次进入,直接点击ok即可.我们可以在「Administrator password:」栏位中输入本服务器Filezilla服务的密码

  • Java进阶学习:网络服务器编程

    文章来源:csdn 作者:DaiJiaLin Java的Socket API提供了一个很方便的对象接口进行网络编程.本文用一个简单的TCP Echo Server做例子,演示了如何使用Java完成一个网络服务器. 用作例子的TCP Echo Server是按以下方式工作的: 当一个客户端通过TCP连接到服务器后,客户端可以通过这个连接发送数据到服务端,而服务端接收到数据后会把这些数据用同一个TCP连接发送回客户端.服务端会一直保持这个连接直到客户端关闭它为止. 因为服务器需要能同时处理多个客户端

  • 在Zeus Web Server中安装PHP语言支持

    前言Zeus是一个运行于Unix下的非常优秀的Web Server,而PHP则是Unix下一个非常优秀的后台脚本语言. 这两个产品都是为非常喜欢的产品.为什么要写这样的一个Howto呢?是因为有大量的网站脚本是使用PHP开发的, 而这些程序运行在Zeus下也是一个非常好的选择.写这份文档的目的在于能让大家的PHP系统良好的运行于Zeus服务器上. 很早的时候我写过一份整合Zeus和PHP的文章,它主要是讲如何将PHP以FastCGI的本地调用方式来运行于Zeus中的, 本份Howto主要是来讲如

  • linux下编译安装kangle web server教程详解

    首先下载kangle kangle web server源代码. kangle linux版下载: kangle源代码(tar.gz包,非windows版本安装) 请先确保你的系统上有g++,libz开发包,libpcre开发包,libiconv开发包.如你的系统为centos/rhel则运行下面命令安装这些包: yum -y install wget make automake gcc gcc-c++ pcre-devel zlib-devel sqlite-devel openssl-dev

随机推荐