Golang学习笔记-大魂师-HTTP

十三、socket编程

13.1、概念介绍

13.1.1、OSI分层

OSI七层模型

  • 物理层:负责传递双绞线、光纤、无线电波等方式。负责传播0和1的电信号
  • 数据链路层:多少个电信号(0和1)算一组,早期每个公司都有自己的电信号分组方式。后来"Ethernet"的协议,占据了主导地位。一组电信号构成一个数据包。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。

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

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

  • 网络层

按照以太网协议的规则我们可以依靠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字节。

  • 传输层:

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

UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

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

  • 应用层

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

13.1.2、Socket

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

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

13.2、C/S 通信模型

网络应用程序涉及模式:

  • C/S: 需要在通讯两端各自部署客户机和服务器来完成数据通信。
  • B/S: 只需在一端部署服务器,而另外一端使用每台 PC 都默认配置的浏览器即可完成数据的传输。

net包提供的常用的函数

net包提供的对socket编程的支持,socket分为客户端和网络断

  • Listen:用于创建监听服务器
  • ListenPacket:用于创建服务器端连接
  • Dial:用于创建与服务器连接
  • JoinHostPort:连接地址和端口
  • SplitHostort:分割地址和端口
  • LookupAddr:查找地址对应主机名
  • LookupHost:根据主机名查看地址
  • ParseCIDR:解析CIDR格式IP

用法入门

package main

import (
	"fmt"
	"net"
)

func main (){
	ip := "127.0.0.1"
	port := "8080"
	fmt.Println(net.JoinHostPort(ip,port))  //拼接127.0.0.1:8080

	str := ip + ":" + port
	h,p,err := net.SplitHostPort(str)
	if err == nil {
		fmt.Printf("HOST:%v PORT:%v\n",h,p) //HOST:127.0.0.1 PORT:8080
	}

	//dns解析
	fmt.Println(net.LookupAddr("180.101.49.11"))
	fmt.Println(net.LookupHost("www.baidu.com"))  //[180.101.49.12 180.101.49.11] <nil>

	//IP解析,ip一定只能用ParseIP解析,CIDR一定只能用ParseCIDR解析
	IP,IPNET,_ := net.ParseCIDR("192.168.1.0/24")
	IP2 := net.ParseIP("192.168.1.2")
	IP3 := net.IP("192.168.1.2")     //注意这种是不行的,必须用ParseIP生成的net.IP对象才可以使用ParseCIDR解析
	IP4 := net.ParseIP("192.168.1.2/32")
	IP5,IPNET2,_ := net.ParseCIDR("192.168.1.2/31")


	fmt.Printf("%T %T\n",IP,IP2) //net.IP net.IP
	fmt.Println(IPNET.Contains(IP))  //true
	fmt.Println(IPNET.Contains(IP2))  //true
	fmt.Println(IPNET.Contains(IP3)) //false
	fmt.Println(IPNET.Contains(IP4)) //false
	fmt.Println(IPNET.Contains(IP5)) //true

	fmt.Println(IPNET.Mask.Size()) //24 32
	fmt.Println(IPNET2.Mask.Size()) //31 32

	//addr
	addrs,_ := net.InterfaceAddrs()
	for _,v := range addrs {
		fmt.Println(v.String()) /*
		192.168.154.1/24
		169.254.225.34/16
		fe80::b877:882b:3550:7ba8/64
		169.254.123.168/16
		::1/128
		127.0.0.1/8
		*/
	}

	//网卡信息
	intfs,_ := net.Interfaces()
	for _,v := range intfs {
		fmt.Print(v.Name,v.HardwareAddr,v.MTU,"#") //本地连接* 2 c2:30:49:85:34:ef 1500
		adrs,_ := v.Addrs()
		for _,m := range adrs {
			fmt.Print(m.String())
		}
		fmt.Println()
	}
	/*
	蓝牙网络连接80:30:49:85:34:f0 1500#fe80::b877:882b:3550:7ba8/64169.254.123.168/16
	以太网00:2b:67:b7:dc:0c 1500#fe80::e9ab:a7c2:70bb:a032/64169.254.160.50/16
	以太网 200:e0:4c:36:08:61 1500#fe80::a4ab:9377:c267:19d0/64169.254.25.208/16
	本地连接00:ff:b9:de:91:7c 1500#fe80::3460:857e:dd1c:b668/64169.254.182.104/16
	本地连接* 182:30:49:85:34:ef 1500#fe80::90bb:ea15:63de:4f78/64169.254.79.120/16
	本地连接* 2c2:30:49:85:34:ef 1500#fe80::a578:fd01:5a7:3732/64192.168.191.1/24
	*/
}

13.2.1、TCP服务端

一个goroutine启动一个goroutine,一个goroutine内部一直循环和client交互

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

一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。

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

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 创建goroutine处理链接。

服务端写法1

package main

import (
   "net"
   "fmt"
)

func processConn(conn net.Conn){
   // 3、与客户端进行通信
   var tmp [128]byte
   for {
      n, err := conn.Read(tmp[:]) //读取了多少个 byte。流式数据,一次从里面读取(n)多少byte
      if err != nil {
         fmt.Println("read失败 ", err)
         return
      }
      fmt.Println(string(tmp[:n])) //注意这里
      writeBack = []byte("Server端已收到")
      conn.Write(writeBack)  //回复已经收到
   }
    defer conn.Close()
}

func main(){
   //1、本地监听端口服务
   listener,err := net.Listen("tcp","127.0.0.1:2000")
   if err !=nil {
      fmt.Println("监听地址失败 ",err)
      return 
   }
   defer listener.Close()
   // 2、等待别人来通信,一个连接建立一个goroutine 进行处理
   for {
      conn, err := listener.Accept() //等待下一个连接
      if err != nil {
         fmt.Println("接收通信失败:", err)
         return
      }
      
      go processConn(conn)
   }
}

服务端写法2

package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"os"
	"time"
)
const dateFormat  = "2006-01-02 15:04:05"

func main(){
	addr := "127.0.0.1:8080"
	listener,err := net.Listen("tcp",addr)  //1、创建监听
	if err != nil {
		log.Fatal("监听失败: ",err)
		os.Exit(128)
	}
	defer listener.Close() //1.1、注意关闭监听

	for {
		conn,err := listener.Accept() //2、循环创建链接接收新的客户端请求
		if err != nil {
			log.Println("接收数据失败",err)
			continue //这里不用else的原因,是不建议函数的层级太深
		}

		//defer conn.Close() //链接要及时关闭,defer是延迟到函数推出的时候执行,因此要用匿名函数,保证链接不会泄露
		func() {  //3、读取客户端内容
			defer conn.Close() //2.1、链接要及时关闭
			log.Printf("client[%s] is connecte...", conn.RemoteAddr())
			reader := bufio.NewReader(conn)
			for { //3.循环处理客户端的单个链接
				line, _, err := reader.ReadLine()  //3.1、从单个链接中读取客户端消息内容
				if err != nil {
					log.Print(err)
					break
				} else {
					if string(line) == "quit" {  //应用client和server同时读的情况
						break
					}
				}
				log.Printf("接收到的数据有: %s", string(line)) //这里string(line)中带有换行,因此format中不需要添加\n
				fmt.Fprintf(conn, "Received: %s\n", time.Now().Format(dateFormat)) // //3.2、从单个链接中回复客户端消息
			}
		}()
	}
}

13.2.2、TCP客户端

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

客户端写法1

package main

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

func main(){
   //1、与服务端建立连接
   conn,err := net.Dial("tcp","127.0.0.1:2000")
   if err !=nil {
      fmt.Println("和服务端建立连接失败",err)
   }
   // 2、发送数据
   var msg string
   reader := bufio.NewReader(os.Stdin)
   for {
      fmt.Print("请输入:")
      input,_  = reader.ReadString('\n')
      msg = strings.ToUpper(input)
      if msg == "EXIT\r\n" {  //注意Read.ReadString的读取内容后面带了 \r\n 这样才能匹配
         break
      }
      fmt.Println("msg is:",msg)
      _,err := conn.Write([]byte(msg))
      if err !=nil {
         fmt.Println("发送数据失败",err)
         break
      }
   }
   //3、关闭连接
   conn.Close()
}

客户端写法2

package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"time"
)

const dateFormat  = "2006-01-02 15:04:05"

func main() {
	addr := "127.0.0.1:8080"
	conn, err := net.Dial("tcp", addr) //1、建立链接
	if err != nil {
		log.Fatal("链接失败")
		return
	}

	defer conn.Close()  //1.1、注意关闭链接
	log.Printf("connected")

	fmt.Fprintf(conn, "TIME1:%s\n", time.Now().Format(dateFormat)) //2、发送数据
	fmt.Fprintf(conn, "TIME2:%s\n", time.Now().Format(dateFormat)) //发送数据
	fmt.Fprintf(conn, "TIME3:%s\n", time.Now().Format(dateFormat)) //发送数据,最后一个不使用\n,server会自动添加换行
	fmt.Fprintf(conn, "quit\n")  //2.1、发送推出提示消息 ,必须要加\n ,因为服务端是readline,是匹配换行符作为一行的

	reader := bufio.NewReader(conn)
	for {  //3、接收客户端的消息
		line, _, err := reader.ReadLine()
		if err !=nil {
			log.Fatal(err)
			break
		}
		log.Printf("收到回复:%s", string(line))
	}
}

13.3、小项目

13.3.1、聊天小工具

实现功能:客户端和服务端可以交互,收发消息

服务端

package main

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

const dateFormat  = "2006-01-02 15:04:05"

func main(){
	addr := "127.0.0.1:8080"
	listener,err := net.Listen("tcp",addr)  //1、创建监听
	if err != nil {
		log.Fatal("监听失败: ",err)
		os.Exit(128)
	}
	defer listener.Close() //1.1、注意关闭监听

	scanner := bufio.NewScanner(os.Stdin)

	for {
		conn,err := listener.Accept() //2、循环创建链接接收新的客户端请求
		if err != nil {
			log.Println("接收数据失败",err)
			continue //这里不用else的原因,是不建议函数的层级太深
		}

		//defer conn.Close() //链接要及时关闭,defer是延迟到函数推出的时候执行,因此要用匿名函数,保证链接不会泄露
		func() {  //3、读取客户端内容
			defer conn.Close() //2.1、链接要及时关闭
			log.Printf("client[%s] is connecte...", conn.RemoteAddr())
			reader := bufio.NewReader(conn)
			for { //3.循环处理客户端的单个链接
				line, _, err := reader.ReadLine()  //3.1、从单个链接中读取客户端消息内容
				if err != nil {
					log.Print(err)
					break
				} else {
					if string(line) == "quit" {  //应用client和server同时读的情况
						fmt.Println("REMOTE:",conn.RemoteAddr())
						conn.Close()
						break
					}
				}
				fmt.Printf("接收到的数据有: %s\n", string(line)) //这里string(line)中带有换行,因此format中不需要添加\n
				fmt.Print("请输入要回复的内容:")
				scanner.Scan()
				fmt.Fprintf(conn, "Received: %s\n", scanner.Text()) // //3.2、从单个链接中回复客户端消息
			}
		}()
	}
}

客户端

package main

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


func main() {
	addr := "127.0.0.1:8080"
	conn, err := net.Dial("tcp", addr) //1、建立链接
	if err != nil {
		log.Fatal("链接失败")
		return
	}

	defer conn.Close()  //1.1、注意关闭链接
	fmt.Println("connected")

	scanner := bufio.NewScanner(os.Stdin)
	reader := bufio.NewReader(conn)

	for {  //3、接收客户端的消息
		fmt.Print("请输入消息:")
		scanner.Scan()
		fmt.Fprintf(conn,"%s\n",scanner.Text())

		line, _, err := reader.ReadLine()
		if err !=nil {
			log.Fatal(err)
			conn.Close()
			break
		}
		log.Printf("收到回复:%s", string(line))
	}
}

13.3.2、文件传输小工具

D:\Program_language\PRD\PG1\src>main.exe -h  用法
Usage of main.exe:
  -d string
        目录名 (default "/opt/")
  -f string
        文件名 (default "/opt/a.file")
  -m string
        工作模式server还是client (default "server|client")
  -s string
        服务端地址 (default "127.0.0.1:2000")
server模式会一直在前台运行,client可以向同一个server发送多次文件,路径和文件名不能包含中文

-----------------------> 代码内容
package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"os"
	"regexp"
	"strings"
)

