11.1 关于golang tcp网络编程的学习

B站影视 2025-01-06 14:30 3

摘要:最近写了很多的学习内容,但是感觉自己总是学了皮毛,也不知道该不该记录下来,或者说有没有必要上来献丑。哎,最后还是记录一下吧。毕竟人来世间,还是给自己留点痕迹吧,就算是再菜鸟,再无用,总是得给自己留点什么在这个世界吧。

最近写了很多的学习内容,但是感觉自己总是学了皮毛,也不知道该不该记录下来,或者说有没有必要上来献丑。哎,最后还是记录一下吧。毕竟人来世间,还是给自己留点痕迹吧,就算是再菜鸟,再无用,总是得给自己留点什么在这个世界吧。

好吧。今天就来看看golang的TCP socket网络编程吧。

和其他我所接触的编程语言差不多,对于tcp Socket编程都是对入门人员比较困难的东西,毕竟它提供的概念还是比较笼统的。比如socket,到底什么是socket?字面翻译插座?或许就是插座吧。不过,网络编程中我们会叫套接字。看这个3个字都认识,但是连在一起就不知道是啥玩意儿了。一般术语上会表达的比较专业:所谓套接字,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。其实,看了这么专业,还是不懂的。我觉得可能把它理解成插座还真比那个套接字显得更简单易懂一些。我们看生活中的插座的特点是什么?对外连接我们的设备,对内它连接的是电的来源端,其实我看来它所谓的抽象就是把网络两端的连接起来的一个插孔,大概就是这个意思吧。

在应用级别的开发中,我们常用的就两种套接字:流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM) ,这两种我理解的就是对应于我们常使用的TCP和UDP协议(大佬,我说错了请纠正哈)。TCP呢,它面向连接、可靠的数据传输服务的。UDP呢,无连接、不可靠的服务。咱们今天讲的就是TCP哈。

对于这种socket的抽象,它分为服务端的ServerSocket和客户端的Socket。它的执行过程分为三步:

监听端口 (listen)客户端请求连接确认 (accept)

有了上面的介绍,我们大概就知道了对于服务器socket来说,它需要监听某个端口,然后等待客户端的连接来。对于客户端来说,向服务器发起连接请求,收到服务器的确认连接信息后,双方就算建立好连接了。

我们来简单的使用golang实现一个tcp socket server。

