Go -- 网络编程TCP/UDP/Socket/HTTP

互联网协议介绍

互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。

一、互联网分层模型

  • 互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。
    在这里插入图片描述
    如上图所示,互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。

1、物理层

我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。

2、数据链路层

单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是”数据链接层”的功能,它在”物理层”的上方,确定了物理层传输的0和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做”以太网”(Ethernet)的协议,占据了主导地位。

以太网规定,一组电信号构成一个数据包,叫做”帧”(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。

那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。

我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。

3、网络层

按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。

因此,必须找到一种方法区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。

“网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。

规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。

根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。

4、传输层

有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

“端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。

我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

5、应用层

应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。

如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。

在这里插入图片描述

二、socket编程

  • Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。
    • Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。
    • 电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

1、socket图解

Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。
在这里插入图片描述

2、Go语言实现TCP通信

2.1、TCP协议

TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

2.2、TCP服务端与客户端

  • 一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。

  • 因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。

  • TCP服务端程序的处理流程:

    • 1.监听端口
    • 2.接收客户端请求建立链接
    • 3.创建goroutine处理链接。
  • 一个TCP客户端进行TCP通信的流程如下:

    • 1.建立与服务端的链接
    • 2.进行数据收发
    • 3.关闭链接

示例:定义一个一次性发送消息的server端和client端。 先启动server端,再启动client端,server就会接收到发送的消息

######################## server端 ##################################
package main

import (
	"fmt"
	"net"
)

// TCP server端
func main() {
	// 1、本地建立连接,利用net.Listen()方法
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("Start tcp server failed,err,", err)
		return
	}

	// 2、等待客户端与我建立连接
	coon, err := listener.Accept()
	if err != nil {
		fmt.Println("Accept failed,err:", err)
		return
	}
	// 3、与客户端通信
	var msg [128]byte
	n, err := coon.Read(msg[:])
	if err != nil {
		fmt.Println("Read msg failed,err:", err)
		return
	}
	fmt.Println(string(msg[:n]))
}

######################## client端 ###############################################

import (
	"fmt"
	"net"
)

// TCP client端
func main() {
	// 1、与server端建立连接
	coon, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("connect to server failed,err:", err)
		return
	}

	// 2、发送数据
	coon.Write([]byte("hello world"))

	coon.Close()
}

示例2:server端和client循环多次接收消息

##################### server端 #################################
package main

import (
	"fmt"
	"net"
)

// 写一个函数,支持并发操作
func processConn(conn net.Conn) {
	// 3、与客户端通信
	var msg [128]byte
	for { // 客户端多次发送消息,因此需要写循环来接收
		n, err := conn.Read(msg[:])
		if err != nil {
			fmt.Println("Read msg failed,err:", err)
			return
		}
		fmt.Println(string(msg[:n]))
	}
}

// TCP server端
func main() {

	// 1、本地建立连接,利用net.Listen()方法
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("Start tcp server failed,err,", err)
		return
	}

	for {
		// 2、等待客户端与我建立连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Accept failed,err:", err)
			return
		}
		go processConn(conn)
	}
}

##################### client端 #################################
package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

// TCP client端
func main() {

	// 1、与server端建立连接
	coon, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("connect to server failed,err:", err)
		return
	}
	for {
		fmt.Print("请输入>>> ")
		// 2、发送数据
		var reader = bufio.NewReader(os.Stdin) // 获取标准输入,使用scanln()的办法,是无法输入空格的
		msg, _ := reader.ReadString('\n')
		msg = strings.TrimSpace(msg) // 去掉两边空格

		if msg == "exit" {
			break
		}
		coon.Write([]byte(msg))

	}
	coon.Close()
}

2.3、TCP黏包

2.3.1、为什么会出现粘包
  • 主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。(比方说发送了两句话,由于黏包现象,两句话合成了一句话)

  • “粘包”可发生在发送端也可发生在接收端:

    • 1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
    • 2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

1、演示示例如下:

################################ server 端 ########################################
// socket_stick/server/main.go

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	var buf [1024]byte
	for {
		n, err := reader.Read(buf[:])
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client发来的数据:", recvStr)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn)
	}
}

