网络编程
- Python系列文章目录
- 一. 背景(Why)
- 二. 什么是网络编程(What)
- 1. ip, 端口和协议
- IP
- 端口
- 协议
- OSI七层参考模型
- 2. TCP/UDP
- 区别
- TCP三次握手
- TCP四次挥手
- 3. 数据包
- 1. ip, 端口和协议
- 三. 如何实现网络编程(How)
- 1. socket编程
- 2. UDP编程
- 实现
- 持续通信
- 基于多线程下的双向持续通信
- 3. TCP编程
- 实现
- 持续通信
- 基于多线程下的双向持续通信
一. 背景(Why)
在信息化, 网络化的时代浪潮下, 基本上所有程序都是网络程序. 最大的区别无非网络环境的区别: 内网和外网. Python语言提供了大量的内置模块和第三方模块用于支持各种网络访问,而且Python语言在网络通信方面的优点特别突出,远远领先其他语言.
二. 什么是网络编程(What)
网络编程就是如何在程序中实现两台计算机的通信.
1. ip, 端口和协议
IP
IP是Internet Protocol Address,即"互联网协议地址". 用来标识网络中的一个通信实体的地址. 通信实体可以是计算机、路由器等. 互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址
ip地址的分类
常用地址分类如下所示, E类地址: 224.0.0.1~239.255.255.254
, F类地址: 240.0..0.1~239.255.255.254
.
因为使用较少因此不做单独介绍
IPV6
- 目前主流使用的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6.
- IPv6采用128位地址长度,几乎可以不受限制地提供地址. 按保守方法估算IPv6实际可分配的地址, 可为整个地球的每平方米面积上分配1000多个地址
公有地址 公有地址(Public address)由Inter NIC(Internet NetworkInformation Center互联网信息中心)负责. 这些IP地址分配给注册并向Inter NIC提出申请的组织机构, 通过它直接访问互联网.
私有地址 私有地址(Private address)属于非注册地址,专门为组织机构内部使用. 以下列出留用的内部私有地址
- A类 10.0.0.0–10.255.255.255
- B类 172.16.0.0–172.31.255.255
- C类 192.168.0.0–192.168.255.255
- 特别说明:
127.0.0.1
本机地址;192.168.0.0--192.168.255.255
: 私有地址,属于非注册地址,专门为组织机构内部使用
端口
端口号用来识别计算机中进行通信的应用程序. 因此它也被称为程序地址. 一台计算机上同时可以运行多个程序, 传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输
- 端口分配 端口是虚拟的概念,并不是说在主机上真的有若干个端口. 通过端口,可以在一个主机上运行多个网络应用程序。. 端口的表示是一个16位的二进制整数,对应十进制的0-65535, 操作系统中一共提供了0~65535可用端口范围.
- 公认端口(Well Known Ports) 从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议
- 常见的公认端口: 端口协议名作用20,21FTP文件传输22SSH远程登录25SMTP邮件传输80HTTP数据传输443HTTPS具有安全加密的数据传输
- 注册端口(Registered Ports): 从1024到65535, 它们松散地绑定于一些服务,这些端口同样用于许多其它目的
- 常用的注册端口 端口作用2181zookeeper 默认端口3306mysql 默认端口3679redis 默认端口5601kafka 默认端口8080tomcat 默认端口8066mycat 默认端口9200elasticsearch默认端口1883emqx 默认端口4369rabbitMQ 默认端口9876rocketMQ 默认端口
协议
OSI七层参考模型
OSI是Open System Interconnection的缩写,意为开放式系统互联. 国际标准化组织(ISO)制定了OSI模型,该模型定义了不同计算机互联的标准,是设计和描述计算机网络通信的基本框架; TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层,网络接口层(物理 数据链路层).
ISO模型与TCP/IP模型的对应关系如图所示
2. TCP/UDP
TCP(Transmission Control Protocol,传输控制协议): 使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据 UDP(User Data Protocol,用户数据报协议): 是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上
区别
TCP是面向连接的,传输数据安全,稳定,效率相对较低 UDP是面向无连接的,传输数据不安全,效率较高
TCP三次握手
- 第一步,客户端发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号.
- 第二步,服务器在收到客户端的SYN报文后,将返回一个SYN ACK标志的同步确认报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)
- 第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理
TCP四次挥手
- 第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;
- 第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;
- 第三次: 由主机B端再提出反方向的关闭请求,将FIN置1 ;
- 第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束
3. 数据包
通信传输中的数据单位,一般也称“数据包”或者“数据报”, 在数据包中包括:包、帧、数据包、段、消息 网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据. 首部的结构由协议的具体规范详细定义, 在数据包的首部,明确标明了协议应该如何读取数据.
数据包结构:
数据包处理流程:
三. 如何实现网络编程(How)
1. socket编程
TCP协议和UDP协议是传输层的两种协议. Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类
socket()函数介绍
- 在Python语言标准库中,通过使用socket模块提供的socket对象,可以在计算机网络中建立可以互相通信的服务器与客户端.
- 在服务器端需要建立一个socket对象,并等待客户端的连接. 客户端使用socket对象与服务器端进行连接,一旦连接成功,客户端和服务器端就可以进行通信了
- 从下图我们可以看出, socket通讯中,发送和接收数据都是通过操作系统控制网卡来进行. 因此我们在使用之后,必须关闭socket
语法 在Python 中,通常用一个Socket表示“打开了一个网络连接”,语法格式如下:
代码语言:javascript复制socket.socket([family[, type[, proto]]])
family
: 套接字家族可以使 AF_UNIX 或者 AF_INET ; AF 表示ADDRESS FAMILY 地址族, AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型;而AF_UNIX 则是 Unix 系统本地通信.type
: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM
(TCP) 或SOCK_DGRAM
(UDP) ;protocol
: 一般不填,默认为0
例如: 创建套接字UDP/IP套接字,可以调用 socket.socket()
. 示例代码如下:
udpSocket=socket.socket (AF_INET,SOCK_DGRAM)
socket对象的内置函数和属性 在Python语言中socket对象中,提供如表所示的内置函数
- 服务器端套接字函数 函数功能s.bind()绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址.s.listen()开始TCP监听. backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量. 该值至少为1,大部分应用程序设为5就可以了。s.accept()被动接受TCP客户端连接,(阻塞式)等待连接的到来
- 客户端套接字函数 函数功能s.connect()主动初始化TCP服务器连接,一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。s.connect_ex()connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
- 客户端套接字函数 函数功能s.recv()接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量. flag提供有关消息的其他信息,通常可以忽略s.send()发送TCP数据,将string中的数据发送到连接的套接字. 返回值是要发送的字节数量,该数量可能小于string的字节大小s.sendall()完整发送TCP数据,完整发送TCP数据. 将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据. 成功返回None,失败则抛出异常s.close()关闭套接字s.recvfrom()接收UDP数据,与recv()类似,但返回值是(data,address).其中data是包含接收数据的字符串,address是发送数据的套接字地址.s.sendto()发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址, 返回值是发送的字节数.s.settimeout(timeout)设置套接字操作的超时期,timeout是一个浮点数,单位是秒. 值为None表示没有超时期. 一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回Nones.setblocking(flag)如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值). 非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常.s.makefile()创建一个与该套接字相关连的文件
2. UDP编程
UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包. 虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议.
实现
实操: 实现udp通信
服务端代码编写
代码语言:javascript复制from socket import *
"""UDP接收数据"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(传入1个元组)
s.bind(("127.0.0.1", 8848))
print("等待接收数据:")
# 3. 接收数据, 1024为当前最大传输字节
rspData = s.recvfrom(1024)
print(rspData)
print(f'收到远程的消息: {rspData[0].decode("utf-8")}, from{rspData[1]}')
# 4. 关闭连接
s.close()
客户端代码编写
代码语言:javascript复制from socket import *
"""udp发送数据"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(发送时绑定的, 是接收的端口)
addr = ("127.0.0.1", 8848)
data = input("请输入:")
# 3. 发送数据
s.sendto(data.encode("utf-8"), addr)
# 4. 关闭连接
s.close()
按顺序分别启动服务端模块(接收数据) 和 客户端模块(发送数据)
运行结果
持续通信
核心: 利用While循环让程序持续挂起, 并且设置一个点让循环关闭
实操: 实现基于UPD的持续通信
服务端代码
代码语言:javascript复制from socket import *
"""持续通信: UDP接收数据"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(传入1个元组)
s.bind(("127.0.0.1", 8848))
print("等待接收数据:")
while True:
# 3. 接收数据, 1024为当前最大传输字节
rspData = s.recvfrom(1024)
rspContent = rspData[0].decode("utf-8")
print(f'收到远程的消息: {rspContent}')
if rspContent == "88":
print("结束聊天! ")
break
# 4. 关闭连接
s.close()
客户端代码
代码语言:javascript复制from socket import *
"""持续通信: udp发送数据"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(发送时绑定的, 是接收的端口)
addr = ("127.0.0.1", 8848)
while True:
data = input("请输入:")
# 3. 发送数据
s.sendto(data.encode("utf-8"), addr)
if data == "88":
print("结束聊天!")
break
# 4. 关闭连接
s.close()
运行结果
基于多线程下的双向持续通信
核心: 结合多线程的实现方式, 然后利用持续通信的代码进行改造, 将接收和发送的方法通过类包装的形式来绑定线程并启动
实操: 基于多线程 UDP实现socket编程
服务端代码
代码语言:javascript复制from socket import *
from threading import Thread
# 不停接收
def recv_data():
while True:
# 3. 接收数据, 1024为当前最大传输字节
rspData = s.recvfrom(1024)
rspContent = rspData[0].decode("utf-8")
print(f'服务端收到远程的消息: {rspContent}')
if rspContent == "88":
print("客户端请求结束聊天! ")
break
# 不停发送
def send_data():
addr = ("127.0.0.1", 7777)
while True:
data = input(">")
s.sendto(data.encode("utf-8"), addr)
if data == "88":
break
if __name__ == '__main__':
"""多线程实现: UDP实现多线程服务端"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(传入1个元组)
s.bind(("127.0.0.1", 9999))
print("等待接收数据:")
# 创建两个线程
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
客户端代码
代码语言:javascript复制from socket import *
from threading import Thread
# 不停接收
def recv_data():
while True:
# 3. 接收数据, 1024为当前最大传输字节
rspData = s.recvfrom(1024)
rspContent = rspData[0].decode("utf-8")
print(f'客户端收到远程的消息: {rspContent}')
if rspContent == "88":
print("服务端请求结束聊天! ")
break
# 不停发送
def send_data():
while True:
data = input(">")
addr = ("127.0.0.1", 9999)
s.sendto(data.encode("utf-8"), addr)
if data == "88":
break
if __name__ == '__main__':
"""多线程实现: UDP实现多线程客户端"""
# 1. 创建套接字
s = socket(AF_INET, SOCK_DGRAM)
# 2. 绑定端口(传入1个元组)
s.bind(("127.0.0.1", 7777))
# 创建两个线程
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
运行结果
3. TCP编程
面向连接的Socket使用的协议是TCP协议. TCP的Socket名称是SOCK_STREAM, 创建套接字TCP套接字,可以调用 socket.socket()
实现
创建Socket服务器程序的步骤如下:
- 创建Socket对象
- 绑定端口号
- 监听端口号
- 等待客户端Socket的连接
- 发送/接收数据
- 关闭连接
实操: 实现TCP通信
服务端代码编写
代码语言:javascript复制from socket import *
"""TCP服务器端接收数据"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 绑定端口号
tcp.bind(("127.0.0.1", 8979))
# 3. 监听端口号
tcp.listen(5)
# 4. 等待socket连接
"""
client 表示这个新的客户端
client_info 表示这个新的客户端的ip以及port
"""
client, client_info = tcp.accept()
# 5. 读取数据
recv_data = client.recv(1024)
print(f"收到信息:{recv_data.decode('utf-8')},来自:{client_info}")
# 6. 关闭连接
client.close()
tcp.close()
客户端代码编写
代码语言:javascript复制from socket import *
"""TCP客户端发送数据到服务端"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 连接端口号(和接收略微不同)
tcp.connect(("127.0.0.1", 8979))
"""
注意:
1. tcp客户端已经链接好了服务器,所以在以后的数据发送中,不需要填写对方的ip和port----->打电话
2. udp在发送数据的时候,因为没有之前的链接,所以需要在每次的发送中,都要填写接收方的ip和port----->写信
"""
# 3. 等待socket连接
tcp.send("客户端发送消息".encode("utf-8"))
# 4. 关闭连接
tcp.close()
按顺序分别启动服务端模块(接收数据) 和 客户端模块(发送数据)
运行结果
持续通信
核心: 利用While循环让程序持续挂起, 并且设置一个点让循环关闭
实操: 实现基于TCP的持续通信
服务端代码
代码语言:javascript复制from socket import *
"""TCP服务器端接收数据"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 绑定端口号
tcp.bind(("127.0.0.1", 8979))
# 3. 监听端口号
tcp.listen(5)
# 4. 等待socket连接
"""
client 表示这个新的客户端
client_info 表示这个新的客户端的ip以及port
"""
client, client_info = tcp.accept()
print("一个客户端建立连接成功! ")
while True:
# 5. 读取数据
recv_data = client.recv(1024).decode("utf-8")
print(f"收到信息:{recv_data},来自:{client_info}")
if recv_data == "88":
break
# 获取控制台信息
msg = input('>')
# tcp连接成功后, 发送或接收的使用的不是一个对象!!!
client.send(msg.encode("utf-8"))
# 6. 关闭连接
client.close()
tcp.close()
客户端代码
代码语言:javascript复制from socket import *
"""
双向通信Socket之客户端
将控制台输入的信息发送给服务器端
读取服务器端的数据,将内容输出到控制台
"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 连接端口号
tcp.connect(("127.0.0.1", 8979))
# 3. 发送和接收数据
while True:
# 获取控制台信息
msg = input('>')
tcp.send(msg.encode("utf-8"))
if msg == "88":
break
# 接收服务器端数据
rsp_data = tcp.recv(1024).decode("utf-8")
print("服务端回复:", rsp_data)
# 4. 关闭连接
tcp.close()
运行结果
基于多线程下的双向持续通信
核心: 结合多线程的实现方式, 然后利用持续通信的代码进行改造, 将接收和发送的方法通过类包装的形式来绑定线程并启动
实操: 基于多线程 TCP实现socket编程
服务端代码
代码语言:javascript复制from socket import *
from threading import Thread
"""TCP服务器端接收数据"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 绑定端口号
tcp.bind(("127.0.0.1", 8979))
# 3. 监听端口号
tcp.listen(5)
# 4. 等待socket连接
"""
client 表示这个新的客户端
client_info 表示这个新的客户端的ip以及port
"""
client, client_info = tcp.accept()
print("一个客户端建立连接成功! ")
def recv_data():
while True:
# 5. 读取数据
recv_data = client.recv(1024).decode("utf-8")
print(f"服务端收到信息:{recv_data},来自:{client_info}")
if recv_data == "88":
break
def send_data():
while True:
# 获取控制台信息
msg = input('>')
# tcp连接成功后, 发送或接收的使用的不是一个对象!!!
client.send(msg.encode("utf-8"))
if msg == "88":
print("服务端结束聊天")
break
if __name__ == "__main__":
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
# 6. 关闭连接
client.close()
tcp.close()
客户端代码
代码语言:javascript复制from socket import *
from threading import Thread
"""
双向通信Socket之客户端
将控制台输入的信息发送给服务器端
读取服务器端的数据,将内容输出到控制台
"""
# 1. 创建Socket对象(TCP)
tcp = socket(AF_INET, SOCK_STREAM)
# 2. 连接端口号
tcp.connect(("127.0.0.1", 8979))
# 3. 发送和接收数据
def send_data():
while True:
# 获取控制台信息
msg = input('>')
tcp.send(msg.encode("utf-8"))
if msg == "88":
print("客户端结束聊天")
break
def recv_data():
while True:
# 接收服务器端数据
rsp_data = tcp.recv(1024).decode("utf-8")
print("服务端回复:", rsp_data)
if rsp_data == "88":
break
if __name__ == "__main__":
t1 = Thread(target=send_data)
t2 = Thread(target=recv_data)
t1.start()
t2.start()
t1.join()
t2.join()
# 4. 关闭连接
tcp.close()
运行结果