go语言网络编程——TCP编程

一. TCP socket编程

go的TCP服务端流程分为三步:

(1)监听端口

(2)接收客户端请求连接,返回conn

(3)创建goroutine处理请求

一个实例如下:

TCP服务端

package main

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

func process(conn net.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])
		fmt.Println("收到client端发来的数据:", recvStr)
		conn.Write([]byte(recvStr)) //发送数据
	}
}

func main() {
	fmt.Println("Listening…………")
	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)  //启动goroutine处理连接
	}
}

TCP客户端

package main

import (
	"net"
	"fmt"
	"bufio"
	"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()
	inputReader := bufio.NewReader(os.Stdin)
	for {
		input, _ := inputReader.ReadString('\n')
		inputInfo := strings.Trim(input, "\r\n")
		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 failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

以上代码可实现一个服务端与多个客户端的通信

 

二、 TCP粘包

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

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

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

解决办法:

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

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

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

 

自定义编解码proto.go

package proto

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

func Encode(message string) ([]byte, error) {
	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
}

func Decode(reader *bufio.Reader) (string, error) {
	header, _ := reader.Peek(4)
	headerBuff := bytes.NewReader(header)
	var length int32
	//注意Read的第三个参数要用引用类型,但目前不太清楚为什么这么用(疑问)
	err := binary.Read(headerBuff, binary.LittleEndian, &length)
	if err != nil {
		return "", err
	}
	if int32(reader.Buffered()) < length + 4 { //判断已缓冲的字节数是否满足条件
		return "", err
	}
	//读取真正的数据
	pack := make([]byte, int(length+4))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil
}


TCP服务端代码Server\main.go

package main

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

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		msg, err := proto.Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("Decode msg failed, err:", err)
			return
		}
		fmt.Println("The message receive from client is: ", msg)
	}
}
func main() {
	fmt.Println("Listening……")
	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)
	}
}

TCP客户端代码Client\main.go

package main

import (
	"fmt"
	"net"
	"awesomeProject/proto"
	"strconv"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("dial failed, err: ", err)
		return
	}
	fmt.Println("已与服务器建立连接……")
	defer conn.Close()
	for i := 0; i < 20; i++{
		msg := "Hello, I am client.How are you? message id: " + strconv.Itoa(i+1)
		encode_msg, err := proto.Encode(msg)
		if err != nil {
			fmt.Println("encode failed, err: ", err)
			return
		}
		conn.Write(encode_msg)
	}
}

服务器端收到的数据如下,可以看到,使用封包加解包策略后,不再发生粘包现象

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页