################################# client 端 ########################################
// socket_stick/client/main.go

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("dial failed, err", err)
		return
	}
	defer conn.Close()
	for i := 0; i < 20; i++ {
		msg := `Hello, Hello. How are you?`
		conn.Write([]byte(msg))
	}
}

输出结果:发现20个数据,被server端两次就接收了

  • 客户端分20次发送的数据,在服务端并没有成功的输出20次,而是多条数据“粘”到了一起。
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
2.3.2、解决黏包的办法
  • 出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
  • 封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
  • 我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

引伸知识点: 大端和小端,比方说我们有个int32类型4字节的16进制数0x123456,把它存到缓存中,如果高位数在左边就是小端,如果高位数在右边就是大端。只有当我们需要将多个字节数据写入到内存中时才需要用到这个知识点(如果传入的是一个字节数据,就不用考虑大端小端问题),规定内存读取时从右开始读还是从左开始读
在这里插入图片描述

1、自定义一个proto包
  • 对socket的消息进行编码和解码
package proto
// 包位置:src\gitee.com\socket\protocol\protocol.go

import (
	"bufio"
	"bytes"
	"encoding/binary"
)

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
	// 读取消息的长度,转换成int32类型(占4个字节)
	var length = int32(len(message))
	// 初始化一个缓存存放字节数据
	var pkg = new(bytes.Buffer)
	// 写入消息头,binary.LittleEndian小端方式,从右往左写入
	err := binary.Write(pkg, binary.LittleEndian, length)
	if err != nil {
		return nil, err
	}
	// 写入消息实体,从右往左写入
	err = binary.Write(pkg, binary.LittleEndian, []byte(message))
	if err != nil {
		return nil, err
	}
	return pkg.Bytes(), nil
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
	// 读取消息的长度
	lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
	lengthBuff := bytes.NewBuffer(lengthByte) // 新初始化一个消息数据大小的缓冲区
	var length int32
	// binary.LittleEndian 规定了小端的方式读取,读取的数据写入到 length 中
	err := binary.Read(lengthBuff, binary.LittleEndian, &length) 
	if err != nil {
		return "", err
	}
	// Buffered返回缓冲中现有的可读取的字节数。
	if int32(reader.Buffered()) < length+4 {
		return "", err
	}
	// 读取真正的消息数据
	pack := make([]byte, int(4+length))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil
}

2、server和client端导入proto包
################## server 端####################################
package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
	proto "gitee.com/socket/protocol"
)

// socket_stick/server2/main.go

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		msg, err := proto.Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("decode msg failed, err:", err)
			return
		}
		fmt.Println("收到client发来的数据:", msg)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn)
	}
}

########################## client 端 ###################################
package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
	proto "gitee.com/socket/protocol"
)
// socket_stick/client2/main.go

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("dial failed, err", err)
		return
	}
	defer conn.Close()
	for i := 0; i < 20; i++ {
		msg := `Hello, Hello. How are you?`
		data, err := proto.Encode(msg)
		if err != nil {
			fmt.Println("encode msg failed, err:", err)
			return
		}
		conn.Write(data)
	}
}

3、UDP通信

3.1、UDP协议

  • UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

3.2、UDP服务端与客户端

############################ server 端 ############################
package main

import (
	"fmt"
	"net"
	"strings"
)

// socket_stick/server2/main.go

func main() {
	// UDP 同样使用的是net包,ListenUDP()方法中传入的参数1是协议名称“udp”,参数2中传入的是net.UDPAddr类型的指针,定义ip和端口
	conn, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0), // 监听来自不同地方发来的消息
		Port: 60000,
	})
	if err != nil {
		fmt.Println("listen udp failed,err:", err)
		return
	}
	defer conn.Close()
	// 不需要建立连接直接收发数据
	var data [1024]byte
	for {
	// 返回切片的字符数,消息的来源地址以及报错消息
		n, addr, err := conn.ReadFromUDP(data[:])
		if err != nil {
			fmt.Println("read from udp failed err:", err)
			return
		}
		// 打印读取的数据
		fmt.Println(string(data[:n]))
		// 发送数据
		reply := strings.ToUpper(string(data[:n])) // 将client端发送过来的数据大写后返回给对方

		conn.WriteToUDP([]byte(reply), addr)
	}

}