func main() {
	//1、参数解析
	var fileName, workMode, serverAddr, dirName string
	flag.StringVar(&fileName, "f", "/opt/a.file", "文件名")
	flag.StringVar(&dirName, "d", "/opt/", "目录名")
	flag.StringVar(&workMode, "m", "server|client", "工作模式server还是client")
	flag.StringVar(&serverAddr, "s", "127.0.0.1:2000", "服务端地址")
	flag.Parse() //解析参数

	r, err := regexp.MatchString(`([0-9]{1,3}\.)([0-9]{1,3}\.)([0-9]{1,3}\.)([0-9]{1,3}):(\d){2,5}`, serverAddr)
	if r != true || err != nil {
		fmt.Println("地址错误:", r)
		os.Exit(126)
	}

	if workMode == "server" {
		//目录判断,暂时不做
		fmt.Printf("参数解析完毕: 保存的目录为:%s 监听地址:%s\n", dirName, serverAddr)
		err = server(serverAddr, dirName)
		if err != nil {
			fmt.Println("something error: ", err)
			os.Exit(126)
		}
		fmt.Println("服务端正常退出")
	} else if workMode == "client" {
		f, err := os.Open(fileName)
		if err != nil {
			fmt.Println("文件打开异常,错误信息为: ", err)
			os.Exit(127)
		}
		f.Close()
		fmt.Printf("参数解析完毕: 要传递的文件:%s 接收端地址:%s\n", fileName, serverAddr)
		err = client(serverAddr, fileName)
		if err != nil {
			fmt.Println("客户端错误:", err)
			os.Exit(125)
		}
		fmt.Println("客户端正常退出")
	} else {
		fmt.Println("必须选择一种工作模式: server或者client")
		os.Exit(128)
	}
}

func server(serverAddr string, dirName string) (err error) {
	//1、监听端口
	listerer, err := net.Listen("tcp", serverAddr)
	if err != nil {
		fmt.Println("端口监听失败")
		return err
	}

	//2、创建channel同步完成情况

	//3、异步处理数据
	for {
		conn, err := listerer.Accept()
		defer listerer.Close()
		if err != nil {
			fmt.Println("accept Error", err)
			break
		}

		//先读取fileName
		tmp := make([]byte, 1024)
		n, err := conn.Read(tmp[:])
		if err != nil {
			fmt.Println("读取文件名 error", err) //如果读取错误,注意返回
			break
		}
		fName := dirName + "/" + string(tmp[:n])
		//先读取fileName

		go processConn(conn, fName)
	}
	return
}

func processConn(conn net.Conn, fName string) {
	fileObj, err := os.OpenFile(fName, os.O_CREATE|os.O_RDWR, os.ModePerm)
	if err != nil {
		fmt.Println("文件打开错误: ", err)
		return
	}
	defer fileObj.Close()
	tmp := make([]byte, 4096)
	for {
		n, err := conn.Read(tmp[:])
		if err != nil {
			fmt.Println("Read error", err) //如果读取错误,注意返回
			return
		}
		fmt.Printf("已经读读取了: %d\n", n)
		_, err = fileObj.Write(tmp[:n])
		if err != nil {
			fmt.Println("写入失败")
			break
		}
		defer conn.Close()
	}
}

func client(serverAddr string, fileName string) (err error) {
	//1、创建和关闭连接
	conn, err := net.Dial("tcp", serverAddr)
	if err != nil {
		fmt.Println("连接打开失败", err)
		return
	}
	defer conn.Close()

	//2、打开文件
	fileObj, err := os.Open(fileName)
	if err != nil {
		fmt.Println("Open error")
		return err
	}

	defer fileObj.Close()

	//3、传输文件
	data := make([]byte, 4096)

	//先传递文件名
	n := strings.LastIndex(fileName, "/") //提取文件名
	fName := fileName[n+1:]
	_, err = conn.Write([]byte(fName))
	if err != nil {
		fmt.Println("写入失败fName", err)
		return err
	}

	fileObj.Seek(0, io.SeekStart) //从头开始读
	for {
		n, err := fileObj.Read(data)
		if err == io.EOF || n == 0 {
			fmt.Println("文件读取完毕")
			break
		}
		if err != nil {
			fmt.Println("Read失败: ", err)
			return err
		}
		_, err = conn.Write(data[:n])
		if err != nil {
			fmt.Println("写入失败", err)
			return err
		}
	}
	return nil //到最后了说明正常读取完毕了,传递内容为空
}

13.2.3、打印网卡信息

/*
打印出:网卡名称、网卡MAC地址、网卡IPv4和IPv6的地址 
*/
package main

import (
	"fmt"
	"net"
)

func main(){
	intfs,_ := net.Interfaces()
	for _,v := range intfs {
		fmt.Printf("网卡名称:[%s]\tMAC地址:[%s]\t",v.Name,v.HardwareAddr) //本地连接* 2 c2:30:49:85:34:ef 1500
		adrs,_ := v.Addrs()
		for k,m := range adrs {
			if k == 0  {
				fmt.Printf("IPV6地址:[%s]\t",m.String())
			} else if k == 1 {
				fmt.Printf("IPV4地址:[%s]\t",m.String())
			} else {
				fmt.Printf("IP地址:[%s]\t",m.String())
			}
		}
		fmt.Println()
	}
}

13.4、黏包

13.4.1、粘包示例

  • server端
package main

import (
   "net"
   "fmt"
   "io"
)

func process(conn net.Conn){
   defer conn.Close() //3、处理完毕请求,注意关闭
   var msg [1024]byte
   for {
      n,err := conn.Read(msg[:])  //4、读取内容,一次读取124字节
      if err == io.EOF {  //5、这里在client 主动关闭连接的时候才会匹配到,如果客户端没有主动关闭连接,这里也没有匹配 io.EOF 会出现打印的内容缺失几条
         break
      }
      if err != nil {
         fmt.Println("读取错误",err)
         return
      }
      fmt.Println("收到数据为:",string(msg[:n]))
   }
}
//三类资源: listener(打开监听端口后)   conn(listener,Accept())后,

func main(){
   listener,err := net.Listen("tcp","127.0.0.1:20000")  //1、监听端口,注意关闭
   if err != nil {
      fmt.Println("监听失败,",err)
      return
   }
   defer listener.Close()

   for {
      conn, err := listener.Accept() //2、接收新的链接
      if err != nil {
         fmt.Println("接收连接失败",err)
      }
      go process(conn)
   }
}
  • client端
package main

import (
   "net"
   "fmt"
)

func main(){
   conn,err := net.Dial("tcp","127.0.0.1:20000")  //1、打开tcp连接
   if err != nil {
      fmt.Println("打开连接失败,",err)
      return
   }
   defer conn.Close()  //2、开启连接后注意关闭  //如果客户端没有主动关闭连接,这里也没有匹配 io.EOF 会出现打印的内容缺失几条
   info := []byte("hello world !")
   for i:=0;i<20;i++ {
       //time.Sleep(500*time.Millisecond)  加上延迟这样也可以实现 server端逐行输出的效果
      conn.Write(info)
   }
}
  • 输出结果

收到数据为: hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world !hello world !

客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。我们期望的是,一条数据只返回一次,每个hello world对应一行,而不是多条黏到一起成为一行。如果接收区满的情况下,还有可能会出现 半截数据。

  • 粘包原因

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

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

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

13.4.2、粘包处理方法1

粘包解决方法:

  • 客户端发送一次就断开连接,需要发送数据的时候再次连接,典型如http
  • 包头+数据的格式,根据包头信息读取到需要分析的数据

方法1:客户端发送一次断开一次

客户端代码修改为:
func main() {
	info := []byte("hello world !")
	for i := 0; i < 20; i++ {  //在循环内打开和关闭连接
		conn, err := net.Dial("tcp", "127.0.0.1:20000") //1、打开tcp连接
		if err != nil {
			fmt.Println("打开连接失败,", err)
			return
		}
		//time.Sleep(500*time.Millisecond)  加上延迟这样也可以实现 server端逐行输出的效果
		conn.Write(info)
		conn.Close()
	}
}

13.4.3、buffer

出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。其实就是协议,协议定义了包头长度,数据大小等信息。只不过这里不像tcp协议那么完整

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

  • 大端小端(高位在前还是高位在后)
1)bufio包
	三个对象:
		Reader
		Writer
		ReadWriter
	方法:	
        reader.read*
        writer.write*
2)bytes包
	三个对象:
		bytes.Reader
        bytes.buffer: 
        bytes:newbuffer: func NewBuffer(buf []byte) *Buffer
	方法:
		Reader.Read*
		buffer.Read*
		buffer.Write*

1、创建缓冲区

  • bytes.buffer

bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte,初始化 b1 := new(bytes.Buffer) //直接使用 new 初始化,可以直接使用

Buffer 就像一个集装箱容器,可以存东西,取东西(存取数据)
创建 一个 Buffer (其实底层就是一个 []byte, 字节切片)
向其中写入数据 (Write mtheods)
从其中读取数据 (Readmethods)

  • bytes:newbuffer

NewBuffer使用buf作为参数初始化Buffer bytesBuffer := bytes.NewBuffer([]byte{})

func main() {
	buf1 := bytes.NewBufferString("hello")
	buf2 := bytes.NewBuffer([]byte("hello"))
	buf3 := bytes.NewBuffer([]byte{'h', 'e', 'l', 'l', 'o'})
	//   以上三者等效,输出//hello
	buf4 := bytes.NewBufferString("")
	buf5 := bytes.NewBuffer([]byte{})
	//  以上两者等效,输出//""
	fmt.Println(buf1.String(), buf2.String(), buf3.String(), buf4, buf5, 1)
}

2、写入到缓冲区

  • bufio.write: func Write(w io.Writer, order ByteOrder, data interface{}) error
参数列表:
1)w  可写入字节流的数据
2)order  特殊字节序,包中提供大端字节序和小端字节序
3)data  需要解码的数据
返回值:error  返回错误
功能说明:
Write将data序列化成字节流写入w中。data必须是固定长度的数据值或固定长数据的slice,或指向此类数据的指针。写入w的字节流可用特殊的字节序来编码。另外,结构体中的(_)名的字段讲忽略。
err := binary.Write(pkg, binary.LittleEndian, length) 将lenght (4字节)的数据写入到pkg中
  • bufio.read: func Read(r io.Reader, order ByteOrder, data interface{}) error
1)r  可以读出字节流的数据源
2)order  特殊字节序,包中提供大端字节序和小端字节序
3)data  需要解码成的数据
返回值:error  返回错误
功能说明:Read从r中读出字节数据并反序列化成结构数据。data必须是固定长的数据值或固定长数据的slice。从r中读出的数据可以使用特殊的 字节序来解码,并顺序写入value的字段。当填充结构体时,使用(_)名的字段讲被跳过。

err := binary.Read(lengthBuff, binary.LittleEndian, &length)
从 lengthBuff 中读出字节数据并反序列化

13.4.5、粘包处理方法2

方法2:读取包头数据格式进行分析

13.5、UDP

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

13.5.1、UDP服务端

package main

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

func main(){
   udpconn,err := net.ListenUDP("udp",&net.UDPAddr{  //1、server监听地址
      IP:net.IPv4(127,0,0,1),   //net.IPv4 返回一个IP类型的变量
      Port:30000,
   })
   if err != nil {
      fmt.Println("listen udp failed.",err)
      return
   }
    //2、注意及时关闭
    defer udpconn.Close()
   //不需要建立连接,直接通信
   var data [1024]byte
   for {
      n,addr,err := udpconn.ReadFromUDP(data[:])  //3、server循环读取数据
      //函数签名:func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) //int代表读取了多少个字节,Addr是对应的地址i,error错误信息
      if err != nil {
         fmt.Println("read from udp failed..",err)
         return
      }
      fmt.Printf("收到数据:%v addr:%v count:%v\n",string(data[:n]),addr,n)  //3、server打印读取到的数据
      reply := strings.ToUpper(string(data[:n]))
      //4、回复数据
      udpconn.WriteToUDP([]byte(reply),addr) 
       //函数签名:func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
   }
}

13.5.2、UDP客户端

package  main

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

func main(){
   //1、建立连接
   conn,err := net.DialUDP("udp",nil,&net.UDPAddr{
      IP:net.IPv4(127,0,0,1),
      Port:30000,
   })  //func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) ,本地地址,远程地址
   if err != nil {
      fmt.Println("建立连接失败",err)
      return
   }
   //2、建立连接后注意关闭连接
   defer conn.Close()
   var reply [1024]byte
   reader := bufio.NewReader(os.Stdin)
   //3、循环发送数据
   for {
      fmt.Print("请输入内容:")
      msg,_ := reader.ReadString('\n')
      conn.Write([]byte(msg))  //注意这里是Write而不是 WriteToUDP
       //服务端已经获取到了client的addr,因此要使用WriteToUdp,而client已经和DialUDP了server,一次你可以直接使用 Write
      n,remoteaddr,err := conn.ReadFromUDP(reply[:]) //4、收到的内容放到reply中
      if err != nil {
         fmt.Println("reply msg failed,err:",err)
      }
      fmt.Printf("收到回复信息:%v 远程地址:%v\n",string(reply[:n]),remoteaddr)
   }
}

十四、web开发

