TCP那些事儿 目录:
以前我也认为TCP是相当底层的东西go语言tcp数据包 , 我永远不需要去了解它 。虽然差不多是这样 , 但是实际生活中,go语言tcp数据包你依然可能遇见和TCP算法相关的bug , 这时候懂一些TCP的知识就至关重要了 。( 本文也可以引申为,系统调用,操作系统这些都很重要,这个道理适用于很多东西 )
这里推荐一篇小短文,人人都应该懂点TCP
使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数据结构,比如连接的状态、读写缓冲区、定时器等 。当通信结束时 , 双方必须关闭连接以释放这些内核数据 。TCP服务基于流,源源不断从一端流向另一端,发送端可以逐字节写入,接收端可以逐字节读出,无需分段 。
需要注意的几点:
TCP状态(11种):
eg.
以上为TCP三次握手的状态变迁
以下为TCP四次挥手的状态变迁
服务器通过 listen 系统调用进入 LISTEN 状态 , 被动等待客户端连接,也就是所谓的被动打开 。一旦监听到SYN(同步报文段)请求,就将该连接放入内核的等待队列,并向客户端发送带SYN的ACK(确认报文段) , 此时该连接处于 SYN_RECVD 状态 。如果服务器收到客户端返回的ACK , 则转到 ESTABLISHED 状态 。这个状态就是连接双方能进行全双工数据传输的状态 。
而当客户端主动关闭连接时,服务器收到FIN报文,通过返回ACK使连接进入 CLOSE_WAIT 状态 。此状态表示——等待服务器应用程序关闭连接 。通常,服务器检测到客户端关闭连接之后,也会立即给客户端发送一个FIN来关闭连接,使连接转移到 LAST_ACK 状态,等待客户端对最后一个FIN结束报文段的最后一次确认 , 一旦确认完成,连接就彻底关闭了 。
客户端通过 connect 系统调用主动与服务器建立连接 。此系统调用会首先给服务器发一个SYN,使连接进入 SYN_SENT 状态 。
connect 调用可能因为两种原因失败:1. 目标端口不存在(未被任何进程监听)护着该端口被 TIME_WAIT 状态的连接占用( 详见后文 ) 。2. 连接超时,在超时时间内未收到服务器的ACK 。
如果 connect 调用失败,则连接返回初始的 CLOSED 状态,如果调用成功,则转到 ESTABLISHED 状态 。
客户端执行主动关闭时,它会向服务器发送一个FIN,连接进入 TIME_WAIT_1 状态,如果收到服务器的ACK,进入 TIME_WAIT_2 状态 。此时服务器处于 CLOSE_WAIT 状态,这一对状态是可能发生办关闭的状态(详见后文) 。此时如果服务器发送FIN关闭连接 , 则客户端会发送ACK进行确认并进入 TIME_WAIT 状态 。
流量控制是为了控制发送方发送速率,保证接收方来得及接收 。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率 。将窗口字段设置为 0,则发送方不能发送数据 。
如果网络出现拥塞,分组将会丢失 , 此时发送方会继续重传 , 从而导致网络拥塞程度更高 。因此当出现拥塞时 , 应当控制发送方的速率 。这一点和流量控制很像,但是出发点不同 。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度 。
TCP 主要通过四种算法来进行拥塞控制: 慢开始、拥塞避免、快重传、快恢复 。
在Linux下有多种实现,比如reno算法 , vegas算法和cubic算法等 。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口 。
为了便于讨论,做如下假设:
发送的最初执行慢开始,令 cwnd=1 , 发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高 。设置一个慢开始门限 ssthresh , 当 cwnd = ssthresh 时 , 进入拥塞避免,每个轮次只将 cwnd 加 1 。
如果出现了超时 , 则令 ssthresh = cwnd/2,然后重新执行慢开始 。
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认 。例如已经接收到 M1 和 M2 , 此时收到 M4,应当发送对 M2 的确认 。
在发送方 , 如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段 。例如收到三个 M2,则 M3 丢失,立即重传 M3 。
在这种情况下 , 只是丢失个别报文段,而不是网络拥塞 。因此执行快恢复,令 ssthresh = cwnd/2,cwnd = ssthresh , 注意到此时直接进入拥塞避免 。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率 。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh 。
??发送端的每个TCP报文都必须得到接收方的应答,才算传输成功 。
??TCP为每个TCP报文段都维护一个重传定时器 。
??发送端在发出一个TCP报文段之后就启动定时器,如果在定时时间类未收到应答 , 它就将重发该报文段并重置定时器 。
??因为TCP报文段最终在网络层是以IP数据报的形式发送 , 而IP数据报到达接收端可能是乱序或者重复的 。TCP协议会对收到的TCP报文进行重排、整理,确保顺序正确 。
TCP报文段所携带的应用程序数据按照长度分为两种: 交互数据和成块数据
对于什么是粘包、拆包问题 , 我想先举两个简单的应用场景:
对于第一种情况 , 服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息 , 然后进行解码和后续处理... 。对于第二种情况 , 如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看 第二种情况 下客户端发送的两条消息递交到服务端有可能出现的情况:
第一种情况:
服务端一共读到两个数据包 , 第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费 。
第二种情况:
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包 。
第三种情况:
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中 , 或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分 , 这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的 。
我们知道tcp是以流动的方式传输数据,传输的最小单位为一个报文段(segment) 。tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小 , 一般为1460比特 。换算成字节,也就是180多字节 。
tcp为提高性能 , 发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方 。同理,接收方也有缓冲区这样的机制,来接收数据 。
发生TCP粘包、拆包主要是由于下面一些原因:
既然知道了tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生 , 那我们只能在应用层数据协议上,加以控制 。通常在制定传输数据时,可以使用如下方法:
写了一个简单的 golang 版的tcp服务器实例,仅供参考:
例子
参考和推荐阅读书目:
注释:
eg.
Golang 网络编程丝绸之路 - TCP/UDP 地址解析TL;DR在使用 Golang 编写 TCP/UDP socket 的时候,第一步做的就是地址解析 。
该函数返回的地址包含的信息如下:
TCPAddr里,IP既可以是 IPv4 地址,也可以是 IPv6 地址 。Port就是端口了 。Zone是 IPv6 本地地址所在的区域 。
从返回结果看该函数的参数, network指address的网络类型; address指要解析的地址,会从中解析出我们想要的IP ,Port和Zone。
从源码中可以看出,参数network只能是如下四个值,否则会得到一个错误 。
解析过程跟ResolveTCPAddr的一样,不过得到的是*UDPAddr。
UDPAddr包含的信息如下:
Go 语言自我提升 (三次握手 - 四次挥手 - TCP状态图 - udp - 网络文件传输)三次握手:
1. 主动发起连接请求端(客户端),发送 SYN 标志位,携带数据包、包号
2. 被动接收连接请求端(服务器) , 接收 SYN,回复 ACK,携带应答序列号 。同时,发送SYN标志位,携带数据包、包号
3. 主动发起连接请求端(客户端) , 接收SYN 标志位,回复 ACK 。
被动端(服务器)接收 ACK —— 标志着 三次握手建立完成( Accept()/Dial() 返回 )
四次挥手:
1. 主动请求断开连接端(客户端), 发送 FIN标志,携带数据包
2. 被动接受断开连接端(服务器),发送 ACK标志,携带应答序列号 。—— 半关闭完成 。
3. 被动接受断开连接端(服务器),发送 FIN标志 , 携带数据包
4. 主动请求断开连接端(客户端),发送 最后一个 ACK标志 , 携带应答序列号 。—— 发送完成,客户端不会直接退出,等 2MSL时长 。
等 2MSL待目的:确保服务器 收到最后一个ACK
滑动窗口:
通知对端本地存储数据的 缓冲区容量 。—— write 函数在对端 缓冲区满时,有可能阻塞 。
TCP状态转换:
1. 主动发起连接请求端:
CLOSED —— 发送SYN —— SYN_SENT(了解) —— 接收ACK、SYN,回发 ACK —— ESTABLISHED (数据通信)
2. 主动关闭连接请求端:
ESTABLISHED —— 发送FIN —— FIN_WAIT_1 —— 接收ACK —— FIN_WAIT_2 (半关闭、主动端)
—— 接收FIN、回复ACK —— TIME_WAIT (主动端) —— 等 2MSL 时长 —— CLOSED
3. 被动建立连接请求端:
CLOSED —— LISTEN —— 接收SYN、发送ACK、SYN —— SYN_RCVD —— 接收 ACK —— ESTABLISHED (数据通信)
4. 被动断开连接请求端:
ESTABLISHED —— 接收 FIN、发送 ACK —— CLOSE_WAIT —— 发送 FIN —— LAST_ACK —— 接收ACK —— CLOSED
windows下查看TCP状态转换:
netstat -an | findstr端口号
Linux下查看TCP状态转换:
netstat -an | grep端口号
TCP和UDP对比:
TCP: 面向连接的可靠的数据包传递 。针对不稳定的 网络层,完全弥补 。ACK
UDP:无连接不可靠的报文传输 。针对不稳定的 网络层,完全不弥补 。还原网络真实状态 。
优点缺点
TCP:可靠、顺序、稳定系统资源消耗大,程序实现繁复、速度慢
UDP:系统资源消耗小,程序实现简单、速度快不可靠、无序、不稳定
使用场景:
TCP:大文件、可靠数据传输 。对数据的 稳定性、准确性、一致性要求较高的场合 。
UDP:应用于对数据时效性要求较高的场合 。网络直播、电话会议、视频直播、网络游戏 。
UDP-CS-Server实现流程:
1.创建 udp地址结构 ResolveUDPAddr(“协议”, “IP:port”) —— udpAddr 本质 struct{IP、port}
2.创建用于 数据通信的 socket ListenUDP(“协议”, udpAddr ) —— udpConn (socket)
3.从客户端读取数据,获取对端的地址 udpConn.ReadFromUDP() —— 返回:n,clientAddr,err
4.发送数据包给 客户端 udpConn.WriteToUDP("数据" , clientAddr)
UDP-CS-Client实现流程:
1.创建用于通信的 socket 。net.Dial("udp", "服务器IP:port") —— udpConn (socket)
2.以后流程参见 TCP客户端实现源码 。
UDPserver默认就支持并发!
------------------------------------
命令行参数: 在main函数启动时 , 向整个程序传参 。【重点】
语法: go run xxx.goargv1 argv2argv3argv4。。。
xxx.exe:第 0 个参数 。
argv1 :第 1 个参数 。
argv2 :第 2 个参数 。
argv3 :第 3 个参数 。
argv4 :第 4 个参数 。
使用: list := os.Args提取所有命令行参数 。
获取文件属性函数:
os.stat(文件访问绝对路径) —— fileInfo 接口
fileInfo 包含 两个接口 。
Name() 获取文件名 。不带访问路径
Size() 获取文件大小 。
网络文件传输 —— 发送端(客户端)
1.获取命令行参数,得到文件名(带路径)filePathlist := os.Args
2.使用 os.stat() 获取 文件名(不带路径)fileName
3.创建 用于数据传输的 socketnet.Dial("tcp",“服务器IP port”) —— conn
4.发送文件名(不带路径)给接收端,conn.write()
5.读取 接收端回发“ok”,判断无误 。封装函数 sendFile(filePath, conn) 发送文件内容
6.实现 sendFile(filePath,conn)
1) 只读打开文件 os.Open(filePath)
for {
2) 从文件中读数据f.Read(buf)
3) 将读到的数据写到socket中conn.write(buf[:n])
4)判断读取文件的 结尾 。io.EOF. 跳出循环
}
网络文件传输 —— 接收端(服务器)
1. 创建用于监听的 socket net.Listen() —— listener
2. 借助listener 创建用于 通信的 socket listener.Accpet()—— conn
3. 读取 conn.read() 发送端的 文件名 , 保存至本地 。
4. 回发 “ok”应答 发送端 。
5. 封装函数,接收文件内容 recvFile(文件路径)
1) f = os.Create(带有路径的文件名)
for {
2)从 socket中读取发送端发送的 文件内容。conn.read(buf)
3)将读到的数据 保存至本地文件 f.Write(buf[:n])
4)判断 读取conn 结束,代表文件传输完成 。n == 0break
}
【golang】高并发下TCP常见问题解决方案首先,看一下TCP握手简单描绘过程:
其握手过程原理,就不必说了,有很多详细文章进行叙述,本文只关注研究重点 。
在第三次握手过程中 , 如果服务器收到ACK,就会与客户端建立连接,此时内核会把连接从半连接队列移除 , 然后创建新的连接,并将其添加到全连接队列 , 等待进程调用 。
如果服务器繁忙,来不及调用连接导致全连接队列溢出,服务器就会放弃当前握手连接,发送RST给客户端,即connection reset by peer 。
在linux平台上 , 客户端在进行高并发TCP连接处理时,最高并发数量都要受系统对用户单一进程同时打开文件数量的限制(这是因为系统每个TCP都是SOCKET句柄 , 每个soker句柄都是一个文件),当打开连接超过限制,就会出现too many open files 。
使用下指令查看最大句柄数量:
增加句柄解决方案
【go语言tcp数据包 go语言socket】关于go语言tcp数据包和go语言socket的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站 。
推荐阅读
- 酒吧门前修路如何引流客人,酒吧门口可以做点什么生意
- 视频调度术语是什么,视频调度机制
- 解绑支付宝公众号绑定,解绑支付宝公众号绑定的银行卡
- 摆摊吃饭直播文案,直播美食文案
- c语言一个函数必须包括 c语言函数需要定义吗
- flutter表单内容,flutter formfield
- 虚拟机穿透主机,虚拟机穿透主机怎么设置
- 海尔电视怎么读取u盘,海尔电视怎么读取u盘文件
- php导入文件到数据库 php如何导入数据库