估计阅读时长: 12 分钟

https://github.com/biocad-cloud/web

HTTP协议(Hypertext Transfer Protocol)是建立在TCP协议基础上的一种文件传输协议。

本质上,HTTP协议是一种CS模式的TCP网络协议,因为存在客户端和服务器端;但是我们更加通常的将其称作为BS模式,即浏览器端对服务器端(虽然我们通常进行REST请求的客户端并不是浏览器)。我们的浏览器进行网页浏览就是进行基于HTTP协议的文件下载操作:例如打开一个网页就是下载一个html文件以及对应的image,css,js等附件文件

执行一个xhr数据请求实际上也是一个文件传输操作:我们一般是使用jQuery的ajax向Web服务器发送一个json文件,然后服务器返回一个json文件或者其他的类型的文件;
如果是执行一个Form请求,则是我们通过浏览器向Web服务器发送一个纯文本文件,然后服务器向我们能返回一个json文件或者其他类型的文件。

HTTP协议基础

在讲解HTTP协议之前,我们首先来看看一个HTTP的请求是长什么样的:

GET /ping HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7

上面的一段代码就是我们使用谷歌浏览器请求Web服务器的时候一般会产生的HTTP请求头。HTTP请求头就是一段纯文本数据,这个纯文本的请求头一般而言,由下面的数据格式结构组成:

<initial line, different for request vs. response>
Header1: value1
Header2: value2
Header3: value3

<optional message body goes here, like file contents or query data;
 it can be many lines long, or even binary data $&*%@!^$@>
  • 第一行文本,必须包含有请求的方式,以及请求的资源的URL,请求的协议版本的信息
  • 理论上我们只需要第一行文本就可以构建出一个最小可用的HTTP请求了
  • 剩下的HTTP头部分是可选的数据,我们一般从浏览器端发送的HTTP请求头包含有一些比较重要的信息数据,例如用来标识我们当前身份的Cookie字符串;用来获取对应浏览器优化的css样式的UA字符串等信息
  • 在HTTP请求头中的每一行文本都需要使用CRLF作为换行符
  • HTTP请求头必须以一行仅包含有CRLF的空白行作为请求头结束的标记,在请求头后面都是我们发送给服务器的payload

HTTP消息返回

HTTP消息返回的格式与HTTP请求的格式是一样的,只不过在格式上我们将HTTP请求的第一行替换为了HTTP状态代码:

HTTP/1.1 200 OK
Date: Wed, 16 Jun 2021 03:06:02 GMT
Server: Apache/2.4.37 (centos)
X-Powered-By: PHP/7.4.19
X-Pingback: https://stack.xieguigang.me/xmlrpc.php
Link: <https://stack.xieguigang.me/wp-json/>; rel="https://api.w.org/", <https://stack.xieguigang.me/wp-json/wp/v2/posts/440>; rel="alternate"; type="application/json", <https://stack.xieguigang.me/?p=440>; rel=shortlink
Set-Cookie: pvc_visits[0]=1623813716b440; expires=Wed, 16-Jun-2021 03:21:56 GMT; Max-Age=954; path=/; HttpOnly
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
Proxy-Connection: keep-alive

上面的一串代码是WordPress网站服务器返回给浏览器的消息返回,可以看到

  • 第一行文本为消息返回的状态,我们一般是使用200表示成功执行请求
  • 下面的都是HTTP消息头,都是可选的
  • 返回的在HTTP消息头之后的文件之类的Payload数据也是可选的
  • HTTP消息头之中的每一行文本都使用CRLF作为分隔符
  • HTTP消息头必须使用一行仅包含有CRLF的空白行作为结束符号,即HTTP消息头与Payload之间必须有一个CRLF做分隔
  • 理论上我们只需要第一行的HTTP状态就可以构成一个最小可用的HTTP消息返回
<initial line, http version and status code>
Header1: value1
Header2: value2
Header3: value3

<optional message body goes here, like file contents or query data;
 it can be many lines long, or even binary data $&*%@!^$@>

GET与POST的区别?

