Go——网络编程

时间:2024-04-14 16:18:47

一. 互联网协议介绍

网络基础——网络传输基本流程_网络传输过程-****博客

应用层HTTP协议-****博客

传输层UDP/TCP协议_udp报文提供的确认号用于接收方跟发送方确认-****博客

网络层IP协议-****博客

链路层以太网详解_以太网数据链路层-****博客

二. Socket编程

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

        2.1 Socket图解

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

  • Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
  • 常用的Socket类型有两种:流式Socket和数据报式Socket,流式是一种面向连接的Socket,针对于面向连接的TCP服务应用,数据报式Socket是一种无连接的Socket,针对于无连接的UDP服务应用。
  • TCP:比较靠谱,有连接,慢
  • UDP:不靠谱,比较快

        2.2 Go语言net包Socket接口

        Go语言提供了对Socket编程的良好支持,开发人员可以使用内置的net包进行Socket编程。net包中包含了许多类型和函数,可用于创建和管理Socket连接,网络数据传输,处理信息等操作。

  • 创建Socket
//创建TCP socket
conn, err := net.Dial("tcp", "127.0.0.1:8080")

//创建UDP Socket
conn, err := net.Dial("udp", "127.0.0.1:8080")
  • 监听Socket

        为了处理来自其它计算机的连接请求,服务器必须在Socket上监听,以下是在Go语言中监听TCP和UDP Socket的代码示例:

//监听TCP socket
listener, err := net.Listen("tcp", "127.0.0.1:8080")
// TCP 建立连接
conn, err := listener.Accpet()

//监听UDP Socket
listener, err := net.ListenPacket("udp", "127.0.0.1:8080")
  • 数据传输

        一旦创建Socket连接,可以使用传输数据。Go语言中提供常见的TCP和UDP Socket数据传输函数。

