服务计算--处理 web 程序的输入与输出

一、概述

设计一个 web 小应用,展示静态文件服务、js 请求支持、模板输出、表单处理、Filter 中间件设计等方面的能力。(不需要数据库支持)

二、任务要求

编程 web 应用程序 cloudgo-io。 请在项目 README.MD 给出完成任务的证据!

基本要求

  • 支持静态文件服务
  • 支持简单 js 访问
  • 提交表单,并输出一个表格
  • 对 /unknown 给出开发中的提示,返回码 5xx

提高要求,以下任务可以选择完成:

  • 分析阅读 gzip 过滤器的源码(就一个文件 126 行)
  • 编写中间件,使得用户可以使用 gb2312 或 gbk 字符编码的浏览器提交表单、显示网页。(服务器模板统一用 utf-8)

三、运行结果

github地址:cloudgo-io
在这里插入图片描述
接着在浏览器中输入:

http://localhost:yourport/login
http://localhost:yourport/public
http://localhost:yourport/unknown

得到结果:
在这里插入图片描述
输入username及password,点击login出现info界面
在这里插入图片描述

点击request或直接输入public得到public界面,完成支持静态文件服务以及简单 js 访问以及。在这里插入图片描述
点击每个选项得到:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后unknown界面
在这里插入图片描述
这样四个要求就全部完成了。

四、实现

首先是框架的安装,我选择iris框架:
Iris是一款Go语言中用来开发web应用的框架,该框架支持编写一次并在任何地方以最小的机器功率运行,如Android、ios、Linux和Windows等。该框架只需要一个可执行的服务就可以在平台上运行了。
Iris框架以简单而强大的api而被开发者所熟悉。iris除了为开发者提供非常简单的访问方式外,还同样支持MVC。另外,用iris构建微服务也很容易。
在iris框架的官方网站上,被称为速度最快的Go后端开发框架。在Iris的网站文档上,列出了该框架具备的一些特点和框架特性,列举如下:

  1. 聚焦高性能
  2. 健壮的静态路由支持和通配符子域名支持
  3. 视图系统支持超过5以上模板
  4. 支持定制事件的高可扩展性Websocket API
  5. 带有GC, 内存 & redis 提供支持的会话
  6. 方便的中间件和插件
  7. 完整 REST API
  8. 能定制 HTTP 错误
  9. 源码改变后自动加载

除此之外还有很多特性可以参考官方文档。
配置iris:

go get -u github.com/kataras/iris

在这里插入图片描述
可以看到缺少net、crypto等相关库,需要我们在golang.org/x/下手动在github上clone下来:
在这里插入图片描述
在这里插入图片描述
相关的库安装好以后我们就成功配置iris了。
接着通过一个小样例对iris进行测试:

package main

import "github.com/kataras/iris"

func main() {
    app := iris.Default()
    app.Get("/ping", func(ctx iris.Context) {
        ctx.JSON(iris.Map{
            "message": "pong",
        })
    })
    // listen and serve on http://0.0.0.0:8080.
    app.Run(iris.Addr(":8080"))
}

得到结果:
在这里插入图片描述
可以看到运行成功。

接着就是通过iris实现web 小应用。

  1. 静态文件服务

在服务实现的Service.go中调用函数app.HandleDir()

func LoadResources(app *iris.Application) {
	app.RegisterView(iris.HTML("./templates", ".html").Reload(true))
	app.HandleDir("/public", "./static")
}

其中第一个参数是一个虚拟路径,第二个参数是系统路径。虚拟路径是提供给用户的,无论系统内路径如何改变,只要用户输入local:8080/public即可进行访问。可以避免系统路径改变导致用户的访问路径也要改变的麻烦。
然后为了方便,直接在go代码中简单写入html,即实现了public静态的界面

func GetStaticPage(app *iris.Application) {
	app.Get("/public", func(ctx iris.Context) {
		ctx.HTML(`<a href='/public/css/main.css'>/public/css/main.css</a><br/><br/>
			<a href='/public/img/test.jpg'>/public/img/test.jpg</a><br/><br/>
			<a href='/public/js/showStatic.js'>/public/js/showStatic.js</a>`)
	})
}
  1. 简单 js 访问

实现了一个简单的点击按钮跳转界面的功能,即点击后跳转到public界面,js文件如下:

function myfunction()
{
    window.open("/public")
}

然后再html文件中设置点击事件:

 js test: <input type="button" onclick="myfunction()" value="request">
  1. 提交表单,并输出一个表格
    实现如下:
type User struct {
	Username string
	Password string
}

func GetInfoPage(app *iris.Application) {
	app.Post("/info", func(ctx iris.Context) {
		form := User{}
		err := ctx.ReadForm(&form)
		if err != nil {
			ctx.StatusCode(iris.StatusInternalServerError)
			ctx.WriteString(err.Error())
		}
		fmt.Println(form)
		username := form.Username
		password := form.Password
		ctx.ViewData("username", username)
		ctx.ViewData("password", password)
		ctx.View("info.html")
	})
}

通过ReadForm函数读取POST过来的表单,并绑定模板对视图进行填充。ViewData填充数据使用键值对即可,第一个参数就是info.html里面定义的模板,第二个参数就是要替换的值。View加载指定文件的视图。

  1. 对 /unknown 给出开发中的提示,返回码 5xx
    实现如下:
func NotImplement(app *iris.Application) {
	app.Get("/unknown", func(ctx iris.Context) {
		ctx.StatusCode(501)
		ctx.JSON(iris.Map{
			"error": "501 not implement error",
		})
	})
}

与上述实现类似,不过需要通过app.StatusCode设置一个返回码让用户知道是什么原因导致的页面无效。在作业中返回码设置为5xx。

五、提高要求:分析阅读 gzip 过滤器的源码

gzip过滤器的源码如下:
https://github.com/phyber/negroni-gzip/blob/master/gzip/gzip.go
源码分析:
首先是相关变量的声明:

// These compression constants are copied from the compress/gzip package.
const (
	encodingGzip = "gzip"

	headerAcceptEncoding  = "Accept-Encoding"
	headerContentEncoding = "Content-Encoding"
	headerContentLength   = "Content-Length"
	headerContentType     = "Content-Type"
	headerVary            = "Vary"
	headerSecWebSocketKey = "Sec-WebSocket-Key"

	BestCompression    = gzip.BestCompression
	BestSpeed          = gzip.BestSpeed
	DefaultCompression = gzip.DefaultCompression
	NoCompression      = gzip.NoCompression
)

各参数的默认值为:NoCompression = 0;BestSpeed = 1;BestCompression = 9,其中压缩level不能超过BestCompression;DefaultCompression = -1

接着是结构体gzipResponseWriter的声明:

// gzipResponseWriter is the ResponseWriter that negroni.ResponseWriter is
// wrapped in.
type gzipResponseWriter struct {
	w *gzip.Writer
	negroni.ResponseWriter
	wroteHeader bool
}

结构体gzipResponseWriter包括:一个gzip.Writer的指针变量,一个negroni.ResponseWriter以及一个布尔变量wroteHeader,用于记录response是否已经编码。

然后是WriteHeader函数:

// Check whether underlying response is already pre-encoded and disable
// gzipWriter before the body gets written, otherwise encoding headers
func (grw *gzipResponseWriter) WriteHeader(code int) {
	headers := grw.ResponseWriter.Header()
	if headers.Get(headerContentEncoding) == "" {
		headers.Set(headerContentEncoding, encodingGzip)
		headers.Add(headerVary, headerAcceptEncoding)
	} else {
		grw.w.Reset(ioutil.Discard)
		grw.w = nil
	}

	// Avoid sending Content-Length header before compression. The length would
	// be invalid, and some browsers like Safari will report
	// "The network connection was lost." errors
	grw.Header().Del(headerContentLength)

	grw.ResponseWriter.WriteHeader(code)
	grw.wroteHeader = true
}

返回值为结构体gzipResponseWriter,作用是检验响应是否预编码,如果目标页面的响应内容未预编码,采用gzip压缩方式压缩后再发送到客户端,同时设置Content-Encoding实体报头值为gzip,否则的话就在写入之前令gzipWriter失效,即使用grw.w.Reset(ioutil.Discard)。
然后是Write函数:

