Golang使用net/http包写http服务端

Posted 玩家_名狱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang使用net/http包写http服务端相关的知识,希望对你有一定的参考价值。

使用 net/http 标准库实现 http服务端,通常有两种方式,分别是处理器和处理器函数。

一、处理器 Handler

下面实现两个页面,每个页面使用一个处理器,处理器处理的必须是一个实现了Handler接口的对象,Handler接口要求对象里面必须有一个ServerHTTP方法,且对方法的参数也有要求,如下:

package main

import (
	"net/http"
)

type SayHello struct {
	Name string
}

func (s *SayHello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!!!" + s.Name))
}

type SayGoodbye struct {
	Name string
}

func (s *SayGoodbye) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Goodbye!!!" + s.Name))
}

func main() {
	// http://127.0.0.1:8888/hello
	http.Handle("/hello", &SayHello{Name: "xiao xiao"})
	// http://127.0.0.1:8888/goodbye
	http.Handle("/goodbye", &SayGoodbye{Name: "hang hang"})
	err := http.ListenAndServe("127.0.0.1:8888", nil)
	if err != nil {
		panic(err)
	}
}

二、处理器函数 HandleFunc

HandleFunc对处理的函数没有要求,只需要函数的参数按照要求即可,下面实现了基本的功能,并输出客户端的请求信息:

package main

import (
	"fmt"
	"net/http"
)

func index(resp http.ResponseWriter, req *http.Request) {
	fmt.Println("------------客户端的请求信息----------")
	fmt.Println("请求地址", req.URL.Path)
	req.ParseForm() // 解析参数,然后存放于map类型的Form中
	fmt.Println("请求参数", req.Form)
	fmt.Println("请求头", req.Header)
	fmt.Println("请求体", req.Body)
	fmt.Println("-----------------end-----------------")
	respdata := []byte("shouye")
	resp.Write(respdata) // 数据写入响应对象
}

func Hello(resp http.ResponseWriter, req *http.Request) {
	resp.Write([]byte("hello"))
}

func main() {
	// http://127.0.0.1:8888/?name=zhong&age=18&name=xiao
	http.HandleFunc("/", index)
	// http://127.0.0.1:8888/hello
	http.HandleFunc("/hello", Hello)
	err := http.ListenAndServe("127.0.0.1:8888", nil)
	if err != nil {
		panic(err)
	}
}

三、使用原理

使用的方法如上,但是,为什么要这样使用?我们来阅读源码,简单分析一下。(接口一般都是以er接尾,如Handler)

  1. 首先,跳转倒http.Handle( )http.HandleFunc( )函数,看看它是怎么写的。如下它们都是调用了DefaultServeMux下对应的方法。注意http.Handle( )函数的第二个参数接受的是Handler接口,而http.HandleFunc( )函数的第二个参数接受的是一个函数类型。
func Handle(pattern string, handler Handler) {
    DefaultServeMux.Handle(pattern, handler)
}

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
  1. 看看DefaultServeMux长什么样。如下,它是一个结构体的实例化对象,原型为ServerMux结构体
var defaultServeMux ServeMux
var DefaultServeMux = &defaultServeMux

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry  
	hosts bool
}

ServeMux是HTTP请求多路复用器,功能是将每个传入请求的URL与已注册的列表进行匹配 。

  1. 上面的结构体中有个muxEntry,muxEntry也是一个结构体,原型为
type muxEntry struct {
	h       Handler
	pattern string
}
  1. muxEntry结构体中有一个Handler,看看Handler长什么样。如下是一个接口,接口定义了一个ServeHTTP函数,原型为
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

到此,我们终于知道了为什么使用处理器写的时候,要自己写一个对象,该对象下面必须有一个ServeHTTP方法了,因为源码中要求我们在处理器中传递的对象必须要实现Handler接口,且ServeHTTP方法必须使用那两个参数

  1. 看看第一步中http.Handle( )函数调用的DefaultServeMux.Handle(pattern, handler)函数指向的是谁
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	XXXX // 这里代码太长省略
}

原来是ServeMux对象下的一个方法Handle,这个方法正式处理我们定义的处理对象(如我们代码中的SayHello结构体)

  1. 像第5步那样,看看第一步中的http.HandleFunc( )函数调用的DefaultServeMux.HandleFunc(pattern, handler)函数指向的是谁。如下,里面除了判断我们的参数是否为空外,就直接调用mux.Handle( )函数
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}
  1. 看看HandlerFunc(handler)指向的是谁。如下,原来是个函数类型的变量
type HandlerFunc func(ResponseWriter, *Request)

这里运用的知识就是函数转换为变量

package main

import "fmt"

// 声明一个函数类型,该类型函数要传入两个整形
type JisuanType func(int, int)

// 函数类型的方法
func (js *JisuanType) Server() {
	fmt.Println("我是一个方法")
}

// 普通的加法计算函数
func add(a, b int) {
	fmt.Println(a + b)
}

