Python 网络编程

网络基础简介

一、软件开发架构

​ 在日常生活中,我们会使用软件进行聊天,视频,收发文件,搜索浏览等等,其本质都是应用程序间进行通信。我们平时涉及到的两个程序之间通讯的应用大致可以分为两种:

第一是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用;

第二是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用;

这两个分类分别对应了两个软件开发的架构:

1.1、C/S 架构

​ C/S 即:Client与Server ,中文意思:客户端与服务器端架构,这种架构是从用户层面(也可以是物理层面)来划分的。这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

​ C/S架构的软件可以充分发挥PC机的性能,但是如果软件需要更新或者维护,需要用户重新下载安装相关的补丁,随着功能的叠加,软件本身也会越来越大。

1.2、B/S 架构

​ B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构也是从用户层面来划分的。Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。严格意义上来说,B/S架构也是隶属于C/S架构的。

​ B/S架构的软件的好处就是统一了应用的接口。服务端进行功能的更新或者维护,是不需要用户后期去参与的。用户始终只需要通过浏览器去使用相关的功能即可。

二、网络通信

2.1、相关知识简介

2.1.1、mac地址

​ mac地址是一个物理地址,ethernet(以太网)规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址。每块网卡出厂时都被烧制上一个世界唯一的mac地址(和身份证号一样),长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号),在 windows 系统中,在控制台输入 “ipconfig -all” 命令可以查看本机的mac地址。

2.1.2、ip地址与ip协议

​ 虽然有了mac地址,计算机之间可以互相识别,但是计算机之间的通信并不是基于mac地址来的,因为只有mac地址是无法确定计算机的实际位置的,需要ip地址的协助。打个比方,mac地址好比是身份证号码,ip地址好比就是你当前住的地方的地址。你网购买了一件商品,商家需要知道你的当前的住址才能快递给你。如果只有一个身份证号,只能查出你的籍贯地址,但是可能与你当前在的位置不相符(比如你是江苏人,现在在上海工作),这样快递就没法准确的寄给你了。

ip地址和ip协议有以下特点:

  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • ip协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
  • ip地址是一个逻辑地址
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1

2.1.3、端口

​ 我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。那么问题来了,主机是怎样区分不同的网络服务呢?举个例子,当你和你的朋友聊QQ的时候,你把一条信息发送给对方,为什么那条消息能显示到对方电脑上的QQ对话框,而不是显示到微信的对话框??显然不只有一个ip地址是不行的,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。ip找到对应主机,端口找到主机中某一个服务。

2.1.4、arp协议

​ 地址解析协议,即ARP(Address Resolution Protocol),是根据目标IP地址获取目标物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间(一般是十分钟,十分钟后会清除所有记录),下次请求时直接查询ARP缓存以节约资源。

​ 地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。

2.1.5、局域网

​ 局域网(Local Area Network,LAN)是指在某一区域内由多台计算机互联成的计算机组。一般是方圆几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、工作组内的日程安排、电子邮件和传真通信服务等功能。局域网是封闭型的,可以由办公室内的两台计算机组成,也可以由一个公司内的上千台计算机组成。

2.1.6、网段

​ 一个局域网内的ip地址范围。可以通过子网掩码和ip地址计算得出。

2.1.7、子网掩码

​ 所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

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

1
2
3
4
5
6
7
8
9
10
11
比如,已知IP地址172.16.10.1172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,

172.16.10.110101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0


172.16.10.210101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
结果都是172.16.10.0,因此它们在同一个子网络。

2.1.8、交换机

​ 交换机(Switch)意为“开关”是一种用于电(光)信号转发的网络设备。它可以为接入交换机的任意两个网络节点提供独享的电信号通路。最常见的交换机是以太网交换机。其他常见的还有电话语音交换机、光纤交换机等。

​ 交换是按照通信两端传输信息的需要,用人工或设备自动完成的方法,把要传输的信息送到符合要求的相应路由上的技术的统称。交换机根据工作位置的不同,可以分为广域网交换机和局域网交换机。广域的交换机就是一种在通信系统中完成信息交换功能的设备,它应用在数据链路层。交换机有多个端口,每个端口都具有桥接功能,可以连接一个局域网或一台高性能服务器或工作站。实际上,交换机有时被称为多端口网桥。