############################## client 端 #################################### 
package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 60000,
	})
	if err != nil {
		fmt.Println("连接服务端失败,err:", err)
		return
	}
	defer socket.Close()
	var reply [1024]byte
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("请输入>>> ")
		msg, _ := reader.ReadString('\n')
		// 发送数据给server端
		socket.Write([]byte(msg))
		// 接收server端数据
		n, addr, _ := socket.ReadFromUDP(reply[:])
		fmt.Printf("接收到来自[%v]的消息: %v", addr, string(reply[:n]))
	}
}

执行效果

请输入>>> abc
接收到来自[127.0.0.1:60000]的消息: ABC
请输入>>> 123
接收到来自[127.0.0.1:60000]的消息: 123
请输入>>> xxzcxzc
接收到来自[127.0.0.1:60000]的消息: XXZCXZC

3、HTTP协议

  • go语言内置的net/http包提供了HTTP 的客户端与服务端的实现

  • 超文本传输协议(HTTP)是互联网上应用最为广泛的一种网络传输协议,所有的www文件都必须遵守这个标准。设计HTTP的最初目的是为了提供发布和接收HTML页面的方法

补充了解:

  • HTTP:超文本传输协议(规定了浏览器和网站服务器之间的通信规则)
  • HTML:是超文本标记语言(一些符号和标签)
  • CSS:层叠样式表(规定了HTML的具体样式,如颜色\图片字体大小\背景\位置 等)
  • JavaScript:一种跑在浏览器上的编程语言(在网页上添加各种功能)

3.1、HTTP的server端

3.1.1、默认HTTP的server端

在这里插入图片描述

  • 代码示例
package main

import (
	"net/http"
)

// 定义一个http处理函数,函数内部传入“响应”和“请求”
func f1(w http.ResponseWriter, req *http.Request) {
	str := `<h1 style="color:red">hello world!</h1>
	<a href="https://www.sogo.com">点击进入搜狗</a>`
	// 将str信息展现在页面上
	w.Write([]byte(str))
}
func f2(w http.ResponseWriter, req *http.Request) {
	str := `<h1 style="color:blue">This is F2</h1>`
	// 将str信息展现在页面上
	w.Write([]byte(str))
}

// 定义从文件中发送响应内容的函数(可以实现动态更新的效果) 
func f3(w http.ResponseWriter, req *http.Request) {
	b, err := ioutil.ReadFile("./test.html")
	if err != nil {
		w.Write([]byte(`<h1>维护中···</h1>`))
	}
	w.Write(b)
}
func main() {
// http.HandleFunc()方法内参数1传入的是具体路径,参数2传入的是函数,当我们访问请求得地址是参数1的地址,那么就执行参数2的请求处理函数
	http.HandleFunc("/posts/Go/15_socket/", f1)
	http.HandleFunc("/posts/f2", f2)
	// 启动服务监听
	http.ListenAndServe("127.0.0.1:6000", nil)
}

  • test.html文件内容可以如下
<h1 style="color:red">Hello World!!</h1>
<!- 给图片标记id ->
<img id="p1" src="http://img.jj20.com/up/allimg/1114/0H320120A7/200H3120A7-1-1200.jpg" width="500"/><br>
<a href="https://www.sina.com.cn/">点击进入搜狗</a> 

<!- 下面是一个JavaScript的简单示例 ->
<button id="b1">点击我查看</button>
<button id="b2">更换图片</button>
<script>
    document.getElementById("b1").onclick=function(){
        alert("你好!!!")
    }
    document.getElementById("b2").onclick=function(){
       document.getElementById("p1").src="http://img.jj20.com/up/allimg/1114/0H320120A7/200H3120A7-2-lp.jpg"
    }
</script>
  • 访问页面查看效果(浏览器就是客户端)
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

自定义Server

要管理服务端的行为,可以创建一个自定义的Server:

s := &http.Server{
	Addr:           ":8080",
	Handler:        myHandler,
	ReadTimeout:    10 * time.Second,
	WriteTimeout:   10 * time.Second,
	MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

3.2、HTTP的client端

  • 在后端开发中,经常需要利用程序去获取网页或者某个接口的信息,因此HTTP的client端在后端开发中经常被使用
3.2.1、基本的HTTP/HTTPS请求
Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。

resp, err := http.Get("http://example.com/")
...
// post上传文件,http.Post()参数1为url路径,参数2为文件类型,参数3是读文件的buffio的对象(buffio.NewReader)
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
// Post上传表单,需要用下面这个格式
resp, err := http.PostForm("http://example.com/form",
	url.Values{"key": {"Value"}, "id": {"123"}})

注意:程序在使用完response后必须关闭回复的主体。

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...
3.2.2、GET请求示例

1、首先我们先开启一个server端,用来验证

代码如下:获取client端使用GET请求得URL、方法、以及BODY

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func f4(w http.ResponseWriter, req *http.Request) {
	// 打印请求的路径
	fmt.Println(req.URL)
	// 打印请求得方法(PSOT\GET\DELETE\OPTION\PUT等)
	fmt.Println(req.Method)
	// 在server端打印客户端发来的body
	b, _ := ioutil.ReadAll(req.Body)
	fmt.Println(string(b))
	// 返回一个消息给server端
	w.Write([]byte("ok"))
}

func main() {
	http.HandleFunc("/posts/f4", f4)
	http.ListenAndServe("0.0.0.0:6000", nil)
}

2、定义client端,使用GET方法向server端请求

  • 注意:在 http.Get()方法内传入的地址必须是以http或者https开头的,否则就会报错first path segment in URL cannot contain colon
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	// 1、获取请求得消息
	resp, err := http.Get("http://127.0.0.1:6000/posts/f4/?name=小王&age=18") // 在路径后面使用问号?,表示传入数据,如这里传入的两个变量name=小王和age=18
	if err != nil {
		fmt.Println("get Url failed,err:", err)
		return
	}
	// 2、从resp中将server端中返回的数据读出来
	// // 2.1、方法一:如下,先定义切片来接收数据,读取后关闭
	// var data [1024]byte
	// resp.Body.Read(data[:])
	// resp.Body.Close()
	// 2.2、方法二:使用ioutil包中的ReadAll()方法读取所有的信息
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("数据读取失败,Err:", err)
		return
	}
	fmt.Println(string(data))
}

3、启动server端并且启动client端向server端发出请求

  • 在client端返回的信息如下
ok
  • 在server端返回的信息如下
/posts/f4/?name=小王&age=18
GET
[] <nil>

根据上面的的结果,可以看出,client端使用GET来请求,请求得参数都是在URL上(query param),请求体Body中是没有数据的

4、http包中Query()可以自动识别?号后面传入的参数(query param)

  • Query()提供了很多方法,如下:
    • Query().Get() 传入key,返回对应的值
    • Query().Add()
    • Query().Set()
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func f4(w http.ResponseWriter, req *http.Request) {
	// 打印请求的路径
	fmt.Println(req.URL)
	fmt.Println(req.URL.Query())
	// 指定query param的键获取值
	name := req.URL.Query().Get("name")
	age := req.URL.Query().Get("age")
	fmt.Printf("姓名:%v,年纪:%v", name, age)
	// 打印请求得方法(PSOT\GET\DELETE\OPTION\PUT等)
	fmt.Println(req.Method)
	// 在server端打印客户端发来的body
	// b, _ := ioutil.ReadAll(req.Body)
	fmt.Println(ioutil.ReadAll(req.Body))
	// 返回一个消息给server端
	w.Write([]byte("ok"))
}

func main() {
	http.HandleFunc("/posts/f4/", f4)
	http.ListenAndServe("0.0.0.0:6000", nil)
}

输出结果如下:自动将GET请求得参数进行解析

map[age:[18] name:[小王]]
姓名:小王,年纪:18GET
[] <nil>
3.2.3、带参数的GET请求示例
  • 上面示例中,GET请求得参数是直接写在URL中,这种方式存在着一定的问题,例如请求参数的键值对中的键或者值也是也有?或者=符号的字符(如:http://127.0.0.1:9090/xxx/?token=abc?cxz=123= 这种键值对参数),那么就容易造成误解。
  • 关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。对url中带有的参数进行编码

Client端代码示例如下

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)

