golang web 服务器 request 与 response 处理

golang web 服务器 request 与 response 处理

介绍常见 web 服务的实现与输入、输出数据处理。包括:静态文件服务、js 请求支持、模板输出、表单处理、Filter 中间件设计。

    • golang web 服务器 request 与 response 处理
      • 一静态文件服务支持
      • 二使用模板输出
      • 三处理 Request
      • 四 negroni 与 Filter 模式

一、静态文件服务支持

访问 html 静态网页是 web 基础服务。

代码与文件结构:: github.com/pmlpml/golang-learning/web/cloudgo-static

1.1 使用文件服务

一般来说,生产环境静态文件的访问交给 WEB 服务器 Apache / Lighttpd / Nginx 处理。在开发、测试阶段也可以让 net/http 库处理。

server.go

package service

import (
    "net/http"
    "os"

    "github.com/codegangsta/negroni"
    "github.com/gorilla/mux"
    "github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

    formatter := render.New(render.Options{
        IndentJSON: true,
    })

    n := negroni.Classic()
    mx := mux.NewRouter()

    initRoutes(mx, formatter)

    n.UseHandler(mx)
    return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
    webRoot := os.Getenv("WEBROOT")
    if len(webRoot) == 0 {
        if root, err := os.Getwd(); err != nil {
            panic("Could not retrive working directory")
        } else {
            webRoot = root
            //fmt.Println(root)
        }
    }

    //mx.HandleFunc("/api/test", apiTestHandler(formatter)).Methods("GET")
    mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))

}

Go 的 net/http 包中提供了静态文件的服务,ServeFileFileServer 等函数。

首先我们需要在服务器上创建目录,以存放静态内容。例如:

assets(静态文件虚拟根目录)
  |-- js
  |-- images
  +-- css

仅一条语句就实现了 mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/"))) 静态文件服务。它的含义是将 path 以 “/” 前缀的 URL 都定位到 webRoot + "/assets/" 为虚拟根目录的文件系统。 有必要描述这语句中的函数:

  • http.Dir 是类型。将字符串转为 http.Dir 类型,这个类型实现了 FileSystem 接口。(Dir 不是函数)
  • http.FileServer() 是函数,返回 Handler 接口,该接口处理 http 请求,访问 root 的文件请求。
  • mx.PathPrefix 添加前缀路径路由。

创建 assets 目录,不要向该目录放任何内容, 运行程序 go run main.go

首先,用浏览器 http;//localhost:8080/ 访问。在缺少文件情况下,观察 FileServer 的行为。逐步添加 cssjs 等目录与文件,确认是否可以浏览文件。直到,根目录添加 index.html 看发生了什么?

问题:为什么会添加了 index.html 就显示网页呢? 自己跟踪代码!

1.2 支持 JavaScript 访问

随着 web 页面技术的进步,页面中大量使用 javascript。 添加一个服务:

apitest.go

package service

import (
    "net/http"

    "github.com/unrolled/render"
)

func apiTestHandler(formatter *render.Render) http.HandlerFunc {

    return func(w http.ResponseWriter, req *http.Request) {
        formatter.JSON(w, http.StatusOK, struct {
            ID      string `json:"id"`
            Content string `json:"content"`
        }{ID: "8675309", Content: "Hello from Go!"})
    }
}

这段代码非常简单,输出了一个 匿名结构 ,并 JSON (JavaScript Object Notation) 序列化输出。 打开前面程序的注释,运行网站并用 curl 测试输出!

$ curl http://localhost:8080/api/test
{
  "id": "8675309",
  "content": "Hello from Go!"
}

为了便于理解,课程给的案例非常简答。index.html 是

<html>
<head>
  <link rel="stylesheet" href="css/main.css"/>
  <script src="http://code.jquery.com/jquery-latest.js">script>
  <script src="js/hello.js">script>
head>
<body>
  <img src="images/cng.png" height="48" width="48"/>
  Sample Go Web Application!!
      <div>
          <p class="greeting-id">The ID is p>
          <p class="greeting-content">The content is p>
      div>