2.1.9、网关

​ 大家都知道,从一个房间走到另一个房间,必然要经过一扇门。同样,从一个网络向另一个网络发送信息,也必须经过一道“关口”,这道关口就是网关。网关(Gateway)就是一个网络连接到另一个网络的“关口”。也就是网络关卡。

​ 网关(Gateway)又称网间连接器、协议转换器。默认网关在网络层上以实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连。网关的结构也和路由器类似,不同的是互连层。网关既可以用于广域网互连,也可以用于局域网互连 。

​ 网关实质上是一个网络通向其他网络的IP地址。比如有网络A和网络B,网络A的IP地址范围为“192.168.1.1192. 168.1.254”,子网掩码为255.255.255.0;网络B的IP地址范围为“192.168.2.1192.168.2.254”,子网掩码为255.255.255.0。在没有路由器的情况下,两个网络之间是不能进行TCP/IP通信的,即使是两个网络连接在同一台交换机(或集线器)上,TCP/IP协议也会根据子网掩码(255.255.255.0)判定两个网络中的主机处在不同的网络里。而要实现这两个网络之间的通信,则必须通过网关。如果网络A中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络B的网关,网络B的网关再转发给网络B的某个主机(如附图所示)。网络A向网络B转发数据包的过程。

2.1.10、路由器

​ 路由器(Router),是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号。 路由器是互联网络的枢纽,”交通警察”。目前路由器已经广泛应用于各行各业,各种不同档次的产品已成为实现各种骨干网内部连接、骨干网间互联和骨干网与互联网互联互通业务的主力军。路由和交换机之间的主要区别就是交换机发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换机在移动信息的过程中需使用不同的控制信息,所以说两者实现各自功能的方式是不同的。

  路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开的网络,所谓逻辑网络是代表一个单独的网络或者一个子网。当数据从一个子网传输到另一个子网时,可通过路由器的路由功能来完成。因此,路由器具有判断网络地址和选择IP路径的功能,它能在多网络互联环境中,建立灵活的连接,可用完全不同的数据分组和介质访问方法连接各种子网,路由器只接受源站或其他路由器的信息,属网络层的一种互联设备。

路由器和交换机的区别:

  • 交换机的主要功能是组织局域网,经过交换机内部处理解析信息之后,将信息以点对点、点多对的形式,发送给固定端

  • 路由器的主要功能: 进行跨网段进行数据传输,路由选择最佳路径。

  • 如果你需要将多台电脑连接到一根网线,用交换机即可;如果你只有一个外网ip,但是你有好多台电脑需要上网,用路由器即可。

2.2、通信方式

  • 点对点通信:使用一根网线就够了
  • **多个计算机通信:**使用交换机
  • **更多计算机之间的通信:**使用路由器和交换机

补充:

​ 交换机的通讯方式主要有广播、单播和组播的方式,单播(固定的一对一)和组播(固定的一对多)比较容易理解,下面就广播的通讯方式来举例介绍一下:

​ 电脑1(源)要找电脑2(目标),电脑1首先发送一个请求帧,期中包含(我的ip是192.168.1.1,我的mac地址是xxxxxxxx,我要找ip地址为192.168.1.2的主机),将此请求发送给交换机。 交换机要广播这条消息给其他所有的主机。目标主机接收到消息后,对比发现自己就是被找的主机,回复给交换机信息(我的ip地址是192.168.1.2,我的mac地址是yyyyyyyyy,请回复给ip地址为192.168.1.1,mac地址为xxxxxxx的主机),最后交换机以单播形式返回给源主机。

2.3、应用程序之间的通信协议

如果说ip地址和mac地址帮我们确定唯一的一台机器,那么应用程序就可以通过应用程序之间的通信协议来确定对应的目标计算机上的目标程序。

2.3.1、TCP/IP 协议

​ 当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。全双工表示客户端和服务器均可自由的收发消息,不用在意先后顺序。这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。

  • TCP协议是一种可靠的,面向连接的,面向字节流形式的传输方式。
  • TCP协议有三次握手(建立连接时)和四次挥手机制(断开连接时)
1
2
3
4
5
6
7
# 三次握手
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1]
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 四次挥手
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。