func (s *TcpServer) Start { log.Logger.Info("server start. listnen address:", zap.String("address", s.address)) listner, err := net.Listen("tcp", s.address) if err != nil { log.Logger.Error("listen failed", zap.Error(err)) panic(err) } //检查encoder Decoder if s.encoder == nil || s.decoder == nil { log.Logger.Error("encoder or decoder is nil") panic("encoder or decoder is nil") } //检查socketHandle if s.socketHandle == nil { log.Logger.Error("socketHandle is nil") panic("socketHandle is nil") } defer listner.Close for { conn, err := listner.Accept if err != nil { log.Logger.Error("accept failed", zap.Error(err)) continue } client := buildClient(conn, s) s.conns.Store(client.id, client) s.socketHandle.OnConnect(client) client.StartWork }}

上面只需要关注两个函数:net.Listen("tcp", s.address)和listner.Accept。这两个就是分别表示监听端口和等待客户端连接确认。根据socket server的描述也就是对于一个服务器的socket的整个抽象的实现过程。

然后,我们可以看看客户端的。

//tcp连接服务器 conn, err := net.Dial("tcp", c.addr) if err != nil { log.Logger.Error("dial failed", zap.Error(err)) return } client := buildClient(conn, c) c.client = client c.socketHandle.OnConnect(client) client.StartWork

在golang中就是在发送客户端请求连接的。

看起来好像并不复杂,但是其实实现过程很复杂的(因为所有我们遇到的语言都帮助我们实现了tcp协议的整个传输过程,那可是厚厚的一大本书,有兴趣的可以去了解tcp协议)。

对于我们应用层开发来说,基本上这个就算是实现了1个tcp服务了(当然,其实并没有这么简单的)。

实际上,我们在应用层服务器还要解决很多问题的。对于tcp协议虽然它帮助我们解决了很多问题了,比如保证数据能够实现无差错、无重复送,并按顺序接收,实现可靠的数据服务等等(有什么拥塞控制呀,什么滑动窗口呢,什么确认重传呢。。反正看了书我也没记住,大概有点概念...)。但是,tcp并没有帮我们解决半包和粘包问题。这两个到底是什么问题呢?这两个问题产生的原因有很多,比如双方的缓冲区大小不同呀,什么不同路由器传输的数据包大小不同呀,甚至网络波动都有可能,反正就是你的电脑接收到的东西并不是你想要的那样完整的数据。这些数据包有可能超出了你的需要的数据大小,也有可能小于你需要的数据大小,反正在你的应用里要用各种方式把你需要的数据包解析出来才行(数据包之间没有边界导致的)。

一般解析分为3种方式

固定长度截取缓冲区作为1个包用特殊字符作为拆分不同的包(比如我使用","逗号作为拆分包的依据)每个包的前面加1个长度来作为包的大小(比如我以收到的头4个字节作为下面包的长度来截取缓冲区中的数据作为一个包)。

上面其实就是我想说的解码过程,当然有解码就应该有编码,编码过程当然就是需要在发送数据的时候把这个拆分加进去。

下面是我写的简单编解码器(也就是上面的第三种)

// 可变长度解码器type VariableDecode struct { FirstRecevedLen int //首部长度字节数支持1, 2, 4字节}type VariableEncode struct { FirstSendLen int //首部长度字节数支持1, 2, 4字节}func (v *VariableEncode) Encode(data byte) (byte, error) { var buf bytes.Buffer switch v.FirstSendLen { case FirstLen_1: buf.WriteByte(data[0]) case FirstLen_2: bs2 := make(byte, 2) binary.BigEndian.PutUint16(bs2, uint16(len(data))) buf.Write(bs2) case FirstLen_4: bs4 := make(byte, 4) binary.BigEndian.PutUint32(bs4, uint32(len(data))) buf.Write(bs4) } buf.Write(data) return buf.Bytes, nil}func (v *VariableDecode) Decode(data byte) (byte, int, error) { if len(data) = bslen { return data[v.FirstRecevedLen : v.FirstRecevedLen+bslen], bslen + v.FirstRecevedLen, nil } return nil, 0, ERROR_DECODE_LEN_NOT_ENOUGH}

上面可以看出,咱们不仅仅要解决半包粘包的问题,还要解决大小端的问题。

所谓大小端问题,就是在计算机系统中,多字节数据在内存中的存储顺序问题。计算机内存按字节划分为连续的线性地址空间,当存储多字节数据时,不同的存储顺序形成两种主要模式:大端(Big-Endian)和小端(Little-Endian)。大端就是高字节存储在低地址,低字节存储在高地址。例如,一个32位整数0x12345678在大端模式下的存储顺序为:12 34 56 78(从低地址到高地址)。小端就是低字节存储在低地址,高字节存储在高地址。例如,同样的32位整数0x12345678在小端模式下的存储顺序为:78 56 34 12(从低地址到高地址)。作为服务器客户都双方有可能大家的硬件架构不同,导致存储模式就不同,为了保证大家一致能解析出来,那么就要确认好大小端。

到这里基础的结构基本上就有了。但是实际开发过程中还要解决的问题还不少。比如是否应该解决解决那些发送数据特别异常的连接,像每秒钟给你发送1000个包过来的那种。比如是不是应该限制一下单个包的大小?因为你的内存不够呀,一个连接应用你要给多大缓冲区呀?你到底允许有多少个连接来呀?然后到了上层,你觉得你的机器能抗多少线程?像golang这种语言因为使用的是协程,这种的话相对来说要轻量级一些,相对还好,但是那种使用线程语言的呢?再到上层,你可能还要监控每个连接的连接情况,是不是已经死了,虽然tcp本身有keepalive,但是这种我记得linux是2个小时,而且不能传输自定义数据,还得想想弄个心跳机制。再到上层,由于业务处理的压力大,你消息来的多,堆积的就多,那么是不是应该来点丢弃消息的策略?

反正到了应用层,各种业务上的考虑还是很多的。我目前还在简单写一个tcp连接的服务器,有兴趣的可以看看 lnzw: 写一个游戏服务器架子,希望能学点东西吧 - Gitee.com

好了,今天就到这里吧。明天新入职一家公司,要去上班了。马上都要过年了。心难以平静。(今日三十来岁的菜鸟,真心自卑!!!)

来源:墨与白

相关推荐