网络编程 套接字socket TCP UDP

 网络编程与套接字

网络编程

网络编程是什么:

    ​ 网络通常指的是计算机中的互联网,是由多台计算机通过网线或其他媒介相互链接组成的
​        编写基于网络的应用程序的过程序称之为网络编程.

  网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组包,
    在接收端按规定好的协议把包进行解析,从而提取出相应的信息,达到通信的目的.
    其中最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,
    其中涉及到代码,数据库,网页设计,服务器架设,
    c/s类与b/s类是目前主要的程序架构,
    c/s是Clien   t与Server即:
    客户端与服务端架构,例如手机上的微信,支付宝,电脑上的qq,等都是需要先下载客户端
    才能使用相应的功能.
    b/s是Browser与Server,浏览器与服务器端架构,入常用的浏览器提供的服务,例如百度,各类
    与目前兴起的各类小程序

网络模型:
    按照分工不同:
    常见的有osi 七层模型
    Open System Interconnection Reference Model
    开放式系统互联通信参考模型
    应用层
    表示层
    会话层
    传输层
    网络层
    数据链路层
    物理层


    注: 在5层模型中 应用层与表示层,会话层可合并为应用层,

        事实上人来使用计算机进行通信往往是要进行程序与程序之间的信息交互,
        而这就需要用到ip与端口 以此在互联网中成千上万的电脑中找到目标电脑中
        的目标程序.


    ps:
            目的是要链接互联网中的其他计算机

        1物理层 用物理介质链接其他计算机 
                    通讯,必须建立物理连接,连接的方式多种多样,包括电缆,光缆,无线电等;
                    基于电子器件发送电流信号,根据电流的高低可以对应到数字0和1,也就是二进制数据

        2数据链路层 数据链路层的由来:
                    单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
                以太网协议:
                以太网协议(Ethernet)工作在数据链路层,其规定了电信号分组方式,以及一组电信号应该包含哪些内容
                ethernet规定如下:

                一组电信号构成一个数据包,叫做‘帧’

                每一数据帧分成:报头head和数据data两部分
                
                head包含:(固定18个字节)

                发送者/源地址,6个字节
                接收者/目标地址,6个字节
                数据类型(标签+以太类型),6个字节
                data包含:(最短46字节,最长1500字节)

                数据包的具体内容
                head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
                
                    ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
                        mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
                                用MAC地址来通讯,但仅限于同一局域网

        3网络层 
                    
                3.1 网络层作用:
                    有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了
                    世界范围的互联网是由一个个彼此隔离的小的局域网组成的,如果所有的计算机都采用以太网的广播方式来寻找其他计算机,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难,(广播风暴就是这么产生的)
                    
                    
                    必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,就采用路由的方式(向不同广播域/子网分发数据包),mac地址是无法区分的,它只跟厂商有关;

                    网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址.
                    
                    IP协议是工作在网络层的协议,全称:Internet Protocol Address,翻译为互联网协议地址
               
                                        
                        用ip确定全球范围的某个局域网中的某一台计算机

            
                3.2ip地址
                    ip地址:
                    目前主流的有老版本的ipv4协议
                    其取值范围为0,0,0,0 只255,255,255,255 可以归纳为点分十进制
                    
                    ipv4
                    ip地址分类
                    
                    A类保留给政府机构

                    ​ 10.0.0.1 - 10.255.255.254

                    ​ B类分配给中等规模公司

                    ​ 172.16.0.1 - 172.31.255.254

                    ​ C类分配给任何需要的人

                    ​ 192.168.0.1 - 192.168.255.254

                    ​ D类用于组播

                    ​ E类用于实验

                    我们的电脑ip通常都是C类的,以192.168开头,正因为C类任何人都可以用
                    
                    但随着科技的发展,很明显ipv4提供的总ip地址渐渐不能满足日益庞大的互联网用户数量的需求,
                    而推出了ipv6协议,正在逐渐普及中.
                    
                    
                    子网掩码
                        什么是子网掩码
                        子网掩码是一个32位地址,用于屏蔽IP地址的一部分以区别网络标识和主机标识,并说明该IP地址是在局域网上,还是在远程网上。

                        它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

                        为什么需要子网掩码
                        单纯的ip地址段只是标识了ip地址的种类,无法辨识一个ip所处的子网
                        例:192.168.10.1与192.168.10.2并不能确定二者处于同一子网,因为不清楚哪些位表示网络号,哪些表示主机号

                        子网掩码如何判断两个ip是否属于同一个子网
                        知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。

                        案例: 已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,
                            
                        172.16.10.1:10101100.00010000.00001010.000000001
                        255255.255.255.0:11111111.11111111.11111111.00000000
                        AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0

                        172.16.10.2:10101100.00010000.00001010.000000010
                        255255.255.255.0:11111111.11111111.11111111.00000000
                        AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
                        结果都是172.16.10.0,因此它们在同一个子网络。
                        总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。

                        3.3 IP数据包(了解)
                        ip数据包也分为head和data部分,无须为ip包定义单独的栏位,直接放入以太网包的data部分

                        head:长度为20到60字节

                        data:最长为65,515字节。

                        而以太网数据包的”数据”部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据帧,分开发送了。

                        image-20181205184151377

                    3.4 ARP协议(了解)
                        ARP协议的由来:IP是通常是动态分配的,是一个逻辑地址,而数据传输则必须依赖MAC地址,那如何才能通过IP得到对方的MAC地址呢? 这就需要ARP协议了

                        arp协议功能:广播的方式发送数据包,获取目标主机的mac地址

                        首先明确每台主机ip都是已知的,并可以通过子网掩码来判断是否属于同一子网

                        案例1:主机192.168.1.101访问192.168.1.102

                        是同一子网内 ARP请求帧内容:

                        image-20181205202915810

                        1.FF:FF:FF:FF:FF:FF是一个特殊的MAC地址 交换机在看到这个地址时会将这个数据向网内所有主机进行广播

                        2.192.168.1.102 收到ARP请求后 回复自己的MAC给 源MAC主机

                        3.发送方(192.168.1.101)收到回复后,会将对方的ip的MAC地址映射关系存储到缓存中,以便下次使用

                        ps:arp -a 可以查看ARP缓存列表

                        image-20181205203120581

                        确定对方MAC地址后的数据帧内容:

                        image-20181205204704674

                        案例2:主机192.168.1.101访问192.168.111.101

                        交换机发现目标IP不在当前子网中,

                        1.交换机发起ARP请求,将目标IP设置为对方的网关IP,默认情况下,网关的主机号都为1; 所以接收方(192.168.111.101)的网关为192.168.111.1
                        ps :网关
                            网关(Gateway)又称网间连接器、协议转换器。网关在网络层以上实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连。网关既可以用于广域网互连,也可以用于局域网互连。
                  网关是一种充当转换重任的计算机系统或设备。使用在不同的通信协议、数据格式或语言,甚至体系结构完全不同的两种系统之间
                ,网关是一个翻译器。与网桥只是简单地传达信息不同,网关对收到的信息要重新打包,以适应目的系统的需求。同层