// Write writes bytes to the gzip.Writer. It will also set the Content-Type
// header using the net/http library content type detection if the Content-Type
// header was not set yet.
func (grw *gzipResponseWriter) Write(b []byte) (int, error) {
	if !grw.wroteHeader {
		grw.WriteHeader(http.StatusOK)
	}
	if grw.w == nil {
		return grw.ResponseWriter.Write(b)
	}
	if len(grw.Header().Get(headerContentType)) == 0 {
		grw.Header().Set(headerContentType, http.DetectContentType(b))
	}
	return grw.w.Write(b)
}

返回值为结构体gzipResponseWriter,写入步骤如下:1.若报头未写入则调用WriteHeader()写入;若gzipWriter未空,说明不gzip压缩,那么ResponseWriter写入数据;若报头未设置 那么久通过net/http库函数自动检测内容类型设置;最后gzipWriter写入数据。

然后是gzipResponseWriterCloseNotifier结构体以及相关的CloseNotify函数,newGzipResponseWriter函数:

func (rw *gzipResponseWriterCloseNotifier) CloseNotify() <-chan bool {
	return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

func newGzipResponseWriter(rw negroni.ResponseWriter, w *gzip.Writer) negroni.ResponseWriter {
	wr := &gzipResponseWriter{w: w, ResponseWriter: rw}

	if _, ok := rw.(http.CloseNotifier); ok {
		return &gzipResponseWriterCloseNotifier{gzipResponseWriter: wr}
	}

	return wr
}

CloseNotify函数的作用是当客户端与服务器的连接断开时,调用CloseNotify()及时关闭信道,可以在服务端响应之前取消二者之间的长连接。newGzipResponseWriter函数的作用是新开辟GzipResponseWriter的空间。

然后是结构体handler以及Gzip函数:

// handler struct contains the ServeHTTP method
type handler struct {
	pool sync.Pool
}

// Gzip returns a handler which will handle the Gzip compression in ServeHTTP.
// Valid values for level are identical to those in the compress/gzip package.
func Gzip(level int) *handler {
	h := &handler{}
	h.pool.New = func() interface{} {
		gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
		if err != nil {
			panic(err)
		}
		return gz
	}
	return h
}

handler结构体中包括了一个临时对象池 pool,用于存储那些被分配了但是没有被使用,而未来可能会使用的gzip对象,以减小回收的压力。Gzip函数返回了一个处理gzip压缩的handler,新建了一个gzip,其中设置writer的默认值为不可用,设置level。
最后是ServeHTTP函数:

// ServeHTTP wraps the http.ResponseWriter with a gzip.Writer.
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	// Skip compression if the client doesn't accept gzip encoding.
	if !strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) {
		next(w, r)
		return
	}

	// Skip compression if client attempt WebSocket connection
	if len(r.Header.Get(headerSecWebSocketKey)) > 0 {
		next(w, r)
		return
	}

	// Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
	// This allows us to re-use an already allocated buffer rather than
	// allocating a new buffer for every request.
	// We defer g.pool.Put here so that the gz writer is returned to the
	// pool if any thing after here fails for some reason (functions in
	// next could potentially panic, etc)
	gz := h.pool.Get().(*gzip.Writer)
	defer h.pool.Put(gz)
	gz.Reset(w)

	// Wrap the original http.ResponseWriter with negroni.ResponseWriter
	// and create the gzipResponseWriter.
	nrw := negroni.NewResponseWriter(w)
	grw := newGzipResponseWriter(nrw, gz)

	// Call the next handler supplying the gzipResponseWriter instead of
	// the original.
	next(grw, r)

	gz.Close()
}

ServeHTTP函数的作用是处理handler中压缩请求的函数,具体步骤为:首先判断请求的头部是否有“Accept-Encoding”一栏,若没有,则说明客户端不接受压缩的响应,那我们就不能压缩响应的报文,直接调用下一个中间件处理该请求;其次判断请求的头部是否有“Sec-WebSocket-Key”一栏,如果有,代表客户端想要进行长连接,那也不能压缩,调用下一个中间件处理该请求。最后创建gzipResponseWriter,进行压缩处理,关闭gzipwriter。
附:gzip完整源代码

// Package gzip implements a gzip compression handler middleware for Negroni.
package gzip

import (
	"compress/gzip"
	"io/ioutil"
	"net/http"
	"strings"
	"sync"

	"github.com/urfave/negroni"
)