简单的web不再需要框架即可

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

  • HTTP协议本身无状态:浏览器发起请求(request),服务端response。浏览器在收到response之后,按照HTML/CSS/JS的规则去渲染整个页面

    • HTML:超文件标记语言
    • CSS:层叠样式表,规定了HTTP中标签的具体样式(颜色\背景\大小\位置) //(让人穿上衣服,动起来)
    • JavaScript:一种跑在浏览器的编程语言(让人动起来)

14.1、HTTP协议简介

  • HTTP请求

HTTP请求消息分为消息头和消息主题(可选),消息头和消息主体用空白行分隔,示例如下:

在HTTP1.1(1999年)中支持了keep-alive,提高传输效率。http2(2015年)

第一行(请求行):三个空格分隔的元素组成,HTTP方法 请求的URL 使用的HTTP版本
	请求方法:GET(无消息主体)/POST/HEAD(响应的消息主体为空)/PUT/DELETE/TRACE/OPTIONS/CONNECT
	URL:请求的资源路径
	版本:HTTP 1.0/1.1 2.0,常用1.1;,在1.1版本中请求消息必须包含 Host 请求头
	
第二行(请求头):
	Host: 指定请求访问的主机名,当1个web站点部署在多个主机上时需要使用Host消息头
	User-Agent: 客户端标识
	Accept: 服务端支持的...
	Referer: 请求来源 
	Cookie: 跟踪用户状态
	Connection: keep-alive ,保持链接
最后(消息体)
  • HTTP响应

http响应消息分为消息头和消息主体

server: 旗标,指明使用的web服务器软件
set-cookie: 设置cookie信息,在随后向服务器发送的请求中由cookie消息头返回
content-type: 指定消息主体类型
content-length: 指定消息主体的字节长度

http包提供了HTTP服务器和窗口端的开发接口,内置web服务器。针对web服务器开发流程分为:

  • 1、定义处理器/处理器函数
  • 接收用户数据
    • 返回信息
  • 2、启动服务器

注意:在真正公司中web开发的时候,一般是结合web框架进行开发的。一般很少直接使用内置的net/http库进行开发

14.2、http入门

14.2.1、简单使用

http服务创建-处理函数

package main

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

/* http服务创建三步骤
1、定义处理器/处理函数,需要满足 ,函数签名: handler func(ResponseWriter, *Request)
	处理函数对应面向过程,处理器对应面向对象
2、绑定URL 处理器/处理函数 ,函数签名: HandleFunc(pattern string, handler func(ResponseWriter, *Request))
3、启动web服务 ,函数签名: ListenAndServe(addr string, handler Handler)
 */

func main(){
	var handFunc = func(response http.ResponseWriter, request *http.Request) {

		fmt.Println(request)
		TIME := time.Now().Format("2006-01-02 15:04:05")

		//io.WriteString(response,TIME)  ,
 		fmt.Fprint(response,TIME)
	}

	http.HandleFunc("/time",handFunc)

	http.HandleFunc("/",func(resp http.ResponseWriter,req *http.Request) {
		ctx,err := ioutil.ReadFile("index.html")
		if err == nil {
			resp.Write(ctx)
		} else {
			fmt.Fprintf(resp,"欢迎")
		}
	})

	http.ListenAndServe("127.0.0.1:8080",nil)
}

http服务-处理器

package main

import (
	"io"
	"net/http"
	"time"
)

/*
处理器(结构体)对象必须要实现 方法:ServeHTTP(response http.ResponseWriter, request *http.Request)
1、定义处理器(结构体|对象),该对象需拥有方法 ServeHTTP(response http.ResponseWriter, request *http.Request)
2、绑定URL 处理器 http.Handle("/time",&TimeHandlerFunc{})
3、监听 http.ListenAndServe("127.0.0.1:8080",nil)

*/

type TimeHandlerFunc struct {
}

func (h * TimeHandlerFunc) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	TIME := time.Now().Format("2006-01-02 15:04:05")
	io.WriteString(response,TIME)
}

func main(){
	http.Handle("/time",&TimeHandlerFunc{})

	http.ListenAndServe("127.0.0.1:8080",nil)
}

14.2.2、请求参数解析

package main

import (
   "log"
   "net/http"
)
/*
URL匹配原则,完全匹配+前缀优先。比如请求url /a/b/c 但是只有/a有 处理器/处理函数,则使用/a 进行解析
HTTP协议 url?param 其中param的格式为 k1=v1&k2=v2这种格式
*/

func main(){

   http.HandleFunc("/test",func(resp http.ResponseWriter,req *http.Request) {
   	log.Printf("METHOD:%v\tADDR:%v\tHOST:%v\n",req.Method,req.RemoteAddr,req.Host)
   	//log.Printf("TYPE:%T\n",req.Header)  //TYPE:http.Header
   	//log.Println(req.Header.Get("User-Agent")) //Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36

   	req.ParseForm()
   	log.Println(req.Form)  //curl -X GET curl  "http://127.0.0.1:8080/test?a=b&c=d" ,返回:map[a:[b] c:[d]]
   	//传递curl  "http://127.0.0.1:8080/test?a=b&c=d&m={\"tom\"=\"m\"\,\"jerry\"=\"f\"}" ,解析为:map[a:[b] c:[d] m:["tom"="m","jerry"="f"]]
   })

   http.HandleFunc("/test2",func(resp http.ResponseWriter,req *http.Request) {
   	log.Println(req.FormValue("a"))  //FormValue会自动调用ParseForm,不需要单独进行参数解析 ,输出param中key为a对应的value 
   })

   http.ListenAndServe("127.0.0.1:8080",nil)
}
  • GET请求解析
    • ParseForm()解析请求内容
      • 然后可以使用req.Form打印请求param内容
    • FormValue("a") 直接解析form中key为a对应的value

14.2.3、POST请求

package main

import (
   "fmt"
   "io"
   "log"
   "net/http"
   "os"
)

/*
POST请求的数据是在请求体中,请求体中有编码格式的。
常见的格式:
   application/x-www-form-urlencoded //k1=v2&k2=v2
   multipart/form-data  #上传文件,可以指定多个文件上传,对应curl -F "a=@a.txt" -F "b=@b.txt"
   application/json #json个数
   text/plain #文本
*/

func main(){
   http.HandleFunc("/test",func(resp http.ResponseWriter,req *http.Request) {
   	//Form中包含请求体和请求param中的数据。PostForm只有请求体中的数据
   	fmt.Println(req.Method) //POST
   	req.ParseForm()  //请求 curl  "http://127.0.0.1:8080/test?a=b&c=d&a=3" -d "a=aaa&b=bbb"
   	log.Println(req.Form)  //map[a:[aaa b 3] b:[bbb] c:[d]]
   	log.Println(req.Form.Get("a")) //aaa //使用Get只是获取了 请求体中的数据。
   	log.Println(req.Form["a"]) //[aaa b 3] //使用GET的Form,解析出来a有三个值
   	log.Println(req.PostForm) //map[a:[aaa] b:[bbb]],这里只解析了post中 -d后的内容
   	log.Println(req.PostFormValue("a")) //只能获取到 aaa
   })

   http.HandleFunc("/file",func(resp http.ResponseWriter,req *http.Request) {
   	/*	上传文件
   		url数据 => Form,FormValue 
   		body值类型 => Form,FormValue,PostForm,PostFormValue,req.MulPartForm.Value
   		file类型 => req.MultiPartForm.File["name"][0].Open(), FormFile()
   		FormFile直接拿到file,不用调用 ParseForm

   		示例:curl  "http://127.0.0.1:8080/file"  -F "a=@server.go" -F "b=server.go"
   		返回:&{map[b:[server.go]] map[a:[0xc000192050]]} POST
   	*/

   	req.ParseMultipartForm(1024*1024)
   	log.Printf("FORM内容:%v\t请求方法:%v\t",req.MultipartForm,req.Method)
   	
   	//参数不进行检查
   	file,Header,_ := req.FormFile("a")  //获取a对应的文件信息
   	io.Copy(os.Stdout,file) //用法1

   	//file,err := req.MultipartForm.File["a"][0].Open() //用法2
   	//io.Copy(os.Stdout,file)


   	log.Printf("文件头部:%v\t文件名称%v\t文件大小%v\n",Header.Header,Header.Filename,Header.Size)
   	/*
   	curl -X POST  http://127.0.0.1:8080/file -F "a=@main.go"
   	文件头部:map[Content-Disposition:[form-data; name="a"; filename="main.go"] Content-Type:[application/octet-stream]]    文件名称main.go 文件大小1195
   	注意: curl -F的用法,@不加的话,是只传递文件名
   	 */
   })
   http.ListenAndServe("127.0.0.1:8080",nil)
}
  • Post请求解析
    • ParseForm()解析请求内容
      • 然后可以使用req.Form打印请求param内容
      • req.Form.Get("a") ,只打印form请求体中的内容
      • req.Form["a"] ,可以打印param和form中的所有内容
      • req.PostForm,只打印form请求体中的数据
      • req.PostFormValue("a"),只打印form中key为a对应的值

14.2.4、JSON解析

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

//需要从请求体中读取

func main() {
	http.HandleFunc("/json",func(resp http.ResponseWriter, req *http.Request) {
		err := req.ParseForm()
		if err != nil {
			log.Println("解析失败",err)
		}
		
		info := make(map[string]interface{})
		
		err = json.NewDecoder(req.Body).Decode(&info) //创建一个decoder,进行decode
		if err != nil {
			fmt.Println("解析失败:",err)
		}
		fmt.Println(info)
		for k,v := range info {
			fmt.Println(k,v)
		}
	})
	
	//curl -X POST  "http://127.0.0.1:8080/json" -H "Content-Type: application/json"  -d "{\"abc\":123}"
	//解析结果: map[abc:123]
	
	http.ListenAndServe("127.0.0.1:8080",nil)
}
  • json.NewDecoder(req.Body).Decode(&info)

14.2.5、cookie

package main

import (
   "fmt"
   "net/http"
   "strconv"
   "strings"
)

/*
   Cookie浏览器中存储
   读取cookie中数据counter
   如果没有设置counter,则counter+1,设置在浏览器中
*/

/*
type Cookie struct {
       Name  string  cookie名称
       Value string	cookie的value

       Path       string    // optional
       Domain     string    // optional
       Expires    time.Time // optional
       RawExpires string    // for reading cookies only

       // MaxAge=0 means no 'Max-Age' attribute specified.
       // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
       // MaxAge>0 means Max-Age attribute present and given in seconds
       MaxAge   int  过期时间
       Secure   bool  https的时候携带
       HttpOnly bool 	cookie可以被js和http操作,设置httponly则js不能操作
   	...  其他忽略
}
*/

func parseCookie(cookie string) map[string]string {
   cookieMap := make(map[string]string)
   if len(cookie) == 0 {  //如果cookie为空,旧不执行下面的逻辑,防止报错
   	return  cookieMap
   }

   values := strings.Split(cookie,";")
   for _,v := range values {
   	kv := strings.Split(v,"=")
   	cookieMap[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
   }
   return  cookieMap
}


func main(){
   counter := 1
   http.HandleFunc("/cookie",func(resp http.ResponseWriter, req *http.Request) {
   	ck := req.Header.Get("Cookie")
   	cookie := parseCookie(ck)
   	fmt.Println("COOKIE:",cookie)

   	counter ++
   	ck2 := http.Cookie{
   		Name: "counter",
   		Value: strconv.Itoa(counter),
   		HttpOnly: true,

   	}
   	http.SetCookie(resp,&ck2)
   })

   http.ListenAndServe("127.0.0.1:8080",nil)  //浏览器访问,即可看到cookie内容
   //cookie可以有多个key,比如在chrome->dev模式->application->Cookies[127.0.0.1:8080]->添加cookie,进行请求则结果为:counter=5; aaaa=bbb
}
  • 获取cookie: req.Headeer.Get("Cookie")
  • 初始化cookie对象 http.Cookie{}
  • 设置cookie: http.SetCookie(resp,$Cookie对象) 。注意客户端可以携带多个cookie

14.2.6、重定向

package main

import (
	"fmt"
	"net/http"
)

func main(){
	//重定向,让浏览器发起请求到新的地址上
	http.HandleFunc("/login",func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp,"登录界面")
	})

	http.HandleFunc("/home",func(resp http.ResponseWriter, req *http.Request) {
		http.Redirect(resp,req,"/login",302)
		//fmt.Fprintln(resp,"首页")
	})

	http.ListenAndServe("127.0.0.1:8080",nil)
}
  • http.Redirect

14.2.7、静态文件服务器

package main

import (
	"net/http"
)

func main(){
	http.Handle("/http2/",http.StripPrefix("/http2/",http.FileServer(http.Dir("."))))
	http.Handle("/http2/",http.FileServer(http.Dir("www")))  //注意:如果没有使用stripPrefix,会在 ./www/http2 目录下找
	//注意 监听路径一定要使用"/$路径/"的格式,前后斜线不能丢
    
	http.ListenAndServe("127.0.0.1:8080",nil)
}

