python之网络编程
1.网络通信
简单来说,网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。
使用网络的目的,就是为了联通多方然后进行通信,即把数据从一方传递给另外一方。
前面的学习编写的程序都是单机的,即不能和其他电脑上的程序进行通信。为了让在不同的电脑上运行的软件,之间能够互相传递数据,就需要借助网络的功能。
- 使用网络能够把多方链接在一起,然后可以进行数据传递
- 所谓的网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信
2.ip地址
生活中的地址指的就是,找到某人或某机关或与其通信的指定地点。在网络编程中,如果一台主机想和另一台主机进行沟通和共享数据,首先要做的第一件事情就是要找到对方。在互联网通信中,我们使用IP地址来查询到各个主机。
ip地址:用来在网络中标记一台电脑,比如192.168.1.1;在本地局域网上是唯一的。
2.1ip地址的分类
每一个IP地址包括两部分:网络地址和主机地址,网络地址表示其属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机。IP地址通常由点分十进制(例如:192.168.1.1)的方式来表示,IP地址要和子网掩码(用来区分网络位和主机位)配合使用。
- A类地址
- 一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”,
- 地址范围:1.0.0.1-126.255.255.254
- 子网掩码:255.0.0.0
- 二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
- 可用的A类网络有126个,每个网络能容纳1677214个主机
- B类地址
- 一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,
- 地址范围:128.1.0.1-191.255.255.254
- 子网掩码:255.255.0.0
- 二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
- 可用的B类网络有16384个,每个网络支持的最大主机数为256的2次方-2=65534台。
- C类地址
- 一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”
- 范围:192.0.1.1-223.255.255.254
- 子网掩码:255.255.255.0
- 二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111111 11111110
- C类网络可达2097152个,每个网络支持的最大主机数为256-2=254台
- D类地址
- D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址,并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。
- E类地址
- 以“1111”开始,为将来使用保留,仅作实验和开发用。
- 私有地址
- 在这么多网络IP中,国际规定有一部分IP地址是用于我们的局域网使用,也就是属于私网IP,不在公网中使用的,它们的范围是:
- 10.0.0.0~10.255.255.255
- 172.16.0.0~172.31.255.255
- 192.168.0.0~192.168.255.255
- 注意事项:
- 每一个字节都为0的地址(“0.0.0.0”)对应于当前主机。
- IP地址中的每一个字节都为1的IP地址(“255.255.255.255”)是当前子网的广播地址。
- IP地址中凡是以“1111”开头的E类IP地址都保留用于将来和实验使用。
- IP地址中不能以十进制“127”作为开头,该类地址中数字127.0.0.1到127.255.255.255用于回路测试,如:127.0.0.1可以代表本机IP地址,用 http://127.0.0.1 就可以测试本机中配置的Web服务器。
- 网络ID的第一个8位组也不能全置为“0”,全“0”表示本地网络。
3.网络通信原理
DHCP:用来分配IP
在网络里都是通过IP地址查找的主机
DNS:域名 IP地址
4.网络通信方式
4.1直接通信
说明:
- 如果两台电脑之间通过网线连接是可以直接通信的,但是需要提前设置好ip地址以及网络掩码
- 并且ip地址需要控制在同一网段内,例如 一台为
192.168.1.1
另一台为192.168.1.2
则可以进行通信
4.2使用集线器通信
说明:
- 当有多台电脑需要组成一个网时,那么可以通过集线器(Hub)将其链接在一起
- 一般情况下集线器的接口较少
- 集线器有个缺点,它以广播的方式进行发送任何数据,即如果集线器接收到来自A电脑的数据本来是想转发给B电脑,如果此时它还连接着另外两台电脑C、D,那么它会把这个数据给每个电脑都发送一份,因此会导致网络拥堵
4.3使用交换机通信
说明:
- 克服了集线器以广播发送数据的缺点,当需要广播的时候发送广播,当需要单播的时候又能够以单播的方式进行发送
- 它已经替代了之前的集线器
- 企业中就是用交换机来完成多台电脑设备的链接成网络的
4.4使用路由器连接多个网络
4.5复杂的通信过程
说明:
- 在浏览器中输入一个网址时,需要将它先解析出ip地址来
- 当得到ip地址之后,浏览器以tcp的方式3次握手链接服务器
- 以tcp的方式发送http协议的请求数据给服务器
- 服务器tcp的方式回应http协议的应答数据给浏览器
总结
- MAC地址:在设备与设备之间数据通信时用来标记收发双方(网卡的序列号)
- IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)
- 网络掩码:用来区分ip地址的网络号和主机号
- 默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,称为网关
- 集线器:已过时,用来连接多态电脑,缺点:每次收发数据都进行广播,网络会变的拥堵
- 交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播
- 路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化
- DNS:用来解析出IP(类似电话簿)
- http服务器:提供浏览器能够访问到的数据
5.端口号和套接字
5.1端口
端口就像一个房子的门,是出入这间房子的必经之路。如果一个程序需要收发网络数据,那么就需要有这样的端口
在linux系统中,端口可以有65536(2的16次方)个之多!
既然有这么多,操作系统为了统一管理,所以进行了编号,这就是端口号
5.2端口号
端口是通过端口号来标记的,端口号只有整数,范围是从0到65535.端口号不是随意使用的,而是按照一定的规定进行分配。端口的分类标准有好几种,我们这里不做详细讲解,只介绍一下知名端口和动态端口。
5.3知名端口号
知名端口是众所周知的端口号,范围从0到1023,一些常用的功能使用的号码是固定的,好比电话号码110、10086、10010一样。一般情况下,如果一个程序需要使用知名端口号需要有root权限。
5.4动态端口号
动态端口的范围是从1024到65535
之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。
动态分配是指当一个系统程序或应用程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。
当这个程序关闭时,同时也就释放了所占用的端口号。
5.5端口号作用
我们知道,一台拥有IP地址的主机可以提供许多服务,比如HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。 需要注意的是,端口并不是一一对应的。比如你的电脑作为客户机访问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。
6.socket简介
1.不同电脑上的进程之间如何通信
首要解决的问题是如何唯一标识一个进程,否则通信无从谈起! 在1台电脑上可以通过进程号(PID)来唯一标识一个进程,但是在网络中这是行不通的。 其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用进程(进程)。 这样利用ip地址,协议,端口就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
注意:
- 所谓进程指的是:运行的程序以及运行时用到的资源这个整体称之为进程(在讲解多任务编程时进行详细讲解)
- 所谓进程间通信指的是:运行的程序之间的数据共享
6.1什么是socket
socket(简称套接字) 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:
它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的
例如我们每天浏览网页、QQ 聊天、收发 email 等等。
6.2创建socket
在 Python 中 使用socket 模块的函数 socket 就可以完成:
import socket
socket.socket(AddressFamily, Type)
说明:
函数 socket.socket 创建一个 socket,该函数带有两个参数:
- Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET
- Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)
创建一个tcp socket(tcp套接字)
import socket # 创建tcp的套接字 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # ...这里是使用套接字的功能(省略)... # 不用的时候,关闭套接字 s.close()
创建一个udp socket(udp套接字)
import socket # 创建udp的套接字 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # ...这里是使用套接字的功能(省略)... # 不用的时候,关闭套接字 s.close()
说明:
套接字使用流程与文件的使用流程很类似
- 创建套接字
- 使用套接字收/发数据
- 关闭套接字
7.udp网络程序
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中,"写信"。
不同电脑之间的通信需要使用socket,socket可以在不同的电脑间通信;还可以在同一个电脑的不同程序之间通信。
7.1Udp发送数据
创建一个基于udp的网络程序流程很简单,具体步骤如下:
- 创建客户端套接字
- 发送/接收数据
- 关闭套接字
import socket # 1. 创建socket,并连接 # AF_INET:表示这个socket是用来进行网络连接 # SOCK_DGRAM:表示连接是一个 udp 连接 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 2. 发送数据 # data:要发送的数据,它是二进制的数据 # address:发送给谁,参数是一个元组,元组里有两个元素 # 第0个表示目标的ip地址,第1个表示程序的端口号 # 给 192.168.0.101 这台主机的 9000 端口上发送了 下午好 # 端口号:0~65536 0~1024 不要用,系统一些重要的服务在使用 # 找一个空闲的端口号 s.sendto('下午好'.encode('utf8'), ('192.168.0.101', 9090)) # 3. 关闭socket s.close()
7.2Udp发送数据
import socket # 创建一个基于 udp 的网络socket连接 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定端口号和ip地址 s.bind(('192.168.0.101', 9090)) # recvfrom 接收数据 # content = s.recvfrom(1024) # print(content) # 接收到的数据是一个元组,元组里有两个元素 # 第 0 个元素是接收到的数据,第 1 个元素是发送方的 ip地址和端口号 data, addr = s.recvfrom(1024) # recvfrom是一个阻塞的方法,等待 print('从{}地址{}端口号接收到了消息,内容是:{}'.format(addr[0], addr[1], data.decode('utf8'))) s.close()
8.TCP协议
TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话"。
8.1TCP特点
- 面向连接
- 通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。
- 双方间的数据传输都可以通过这一个连接进行。
- 完成数据交换后,双方必须断开此连接,以释放系统资源。
- 这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。
- 可靠传输
- 1)TCP采用发送应答机制
TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
-
- 2)超时重传
发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。
TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
-
- 3)错误校验
TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
-
- 4) 流量控制和阻塞管理
流量控制用来避免主机发送得过快而使接收方来不及完全收下。
- TCP与UDP的区别
- 面向连接(确认有创建三方交握,连接已创建才作传输。)
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞/流量控制
8.2TCP通信模型
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据。
8.3服务器和客户端
服务器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。 客户端(Client),与服务器相对应,为客户提供本地服务的程序。 客户端服务器架构又被称为主从式架构,简称C/S结构,是一种网络架构,它把客户端与服务器分开来,一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。
8.3.1TCP客户端
相比较于TCP服务端,tcp的客户端要简单很多,如果说服务器端是需要自己买手机、查手机卡、设置铃声、等待别人打电话流程的话,那么客户端就只需要找一个电话亭,拿起电话拨打即可,流程要少很多。
import socket # 基于tcp协议的socket连接 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 在发送数据之前,必须要先和服务器建立连接 s.connect(('192.168.31.199', 9090)) # 调用connect 方法连接到服务器 s.send('hello'.encode('utf8')) # udp 直接使用sendto发送数据 # s.sendto('hello'.encode('utf8'),('192.168.31.199',9090)) s.close()
8.3.2TCP服务端
在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:
- socket创建一个套接字
- bind绑定ip和port
- listen使套接字变为可以被动链接
- accept等待客户端的链接
- recv/send接收发送数据
import socket # 创建一个socket连接 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('192.168.31.199', 9090)) s.listen(128) # 把socket变成一个被动监听的socket,backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求。 # ( # <socket.socket fd=512, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.31.199', 9090), raddr=('192.168.31.185', 38096)>, # ('192.168.31.185', 38096) # ) # 接收到的结果是一个元组,元组里有两个元素 # 第 0 个元素是客户端的socket连接,第 1 个元素是客户端的 ip 地址和端口号 # x = s.accept() # 接收客户端的请求 client_socket, client_addr = s.accept() # udp里接收数据,使用的recvfrom data = client_socket.recv(1024) # tcp里使用recv获取数据 print('接收到了{}客户端{}端口号发送的数据,内容是:{}'.format(client_addr[0], client_addr[1], data.decode('utf8')))
8.4TCP注意事项
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
- tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
- tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
- 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
9.文件下载案例
TCP服务器端:
import socket, os server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('192.168.31.199', 9090)) server_socket.listen(128) # 接收客户端的请求 client_socket, client_addr = server_socket.accept() file_name = client_socket.recv(1024).decode('utf8') # print('接收到了来自{}地址{}端口的数据,内容是:{}'.format(client_addr[0], client_addr[1], data)) if os.path.isfile(file_name): # print('读取文件,返回给客户端') with open(file_name, 'rb') as file: content = file.read() client_socket.send(content) else: print('文件不存在')
TCP客户端:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('192.168.31.199', 9090)) # s.send('hello'.encode('utf8')) file_name = input('清输入您要下载的文件名:') s.send(file_name.encode('utf8')) with open(file_name, 'wb') as file: while True: content = s.recv(1024) if not content: break file.write(content) s.close()