十、网络编程
引言
两个程序a.py和b.py之间传递一个文件:a.py---------->文件------------>b.py
若在不同的电脑:计算机网盘、qq等,实现两个程序的通信
软件开发的架构
两个程序之间的通讯的应用大致分为两种:
第一种应用类:qq、微信、网盘、优酷这一类是需要安装的桌面应用
第二种是web类:百度、知乎、博客园等使用浏览器访问就可以直接使用的应用
这些应用的本质其实都是两个程序之间的通讯,而这两个分类又对应了两个软件开发的架构
c/s架构
client与sever,客户端与服务器端架构,这种架构是从用户层面(也可以是物理层面)来划分的
这里的客户端一般泛指客户端应用程序exe,程序需要先安装之后,才能在用户的电脑上,对用户的电脑操作系统环境依赖较大
B/s架构
browser与sever,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的
Browser浏览器,其实也是一种client客户端,只是这个客户端不需要安装应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能增删改查
网络基础
一个程序如何在网络上找到另一个程序:ip地址精确到每一台电脑,端口精确到具体的程序
IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。
osi七层模型


互联网的本质就是一系列的网络协议,这个协议就叫OSI协议(一系列协议),按照功能不同,分工不同,人为的分层七层,实际上这七层是不存在的,没有这七层的概念,只是认为的划分而已,区分出来的目的只是明白那一层是干什么的
1.物理层:主要定义物理设备标准,如网线的接口类型,各种传输介质的传输速率等,主要作用是传输比特流(就是1、0转化为电流强弱进行传输,到达目的地后再转化为1、0,也就是常说的数模与模数转换),这一层的数据叫做比特(bit),主要设备:网卡,网线,集线器,中继器,调制解调器
对应网络协议:IEEE 802.1A,IEEE 802.2到IEEE 802.11
2.数据链路层:主要将从物理层接收的数据进行MAC地址的封装与解封装,常把这一层的数据叫做帧,主要设备:网桥,交换机
早期数据链路层就是对电信号做分组(遵循统一的标准协议,即以太网协议Ethernet),Ethernet规定一组电信号称之为一个数据包,或者叫帧,每一数据帧分为:报头head和数据data两部分:
head(固定18个字节),包括发送者(源地址,6字节)、接收者(目标地址,6字节)、数据类型(6字节)
data(最短46字节,最长1500字节)
数据包的具体内容:head长度+data长度 = 最短64字节,最长1518字节,超过最大限制就分片发送,类似于写信,源地址:写信人,目标地址:收信人,路由器:邮局,计算机通信中的源地址和目标地址指的是mac地址
mac地址:Ethernet规定接入Internet的设备都必须具备网卡,发送端和接收端的地址指的是网卡的地址,即MAC地址,每块网卡出厂时都被烧录一个实际唯一的MAC地址,长度为48位2进制,通常由12位16进制表示(前六位是厂商编码,后六位是流水线号:00-16-EA-AE-3C-40),用来确认网络设备位置的位址
有了MAC地址就可以实现通信,在局域网中广播,所有设备拆包,如若接收者不是自己就会被丢弃
跨网络中就需要网络层
对应网络协议:FDDI,Ethernet,Arpanet,PDN,SLIP,PPP
3.网络层:选择合适的网间路由和交换节点,确保数据及时传送,将从下层接收到的数据进行IP地址的封装和解封装,常把这一层数据叫做数据包,主要设备:路由器
网络层定义了一个IP协议,若是跨网络发包,会将包交给网关(从一个网络连接到另一个网络的关口,通常网关就是路由器的IP)来转发,MAC地址和IP地址标识了在互联网中的位置
注:从外网来看,局域网内所有的主机都为同一地址,从局域网内部来看,各主机的IP地址是唯一的,也就是私有IP地址(标识internet的主机)
在数据链路层,数据封装了两层
如果不知道对方的MAC地址,需要获取对方的MAC地址,需要ARP协议(无论是在局域网还是跨局域网):
局域网:首先自己的MAC、源IP已知,目标IP已知,目标MAC设置为12个F(广播地址),表达的是想获取目标IP为172.16.10.11的MAC地址,MAC为12个F代表的是一种功能,就是获取对方的MAC地址,当广播了,所有设备解包,只要IP地址为172.16.10.11才会返回MAC地址,其他的全部丢弃
跨网络:通过IP地址区分,目标IP变成了网关的地址,
对应网络协议:IP,ICMP,ARP,RARP,AKP,UUCP
4.传输层:定义了一些传输数据的协议和端口,如TCP、UDP协议,主要将从下层接收的数据进行分段和传输,到达目的地址后再进行组装,以往把这一层数据叫做段
传输层功能:建立端口到端口的通信
端口即应用程序和网卡关联的编号,通过‘IP地址+端口号’来区分不同的服务,比如web服务,FTP服务,SMTP服务等