注意路径问题 http.Handle("/http2/",http.StripPrefix("/http2/",http.FileServer(http.Dir("."))))

14.2.8、客户端

package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/url"
)

func main(){
	//get请求
	resp,err := http.Get("http://127.0.0.1:8080/cookie")
	if err != nil {
		fmt.Println("建立连接失败",err)
	}
	fmt.Println(resp.Cookies())

	fmt.Printf("COOKIE:%v\tCODE:%v",resp.Cookies(),resp.StatusCode)

	//post请求-application/json
	buffer := bytes.NewBufferString(`{"a":1}`)

	reply,err := http.Post("http://127.0.0.1:8080/json","application/json",buffer)
	if err != nil {
		fmt.Println("post请求失败",err)
	}
	fmt.Println("REPLY:",reply)

	//post请求-urlencode
	params := url.Values{}
	params.Add("a","a1")
	params.Add("a","a2")
	params.Add("a","a3")

	rep,err := http.PostForm("http://127.0.0.1:8080/json",params)
	if err != nil {
		fmt.Println("postform请求失败",err)
	} else {
		fmt.Println("REP:",rep)
	}
}

客户端请求:

  • GET: http.Get("http://127.0.0.1:8080/cookie")

  • POST: http.Post("http://127.0.0.1:8080/json","application/json",buffer)

  • POSTFORM:

    • 先初始化url.Values{}对象,添加kv数据

    • 然后http.PostForm("http://127.0.0.1:8080/json",params)

  • 在涉及到设置头部参数,比如Content-Type: application/json的时候就需要使用http.Client对象的相关方法

14.2.9、Client对象

  • 构建http.Client对象
  • 构造消息
    • 使用strings.NewReader 构建消息体
    • 构建http.NewRequest 对象,并设置消息头部信息req.Header.Set
  • 发起请求client.Do(req)
  • 关闭body resp.Body.Close()
  • 关闭连接client.CloseIdleConnections()

功能:实现推送消息到钉钉告警

package conf

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
)

type MSG struct {
	Text map[string]string
	Msgtype string
	AtALL bool
}


type Mark struct {  //注意,使用json.Marsha需要外部的函数能够访问到结构体属性,因此必须大写
	Title string
	Text string
}

type And struct {
	IsAtAll bool
}

type MarkDown struct {
	Msgtype string
	Markdown Mark
	At And
}


func PostMessageMarkDown(uri,title,text string)(err error) {
	client := &http.Client{}  //1、构造http.Client对象,注意传递地址

	info := MarkDown{
		Msgtype: "markdown",
		Markdown: Mark{
			Title: title,
			//Text: "## 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n",
			Text: text,
		},
		At: And{
			IsAtAll: true,
		},
	}

	b,err := json.Marshal(info)
	if err != nil || len(b) <= 2 {
		err = errors.New("json内容解析失败")
		return err
	}

	t := strings.ToLower(string(b))
	t = strings.Replace(t,"isatall","isAtall",1)

	//body := strings.NewReader(`{"msgtype": "text","text": {"content":"我就是我, 是哈22哈火"}}`)
	body := strings.NewReader(t)  	//2、构造请求体
	req,err := http.NewRequest("POST",uri,body)  //3、构造 http.Request对象,存储body数据
	if err !=nil {
		return err
	}
	req.Header.Set("Content-Type","application/json")  //4、设置请求头部信息
	req.Header.Set("User-Agent","curl/7.29.0")

	resp,err := client.Do(req)  //发起请求
	if err !=nil  {
		return err
	}
	if resp.StatusCode != 200 {
		err = errors.New("消息推送失败")
		resp.Body.Close() //如果这里加上defer,会导致消息不能及时发送, 5、关闭body连接
		return
	}

	resp.Body.Close() //如果这里加上defer,会导致消息不能及时发送, 5、关闭body连接
	client.CloseIdleConnections()  //这个加上,不然可能推送消息不及时,我们需要的是短链接,推送一个接收一个的那种
	fmt.Println("RESP:",resp)
	return
}

参考地址:https://open.dingtalk.com/document/group/custom-robot-access

14.3、正则补充

基础参考:https://www.cnblogs.com/hmtk123/p/15095692.html#_label5_9

针对三种类型数据,有三个函数机型match
func QuoteMeta(s string) string #特殊字符加上 \ ,表示真实的特殊字符
func Match(pattern string, b []byte) (matched bool, err error)
func MatchString(pattern string, s string) (matched bool, err error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, err error)

本包采用的正则表达式语法,默认采用perl标志。某些语法可以通过切换解析时的标志来关闭。

单字符:

        .              任意字符(标志s==true时还包括换行符)
        [xyz]          字符族
        [^xyz]         反向字符族
        \d             Perl预定义字符族
        \D             反向Perl预定义字符族
        [:alpha:]      ASCII字符族
        [:^alpha:]     反向ASCII字符族
        \pN            Unicode字符族(单字符名),参见unicode包
        \PN            反向Unicode字符族(单字符名)
        \p{Greek}      Unicode字符族(完整字符名)
        \P{Greek}      反向Unicode字符族(完整字符名)

结合:

        xy             匹配x后接着匹配y
        x|y            匹配x或y(优先匹配x)

重复:

        x*             重复>=0次匹配x,越多越好(优先重复匹配x)
        x+             重复>=1次匹配x,越多越好(优先重复匹配x)
        x?             0或1次匹配x,优先1次
        x{n,m}         n到m次匹配x,越多越好(优先重复匹配x)
        x{n,}          重复>=n次匹配x,越多越好(优先重复匹配x)
        x{n}           重复n次匹配x
        x*?            重复>=0次匹配x,越少越好(优先跳出重复)
        x+?            重复>=1次匹配x,越少越好(优先跳出重复)
        x??            0或1次匹配x,优先0次
        x{n,m}?        n到m次匹配x,越少越好(优先跳出重复)
        x{n,}?         重复>=n次匹配x,越少越好(优先跳出重复)
        x{n}?          重复n次匹配x

实现的限制:计数格式x{n}等(不包括x*等格式)中n最大值1000。负数或者显式出现的过大的值会导致解析错误,返回ErrInvalidRepeatSize。

分组:

        (re)           编号的捕获分组
        (?P<name>re)   命名并编号的捕获分组
        (?:re)         不捕获的分组
        (?flags)       设置当前所在分组的标志,不捕获也不匹配
        (?flags:re)    设置re段的标志,不捕获的分组

标志的语法为xyz(设置)、-xyz(清楚)、xy-z(设置xy,清楚z),标志如下:

        I              大小写敏感(默认关闭)
        m              ^和$在匹配文本开始和结尾之外,还可以匹配行首和行尾(默认开启)
        s              让.可以匹配\n(默认关闭)
        U              非贪婪的:交换x*和x*?、x+和x+?……的含义(默认关闭)

边界匹配:

        ^              匹配文本开始,标志m为真时,还匹配行首
        $              匹配文本结尾,标志m为真时,还匹配行尾
        \A             匹配文本开始
        \b             单词边界(一边字符属于\w,另一边为文首、文尾、行首、行尾或属于\W)
        \B             非单词边界
        \z             匹配文本结尾

转义序列:

        \a             响铃符(\007)
        \f             换纸符(\014)
        \t             水平制表符(\011)
        \n             换行符(\012)
        \r             回车符(\015)
        \v             垂直制表符(\013)
        \123           八进制表示的字符码(最多三个数字)
        \x7F           十六进制表示的字符码(必须两个数字)
        \x{10FFFF}     十六进制表示的字符码
        \*             字面值'*'
        \Q...\E        反斜线后面的字符的字面值

字符族(预定义字符族之外,方括号内部)的语法:

        x              单个字符
        A-Z            字符范围(方括号内部才可以用)
        \d             Perl字符族
        [:foo:]        ASCII字符族
        \pF            单字符名的Unicode字符族
        \p{Foo}        完整字符名的Unicode字符族

预定义字符族作为字符族的元素:

        [\d]           == \d
        [^\d]          == \D
        [\D]           == \D
        [^\D]          == \d
        [[:name:]]     == [:name:]
        [^[:name:]]    == [:^name:]
        [\p{Name}]     == \p{Name}
        [^\p{Name}]    == \P{Name}

Perl字符族:

        \d             == [0-9]
        \D             == [^0-9]
        \s             == [\t\n\f\r ]
        \S             == [^\t\n\f\r ]
        \w             == [0-9A-Za-z_]
        \W             == [^0-9A-Za-z_]

