动手实现一个简易的 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&param2=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>"))  
}  

本地测试

img.png

img_2.png

img_3.png

不足

  • 仅仅是对明文协议进行了解析。无法支持 https 等加密协议
  • 没有针对安全方面进行设计,如同源检查,请求参数注入等问题进行防范
  • 没有结合传输层与应用层之间解复用的功能进行设计等等
全部评论

相关推荐

沉淀一会:**圣经 1.同学你面试评价不错,概率很大,请耐心等待;2.你的排名比较靠前,不要担心,耐心等待;3.问题不大,正在审批,不要着急签其他公司,等等我们!4.预计9月中下旬,安心过节;5.下周会有结果,请耐心等待下;6.可能国庆节前后,一有结果我马上通知你;7.预计10月中旬,再坚持一下;8.正在走流程,就这两天了;9.同学,结果我也不知道,你如果查到了也告诉我一声;10.同学你出线不明朗,建议签其他公司保底!11.同学你找了哪些公司,我也在找工作。
点赞 评论 收藏
分享
沉淀一会:1.同学你面试评价不错,概率很大,请耐心等待; 2.你的排名比较靠前,不要担心,耐心等待; 3.问题不大,正在审批,不要着急签其他公司,等等我们! 4.预计9月中下旬,安心过节; 5.下周会有结果,请耐心等待下; 6.可能国庆节前后,一有结果我马上通知你; 7.预计10月中旬,再坚持一下; 8.正在走流程,就这两天了; 9.同学,结果我也不知道,你如果查到了也告诉我一声; 10.同学你出线不明朗,建议签其他公司保底! 11.同学你找了哪些公司,我也在找工作。
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务