Golang TCP/IP服务器/客户端应用程序,设计一个简单可靠帧传送通信协议。(并且正确处理基于流式控制协议,带来的应用层沾帧[沾包]问题)

这篇具有很好参考价值的文章主要介绍了Golang TCP/IP服务器/客户端应用程序,设计一个简单可靠帧传送通信协议。(并且正确处理基于流式控制协议,带来的应用层沾帧[沾包]问题)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在 Golang 语言标准库之中提供了,对于TCP/IP链接、侦听器的高级封装支持,这易于上层开发人员轻松基于这些BCL(基础类库)实现期望的功能。

TCP/IP链接(客户端)

net.Conn 接口

TCP/IP侦听器(服务器)

net.Listener

Golang 提供了易用的写入数据到远程(对端)实现,而不比像 C/C++ 这类传统的编程语言,人们需要自行处理发送的字节数。

例如:

原生:send、WSASend、WSPSend 等函数

ASIO:stream::async_write_some    等函数

它与 Microsoft .NET 提供的 System.Net.Socket 类的发送函数功能是类似的,调用该函数发送数据,它会确保数据全部发送到对端(远端),否则视为失败。

在实际生产环境之中,绝大多数的场景上面,人们的确不需要调用一次发送函数,但不保证本次期望传送数据全部发送成功,而是潜在的可能只发送一部分,还需要开发人员自行处理,这样繁琐的TCP网络程序发送实现的。

但这在一些特定场景的网络程序上面是有意义的,例如我们需要知道已用掉了多少的流量,因为这一次缓冲区发送并没有全部传送到远端,但已经传送了一部分也生产了网络带宽资源的浪费,所以,像这种问题,Golang 不提供类似接口,它这块的不自由,是会有一些问题的。

较为庆幸的是:

net.Conn 接口提供的 Read 函数并非是保证一定读入期望BUF大小的,否则这个在很多类型的网络程序上面就很坑人了。

它就相当于传统阻塞的 recv,不会出现非阻塞的EAGIN要求开发人员重试的操作的问题,所有它只有返回已收到的字节数,或发生错误。

当然人们仍需处理一个特殊的情况,recv 可能返回FIN 0字节,但并非错误,这是因为对端正确的关闭了TCP链接时产生的。

但遇到类似这类型的场景还是用 C/C++、或者CGO调用原生C API来实现把,功能上面都可以解决,只是用GO语言整会很麻烦就是了。

本文提供一个简单的网络传输协议,适用四个字节来表示长度,一个字节来表示关键帧字,不考虑对于流的效验合(checksum)的计算及验证,人们若有需求可以自行修改,在大多数的TCP应用协议服务器上面,它都可以经过少量修改集成到解决方案之中。(Go 语言之中或许该称为集成到 Package 程序包之中)

四个字节长度,可以描述到一帧最大 INT32_MAX(2147483647)字节封装传送,其实绝大多数情况传递大包是没有太大意义的,人们可以自行评估调整。

值得一提,在绝大多数的场景之中,如若产生大包,三个字节来表示长度,人们自行位运算即可,这是因为过大的帧长,可能会导致网络程序在接受这些大数据帧时,产生严重的内存恐慌问题。

个人一个好的建议是,对于追求网络吞吐性能的TCP应用协议,人们在适用 Golang 应该直接废弃掉,没有任何意义的各种接口及封装实现,如返回  io.Reader,并且应当适用固定缓冲区的最大帧对齐,如:4K,即用户不要发送超过最大对齐(4K)的单帧报文。

随机内存分配会导致碎片化的产生,影响网络程序的吞吐能力,同时频繁的内存复制也会导致内存、及CPU计算资源负载升高。

但在大多数场景的网络程序来说,并不需要在意这块的优化,因为没有太大意义,但对于纯网络IO密集型应用来说,这是有很大必要的。

本文提供的实现不适用上述场景,但可以适用于略微带一些大包处理(即用户不愿意在业务层分片、组片的场景),但本人更希望大家趋近于共同学习目的。

运行测试:

go run -race test.go

Golang TCP/IP服务器/客户端应用程序,设计一个简单可靠帧传送通信协议。(并且正确处理基于流式控制协议,带来的应用层沾帧[沾包]问题),Extension,golang,tcp/ip,服务器

服务器及客户端实现及封装:(含测试用例)