--应用层。    网关实质上是一个网络通向其他网络的IP地址。比如有网络A和网络B,网络A的IP地址范围为“192.168.1.1~192. 168.1.254”,子网掩码为255.255.255.0;网络B的IP地址范围为“192.168.2.1~192.168.2.254”,
                网掩码为255.255.255.0。在没有路由器的情况下,两个网络之间是不能进行TCP/IP通信的,
                  即使是两个网络连接在同一台交换机(或集线器)上,TCP/IP协议也会根据子网掩码(255.255.255.0)判定两个网络中的主机处在不同的网络里。而要实现这两个网络之间的通信,则必须通过网关。
                  如果网络A中的主机发现数据包的目的主机不在本地网络中,
                    就把数据包转发给它自己的网关,再由网关转发给网络B的网关,
                  网络B的网关再转发给网络B的某个主机

            网络A向网络B转发数据包的过程。 所以说,只有设置好网关的IP地址,TCP
/IP协议才能实现不同网络之间的相互通信。那么这个IP地址是哪台机器的IP地址呢?网关的IP地址是具有路由功能的设备的IP地址,具有路由功能的设备有路由器、
            启用了路由协议的服务器(实质上相当于一台路由器)、代理服务器(也相当于一台路由器)。 在和 Novell NetWare 网络交互操作的上下文中,网关在 Windows 网络中使用的服务器信息块 (SMB) 协议以及NetWare网络使用的 NetWare 核心协议 (NCP) 之间起着桥梁的作用。网关也被称为 IP路由器。 发送方网关发起的ARP数据帧: image