2.3.2、UDP协议

当应用程序希望通过UDP与一个应用程序通信时,传输数据之前源端和终端不建立连接。当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。

  • UDP协议时不可靠的,不面向连接的,面向数据报的传输方式,但是它速度快。

2.3.3、TCP和UDP比较

​ TCP(Transmission Control Protocol)传输控制协议,提供的是面向连接、传输效率低、全双工通信(发送缓存&接收缓存)、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
​ UDP(User Datagram Protocol)用户数据报协议,是一个简单的面向数据报、无连接的服务,它传输效率高(发送前时延小),是可以一对一、一对多、多对一、多对多的面向报文、尽最大努力服务、无拥塞控制的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。使用UDP的应用:域名系统 (DNS);视频流;IP语音。

2.4、互联网协议和IOS模型

​ 一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的),如果你要跟别人一起玩,那你就需要上网了。互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

人们按照分工不同把互联网协议从逻辑上划分了层级:

我们在计算机A上通过QQ,给计算机B发了一个消息,B接收到消息并在自己的屏幕上显示的整个过程,究竟经历了哪些环节呢?

  • 首先,用户在计算机A上输入的QQ消息,是在应用层的,先按照文本或文件传输协议(http/https/ftp)对要发送的数据进行第一层的封装(加上请求头和请求方式之类),将封装好的数据传入传输层;
  • 数据到达传输层后,再在应用层封装完的数据的基础上按照TCP/UDP协议格式再次进行封装(添加报文头之类的),将封装好的数据传入网络层;
  • 数据到达网络层后,再在传输层封装完的数据的基础上按照IP协议格式再次进行封装,将封装好的数据传入数据链路层;
  • 数据到达数据链路层后,再在网络层封装完的数据的基础上按照ARP协议格式再次进行封装,将封装好的数据传入物理层;
  • 数据到达物理层后,会转换成电信号,通过网线、光纤等设备传给计算机B;
  • 电信号到达计算机B后,会分别经过 物理层 —> 数据链路层 —> 网络层 —> 传输层 —> 应用层 并按照每一层对应的协议进行反解,恢复成最开始的消息。

socket套接字

一、简介

​ Socket是应用层与TCP/IP协议族通信(运输层)的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

​ 其实站在我们的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。

二、socket中tcp/udp通信流程

三、python中的socket模块

1
sk = socket.socket(family = AF_INET, type=SOCK_STREAM)
1
2
3
4
5
6
7
8
# 参数
# 1. family :
# AF_UNIX 基于文件类型的套接字(早期socket是源自于unix系统而研发的一个功能,主要是为了同一台电脑上,多个程序直接通信) unix系统的中心思想是 : 一切皆文件
# AF_INET 基于网络类型的套接字
# 2.type:
# 基于TCP协议 SOCK_STREAM
# 基于UDP协议 SOCK_DGRAM

四、基于TCP的套接字编程

4.1 客户端和服务端的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测试连接  server端
import socket

sk = socket.socket() # 不传参数,默认使用基于网络类型的套接字, 协议 : TCP
sk.bind(("127.0.0.1", 8080)) # 端口的范围是0-65535 但是 0-1023 这些最好别用
sk.listen(5) # 表示最多可以监听5个客户端的连接

print(123)
conn, addr = sk.accept() # 等待接受客户端的连接 没有客户端来连接,代码就阻塞在这里
print(456)
print(conn) # <socket.socket fd=440, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 59832)>
print(addr) # ('127.0.0.1', 59832)

conn.close() # 关闭和客户端的连接
sk.close() # 关闭自己的连接端口
1
2
3
4
5
6
7
8
# 测试连接  client端
import socket
import time

sk = socket.socket()
sk.connect(("127.0.0.1", 8080))
time.sleep(10)
sk.close()

注意:

  • 127.0.0.1 是一个回环地址,每个计算机都有的这么一个本机地址,只能被本机识别,不会被其它机器识别。
  • tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端。
  • 如果遇到端口被占用的错误,可以进行如下配置。
1
2
3
4
5
6
7
#加入一条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))
... ...

4.2 客户端和服务端通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# server端
import socket