body>
html>

使用的 hello.js 是:

$(document).ready(function() {
    $.ajax({
        url: "/api/test"
    }).then(function(data) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
    });
});

通过 web 应用控制台 Negroni 输出追踪,获知网页使用 javascript 获取了信息。

现在,你应该可以顺利的与 VUE,Bootstrap 这些做的应用前端与 golang 集成在一起了!

问题: 交互路由两个语句位置,会发生什么?

1.3 处理静态路径前缀

在 web 应用中,部分应用会将所有静态文件访问路径用独立前缀,例如: http://localhost:8080/static/js/hello.js,这时路由如何设置呢?

mx.PathPrefix("/static").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))

显然,404 页面出现了。

正确的代码是:

mx.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(webRoot+"/assets/"))))

请自己研究 StripPrefix 的实现。好奇妙的 HandlerFunc 类型 ,把一个函数转为一个接口!

小练习

  1. 模仿 NotFound 函数实现,写一个 501 Not Implemented 函数 NotImplementedNotImplementedHandler
  2. 当用户访问 /api/unkown 时返回页面提示 501 Not Implemented

二、使用模板输出

由于 Angluar,React 等 web 前端框架的流行, web 服务器对模板的需求已经不是非常强烈。然而,它依然是格式化数据输出的一种重要手段。 熟悉 jinja2 模板的人对 go 的模板设计应非常亲切。

代码与文件结构:github.com/pmlpml/golang-learning/web/cloudgo-template

2.1 输出 html 页面

如果你仅是打算输出 html 页面,github.com/unrolled/render 是最为简单和直接。

srver.go

package service

import (
    "net/http"
    "os"

    "github.com/codegangsta/negroni"
    "github.com/gorilla/mux"
    "github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

    formatter := render.New(render.Options{
        Directory:  "templates",
        Extensions: []string{".html"},
        IndentJSON: true,
    })

    n := negroni.Classic()
    mx := mux.NewRouter()

    initRoutes(mx, formatter)

    n.UseHandler(mx)
    return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
    webRoot := os.Getenv("WEBROOT")
    if len(webRoot) == 0 {
        if root, err := os.Getwd(); err != nil {
            panic("Could not retrive working directory")
        } else {
            webRoot = root
            //fmt.Println(root)
        }
    }

    mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
    mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
}

要点:
(1)formatter 构建,指定了模板的目录,模板文件的扩展名
(2)homeHandler 使用了模板

我们在当前目录下,建立了 assetstemplates 目录。 index.html 在 templates 目录中

<html>
<head>
  <link rel="stylesheet" href="css/main.css"/>
head>
<body>
  <img src="images/cng.png" height="48" width="48"/>
  Sample Go Web Application!!
      <div>
          <p class="greeting-id">The ID is {{.ID}}p>
          <p class="greeting-content">The content is {{.Content}}p>
      div>
body>
html>

其中 {{.}} 表示数据填充位置。 {{.ID}} 表示该数据的 ID 属性。

home.go 处理了数据填充。

package service

import (
    "net/http"

    "github.com/unrolled/render"
)

func homeHandler(formatter *render.Render) http.HandlerFunc {

    return func(w http.ResponseWriter, req *http.Request) {
        formatter.HTML(w, http.StatusOK, "index", struct {
            ID      string `json:"id"`
            Content string `json:"content"`
        }{ID: "8675309", Content: "Hello from Go!"})
    }
}

我们使用 formatter 的 HTML 直接将数据注入模板,并输出到浏览器。 更多内容见 render 在 git 上的 README。

运行程序:$ go run main.go

在浏览器访问:http://localhost:8080

注意观察控制台 negroni 输出。

  • 访问 “/” 第一次与后续访问时间的差异
  • 访问 css 与 js 的返回状态, 为什么是 304 ?

2.2 使用 text/template 库