// These compression constants are copied from the compress/gzip package.
const (
	encodingGzip = "gzip"

	headerAcceptEncoding  = "Accept-Encoding"
	headerContentEncoding = "Content-Encoding"
	headerContentLength   = "Content-Length"
	headerContentType     = "Content-Type"
	headerVary            = "Vary"
	headerSecWebSocketKey = "Sec-WebSocket-Key"

	BestCompression    = gzip.BestCompression
	BestSpeed          = gzip.BestSpeed
	DefaultCompression = gzip.DefaultCompression
	NoCompression      = gzip.NoCompression
)

// gzipResponseWriter is the ResponseWriter that negroni.ResponseWriter is
// wrapped in.
type gzipResponseWriter struct {
	w *gzip.Writer
	negroni.ResponseWriter
	wroteHeader bool
}

// Check whether underlying response is already pre-encoded and disable
// gzipWriter before the body gets written, otherwise encoding headers
func (grw *gzipResponseWriter) WriteHeader(code int) {
	headers := grw.ResponseWriter.Header()
	if headers.Get(headerContentEncoding) == "" {
		headers.Set(headerContentEncoding, encodingGzip)
		headers.Add(headerVary, headerAcceptEncoding)
	} else {
		grw.w.Reset(ioutil.Discard)
		grw.w = nil
	}

	// Avoid sending Content-Length header before compression. The length would
	// be invalid, and some browsers like Safari will report
	// "The network connection was lost." errors
	grw.Header().Del(headerContentLength)

	grw.ResponseWriter.WriteHeader(code)
	grw.wroteHeader = true
}

// Write writes bytes to the gzip.Writer. It will also set the Content-Type
// header using the net/http library content type detection if the Content-Type
// header was not set yet.
func (grw *gzipResponseWriter) Write(b []byte) (int, error) {
	if !grw.wroteHeader {
		grw.WriteHeader(http.StatusOK)
	}
	if grw.w == nil {
		return grw.ResponseWriter.Write(b)
	}
	if len(grw.Header().Get(headerContentType)) == 0 {
		grw.Header().Set(headerContentType, http.DetectContentType(b))
	}
	return grw.w.Write(b)
}

type gzipResponseWriterCloseNotifier struct {
	*gzipResponseWriter
}

func (rw *gzipResponseWriterCloseNotifier) CloseNotify() <-chan bool {
	return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

func newGzipResponseWriter(rw negroni.ResponseWriter, w *gzip.Writer) negroni.ResponseWriter {
	wr := &gzipResponseWriter{w: w, ResponseWriter: rw}

	if _, ok := rw.(http.CloseNotifier); ok {
		return &gzipResponseWriterCloseNotifier{gzipResponseWriter: wr}
	}

	return wr
}

// handler struct contains the ServeHTTP method
type handler struct {
	pool sync.Pool
}

// Gzip returns a handler which will handle the Gzip compression in ServeHTTP.
// Valid values for level are identical to those in the compress/gzip package.
func Gzip(level int) *handler {
	h := &handler{}
	h.pool.New = func() interface{} {
		gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
		if err != nil {
			panic(err)
		}
		return gz
	}
	return h
}

// ServeHTTP wraps the http.ResponseWriter with a gzip.Writer.
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	// Skip compression if the client doesn't accept gzip encoding.
	if !strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) {
		next(w, r)
		return
	}

	// Skip compression if client attempt WebSocket connection
	if len(r.Header.Get(headerSecWebSocketKey)) > 0 {
		next(w, r)
		return
	}

	// Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
	// This allows us to re-use an already allocated buffer rather than
	// allocating a new buffer for every request.
	// We defer g.pool.Put here so that the gz writer is returned to the
	// pool if any thing after here fails for some reason (functions in
	// next could potentially panic, etc)
	gz := h.pool.Get().(*gzip.Writer)
	defer h.pool.Put(gz)
	gz.Reset(w)

	// Wrap the original http.ResponseWriter with negroni.ResponseWriter
	// and create the gzipResponseWriter.
	nrw := negroni.NewResponseWriter(w)
	grw := newGzipResponseWriter(nrw, gz)

	// Call the next handler supplying the gzipResponseWriter instead of
	// the original.
	next(grw, r)

	gz.Close()
}

六、实验总结与心得

这次作业实现了设计一个 web 小应用,并实现了静态文件服务、js 请求支持、模板输出、表单处理等多种功能。在这期间我学习了iris框架,并将他应用在了作业之中,很好地完成了作业内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值