书写简单的Web服务器

本文最后更新于:2022年7月29日 下午

这里讲一下如何书写一个简易的Web服务器

书写简单的Web服务器

前言

这篇文章主要是写给一些大一大二的学弟学妹们,可能正学完了一门语言但是不知道做什么项目,或者只听闻这些软件都是通过编程实现的但是又不知道自己可以做什么,并且学校做的项目基本上都是xx管理系统,而我相信你肯定已经写腻了,今天我就教大家做一个简单的服务器,他可以接收浏览器的一个请求并返回响应。

其实这里设计到了很多计算机网络基础知识,可能还设计到了一些设计模式,对于大一大二的学弟学妹可能会比较难懂,但是没有关系,我会在这里尽可能的讲通俗易懂。

当然学过PHP的同学就开始会心一笑了,我随便写段代码就可以做到。哈哈,这里注意一下哦,是实现一个Web服务器,也就是说我们会亲自解析HTTP报文等,并不是直接使用封装好的Req和Resp

预览一下成果吧

大家肯定想先看下成果,好吧,其实我们要做的非常简单,就是一个非常简单(简陋)的Web服务器。简单来说就是我们在浏览器搜索框输入一个网址,浏览器可以显示一定的内容。

image-20211005110325166

好了,这就是我们需要实现的功能,是不是非常简单。

一些基础知识

千里之行始于足下,虽然我也想开始写代码,但是还是需要补充一下基础知识。我们还是从上一步的成果开始吧,我们首先要了解在浏览器中的搜索框输入一个网址发送了什么(哈哈非常经典的面试题啊)。我们可以打开控制台来看看,也就是如下图这玩意。可以右键点检查可以弹出、或者按F12。之后我们选中网络,它可以显示出当前网页中的所有网络请求。

image-20211005110943766

可以看到这里发送了两个请求,一个是 localhost 一个是 favicon.ico ,其中第一个是加载整个网页,第二个是加载一个小图标,也就是显示在标签页中的哪个小图标,我们不管它,直接看第一个请求。

image-20211005111153405

这里需要设计到一些HTTP协议的基本原理,之后再看这里面的东西你就全部理解了。那么什么是HTTP协议呢,可以想象一下这种场景,A和B说话,A说你好,B说你也好,很简单的通话,我相信双方都可以理解,这里就涉及到了语言方面的东西,正是因为A、B都了解汉语,他们才互相可以理解对方的意思。那么这里的语言就是协议了,也就是说双方共同约定的一些规则。而HTTP是什么呢,HTTP就是某种特定的语言。我们可以画一个草图

image-20211005112017221

HTTP协议也大致是这个过程,就是一问一答的过程,更加细节的HTTP协议可以见 菜鸟教程 ,这是我见过的比较简单、全面的教程了。

相信到了这里大家也开始有一点点了解浏览器的作用了,就是一个请求发送器、响应渲染器。当然这里面要做的事情非常复杂(Chrome代码几百G,某乎说的)。

读到这里可能大家又有点迷糊了,我知道了HTTP协议,但是如果解析、响应它呢?嘿嘿,这里就涉及一些计算机网络方面的知识了,因为 HTTP是一个基于TCP/IP通信协议来传递数据 ,这段话的意思是我们需要新学一个协议 TCP 吗?不不不,这里不加重各位同学的学习负担了,我们直接直接使用 Socket 来编程即可。Socket 可以理解为是空调遥控器,而空调就是 TCP 协议。同理画一个简图来帮助理解(这里把响应报文的过程的简化了,和请求报文的过程是一样的)

这里强烈推荐各位同学在本篇文章之外去努力学习 TCP 协议

image-20211005113554988

让我们开始吧

这里我们使用Go语言作为我们的开发语言。前文我们提到了 SocketHTTP 我们都会在这里用到。Go语言的 Socket 编程十分简单(如果需要了解其它语言的 Socket 编程可以直接 Google)。其中 net.Listen(network,address) 函数就会直接帮我们监听一个端口,并且这个端口使用的协议为 network

监听的端口我们可以随意选择,这里我们选中 8080 作为我们的端口,上文提到 HTTP是一个基于TCP/IP通信协议来传递数据 ,因为我们要做的是一个提供Web服务的程序,所以这里只能选中 TCP 作为我们监听的协议。

所以我们写下我们的第一行代码

1
listen, err := net.Listen(`tcp`, ":8080") // Go语言可以有多返回值,