就HTTP请求的本质上来讲GET与POST请求之间没有任何差别,差别仅仅是在于GET于POST在语义上的差异:

  • GET请求一般是请求一个服务器资源而不对服务器上的内容做修改
  • POST请求一般是向服务器传输payload,并且服务器可能会对服务器端的一些资源内容产生了修改

一般而言,我们只需要

GET /ping HTTP/1.1
# modify http method from GET to POST
POST /ping HTTP/1.1

就可以将GET请求转换为POST请求,其他的数据都不用变化。这两者在HTTP请求的本质上确实没有任何区别。我们在GET请求中一般不添加任何payload,但是我们也可以将payload添加进入GET请求,以GET请求的方式在Web服务器端执行POST请求相同的过程代码。

除了我们最常用的GET请求和POST请求之外,还有一些其他的请求类型:

  • PUT请求一般是用于上传一个文件,但是我们更加常用POST请求上传一个文件
  • DELETE请求一般是用于删除服务器上的一个资源,但是我们更加常用POST请求删除一个资源
  • HEAD/CONNECT/OPTIONS/TRACE等等
  • 可以自己添加一些自定义的Http请求方法

MIME类型

在前面我们也聊过,HTTP协议其实就是一个文件传输协议。对于我们传递给服务器的文件资源以及服务器返回给我们的文件资源,我们必须要知道其具体的文件类型才可以进行正确的处理。一般而言,我们在Windows系统上是通过文件拓展名来分辨文件类型的。而在HTTP协议中则是通过MIME type来进行区分。

注:在Web服务器上可能仍然是通过文件拓展名来识别文件类型;除了文件的拓展名,在服务器端也还有通过二进制文件的Magic幻数进行文件类型识别的方法。

文件的MIME类型,在服务器返回的消息头中,一般是在Content-Type头之中,例如最常见的返回为:

Content-Type: text/html

从零开始编写一个Web服务器

在了解了HTTP协议的基础之后,开始来介绍一下怎么从头开始编写一个简单的Web服务器吧。因为HTTP协议在本质上就是一种基于TCP协议的文件传输协议,所以我们的Web服务器的最基础的一个组件就是TCP socket,在这里我使用.NET框架中自带的TcpListener作为Web服务器的基础组件

Protected Friend ReadOnly _httpListener As TcpListener

Web服务器Daemon代码

一个Web服务器就是一个daemon进程,其需要常驻于服务器后台,因为我们会需要监听来自于客户端的HTTP请求,所以在这里我们使用一个While无限循环来建立这样的一个daemon过程

While Is_active
    If Not _threadPool.FullCapacity Then
        Call _threadPool.RunTask(AddressOf accept)
    Else
        Thread.Sleep(1)
    End If
End While

Private Sub accept()
    Try
        Dim s As TcpClient = _httpListener.AcceptTcpClient
        Dim processor As HttpProcessor = getHttpProcessor(s, BufferSize)

        Call Time(AddressOf processor.Process)
    Catch ex As Exception
        Call App.LogException(ex)
    End Try
End Sub

解析HTTP消息请求

在这里我构建了一个HttpProcessor模块进行HTTP消息的解析操作:

Private Sub processHttpRequest()
    If Not parseRequest() Then
        Return
    Else
        Call readHeaders()
    End If

    If http_method = "GET" Then
        handleGETRequest()
    ElseIf http_method = "POST" Then
        HandlePOSTRequest()
    Else
        Call srv.handleOtherMethod(Me)
    End If
End Sub

因为按照之间的讲解,我们知道,在一个合法的HTTP消息请求之中,第一行为HTTP方法类型,以及对应的服务器资源的URL;之后为HTTP消息头;HTTP消息头与playload之间使用一个CRLF进行分隔。所以在上面的方法中,一个完整的HTTP请求的处理过程就是:

  • 首先parseRequest,解析出请求的URL,HTTP方法等信息
  • 接着,就可以readHeaders将payload之前的纯文本都解析为HTTP消息头
  • 最后就是根据不同的HTTP方法,执行不同的HTTP请求过程了

因为在HTTP的消息头之中,纯文本的行之间都是以CRLF进行结尾的,所以我们在解析HTTP消息头的时候,一般是按照行,一行一行的解析读取,所以专门编写了一个streamReadLine函数用来按行读取浏览器发送过来的HTTP消息中的消息头文本数据

