【20天快速掌握Python】day15-网络编程

  • Post author:
  • Post category:python


1.网络通信的概念

简单来说,网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。

使用网络的目的,就是为了联通多方然后进行通信,即把数据从一方传递给另外一方。

前面的学习编写的程序都是单机的,即不能和其他电脑上的程序进行通信。为了让在不同的电脑上运行的软件,之间能够互相传递数据,就需要借助网络的功能。

  • 使用网络能够把多方链接在一起,然后可以进行数据传递

  • 所谓的网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信

2.IP地址

生活中的地址指的就是,找到某人或某机关或与其通信的指定地点。在网络编程中,如果一台主机想和另一台主机进行沟通和共享数据,首先要做的第一件事情就是要找到对方。在互联网通信中,我们使用IP地址来查询到各个主机。


ip地址:用来在网络中标记一台电脑,比如192.168.1.1;在本地局域网上是唯一的。


IP地址的分类

每一个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 11111110 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


注意事项:

.

  1. 每一个字节都为0的地址(“0.0.0.0”)对应于当前主机。

  2. IP地址中的每一个字节都为1的IP地址(“255.255.255.255”)是当前子网的广播地址。

  3. IP地址中凡是以“1111”开头的E类IP地址都保留用于将来和实验使用。

  4. IP地址中不能以十进制“127”作为开头,该类地址中数字127.0.0.1到127.255.255.255用于回路测试,如:127.0.0.1可以代表本机IP地址,用

    http://127.0.0.1

    就可以测试本机中配置的Web服务器

  5. 网络ID的第一个8位组也不能全置为“0”,全“0”表示本地网络。

3.网络通信方式

3.1 直接通信


说明

  1. 如果两台电脑之间通过网线连接是可以直接通信的,但是需要提前设置好ip地址以及网络掩码

  2. 并且ip地址需要控制在同一网段内,例如 一台为

    192.168.1.1

    另一台为

    192.168.1.2

    则可以进行通信

3.2 使用集线通信


说明

  1. 当有多态电脑需要组成一个网时,那么可以通过集线器(Hub)将其链接在一起

  2. 一般情况下集线器的接口较少

  3. 集线器有个缺点,它以广播的方式进行发送任何数据,即如果集线器接收到来自A电脑的数据本来是想转发给B电脑,如果此时它还连接着另外两台电脑C、D,那么它会把这个数据给每个电脑都发送一份,因此会导致网络拥堵

3.3 通用交换机通信


说明

  1. 克服了集线器以广播发送数据的缺点,当需要广播的时候发送广播,当需要单播的时候又能够以单播的方式进行发送

  2. 它已经替代了之前的集线器

  3. 企业中就是用交换机来完成多态电脑设备的链接成网络的

3.4 使用路由器连接多个网络

3.5 复杂的通信过程


说明

  1. 在浏览器中输入一个网址时,需要将它先解析出ip地址来

  2. 当得到ip地址之后,浏览器以tcp的方式3次握手链接服务器

  3. 以tcp的方式发送http协议的请求数据 给 服务器

  4. 服务器tcp的方式回应http协议的应答数据 给浏览器

总结

  • MAC地址:在设备与设备之间数据通信时用来标记收发双方(网卡的序列号)

  • IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)

  • 网络掩码:用来区分ip地址的网络号和主机号

  • 默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,成为网关

  • 集线器:已过时,用来连接多态电脑,缺点:每次收发数据都进行广播,网络会变的拥堵

  • 交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播

  • 路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化

  • DNS:用来解析出IP(类似电话簿)

  • http服务器:提供浏览器能够访问到的数据

4.端口

端口就像一个房子的门,是出入这间房子的必经之路。如果一个程序需要收发网络数据,那么就需要有这样的端口

在linux系统中,端口可以有65536(2的16次方)个之多!

既然有这么多,操作系统为了统一管理,所以进行了编号,这就是

端口号

4.1 端口号

端口是通过端口号来标记的,端口号只有整数,范围是从0到65535.端口号不是随意使用的,而是按照一定的规定进行分配。端口的分类标准有好几种,我们这里不做详细讲解,只介绍一下知名端口和动态端口。

4.2 知名端口号

知名端口是众所周知的端口号,范围从0到1023,以理解为,一些常用的功能使用的号码是估计的,好比 电话号码110、10086、10010一样。一般情况下,如果一个程序需要使用知名端口的需要有root权限。

4.3 动态端口号

动态端口的范围是从1024到65535

之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

动态分配是指当一个系统程序或应用程序程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。

当这个程序关闭时,同时也就释放了所占用的端口号。

4.4 端口号作用

我们知道,一台拥有IP地址的主机可以提供许多服务,比如HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。 需要注意的是,端口并不是一一对应的。比如你的电脑作为客户机访问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。

5.socker概念

5.1 不同电脑上的进程之间如何通信