ASCII字符族:

        [:alnum:]      == [0-9A-Za-z]
        [:alpha:]      == [A-Za-z]
        [:ascii:]      == [\x00-\x7F]
        [:blank:]      == [\t ]
        [:cntrl:]      == [\x00-\x1F\x7F]
        [:digit:]      == [0-9]
        [:graph:]      == [!-~] == [A-Za-z0-9!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]
        [:lower:]      == [a-z]
        [:print:]      == [ -~] == [ [:graph:]]
        [:punct:]      == [!-/:-@[-`{-~]
        [:space:]      == [\t\n\v\f\r ]
        [:upper:]      == [A-Z]
        [:word:]       == [0-9A-Za-z_]
        [:xdigit:]     == [0-9A-Fa-f]
package main

import (
	"fmt"
	"regexp"
)

func main ()  {
	patter := "^jfidjs^[abc]$" //特殊字符
	f := regexp.QuoteMeta(patter) //\^jfidjs\^\[abc\]\$
	fmt.Println(f)

	reg,_ := regexp.Compile("^1[35]\\d{9}$")  //匹配只有电话的行
	fmt.Println(reg.MatchString("15230887677")) //true

	reg,_ = regexp.Compile("[,;#\\t]") //指定多个分隔符
	fmt.Println(reg.Split("abc#dc\tbmfjifd;mno",-1))  //[abc dc bmfjifd mno]

	//贪婪模式
	/*
	贪婪匹配 :贪婪模式在整个表达式匹配成功的提前下,尽可能多的匹配。即,正则表达式一般趋于最大长度匹配。如果用regex匹配str(Regex.Match(str,regex)),结果为abcaxc。//默认为贪婪模式
	非贪婪匹配:在整个表达式匹配成功的前提下,以最少的匹配字符。如果使用regex匹配str(Regex.Match(str.regex)),结果为abc。 regexp.Compile("(?U)[,;#\\t]")  ,(?U) 开启非贪婪模式
	*/

	//reg.Longest() //将非贪婪模式,转换为贪婪模式	
}

十五、web爬虫

  • web 1.0 主要是html文件 (主要介绍这一部分)
  • web 2.0 前后端分类,前端使用js动态的解析,传输json,xml等格式,而非html格式的内容

HTML文档介绍:

标签类型:<html>...</html>大标签,<meta charset=utf-8>小标签
  • goquery类似于jquery的使用方式,使用css选择器
  • htmlquery 使用xpath选择器

15.1、goquery概述

其中goquery主要有两个对象:

  • Document: 创建Document对象
    • 1、发起请求:goquery.NewDocument(url)
  • Selection:选择器对象
    • 2、解析
      • 查找元素:Find
      • 查找子元素:ChildrenFilterd
      • 获取内容:Text/html #获取标签内部的信息<a>test</a> value test
      • 获取属性:Attr("href") #对应href对应的value
      • 遍历元素:Each
    • 3、选择器
      • .class/#id/tag
      • 复合选择器
        • $标签.$class value
    • 4、快捷使用
      • 对于goquery,选择路径:Chrome->Elements->选中位置->右键 Copy[Copy Selector]
      • 对于htmlquery,选择路径:Chrome->Elements->选中位置->右键 Copy[Copy xpath]

15.1.1、基础介绍

从html页面中匹配元素的方式: regexp,xpath,goquery,CSS选择器

chrome中支持元素提取,xpath,selector,JS path

正则表达式的可读性和可维护性不够好。goquery类似于Jquery的东西,是go语言版本的实现。
链接: https://pkg.go.dev/github.com/PuerkitoBio/goquery goquery 暴露了两个结构体:DocumentSelection. Document 表示一个 HTML 文档,Selection 用于像 jQuery 一样操作,支持链式调用

type Selection struct {
	Nodes    []*html.Node
	document *Document
	prevSel  *Selection
}

type Document struct {
	*Selection
	Url      *url.URL
	rootNode *html.Node
}

创建文档

1)使用io.Reader创建 NewDocumentFromReader(r io.Reader)
	a := strings.NewReader(str)
	dom, err := goquery.NewDocumentFromReader(a)
2)使用url
	d,e := goquery.NewDocument(url string)

goquery把html对象抽象为了Document对象

Selecttion元素的方法:这里举出部分内容

  • func (s *Selection) Html() (ret string, e error) 获取文档的html内容
  • func (s *Selection) Text() string 类似于html ;一般用html
  • func (s *Selection) Find(selector string) *Selection 查找元素,返回一个selection对象
  • func (s *Selection) Each(f func(int, *Selection)) *Selection 遍历
  • 更多: https://pkg.go.dev/github.com/PuerkitoBio/goquery#section-readme

15.1.2、查找文档

基础函数1:

//函数,遍历每一个匹配元素
func goquery_selector(htmlContent string, selectorContent string) {
	//strings.NewReader(htmlContent) 返回的是 io.Reader,负责将htmlContent转换成为io.Reader
	dom, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
	if err != nil {
		fmt.Println(err)
	}

	//符合选择器selectorContent的所有集合,都会进行Each进行遍历,遍历后打印
	dom.Find(selectorContent).Each(func(i int, selection *goquery.Selection) { //遍历所有节点
		fmt.Println(selection.Text())
	})
}

func readFromlocalhtml() {
	//1、从文件中读取内容,并格式化为string
	f, err := ioutil.ReadFile("b.html")
	if err != nil {
		fmt.Println("读取文件失败!", err)
	}
	htmlContent := string(f)
	htmlContent = `
<div class="profile-navbar clearfix">
<a class="item " href="/people/jixin/asks">提问<span class="num">1336</span></a>
<a class="item " href="/people/jixin/answers">回答<span class="num">785</span></a>
<a class="item " href="/people/jixin/posts">文章<span class="num">91</span></a>
<a class="item " href="/people/jixin/collections">收藏<span class="num">44</span></a>
<a class="item " href="/people/jixin/logs">公共编辑<span class="num">51648</span></a>
</div>
<div class="profile-post clearfix">
<a class="item " href="/people/jixin/asks">提问2<span class="num">1336</span></a>
<a class="item " href="/people/jixin/answers">回答2<span class="num">785</span></a>
<a class="item " href="/people/jixin/posts">文章2<span class="num">91</span></a>
<a class="item " href="/people/jixin/collections">收藏2<span class="num">44</span></a>
<a class="item " href="/people/jixin/logs">公共编辑2<span class="num">51648</span></a>
</div>
`

	//2、从string中创建Document对象
	var s string
	fmt.Print("请输出表达式: ")
	fmt.Scan(&s)
	selection := dom.Find(s)

	goquery_selector(htmlContent, s)
}

func main() {
	readFromlocalhtml()
}
D:\Program_language\PRD\PG1\src>main.exe
请输出表达式: a.item  //表达式1: <a 标签中,属性value为 item 的所有doc 
提问1336
回答785
文章91
收藏44
公共编辑51648
提问21336
回答2785
文章291
收藏244
公共编辑251648

请输出表达式: span.num  //只提取< span 中,属性value为 num的所有doc
1336
785
91
44
51648
1336
785
91
44
51648
请输出表达式: .item //同a.item
请输出表达式: .num //同span.num
请输出表达式: span[class="num"]  //同 span.num ;<span标签中,包含属性class="num"的doc

选择器:参考 https://www.w3school.com.cn/cssref/css_selectors.asp

选择器 例子 例子描述
.class .intro 选择 class="intro" 的所有元素。
.class1.class2 .name1.name2 选择 class 属性中同时有 name1 和 name2 的所有元素。
.class1 .class2 .name1 .name2 选择作为类名 name1 元素后代的所有类名 name2 元素。
#id #firstname 选择 id="firstname" 的元素。
* * 选择所有元素。
element p 选择所有

元素。

element.class p.intro 选择 class="intro" 的所有

元素。

element,element div, p 选择所有
元素和所有

元素。

element element div p 选择
元素内的所有

元素。

element>element div > p 选择父元素是
的所有

元素。

element+element div + p 选择紧跟
元素的首个

元素。

element1~element2 p ~ ul 选择前面有

元素的每个

    元素。
[attribute] [target] 选择带有 target 属性的所有元素。
[attribute=value] [target=_blank] 选择带有 target="_blank" 属性的所有元素。
[attribute~=value] [title~=flower] 选择 title 属性包含单词 "flower" 的所有元素。
[attribute|=value] [lang|=en] 选择 lang 属性值以 "en" 开头的所有元素。
[attribute^=value] a[href^="https"] 选择其 src 属性值以 "https" 开头的每个 元素。
[attribute$=value] a[href$=".pdf"] 选择其 src 属性以 ".pdf" 结尾的所有 元素。
[attribute**=value*] a[href*="w3schools"] 选择其 href 属性值中包含 "abc" 子串的每个 元素。
:active a:active 选择活动链接。
::after p::after 在每个

的内容之后插入内容。

::before p::before 在每个

的内容之前插入内容。

:checked input:checked 选择每个被选中的 元素。
:default input:default 选择默认的 元素。
:disabled input:disabled 选择每个被禁用的 元素。
:empty p:empty 选择没有子元素的每个

元素(包括文本节点)。

:enabled input:enabled 选择每个启用的 元素。
:first-child p:first-child 选择属于父元素的第一个子元素的每个

元素。

::first-letter p::first-letter 选择每个

元素的首字母。

::first-line p::first-line 选择每个

元素的首行。

:first-of-type p:first-of-type 选择属于其父元素的首个

元素的每个

元素。

:focus input:focus 选择获得焦点的 input 元素。
:fullscreen :fullscreen 选择处于全屏模式的元素。
:hover a:hover 选择鼠标指针位于其上的链接。
:in-range input:in-range 选择其值在指定范围内的 input 元素。
:indeterminate input:indeterminate 选择处于不确定状态的 input 元素。
:invalid input:invalid 选择具有无效值的所有 input 元素。
:lang(language) p:lang(it) 选择 lang 属性等于 "it"(意大利)的每个

元素。

:last-child p:last-child 选择属于其父元素最后一个子元素每个

元素。

:last-of-type p:last-of-type 选择属于其父元素的最后

元素的每个

元素。

:link a:link 选择所有未访问过的链接。
:not(selector) :not(p) 选择非

元素的每个元素。

:nth-child(n) p:nth-child(2) 选择属于其父元素的第二个子元素的每个

元素。

:nth-last-child(n) p:nth-last-child(2) 同上,从最后一个子元素开始计数。
:nth-of-type(n) p:nth-of-type(2) 选择属于其父元素第二个

元素的每个

元素。

:nth-last-of-type(n) p:nth-last-of-type(2) 同上,但是从最后一个子元素开始计数。
:only-of-type p:only-of-type 选择属于其父元素唯一的

元素的每个

元素。

:only-child p:only-child 选择属于其父元素的唯一子元素的每个

元素。

:optional input:optional 选择不带 "required" 属性的 input 元素。
:out-of-range input:out-of-range 选择值超出指定范围的 input 元素。
::placeholder input::placeholder 选择已规定 "placeholder" 属性的 input 元素。
:read-only input:read-only 选择已规定 "readonly" 属性的 input 元素。
:read-write input:read-write 选择未规定 "readonly" 属性的 input 元素。
:required input:required 选择已规定 "required" 属性的 input 元素。
:root :root 选择文档的根元素。
::selection ::selection 选择用户已选取的元素部分。
:target #news:target 选择当前活动的 #news 元素。
:valid input:valid 选择带有有效值的所有 input 元素。
:visited a:visited 选择所有已访问的链接。
package main

import (
	"fmt"
	"log"
	"strings"

	goquery "github.com/PuerkitoBio/goquery"
	// colly "github.com/gocolly/colly"
	// queue "github.com/gocolly/colly/queue"
)

//封装goquery函数
func goquery_selector(htmlContent string, selectorContent string) {
	//strings.NewReader(htmlContent) 返回的是 io.Reader,负责将htmlContent转换成为io.Reader
	dom, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
	if err != nil {
		log.Fatal(err)
	}

	//符合选择器selectorContent的所有集合,都会进行Each进行遍历,遍历后打印
	dom.Find(selectorContent).Each(func(i int, selection *goquery.Selection) { //遍历所有节点
		fmt.Println(selection.Text())
	})
}


func parent_childselector() {
	htmlContent := `<body>
		<div lang="zh_cn"> DIV1 </div>
		<div lang="cn"> DIV2 </div>
		<div lang="japan"> DIV3 </div>
		<span>
			<div>DIV4</div
		</span> 
	</body>
	`
	selectorContent := "body>div"                  //body中包含div的都要
	goquery_selector(htmlContent, selectorContent) //筛选出body.div的所有元素的值,结果为DIV1-DIV4

}

//先查找一个元素,进行偏移查找
func pre_nextselector() {
	htmlContent := `<body>
		<p>P3</p>
		<div>DIV001</div>
		<div lang="cn"> DIV6 </div>
		<div lang="zh"> DIV1 </div>
		<p>P1</p>
		<tom>
			<div>DIV44</div>
			<p> p5 </p>
			<div>DIV14</div>
		</tom> 

		<div lang="cn"> DIV2 </div>
		<div lang="japan"> DIV3 </div>
		<span>
			<div>DIV4</div>
			<p> p5 </p>
			<div>DIV14</div>
		</span> 
		
		<p>P2</p>
	</body>
	`
	selectorContent := "div[lang=zh]+p" //P1 筛选出当前元素的下一个p元素,如果p元素和当前元素中间有其他元素则不会输出
	goquery_selector(htmlContent, selectorContent)
	fmt.Println()
	selectorContent = "div[lang=zh]~p" //P1,P2 ,对同级别往下包含p的都输出,p5,P3不包含
	goquery_selector(htmlContent, selectorContent)
	fmt.Println()
	selectorContent = "div:first-of-type" //DIV001,DIV44,DIV4 筛选中同级中第一次出现(div)类型的元素
	goquery_selector(htmlContent, selectorContent)
	fmt.Println()
	selectorContent = "div:first-child" //DIV44,DIV4 ; 同级别div为当前父元素的第一个子元素的则匹配
	goquery_selector(htmlContent, selectorContent)

}

func main() {
	// fmt.Println("begin call function element_selector ")
	// element_selector()
	// fmt.Println("begin call function element_idselector ")
	// element_idselector()
	// fmt.Println("begin call function class_idselector ")
	// class_selector()
	// fmt.Println("begin call function parent-child_idselector ")
	// parent_childselector()

	fmt.Println("begin call pre_nextselector")
	pre_nextselector()
}

15.1.3、属性和过滤操作

  • Attr(attrName string) (val string, exists bool): 返回属性值和该属性是否存在,类似从 map中取值

  • AttrOr(attrName, defaultValue string) string: 和上一个方法类似,区别在于如果属性不存在,则返回给定的默认值

  • Filter() 过滤

  • Has() 是否包含

func readFromlocalhtml() {
	//1、从文件中读取内容,并格式化为string
	f, err := ioutil.ReadFile("b.html")
	if err != nil {
		fmt.Println("读取文件失败!", err)
	}
	htmlContent := string(f)
	htmlContent = `
<div class="profile-navbar test">
<a class="item " href="/people/jixin/asks">提问<span class="num">1336</span></a>
<a class="item " href="/people/jixin/answers">回答<span class="num">785</span></a>
<a class="item " href="/people/jixin/posts">文章<span class="num">91</span></a>
<a class="item " href="/people/jixin/collections">收藏<span class="num">44</span></a>
<a class="item " href="/people/jixin/logs">公共编辑<span class="num">51648</span></a>
<a class2="item " href="/test/jixin/logs">test属性<span class="num">testvalue</span></a>

</div>
<div class="profile-navbar clearfix">
<a class="item " href="/people/jixin/asks">提问2<span class="num">1336</span></a>
<a class="item " href="/people/jixin/answers">回答2<span class="num">785</span></a>
<a class="item " href="/people/jixin/posts">文章2<span class="num">91</span></a>
<a class="item " href="/people/jixin/collections">收藏2<span class="num">44</span></a>
<a class="item " href="/people/jixin/logs">公共编辑2<span class="num">51648</span></a>
</div>
`

	//2、从string中创建Document对象
	dom, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
	if err != nil {
		fmt.Println(err)
	}

	text := dom.Find("div.profile-navbar").Find("span.num").Eq(7).Text() //对应785,对应第二个div的 "回答2"  ,
    //匹配这个表达式的共有 11行,最大值为10(0-10)。Eq($id)代表取出第几个元素

	fmt.Println(text)

	href, _ := dom.Find("div.profile-navbar").Find("a.item").Eq(6).Attr("href") //对应 "/people/jixin/answers"
	fmt.Println(href)

	//遍历输出所有属性
	dom.Find("div.profile-navbar").Find("a.item").Each(func(i int, selection *goquery.Selection) { //遍历所有节点
		selection.Has("href")
		attr, _ := selection.Attr("href")
		fmt.Println(attr)
	})

	/*
	   /people/jixin/answers
	   /people/jixin/asks
	   /people/jixin/answers
	   /people/jixin/posts
	   /people/jixin/collections
	   /people/jixin/logs
	   /people/jixin/asks
	   /people/jixin/answers
	   /people/jixin/posts
	   /people/jixin/collections
	   /people/jixin/logs

	*/

}

func main() {
	readFromlocalhtml()
}

15.2、练习

15.2.1、简单的http客户端

func main() {
	resp, err := http.Get("http://www.baidu.com")
	defer resp.Body.Close() //注意:程序在使用完response后必须关闭回复的主体
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("Status:%v StatusCode:%v\n", resp.Status, resp.StatusCode) //状态和状态码 Status:200 OK StatusCode:200
	fmt.Println("Header", resp.Header)
	//Header map[Accept-Ranges:[bytes] Cache-Control:[no-cache] Connection:[keep-alive] Content-Length:[227] Content-Type:[text/html] Date:[Fri, 06 Aug 2021 11:22:58 GMT] ...
	fmt.Println("Body: ", resp.Body) //Body:  &{0xc000216040 {0 0} false <nil> 0x30b420 0x30b3a0}

	buf := make([]byte, 4096) //定义切片缓冲区
	var result string
	for {
		n, err := resp.Body.Read(buf)
		if n == 0 {
			fmt.Println("read finished...")
			break
		}
		if err != nil && err != io.EOF {
			fmt.Println("resp.body read Error: ", err)
			return
		}
		result += string(buf[:n])
	}
	fmt.Println("result=", result)
}

爬虫工作逻辑:

  1. 明确URL,发起连接
  2. 发送请求,获取服务器返回的响应数据
  3. 保存得到的数据
  4. 处理数据

双向爬取:

  1. 横向爬取:是指在爬取的网页页面中,以页为单位"下一页";寻找站点分页规律,按照规律逐页爬取
  2. 纵向爬取:在一个页面内,按不同的条目为单位,寻找各个条目之间的关系,一条一条的爬取网页中的数据

15.2.2、贴吧爬虫简单版

func httpget(url string) (result string, err error) {
	resp, err1 := http.Get(url)
	if err1 != nil {
		err = err1
		return
	}
	defer resp.Body.Close() //注意关闭链接
	buf := make([]byte, 4096)
	for {
		n, err2 := resp.Body.Read(buf)
		if n == 0 {
			fmt.Println("文件读取完毕!")
			break
		}
		if err2 != nil && err2 != io.EOF {
			fmt.Println("-====> ", err2)
			err = err2
			return
		}
		result += string(buf)

	}

	return
}

func writeTofile(value string, fid int) (err error) {
	fd, err := os.Create(fmt.Sprintf("tieba_%d.html", fid))
	if err != nil {
		return err
	}
	n, err := fd.WriteString(value)
	if err != nil {
		return err
	}
	fd.Close() //写入完成后,注意关闭文件
	fmt.Println(n)
	return err
}

func working(start, end int) (err error) {
	for ; start <= end; start++ {
		url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((start-1)*50)
		fmt.Println("Visiting URL->: ", url)
		result, err := httpget(url)
		if err != nil {
			fmt.Println("HTTP get failed. ", err)
			continue
		}
		// fmt.Println(result)
		err = writeTofile(result, start)
		if err != nil {
			fmt.Println(err)
		}
	}
	return err
}

func main() {
	var start, end int
	fmt.Print("请输入起始页: ")
	fmt.Scan(&start)
	fmt.Print("请输入结束页: ")
	fmt.Scan(&end)

	if start <= 0 || start > end {
		fmt.Println("输入错误,请重新输入")
	}

	err := working(start, end)
	if err != nil {
		fmt.Println(err)
	}
}

/* 功能概述
1)提示用户指定起始页和终止页
2)创建working函数,循环爬取各页面html
3)获取每一页的URL -> 下一页等于前一页*50
4)封装httpget()函数,爬取单个网页的html,http.Get(),resp.Body.Close(),resp.Body.Body()
5)写入到文件 os.Create(),file.WriteString(),file,Close()
*/

15.2.3、贴吧爬虫并发版

需要封装一个爬取单个页面的函数版即可

package main

import (
	"io"
	"net/http"
	"os"
	"strconv"

	// "encoding/csv"
	// "encoding/json"
	// "io"
	// "net/http"
	// "strings"

	// "regexp"
	"fmt"
	// "log"
	// "os"
	// goquery "github.com/PuerkitoBio/goquery"
	// colly "github.com/gocolly/colly"
	// queue "github.com/gocolly/colly/queue"
)

func httpget(id int, url string, page chan int) {
	var result string
	resp, err1 := http.Get(url)
	if err1 != nil {
		fmt.Println(err1)
		return
	}
	defer resp.Body.Close() //注意关闭链接
	buf := make([]byte, 4096)
	for {
		n, err2 := resp.Body.Read(buf)
		if n == 0 {
			break
		}
		if err2 != nil && err2 != io.EOF {
			fmt.Println("-====> ", err2)
			return
		}
		result += string(buf)
	}

	//写入到文件中
	fd, err := os.Create(fmt.Sprintf("tieba_%d.html", id))
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = fd.WriteString(result)
	if err != nil {
		fmt.Println(err)
		return
	}
	fd.Close() //写入完成后,注意关闭文件
	page <- id //用于与主进程完成同步

}

func working(start, end int) {
	page := make(chan int)
	for s := start; s <= end; s++ {
		url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((s-1)*50)
		fmt.Println("Visiting URL->: ", url)
		go httpget(s, url, page)
	}

	for i := start; i <= end; i++ {
		fmt.Printf("第%d页写入完成 !\n", <-page)
	}
}

func main() {
	var start, end int
	fmt.Print("请输入起始页: ")
	fmt.Scan(&start)
	fmt.Print("请输入结束页: ")
	fmt.Scan(&end)

	if start <= 0 || start > end {
		fmt.Println("输入错误,请重新输入")
	}

	working(start, end)
}

/*
1.封装爬取一个网页的代码 为一个函数  func httpget(id int, url string, page chan int)
2.在working函数中循环 协程,调用httpget
3.为防止主go程提前结束,创建channel实现同步
4.在 httpget函数结尾,向chanel中写入内容,channel <- index;并在 working for循环内部读取channel,<- channel;写入n次,读取n次
*/

15.2.4、美女图片爬取

从`https://pic.netbian.com/4kmeinv/index_3.html` 中下载所有图片,设定起止页面
package main

import (
	"bufio"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
)

var  start,end int
var baseUrl string = "https://pic.netbian.com/4kmeinv/"
var mainUrl string = "https://pic.netbian.com"
var wg sync.WaitGroup //对每一个 下载图片的请求使用一个goroutine处理
var wg2 sync.WaitGroup  //多个页面的并发


func get(page,url string){  //下载文件并保存
	defer wg.Done()
	baseName := strings.SplitAfter(url,"/")
	fileName := baseName[len(baseName)-1]  //获取文件名
	turl := mainUrl + url
	log.Println("URL:",turl)

	//创建文件,
	file,err := os.OpenFile(fmt.Sprintf("%s/%s",page,fileName),os.O_CREATE|os.O_WRONLY,os.ModePerm)
	if err != nil {
		log.Printf("图片 [%v] 创建失败 %v",turl,err)
		return
	}
	defer file.Close()  //关闭文件

	//下载图片
	resp,err := http.Get(turl)
	defer resp.Body.Close()
	if err != nil {
		log.Printf("创建链接失败 [%v]",err)
		return
	}

	photo,err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Printf("读取图片信息 [%v]",err)
		return
	} else {
		n,err := file.Write(photo)
		if err != nil {
			log.Println("文件写入失败,",err)
			return
		} else {
			var result  float32
			result = float32(n)/1024
			log.Printf("GET Photo [%v] 文件大小:%.2fKB",mainUrl + url,result)
		}
	}
}


func httpget(page,url string){  //解析单个图片的url
	log.Println("httpget:",page,url)
	document,err := goquery.NewDocument(url)
	if err != nil {
		log.Println("HTTPGET 创建document失败",err)
		return
	}
	
	document.Find("div.photo-pic").Find("img").Each(func(i int,section *goquery.Selection){
		val,e :=  section.Attr("src")
		if e {
			wg.Add(1)
			//log.Printf("PHOTO URL [%v]",val)
			go get(page,val)
		}
	})
	wg.Wait()
}


func worker(page,url string)(err error){  // 对单个分页页面中20张图片进行解析,
	log.Println("worker:",page,url)
	document,err := goquery.NewDocument(url)
	if err != nil {
		log.Println("创建document失败",err)
		return
	}
   	document.Find("#main > div.slist > ul").Find("a").Each(func(i int,section *goquery.Selection){
//	document.Find("div.slist").Find("a").Each(func(i int,section *goquery.Selection){
		val,e :=  section.Attr("href")
		if e {
			log.Printf("URL [%v] start download",mainUrl + val)
			httpget(page,mainUrl + val)
		} else {
			fmt.Println(val)
		}
	})
	wg2.Done()
	return err
}

func main(){
	//1、获取起止id,对输入进行判断
	scan := bufio.NewScanner(os.Stdin)
	fmt.Print("请输入开始页面:")
	scan.Scan()
	start,err := strconv.Atoi(scan.Text())
	if err != nil {
		log.Println("输出有误,请输入数字",err)
		return
	}
	fmt.Print("请输入结束页面:")
	scan.Scan()
	end,err = strconv.Atoi(scan.Text())
	if err != nil {
		log.Println("输出有误,请输入数字",err)
		return
	}

	if start <= 0 || end  < start  {
		log.Println("开始页面必须小于截止页面,开始页面最小为1")
		return
	}

	var url,id string

	for ;start <= end ;start ++ {
		if start == 1 {
			url = baseUrl + "index.html"
			id = strconv.Itoa(start)

		} else {
			id = strconv.Itoa(start)
			url =  baseUrl + "index_" + id + ".html"
		}
		_,err := os.Stat(id) //如果不存在,就创建文件夹,用于区分每一页的图片
		if err != nil {
			err := os.Mkdir(id,0777)
			if err != nil {
				log.Println("目录创建失败",err)
			}
		}


		//适用于只有get本身并发的场景
		//err = worker(id,url)  //处理各个主url[分页主页面,]每个分页页面包含20张图片
		//if err != nil {
		//	log.Printf("URL [%v] 抓取失败...",url)
		//	break
		//}
		wg2.Add(1)
		go worker(id,url)  //处理各个主url[分页主页面,]每个分页页面包含20张图片
	}
	wg2.Wait()
}

十六、HTML

16.1、概述

16.1.1、Template

text/template包用于处理使用字符串模板和数据驱动生成目标字符串,在字符串模板中可使用 数据显示、流程控制、函数、管道、子模板等功能

html/template 在web开发一定要选择,展示数据的时候进行一些实体的转换,可以防止xss攻击

常用结构体

  • Template:

    • 常用函数:

      New: 创建模板
      ParseFiles:指定文件模板
      ParseGlob:指定文件模板匹配格式
      Must:帮助函数,对模板创建结果进行验证,并返回模板对象指针

    • 常用方法:
      Parse: 解析模板字符串
      ParseFiles:指定文件模板
      ParseGlob:指定文件模板匹配格式
      Execute:模板渲染
      ExecuteTemplate:指定模板执行模板渲染
      Funcs:指定自定义函数字典
      Clone:克隆模板进行模板复用

语法参考:

  • html/template: https://pkg.go.dev/html/template ,html模板是基于text模板的,学习之前建议先了解下text模板
  • text/template: https://pkg.go.dev/text/template

16.1.2、基础语法

步骤:

  • 1、定义模板文本
    • 定义模板函数:funcs := template.FuncMap{...} (可选),如果使用要在定义模板文件前定义
  • 2、解析模板文本生产模板对象
    • 方式1:tpl,_ := template.New("tp1").Parse($模板文件)
    • 方式2:tpl2 := htmltemplate.Must(htmltemplate.New("tp2").Parse(tpltext))
    • 方式3模板函数: tpl := template.Must(template.New("tpl").Funcs($函数).Parse(tplText))
  • 3、执行,带入参数值
    • tpl.Execute(os.Stdout,"abcdef")
    • tpl2.ExecuteTemplate(os.Stdout,"$模板名字","abcdef")
package main

import (
	"fmt"
	htmltemplate "html/template"
	"text/template"
	"os"
)

func main(){
	//  text/template和html/template对比
	text := "一见钟情我是 {{.}} 神" //{{ .}} 进行占位
	tpl,_ := template.New("tp1").Parse(text)
	tpl.Execute(os.Stdout,"神") //一见钟情我是 神 神
	tpl.Execute(os.Stdout,`<img src="xxx" />`) //一见钟情我是 <img src="xxx" /> 神

	fmt.Println()

	htmltpl,_ :=htmltemplate.New("tpl").Parse(text)
	htmltpl.Execute(os.Stdout,`<img src="xxx" />`) //进行了替换  一见钟情我是 &lt;img src=&#34;xxx&#34; /&gt; 神

	//Must用法,
	tpltext := "{{ . }}"
	//tpltext := "{{ . }"  //如果写错误,解析失败,会直接panic  神panic: template: tp2:1: unexpected "}" in operand
	tpl2 := htmltemplate.Must(htmltemplate.New("tp2").Parse(tpltext))
	tpl2.Execute(os.Stdout,"大家好")

	//切片
	tpl2.Execute(os.Stdout,[]int{1,2,3,4})  //[1 2 3 4]

	//map
	tpl2.Execute(os.Stdout,map[string]string{"one":"test","two":"testto"}) //map[one:test two:testto]

	//struct
	tpl2.Execute(os.Stdout, struct {  //{1 令狐冲}
		ID int
		Name string
	}{1,"令狐冲"})


	//index-索引
	fmt.Println()
	tplText := "{{ index . 1 }}"
	htmltpl,_ =htmltemplate.New("tpl").Parse(tplText)
	htmltpl.Execute(os.Stdout,[]int{1,2,3,4})  //2
	
	//map展示
	fmt.Println()
	tplText = "{{ .one }}-{{ .two }}-{{ .three }}" //返回value
	tpl2 = htmltemplate.Must(htmltemplate.New("tp2").Parse(tplText))
	tpl2.Execute(os.Stdout,map[string]string{"one":"1","two":"2"}) //1-2-
	
	//结构体元素展示-同上
}

16.1.3、流程控制语句

  • 使用 {{ block "template filename" . }} {{ end }} 来调用一个模板
package main

import (
	"fmt"
	"html/template"
	"os"
	"strings"
)

func main(){
	IF()
	RANGE()
	FUNCMAP()
	BLOCK()
	BLOCK2()
	//更多语法,参考: https://pkg.go.dev/text/template
}

func IF(){
	//如果是1就为男,0就位女
	fmt.Println("IF->")
	tplText := `
{{ .ID }}--{{ .Name }}
{{ if eq .Sex 1 }}男{{ else }}女{{ end}}
`
	tpl := template.Must(template.New("t1").Parse(tplText))  //template.New 中的 名称 t1 好像没啥用?
	tpl.Execute(os.Stdout, struct {
		ID int
		Name string
		Sex int
	}{1,"小刚",1})
}

func RANGE() {
	//如果是1就为男,0就位女
	tplText := `
{{ range . }}
	{{ .ID }} {{ .Name }} {{ .Sex }}
{{ end }}
`
	tpl := template.Must(template.New("t1").Parse(tplText))
	tpl.Execute(os.Stdout, []struct {
		ID   int
		Name string
		Sex  int //1为男,0为女
	}{{1,"小红",0},{2,"小刚",1}})
}

func FUNCMAP(){ //自定义函数
	//自定义函数又,
	funcs := template.FuncMap{
		"upper" : strings.ToUpper,
		"title" : func(text string) string{  //参数个数可以根据需要填写,不一定是一个
			return strings.ToUpper(text[:1]) + text[1:]
		},
	}

	tplText := "{{ upper . }}" //再模板中调用函数
	tplText2 := "{{ title . }}" //再模板中调用函数
	tpl := template.Must(template.New("tpl").Funcs(funcs).Parse(tplText))
	tpl2 := template.Must(template.New("tpl").Funcs(funcs).Parse(tplText2))
	tpl.Execute(os.Stdout,"test")
	fmt.Println()
	tpl2.Execute(os.Stdout,"test")
	fmt.Println()
}


func BLOCK(){
        tplText := `输入内容: {{ block "content" . }} {{ .}} {{ end }} `
        tpl := template.Must(template.New("tpl").Parse(tplText))
        //tpl.Execute(os.Stdout,"abcdef") //输入内容:  abcdef

        //tp1,_  := tpl.Parse(`{{ define "content" }} {{ len . }} {{ end }}`)
        //tp2,err  := tpl.Parse(`{{ define "content" }} {{ len . }} {{ end }}`)
        //if err != nil {fmt.Println(err) } //html/template: cannot Parse after Execute ,改为 text/template 可以解决,或者把 tpl.Execute(os.Stdout,"abcdef") 放到Parse后面

        tp3,_ := template.Must(tpl.Clone()).Parse(`{{ define "content" }} {{ len . }} {{ end }}`)

        //会把上面的 .Parse(tplText)中的content内容进行覆盖为 len . 的结果

        tpl.Execute(os.Stdout,"abcdef") //输入内容:  abcdef
        tp3.Execute(os.Stdout,"abcdef")  //输入内容:  6
        //tp2.Execute(os.Stdout,"abcdef")  //输入内容:  6

        //注意:如果想要define之前的内容,要clone;clone必须在execute之前;直接再次tpl.Parse 会导致都变成 define之后的
}

func BLOCK2(){
	//模板嵌入, template "leng" .  引入模板
	tplText := `
{{ define "leng" }} {{ len . }}{{ end }}
{{ define "raw" }}  {{ . }} {{ end }}
{{ template "leng" . }}
`

	tplText2 := `
{{ define "leng" }} {{ len . }}{{ end }}
{{ define "raw" }}  {{ . }} {{ end }}
{{ template "leng" . }}
`
	tpl := template.Must(template.New("tpl").Parse(tplText))
	tpl2 := template.Must(template.New("tpl").Parse(tplText2))
	
	tpl.Execute(os.Stdout,"abcdef") //6
	tpl2.ExecuteTemplate(os.Stdout,"leng","abcdef")  //6
	tpl2.ExecuteTemplate(os.Stdout,"raw","abcdef") //abcdef
	tpl2.Execute(os.Stdout,"abcdef") //6 使用的tplText2的默认模板 "leng" ,如果没有指定模板,即调用默认模板
}

16.1.4、结合html

>>>>> html/index.html
{{ range . }}
    {{ . }}
{{ end }}

{{ template  "len.html" . }}

index.html<br>


>>>>> html/len.html
{{ len . }}

len.html<br>


>>>>>> main.go
package main

import (
	"html/template"
	"os"
)

func main(){
	//tpl := template.Must(template.ParseFiles("html/index.html","html/len.html"))
	//tpl.ExecuteTemplate(os.Stdout,"index.html",[]int{1,2,3}) // 1  2 3 name对应文件名,当然也可以在 index.html中引用 len.html
	//tpl.ExecuteTemplate(os.Stdout,"len.html",[]int{1,2,3}) //3

	//使用技巧,可以加载所有的template,使用的时候,想加载哪个 ExecluteTemplate指定哪个

	tpl2 := template.Must(template.ParseGlob("html/*.html"))  //解析多个文件,传递的是文件匹配模式
	tpl2.ExecuteTemplate(os.Stdout,"index.html",[]int{1,2,3}) //使用方法和效果同上
}

16.2、实操

16.2.1、HTML标签示例

>>> main.go
package main

import "net/http"

func main(){
	addr := "127.0.0.1:1234"
		http.Handle("/static/",http.StripPrefix("/static/",http.FileServer(http.Dir("./html"))))

	http.ListenAndServe(addr,nil)
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我的第一个标签</title>
</head>
<body>
    测试页面
     <!--  注释内容,网页源码也是可以看到的 : Devtool页面介绍:
        Elements: 元素,为页面组织结构
        Console:主要是和js相关,调试js
        Sources:请求到的资源
        Network: 请求和响应信息,Preserve log是否清空历史数据。Disable Cache:缓存失效;XHR (ajax);WS websocket;
        Application: 检查 web 应用加载的所有资源


      -->
    <h1>H标签-标题</h1>
    <p>
        P标签-段落
        出自唐代元稹的《离思五首·其四》

        曾经沧海难为水,除却巫山不是云。
        取次花丛懒回顾,半缘修道半缘君。
    </p>
    <a href="https://www.bing.com" target="_blank">超链接标签-标签 _blank新页面打开</a> <br>
    <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F82%2Fce%2Fce%2F82cece1703856a860cb39d6a22d7ca26.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651196163&t=4fff92bca5883e2845d4771d0af7f4fc" alt="">
    <a href="./about.html">关于</a> <!--- 建议用相对路径-->

    <ol>
        <!--- 有序列表,会自动添加序号-->
        <li>洗衣服</li>
        <li>刷牙</li>
        <li>做饭</li>
    </ol>
	
    <ul>
        <!--- 无序列表,前面会添加一个 点 -->
        <li>洗衣服</li>
        <li>刷牙</li>
        <li>做饭</li>
    </ul>

    自定义标签
    <d1>
        <dt>令狐冲</dt>
        <dd>僻邪剑谱</dd>

        <dt>詹姆斯</dt>
        <dd>球王</dd>
    </d1>
    表格
    <table>
        <thead>
            <tr>
                <th>学号</th>
                <th>姓名</th>
                <th>性别</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>001</td>
                <td>令狐冲</td>
                <td>男</td>
            </tr>
            <tr>
                <td>002</td>
                <td>东方不败</td>
                <td>男</td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <td></td>
                <td></td>
                <td>平均年龄xx</td>
            </tr>
        </tfoot>
    </table>

    表单
    <!---  action="/register/" 设置提交到的url  method="post"设置提交使用的方法 -->
    <form >
        <label for="">登录名称: </label><input type="text" value="用户名(默认用户名)" name="username"> <br>
        <label for="">登录密码: </label><input type="text" value="用户密码(默认密码)" name="password"> <br>
        <label for="">性别:</label>
            <!-- 通过type+name进行分组,组内为单选。如果没有name则默认为多选,checked设置默认值--->
            <input type="radio" name="sex" value="0" checked="checked"><label for="">男</label>
            <input type="radio" name="sex" value="1"><label for="">女</label> <br>
        <label for="">备注: </label><textarea name="remark">这是备注信息,可以放大和缩小</textarea> <br>
        <label for="">爱好:(多选,type要求相同) </label>
            <input type="checkbox" name="hobby" value="basketball"><label for="">篮球</label>
            <input type="checkbox" name="hobby" value="tiaosheng"><label for="">跳绳</label>
            <input type="checkbox" name="dsf" value="tes"><label for="">跳1绳</label> <br>
        <label for="">部门【下拉框】,默认运维</label><select name="departmeng">
            <option value="develop">开发者</option>
            <option value="test">测试</option>
            <option value="ops" selected="selected">运维</option>
        </select> <br>
        <label for="">部署服务器【ctrl多选下拉框】</label><select name="departmeng" multiple="multiple">
            <option value="192.168.1.1">192.168.1.1</option>
            <option value="192.168.1.2">192.168.1.2</option>
            <option value="192.168.1.3">192.168.1.3</option>
        </select>

        <input type="submit" value="提交数据">
    </form>
    <!---  div 分块逻辑组合 块级别,span 也是一个逻辑组合 行级别-->
</body>
</html>

16.2.2、页面增删改查

  • 功能概述
实现一个学生信息管理系统,实现学生信息的增删改查

实现思路:
1、定义一个全局的 Students对象,存储所有student对象
2、分别定义 四个HandlerFunc处理,/add/,/del/,/modify/,/list/ 四个功能uri请求页面
3、每个HandlerFunc使用html/template进行模板渲染,并分别定义函数实现功能细节

注意事项:
1、前后端参数传递,展示页面展示了某个学生,修改页面引用的时候,要传递响应的信息修改界面
2、修改的数据流:展示页面list.html--[传递某个学生的学生id]->modify后端提取该学生信息信息--->modify.html展示--->页面修改后post提交-->modify后端处理
  • 展示页面html/list.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <table>
        <thead>
            <tr>
                <th>学号</th>
                <th>姓名</th>
                <th>年龄</th>
                <th>性别</th>
            </tr>
        </thead>
        <tbody>
            {{ range . }}
            <tr>
                <td>{{ .ID}}</td>
                <td>{{ .NAME}}</td>
                <td>{{ .AGE}}</td>
                <td>{{ .SEX}}</td>
                <td><a href="/del/?ID={{ .ID}}">删除</a></td>
                <td><a href="/modify/?ID={{ .ID}}">修改</a></td>
            </tr>
        </tbody>
            {{ end }}
    </table> <br>

    <a href="/add/"> 新增</a>
</body>
</html>

<a href="/del/?ID={{ .ID}}" 传递给其他界面,要操作的学生学号信息

  • 添加界面 html/add.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/add/" method="post">
    <label for="">姓名:</label>    <input type="text" name="name" value=""> <br/>
    <label for="">年龄:</label>    <input type="text" name="age" value=""> <br/>
    <label for="">性别:</label>
    <!-- 通过type+name进行分组,组内为单选。如果没有name则默认为多选,checked设置默认值--->
        <input type="radio" name="sex" value="男" checked="checked"><label for="">男</label>
        <input type="radio" name="sex" value="女"><label for="">女</label> <br>

    <input type="submit" value="新增">
</form>

</body>
</html>

  • 删除界面 html/del.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/del/?ID={{ .ID}}" method="post">
    学号:<input type="text" name="id" value="{{ .ID}}">
    姓名:<input type="text" name="name" value="{{ .NAME}}">
    年龄:<input type="text" name="status" value="{{ .AGE}}">
    性别:<input type="text" name="status" value="{{ .SEX}}">
    <input type="submit" value="删除">
</form>

</body>
</html>

  • 修改界面html/modify.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

要修改记录的学号是:{{ .ID }}

<form action="/modify/?ID={{ .ID}}" method="post">
    <label for="">姓名:</label>    <input type="text" name="name" value=""> <br/>
    <label for="">年龄:</label>    <input type="text" name="age" value=""> <br/>
    <label for="">性别:</label>
    <!-- 通过type+name进行分组,组内为单选。如果没有name则默认为多选,checked设置默认值--->
        <input type="radio" name="sex" value="男" checked="checked"><label for="">男</label>
        <input type="radio" name="sex" value="女"><label for="">女</label> <br>
    <input type="submit" value="提交">
</form>

</body>
</html>

  • 主函数main.go
package main

import (
	"fmt"
	"html/template"
	"net/http"
	"strconv"
)

type Student struct {
	ID int
	NAME string
	AGE int
	SEX string
}

var Students =  []*Student{
	&Student{1,"小明",10,"男"},
	&Student{2,"小红",12,"女"},
}

func main(){
	addr := ":9999"
	http.HandleFunc("/list/", func(resp http.ResponseWriter,req  *http.Request){
		tpl := template.Must(template.ParseFiles("html/list.html"))
		tpl.ExecuteTemplate(resp,"list.html",Students)
	})

	http.HandleFunc("/add/", func(resp http.ResponseWriter,req  *http.Request){

		if req.Method == http.MethodPost {
			name := req.PostFormValue("name")
			age := req.PostFormValue("age")
			ag,_ := strconv.Atoi(age)

			sex := req.PostFormValue("sex")
			taskAdd(name,ag,sex)

			http.Redirect(resp,req,"/list",302)
		}

		tpl := template.Must(template.ParseFiles("html/add.html"))
		tpl.ExecuteTemplate(resp,"add.html",nil)
	})

	http.HandleFunc("/modify/", func(resp http.ResponseWriter,req  *http.Request){
		fmt.Printf("",)

		req.ParseForm()
		id,err := strconv.Atoi(req.FormValue("ID"))  //获取要修改的学号
		if err != nil {
			fmt.Printf("modify get ID error : %v",err)
		}

		if req.Method == http.MethodPost {  //针对提交的数据进行处理
			name := req.PostFormValue("name")
			status := req.PostFormValue("status")
			age := req.PostFormValue("age")
			ag ,_ := strconv.Atoi(age)
			taskModify(id,ag,name,status)

			http.Redirect(resp,req,"/list",302)
		}

		fmt.Println("ID",id)
		var a *Student  //展示要修改的数据信息

		for _,v := range Students {
			if v.ID == id {
				a = v
			}
		}

		tpl := template.Must(template.ParseFiles("html/modify.html"))
		tpl.ExecuteTemplate(resp,"modify.html",a)
	})


	http.HandleFunc("/del/", func(resp http.ResponseWriter,req  *http.Request){
		req.ParseForm()
		id,err := strconv.Atoi(req.FormValue("ID"))
		if err != nil {
			fmt.Printf("del get ID error : %v",err)
		}
		if req.Method == http.MethodPost {
			taskDel(id)

			http.Redirect(resp,req,"/list",302)
		}

		fmt.Println("ID",id)
		var a *Student

		for _,v := range Students {
			if v.ID == id {
				a = v
			}
		}

		tpl := template.Must(template.ParseFiles("html/del.html"))
		tpl.ExecuteTemplate(resp,"del.html",a)




		//http.Redirect(resp,req,"/list/",302)
	})


	http.ListenAndServe(addr,nil)
}


func taskAdd(name string,age int ,sex string){
	id := 0
	for _,v := range Students {
		if id < v.ID {
			id = v.ID
		}
	}

	Students = append(Students,&Student{
		id+1,name,age,sex,
	})
}

func taskDel(id int){
	var index int
	for k,v := range Students {
		if v.ID == id {
			index =k
		}
	}
	Students = append(Students[:index],Students[index+1:]...)
}

func taskModify(id int,age int,name,sex string){
	fmt.Println("修改的信息:")

	var index int
	for k,v := range Students {
		if v.ID == id {
			index = k
			fmt.Println("修改的信息:",index,id,Students[k])
		}
	}
	Students[index].NAME = name
	Students[index].SEX = sex
	Students[index].AGE = age
}

16.3、实操二

其他页面同 实验一

package main

/*
需求:学生信息管理系统
实现步骤:
	1)定义学生结构体和全局结构体(定义全局结构体方法),存储学生信息
	2)定义http 处理函数,并监听
	3)设置各个html页面完成功能展示

*/

import (
	"html/template"
	"log"
	"mineproject/minebase"
	"net/http"
	"strconv"
)

var ALLSTU = &minebase.ALLSTUTYPE{ //初始化的数据
	minebase.Student{0, "小明", 16, "男"},
	minebase.Student{1, "小红", 18, "女"},
}

func main() {
	http.HandleFunc("/list/", func(resp http.ResponseWriter, req *http.Request) {
		tpl := template.Must(template.ParseFiles("html/list.html"))
		tpl.ExecuteTemplate(resp, "list.html", ALLSTU)
	})

	http.HandleFunc("/add/", func(resp http.ResponseWriter, req *http.Request) {
		if req.Method == http.MethodPost {
			name := req.PostFormValue("name")
			age := req.PostFormValue("age")
			sex := req.PostFormValue("sex")
			if sex != "男" && sex != "女" {
				log.Printf("未知性别:%v\n", sex)
				http.Redirect(resp, req, "/list/", 302)
			}
			age2, err := strconv.Atoi(age)
			if err != nil {
				log.Printf("年龄错误:%v\n", age2)
				http.Redirect(resp, req, "/list/", 302)
			}

			ALLSTU.STUADD(name, sex, uint(age2))
			http.Redirect(resp, req, "/list/", 302)
		}
		tpl := template.Must(template.ParseFiles("html/add.html"))
		tpl.ExecuteTemplate(resp, "add.html", nil)
	})

	http.HandleFunc("/del/", func(resp http.ResponseWriter, req *http.Request) {
		id, err := strconv.Atoi(req.FormValue("ID")) //这里不能用postformvalue因为这里没有用post方法
		if err != nil {
			log.Printf("id错误:%v %v\n", id, err)
			http.Redirect(resp, req, "/list/", 302)
		}
		log.Printf("要删除的学生学号是:%v\n", id)
		ALLSTU.STUDEL(uint(id))
		http.Redirect(resp, req, "/list/", 302)
	})

	http.HandleFunc("/modify/", func(resp http.ResponseWriter, req *http.Request) {
		//1)获取要修改的学生学号;2)如果非POST方法则展示 学生信息;3)如果是post则执行修改动作
		var stu minebase.Student //存储要修改的学生原有的信息

		id, err := strconv.Atoi(req.FormValue("ID")) //这里不能用postformvalue因为这里没有用post方法
		if err != nil {
			log.Printf("id错误:%v %v\n", id, err)
			http.Redirect(resp, req, "/list/", 302)
		}
		log.Printf("要修改的学生学号是:%v\n", id)

		for _, v := range *ALLSTU { //找到要修改的学生
			if v.ID == uint(id) {
				stu = v
			}
		}

		if req.Method == http.MethodPost {
			log.Printf("开始修改:%v\n", stu)
			name := req.PostFormValue("name")
			age := req.PostFormValue("age")
			sex := req.PostFormValue("sex")
			if sex != "男" && sex != "女" {
				log.Printf("未知性别:%v\n", sex)
				http.Redirect(resp, req, "/list/", 302)
			}
			age2, err := strconv.Atoi(age)
			if err != nil {
				log.Printf("年龄错误:%v\n", age2)
				http.Redirect(resp, req, "/list/", 302)
			}

			stu = minebase.Student{ID: uint(id), NAME: name, AGE: uint(age2), SEX: sex}

			log.Printf("开始修改:%v\n", stu)
			err = ALLSTU.STUModify(stu.ID, stu)
			if err != nil {
				log.Printf("添加失败:%v\n", err)
			}
			http.Redirect(resp, req, "/list/", 302)
		}
		tpl := template.Must(template.ParseFiles("html/modify.html"))
		tpl.ExecuteTemplate(resp, "modify.html", stu)

	})

	http.ListenAndServe(":9999", nil)
}

base.go

package minebase

import (
	"errors"
	"fmt"
	"log"
)

type Student struct { //单个学生对象
	ID   uint
	NAME string
	AGE  uint
	SEX  string
}

type ALLSTUTYPE []Student

func (a *ALLSTUTYPE) STULIST() {
	for k, v := range *a {
		fmt.Printf("Index:%v\t学号:%v姓名:%v年龄%v性别%v\n", k, v.ID, v.NAME, v.AGE, v.SEX)
	}
}

func (a *ALLSTUTYPE) STUADD(name, sex string, age uint) (err error) { //添加和修改的时候需要进行参数校验
	id := uint(len(*a))
	if sex != "男" && sex != "女" {
		log.Println("性别错误")
		return errors.New("性别错误")
	}

	if age <= 0 || age >= 120 {
		log.Println("年龄范围0-120")
		return errors.New("年龄范围0-120")
	}

	if len(name) <= 0 || len(name) >= 12 {
		log.Println("姓名最长不超过12个字符")
		return errors.New("姓名最长不超过12个字符")
	}

	//fmt.Println("==>", id, name, age, sex, ":||", *a)

	tmp := append(*a, Student{id, name, age, sex})
	//fmt.Println("==>", tmp)
	*a = tmp //回传给 *a才对,如果设置了 a= *tmp 则不会改变原有的值

	return nil
}

func (a *ALLSTUTYPE) STUDEL(id uint) (err error) { //添加和修改的时候需要进行参数校验
	//删除的思路有两种:一种是使用append(tmp[:id],tmp[id+1:],注意不能直接拿地址来运算(有问题,有时间再研究) ,二种是新建一个变量,直接覆盖原有的指针地址
	var tmp ALLSTUTYPE
	tmp2 := *a

	for k, v := range tmp2 {
		if v.ID != id { //一次只能删除一个
			tmp = append(tmp, tmp2[k])
		}
	}

	*a = tmp //注意这里一定是 *a,否则不会改变原值

	//fmt.Printf("TYPE:%T\t%T\t%T\n", *a, a, &a) //minebase.ALLSTUTYPE        *minebase.ALLSTUTYPE    **minebase.ALLSTUTYPE
	//针对指针类型操作: *a取地址,&a对应 ** a

	//方式1:
	//tmp3 := *a
	//tmp4 := tmp3[id+1:]
	//
	////tmp5 := append(tmp3[:id], Student{ID: 10, AGE: 16, NAME: "123"}) //apend 用法,第二个参数只能是 Student类型,如果
	////tmp5 := append(tmp3[:id], tmp4) //报错: Cannot use 'tmp4' (type ALLSTUTYPE) as the type Student
	//
	//tmp6 := append(tmp3[:id], tmp4...)
	//
	//fmt.Println("TMP5=>", tmp6)
	return err
}

func (a *ALLSTUTYPE) STUModify(id uint, stu Student) (err error) {
	/*
		1、判断要入参id(学号)是否存在,并且stu.ID是否有人在用
	*/
	exists, exists2 := false, false
	tmp := *a
	for _, v := range tmp {
		if v.ID == id {
			exists = true //判断要修改的学号是存在的
		}
		if v.ID == stu.ID && v.ID != id {
			exists2 = true //新的学号是否存在
		}
	}

	if !exists || exists2 { //如果要修改的学生学号不存在  或者 修改后的学号已被占用,则返回错误
		return errors.New("stu num is not exists;or dest num is in use !")
	}

	if stu.SEX != "男" && stu.SEX != "女" {
		log.Println("性别错误")
		return errors.New("性别错误")
	}

	if stu.AGE <= 0 || stu.AGE >= 120 {
		log.Println("年龄范围0-120")
		return errors.New("年龄范围0-120")
	}

	if len(stu.NAME) <= 0 || len(stu.NAME) >= 12 {
		log.Println("姓名最长不超过12个字符")
		return errors.New("姓名最长不超过12个字符")
	}

	//fmt.Println("==>", id, name, age, sex, ":||", *a)

	tmp[id] = stu
	*a = tmp

	return err
}

posted @ 2022-04-22 17:50  MT_IT  阅读(258)  评论(0编辑  收藏  举报