func main() {
	apiUrl := "http://127.0.0.1:6000/posts/f4/"
	// 定义一个map,传入url param参数
	data := url.Values{}
	// 使用Set()方法给map进行赋值
	data.Set("name", "小明")
	data.Set("age", "18")
	data.Set("token", "abc?xxx=123")
	// ParseRequestURI()方法中传入url,进行解析,返回包含url的信息的指针以及error报错信息
	u, err := url.ParseRequestURI(apiUrl)
	if err != nil {
		fmt.Println("parse url request url falied,err:", err)
		return
	}
	queryStr := data.Encode() // Url Encode之后的url
	u.RawQuery = queryStr     // 将url Encode后的url赋值给 u.RawQuery
	fmt.Println(u.String())
	// 定义一个新的请求对象,传入请求得方法,带参数的url 以及 请求主体
	req, err := http.NewRequest("GET", u.String(), nil)
	// 调用http.DefaultClient.Do()方法,发送请求,传入的参数就是上面定义的 请求对象
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("get Url failed,err:", err)
		return
	}
	// 读取完毕后关闭response对象
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("数据读取失败,Err:", err)
		return
	}
	fmt.Println(string(b))
}

输出结果:如下所示,对url后面的参数进行编码格式化

http://127.0.0.1:6000/posts/f4/?age=18&name=%E5%B0%8F%E6%98%8E&token=abc%3Fxxx%3D123
ok
3.2.4、Post请求示例
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

// net/http post demo

func main() {
	url := "http://127.0.0.1:9090/post"
	// 表单数据
	//contentType := "application/x-www-form-urlencoded"
	//data := "name=小王子&age=18"
	// json
	contentType := "application/json"
	data := `{"name":"小王子","age":18}`
	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("get resp failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))
}

对应的Server端HandlerFunc如下:

func postHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
	r.ParseForm()
	fmt.Println(r.PostForm) // 打印form数据
	fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 请求类型是application/json时从r.Body读取数据
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("read request.Body failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}
3.2.5、自定义Client

要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:

client := &http.Client{
	CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// 造一个请求对象
req, err := http.NewRequest("GET", "http://example.com", nil)
// 添加键值对
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...
3.2.6、自定义Transport

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

tr := &http.Transport{
	TLSClientConfig:    &tls.Config{RootCAs: pool},
	DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Client和Transport类型都可以安全的被多个goroutine同时使用。出于效率考虑,应该一次建立、尽量重用(即用全局变量定义)

示例:利用Transport创建一个短连接

  • 应用场景:比如需要5秒钟去某个网站获取一些数据,这种场景就必须使用短连接
  • 适用于请求不是特别频繁,用完就关闭链接
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
)

// 禁用 KeepAlive的client,那么创建的client就是个短连接(默认是开启Keepalive长链接的)
var (
	client = http.Client{
		Transport: &http.Transport{
			DisableKeepAlives: true,
		},
	}
)

func main() {
	apiUrl := "http://127.0.0.1:6000/posts/f4/"
	// 定义一个map,传入url param参数
	data := url.Values{}
	// 使用Set()方法给map进行赋值
	data.Set("name", "小明")
	data.Set("age", "18")
	data.Set("token", "abc?xxx=123")
	// ParseRequestURI()方法中传入url,进行解析,返回包含url的信息的指针以及error报错信息
	u, err := url.ParseRequestURI(apiUrl)
	if err != nil {
		fmt.Println("parse url request url falied,err:", err)
		return
	}

	queryStr := data.Encode() // Url Encode之后的url
	u.RawQuery = queryStr     // 将url Encode后的url赋值给 u.RawQuery
	fmt.Println(u.String())
	// 定义一个新的请求对象,传入请求得方法,带参数的url 以及 请求主体
	req, err := http.NewRequest("GET", u.String(), nil)

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("get Url failed,err:", err)
		return
	}
	// 读取完毕后关闭response对象
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("数据读取失败,Err:", err)
		return
	}
	fmt.Println(string(b))
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值