无闻Web1.初窥HTTP服务器

初窥HTTP服务器

想要学习Go语言的Web开发,就必须知道如何在Go语言中启动一个HTTP服务器用于接受和响应来自客户端的HTTP请求。虽然Web应用协议不止于HTTP(HyperText Transfer Protocol),还包括常见的Socket、WebSocket和SPDY等等,但是HTTP是当下最简单和最常见的交换形式。

Hello World!

先创建一个名为http_server.go的文件,然后输入以下代码:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/",func(w http.ResponseWriter,r *http.Request{
		w.Write([]byte("Hello World!"))
	}) 

	log.Println("Strating HTTP server...")
	log.Fatal(http.ListenAndServe("localhost:4000",nil))
}

是不是觉得几个简单的步骤就实现了一个完全可用的HTTP服务器?这就是Go语言的魅力之一!
接下来,让我们分析一下这段代码具体都做了什么事情。
我们需要明白这里有三个关键点:

  1. http.HandleFunc函数的作用是将某一个函数与一个路由规则进行绑定,当用户访问指定路由时(某个路由规则匹配成功),所绑定的函数就会被执行。它接受两个参数,第一个参数就是指定的路由规则,本例中我们使用/来表示根路径;第二个参数就是与该路由进行绑定的函数
  2. http.HandleFunc的第二个参数必须符合函数签名func(http.ResponseWriter,*http.Request),这个函数同样接受两个参数,第一个参数是请求所对应的响应对象http.ResponseWriter,包括响应码(Response Code)、响应头(Response Header)和响应体(Response Body),我们就是通过调用这个对象的Write方法向响应体写入"Hello World!"字符串的;第二个参数则是请求所对应的请求对象*http.Request,该对象包含当前这个HTTP请求所有的信息,包括请求头(Request Header)、请求体(Request Body)和其它相关的内容。
  3. http.ListenAndServe函数的作用就是启动HTTP服务器,并监听发送到指定地址和端口号的HTTP请求,本例中我们要求HTTP服务器监听并接受发送到地址localhost且端口号为4000的HTTP请求。这个函数也接受两个参数,我们目前只使用到了第一个参数,即监听地址和端口号;第二个参数会在后文讲解,因此暂时可以使用nil作为它的值。另外,如果监听地址为127.0.0.1或者localhost,则可以使用更简洁的写法,即`http.ListenAndServe(":4000",nil)。

除此以外,你可能已经注意到为了节省代码行数,我们在这段代码中使用了匿名函数来编写HTTP请求的处理逻辑。这在编写简单的逻辑时非常方便,但当逻辑处理较为复杂时,应该定义一个独立的函数以提升代码的可读性我们可以将这段代码等价的转换为如下形式:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/",hello)
	
	log.Println("Strating HTTP server...")
	log.Fatal(http.ListenAndServe("localhost:4000",nil))
}

func hello(w http.ResponseWriter,r *http.Request) {
	w.Write([]byte("hello World!")
}

实际上,http.HandleFunc也是标准库提供给用户的一种简便写法,它的第二个参数的函数签名必须为func(http.ResponseWriter,*http.Request)是因为在http.HandleFunc函数内部会将我们传入的绑定函数转化为类型http.HandleFunc,即一个Go语言中标准的HTP请求处理对象,这个对象类型实现了http.Handler接口:

type Handler interface {
	ServeHTTP(ResponseWriter,*Request)
}

通过http.Handler的接口定义我们发现,函数签名func(http.ResponseWriter,*http.Request)的由来是因为要实现接口的ServeHTTP方法。现在我们知道了http.HandleFunc的根本作用是将一个函数转化为一个实现了http.Handler接口的类型(http.HandlerFunc)
,那么我们可不可以字节创建一个类型并实习http.Handler接口呢?答案当然是肯定的。
一摸一样的功能,下面的代码使用了更加复杂的用法:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.Handle("/",&helloHandler{})
	log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe(":4000", nil))
}

type helloHandler struct{}
func (_ *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

这段代码不再使用http.HandleFunc函数,取而代之的是直接调用http,Handle并传入我们自定义的http.Handler实现。想要实现http.Handler接口,就必须实现接口的方法。

服务复用器(ServeMux)

根据http.ListenAndServe的函数声明可以得知,这里的nil代替的其实是一个实现了http.Handler接口的对象:

func ListenAndServe(addr string, handler Handler) error {...}

是不是有点眼熟?因为这说明我们之前的定义的helloHandler就可以被用作这里的参数:

package main

import (
    "log"
    "net/http"
)

func main() {
    log.Println("Starting HTTP server...")
    log.Fatal(http.ListenAndServe("localhost:4000", &helloHandler{}))
}

type helloHandler struct{}

func (_ *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

运行这段代码同样可以得到和之前一摸一样的结果!
不过,在实际开发中很少直接将一个纯粹的处理器(Handler)作为这里的参数,因为它缺失了一个非常重要的功能,它不能像之前调用http.HandleFunchttp.Handle那样方便的将路由规则和执行函数进行绑定。

通过查看http.Handle源码可以得知,它其实是一个默认的http.ServeMux对象(http.DefaultServeMux)进行了一层封装:

func Handle(pattern string, handler Handler) {
    DefaultServeMux.Handle(pattern, handler)
}

这个http.ServeMux的作用是什么呢?它就是Go语言标准库实现的一个带有基本路由功能的服务复用器(Multipexer)。除了可以通过http.HandleFunchttp.Handle这类方法操作http.DefaultServeMux对象之外,我们也可以通过标准库提供的方法http.NewServeMux来创建一个新的http.ServeMux对象:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.Handle("/",&helloHandler{})
	log.Println("Starting HTTP server ...")
	log.Fatal(http.ListenAndServe("localhost:4000",mux))
}

type helloHandler struct {}
func (_ *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

你可能已经猜到,这段代码依旧和之前做的是一摸一样的事情。不过做到这个程度,便是大多数Web框架的底层用法了。它们本质上就是一个带有路由层的http.Handler具体实现,并以此为基础提供大量便利的辅助方法。

服务器对象(Server)

既然http.HandleFunchttp.Handle都是一个默认对象http.DefaultServeMux的封装,那http.ListenAndServe是否也是如此?我们可以从它源码中找到答案:

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

显而易见,虽然不是针对某个全局对象的封装,但也通用是在调用的时候创建了http.Server的对象。相比教而言,http.Server的自定义程度就非常高了,它包含了Go语言标准库提供的所有可能的选项,包括监听地址、服务复用器和读写超时等等。

接下来,让我们使用http.Server对象来改写一下我们的小程序:

package mian

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.Handle("/",&helloHandler{})

	server := &http.Server{
		Addr:":4000",
		Handler:mux,
	}
	log.Println("Starting HTTP server...")
    log.Fatal(server.ListenAndServe())
}

type helloHandler struct{}
func (_ *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

如果只是单纯的将标准库的封装强行抽离出来未免显得有点小题大做,那么我们可以对这个自定义的http.Server对象做点什么事情呢?不如就设置一个写超时(Write Timeout)好啦。写超时包含的范围是当请求头被解析后直到响应完成,浅显一点的理解就是我们绑定的函数开始执行到执行结束为止,如果这个时间范围超过定义的周期则会触发写超时。

package mian

import (
	"log"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.Handle("/",&helloHandler{})
	mux.HandleFunc("/timeout",func(w http.ResponseWriter,r *http.Request) {
		time.Sleep(2 * time.Second)
		w.Write([]byte("Timeout"))
	})
	
	server := &http.Server{
		Addr:	":4000",
		Handler:	mux,
		WriteTimeout:	2 * time.Second,
	}
	log.Println("Starting HTTP server...")
	log.Fatal(server.ListenAndServe())
}

type helloHandler struct{}

func (_ *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world!"))
}

在这段代码里,我们添加了对WriteTimeout字段的赋值,设为2秒钟,同时添加了一个新的执行函数用于先休眠2秒然后向客户端输出"Timeout"字符串。

启动服务器后,如果我们访问http://localhost:4000会收到和之前一样的结果,但如果尝试访问http://localhost:4000/timeout则不会收到任何消息。

这是因为我们的执行函数在休眠2秒后被http.Server对象认为已经超时,提前关闭了与客户端之间的连接,因此无论执行函数后面响应体写入任何东西都不会有任何作用。部分Web框架使用的便是自定义的http.Server对象,因此你只能通过调用框架提供的特定方法来启动服务。

优雅的停止服务

在生产环境中,许多开发者面临的一个困难就是当需要更新服务端程序时需要重启服务,但此时可能有一部分请求进行到一半,如果强行中断这些请求可能会导致意外的结果。因此,开源社区提供了多种优雅停止的方案,像facebookgo/grace就是其中的代表作之一。不过从Go1.8版本开始,标准库终于支持原生的优雅停止方案了。

这种方案同样需求用户创建自定义的http.Server对象,因为对应的Close方法无法通过其它途径调用。我们来看下面的代码,这段代码通过结合捕捉系统信号(Signal)、goroutine和管道(Channel)来实现服务器的优雅停止:

package mian

import (
	"log"
	"net/http"
	"os"
	"os/signal"
)

func main() {
	mux := http.NewServeMux()
	mux.Handle("/",&helloHandler{})
	
	server := &http.Server{
		Addr:	":4000",
		Handler: mux,
	}
	
	// 创建系统信号接收器
	quit := make(chan os.Signal)
	signal.Notify(quit,os.Interrupt)
	go func(){
		<- quit
		if err := server.Shutdown(context.Background());err != nil {
		log.Fatal("Shutdown server:",err)
		}
	}()
	
	log.Printn("Starting HTTP server...")
	err := server.ListenAndServe()
	if err != nil {
		if err == http.ErrServerClosed {
			log.Print("Server closed under request")
		} else {
			log.Fatal("Server closed unexpected")
		}
	}
}

type helloHandler struct{}

func (_ *helloHandler) ServerHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World!"))
}

这段代码通过捕捉os.Interrupt信号(Ctrl+C)然后调用server.Shutdown方法告知服务器应停止接受新的请求并在处理完当前已接受的请求后关闭服务器。为了与普通错误相区别,标准库提供了一个特定的错误类型http.ErrServerClosed,我们可以在代码中通过判断是否为该错误类型来确定服务器是正常关闭还是意外关闭

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值