首要解决的问题是如何唯一标识一个进程,否则通信无从谈起! 在1台电脑上可以通过进程号(PID)来唯一标识一个进程,但是在网络中这是行不通的。 其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用进程(进程)。 这样利用ip地址,协议,端口就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

注意:

所谓

进程

指的是:运行的程序以及运行时用到的资源这个整体称之为进程(在讲解多任务编程时进行详细讲解)

所谓

进程间通信

指的是:运行的程序之间的数据共享

5.2 什么是socket

socket(简称

套接字

) 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:

它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的

例如我们每天浏览网页、QQ 聊天、收发 email 等等。

5.3 创建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()


说明:

套接字使用流程 与 文件的使用流程很类似

  1. 创建套接字

  2. 使用套接字收/发数据

  3. 关闭套接字

6.UDP协议

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中,”写信”。


UPD通信模型

6.1 UDP发送接受数据


UDP网络程序-发送数据

创建一个基于udp的网络程序流程很简单,具体步骤如下:

  1. 创建客户端套接字

  2. 发送/接收数据

  3. 关闭套接字

 import socket
 # 1. 创建一个UDP的socket连接
 udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 # 2. 获取用户输入的内容
 data = input('请输入内容')
 # 3. 准备接收方的地址和端口号
 addr = ('127.0.0.1', 8080)
 # 4. 将用户的输入内容进行编码,并发送到指定地址和端口
 udp_socket.sendto(data.encode('gbk'), addr)
 # 5. 接收传递过来的消息,并指定接受的字节大小
 recv_data = udp_socket.recvfrom(1024)
 # 6. 接收到的对象是一个元组,元组里有两个元素
 print(recv_data)
 # 6.1 元组里的第一个数据显示接收到内容
 print(recv_data[0].decode('gbk'))
 # 6.2 元组里的第二个数据显示发送方的地址和端口号
 print(recv_data[1])
 # 7. 关闭socket连接
 udp_socket.close()

运行网络调试助手,查看运行效果:

6.2 UDP发送接受数据

6.2.1 UPD网络程序-端口问题

  • 会变的端口号

重新运行多次脚本,然后在“网络调试助手”中,看到的现象如下:

说明:

  • 每重新运行一次网络程序,上图中红圈中的数字,不一样的原因在于,这个数字标识这个网络程序,当重新运行时,如果没有确定到底用哪个,系统默认会随机分配

  • 记住一点:这个网络程序在运行的过程中,这个就唯一标识这个程序,所以如果其他电脑上的网络程序如果想要向此程序发送数据,那么就需要向这个数字(即端口)标识的程序发送即可

6.2.2 UDP绑定信息


1.绑定信息

一般情况下,在一台电脑上运行的网络程序有很多,为了不与其他的网络程序占用同一个端口号,往往在编程中,udp的端口号一般不绑定 但是如果需要做成一个服务器端的程序的话,是需要绑定的,想想看这又是为什么呢? 如果报警电话每天都在变,想必世界就会乱了,所以一般服务性的程序,往往需要一个固定的端口号,这就是所谓的端口绑定。


2.绑定示例

 from socket import *
 ​
 # 1. 创建套接字
 udp_socket = socket(AF_INET, SOCK_DGRAM)
 ​
 # 2. 绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
 local_addr = ('', 7788) #  ip地址和端口号,ip一般不用写,表示本机的任何一个ip
 udp_socket.bind(local_addr)
 ​
 # 3. 等待接收对方发送的数据
 recv_data = udp_socket.recvfrom(1024) #  1024表示本次接收的最大字节数
 ​
 # 4. 显示接收到的数据
 print(recv_data[0].decode('gbk'))
 ​
 # 5. 关闭套接字
 udp_socket.close()


运行结果:


3.总结

  • 一个udp网络程序,可以不绑定,此时操作系统会随机进行分配一个端口,如果重新运行此程序端口可能会发生变化

  • 一个udp网络程序,也可以绑定信息(ip地址,端口号),如果绑定成功,那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的

7.TCP协议


TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)

是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

TCP通信需要经过

创建连接、数据传送、终止连接

三个步骤。

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,”打电话”。


TCP特点

1.面向连接


TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)

是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

TCP通信需要经过

创建连接、数据传送、终止连接

三个步骤。

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,”打电话”。

2.可靠传输

1)

TCP采用发送应答机制

TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功

2)

超时重传

发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。

TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。

3)

错误校验

TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

4)

流量控制和阻塞管理

流量控制用来避免主机发送得过快而使接收方来不及完全收下。

3.TCP与UDP的区别

  • 面向连接(确认有创建三方交握,连接已创建才作传输。)

  • 有序数据传输

  • 重发丢失的数据包

  • 舍弃重复的数据包

  • 无差错的数据传输

  • 阻塞/流量控制


TCP通信模型

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据。

7.1 TCP客户端


服务器和客户端