大家都遵守的网络通信协议:TCP/IP协议
TCP协议:
当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据,例如,当你想查看网页或查看电子邮件时,希望完成且按顺序查看网页,而不丢失任何内容,当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都是希望得到的结果,于是用到了TCP
可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割
当应用程序希望通过TCP与另一个应用程序通信时,它会发送一个通信请求,这个请求必须被送到一个确切的地址,在双方‘握手’之后,TCP将在两个应用程序之间建立一个全双工(full-duplex)的通道,这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或者双方关闭为止
TCP通信需要经过创建连接、数据传送、终止连接三个步骤
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中‘打电话’
TCP协议的特点:
面向连接:发送数据前必须在两端建立连接,建立连接的方法是‘三次握手’,这样能建立可靠的连接,建立连接,为数据的可靠传输打下了基础
仅支持单播通道:每个TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式
面向字节流:TCP不像UDP一样那样一个个报文独立的传输,而是在不保留报文边界的情况下以字节流方式进行传输
可靠传输:对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号,TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号保证了传送到接收端实体的包的按序接收,然后接收端实体对已成功收到的字节发一个相应的确认(ACK);如果端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传
提高拥塞控制:当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
TCP提供全双工通信:TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据,当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
UDP协议:
不可靠传输,‘报头’部分一共只有8个字节,总长度不超过65535字节,正好放进一个IP数据包,UDP与TCP位于同一层,但对于数据包的顺序错误或重发,因此,UDP不被应用于那些使用虚电路的面向连接的服务,UDP主要用于那些面向查询应答的服务。UDP通信模型中,在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中的‘写信’
UDP的主要特点:
1)UDP是面向无连接的,即发送数据之前不需要建立连接(发送数据结束时也没用连接可释放),减少了开销和发送数据,不需要和TCP一样在发送数据前进行三次握手建立连接,想发数据就可以开始发送了,并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作
具体来说:在发送端,应用层将数据传递给传输层的UDP协议,UDP只会给数据增加一个UDP头标识下是UDP协议,然后就传递给网络层了
在接受端,网络层将数据传递给传输层,UDP只会去除IP报文头就传递给应用层,不会任何拼接操作
2)UDP使用尽最大努力交付,即不保证可靠交付,主机不需要维持复杂的连接状态表。首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发;并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不关心对方是否已经正确接收到数据了;再者网络环境时好时坏,但是UDP因为没有堵塞控制,一直会以恒定的速度发送数据,即使网络条件不好,也不会对发送速率进行调整,这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用UDP而不是TCP
3)UDP是面向报文的,发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留报文的边界
4)头部开销小,传输数据报文时是很高效的


UDP用户数据报首部中检验和的计算方法有些特殊,在计算检验和时,要在UDP用户数据报之前增加12个字节的伪首部,所谓伪首部是因为这种伪首部并不是UDP用户数据报真正的首部,只是在计算检验和时,临时添加在UDP用户数据报前面,得到一个临时的UDP用户数据报,检验和就是按照这个临时用户数据报来计算的,伪首部既不向下传,也不向上递交,而仅仅是为了计算检验和。
UDP在IP报文中的协议号是17
UDP头部包含了以下几个数据:
两个十六位的端口号,分别为源端口(可选字段)和目标端口
UDP用户数据报文的长度,其最小值是8(仅有首部)
整个数据报文的检验和(IPV4 可选字段),该字段用于发现头部信息和数据中的错误,有错就丢弃,因此UDP的头部开销小,只有八字节,相比TCP的至少二十字节要少的多,在传输数据报文时是很高效的
优点:1)协议标准是完全开放的,可以供用户免费使用,并且独立于特定的计算机硬件与操作系统
2)独立于网络硬件系统,网络中每一设备和终端都具有一个唯一地址
3)网络地址统一分配,网络中每一设备和终端都具有一个唯一地址
4)高层协议标准化,可以提供多种多样可靠网络服务