main.go文章来源地址https://www.toymoban.com/news/detail-820029.html

package main

import (
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math"
	"math/rand"
	"net"
	"strconv"
	"sync"
	"time"
)

type _ConnectionReader struct {
	owner       *Connection
	length      int
	offset      int
	checksum    uint32
	header_recv []byte
	lock_recv   sync.Mutex
}

type Connection struct {
	disposed    bool
	connection  net.Conn
	header_send []byte
	lock_sent   sync.Mutex
	reader      *_ConnectionReader
	listener    *Listener
}

type Listener struct {
	sync.Mutex
	disposed    bool
	listener    net.Listener
	connections map[*Connection]bool
}

/*
#pragma pack(push, 1)
typedef struct {
	BYTE  bKf;       // 关键帧字
	DWORD dwLength;  // 载荷长度
} PACKET_HEADER;
#pragma pack(pop)

static constexpr int PACKET_HEADER_SIZE = sizeof(PACKET_HEADER); // 4 + 1 = 5 BYTE
*/

const (
	_CONNECTION_PACKET_HEADER_KF   = 0x2A // 置关键帧字
	_CONNECTION_PACKET_HEADER_SIZE = 5
	CONNECTION_MIN_PORT            = 0
	CONNECTION_MAX_PORT            = math.MaxUint16
)

var ErrConnectionClosed = errors.New("connection has been closed")
var ErrConnectionArgP = errors.New("the parameter p cannot be incorrectly null or array length 0")
var ErrConnectionProtocolKf = errors.New("network protocol error, kf check error")
var ErrConnectionProtocolLength = errors.New("network protocol error, length check error")
var ErrConnectionArgAcceptor = errors.New("the acceptor parameter cannot be null")
var ErrConnectionDisconnect = errors.New("connection has been disconnect")

// 功能名:发送数据
// 返回值:
// <  0 发送错误(ERR)
// == 0 链接断开(FIN)
// >  0 已发送字节数
func (my *Connection) Send(buffer []byte, offset int, length int) int {
	// 对于欲发送数据的参数检查
	if buffer == nil || offset < 0 || length < 1 {
		return -1
	}

	// 检查是否溢出BUFF缓存大小
	len := len(buffer)
	if offset+length > len {
		return -1
	}

	// 检查链接是否存在
	connection := my.connection
	if connection == nil {
		return -1
	}

	// 预备环境及变量
	bytes_transferred := 0
	sync := &my.lock_sent
	header := my.header_send
	payload := buffer[offset : offset+length]

	// 如果可以直接获取到信号,否则其它协同程序就等待发送结束,不要用管道这些莫名其妙的东西。
	sync.Lock()
	defer sync.Unlock()

	// 检查当前链接是否已经释放
	if my.disposed {
		return -1
	}

	// 先发送协议帧头
	header[0] = _CONNECTION_PACKET_HEADER_KF
	binary.BigEndian.PutUint32(header[1:], uint32(length))

	written_size, err := connection.Write(header)
	if err != nil {
		return -1
	} else {
		bytes_transferred += written_size
	}

	// 在发送协议载荷
	written_size, err = connection.Write(payload)
	if err != nil {
		return -1
	}

	// 加上已传送的字节数
	bytes_transferred += written_size
	return bytes_transferred
}

// 功能名:收取数据
// 上个 Reader 未完成之前一直阻塞当前协程直到对方结束后返回
func (my *Connection) Receive() io.Reader {
	// 检查当前链接是否已经释放
	if my.disposed {
		return nil
	}

	// 检查链接是否存在
	connection := my.connection
	if connection == nil {
		return nil
	}

	// 返回帧读入器
	reader := my.reader
	reader.lock_recv.Lock()
	return reader
}

// 功能名:实例化一个链接对象
func NewConnection(conn net.Conn, listener *Listener) *Connection {
	var connection *Connection

	if conn != nil {
		connection = &Connection{
			disposed:    false,
			connection:  conn,
			listener:    listener,
			header_send: make([]byte, _CONNECTION_PACKET_HEADER_SIZE),
		}
		connection.reader = &_ConnectionReader{
			owner:       connection,
			length:      0,
			offset:      0,
			checksum:    0,
			header_recv: make([]byte, _CONNECTION_PACKET_HEADER_SIZE),
		}
	}
	return connection
}