''' <summary>
''' each stream line is end with ``cr + lf``.
''' </summary>
''' <param name="inputStream"></param>
''' <returns></returns>
Private Function streamReadLine(inputStream As Stream) As String
    Dim nextChar As Integer
    Dim chrbuf As New List(Of Char)
    Dim n As Integer

    While True
        nextChar = inputStream.ReadByte()

        If nextChar = ASCII.Byte.LF Then
            Exit While
        End If
        If nextChar = ASCII.Byte.CR Then
            Continue While
        End If

        If nextChar = -1 Then
            Call Thread.Sleep(1)

            n += 1

            If n > 1024 Then
                Exit While
            Else
                Continue While
            End If
        End If

        Call chrbuf.Add(Convert.ToChar(nextChar))
    End While

    Return New String(chrbuf.ToArray)
End Function

parseRequest函数中解析HTTP请求,就是将第一行读取出来过后,按照空格进行字符串切分,然后分别按照Index取出数据就好了。例如:

' GET /ping HTTP/1.1
Dim request As String = streamReadLine(_inputStream)
Dim tokens As String() = request.Split(" "c)

' GET
http_method = tokens(0).ToUpper()
' /ping
http_url = tokens(1)
' HTTP/1.1
http_protocol_versionstring = tokens(2)

readHeaders函数中解析HTTP消息头,就是将使用CRLF空白行分隔的payload之前的纯文本解析为一个字典对象就可以了。我们观察上面所给出的HTTP请求的示例,可以看出,HTTP消息头都是key: value这样子的键值对,所以解析过程就可以非常简单的写出来:

While (s = streamReadLine(_inputStream)) IsNot Nothing
    If s.Value.StringEmpty Then
        Return
    Else
        line = s.Value
        separator = line.IndexOf(":"c)
    End If

    If separator = -1 Then
        Throw New Exception("invalid http header line: " & line)
    End If

    Dim name As String = line.Substring(0, separator)
    Dim pos As Integer = separator + 1

    While (pos < line.Length) AndAlso (line(pos) = " "c)
        ' strip any spaces
        pos += 1
    End While

    Dim value As String = line.Substring(pos, line.Length - pos)

    httpHeaders(name) = value
End While

至此,我们的HTTP请求消息解析部分的代码就编写完了

产生HTTP响应

对于每一个来自浏览器的HTTP消息,我们的Web服务器都需要返回一个响应结果。我们在Web服务器代码中,只需要按照前面所介绍的响应格式向流对象中写入对应格式的数据就可以了:

' this is the successful HTTP response line
Call outputStream.WriteLine("HTTP/1.0 200 OK")
' these are the HTTP headers...
Call outputStream.WriteLine("Content-Length: " & content.length)
Call outputStream.WriteLine("Content-Type: " & content_type)
Call outputStream.WriteLine("Connection: close")
' ..add your own headers here if you like

Call outputStream.WriteLine(XPoweredBy)

' 2018-1-31
' The server committed a protocol violation.
' Section = ResponseHeader
' Detail  = CR must be followed by LF
'
' RFC 822中的httpHeader必须以CRLF结束的规定的服务器响应。
'
' app.config配置文件修改
'
' <?xml version="1.0" encoding="utf-8" ?>
' <configuration>
' <system.net>
'        <settings>
'               <httpWebRequest useUnsafeHeaderParsing = "true" />
'        </settings>
' </system.net>
' </configuration>
Call outputStream.WriteLine()
' this terminates the HTTP headers.. everything after this is HTTP body..
Call outputStream.Flush()

谢桂纲
Latest posts by 谢桂纲 (see all)

Attachments

One response

Leave a Reply to 使用R#脚本编写一个Http服务器 – この中二病に爆焔を! Cancel reply

Your email address will not be published. Required fields are marked *

博客文章
April 2024
S M T W T F S
 123456
78910111213
14151617181920
21222324252627
282930  
  1. 空间Spot结果在下载到的mzkit文件夹中有示例吗?我试了试,不是10X结果中的tissue_positions_list.csv(软件选择此文件,直接error);在默认结果中也没找到类似的文件;