// 普通的乘法计算函数
func mul(a, b int) {
	fmt.Println(a * b)
}

func main() {
	// 将add函数强制转换成JisuanType类型,add函数要求有两个整形传入参数
	jisuan_add := JisuanType(add)
	// 将mul函数强制转换成JisuanType类型
	jisuan_mul := JisuanType(mul)
	jisuan_add(3, 4)
	jisuan_mul(3, 4)
	jisuan_add.Server()
	jisuan_mul.Server()
}
// 结果
/*
7
12
我是一个方法
我是一个方法
*/

如果你看懂了这个例子,那么你就可以知道

之前使用处理器函数写index路由时,直接使用一个普通函数传参,原来在第6步哪里,将一个普通函数转换成对象,其实就是net/http包帮我们做了封装,底层原理上还是和处理器一样。它们最终都是传递对象,只不过处理器有个接口要求一定要实现ServeHTTP方法

因此我们终于知道为什么处理器函数写的时候可以直接使用普通函数写了

  1. 查看第6步的mux.Handle( )函数,跳转到的是第5步哪个方法。这样就更加证实了处理器函数底层上还是处理器,只是被封装了而已

四、请求多路复用器

分析使用原理的第2步中说过ServeMux是一个HTTP请求多路复用器,那我们可以使用多个请求多路复用器吗?可以。那什么时候会使用到?想要同时监听多个端口。

先看看写服务端时,使用到的http.ListenAndServe(addr string, handler http.Handler)函数,本来是就一个监听端口并使用服务(根据我们给定的如地址端口等信息创建网络服务)的,重点是第二个参数http.Handler类型,前面第4步已经知道它是一个接口,要求必须实现ServeHTTP方法。

然后使用http.Handle( )或者http.HandleFunc( )函数时,会自动帮我们创建一个defaultServeMux,可我们不希望使用包自动帮我们创建的多路复用器,我们要自己实现一个,如下

package main

import (
	"net/http"
)

type SayHello struct {
	Name string
}

func (s *SayHello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!!!" + s.Name))
}

type SayGoodbye struct {
	Name string
}

func (s *SayGoodbye) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Goodbye!!!" + s.Name))
}

func main() {
	sm := http.NewServeMux()
	// http://127.0.0.1:8888/hello
	sm.Handle("/hello", &SayHello{Name: "xiao xiao"})
	// // http://127.0.0.1:8888/goodbye
	sm.Handle("/goodbye", &SayGoodbye{Name: "hang hang"})
	err := http.ListenAndServe("127.0.0.1:8888", sm)
	if err != nil {
		panic(err)
	}
}


http.NewServeMux( )新建一个多路复用器,然后我们的路由就可以绑定到这个多路复用器下,最后把多路复用器传递给http.ListenAndServe( )函数的第二个参数。为什么传递的是*http.ServeMux类型而不是Handler类型,因为ServeMux内部实现了ServeHTTP方法,所以ServeMux实现了Handler接口。

基本的使用方法已经知道了,现在说明使用多路复用器有什么用,看代码:

package main

import (
	"net/http"
)

type SayHello struct {
	Name string
}

func (s *SayHello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!!!" + s.Name))
}

type SayGoodbye struct {
	Name string
}

func (s *SayGoodbye) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Goodbye!!!" + s.Name))
}

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

func main() {
	go func() {
		sm1 := http.NewServeMux()
		// http://127.0.0.1:8888/zhong
		sm1.HandleFunc("/zhong", zhong)  // 处理器函数注册页面
		err1 := http.ListenAndServe(":8888", sm1)
		if err1 != nil {
			panic(err1)
		}
	}()

	go func() {
		sm2 := http.NewServeMux()
		// http://127.0.0.1:8889/goodbye
		sm2.Handle("/goodbye", &SayGoodbye{Name: "hang hang"})  // 处理器注册页面
        // http://127.0.0.1:8889/hello
		sm2.Handle("/hello", &SayHello{Name: "xiao xiao"})
		err2 := http.ListenAndServe(":8889", sm2)
		if err2 != nil {
			panic(err2)
		}
	}()

	// 一直监听,但是一直没有数据过来,所以一直不退出
	select {}
}

代码中使用了连个多路复用器,每个多路复用器都注册了页面路由,将多路复用器传递给http.ListenAndServe( )函数,这样就可以达到了监听多个端口,每个端口实现不同功能的效果,具体页面路由的注册使用处理器还是处理器函数都可以。注意一定是使用多个线程,也就是go关键字,不能在同一个线程里面同时监听多个端口

以上是关于Golang使用net/http包写http服务端的主要内容,如果未能解决你的问题,请参考以下文章

golang实现http服务端接收post请求

[Go] 实现websocket服务端

深入理解Golang之http server

golang 简单web服务

摸鱼快报:golang net/http中的雕虫小技

golang(10):web开发 & 连接数据库