// 功能名:链接主机
func Connect(host string, port int) *Connection {
	// 检查端口参数的有效性
	if port <= CONNECTION_MIN_PORT || port > CONNECTION_MAX_PORT {
		return nil
	}

	// 服务器主机地址不可为空
	if len(host) < 1 {
		return nil
	}

	// 服务器地址并且尝试链接
	address := host + ":" + strconv.Itoa(port)
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return nil
	}

	// 返回TCP链接的封装对象
	return NewConnection(conn, nil)
}

// 功能名:关闭链接(网络)
func (my *Connection) close(connection net.Conn) error {
	// 强制关闭链接,但可能会失败
	if my.disposed {
		return nil
	}

	my.disposed = true
	return connection.Close()
}

// 功能名:关闭链接
func (my *Connection) Close(await bool) (err error) {
	// 检查链接是否存在
	connection := my.connection
	if connection == nil {
		return
	}

	// 如果可以直接获取到信号,否则其它协同程序就等待发送结束,不要用管道这些莫名其妙的东西。
	sync := &my.lock_sent
	if await {
		sync.Lock()
		sync.Unlock()

		// 检查当前链接是否已经释放
		err = my.close(connection)
	} else {
		err = my.close(connection)
	}

	// 如果是服务器接受的链接对象,就从服务器列表之中删除这个链接实例。
	listener := my.listener
	if listener != nil {
		listener.Lock()
		delete(listener.connections, my)
		listener.Unlock()
	}

	return
}

func (my *Connection) connection_get_ip_end_point(remote bool) string {
	connection := my.connection
	if connection == nil {
		return ""
	}

	var address net.Addr
	if remote {
		address = connection.RemoteAddr()
	} else {
		address = connection.LocalAddr()
	}

	if address == nil {
		return ""
	}

	return address.String()
}

// 功能名:获取远程地址
func (my *Connection) GetRemoteEndPoint() string {
	return my.connection_get_ip_end_point(true)
}

// 功能名:获取本地地址
func (my *Connection) GetLocalEndPoint() string {
	return my.connection_get_ip_end_point(false)
}

// 功能名:读入帧数据
func (my *_ConnectionReader) Read(p []byte) (n int, err error) {
	// 检查当前链接是否已经释放
	owner := my.owner
	if owner.disposed {
		return 0, ErrConnectionClosed
	}

	// 检查参数P不可以为NUL或数组长度为0
	length := len(p)
	if length < 1 {
		return 0, ErrConnectionArgP
	}

	// 帧已经被全部收取完成
	if my.length < 0 {
		my.length = 0
		my.lock_recv.Unlock()
		return 0, io.EOF
	}

	// 收取协议报文的头部
	if my.length == 0 {
		header := my.header_recv
		n, err := io.ReadFull(owner.connection, header)
		if err != nil {
			return n, err
		}

		// 判断协议关键帧字
		kf := header[0]
		if kf != _CONNECTION_PACKET_HEADER_KF {
			return 0, ErrConnectionProtocolKf
		}

		// 检查载荷的总长度
		my.length = int(binary.BigEndian.Uint32(header[1:]))
		my.offset = 0
		my.checksum = 0
		if my.length < 1 {
			return 0, ErrConnectionProtocolLength
		}
	}

	// 循环收取数据到缓存区P之中
	remain := my.length - my.offset
	if length <= remain {
		n, err = owner.connection.Read(p)
	} else {
		n, err = owner.connection.Read(p[:remain])
	}

	// 从链接之中读入数据出现错误
	if err != nil {
		return n, err
	}

	// 是否收取到FIN字节(0)
	if n < 1 {
		return n, ErrConnectionDisconnect
	}

	// 计算当前帧是否已经收取完毕
	my.offset += n
	if my.offset < my.length {
		return n, nil
	} else {
		my.offset = 0
		my.length = -1
		my.checksum = 0
		return n, nil
	}
}

// 功能名:实例化一个侦听器
func NewListener(host string, port int) *Listener {
	// 检查端口参数的有效性
	if port <= CONNECTION_MIN_PORT || port > CONNECTION_MAX_PORT {
		return nil
	}

	// 服务器主机地址不可为空
	if len(host) < 1 {
		return nil
	}

	// 服务器地址并且尝试绑定
	address := host + ":" + strconv.Itoa(port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return nil
	}

	return &Listener{
		disposed:    false,
		listener:    listener,
		connections: make(map[*Connection]bool),
	}
}

