设计一个 web 小应用,展示静态文件服务、js 请求支持、模板输出、表单处理、Filter 中间件设计等方面的能力。(不需要数据库支持)
编程 web 应用程序 cloudgo-io。 请在项目 README.MD 给出完成任务的证据!
基本要求
提高要求,以下任务可以选择完成:
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的网站文档上,列出了该框架具备的一些特点和框架特性,列举如下:
除此之外还有很多特性可以参考官方文档。
配置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 小应用。
在服务实现的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(`/public/css/main.css
/public/img/test.jpg
/public/js/showStatic.js`)
})
}
实现了一个简单的点击按钮跳转界面的功能,即点击后跳转到public界面,js文件如下:
function myfunction()
{
window.open("/public")
}
然后再html文件中设置点击事件:
js test: <input type="button" onclick="myfunction()" value="request">
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加载指定文件的视图。
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过滤器的源码如下:
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框架,并将他应用在了作业之中,很好地完成了作业内容。