文章阅读目录大纲
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()
- 【MZKit】简单自动化组织分区 - 2023年11月5日
- 【MZKit教程】质谱成像原始数据文件查看 - 2023年6月29日
- 生物序列图嵌入算法 - 2023年6月29日
One response
[…] 关于HTTP协议的基础,可以阅读文章《从零开始手撸一个Web服务器》 […]