//TCP Socket发送和接收数据
conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn.Write([]byte("GET /HTTP/1.0

"))
buffer := make([]byte, 1024)
conn.Read(buffer)


//UDP Socket发送和接收数据
conn, err := net.Dial("udp", "127.0.0.1:8080")
conn.Write([]byte("hello world"))
buffer := make([]byte, 1024)
conn.Read(buffer)
  • 关闭Socket 
//关闭tcp Socket
conn, err := net.Dial("tcp", "127.0.0.1:8080")
defer conn.Close()

//关闭udp Socket
conn, err := net.Dial("udp", "127.0.0.1:8080")
defer conn.Close()

        其它接口可以查看:Go语言标准库文档中文版 

        2.3 Go语言实现TCP通信

  • TCP协议

        TCP/IP协议即传输控制协议/网间协议,是一种面向连接(连接导向)的,可靠的,基于字节流的传输层通信协议。因为是面向连接和字节流的协议,数据会存在粘包问题。

  • TCP服务器端

        一个TCP服务器端可以同时连接多个客户端。而Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每创建一个连接就创建一个goroutine去处理。

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

  • 监听端口
  • 接收客户端请求建立连接
  • 创建goroutine处理连接

        使用Go语言的net包实现的TCP服务器代码如下:

        创建套接字,绑定套接字,监听套接字,监听到连接后,获取到监听套接字,然后往监听套接字中收发数据。

package main

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

func process(conn net.Conn) {
	defer conn.Close()
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		//读取数据
		n, err := reader.Read(buf[:]) //参数是切片 n返回的是长度
		if err != nil {
			fmt.Println("read from client failed err: ", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到客户端数据:", recvStr)
		//返回响应
		conn.Write([]byte(recvStr)) //写数据到socket
	}
}

func main() {
	//获得监听套接字,直接完成创建套接字,绑定和设置监听
	listen, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("listen fail")
		return
	}
    defer listen.Close()
	for {
		//监听连接
		conn, err := listen.Accept() //建立连接
		if err != nil {
			fmt.Println("Accept fail")
			continue
		}
		//指向请求
		go process(conn)
	}
}
  • TCP客户端

        一个TCP客户端进行TCP通信的流程如下:

  • 建立于服务器的连接
  • 进行数据收发
  • 关闭连接

        使用Go语言的net包实现的TCP客户端代码如下:

        创建好套接字后,连接套接字,然后发收数据。

package main

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

func main() {
	//连接服务器,获得连接
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("err: ", err)
		return
	}
	//关闭连接
	defer conn.Close()

	inputReader := bufio.NewReader(os.Stdin)
	for {
		//读取用户输入,碰到回车读取,包括回车也会读取上来
		input, _ := inputReader.ReadString('\n')
		//去除回车
		inputInfo := strings.Trim(input, "\r\n")
		//只输入回车
		if len(inputInfo) == 0 {
			continue
		}
		//退出条件
		if strings.ToUpper(inputInfo) == "Q" {
			return
		}
		//发送数据
		_, err := conn.Write([]byte(inputInfo))
		if err != nil {
			return
		}

		//读取服务器返回
		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv err: ", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

        演示:

        2.4 Go语言实现UDP通信

  • UDP协议

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

  • UDP服务端

        使用Go语言net包实现UDP服务器代码:

        创建和绑定好套接字后,直接收发数据,因为UDP无连接。

package main

import (
	"fmt"
	"net"
)

func main() {
	//得到套接字 直接完成了套接字生成和绑定套接字
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 8080,
	})
	if err != nil {
		fmt.Println("listen fail err: ", err)
		return
	}
	//关闭监听套接字
	defer listen.Close()

	for {
		var data [1024]byte
		//addr为客户端信息 ip+port等
		//读取套接字中的数据
		n, addr, err := listen.ReadFromUDP(data[:])
		if err != nil {
			fmt.Println("read udp fail err: ", err)
			return
		}

		fmt.Printf("收到客户端数据:data:%v, addr:%v, count:%v\n", string(data[:n]), addr, n)
		//向套接字中写数据
		_, err = listen.WriteToUDP(data[:n], addr) //发送数据
		if err != nil {
			fmt.Println("write udp fail err: ", err)
			return
		}
	}
}
  •  UDP客户端

        使用Go语言的net包来实现UDP客户端。

        创建好套接字后,直接从套接字中发收数据。

package main

import (
	"fmt"
	"net"
)

func main() {
	//获得udp服务器套接字
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 8080,
	})
	if err != nil {
		fmt.Println("udp link fail, err: ", err)
		return
	}
	//关闭套接字
	defer socket.Close()
	//fmt.Println(string(net.IPv4(0, 0, 0, 0)))
	//发送数据,短连接
	var data = []byte("hello world")
	//向套接字发送数据
	_, err = socket.Write(data)
	if err != nil {
		fmt.Println("udp write fail, err: ", err)
		return
	}

	buf := make([]byte, 1024)
	//从套接字中读数据
	n, addr, err := socket.ReadFromUDP(buf)
	if err != nil {
		fmt.Println("udp Read fail, err: ", err)
		return
	}
	fmt.Printf("服务端响应:data:%v, addr:%v, count:%v", string(buf[:n]), addr, n)
}

        演示:

        2.5 TCP粘包

  • 发现问题

        输出:

        我们发现客户端分50次向服务器发送数据,在服务器并没有成功的输出50次,而是多条数据粘到一块了。

        对比UDP问题就很明显了:

         输出:

  • 为什么会出现粘包

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

        粘包问题可发生在发送端,也可发生在接收端。

        由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。

         接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,会照成TCP缓冲区中存放了几段数据。

  • 解决办法

        出现粘包的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

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

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

        加报头和解析报头代码:

//文件目录src/proto
package proto

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

func EnCode(message string) ([]byte, error) {
	//读取消息长度
	var length = int32(len(message))
	//创建包
	pkg := new(bytes.Buffer)

	//写入包头,将消息长度写入
	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
}

