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

VisualBasic 中的简单 HTTP 服务器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2016 年 1 月 1 日

CPOL

4分钟阅读

viewsIcon

34725

downloadIcon

2404

这是“C# 中的简单 HTTP 服务器”的替代方案。

下载 httpd.zip

引言

最近我为我的 GCModeller 系统编写了一个 REST 服务系统,它提供的在线服务与 KEGG 服务器上的注释系统或 NCBI 上的在线 blast 服务类似。由于 ASP.NET 的限制,我无法构建一个不够方便或不够定制化的服务系统,因此我决定为 GCModeller 开发自己的 http 服务器系统,而不是使用 ASP.NET。

背景

实际上,这篇文章是对“C# 简单 HTTP 服务器”的替代,灵感来自 @David Jeske 的精彩工作,然后我就可以开始编码这个服务器系统了。

使用代码

我对这个 http 服务器核心的原始源代码做了一些改进,以方便我的编码工作。

404 页面

我修改了 David 源代码中的 writeFailure 函数,以便我可以定制 404 页面。

''' <summary>
''' You can customize your 404 error page at here.
''' </summary>
''' <returns></returns>
Public Property _404Page As String

''' <summary>
''' 404
''' </summary>
Public Sub writeFailure(ex As String)
    On Error Resume Next

    ' this is an http 404 failure response
    Call outputStream.WriteLine("HTTP/1.0 404 Not Found")
    ' these are the HTTP headers
    '   Call outputStream.WriteLine("Connection: close")
    ' ..add your own headers here

    Dim _404 As String

    If String.IsNullOrEmpty(_404Page) Then
        _404 = ex
    Else
       ' 404 page html file usually located in the root directory of the site, 
       ' If the Then file exists the read the page And replace the 
       ' Exception message With the Placeholder %Exception%

        _404 = Me._404Page.Replace("%EXCEPTION%", ex)
    End If

    Call outputStream.WriteLine(_404)
    Call outputStream.WriteLine("")         ' this terminates the HTTP headers.
End Sub

传输数据的方法

此服务器中有两种可以传输的数据类型:HTML 文本和字节流。仅此而已!足够简单!

HttpProcessor.outputStream.WriteLine

此方法用于将 HTML 数据传输到您的浏览器,您可以使用下面的代码进行调用。

  ' Transfer HTML document.
  Dim html As String = System.Text.Encoding.UTF8.GetString(buf)

  Call p.writeSuccess()
  Call p.outputStream.WriteLine(html)

HttpProcessor.outputStream.BaseStream.Write

此方法用于传输其他类型的数据,包括 css、js 脚本、图像文件等,但不包括 HTML 文档。

''' <summary>
''' 为什么不需要添加content-type说明??
''' </summary>
''' <param name="p"></param>
''' <param name="ext"></param>
''' <param name="buf"></param>
Private Sub __transferData(p As HttpProcessor, ext As String, buf As Byte())
    If Not Net.Protocol.ContentTypes.ExtDict.ContainsKey(ext) Then
       ext = ".txt"
    End If

    Dim contentType = Microsoft.VisualBasic.Net.Protocol.ContentTypes.ExtDict(ext)

    ' Call p.writeSuccess(contentType.MIMEType)
    Call p.outputStream.BaseStream.Write(buf, Scan0, buf.Length)
    Call $"Transfer data:  {contentType.ToString} ==> [{buf.Length} Bytes]!".__DEBUG_ECHO
End Sub

文件系统 IO

在我们开始将文档数据传输到浏览器之前,我们必须首先从 URL 请求中定位文件,并且在 http 服务器类的构造函数中,我们已经指定了 wwwroot 目录的位置,因此我们只需将请求 URL 与 wwwroot 目录路径结合起来,就可以从浏览器请求中定位文件。

Public ReadOnly Property HOME As DirectoryInfo

Private Function __requestStream(res As String) As Byte()
    Dim file As String = $"{HOME.FullName}/{res}"
    If Not FileExists(file) Then
       If _nullExists Then
          Call $"{file.ToFileURL} is not exists on the file system, returns null value...".__DEBUG_ECHO
          Return New Byte() {}
       End If
    End If
    Return IO.File.ReadAllBytes(file)
End Function

由于我们传输的文档不仅是 html、css 或 js 等文本文档,还包括图像、zip、音频或视频,因此此方法读取文件中的所有字节,而不是像原始源代码那样读取所有文本。

我不确定为什么 HTML 文档不能与其他文档使用相同的方法传输,可能是浏览器的一个 bug,也可能不是,我不确定,因此我将 HTML 文档和其他类型数据传输的路由分开。 

Private Sub __handleFileGET(res As String, p As HttpProcessor)
    Dim ext As String = FileIO.FileSystem.GetFileInfo(res).Extension.ToLower
    Dim buf As Byte() = __requestStream(res)

    If String.Equals(ext, ".html") Then ' Transfer HTML document.
        Dim html As String = System.Text.Encoding.UTF8.GetString(buf)
        Call p.writeSuccess()
        Call p.outputStream.WriteLine(html)
    Else
        Call __transferData(p, ext, buf)
    End If
End Sub

 

从上面的代码可以看出,我们能够在此 http 服务器程序中构建一个虚拟文件系统,这是 HTTP 服务器将文档从服务器文件系统传输到浏览器的基本功能。然后我们可以测试这个基本虚拟文件系统。

在我的浏览器中输入地址 127.0.0.1,然后 HTTP 请求就从浏览器开始发送到我自己的 HTTP 服务器程序。哦,是的,GCModeller.org 的主页出现在我的浏览器中!太棒了!!!

 

REST API 路由

我使用 If 语句使 REST API 调用可以与文件 GET 请求兼容:

通常 REST GET/POST 请求需要参数,参数值从 API 名称后面的 ? 字符开始。? 字符在 Windows 文件系统中是非法的,因此我们可以使用此属性来区分文件 GET 请求和 REST API 请求。

''' <summary>
''' 为什么不需要添加<see cref="HttpProcessor.writeSuccess(String)"/>方法???
''' </summary>
''' <param name="p"></param>
Public Overrides Sub handleGETRequest(p As HttpProcessor)
     Dim res As String = p.http_url

     If String.Equals(res, "/") Then
         res = "index.html"
     End If

     ' The file content is null or not exists, that possible means this is a GET REST request not a Get file request.
     ' This if statement makes the file GET request compatible with the REST API
     If res.PathIllegal Then
         Call __handleREST(p)
     Else
         Call __handleFileGET(res, p)
     End If
End Sub

确定路径非法

使用此扩展函数很容易确定文件路径是否非法,只需在路径中查找非法字符即可。

''' <summary>
''' 枚举所有非法的路径字符
''' </summary>
''' <remarks></remarks>
Public Const ILLEGAL_PATH_CHARACTERS_ENUMERATION As String = ":*?""<>|"
Public Const ILLEGAL_FILENAME_CHARACTERS As String = "\/" & ILLEGAL_PATH_CHARACTERS_ENUMERATION

''' <summary>
''' File path illegal?
''' </summary>
''' <param name="path"></param>
''' <returns></returns>
<ExportAPI("Path.Illegal?")>
<Extension> Public Function PathIllegal(path As String) As Boolean
    Dim Tokens As String() = path.Replace("\", "/").Split("/"c)
    Dim fileName As String = Tokens.Last

    For Each ch As Char In ILLEGAL_PATH_CHARACTERS_ENUMERATION
       If fileName.IndexOf(ch) > -1 Then
           Return True
       End If
    Next

    For Each DIRBase As String In Tokens.Takes(Tokens.Length - 1)
        For Each ch As Char In ILLEGAL_PATH_CHARACTERS_ENUMERATION
            If fileName.IndexOf(ch) > -1 Then
               Return True
            End If
        Next
    Next

    Return False
End Function

处理 API 调用

''' <summary>
''' handle the GET/POST request at here
''' </summary>
''' <param name="p"></param>
Private Sub __handleREST(p As HttpProcessor)
    Dim pos As Integer = InStr(p.http_url, "?")
    If pos <= 0 Then
        Call p.writeFailure($"{p.http_url} have no parameter!")
        Return
    End If

    ' Gets the argument value, value is after the API name from the ? character
    ' Actually the Reflection operations method can be used at here to calling 
    ' the different API 
    Dim args As String = Mid(p.http_url, pos + 1)
    Dim Tokens = args.requestParser
    Dim query As String = Tokens("query")
    Dim subject As String = Tokens("subject")
    Dim result = LevenshteinDistance.ComputeDistance(query, subject)

    ' write API compute result to the browser
    Call p.writeSuccess()
    Call p.outputStream.WriteLine(result.Visualize)
End Sub

现在 http 服务器已经能够处理 REST API 了。在启动本文档中的示例应用程序后,您可以尝试使用此 URL 来测试示例 REST API。

http://127.0.0.1/rest-example?query=12345&subject=2346

此 API 地址 URL 中有三个元素:

rest-example 是此 REST API 测试示例的 API 名称。

query=/string/ and subject=/string/ 是 API 的参数名称,分别命名为 querysubject,参数值可以在您在浏览器中输入的 URL 中修改。

示例 REST API 测试成功!

完成服务器

最后,通过添加程序的主入口点和 CLI 解释器,我们可以完成这个 http 服务器项目。

Imports Microsoft.VisualBasic.CommandLine.Reflection

Module Program

    Public Function Main() As Integer
        Return GetType(CLI).RunCLI(App.CommandLine)
    End Function
End Module

Module CLI

    ''' <summary>
    ''' Run the http server
    ''' </summary>
    ''' <param name="args"></param>
    ''' <returns></returns>
    <ExportAPI("/start", Usage:="/start [/port <default:=80> /root <./wwwroot>]",
               Info:="Start the simple http server.",
               Example:="/start /root ~/.server/wwwroot/ /port 412")>
    <ParameterInfo("/port", True,
                   Description:="The data port for this http server to bind.")>
    <ParameterInfo("/root", True,
                   Description:="The wwwroot directory for your http html files, default location is the wwwroot directory in your App HOME directory.")>
    Public Function Start(args As CommandLine.CommandLine) As Integer
        Dim port As Integer = args.GetValue("/port", 80)
        Dim root As String = args.GetValue("/root", App.HOME & "/wwwroot")
        Dim httpd As New HttpInternal.HttpFileSystem(port, root, True)

        Call httpd.Run()

        Return 0
    End Function
End Module

尝试从 cmd 控制台调用命令来启动我们自己的 http 服务器。

httpd /start

 

我自己的 http 服务器在控制台上运行顺利。

http 服务器通过您的防火墙请求 Internet 访问,请启用它。

关注点

这里只是一个构建我自己的 http 服务器的非常简单的简要思路,通过重写 handleGETRequest(p As HttpProcessor) handlePOSTRequest(p As HttpProcessor, inputData As StreamReader) 方法,我们就可以实现 http 文件请求处理程序的功能或实现您自己的 http REST 服务服务器。

 

出于某些原因,此 http 服务器与 Microsoft IE 或 Edge 浏览器不兼容,我不知道为什么……对此表示歉意。

© . All rights reserved.