// 功能名:侦听服务器
func (my *Listener) ListenAndServe(acceptor func(*Connection)) error {
	// 接收器参数不可以为空
	if acceptor == nil {
		return ErrConnectionArgAcceptor
	}

	// 网络侦听器已经关闭
	if my.disposed {
		return ErrConnectionClosed
	}

	any := false
	listener := my.listener
	for {
		// 网络如果已经被关闭了
		if my.disposed {
			return nil
		}

		// 尝试接收一个网络链接
		conn, err := listener.Accept()
		if err != nil {
			if any {
				return nil
			} else {
				return err
			}
		}

		// 如果没有获取到链接的引用则迭代到下个链接接受
		if conn == nil {
			continue
		}

		// 构建一个封装的网络链接对象
		connection := NewConnection(conn, my)
		my.Lock()
		my.connections[connection] = true
		my.Unlock()

		// 启动对于链接处理的协同程序
		go acceptor(connection)
	}
}

// 功能名:关闭全部链接
func (my *Listener) Close() {
	// 强制关闭服务器的侦听器
	listener := my.listener
	if listener != nil {
		listener.Close()
	}

	// 释放全部持有的托管资源
	my.Lock()
	my.disposed = true
	connections := my.connections
	my.connections = make(map[*Connection]bool)
	my.Unlock()

	// 强制关闭全部的网络链接
	for connection := range connections {
		connection.Close(false)
	}
}

func test() {
	rand.Seed(time.Now().UnixNano())

	// 链接服务器
	packet := 0
	connection := Connect("127.0.0.1", 11111)
	for i, c := 0, rand.Intn(100)+1; i < c; i++ {
		length := rand.Intn(128) + 1
		buffer := make([]byte, length)
		for j := 0; j < length; j++ {
			buffer[j] = byte(rand.Intn(26)) + 97
		}

		// 发送数据
		transferred := connection.Send(buffer, 0, length)
		if transferred < 1 {
			break
		} else {
			// 接受数据
			r := connection.Receive()
			if r == nil {
				break
			}

			// 读取全部数据(一帧)
			buf, err := io.ReadAll(r)
			if err != nil {
				break
			} else if len(buf) < 1 {
				break
			}

			// 打印收到的帧数据
			packet++
			fmt.Printf("[%s]: client packet=%d length=%d string:%s\r\n", time.Now().Format("2006-01-02 15:04:05"), packet, len(buf), string(buf))
		}
	}

	// 关闭链接
	connection.Close(true)

	// 客户端关闭网络链接
	fmt.Printf("[%s]: %s\r\n", time.Now().Format("2006-01-02 15:04:05"), "client connection closed")
}

func main() {
	// 运行客户端测试协程
	go test()

	// 打开服务器侦听器哟
	listener := NewListener("127.0.0.1", 11111)
	listener.ListenAndServe(func(c *Connection) {
		packet := 0
		remoteEP := c.GetRemoteEndPoint()
		for {
			// 获取网络接收器
			r := c.Receive()
			if r == nil {
				break
			}

			// 读取全部数据(一帧)
			buf, err := io.ReadAll(r)
			if err != nil {
				break
			} else if len(buf) < 1 {
				break
			}

			// 打印收到的帧数据
			packet++
			fmt.Printf("[%s]: server packet=%d length=%d remote=%s string:%s\r\n", time.Now().Format("2006-01-02 15:04:05"), packet, len(buf), remoteEP, string(buf))

			// 回显客户端的数据
			transferred := c.Send(buf, 0, len(buf))
			if transferred < 1 {
				break
			}
		}

		// 关闭客户端链接
		c.Close(true)

		// 服务器关闭网络链接
		fmt.Printf("[%s]: %s\r\n", time.Now().Format("2006-01-02 15:04:05"), "server connection closed")
	})
}