sk = socket.socket() # 不传参数,默认使用基于网络类型的套接字, 协议 : TCP
sk.bind(("127.0.0.1", 8080)) # 端口的范围是0-65535 但是 0-1023 这些最好别用
sk.listen(5) # 表示最多可以监听5个客户端的连接

conn, addr = sk.accept() # 等待接受客户端的连接 没有客户端来连接,代码就阻塞在这里

conn.send("hello".encode("utf-8")) # tcp 是基于字节流的传输,所以发送的内容要先编码
msg = conn.recv(1024).decode("utf-8") # 每次接收1024字节大小的数据,对接收的内容进行解码
print(msg)

conn.close() # 关闭和客户端的连接
sk.close() # 关闭自己的连接端口
1
2
3
4
5
6
7
8
9
10
11
12
13
# client 端
import socket

sk = socket.socket()
sk.connect(("127.0.0.1", 8080))

# 由于服务器是先发的数据,所以客户端必须先接收数据
info = sk.recv(1024).decode("utf-8")
print(info)

sk.send("你好".encode("utf-8"))

sk.close()

**注:**由于TCP是基于连接的,所以无论是客户端还是服务器都可以收发数据,没有顺序先后之分(全双工),但是如果某一端是发送的数据,那么另一端必须接收数据(不能同时都是发数据或者接收数据,一定是一发一收。)

客户端和服务端通信升级版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 持续通信 server
import socket

sk = socket.socket() # 不传参数,默认使用基于网络类型的套接字, 协议 : TCP
sk.bind(("127.0.0.1", 8080)) # 端口的范围是0-65535 但是 0-1023 这些最好别用
sk.listen(5) # 表示最多可以监听5个客户端的连接

while 1:
conn, addr = sk.accept() # 等待接受客户端的连接 没有客户端来连接,代码就阻塞在这里

while 1:
msg_r = conn.recv(1024).decode("utf-8") # 每次接收1024字节大小的数据,对接收的内容进行解码
print(msg_r)
if msg_r == "q":
break

msg_s = input(">>>")
conn.send(msg_s.encode("utf-8")) # tcp 是基于字节流的传输,所以发送的内容要先编码
if msg_s == "q":
break

conn.close() # 关闭和当前客户端的连接,此时其他客户端还是可以连接的
sk.close() # 关闭自己的连接端口,这时所有客户端都无法连接了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 持续通信 client
import socket

sk = socket.socket()
sk.connect(("127.0.0.1", 8080))

while 1:

msg_s = input(">>>")
sk.send(msg_s.encode("utf-8"))
if msg_s == "q":
break

msg_r = sk.recv(1024).decode("utf-8")
print(msg_r)
if msg_r == "q":
break

sk.close()

4.3 客户端与多个服务器之间通信

​ 还是使用上述代码,当开启多个客户端,都给服务器发送消息时,你会发现服务器可以和所有客户端建立连接,但是永远只能和最先建立连接的客户端进行通信;对于其他客户端,只有将当前与服务器连接的客户端断开,他们才能开始和服务器进行通信。这就是TCP协议通信的弊端

基于UDP的套接字编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# server 端
import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # 基于udp协议
sk.bind(("127.0.0.1", 8080))

while 1:
msg_r, addr = sk.recvfrom(1024) # 从哪里接收消息
print(addr)
print(msg_r.decode("utf-8"))
msg_s = input(">>>")
sk.sendto(msg_s.encode("utf-8"), addr) # 给谁发消息

sk.close()
1
2
3
4
5
6
7
8
9
10
11
12
# client 端
import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

while 1:
msg_s = input(">>>")
sk.sendto(msg_s.encode('utf-8'), ("127.0.0.1", 8080)) # 给这个地址发消息
msg_r, addr = sk.recvfrom(1024)
print(msg_r.decode("utf-8"))
print(addr)
sk.close()

注意:

  • 由于udp不是面向连接通信的,启动服务之后可以直接接受消息,不需要提前建立链接,因此不必先启动客户端
  • udp通信必须先有客户端发出消息,否则服务器无法知道客户端的地址,就没有办法返回消息了

客户端与多个服务器之间通信

​ 还是使用上述代码,当开启多个客户端,都给服务器发送消息时,服务器都可以接收到。

粘包问题

一、粘包问题简介

