目的
没错,看到了标题有一个序列号,代表着 这是一个系列的文章,写这一系列文章的目的是,我们在学习源码时,是在作者融合了各种功能,以及实现过程增加的各种异常保护,和优化过代码设计的基础上看的,因为难免会出现,咦这里为啥还要抽离一个结构体出来,咦这个判断有什么用,这协程和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()获取hostURL.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方法