一文搞懂Go语言网络编程【tcp、udp】


前言

本文介绍的不是http.net包,仅仅介绍传统的网络通信(后期会单独进行http.net包的更新)


一、互联网的层次结构

大致分为四层,细分的话可以分为7层

1.应用层

应用层、表示层、会话层

2.传输层

传输层

3.网络层

网络层

4.网络接口层

数据链路层、 物理层

5.图解

在这里插入图片描述

	其中socket层是为我们准备的,我们可以在这一层进行一些功能的实现达到用户的需求

二、tcp协议概述

1.三次握手

第一次握手

客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,
以及初始序号X,保存在包头的序列号(Sequence Number)字段里。

第二次握手

服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同
时,将确认序号(Acknowledgement Number)设置为客户的I S N加1以.即X+1。

第三次握手

客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服
务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1

2.tcp通常用来做什么?

  • 由于tcp通信数据包传输精确可以用来传输文件图片,等重要的数据
  • tcp传输准确率高但效率低,不易传数据量大重要程度不高的数据

3.代码实现tcp通信

客户端代码如下:

package main

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

// tcp/client/main.go

// 接收数据
func getMsg(conn net.Conn) {
	for {
		buf := [512]byte{}
		// n为获取到的数据长度
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}

}

// 发送数据
func sendMsg(conn net.Conn) {
	for {
		// 创建一个缓冲区阅读器
		inputReader := bufio.NewReader(os.Stdin)
		// 接收用户输入,以\n为终结符
		input, _ := inputReader.ReadString('\n') // 读取用户输入
		// 去掉获取到的\n
		inputInfo := strings.Trim(input, "\r\n")

		// 判断是否满足退出条件
		if strings.ToUpper(inputInfo) == "Q" {
			// 对服务器发送退出请求
			_, _ = conn.Write([]byte("q"))
			return
		}
		// 给服务端写入数据(数据在两端之间以字节的形式传输)
		_, err := conn.Write([]byte(inputInfo))
		if err != nil {
			return
		}
	}
}

// 客户端
func main() {
	// 使用tcp的方式去连接127.0.0.1:20000
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("err :", err)
		return
	}
	// 在函数执行到末端的时候进行关闭连接
	defer conn.Close()
	go getMsg(conn)
	go sendMsg(conn)
	time.Sleep(time.Hour)
}

服务端代码如下:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)
// TCP server端

func sendMsg(conn net.Conn) {
	for {
		inputer := bufio.NewReader(os.Stdin)
		input, _ := inputer.ReadString('\n')
		input = strings.Trim(input, "\r\n")
		conn.Write([]byte(input))
	}

}

// 接收处理函数
func process(conn net.Conn) {
	fmt.Println("线路:", conn, "连接成功!")
	defer conn.Close() // 关闭连接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 读取数据
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		if recvStr == "q" {
			fmt.Println(conn, "断开了连接!")
			return
		}
		fmt.Println(conn, ":", recvStr)
		fmt.Printf("请输入:")
	}
}

func main() {
	// 绑定网关的127.0.0.1:20000,以tcp的形式进行数据传输
	listen, err := net.Listen("tcp", "127.0.0.1:20000")

	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		// 等待客户端进行连接
		conn, err := listen.Accept() // 建立连接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		// 将接受到的连接添加到后台进程
		go process(conn)
		go sendMsg(conn)
	}
}

三、udp协议概述

1.udp通常用来做什么?

  • udp传输效率高,但容易丢失数据
  • 可用来进行网络直播,视频通话
  • 保留重要的信息,对可有可无的信息尽力保存

2.代码实现

客户端代码如下:

package main

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

// UDP client

func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 40000,
	})
	if err != nil {
		fmt.Println("连接服务端失败,err:", err)
		return
	}
	defer socket.Close()
	var reply [1024]byte
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("请输入内容:")
		msg, _ := reader.ReadString('\n')
		socket.Write([]byte(msg))
		// 收回复的数据
		n, _, err := socket.ReadFromUDP(reply[:])
		if err != nil {
			fmt.Println("redv reply msg failed,err:", err)
			return
		}
		fmt.Println("收到回复信息:", string(reply[:n]))
	}
}