func Decode(reader *bufio.Reader) (string, error) {
	//读取消息长度
	lengthByte, _ := reader.Peek(4) //读取前4字节数据
	lengthBuff := bytes.NewBuffer(lengthByte)

	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
	if err != nil {
		fmt.Println("binary.Read")
		return "", err
	}
	//buffered返回缓冲中现有的可读取的字节数,包括报头
	if int32(reader.Buffered()) < length+4 {
		return "", errors.New("没有有效数据")
	}

	//读取全部数据
	pack := make([]byte, int(length+4))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}

	//去掉报头
	return string(pack[4:]), nil
}

        服务端代码:

package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
	"proto"
)

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		//读取数据,解码
		data, err := proto.Decode(reader)
		//最后解码没有数据了 报EOF错
		if err == io.EOF {
			break
		}

		if err != nil {
			fmt.Println("decode err: ", err)
			return
		}
		fmt.Println("收到客户端的数据:", data)
	}
}

func main() {
	//获得监听套接字,直接完成创建套接字,绑定和设置监听
	listen, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("listen fail")
		return
	}
	defer listen.Close()
	for {
		//监听连接
		conn, err := listen.Accept() //建立连接
		if err != nil {
			fmt.Println("Accept fail")
			continue
		}
		//指向请求
		go process(conn)
	}
}

        客户端代码:

package main

import (
	"fmt"
	"net"
	"proto"
)

func main() {
	//连接服务器,获得连接
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("err: ", err)
		return
	}
	//关闭连接
	defer conn.Close()

	for i := 0; i < 20; i++ {
		msg := "hello, hello. how are you"
        //编码
		msgbyte, err := proto.EnCode(msg)
		if err != nil {
			return
		}
		conn.Write(msgbyte)
	}
}

 三.http编程

        应用层HTTP协议-****博客

        3.1 http协议

  • 超文本传输协议是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
  • HTTP协议通常承载于TCP协议之上。

        3.2 net/http包接口

        http包提供了HTTP客户端和服务端的实现:

  • Get,Head,Post和PostFrom函数发出HTTP/HTTPS请求
resp, err := http.Get("http://example.com")
...
resp, err := http.Post("http://example.com", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com", url.Values{"Key": {"Value"}, "id": {"123"}})
  • 程序在使用完回复必须关闭回复主体
resp, err := http.Get("http://example.com")
if err != nil{
    //handle error
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
//...
  • 要管理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)
//...
  •  要管理代理,TLS配置,keep-live,压缩和其它设置,创建一个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类型都可以安全的被多个go协程同时使用,处于效率考虑,应该一次建立,尽量重用。

  • ListenAndServer使用指定的监听地址和处理启动一个HTTP服务器端。处理器参数通常是nil,这表示采用包变量DefaultServerMux作为处理器。Handle和HandleFunc函数可以向DefaultServerMux添加处理器。
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
  • 要管理服务器行为,可以创建一个自定义server:
s := &http.Server{
	Addr:           ":8080",
	Handler:        myHandler,
	ReadTimeout:    10 * time.Second,
	WriteTimeout:   10 * time.Second,
	MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

         其它接口可以通过查看Go语言标准库文档中文版 

        3.3 HTTP服务端

package main

import (
	"fmt"
	"net/http"
)

func myHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println(r.RemoteAddr, "连接成功")
	//打印状态行和报头
	fmt.Println("method: ", r.Method)
	fmt.Println("url: ", r.URL)
	fmt.Println("header: ", r.Header)
	fmt.Println("body: ", r.Body)
	//发送响应
	w.Write([]byte("www.baidu.com"))
}

func main() {
	http.HandleFunc("/go", myHandler)

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

        3.4 HTTP客户端

package main

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

func main() {
	//发送Get请求
	resp, err := http.Get("http://127.0.0.1:8080/go")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	//打印状态和报头
	fmt.Println(resp.Status)
	fmt.Println(resp.Header)
	//收响应
	buf := make([]byte, 1034)
	for {
		n, err := resp.Body.Read(buf)
		if err != nil && err != io.EOF {
			fmt.Println(err)
			return
		} else {
			fmt.Println("读取完毕")
			res := string(buf[:n])
			fmt.Println(res)
			break
		}
	}
}

        演示: