golang网络编程基础

golang网络编程

1.TCP编程

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

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

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


服务端

package main

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

func process(conn net.Conn)  {
	//延迟关闭连接
	defer conn.Close()
	for {
		//阅读conn中的内容
		//bufio.NewReader打开一个文件,并返回一个文件句柄
		reader := bufio.NewReader(conn)
		//开一个128字节大小的字符缓冲区
		var buf [128]byte
		//读取reader中的内容放到buf中,n是大小
		n, err := reader.Read(buf[:])
		if err != nil {
			fmt.Println("从客户端读取消息失败..., err", err)
			break
		} else {
			fmt.Println("收到一条数据:")
		}
		recvStr := string(buf[:n])
		fmt.Println(recvStr)
		//回复接收成功
		fmt.Println("向客户端发送确认消息!")
		echo := "echo: " + recvStr
		conn.Write([]byte(echo))
	}
}

func main()  {
	//监听
	//服务器开始等待客户端的连接,listen函数不会阻塞
	listen, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("建立连接失败, err", err)
		return
	}
	fmt.Println("准备接收客户端的连接...")
	for {
		//建立连接
		//accept函数从已经建立连接的队列中取出一个连接给客户端程序,进行点对点的连接
		//accept函数会返回一个新的可用的socket
		//accept函数会阻塞
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("连接失败, err", err)
			continue
		} else {
			fmt.Println("连接成功!")
		}
		go process(conn)
	}
}

客户端

package main

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

func main()  {
	//开始拨号
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("err: ", err)
		return
	}
	defer conn.Close()
	//打开os.Stdin,并返回一个文件句柄
	inputReader := bufio.NewReader(os.Stdin)
	for  {
		//读取用户输入
		fmt.Println("请输入待发送的消息:")
		//输入换行结束输入
		input, _ := inputReader.ReadString('\n')
		//strings.Trim去掉字符串首部和尾部的匹配内容
		inputInfo := strings.Trim(input, "\r\n")
		//如果输入q就退出
		if strings.ToUpper(inputInfo) == "Q" {
			fmt.Println("停止输入,断开连接!")
			return
		}

		//发送数据
		fmt.Println("开始发送数据...")
		_, err = conn.Write([]byte(inputInfo))
		if err != nil {
			return
		} else {
			fmt.Println("发送成功!")
		}

		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("接收失败, err: ", err)
			return
		} else {
			fmt.Println("收到了服务器的回复:")
		}
		fmt.Println(string(buf[:n]))
	}
}

2.UDP编程

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

使用Go语言的net包实现的UDP服务端代码如下


服务端

package main

import (
	"fmt"
	"net"
)

func main() {
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP: net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("建立监听失败!, err:", err)
		return
	}
	defer listen.Close()
	for {
		var data [1024]byte
		//接收数据
		n, addr, err := listen.ReadFromUDP(data[:])
		if err != nil {
			fmt.Println("接收数据失败!err:", err)
			continue
		}
		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("发送数据失败!err:", err)
			continue
		}
	}
}

客户端

package main

import (
	"fmt"
	"net"
)

func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP: net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("连接服务器失败!err:", err)
		return
	}
	defer socket.Close()
	sendData := []byte("hello server")
	//发送数据
	_, err = socket.Write(sendData)
	if err != nil {
		fmt.Println("发送数据失败!err:", err)
		return
	}

	//接收数据
	data := make([] byte, 4096)
	n, remoteAddr, err := socket.ReadFromUDP(data)
	if err != nil {
		fmt.Println("接收数据失败! err:", err)
		return
	}
	fmt.Printf("recv:%v addr:%v count:%v\n\n", string(data[:n]), remoteAddr, n)
}

3. TCP黏包

黏包的场景


以下代码是用golang实现的TCP服务端和客户端,会产生黏包。


服务端:

package main

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

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)
	}
}

客户端:

package main

import (
	"fmt"
	"net"
)

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))
	}
}

输出结果:

收到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?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?

客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。

黏包的原因


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

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

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



解决办法


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

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

常用的协议是TLV编码:
在这里插入图片描述
下面我们将实现简单的LV编码,T就暂时不管。
数据包的前4个字节为包头,里面存储的是发送的数据的长度。


编码解码

package LVEcode

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

// Encode 将消息编码
func Encode(message string) ([]byte, error)  {
	//读取消息的长度,转化成int32类型(占4个字节)
	var length = int32(len(message))
	//开启一个缓冲区
	var pkg = new(bytes.Buffer)
	//写入消息头(将length写入pkg)
	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) {
	//读取消息的长度
	//读取前4个字节的数据(int32占4个字节)
	lengthByte, _ := reader.Peek(4)
	//读取lengthByte并返回句柄
	lengthBuff := bytes.NewReader(lengthByte)
	var length int32
	//将lengthBuff中的内容写入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))
	//将reader中的数据读到pack切片中
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	//返回有效数据的切片
	return string(pack[4:]), nil
}

服务端

package main

import (
	"NetWork/TCP3/LVEcode"
	"bufio"
	"fmt"
	"io"
	"net"
)

func process(conn net.Conn) {
	defer conn.Close()
	//打开conn,返回一个句柄
	reader := bufio.NewReader(conn)
	for {
		//将收到的消息进行解码
		msg, err := LVEcode.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)
	}
}

客户端

package main

import (
	"NetWork/TCP3/LVEcode"
	"fmt"
	"net"
)

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 := LVEcode.Encode(msg)
		if err != nil {
			fmt.Println("encode msg failed, err:", err)
			return
		}
		conn.Write(data)
	}
}

输出:

收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?

可以发现,现在已经不黏包了。



4. HTTP编程


web工作流程


Web服务器的工作原理可以简单地归纳为:

  • 客户机通过TCP/IP协议建立到服务器的TCP连接
  • 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
  • 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  • 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

HTTP协议

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

HTTP服务端

package main

import (
	"fmt"
	"net/http"
)

func main()  {
	//注册默认路由
	// /go 是请求路径
	//myHandler是自己写的回调函数
	http.HandleFunc("/go", myHandler)
	//addr:监听的地址
	//hander:回调函数
	http.ListenAndServe("127.0.0.1:8000", nil)
}

// hander函数
func myHandler(w http.ResponseWriter, r *http.Request)  {
	fmt.Println(r.RemoteAddr, "连接成功")
	//请求方式:GET POST DELETE PUT UPDATE
	fmt.Println("method:", r.Method)
	// /go
	fmt.Println("url:", r.URL.Path)
	fmt.Println("header:", r.Header)
	fmt.Println("body:", r.Body)
	//回复
	w.Write([]byte("www.51mh.com"))
}

HTTP客户端

package main

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

func main()  {
	resp, _ := http.Get("http://127.0.0.1:8000/go")
	defer resp.Body.Close()
	//200 ok
	fmt.Println(resp.Status)
	fmt.Println(resp.Header)

	buf := make([]byte, 1024)
	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
		}
	}
}

服务端运行结果:

127.0.0.1:8187 连接成功
method: GET
url: /go
header: map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
body: {}

客户端运行结果:

200 OK
map[Content-Length:[12] Content-Type:[text/plain; charset=utf-8] Date:[Thu, 28 Oct 2021 14:38:58 GMT]]
读取完毕
www.51mh.com
posted @ 2021-10-28 22:55  Dawnlight-_-  阅读(100)  评论(0编辑  收藏  举报