服务端代码如下:

package main

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

// UDP server

func main() {
	// 建立UDP服务端,不需要使用accept方法
	conn, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 40000,
	})
	if err != nil {
		fmt.Println("listen UDP failed,err:", err)
		return
	}
	defer conn.Close()

	// 不需要建立连接,直接收发数据
	var data [1024]byte
	for {
		n, addr, err := conn.ReadFromUDP(data[:])
		if err != nil {
			fmt.Println("read from UDP failed,err:", err)
			return
		}
		fmt.Println(data[:n])
		reply := strings.ToUpper(string(data[:n]))
		// 发送数据
		conn.WriteToUDP([]byte(reply), addr)
	}
}

四、粘包问题【及解决方法】

1.为什么会粘包?

  • 由于频繁的进行数据的收发,底层为了提高收发效率,在发送消息前会检查一下是否还有需要发送的其他数据
  • 如果发送过于频繁,上一次数据包将与夏下一次的数据报粘在一起,导致数据非常乱
  • 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去
/*
	在服务端接收客户端发送来的20次Tomhello并打印

	在粘包下的打印效果(各个信息有可能会直接相连)
	hello Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello
	 Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello Tomhello Tom
	--------------
	hello Tomhello Tomhello Tom
	--------------

	正常的打印结果为:
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	hello Tomhello 
	--------------
	......
*/

2.解决方案

  • 将每一个数据包大小进行保存,数据包写一表头用于存储数据包的大小,每次进行数据的读取时先
  • 进行数据包大小的读取,在进行数据信息实体的读取
  • 降低发送数据的频率

3.编码解码函数

编码解码函数代码如下:

package edcode

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

/*
	小端低低:低地址存低位
	大端高低:高地址存低位
	对需要发送的信息进行编码解码(打包处理)
	其中包内的前四个字节用于存储包的大小,后面用于存储信息的主要内容
*/
// 解码
// 取出bytes前四个字节,转化为int32类型,然后再将剩余的消息体取出
func Mydecode(reader *bufio.Reader) (string, error) {
	Size, _ := reader.Peek(4)
	lenthbuff := bytes.NewBuffer(Size)
	var lenth int32
	// 读取lenthbuff内的内容,读取完将数据放在lenth内,读取的方式是小端方式
	err := binary.Read(lenthbuff, binary.LittleEndian, &lenth)
	// 判断一下是否出错,长度不够包头指定的长度就抛异常
	if err != nil || int32(reader.Buffered()) < lenth+4 {
		return "", err
	}
	//
	pack := make([]byte, int(lenth+4))
	// read读取之后缓冲区就少一部分数据
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil

}

// 编码
// 先计算出字符串的长度,再进行打包
func MyEncode(message string) ([]byte, error) {
	// 读取消息的长度,转换成int32类型(占4个字节)
	var length = int32(len(message))
	var 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
}


客户端代码如下:
package main

import (
	"fmt"
	"net"

	ed "aCorePackage/edcode"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println(err)
	}
	// 快速发送信息还是会黏在一起,只不过通过一定的编码解码方式可以使信息很好的分离
	for i := 0; i < 20; i++ {
		str := "hello Tom"
		mybtys, err := ed.MyEncode(str)
		if err != nil {
			fmt.Println(err)
		}
		conn.Write(mybtys)
		fmt.Println("-------------------")
	}

}

服务端代码如下:

package main

import (
	ed "aCorePackage/edcode"
	"bufio"
	"fmt"
	"io"
	"net"
)

func readMsd(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		str, err := ed.Mydecode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println(str)
		fmt.Println("--------------")
	}

}

func main() {
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println(err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println(err)
			return
		}
		go readMsd(conn)

	}
}

总结

TCP 需要连接,UDP 是无连接的,发送数据之前不需要建立连接, TCP 提供可靠的服务,通过 TCP 连接传送的数据,无差错,不丢失 TCP 逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道,两者各有所长,对他们进行各取所需。

在这里插入图片描述


GO GO GO !

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酷尔。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值