所有文章

golang web开发

编写一个基于TLS的WEB服务器。

Server主函数

package main

func main() {
	// 创建一个logger,这里只作为示例,该步骤不是必须的
	out, _ = os.OpenFile("/var/log/my.log", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	loger = log.New(out, "", log.LstdFlags)
	
	// 创建服务并启动,第三个参数如果不指定,Server内部的错误信息就直接输出到stderr
	s := &http.Server{Addr: ":443", Handler: buildMux(), ErrorLog: loger}
	go s.ListenAndServeTLS("/opt/ssl/cert.pem", "/opt/ssl/privkey.pem")

	// 再创建一个监听在80端口的Server,它负责把80上的所有请求重定向到443端口
	s80 := &http.Server{Addr: ":80", Handler: http.HandlerFunc(redirect80), ErrorLog: ctx.GetLogger()}
	go s80.ListenAndServe()

	// 创建一个channel,用来监听kill信号
	stop := make(chan string, 1)
	ctx.RegistryStoper(stop)
	<-stop
	
	// 监听到信号后执行一系列回收资源等动作
	close(stop)
	c, _ := context.WithTimeout(context.Background(), 1*time.Second)
	s.Shutdown(c)
	s80.Shutdown(c)

}

redirect80函数

func redirect80(w http.ResponseWriter, r *http.Request) {
	target := "https://" + r.Host + r.URL.Path
	if len(r.URL.RawQuery) > 0 {
		target += "?" + r.URL.RawQuery
	}

	http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}

buildMux函数

golang中的handler是一个封装了对某个请求的处理逻辑的函数,而ServerMux则是包含了多个handler的集合。

func buildMux() *http.ServeMux {
	mux := http.NewServeMux()

	// 自定义的handler,可添加多个,前面是uri,后面是处理逻辑
	mux.HandleFunc("/", index)
	mux.HandleFunc("/articles/", article)
	mux.HandleFunc("/static/", static)

	return mux
}

定义一个handler

func index(w http.ResponseWriter, r *http.Request) {
	method := r.Method
	if method == GET {
		w.Write([]byte("hello world!"))
	}
}

ctx.RegistryStoper(stop)函数的定义

它负责监听信号,然后通知所有曾经注册进来的通道:

package ctx

var (
	l               = &sync.Mutex{}
	stoperList      = []chan string{}
)

// init函数会在该包被第一次导入的时候自动执行
func init() {
	go monitorSignal()
}

func monitorSignal() {
	sigs := make(chan os.Signal, 1)
	
	// 开始监听,但这个函数是导步的
	signal.Notify(sigs, syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
	sig := <-sigs
	
	// 避免用户连续多次按下 control + c 导致程序被强制中断进而来不及回收其它资源
	signal.Ignore(syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    
	close(sigs)
	
	// 通知所有监听者
	for _, c := range stoperList {
		c <- "stop"
	}
}

// 用来注册监听者
func RegistryStoper(msg chan string) {
	l.Lock()
	stoperList = append(stoperList, msg)
	l.Unlock()
}

URI匹配规则

下面代码是向ServerMux注册一个Handler:

mux.HandleFunc("/users", user)

理想情况是当访问/users/jack时执行user函数,而实事上你会得到一个404,它只能通过/users被访问,为了达到理想结果,正确的写法应该是下面这样:

mux.HandleFunc("/users/", user)

这样一来,当我们访问/users/jack时,服务器会先匹配/users/jack,如果没有该路径则会查找/users/,如果还没有匹配到则查找/,如果还没有就404。

请求体解析规则

现在我们来向服务器发送一个POST请求,这个请求不是通过HTML中的form表单发起的,而是通过js中的ajax或者是curl命令行来发起,以curl为例:

curl -d 'user=jack' localhost/users/

-d参数后面是请求体的数据,相当于form表单中的数据,这样我们在服务器中就可以用http.Request对象获取该值,如下:

func user(w http.ResponseWriter, r *http.Request) {
	method := r.Method
	if method == POST {
		user := r.FormValue("user")
		println(user)
	}
}

而实际上它有可能输出为空,为什么呢?这是由于服务器并不知道你传过来的请求体中的数据是什么格式,很显然,如果请求体中的数据是JSON或者XML格式时我们是不希望它解析的,这时候我们需要在发起请时指明该请求体的数据格式是怎样的,我们修改一下请求命令:

curl -H "Content-Type: application/x-www-form-urlencoded" -d 'user=jack' localhost/users/

这样在user函数中就可以获取到jack了。

请求体只能被读取一次

有时候我们会写这样的代码:

func user(w http.ResponseWriter, r *http.Request) {
	// 先测试一下r.Body中有没有值
	body := ioutil.ReadAll(r.Body)
	println(body)
	
        method := r.Method
        if method == POST {
		user := r.FormValue("user")
                println(user)
        }
}

然后发起请求:

curl -H "Content-Type: application/x-www-form-urlencoded" -d 'user=jack' localhost/users/

结果是在第一次获取请求体时没问题,但r.FormValue("user")返回的值为空,为什么?这是因为r.Body本身是个输入流,即然是流当然是有指针的,当第一次从流中读完数据后,流中的指针已经指向了流的最后,当执行r.FormValue("user")的时候,它会触发r.ParseForm()函数,进而再次从r.Body中读取数据并解析到map中供我们使用,但这时流中已经没有数据可以读了,自然也就得不到正确的结果。


编写日期:2017-12-27