动手实现一个简易的 Http server | 青训营
动手实现一个简易的 Http server
在学完 go 语言基础后,想做一个较为综合的实践项目巩固知识。在搜寻相关资料后,发现动手实现一个 http server 比较合适。涉及到结构体,闭包,字符串处理,map,IO流,异常处理,socket,和 http 的相关知识。有助于将这些基础知识整合运用.
前言
一些现成的Http框架,如 net/http, Gin,可以做到开箱即用,但作为开发者我们也要大致了解一个框架背后的原理。
今天分享一个基于原生的 TCP socket 库封装一个简易的 Http 框架的小项目。同时简易地实现一个 Http 框架的基本功能,包括:
- 配置端口和静态资源路径
- 设置处理函数的路由映射
- 通过请求响应对象获取参数和设置响应
- 基本的错误响应
整体设计
整个实现基于 Http 1.1 明文协议,据此设计几个功能模块:
- 配置解析,暂定只有配置端口和静态资源路径
- 对 TCP socket 的封装,控制请求的生命周期
- 解析报文,读取参数并封装成 httpRequest 对象
- 处理请求,调用处理器函数、返回静态文件、返回错误等
首先定义以下结构
// server 配置
type config struct {
Resource string `yaml:"resource"`
Port int `yaml:"port"`
}
// server
type HttpServer struct {
config
handlers map[string]func (*Request, *Response)
}
// 套接字 client
type client struct {
conn net.Conn
}
// 请求报文第一行的元数据,包括请求参数,请求方法,请求路径
type metadata struct {
params map[string]string
version string
method string
uri string
}
// 请求对象,包括元数据,请求头,请求体
type Request struct {
metadata
headers map[string]string
body []byte
}
接着定义函数,主要包括 server启动和对报文的处理流程, 以大写字母开头的是对外暴露的函数。
省略 Request, Response 结构的 setter 和 getter
// 处理请求报文元数据
func loadMetadata(first string) metadata {}
// 解析请求头和请求体
func (r *Request) Load(meg string) {}
// 解析请求
func (c *client) decode() *Request {}
// 处理路由
func (c *client) router(request *Request) (string, *Response, bool) {}
// 封装响应
func (c *client) encode(response *Response) error {}
// client 套接字生命周期
func do(c *client, s *HttpServer) {}
// 增加处理器映射
func (s *HttpServer) AddRouterFunc(path string, fun func(*Request, *Response)) error {}
// 调用处理器方法
func (s *HttpServer) handle(path string, req *Request, resp *Response) {}
// server 持续监听连接
func (s *HttpServer) serve() error {
// ...
for {
conn, err := listener.Accept()
var c = client{conn: conn}
go do(&c, s)
}
}
// 解析配置
func (s *HttpServer) loadConfig() error {}
// 初始化 server 对象
func Init(server *HttpServer) error {}
// Server Main 函数
func (s *HttpServer) Run() {}
因为篇幅原因不便展示所有的代码,仅讲解关键部分。
解析报文
参考 RFC 的协议文档
Http 1.1 报文结构如下:
[METHOD] [URI] [VERSION]
[HEADER]: [VALUE]
[HEADER]: [VALUE]
[HEADER]: [VALUE]
[PAYLOAD]
-
在报文的第一行是请求方法 [GET/POST/PUT]、请求URI [/path/to?param1=123¶m2=xxx]、协议版本号 [HTTP/1.1].
-
之后连续的几行是请求头,格式为 [HEADER]: [VALUE],如 Connection: keep-alive. 其值可以有多个
-
一个空行之后是请求体,可以是 json等文本或编码后的二进制数据。
以解析报文第一行为例:
可以使用正则表达式 (标准库的regexp
包) 匹配字符串模板,并使用捕获组提取字符。这里简单起见使用空格分割
// 传入首行字符串,返回请求参数,请求方法,请求uri
func loadMetadata(first string) metadata {
var parts = strings.Split(first, " ")
var method = parts[0]
var version string
var uri string
var params = make(map[string]string)
query := strings.Split(parts[1], "?")
uri = query[0]
if len(query) > 1 {
tmp := strings.Split(query[1], "&")
for _, param := range tmp {
peer := strings.Split(param, "=")
params[peer[0]] = peer[1]
}
}
version = parts[2]
return metadata{
params: params,
version: version,
method: method,
uri: uri,
}
}
完成请求处理
解析完报文后,实现对报文的处理和响应.
框架的实现过程中在处理请求时,需要经历下面的阶段:
- 解析报文生成 HttpRequest对象
- 解析路径uri,判断 1.返回静态资源 2.调用处理器函数 3.返回错误
- 将静态资源/处理器结果/错误信息 转为字节,封装响应报文
- 请求处理结束
以下是一个处理请求的生命周期示例
// c *client 是accept()套接字的封装对象
func do(c *client, s *HttpServer) {
defer func() {
if r := recover(); r != nil {
var resp Response
resp.SetError(500, "[500] Server error")
}
}()
defer c.conn.Close()
for {
req := c.decode()
path, resp, sendFile := c.router(req)
if req == nil {
resp.SetError(400, "[400] Bad request")
break
}
if !sendFile {
s.handle(path, req, resp)
} else {
// 拼接文件路径,读取文件
resp.sendFile(s.Resource + path)
}
err := c.encode(resp)
if err != nil {
println(err.Error())
break
}
}
}
测试
- 配置 config.yml
resource: /practice/static/
port: 8080
- 启动服务器并设置路由映射
func main() {
var server myserver.HttpServer
err := myserver.Init(&server)
if err == nil {
server.AddRouterFunc("/", router.Index)
server.AddRouterFunc("/test", router.TestRouter)
server.Run()
}
}
- 处理器函数示例
- 根路径返回 index.html文件
- 测试获取请求数据
func Index(request *myserver.Request, resp *myserver.Response) {
base, _ := os.Getwd()
file, err := os.Open(base + "\\practice\\static\\index.html")
if err != nil {
resp.SetError(404, "[404]: no index.html found")
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
resp.Write(scanner.Bytes())
}
}
func TestRouter(request *myserver.Request, resp *myserver.Response) {
println(request.GetURI())
println(request.GetMethod())
println(request.GetHeader("User-Agent"))
println(request.GetHeader("Connection"))
println(request.GetQueryParam("test"))
println(string(request.GetRequestBody()))
resp.Write([]byte("<h1>This is a test handler</h1>"))
}
本地测试
不足
- 仅仅是对明文协议进行了解析。无法支持 https 等加密协议
- 没有针对安全方面进行设计,如同源检查,请求参数注入等问题进行防范
- 没有结合传输层与应用层之间解复用的功能进行设计等等