golang 提供了强大的 template 库,上述 Render 仅是它的简单包装。 官网的例子也非常简单:

type Inventory struct {
    Material string
    Count    uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }

这里给出了模板使用的过程:创建 - 编译 - 执行。 即 template.New("name").Parse("{{.Content}} string").Execute(writer,data)

问题1New("name") 为什么需要 name ? 如果程序中多处创建同名模板但内容不一样,有问题吗?

阅读 text/template API 文档,解释输出

    t := template.Must(template.New("letter").Parse("A{{.}}\n"))
    t1 := template.Must(t.New("letter1").Parse("B{{.}}\n"))
    t.Execute(os.Stdout, "1")
    t1.Execute(os.Stdout, "2")
    fmt.Println(len(t1.Templates()))
    for _, tt := range t1.Templates() {
        fmt.Println(tt.Name())
    }

这段代码的输出是?…

如果 letter1 改为 letter 呢?
如果 t.New 改为 template.New 呢?

问题2:如果 ParseFiles(...) 文件模板都被缓存,如何才能实现模板热更新(运行时检测文件更新)?

注意:网上很多文章使用模板的方法都是不正确的,应该用 ParseFilesParseGlob 获得一个模板 set 的第一个元素。具体参考 formatter.HTML 的实现。

简明了解模板的语法:golang模板语法简明教程
代码与实例:Go语言核心之美 3.6-template模版

看完后,请完成:

  • 输出一个 html 报表。 表格头是 “手机销量,Q1,Q2,Q3,Q3同比,Q3比上季”,数据是“华为,OPPO,小米 ,OV,Apple”
  • 使用自定义格式化函数,格式化百分数

三、处理 Request

3.1 request 定义

https://go-zh.org/pkg/net/http/#Request

要点:

  • 与 http 协议的关系

3.2 如何处理表单

https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/04.0.md

补充:

  • UUID 的生成。 建议: https://github.com/satori/go.uuid
  • 将表单 map 映射到 struct
    • 建议写静态函数实现,例如 return &MyStruct{map["xx"]}
    • 不追求性能,只考虑方便。 例如: http://www.gorillatoolkit.org/pkg/schema

四、 negroni 与 Filter 模式

java web 有三大神器 Filter、Servlet、Listener。

  • Servlet 称为服务小程序,处理 request,产生 response。 golang 的 net/http 的 Handler、HandlerFunc 以及 第三方 http mux 或 http router 包都完成这类功能
  • Filter 称为过滤器或拦截器,处理 request 预处理 或 response 后处理,它常用的用途是 处理输入、输出的语言编码、日志、用户权限管理、session管理等等。它采用 Filter Chain 将一组 Filter 组件联系起来。 golang negroni 完成该工作。
  • Listener 称为侦听器,用户感知 web 上下文(Context)变化,并通知用户。 golang ?
    • application
    • session
    • request

对比 servlet: Java Servlet pk Golang Handler

对比 Filter: Java Filter pk negroni Handler

java 文档给出了常用的 Filter!

4.1 Filter 原理

golang web 服务器 request 与 response 处理_第1张图片

go negroni “中间件”模板代码:

func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  // do some stuff before
  next(rw, r)
  // do some stuff after
}

4.2 negroni组件

  • 使用 negroni : https://github.com/urfave/negroni/blob/master/translations/README_zh_CN.md

  • negroni classic 的实现:

// Classic returns a new Negroni instance with the default middleware already
// in the stack.
//
// Recovery - Panic Recovery Middleware
// Logger - Request/Response Logging
// Static - Static File Serving
func Classic() *Negroni {
    return New(NewRecovery(), NewLogger(), NewStatic(http.Dir("public")))
}
  • 建立标准的意义 - negroni 第三方扩展

https://github.com/urfave/negroni#third-party-middleware

例如,让 web 支持 gzip 协议:

https://github.com/phyber/negroni-gzip

注意,如果要支持静态文件压缩,请注意顺序!

你可能感兴趣的:(golang)