其中返回的参数 listenSocket 可操控的对象,这里有一个方法 Accept() (Conn, error) ,它会阻塞到获取一个 TCP 连接才继续运行下一段代码,这个 TCP 连接以一个 Conn 对象描述,我们可以往连接中写入一些数据,或从连接中读取一些数据,或直接关闭这个连接。

1
2
3
func Func(a int) (Object, error){
// do something.....
}

Go语言的函数以 func 作为定义。把返回值写在函数参数之后。这里可以看到 Func(a int) 函数有一个int类型的参数,这里Go的参数的类型也是放在后面的,其中 error 为一个类型,它代表函数的异常,如果返回的 error 不为空则代表函数发生了异常。也就是看到很多人吐槽的

1
2
3
if err != nil {

}

了解了这些我们开始写下一行代码

1
conn, err := listen.Accept() // 等待一个TCP连接

我们拿到了 TCP 连接之后就可以开始我们的Web服务器代码的编写。可以封装出一个函数 handler (我随便取的名字)来做具体的操作。

也就是 handler(conn)

说到这里我们就可以书写出较为完整的代码了,这里做了一些 TCP 相关的操作,之后我们就只需要针对连接进行操作就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

func main() { // 只有在main包的main函数会被认为是程序的入口
listen, err := net.Listen(`tcp`, ":8080")// 监听一个协议为tcp的8080端口,让他来获取一些连接
if err != nil {
log.Fatalf("net.Listen got err:%v", err)
return
}
defer listen.Close() // 需要关闭连接
conn, err := listen.Accept() // 等待一个请求
if err != nil {
log.Fatalf("Accept got err:%v", err)
return
}
handler(conn) // 对这个连接进行操作
}

handler函数

其中 Conn 对象中有两个比较重要的方法

1
2
Read(b []byte) (n int, err error) // 从连接中读取一些数据到 b 中
Write(b []byte) (n int, err error) // 往连接中写入一些数据

我们先来读取一些这些数据吧。

由于这里的读取稍微复杂,这里只写最简单的版本,我们先新建一个 byte 数组来存放请求报文的数据,之后在从连接中读取数据到 byte 数组中取。

1
2
var buff = make([]byte, 512) // 用来存放请求报文的数据 
c.Read(buff) // 从连接中读取数据

我们之后再把数据打印出来。这里完整的代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func handler(c net.Conn) {
defer c.Close() // 中断连接,所有的连接都是短链接
defer func() {
if err := recover(); err != nil { // recover panic,不写也是一样的
log.Printf("handler got panic:%v", err)
}
}()

var buff = make([]byte, 512)
n, err := c.Read(buff)
log.Println("Read done")
if err != nil {
log.Println("read from client failed, err:", err)
}
log.Printf("read in conn: %v", n)

log.Println(string(buff))
}

这里我们运行一次程序并在浏览器中请求一次,就可以看到请求报文的数据

image-20211005122812965

可以看到我们已经拿到了请求报文的数据,但是这里还是有点瑕疵,其实请求报文远远不止 512个字节,所以可以看到这里已经读满了数组但却没有更多的内存供其使用,也就是说连接中还有数据是我们没有读取出来的,这是非常致命的异常。但是我打算留到下一个版本中解决。

你会发现现在浏览器中显示的无法访问此网址。这是因为我们的程序还没有返回响应报文,我们把响应报文给加上

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<h2>Hello.This is my Web Server</h2>

只需要用 c.Write(b []byte) 写入即可。再次运行我们就可以看到成果啦。

完整代码在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"log"
"net"
)

func handler(c net.Conn) {
defer c.Close()
defer func() {
if err := recover(); err != nil {
log.Printf("handler got err:%v", err)
}
}()

var buff = make([]byte, 512)
n, err := c.Read(buff)
log.Println("Read done")
if err != nil {
log.Println("read from client failed, err:", err)
}
log.Printf("read in conn: %v", n)

log.Println(string(buff))

c.Write([]byte(`HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<h2>Hello.This is my Web Server</h2>`))
}

func main() {
listen, err := net.Listen(`tcp`, ":8080")
if err != nil {
log.Fatalf("net.Listen got err:%v", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
log.Fatalf("Accept got err:%v", err)
return
}
go handler(conn)
}
}

其实真正重要的代码就只有十行左右的而已,我们就可以书写出简单的Web服务器

写在最后

因为下午还要去约会。就先写到这里,本来是打算一篇文章写完的,大意了,没有闪,先溜了。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!