粘包问题时指,发送端发送数据,接收端不知道应该如何去接收而造成的一种数据混乱的现象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# server 端
conn.send(b"hello")
conn.send(b"world")

# client 端
print(sk.recv(1024))
print(sk.recv(1024))

# 结果
b"helloworld"
b""

# 显然正确的结果应该是
b"hello"
b"world"

需要强调的是,udp协议通信是不会发生粘包现象的,该现象只会在tcp中发生。这是由于tcp中有合包和拆包的机制。

1、tcp的合包机制(Nagle算法)

​ 如图所示,在服务器发送数据时,由于采用了Nagle算法来优化性能,服务器端会将多次连续发送且间隔较小的数据,进行打包成一块数据传送。当数据发送到客户端时,客户端就会一次接收所有的数据 ,从而导致数据粘包。

2、拆包机制

​ 如图所示,在发送端,因为受到网卡的MTU限制,会将大的超过MTU限制的数据拆分成多个小的数据进行传输。 当传输到目标主机的操作系统层时,会重新将多个小的数据合并成原本的数据。

​ 比如发送端传输一个1500大小的文件,由于MTU限制,会拆分成3个500大小数据再传输。由于网络问题,小数据块的传输可能不会按照顺序,但是都会停留在接收端的缓冲区,等待所有的小数据块都到了之后再被接收端接收。

​ 在接收端,由于不知道目标文件的大小,每次都接收固定大小的内容(比如每次接收1000),直到所有的小数据块都被接收完。这样看似没有什么问题,但是如果和途中所示,发送端连续传了两个1500的文件,此时接收端在第二次接收1000大小的数据时,实际上分别包含了第一个文件的部分内容和第二个文件的部分内容,此时对数据进行解析的话,就会出现数据混乱的问题。

​ 而在udp中不会出现粘包,这是因为udp是不可靠的,它不能保证数据传输的完整性,它更注重于怎么快速的让接收端接收到数据,至于数据内容是什么,并不关心。那么针对使用udp协议发送数据,一次收发大小究竟多少才合适呢?

​ udp协议本层对一次收发数据大小的限制是:65535 - ip包头(20) - udp包头(8) = 65507;站在数据链路层,因为网卡的MTU一般被限制在了1500,所以对于数据链路层来说,一次收发数据的大小被限制在 1500 - ip包头(20) - udp包头(8) = 1472。因此,如果使用udp传输数据,如果数据大小 num > 65507 ,则会报错;如果 1472 < num < 65507,则数据会在数据链路层拆包,而udp本身就是不可靠协议,所以一旦拆包之后的多个小数据包在网络传输中,如果丢任何一个,那么此次数据传输失败。所以 num < 1472 是比较理想的状态。

二、解决粘包

我们可以先通知接收端要传输的数据的大小,这样接收端就可以提前规划接收的策略,避免粘包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 解决文件传输的粘包现象  client
import socket
import os
import json

sk = socket.socket()
sk.connect(("127.0.0.1", 8001))
menu = {"1": "upload", "2": "download"}
for k, v in menu.items():
print(k, v)
num = input("请输入功能选项:")
if num == "1":
dic = {"opt": menu.get(num), "filename": None, "filesize": None}
file_path = input("请输入一个绝对路径:") # 文件的绝对路径
filename = os.path.basename(file_path) # 文件名字
filesize = os.path.getsize(file_path) # 获取用户输入的路径中文件的大小

dic["filename"] = filename
dic["filesize"] = filesize
str_dic = json.dumps(dic)
sk.send(str_dic.encode("utf-8")) # 将被填充完成的字典先发送给服务器
sk.recv(1024) # 为什么要有一个recv?
# 因为上边send字典时,如果程序执行过快,可能会马上执行到下边的send(content)
# 此时有可能会发生粘包,所以在此中间加一个recv,为了避免粘包
with open(file_path, "rb") as f:
while filesize:
content = f.read(1024)
sk.send(content)
filesize -= len(content)

elif num == "2":
pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 解决文件传输的粘包现象  server
import socket
import json