到了这里,关于Golang TCP/IP服务器/客户端应用程序,设计一个简单可靠帧传送通信协议。(并且正确处理基于流式控制协议,带来的应用层沾帧[沾包]问题)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《TCP/IP网络编程》阅读笔记--基于TCP的服务器端/客户端

    目录 1--TCP/IP协议栈 2--TCP服务器端默认函数调用顺序 3--TCP客户端的默认函数调用顺序 4--Linux实现迭代回声服务器端/客户端 5--Windows实现迭代回声服务器端/客户端 6--TCP原理 7--Windows实现计算器服务器端/客户端         TCP/IP协议栈共分 4 层,可以理解为数据收发分成了 4 个层

    2024年02月10日
    浏览(58)
  • 《TCP/IP网络编程》阅读笔记--基于UDP的服务器端/客户端

    目录 1--TCP和UDP的主要区别 2--基于 UDP 的数据 I/O 函数 3--基于 UDP 的回声服务器端/客户端 4--UDP客户端Socket的地址分配 5--UDP存在数据边界 6--UDP已连接与未连接的设置 ① TCP 提供的是可靠数据传输服务,而 UDP 提供的是不可靠数据传输服务; ② UDP 在结构上比 TCP 更简洁,其不会

    2024年02月09日
    浏览(58)
  • socket的使用 | TCP/IP协议下服务器与客户端之间传送数据

    谨以此篇,记录TCP编程,方便日后查阅笔记 注意:用BufferedWriter write完后,一定要flush;否则字符不会进入流中。去看源码可知:真正将字符写入的不是write(),而是flush()。 服务器端代码: 客户端代码: 运行后结果: 服务器端: 客户端: 参考资料: https://www.bilibili.com/vid

    2024年02月09日
    浏览(58)
  • 《TCP/IP网络编程》阅读笔记--基于Windows实现Hello Word服务器端和客户端

    目录 1--Hello Word服务器端 2--客户端 3--编译运行 3-1--编译服务器端 3-2--编译客户端 3-3--运行 运行结果:

    2024年02月10日
    浏览(61)
  • 【无标题】TCP,UDP,DNS以及配置网关IP地址和在ensp中,在客户端用域名或IP地址获取服务器的文件的实验

    PDU:协议数据单元 应用层:数据报文 传输层:数据段 网络层:数据包 数据链路层:数据帧 物理层:比特流(电流)   应用层的常见协议: HTTP----基于TCP协议 占据80端口号 超文本传输协议 HTTPS----基于TCP协议 占据443端口号 安全传输协议 SSH----基于TCP协议 占据22端口号 安全外

    2024年02月04日
    浏览(49)
  • FPGA实现10G万兆网TCP/IP 协议栈,纯VHDL代码编写,提供服务器和客户端2套工程源码和技术支持

    目前网上fpga实现udp协议的源码满天飞,我这里也有不少,但用FPGA纯源码实现TCP的项目却很少,能上板调试跑通的项目更是少之又少,甚至可以说是凤毛菱角,但很不巧,本人这儿就有一个; 本设采用纯VHDL实现了10G万兆网TCP/IP协议栈,该协议栈分为TCP服务器核客户端,没有使

    2024年02月09日
    浏览(61)
  • TCP实现服务器和客户端通信

    目录 TCP介绍 代码实现 server(服务器端) 代码分析 client(客户端) 代码分析 结果展示 TCP (Transmission Control Protocol) 是一种面向连接的协议,用于在计算机网络中传输数据。TCP 可以确保数据的可靠传输,即使在网络环境不稳定的情况下也能够保证数据的完整性和顺序。以下是

    2024年02月15日
    浏览(56)
  • QT实现tcp服务器客户端

    2024年02月07日
    浏览(53)
  • Qt 服务器/客户端TCP通讯

    最近需要用到TCP/IP通讯,这边就先找个简单的例程学习一下。Qt的TCP通讯编程可以使用QtNetwork模块,QtNetwork模块提供的类能够创建基于TCP/IP的客户端与服务端应用程序,一般会使用QTcpSocket、QTcpServer类 网络通信方式主要有两种:TCP与UDP。以下拷贝网络上总结两者之间的区别:

    2023年04月26日
    浏览(67)
  • 简易TCP客户端和服务器端通信

    #includeiostream #include winsock2.h   #include ws2tcpip.h   #includestdlib.h using namespace std; #define  BUF_SIZE  1024 int main() {     cout \\\"客户端\\\" endl;     //设置Winsock版本,     WSADATA   wsaData;     if (WSAStartup(MAKEWORD(2, 2), wsaData) != 0)     {         cout \\\"error\\\" endl;         exit(1);     }     //创建通

    2024年04月29日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包