TCP和UDP的区别
1)TCP是面向连接的传输协议,传输前双方需建立连接通道,而UDP可以直接传输
2)TCP传输信息可靠,信息传输无差错,不丢失,不重复,且按序到达,UDP传输不保证可靠
3)TCP是字节流传输(较长数据分割成数据块进行传输),而UDP是报文流传输(给多少传多少)
4)TCP为了实现传输可靠性使用了较为复杂的算法和实现过程,不适用于实时传输,UDP实时性较好
5)每一条TCP连接只能是点对点的,UDP支持一对一,一对多,多对一,多对多
6)TCP上占用系统资源较多,UDP相对较少
TCP三次握手的具体过程,并解释为什么需要三次握手
第一次握手:客户端向服务器端发送请求,发送SYN包(同步序列编号)到客户端,并进入SYN_SENT状态
第二次握手:服务器端获取到SYN包后,需要回复确认ACK,同时发送自己的SYN,并进入SYN_RECV状态
第三次握手:客户端获取到服务端发送的ACK和SYN后,需要回复确认ACK,发送完毕后,两端同时进入ESTABLISHED
分析:
TCP可视为全双工通道,分为两个通信方向:client>>sever / sever>>client
第一次握手:客户端提交数据传输请求(c提交申请,但c自身不知道c>>s和s>>s是否可行)
第二次握手:服务端应答客户端,表示确认接收到客户端数据,并发出SYN,等待客户端应答(s对c的请求进行应答,s可确认c>>s可行,但不知道s>>c是否可行)
第三次握手:客户端应答服务器端,表示确认接受到服务器端数据,建立连接(c对s请求进行应答,c可知s>>c和c>>s均可行,s接收应答后可知s>>c可行,即可建立连接)
TCP四次断开连接的过程,并分析为什么断开需要四次
第一步:当主机A的应用程序通知TCP数据已经发送完毕时,TCP向主机B发送一个带有FIN附加标记的报文段(FIN表示英文finish)
第二步:主机B接受到这个FIN报文段之后,并不立即用FIN报文段回复主机A,而是先向主机A发送一个确认序号ACK,同时通知自己相应的应用程序:对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段)
第三步:主机B的应用程序告诉TCP:我要彻底的关闭连接,TCP向主机A发送一个FIN报文段
第四步:主机A收到这个FIN报文段后,向主机B发送一个ACK表示连接彻底释放
分析:
同样,关闭连接前要两端都确认自己本端和对端数据传输完毕,即获取对方发送的FIN并应答(ACK)告诉对方自己获取
第一步:A主机发送给B主机的消息传输完毕,向B发送FIN,告知B自己发送完毕,准备关闭连接
第二步:B主机在未发送完毕的情况下接受到A的FIN,向A回复ACK且通知本机应用层,并将未发送完毕的数据继续发送完(B得知A发送完毕,A接受ACK后得知B接受到自己发送的FIN报文)
第三步:B主机发送完毕后,发出FIN报文,并等待A的应答(2msl内没有收到A的应答,则重发)(B传输完毕,但不知道A是否接受到自己的FIN报文)(msl指一个片段在网络中最大的存活时间,2msl就是一个发送和一个回复所需的最大时间)
第四步:A接收到B的FIN报文,做出ACK应答,在等待2msl后自行关闭(A应答,B收到后得知A收到了自己的FIN报文,B关闭,A在2msl之内没有收到B的FIN重发,判断B已经收到了自己的ACK,A关闭)
四次挥手可以看作是改良版的三次握手,只不过在第二次握手时考虑到B端没有发送完毕的情况,因此ACK和FIN分两次进行发送,变成了四次
对应网络协议:TCP,UDP