sk = socket.socket()
sk.bind(("127.0.0.1", 8001))
sk.listen()
conn, addr = sk.accept()
str_dic = conn.recv(100).decode("utf-8")
conn.send(b'ok') # 防止粘包
dic = json.loads(str_dic)
if dic["opt"] == "upload":
filename = "1" + dic["filename"]
with open(filename, "ab") as f:
while dic['filesize']:
content = conn.recv(1024)
f.write(content)
dic['filesize'] -= len(content)

elif dic["opt"] == "download":
pass

conn.close()
sk.close()

​ 上述思路是先将要传输的文件的信息封装在一个字典中,将文件信息先发给接收端,这样接收端就可以提前知道文件的大小了。但是这个思路还存在一个问题,虽然接收端可以通过字典信息来得到文件的大小,但是前提是接收端必须先接收该字典,但是字典的长度接收端在一开始是不知道的,如果接收的阈值定的太小,很有可能会导致字典信息接收的不完整从而无法反序列化。

​ 按照这个思路,我们可以先发送一下这个字典的长度,但是字典的长度也是不固定的,接收端到底要接收多大呢?这时候我们就可以使用 python 中的一个第三方模块 struct 来解决该问题。struct模块可以将不同长度的数字都转换成一个标准大小的4字节数字。

1
2
3
4
5
6
7
import struct

a = 2140000000
pack_res = struct.pack("i", a) # 转换
print(pack_res) # b'\x00\xcf\x8d\x7f'
unpack_res = struct.unpack("i", pack_res) # 反转
print(unpack_res) # (2140000000,)

所以,我们可以使用struct模块对字典信息的长度进行一个转换,这样每次接收端都固定的接收前4个字节就可以知道字典的长度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 使用struct优化  client
import socket
import os
import json
import struct
sk = socket.socket()
sk.connect(("127.0.0.1",8001))
menu = {"1":"upload","2":"download"}
for k,v in menu.items():
print(k,v)
num = input("请输入功能选项:")
if num == "1":
dic = {"opt":menu.get(num),"filename":None,"filesize":None}
file_path = input("请输入一个绝对路径:")# 文件的绝对路径
# E:\Python S14\day32\实现大文件的传输\11.mp4
filename = os.path.basename(file_path)# 文件名字
filesize = os.path.getsize(file_path)# 获取用户输入的路径中文件的大小
dic["filename"] = filename
dic["filesize"] = filesize
str_dic = json.dumps(dic)
len_dic = len(str_dic)# 获取到字典的长度,是一个int类型的数据 46 146
b_len_dic = struct.pack('i',len_dic)# 用一个4bytes的数据表示字典的长度

sk.send(b_len_dic + str_dic.encode("utf-8"))# 将bytes类型的字典的长度 + bytes类型的字典的内容,一起发送给服务器

with open(file_path,"rb") as f:
while filesize:
content = f.read(1024)
sk.send(content)
filesize -= len(content)

elif num == "2":
pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 使用struct优化  server

import socket
import json
import struct
sk = socket.socket()
sk.bind(("127.0.0.1",8001))
sk.listen()
conn,addr = sk.accept()
b_len_dic = conn.recv(4)
len_dic = struct.unpack('i',b_len_dic)[0]# 获取到int类型字典的长度,
# unpack得到的是一个元组,要取下标为0的位置
str_dic = conn.recv(len_dic).decode('utf-8')
# str_dic = {"opt":menu.get(num),"filename":None,"filesize":None}
dic = json.loads(str_dic)
if dic["opt"] == "upload":
filename = "1"+ dic["filename"]
with open(filename,"ab") as f:
while dic['filesize']:
content = conn.recv(1024)
f.write(content)
dic['filesize'] -= len(content)

elif dic["opt"] == "download":
# 客户端发来一个字典要执行的功能,以及客户端自己的绝对路径
# 服务器要返回这个绝对路径中所有文件及文件夹
# 客户端自己选择进入到哪一层目录下
# 服务器都要返回对应目录下所有文件及文件夹
# 客户随时选择某一个目录下的某一个文件进行下载


# 客户端发送来一个字典,包含了要进行的操作,要下载的文件的绝对路径,
# 根据绝对路径去读取文件内容
# 一边读,一遍发
pass

conn.close()
sk.close()

Python 网络编程
https://clark-cdc.github.io/2019/06/03/0018-网络通信/
作者
clark
发布于
2019年6月3日
许可协议