-20181205203744008 2.对方网关收到请求后发现ip是自己的ip则回复ARP请求,将其MAC地址告知发送方网关, 3.发送方网关将对方的网关的MAC地址与IP存储到自己的ARP缓存中, 4.告知发送方(192.168.1.101)对方网关的MAC地址,发送方同样将对方网关MAC与目标IP映射关系存储到,本机ARP缓存中 至此ARP请求结束可以开始传输数据 后续确定了MAC地址后发送的数据帧内容: image-20181205204404934 总结:ARP通过广播的方式来获取MAC地址, 不在同一子网时 ARP得到的时对方网关的MAC地址,数据到达对方网关后,由网关根据IP交给对应的主机,当然对方网关获取主机MAC也是通过ARP ps:路由器 交换机都可以称之为网关! 传输层 传输层的由来: ​ 通过物理层建立链接通道 ​ 通过数据链路层的MAC,可以定位到某个局域网中的某台主机, ​ 通过网络层的IP地址,子网掩码,可以定位到全球范围某一局域网下的某台主机 剩下的 只需要 我们提供相应的程序端口 即可实现不同电脑之间程序与程序的通讯 端口号,端口是需要联网的应用程序与网卡关联的编号,用端口来确定 某一计算机中的某一个进程 端口范围0-65535,0-1023为系统占用端口 传输层功能:建立端口到端口的通信        TCP 与 UDP TCP:传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、 基于字节流的传输层通信协议. TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间 提供一条具有下列特点的通信方式: (1)基于流的方式; (2)面向连接; (3)可靠通信方式; (4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销; (5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。 为满足TCP协议的这些特点,TCP协议做了如下的规定: ①数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组; ②到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认; ③超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片; ④滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据, TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出; ⑤失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序, 将收到的数据以正确的顺序交给应用层; ⑥重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据; ⑦数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。 如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。 其中最著名的是其3次握手建链接,4次挥手断链接. 而TCP的稳定性并不来源于其双向通道,而是来自于TCP消息反馈机制,即:不论是接收还是发送消息都要得到目标的回应后, 才终止当前工作,而不是发送数据后不管不顾.

            TCP协议要求在发送数据后,必须接收到对方的回复信息才能确认数据成功发送,如果一段时内没有收到回复信息,会自动重新发送,如果重试的次数过多则表示链接可能已经中断!


        

      

                 

 

                              

总结

其优点很明显:能够保证数据传输是完整的

缺点:由于每次都需要传输确认信息,导致传输效率降低

场景:多用于必须保证数据完整性的场景,例如文本信息,支付信息等!

 

      

                          

  UDP  

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

忧点:由于不需要传输确认信息,所以传输效率高于TCP协议

缺点:传输数据可能不完整

场景:视频聊天,语音聊天等,不要求数据完整性,但是对传输速度要求较高

应用层

应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开放的,大家都可以开发自己的应用程序,用什么样的数据格式来传输,就需要由应用程序开发者自己来制定

应用层功能:规定应用程序的数据格式。

例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。

至此一连串高低电压就通过层层协议,变成了我们在应用程序中看到的各种数据

套接字(socket):

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

基于文件类型的套接字家族

套接字家族的名字:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

基于网络类型的套接字家族

套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

TCP 的套接字使用

server端

 
 
 
import socket
sk = socket.socket()
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()        #关闭服务器套接字(可选)
 
 
 

client端

 
 
 
import socket
sk = socket.socket()           # 创建客户套接字
sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024)         # 对话(发送/接收)
print(ret)
sk.close()            # 关闭客户套接字
 
 
 

 UDP的套接字使用:

udp是无链接的,启动服务之后可以直接接受消息不需要提前建立链接

简单使用

server端

 
 
 
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
udp_sk.close()                         # 关闭服务器套接字
 
 
 

client端

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

socket 的四个 参数:

family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

注意 :尽管TCP很安全 但是 随之而来的粘包 问题也需要解决:

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

 

其中

官方文档对socket模块下的socket.send()和socket.sendall()解释如下:

socket.send(string[, flags])
Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above.
Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。 – socket.sendall(string[, flags]) Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(),
this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error,
an exception is raised, and there is no way to determine how much data, if any, was successfully sent. 尝试发送string的所有数据,成功则返回None,失败则抛出异常。 故,下面两段代码是等价的: #sock.sendall('Hello world ') #buffer = 'Hello world ' #while buffer: # bytes = sock.send(buffer) # buffer = buffer[bytes:]


TCP并发编程:示例:
TCP 从原理上流式协议,不能采用并发编程,但是可以借助模块,采用标记的方法,实现并发编程.



服务端

import socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 9999

    # 设置allow_reuse_address允许服务器重用地址
    socketserver.TCPServer.allow_reuse_address = True
    # 创建一个server, 将服务地址绑定到127.0.0.1:9999
    server = socketserver.TCPServer((HOST, PORT),Myserver)
    # 让server永远运行下去,除非强制停止程序
    server.serve_forever()
 客户端
import socket

HOST, PORT = "127.0.0.1", 9999
data = "hello"

# 创建一个socket链接,SOCK_STREAM代表使用TCP协议
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))          # 链接到客户端
    sock.sendall(bytes(data + "
", "utf-8")) # 向服务端发送数据
    received = str(sock.recv(1024), "utf-8")# 从服务端接收数据

print("Sent:     {}".format(data))
print("Received: {}".format(received))

粘包的原因:

    tcp协议的拆包机制


当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。

                   大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

面向流的通信特点和Nagle算法

 
 
 
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

 解决方案:

  借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。

  即通过制作报头来传输数据长度信息,方便接收端的解析工作,即默认长度采取事先约定的格式,进行解析.

  在此之上利用这个特性:我们也可以制作传输数据的字典 ,然后以字典长度为报头,即:先传输字典长度报头,使接收端收到字典,然后接收端从字典中读取数据:长度,名字,类型等关键信息,再按长度取值,得到原始数据.

   代码示例如下:

服务端:

import socket
import subprocess
import struct
import json


server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)


while True:
    conn, addr = server.accept()
    while True:
        try:
            cmd = conn.recv(1024)
            if len(cmd) == 0:break
            cmd = cmd.decode('utf-8')
            obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            res = obj.stdout.read() + obj.stderr.read()
            d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'}
            json_d = json.dumps(d)
            # 1.先制作一个字典的报头
            header = struct.pack('i',len(json_d))
            # 2.发送字典报头
            conn.send(header)
            # 3.发送字典
            conn.send(json_d.encode('utf-8'))
            # 4.再发真实数据
            conn.send(res)
            # conn.send(obj.stdout.read())
            # conn.send(obj.stderr.read())
        except ConnectionResetError:
            break
    conn.close()

 客户端:

import socket
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1',8080))

while True:
    msg = input('>>>:').encode('utf-8')
    if len(msg) == 0:continue
    client.send(msg)
    # 1.先接受字典报头
    header_dict = client.recv(4)
    # 2.解析报头 获取字典的长度
    dict_size = struct.unpack('i',header_dict)[0]  # 解包的时候一定要加上索引0
    # 3.接收字典数据
    dict_bytes = client.recv(dict_size)
    dict_json = json.loads(dict_bytes.decode('utf-8'))
    # 4.从字典中获取信息
    print(dict_json)
    recv_size = 0
    real_data = b''
    while recv_size < dict_json.get('file_size'):  # real_size = 102400
        data = client.recv(1024)
        real_data += data
        recv_size += len(data)
    print(real_data.decode('gbk'))

待补充:()

原文地址:https://www.cnblogs.com/Sunbreaker/p/11318288.html