5.会话层:通过传输层建立数据传输通路,在系统之间发起会话或者接受会话请求(设备之间要互相认识)
会话层:SMTP,DNS
6.表示层:主要是进行对接收的数据进行解释、压缩与解压等操作,即把计算机能够设别的东西转化为人能够识别的东西(如图片、声音等)
Telnet,Rlogin,SNMP,Gopher
7.应用层:主要是一些终端的应用,如FTP(各种文件下载)、浏览器、QQ等,可以将其理解为在电脑屏幕上可以看到的东西,也就是终端应用
对应网络协议:HTTP,TFTP,FTP,NFS,WAIS,SMTP
socket概念
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket,socket通常也称作‘套接字’,用于描述IP地址和端口,是一个通信链的句柄,是协议和应用程序的接口,通过它来实现通信,可以跨网络,是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本机主机的IP地址,本地进程的协议端口,远地主机的ip地址,远地进程的协议端口,应用层可以和传输层通过socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,Socket其实就是一个门面模式,它把负责的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
Socket就是一个模块,通过调用模块中已经实现的方法建立两个进程之间的连接和通信,也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来表示这台机器上的一个应用程序,所以只需要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信,在Internet上的主机一般运行了多个服务软件,同时提供了多种服务,每种服务都打开一个socket,并绑定到端口上,不同的端口对应于不同的服务
socket的英文原意为孔和插座,像一个多孔插座,一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电,有的提供110伏交流电,有的则提供有限电视,客户软件将插头插在不同编号的插座可以得到不同的服务
两个程序通过‘网络’交互数据就使用socket,它只负责两件事:建立连接,传递数据。
socket的实现就像是快递员取快递的过程,寄出包裹的是Server端,快递员是client端,send和recv中的内容就是我们要寄出的东西,准备好了要寄出的东西,要告诉快递员来那里取件,其中sever中socket.bind方法里传了一个元组('ip','port')给bind方法,就是把包裹放在这个地址的这个端口等待快递员,而client端connect方法告诉快递员去这个地址取东西,所以元组中的ip和port内容必须一直,与快递员约定好,send什么,客户端就recv什么,因为快递员只管寄,其余不管。
补充知识:
一个指定的端口号不能被多个程序共用,比如IIS占用了80端口,那么Apache就不能也用80端口了
很多防火墙只允许特定目标端口的数据包通过
服务程序在listen某个端口并accept某个连接请求后,会生成一个新的socket来对该请求进行处理
套接字(socket)初使用
基于TCP协议的socket
TCP是基于链接的,必须先启动服务器,然后再启动客户端去链接服务器
#sever端(服务器端) import socket sk = socket.socket() #创建socket对象 sk.bind(('127.0.0.1',8898)) #绑定监听ip,端口,把地址绑定到套接字,模板可替换,在这里等待 sk.listen() #监听链接,开始监听,设置client端最大等待连接数 while True: print('waiting...') conn,addr = sk.accept() #接受客户端链接,accept阻塞,等待。。。直到有client端请求连接 ;conn代表客户端socket对象,addr是客户端IP地址 ret = conn.recv(1024) #接受客户端信息,接受套接字的数据,数据以字符串形式返回,bufsize指定最多可以接受的数量 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息,模板可替换 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选) #client端(客户端) import socket sk = socket.socket() #创建客户套接字 sk.connect(('127.0.0.1',8898)) #尝试连接服务器,模板可替换,去这里取,与上述要一致
sk.send(b'hello!') #加b无意义,发消息给服务端 ret = sk.recv(1024) #对话(发送/接受),从服务端接收消息 print(ret) sk.close() #关闭客户套接字
可能会遇到问题
#加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv(1024) #接收客户端信息 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选)
基于UDP协议的socket
UDP是无链接的,启动服务之后可以直接接收消息,不需要提前建立链接
#sever端(服务器端) import socket udp_sk = socket.socket(type = socket.SOCK_DGRAM) #创建一个服务器的套接字 udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字,将套接字绑定到地址,Address的地址的格式取决于地址簇,在AF_INET下,以元组(host,port)的形式表示地址。 msg,addr = udp_sk.recvfrom(1024)#与recv()类似,但返回值是(data,address),其中data是包含接受数据的字符串,address是发送数据的套接字地址 print(msg) udp_sk.sendto(b'hi',addr) #对话(接受与发送) udp_sk.close() #client端(客户端) import socket ip_port = ('127.0.0.1',9000) udp_sk.sento(b'hello',ip_port) #将数据发送到连接的套接字,返回值是要发送的字节数量,该数量可能小于String的字节大小,即:可能未将制定内容全部发送 back_msg,addr = udp_sk.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
tcp协议和udp协议
https://www.cnblogs.com/fundebug/p/differences-of-tcp-and-udp.html
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接受缓存)、面向字节流,使用TCP的应用:Web浏览器;电子邮件、文件传输程序
UDP(User Datagram Protocol)不可靠的、无法连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面对报文,尽最大努力服务,无拥塞控制,使用UDP的应用:域名系统(DNS);视频流;IP语言(VoIP)
#qq聊天 import socket ip_port = ('127.0.0.1',8081) #把地址绑定到套接字 udp_server_sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#创造一个服务器套接字 udp_server_sock.bind(ip_port)#绑定服务器套接字 while True: qq_msg,addr = udp_server_sock.recvfrom(1024) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg = input('回复消息:').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr) import socker BUFSIZE = 1024 udp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic = {'金老板':('127.0.0.1',8081), '哪吒':('127.0.0.1',8081), 'egg':('127.0.0.1',8081), 'yuan':('127.0.0.1',8081),} while True: qq_name = input('请选择聊天对象:').strip() while True: msg = input('请输入消息,回车发送,输入q结束和他聊天:').strip() if msg == '1':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr = udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
sk = socket.socket(family = AF_INET, type = SOCK_STREAM, proto = 0, fileno = None)
family:
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用就是底层的文件系统来抓取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信(使用本地socket文件来通信)
基于网络类型的套接字家族
套接字家族的名字:AF_INET(默认值) ,所有的地址家族中,AF_INET是使用最广泛的,python支持多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET
type:
socket.SOCK_STREAM 流式socket,for TCP(默认),有保障的(即能保证数据正确传送到对方)面向连接的socket,多用于资料传送
socket.SOCK_DGRAM 数据报式socket,for UDP,无保障面向消息的socket,多用于在网络上发广播信息
proto:
通常为0,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
黏包现象
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显然就是黏包
只有TCP有黏包现象,UDP永远不会黏包
黏包成因
TCP协议中的数据传递
TCP协议的拆包机制
当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去,MTU是maximum Transmission unit的缩写,意思是网络上传送的最大数据包,MTU的单位事字节,大部分网络设备的MTU都是1500,如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度
基于TCP协议特点的黏包现象成因

