目录
TCP 发送数据的四种情况
粘包的两种情况
服务端
客户端
服务端
客户端
补充问题一: 为何 TCP 是可靠传输, udp 是不可靠传输
补充问题二: send(字节流)和 recv(1024)及 sendall
注意: 只有 TCP 有粘包现象, UDP 永远不会粘包, 为何, 且听我娓娓道来.
首先需要掌握一个 socket 收发消息的原理
发送端可以是一 K 一 K 地发送数据, 而接收端的应用程序可以两 K 两 K 地提走数据, 当然也有可能一次提走 3K 或 6K 数据, 或者一次只提走几个字节的数据, 也就是说, 应用程序所看到的数据是一个整体, 或说是一个流(stream), 一条消息有多少字节对应用程序是不可见的, 因此 TCP 协议是面向流的协议, 这也是容易出现粘包问题的原因. 而 UDP 是面向消息的协议, 每个 UDP 段都是一条消息, 应用程序必须以消息为单位提取数据, 不能一次提取任意字节的数据, 这一点和 TCP 是很不同的. 怎样定义消息呢? 可以认为对方一次性 write/send 的数据为一个消息, 需要明白的是当对方 send 一条信息的时候, 无论底层怎样分段分片, TCP 协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区.
例如基于 TCP 的套接字客户端往服务端上传文件, 发送时文件内容是按照一段一段的字节流发送的, 在接收方看了, 根本不知道该文件的字节流从何处开始, 在何处结束.
所谓粘包问题主要还是因为接收方不知道消息之间的界限, 不知道一次性提取多少字节的数据所造成的.
此外, 发送方引起的粘包是由 TCP 协议本身造成的, TCP 为提高传输效率, 发送方往往要收集到足够多的数据后才发送一个 TCP 段. 若连续几次需要 send 的数据都很少, 通常 TCP 会根据优化算法把这些数据合成一个 TCP 段后一次发送出去, 这样接收方就收到了粘包数据.
TCP(transport control protocol, 传输控制协议)是面向连接的, 面向流的, 提供高可靠性服务. 收发两端 (客户端和服务器端) 都要有一一成对的 socket, 因此, 发送端为了将多个发往接收端的包, 更有效的发到对方, 使用了优化方法(Nagle 算法), 将多次间隔较小且数据量小的数据, 合并成一个大的数据块, 然后进行封包. 这样, 接收端, 就难于分辨出来了, 必须提供科学的拆包机制. 即面向流的通信是无消息保护边界的.
UDP(user datagram protocol, 用户数据报协议)是无连接的, 面向消息的, 提供高效率服务. 不会使用块的合并优化算法,, 由于 UDP 支持的是一对多的模式, 所以接收端的 skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的 UDP 包, 在每个 UDP 包中就有了消息头(消息来源地址, 端口等信息), 这样, 对于接收端来说, 就容易进行区分处理了. 即面向消息的通信是有消息保护边界的.
TCP 是基于数据流的, 于是收发的消息不能为空, 这就需要在客户端和服务端都添加空消息的处理机制, 防止程序卡住, 而 udp 是基于数据报的, 即便是你输入的是空内容(直接回车), 那也不是空消息, udp 协议会帮你封装上消息头, 实验略
udp 的 recvfrom 是阻塞的, 一个 recvfrom(x)必须对唯一一个 sendinto(y), 收完了 x 个字节的数据就算完成, 若是 y>x 数据就丢失, 这意味着 udp 根本不会粘包, 但是会丢数据, 不可靠
TCP 的协议数据不会丢, 没有收完包, 下次接收, 会继续上次继续接收, 己端总是在收到 ack 时才会清除缓冲区内容. 数据是可靠的, 但是会粘包.
TCP 发送数据的四种情况
假设客户端分别发送了两个数据包 D1 和 D2 给服务端, 由于服务端一次读取到的字节数是不确定的, 故可能存在以下 4 种情况.
服务端分两次读取到了两个独立的数据包, 分别是 D1 和 D2, 没有粘包和拆包;
服务端一次接收到了两个数据包, D1 和 D2 粘合在一起, 被称为 TCP 粘包;
服务端分两次读取到了两个数据包, 第一次读取到了完整的 D1 包和 D2 包的部分内容, 第二次读取到了 D2 包的剩余内容, 这被称为 TCP 拆包;
服务端分两次读取到了两个数据包, 第一次读取到了 D1 包的部分内容 D1_1, 第二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包.
特例: 如果此时服务端 TCP 接收滑窗非常小, 而数据包 D1 和 D2 比较大, 很有可能会发生第五种可能, 即服务端分多次才能将 D1 和 D2 包接收完全, 期间发生多次拆包.
粘包的两种情况
1. 发送端需要等缓冲区满才发送出去, 造成粘包(发送数据时间间隔很短, 数据了很小, 会合到一起, 产生粘包)
服务端
- # _*_coding:utf-8_*_
- __author__ = 'nickchen121'
- from socket import *
- ip_port = ('127.0.0.1', 8080)
- TCP_socket_server = socket(AF_INET, SOCK_STREAM)
- TCP_socket_server.bind(ip_port)
- TCP_socket_server.listen(5)
- conn, addr = TCP_socket_server.accept()
- data1 = conn.recv(10)
- data2 = conn.recv(10)
- print('----->', data1.decode('utf-8'))
- print('----->', data2.decode('utf-8'))
- conn.close()
客户端
- # _*_coding:utf-8_*_
- __author__ = 'nickchen121'
- import socket
- BUFSIZE = 1024
- ip_port = ('127.0.0.1', 8080)
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- res = s.connect_ex(ip_port)
- s.send('hello'.encode('utf-8'))
- s.send('feng'.encode('utf-8'))
2. 接收方不及时接收缓冲区的包, 造成多个包接收(客户端发送了一段数据, 服务端只收了一小部分, 服务端下次再收的时候还是从缓冲区拿上次遗留的数据, 产生粘包)
服务端
- # _*_coding:utf-8_*_
- __author__ = 'nickchen121'
- from socket import *
- ip_port = ('127.0.0.1', 8080)
- TCP_socket_server = socket(AF_INET, SOCK_STREAM)
- TCP_socket_server.bind(ip_port)
- TCP_socket_server.listen(5)
- conn, addr = TCP_socket_server.accept()
- data1 = conn.recv(2) # 一次没有收完整
- data2 = conn.recv(10) # 下次收的时候, 会先取旧的数据, 然后取新的
- print('----->', data1.decode('utf-8'))
- print('----->', data2.decode('utf-8'))
- conn.close()
客户端
- # _*_coding:utf-8_*_
- __author__ = 'nickchen121'
- import socket
- BUFSIZE = 1024
- ip_port = ('127.0.0.1', 8080)
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- res = s.connect_ex(ip_port)
- s.send('hello feng'.encode('utf-8'))
补充问题一: 为何 TCP 是可靠传输, udp 是不可靠传输
基于 TCP 的数据传输请参考我的另一篇文章, TCP 在数据传输时, 发送端先把数据发送到自己的缓存中, 然后协议控制将缓存中的数据发往对端, 对端返回一个 ack=1, 发送端则清理缓存中的数据, 对端返回 ack=0, 则重新发送数据, 所以 TCP 是可靠的
udp 发送数据, 对端是不会返回确认信息的, 因此不可靠
补充问题二: send(字节流)和 recv(1024)及 sendall
recv 里指定的 1024 意思是从缓存里一次拿出 1024 个字节的数据
send 的字节流是先放入己端缓存, 然后由协议控制将缓存内容发往对端, 如果待发送的字节流大小大于缓存剩余空间, 那么数据丢失, 用 sendall 就会循环调用 send, 数据不会丢失
粘包问题
来源: http://www.bubuko.com/infodetail-3093923.html