赞
踩
http
是比 tcp
更高层的协议,它描述了网页服务器如何与客户端浏览器进行通信。Go
提供了 net/http
包,我们马上就来看下。
我们引入了 http
包并启动了网页服务器,和之前的 net.Listen("tcp", "localhost:50000")
函数的 tcp
服务器是类似的,使用 http.ListenAndServe("localhost:8080", nil)
函数,如果成功会返回空,否则会返回一个错误(地址 localhost
部分可以省略,8080
是指定的端口号)。
//第二个参数是关于请求和对应处理函数的映射器的
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
http.URL
用于表示网页地址,其中字符串属性 Path
用于保存 url
的路径;http.Request
描述了客户端请求,内含一个 URL
字段。
如果 req
是来自 html
表单的 POST
类型请求,“var1”
是该表单中一个输入域的名称,那么用户输入的值就可以通过 Go
代码 req.FormValue("var1")
获取到。
还有一种方法是先执行 request.ParseForm()
,然后再获取 request.Form["var1"]
的第一个返回参数,就像这样:
var1, found := request.Form["var1"]
第二个参数 found
为 true
。如果 var1
并未出现在表单中,found
就是 false
。
表单属性实际上是 map[string][]string
类型。网页服务器发送一个 http.Response
响应,它是通过 http.ResponseWriter
对象输出的,后者组装了 HTTP
服务器响应,通过对其写入内容,我们就将数据发送给了 HTTP
客户端。
现在我们仍然要编写程序,以实现服务器必须做的事,即如何处理请求。这是通过 http.HandleFunc
函数完成的。在这个例子中,当根路径“/”(url地址是 http://localhost:8080)
被请求的时候(或者这个服务器上的其他任意地址),HelloServer
函数就被执行了。这个函数是 http.HandlerFunc
类型的,它们通常被命名为 Prefhandler
,和某个路径前缀 Pref
匹配。
http.HandleFunc
注册了一个处理函数(这里是 HelloServer
)来处理对应 /
的请求。
/
可以被替换为其他更特定的 url
,比如 /create,/edit
等等;你可以为每一个特定的 url
定义一个单独的处理函数。
这个函数需要两个参数:第一个是 ReponseWriter
类型的 w
;第二个是请求 req
。程序向 w
写入了 Hello
和 r.URL.Path[1:]
组成的字符串:末尾的 [1:]
表示“创建一个从索引为 1
的字符到结尾的子切片”,用来丢弃路径开头的“/”
,fmt.Fprintf()
函数完成了本次写入;另一种可行的写法是 io.WriteString(w, "hello, world!\n")
。
总结:第一个参数是请求的路径,第二个参数是当路径被请求时,需要调用的处理函数的引用。
package main import ( "fmt" "log" "net/http" ) func HelloServer(w http.ResponseWriter, req *http.Request) { fmt.Println("Inside HelloServer handler") fmt.Fprintf(w, "Hello,"+req.URL.Path[1:]) } func main() { http.HandleFunc("/", HelloServer) err := http.ListenAndServe("localhost:8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } }
使用命令行启动程序,会打开一个命令窗口显示如下文字:
Starting Process E:/Go/GoBoek/code_examples/chapter_14/hello_world_webserver.exe...
然后打开浏览器并输入 url
地址:http://localhost:8080/world
,浏览器就会出现文字:Hello, world
,网页服务器会响应你在 :8080/
后边输入的内容。
fmt.Println
在服务器端控制台打印状态;在每个处理函数被调用时,把请求记录下来也许更为有用。
注: 1)前两行(没有错误处理代码)可以替换成以下写法:
http.ListenAndServe(":8080", http.HandlerFunc(HelloServer))
2)fmt.Fprint
和 fmt.Fprintf
都是可以用来写入 http.ResponseWriter
的函数(他们实现了 io.Writer
)。 比如我们可以使用
fmt.Fprintf(w, "<h1>%s<h1><div>%s</div>", title, body)
来构建一个非常简单的网页并插入 title
和 body
的值。
如果你需要更多复杂的替换,使用模板包
3)如果你需要使用安全的 https
连接,使用 http.ListenAndServeTLS()
代替 http.ListenAndServe()
4)除了 http.HandleFunc("/", Hfunc)
,其中的 HFunc
是一个处理函数,签名为:
func HFunc(w http.ResponseWriter, req *http.Request) {
...
}
也可以使用这种方式:http.Handle("/", http.HandlerFunc(HFunc))
HandlerFunc
只是定义了上述 HFunc
签名的别名:
type HandlerFunc func(ResponseWriter, *Request)
它是一个可以把普通的函数当做 HTTP
处理器(Handler)
的适配器。如果函数 f
声明的合适,HandlerFunc(f)
就是一个执行 f
函数的 Handler
对象。
http.Handle
的第二个参数也可以是 T
类型的对象 obj:http.Handle("/", obj)。
如果 T
有 ServeHTTP
方法,那就实现了http
的 Handler
接口:
func (obj *Typ) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
}
这个用法也在 Counter 和 Chan 类型上使用。只要实现了 http.Handler,http 包就可以处理任何 HTTP 请求.
package main import ( "fmt" "io" "log" "net/http" "time" ) type Stu struct { mappings map[string]func(w http.ResponseWriter, req *http.Request) } func NewStu() *Stu { return &Stu{mappings: make(map[string]func(w http.ResponseWriter, req *http.Request), 3)} } func (s *Stu) ServeHTTP(w http.ResponseWriter, req *http.Request) { extractPrefixUrl := req.URL.Path[5:] handleFunc := s.mappings[extractPrefixUrl] if handleFunc == nil { io.WriteString(w, "<h1>404 NOT FOUND TARGET HANDLE FUNC</h1>") return } handleFunc(w, req) } func (*Stu) delete(w http.ResponseWriter, req *http.Request) { w.Write([]byte("<h1>删除学生</h1>")) } func (*Stu) put(w http.ResponseWriter, req *http.Request) { w.Write([]byte("<h1>修改学生</h1>")) } func (*Stu) post(w http.ResponseWriter, req *http.Request) { w.Write([]byte("<h1>新增学生</h1>")) } func main() { registerMappings() err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } } //registerMappings 注册请求和处理器的映射 func registerMappings() { http.HandleFunc("/", DefaultHandle) stu := NewStu() stu.mappings["delete"] = stu.delete stu.mappings["put"] = stu.put stu.mappings["post"] = stu.post http.Handle("/stu/", stu) } func DefaultHandle(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "<h1>默认首页</h1><h3>当前请求为: %s</h3><h3>当前系统时间: %s</h3>", req.URL, time.Now().Format("2006-01-02 15:04:05")) }
我们使用 http.Get() 获取并显示网页内容; Get 返回值中的 Body 属性包含了网页内容,然后我们用 ioutil.ReadAll 把它读出来:
package main import ( "fmt" "io" "log" "net/http" ) func main() { response, err := http.Get("https://www.baidu.com/") if err != nil { checkError(err) return } bytes, err := io.ReadAll(response.Body) if err != nil { checkError(err) return } fmt.Println(string(bytes)) } func checkError(err error) { if err != nil { log.Fatalf("Get : %v", err) } }
http 包中的其他重要的函数:
http.Redirect(w ResponseWriter, r *Request, url string, code int)
:这个函数会让浏览器重定向到 url(可以是基于请求 url 的相对路径),同时指定状态码。http.NotFound(w ResponseWriter, r *Request)
:这个函数将返回网页没有找到,HTTP 404错误。http.Error(w ResponseWriter, error string, code int)
:这个函数返回特定的错误信息和 HTTP 代码。http.Request
对象 req
的重要属性:req.Method
,这是一个包含 GET
或 POST
字符串,用来描述网页是以何种方式被请求的。go为所有的HTTP状态码定义了常量,比如:
http.StatusContinue = 100
http.StatusOK = 200
http.StatusFound = 302
http.StatusBadRequest = 400
http.StatusUnauthorized = 401
http.StatusForbidden = 403
http.StatusNotFound = 404
http.StatusInternalServerError = 500
你可以使用 w.header().Set("Content-Type", "../..")
设置头信息。
比如在网页应用发送 html
字符串的时候,在输出之前执行 w.Header().Set("Content-Type", "text/html")
(通常不是必要的)。
content-type
会让浏览器认为它可以使用函数 http.DetectContentType([]byte(form)
) 来处理收到的数据。
当网页应用的处理函数发生 panic,服务器会简单地终止运行。这可不妙:网页服务器必须是足够健壮的程序,能够承受任何可能的突发问题。
首先能想到的是在每个处理函数中使用 defer/recover,不过这样会产生太多的重复代码。
使用闭包的错误处理模式是更优雅的方案。我们把这种机制应用到前一章的简单网页服务器上。实际上,它可以被简单地应用到任何网页服务器程序中。
为增强代码可读性,我们为页面处理函数创建一个类型:
type HandleFnc func(http.ResponseWriter, *http.Request)
我们的错误处理函数应用了闭包处理错误的模式,成为 logPanics 函数:
func logPanics(function HandleFnc) HandleFnc {
return func(writer http.ResponseWriter, request *http.Request) {
defer func() {
if x := recover(); x != nil {
log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
}
}()
function(writer, request)
}
}
然后我们用 logPanics 来包装对处理函数的调用:
http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
处理函数现在可以恢复 panic 调用
实例演示:
package main import ( "io" "log" "net/http" ) const form = `<html><body><form action="#" method="post" name="bar"> <input type="text" name="in"/> <input type="submit" value="Submit"/> </form></html></body>` type HandleFnc func(http.ResponseWriter, *http.Request) func SimpleServer(w http.ResponseWriter, request *http.Request) { io.WriteString(w, "<h1>hello, world</h1>") } func FormServer(w http.ResponseWriter, request *http.Request) { w.Header().Set("Content-Type", "text/html") switch request.Method { case "GET": io.WriteString(w, form) case "POST": //request.ParseForm(); //io.WriteString(w, request.Form["in"][0]) io.WriteString(w, request.FormValue("in")) } } func main() { http.HandleFunc("/test1", logPanics(SimpleServer)) http.HandleFunc("/test2", logPanics(FormServer)) if err := http.ListenAndServe(":8088", nil); err != nil { panic(err) } } func logPanics(function HandleFnc) HandleFnc { return func(writer http.ResponseWriter, request *http.Request) { defer func() { if x := recover(); x != nil { log.Printf("[%v] caught panic: %v", request.RemoteAddr, x) } }() function(writer, request) } }
考虑到例子的复杂性,这里先将例子进行拆分,一点点看:
func main() { //注册相关映射器 http.Handle("/", http.HandlerFunc(Logger)) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } } func Logger(w http.ResponseWriter, req *http.Request) { //打印请求访问的URL log.Print(req.URL.String()) //输出 “404 Not Found”头部。 w.WriteHeader(404) //响应体填充oops w.Write([]byte("oops")) }
此项技术通常很有用,无论何时服务器执行代码产生错误,都可以应用类似这样的代码:
if err != nil {
w.WriteHeader(400)
return
}
另外利用 logger 包的函数,针对每个请求在服务器端命令行打印日期、时间和 URL。
它会在 HTTP URL /debug/vars 上以 JSON 格式公布。通常它被用于服务器操作计数。
这其实SpringBoot Actuator功能类似,提供相关系统运行时参数,默认提供了很多默认监控端点,我们也可以通过expvar 包来加入我们自定义的监控端点.
实例演示:
package main import ( "expvar" "io" "log" "net/http" ) //暴露出我们自定义的监控端点 var helloRequests = expvar.NewInt("hello-requests") func main() { http.Handle("/go/hello", http.HandlerFunc(HelloServer)) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } } func HelloServer(w http.ResponseWriter, req *http.Request) { helloRequests.Add(1) io.WriteString(w, "hello, world!\n") }
每当我们访问一次/go/hello请求,helloRequests计数就会增加,我们在监控页面就可以实时查询到。
计数器对象 ctr 有一个 String() 方法,所以它实现了 expvar.Var 接口。这使其可以被发布,尽管它是一个结构体。ServeHTTP 函数使 ctr 成为处理器,因为它的签名正确实现了 http.Handler 接口。
package main import ( "bytes" "expvar" "fmt" "io" "log" "net/http" "strconv" ) type Counter struct { n int } func main() { ctr := new(Counter) expvar.Publish("counter", ctr) http.Handle("/counter", ctr) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } } func (ctr *Counter) String() string { return fmt.Sprintf("%d", ctr.n) } func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.Method { //计算GET请求的次数 case "GET": ctr.n++ case "POST": //拿到请求体中的数据 buf := new(bytes.Buffer) io.Copy(buf, req.Body) body := buf.String() //将请求体中的字符串转换为整数 if n, err := strconv.Atoi(body); err != nil { fmt.Fprintf(w, "bad POST: %v\nbody: [%v]\n", err, body) } else { //利用请求体中的数字重置Counter计数器的值 ctr.n = n fmt.Fprint(w, "counter reset\n") } } //将当前计数器中的值返回给浏览器 fmt.Fprintf(w, "counter = %d\n", ctr.n) }
FileServer(root FileSystem) Handler 返回一个处理器,它以 root 作为根,用文件系统的内容响应 HTTP 请求。要获得操作系统的文件系统,用 http.Dir,例如:
http.Handle("/go/", http.FileServer(http.Dir("/tmp")))
完整案例:
package main import ( "flag" "log" "net/http" ) var webroot = flag.String("root", "./", "web root directory") func main() { //解析命令行参数 flag.Parse() http.Handle("/go/", //将请求交给具体处理器前,会帮我们剥掉请求前缀 http.StripPrefix("/go/", //拿到文件路径,去系统文件中查找,然后以附件下载的形式返回给客户端 http.FileServer(http.Dir(*webroot)))) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } }
该处理函数使用了 flag 包。VisitAll 函数迭代所有的标签(flag),打印它们的名称、值和默认值(当不同于“值”
时)
package main import ( "flag" "fmt" "log" "net/http" ) var booleanflag = flag.Bool("boolean", true, "another flag for testing") func main() { http.Handle("/flags", http.HandlerFunc(FlagServer)) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } } func FlagServer(w http.ResponseWriter, req *http.Request) { //设置响应头 w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprint(w, "Flags:\n") //遍历所有标签 flag.VisitAll(func(f *flag.Flag) { //DefValue是当前标签的默认值 if f.Value.String() != f.DefValue { //用户通过命令行参数获得了非默认值 fmt.Fprintf(w, "%s = %s [default = %s]\n", f.Name, f.Value.String(), f.DefValue) } else { //解析后得到的值和默认值一致 fmt.Fprintf(w, "%s = %s\n", f.Name, f.Value.String()) } }) }
利用通道完成累加计数,程序如下:
每当有新请求到达,通道的 ServeHTTP 方法从通道获取下一个整数并显示。由此可见,网页服务器可以从通道中获取要发送的响应,它可以由另一个函数产生(甚至是客户端)。
package main import ( "fmt" "io" "log" "net/http" ) type Chan chan int func main() { http.Handle("/chan", ChanCreate()) err := http.ListenAndServe(":12345", nil) if err != nil { log.Panicln("ListenAndServe:", err) } } func ChanCreate() Chan { c := make(Chan) //利用通道完成累加计数 go func(c Chan) { for x := 0; ; x++ { c <- x } }(c) return c } func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { io.WriteString(w, fmt.Sprintf("channel send #%d\n", <-ch)) }
下面的代码片段正是一个这样的处理函数,但会在 30 秒后超时:
func ChanResponse(w http.ResponseWriter, req *http.Request) {
timeout := make (chan bool)
go func () {
time.Sleep(30e9)
timeout <- true
}()
select {
case msg := <-messages:
io.WriteString(w, msg)
case stop := <-timeout:
return
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。