发送端可以是1k1k的发送数据,而接收端的应用程序可以是2k2k的提走数据,当然也有可能一次提走3k或6k数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或者说是一个流,一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现黏包问题的原因
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的,如何定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条消息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成之后才呈现在内核缓冲区
例如基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何时开始,在何时结束,此外,发送方引起的黏包是由TCP协议本身造成的,TCP为了提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段,若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段之后一次发送出去,这样接收方就收到了黏包数据
UDP不会发生黏包
UDP是无连接的,面向消息的,提供高效率服务的,不会使用块的合并优化算法,由于UDP支持一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了,即面向消息的通信是有消息保护边界的
对于空消息:TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息处理机制,防止程序卡住,而UDP是基于数据报的,即使输入的内容为空(直接回车),也可以被发送,UDP协议会帮你封装上消息头发送过去
不可靠不黏包的UDP协议:UDP的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y,x数据就丢失,这意味着UDP根本不会黏包,但是会丢数据,不可靠
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535 - IP头(20) - UDP头(8)= 65507字节,用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误(丢弃这个包,不进行发送)
用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制,而实际上,所制定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送
会发生黏包的两种情况
情况一:发送方的缓存机制,发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据量很小,会合在一起,产生黏包)
情况二:接收方不及时接受缓冲区的包,造成多个包接受(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生黏包)
总结:
黏包现象只发生在TCP协议中:
1)从表面看,黏包问题主要是因为发送方和接收方的缓存机制、TCP协议面向流通信的特点
2)实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
黏包解决方案:
解决方案一:让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端一个死循环接收完所有的数据(问题在于程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会方法网络延迟带来的性能损耗)
解决方案二:借助模块,可以把要发送的数据长度转换为固定长度的字节,这样客户端每次接受消息之前只要先接受这个固定长度字节的内容看一下接下来要接受信息的大小,那么最终接受的数据只要达到这个值就停止,就可以不多不少的接受完整的数据,利用struct模块,要发送的数据长度数字可以被转换成一个标准大小的4字节数字,因此可以利用这个特点来预先发送数据长度,无论输入多大的数字,都可以转换成四位字节,这样接收端就固定收四个字节就好了。

还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struct将序列化后的数据长度打包成4个字节
??????????????????????????????????????????????????????????????????????


浙公网安备 33010602011771号