golang 实现http client (一)最原生的GET方法

2,407 阅读3分钟

目的

没错,看到了标题有一个序列号,代表着 这是一个系列的文章,写这一系列文章的目的是,我们在学习源码时,是在作者融合了各种功能,以及实现过程增加的各种异常保护,和优化过代码设计的基础上看的,因为难免会出现,咦这里为啥还要抽离一个结构体出来,咦这个判断有什么用,这协程和chan是干嘛的,看起来一头雾水。 因此我们从一个最简单原始的http 客户端,通过给他附加各种功能,以及结构的优化,来达到学习的目的。

最原生的客户端实现

什么是http客户端

一个HTTP"客户端"是一个应用程序(Web浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。

HTTP客户端发送消息的结构

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

实现

func main(){
    	conn, err := net.Dial("tcp","baidu.com:80")
	if err != nil {
		fmt.Printf("connect err => %s\n", err.Error())
	}
	buf := bytes.Buffer{}
	buf.WriteString("GET / HTTP/1.1\r\n")
	buf.WriteString("Host: baidu.com\r\n")
	buf.WriteString("USer-Agent: Go-http-client/1.1\r\n")
	// 请求头结束
	buf.WriteString("\r\n")
	// 请求body结束
	buf.WriteString("\r\n\r\n")
	conn.Write(buf.Bytes())
	// 获取响应信息
	resp, _ := ioutil.ReadAll(conn)
	fmt.Printf("响应信息\n%q",resp)
}

执行后,查看结果

可以看到我们正常获取到了baidu.com的信息

包装成http.Get()方法

刚才我们在main函数内,直接使用baidu.com地址验证了一下逻辑,现在我们按照golang的写法http.Get(),把我们的代码包装成一个方法

1.创建一个名为http的包,由于我们是实现客户端,因此创建一个client.go

现在的目录结构是

2.将我们的main函数的代码,拷贝到client.go,此时client.go内的代码如下
type Client struct {

}

var DefaultClient = &Client{}

func Get(url string) (resp []byte, err error) {
	return DefaultClient.Get(url)
}

func (c *Client) Get(url string) (resp []byte, err error) {
	conn, err := net.Dial("tcp","baidu.com:80")
	if err != nil {
		return nil, err
	}
	buf := bytes.Buffer{}
	buf.WriteString("GET / HTTP/1.1\r\n")
	buf.WriteString("Host: baidu.com\r\n")
	buf.WriteString("USer-Agent: Go-http-client/1.1\r\n")
	buf.WriteString("\r\n")
	buf.WriteString("\r\n\r\n")
	conn.Write(buf.Bytes())
	return ioutil.ReadAll(conn)
}
3. Get方法会接受一个string类型的地址,因此我们需要解析下url中的host跟uri,这里我们使用golang内部已经提供的url.Parse方法来获取url的相关信息,函数声明为func Parse(rawurl string) (*URL, error),函数内部会返回URL结构体,
  • URL.Hostname() 获取host
  • URL.Port() 获取port 由于port存在为空的情况,因此我们需要为他增加http默认端口
var portMap = map[string]string{
	"http": "90",
	"https": "443",
}
func canonicalAddr(url *url.URL) string {
	addr := url.Hostname()
	port := url.Port()
	if port == "" {
		port = portMap[url.Scheme]
	}
	return addr + ":" + port
}

修改后的代码

var parseUrl = url.Parse

func (c *Client) Get(url string) (resp []byte, err error){
	URL, err := parseUrl(url)
	if err != nil {
		return nil, err
	}
	conn, err := net.Dial("tcp", canonicalAddr(URL))
	if err != nil {
		return nil, err
	}
	host := URL.Hostname()
	buf := &bytes.Buffer{}
	fmt.Fprintf(buf, "%s %s HTTP/1.1\r\n", "GET", URL.RequestURI())
	fmt.Fprintf(buf, "Host: %s\r\n", host)
	buf.WriteString("User-Agent: Go-http-client/1.1\r\n")
	buf.WriteString("\r\n")  // header结束
	buf.WriteString("\r\n\r\n") // body结束
	conn.Write(buf.Bytes())
	return ioutil.ReadAll(conn)
}

到这里我们就实现了一个简单的http客户端的GET方法