服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。 客户端(Client)也被称为用户端,是指与服务器相对应,为客户提供本地服务的程序。 客户端服务器架构又被称为主从式架构,简称C/S结构,是一种网络架构,它把客户端与服务器分开来,一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。


TCP客户端

相比较于TCP服务端,tcp的客户端要简单很多,如果说服务器端是需要自己买手机、查手机卡、设置铃声、等待别人打电话流程的话,那么客户端就只需要找一个电话亭,拿起电话拨打即可,流程要少很多。

示例代码:

 from socket import *
 ​
 # 创建socket
 tcp_client_socket = socket(AF_INET, SOCK_STREAM)
 ​
 # 目的信息
 server_ip = input("请输入服务器ip:")
 server_port = int(input("请输入服务器port:"))
 ​
 # 链接服务器
 tcp_client_socket.connect((server_ip, server_port))
 ​
 # 提示用户输入数据
 send_data = input("请输入要发送的数据:")
 tcp_client_socket.send(send_data.encode("gbk"))
 ​
 # 接收对方发送过来的数据,最大接收1024个字节
 recvData = tcp_client_socket.recv(1024)
 print('接收到的数据为:', recvData.decode('gbk'))
 ​
 # 关闭套接字
 tcp_client_socket.close()


运行流程

 输入服务器ip:10.10.0.47
 请输入服务器port:8080
 请输入要发送的数据:你好啊
 接收到的数据为: 我很好,你呢

1

7.2 TCP服务端

在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:

  1. socket创建一个套接字

  2. bind绑定ip和port

  3. listen使套接字变为可以被动链接

  4. accept等待客户端的链接

  5. recv/send接收发送数据

示例代码:

from socket import *

# 创建socket
tcp_server_socket = socket(AF_INET, SOCK_STREAM)

# 本地信息
address = ('', 7788)

# 绑定
tcp_server_socket.bind(address)

# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_server_socket.listen(128)

# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_socket用来为这个客户端服务
# tcp_server_socket就可以省下来专门等待其他新客户端的链接
client_socket, clientAddr = tcp_server_socket.accept()

# 接收对方发送过来的数据
recv_data = client_socket.recv(1024)  # 接收1024个字节
print('接收到的数据为:', recv_data.decode('gbk'))

# 发送一些数据到客户端
client_socket.send("thank you !".encode('gbk'))

# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
client_socket.close()


TCP注意事项

  1. tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器

  2. tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机

  3. tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的

  4. 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信

  5. 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务

  6. listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的

  7. 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。

  8. 关闭accept返回的套接字意味着这个客户端已经服务完毕

  9. 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线

7.3 文件下载案列

TCP服务器端:

from socket import *


def get_file_content(file_name):
    """获取文件的内容"""
    try:
        with open(file_name, "rb") as f:
            content = f.read()
        return content
    except:
        print("没有下载的文件:%s" % file_name)


def main():
    # 创建socket
    tcp_server_socket = socket(AF_INET, SOCK_STREAM)
    # 本地信息
    address = ('', 7890)
    # 绑定本地信息
    tcp_server_socket.bind(address)
    # 将主动套接字变为被动套接字
    tcp_server_socket.listen(128)

    while True:
        # 等待客户端的链接,即为这个客户端发送文件
        client_socket, clientAddr = tcp_server_socket.accept()
        # 接收对方发送过来的数据
        recv_data = client_socket.recv(1024)  # 接收1024个字节
        file_name = recv_data.decode("utf-8")
        print("对方请求下载的文件名为:%s" % file_name)
        file_content = get_file_content(file_name)
        # 发送文件的数据给客户端
        # 因为获取打开文件时是以rb方式打开,所以file_content中的数据已经是二进制的格式,因此不需要encode编码
        if file_content:
            client_socket.send(file_content)
        # 关闭这个套接字
        client_socket.close()

    # 关闭监听套接字
    tcp_server_socket.close()
if __name__ == '__main__':
    main()

TCP客户端:

from socket import *
def main():

    # 创建socket
    tcp_client_socket = socket(AF_INET, SOCK_STREAM)

    # 目的信息
    server_ip = input("请输入服务器ip:")
    server_port = int(input("请输入服务器port:"))

    # 链接服务器
    tcp_client_socket.connect((server_ip, server_port))

    # 输入需要下载的文件名
    file_name = input("请输入要下载的文件名:")

    # 发送文件下载请求
    tcp_client_socket.send(file_name.encode("utf-8"))

    # 接收对方发送过来的数据,最大接收1024个字节(1K)
    recv_data = tcp_client_socket.recv(1024)
    # print('接收到的数据为:', recv_data.decode('utf-8'))
    # 如果接收到数据再创建文件,否则不创建
    if recv_data:
        with open("[接收]"+file_name, "wb") as f:
            f.write(recv_data)

    # 关闭套接字
    tcp_client_socket.close()


if __name__ == "__main__":
    main()



版权声明:本文为Hao1999_原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。