网络服务器如何工作?
HTTP
我们做了什么?
服务器如何工作?
服务端点
Web服务器
它们真是个神秘的东西……你只需为你的项目初始化一个,它就会处理剩下的事情。但这背后究竟发生了什么?
我很好奇,就问了我的朋友Jonathan Kingsley是否了解它们。结果发现他是那种喜欢读HTTP 论文的人,于是我们一起在我的直播间里,用 Go 语言构建了自己的 Web 服务器。
我是否提到过该服务器是米老鼠主题的?
HTTP
HTTP 代表“超文本传输协议”——它是 90 年代早期作为万维网项目的一部分创建的。
英国科学家蒂姆·伯纳斯-李 (Tim Berners-Lee) 于 1989 年在欧洲核子研究中心 (CERN) 工作期间发明了万维网 (WWW)。万维网最初的构想和开发是为了满足世界各地大学和研究所科学家之间自动共享信息的需求。—— CERN
该项目已成为互联网运作的基础——它概述了如何在服务器之间传输数据和信息的预期。正是由于万维网项目的努力,你的计算机才知道如何解读网站。
我们做了什么?
我们决定对一个“从零开始”的 Web 服务器进行两次迭代——第一次迭代(本文概述了它)处理对硬编码端点的 get 请求。在另一篇博文中,我将介绍如何构建一个能够处理更多动态实现端点的 Web 服务器。
服务器如何工作?
主要的
听力
在基本层面上,Web 服务器监听:
port, err := net.Listen("tcp", ":1928")
我们创建一个Listener对象,port
用于监听特定流量端口/通信端点,以接收特定网络协议的传入请求。这行代码实际上启动了服务器——现在您可以接收传入请求了。在本例中,我们期望在端口 1928 上接收 TCP 类型的请求。(米老鼠诞生于 1928 年,因此我们选择监听 1928 端口!)
太棒了!我们完成了!对吧?
不完全是……
我们现在可以接收请求,但是我们如何处理和读取它们,以及如何发回响应?
接受
conn, err := port.Accept()
已实现的通用函数之一port
是Accept()
函数。Accept会暂停/阻塞,直到新的传入请求被接收port
——传入的连接将作为Conn对象返回。在服务器运行时,此函数应该持续执行——这意味着在代码中,它被放置在一个无限的 中for loop
。
是的,你没看错——这是一个鼓励无限循环的时刻!
处理
go handleConnection(conn)
我们创建了一个名为 的新自定义函数handleConnection
- 这个函数是我们实际开始处理、解释和读取来自传入连接的数据的地方。每当有来自其他客户端的连接(也就是任何外部连接,因为我们是服务器)时,我们都会接受它,并衍生出一个新的 handleConnection 来处理该客户端。
我们使用关键字go
来分离goroutines,从而为服务器启用多线程异步功能。如果我们不使用goroutines来实现并发,阻塞的传入请求将阻塞该线程上的所有内容,因此您将无法处理那么多传入请求。
主要功能代码
func main() {
fmt.Println("Goofy: hyuck - booting up!")
port, err := net.Listen("tcp", ":1928")
if err != nil { //failed to set up server
fmt.Println("Mickey: Oh no Goofy! It looks like there was an error starting up the server! ")
fmt.Println(err.Error())
return
}
for {
conn, err := port.Accept()
if err != nil { //client failed to connect with server
fmt.Println(err.Error())
return
}
go handleConnection(conn)
fmt.Println("Welcome to the Mickey Mouse Web House!")
}
}
打印两次
fmt.Println("Welcome to the Mickey Mouse Web House!")
当你运行上面的代码时,这个打印可能会出现两次。当浏览器执行来自服务器的请求时,它们会对favicon
页面的 和页面的内容分别执行单独的请求。
处理连接()
HTTP 请求消息的剖析
根据 HTTP 的规范,必需的信息必须按照特定的顺序传递,以便接收方能够正确地消化。
GET / HTTP/1.1\r\n
Content-Type: text/plain; charset=UTF-8\r\n
Content-Length: <length>\r\n
\r\n
Hello World!\n
注意:当你读取传入的请求时,每行都以
\r\n
- 结尾,这能让缓冲区知道协议的每一行在哪里结束。基本上,它有助于分隔新行。
GET / HTTP/1.1\r\n
你可以从表面上理解这一点。它告诉服务器传入的请求类型、请求目标(试图到达的端点)以及 HTTP 版本。
如果您想了解有关 HTTP 消息结构的更多信息,请查看Mozilla 团队的这篇博客文章。
👋 我想指出 Content-Type 标头——直到我开始这个项目之前,我并没有意识到设置和检查
Content-Type
HTTP 请求的重要性。但现在,我明白了,每个 HTTP 标头的作用几乎就像一个基本的 if-else 检查点。如果不Content-Type
具体设置,解释服务器就不知道该做什么或如何处理请求的内容。处理每种类型的请求基本上都是硬编码的。基本上,
Content-Type
如果您正在发送请求,请务必检查您的标头;如果您正在处理/接收请求,请务必Content-Type
在您的文档中指定预期内容,以减少双方的挫败感!
HTTP 响应消息的剖析
HTTP 响应消息预计如下所示:
HTTP/1.1 200 OK\r\n
Content-Type: text/plain; charset=UTF-8\r\n
Content-Length: <length>\r\n
\r\n
Hello World!\n
让我们来分析一下。
HTTP/1.1 200 OK\r\n
这一行告诉我们正在使用的 HTTP 版本、状态代码以及与状态代码相关的文本
Content-Type: text/plain; charset=UTF-8\r\n
该标头告诉接收客户端传入内容的类型,以便它知道如何处理它。
Content-Length: <length> \r\n
\r\n
内容长度的值很重要,因为它可以帮助接收方了解接收消息何时已完整传递。它以两个字符结尾\r\n
- 第二个字符是请求处理器用来标记消息正文开头的空行。
Hello World!\n
这是邮件正文!
读取传入的请求
request, err := bufio.NewReader(connection).ReadString('\n')
我们根据传入的连接,使用bufio对象读取传入请求的信息。为了尽可能简化服务器,我们甚至不会检查传入的请求类型,而只检查请求的端点。这样我们只需要读取传入 HTTP 请求的第一行——无需关心请求的其余部分。
requestParts := strings.Split(request, " ")
我们将传入的请求分成几部分并检查...
if requestParts[1] == "/clubhouse" {
正在请求哪个端点。对于此服务器,我们仅接受一个端点:clubhouse
。
message := "if goofy has a dog, and goofy is a dog....????"
connection.Write([]byte("HTTP/1.1 200 OK\r\n"))
connection.Write([]byte("Content-Type: text/plain; charset=UTF-8\r\n"))
connection.Write([]byte("Content-Length: " + strconv.Itoa(len(message)) + "\r\n\r\n"))
connection.Write([]byte(message + "\n"))
return
为了发送响应,我们使用连接对象Write
的功能- 但在此之前,我们需要发送所需的标头(如上所述和超文本传输协议中所述)。
如果传入的请求不是在寻找/clubhouse
,那么我们会发回 404 错误。
connection.Write([]byte("HTTP/1.1 404 Not Found\r\n"))
connection.Write([]byte("Content-Type: text/plain; charset=UTF-8\r\n"))
connection.Write([]byte("Content-Length: 0\r\n\r\n"))
在完成该方法之前发生的最后一件事是我们必须关闭连接
defer connection.Close()
实际上,这是我们在handleConnection
函数中做的第一件事 - 如果您是 Go 新手,defer
这是一个关键字,用于描述在主函数完成时应该执行的函数,无论它在哪里结束。
handleConnection() 的代码
func handleConnection(connection net.Conn) {
defer connection.Close()
request, err := bufio.NewReader(connection).ReadString('\n')
if err != nil {
fmt.Println(err.Error())
return
}
requestParts := strings.Split(request, " ")
if requestParts[1] == "/clubhouse" {
message := "If Goofy has a dog, and Goofy is a dog....????"
connection.Write([]byte("HTTP/1.1 200 OK\r\n"))
connection.Write([]byte("Content-Type: text/plain; charset=UTF-8\r\n"))
connection.Write([]byte("Content-Length: " + strconv.Itoa(len(message)) + "\r\n\r\n"))
connection.Write([]byte(message + "\n"))
return
}
connection.Write([]byte("HTTP/1.1 404 Not Found\r\n"))
connection.Write([]byte("Content-Type: text/plain; charset=UTF-8\r\n"))
connection.Write([]byte("Content-Length: 0\r\n\r\n"))
}
服务端点
我很惊讶地发现,服务器本质上只是读取和解析字符串。经过多年的使用,我真心觉得服务器要复杂得多,像这样一个简单的例子,其实会复杂得多。
目前,我们的服务器只能处理一个端点/clubhouse
,该端点的响应是硬编码的。那么动态实现的端点呢?那些未直接在服务器代码中定义的端点——不用担心,我们也支持。博客文章即将发布。
顺便说一句,如果你好奇 New Relic 是否支持服务器……是的,它支持。点击此处了解更多。
特别感谢Jonathan Kingsley与我一起完成这个项目!有一位导师指导我完成整个过程,让我的理解变得非常容易!我非常感激!
真希望能看到我的学习直播?我几乎每天都会在 Twitch 上直播编程和其他有趣的科技内容。快来一起玩吧!期待下次再见!
米老鼠
是的……虽然这并没有用米老鼠进行任何类比,但我一直在努力模仿米老鼠,而且在我和乔纳森直播这个项目的整个过程中,我一直在模仿米老鼠,因此使用了米老鼠。
文章来源:https://dev.to/endingwithali/how-do-web-servers-work-54ci