Python网络编程
- 1. 网络通信概述
- 2. TCP/IP 协议
- 2.1 认识 TCP/IP
- 2.2 IP 地址
- 3. socket 编程
- 3.1 网络进程间的通信 (socket)
- 3.2 UDP
- 3.3 TCP
- 3.3.1 认识 TCP
- 3.3.2 TCP 网络编程代码实现
- 3.3.3 TCP 详解
- 4. 并发服务器
- 4.1 认识并发服务器
- 4.2 多进程服务器 (处理并发)
- 4.2 多线程服务器 (处理并发)
- 4.3 协程——gevent版并发服务器(TCP)
1. 网络通信概述
网络的目的:
用网络能够把多方链接在一起,以进行数据传递
网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信
2. TCP/IP 协议
2.1 认识 TCP/IP
不同种类的计算机间到底是怎么进行数据传递的呢?
就像说不同语言的人沟通一样,只要有一种大家都认可都遵守的协议即可, 那么这个计算机都遵守的网络通信协议叫做 TCP/IP协议
TCP/IP协议(族):互联网协议包含了上百种协议标准,但是最重要的两个协议是 TCP 和 IP 协议,所以,大家把互联网的协议简称TCP/IP协议
TCP/IP协议的体系架构如下图所示:
2.2 IP 地址
ip 地址:用来在网络中标记一台电脑的一串数字。
- ip 地址的分类
每一个IP地址包括两部分:网络地址
和主机地址
- 私有 ip
网络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 - 回环地址 ip
IP地址127.0.0.1 代表本机IP地址,等价于localhost, 用 http://127.0.0.1 就可以测试本机中配置的Web服务器。 - 子网掩码
子网掩码不能单独存在,它必须结合IP地址一起使用。
子网掩码的作用: 将某个IP地址划分成网络地址和主机地址两部分。子网掩码的设定必须遵循一定的规则, 用来判断两个IP是否在同一个网络。
示例:
A: 172.25.254.18/24
B: 172.25.0.10/24
===> 此时,A, B不在同一网络
A: 172.25.254.18/16
B: 172.25.0.10/16
===> 此时,A, B在同一网络 - 端口
端口号只有整数,范围是从0到65535;
3. socket 编程
3.1 网络进程间的通信 (socket)
问题:本地通过进程 PID 来唯一标识一个进程,在网络中如何唯一标识一个进程?又是如何通信的?
网络层的 “IP地址” 可以唯一标识网络中的主机,而传输层的 “协议+端口” 可以唯一标识主机中的应用程序(进程)。因此 利用IP地址
,协议
,端口
就可以标识网络的进程。socket
(简称套接字) 是进程间通信的一种方式, 能实现不同主机间的进程间通信,网络上各种各样的服务大多都是基于 Socket 来完成通信的。
在 Python 中 使用 socket 模块的函数 socket 实现套接字的创建:
socket.socket(AddressFamily, Type)
1. Address Family
AF_INET:IPV4用于 Internet 进程间通信
AF_INET6:IPV6用于 Internet 进程间通信
2. Type:套接字类型
SOCK_STREAM:流式套接字,主要用于 TCP
协议 (默认)
SOCK_DGRAM:数据报套接字,主要用于 UDP
协议
3.2 UDP
UDP (用户数据报协议),是一个无连接的
简单的面向数据报
的运输层
协议。UDP不提供可靠性,它只是把应用程序传给 IP 层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
UDP的特点:
- UDP是面向无连接的通讯协议
- UDP是不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方
- UDP数据报包括目的端口号和源端口号信息
- UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。
- 可实现广播
UDP 应用场景:
UDP是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP一般用于多点通信和实时的数据业务,比如:
- 语音广播
- 视频
- TFTP (简单文件传送)
- SNMP (简单网络管理协议)
- DNS (域名解释)
UDP 网络程序编写的主要流程:
UDP 编程——模拟QQ聊天,代码示例:
- 用户B (客户端) 代码:
import socket
# 1. 创建 UDP 套接字
udpclient = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
print("QQ用户B上线.........")
while True:
# 2. 对需要发送的数据进行编码预处理,str --> byte
send_data = input('B:>> ').encode('utf-8')
if not send_data:
continue
# 3. 与服务端建立连接,并发送数据
udpclient.sendto(send_data, ('127.0.0.1', 9999))
if send_data == b'quit':
print("聊天结束.....")
break
# 4. 阻塞状态,直到接收到服务端回传的数据和地址信息(address=(ip, port))
# 才会解阻塞
recv_data, address = udpclient.recvfrom(1024)
print("A:>> ", recv_data.decode('utf-8'))
# 5. 关闭 socket 对象
udpclient.close()
- 用户A (服务端) 代码:
import socket
# 1. 创建 UDP 套接字
udpserver = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 2. 限定连接的ip,绑定端口
udpserver.bind(('0.0.0.0', 9999))
print('QQ用户A上线.........')
while True:
# 3. 阻塞状态,直到接收到客户端回传的数据才解阻塞
# 返回的是元组, 一个元素是客户端发送的信息, 第二个元素是客户端和服务端交互的地址(IP, port)
recv_data, address = udpserver.recvfrom(1024)
print("B:>> ", recv_data.decode('utf-8'))
if recv_data == b'quit':
print("聊天结束.......")
break
# 4. 数据预处理
# 发送的消息必须是bytes类型
# bytes --> str bytesObj.decode('utf-8')
# str --> bytes strObj.encode('utf-8')
send_data = input('A:>> ').encode('utf-8')
if not send_data:
continue
# 5. 与客户端建立连接,并发送数据
udpserver.sendto(send_data, address)
# 6. 关闭 socket 对象
udpserver.close()
执行结果:
3.3 TCP
3.3.1 认识 TCP
TCP:传输控制协议 (英语:Transmission Control Protocol,缩写为TCP) 是一种面向连接的
、可靠的
、基于字节流
的传输层
通信协议。
TCP 与 UDP对比:
3.3.2 TCP 网络编程代码实现
TCP 网络程序编写的主要流程:
代码示例:
- 客户端代码
import socket
# 1. 创建 TCP 套接字
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 连接服务端
client.connect(('127.0.0.1', 9998))
# 3.给服务端发送消息
client.send(b'hello server')
# 4. 接收服务端发送的消息
recv_data = client.recv(1024).decode('utf-8')
print("接收服务端发送的消息:", recv_data)
# 5. 关闭socket对象
client.close()
- 服务端代码
import socket
# 1. 创建服务端的 TCP 套接字
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 绑定地址和端口(IP:port)
server.bind(('0.0.0.0', 9998))
# 3. 监听是否有客户端连接?listen
server.listen(5)
print('server start .........')
# 4.接收客户端的连接accept
clientSocketObj, clientAddress = server.accept()
# 5. 接收客户端发送的消息
recv_data = clientSocketObj.recv(1024).decode('utf-8')
print("接收到客户端发送的消息:", recv_data)
# 6. 给客户端发送消息
send_data = b'hello client'
clientSocketObj.send(send_data)
# 7. 关闭socket对象
clientSocketObj.close()
server.close()
3.3.3 TCP 详解
三次握手:TCP连接是通过三次握手来连接的。
- 两个包: 同步序列标号 SYN;确认包 ACK
- 四种状态: SYN_SENT, LISTEN, SYN_RECV, ESTABLISHED
- 第一次握手
当客户端向服务器发起连接请求时,客户端会发送同步序列标号SYN到服务器,在这里我们设SYN为x,等待服务器确认,这时客户端的状态为SYN_SENT。 - 第二次握手
当服务器收到客户端发送的SYN后,服务器要做的是确认客户端发送过来的SYN,在这里服务器发送确认包ACK,这里的ACK为x+1,意思是说“我收到了你发送的SYN了”,同时,服务器也会向客户端发送一个SYN包,这里我们设SYN为y。这时服务器的状态为SYN_RECV。(一句话,服务器端发送SYN和ACK两个包) - 第三次握手
客户端收到服务器发送的SYN和ACK包后,需向服务器发送确认包ACK,“我也收到你发送的SYN了,我这就给你发个确认过去,然后我们即能合体了”,这里的ACK为y+1,发送完毕后,客户端和服务器的状态为ESTABLISH,即TCP连接成功。
在三次握手中,客户端和服务器端都发送两个包SYN和ACK,只不过服务器端的两个包是一次性发过来的,客户端的两个包是分两次发送的。
四次挥手:当A端和B端要断开连接时,需要四次握手,这里称为四次挥手。
断开连接请求可以由客户端发出,也可以由服务器端发出,在这里我们称A端向B端请求断开连接。
- 第一次挥手
A端向B端请求断开连接时会向B端发送一个带有FIN标记的报文段,这里的FIN是Finish的意思。 - 第二次挥手
B端收到A发送的FIN后,B段现在可能现在还有数据没有传完,所以B端并不会马上向A端发送FIN,而是先发送一个确认序号ACK,意思是说“你发的断开连接请求我收到了,但是我现在还有数据没有发完,请稍等一下呗”。 - 第三次挥手
当B端的事情忙完了,那么此时B端就可以断开连接了,此时B端向A端发送FIN序号,意思是这次可以断开连接了。 - 第四次挥手
A端收到B端发送的FIN后,会向B端发送确认ACK,然后经过两个MSL时长后断开连接。
MSL是 Maximum Segment Lifetime,最大报文段生存时间,2个MSL是报文段发送和接收的最长时间
4. 并发服务器
4.1 认识并发服务器
并发服务器是 socket 应用编程中最常见的应用模型。
- 并发服务器模型根据连接方式分为
长连接
和短连接
。
通信方式 | 具体通信过程 |
长连接 | 建立 socket 连接后不管是否使用都保持连接 |
短连接 | 书暗访有数据交互时,建立 socket 连接,数据发送完成后断开连接 |
- 并发服务器模型根据处理方式可分为
同步
方式和异步
方式。
同步:指客户端发送请求给服务器等待服务器返回处理结果
异步:指客户端发送请求给服务器,不等待服务器返回处理结果,而是直接去完成其他的流程,对于处理结果客户端可以事后查询或让服务器进行主动通知。
4.2 多进程服务器 (处理并发)
单进程服务器:同一时刻只能为一个客户进行服务,不能同时为多个客户服务
多进程服务器:优点在于
通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务;缺点则是
当客户端不是特别多的时候,这种方式还行,如果有成百上千个,就不可取了,因为每次创建进程等过程需要好较多的资源
代码示例
- 客户端代码
import socket
# 1. 创建服务端 TCP 套接字
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 连接服务端
client.connect(('127.0.0.1', 9997))
while True:
# 3.给服务端发送消息
send_data = input('client: >> ').encode('utf-8')
if not send_data:
continue
client.send(send_data)
if send_data == 'quit':
break
# 5. 关闭socket对象
client.close()
- 服务端代码
# 实现多进程的方式:
# 1. 实例化对象
# 2. 继承子类
# 注意: 一定要确定多进程要处理的任务
import socket
from multiprocessing import Process
from threading import Thread
import gevent
from gevent import monkey
monkey.path_all()
def dealWithClient(clientSocketObj, clientAddress):
'''任务: 处理客户端请求并为其服务'''
while True:
# 5. 接收客户端发送的消息
recv_data = clientSocketObj.recv(1024).decode('utf-8')
print(clientAddress[0] + str(clientAddress[1]) + ':> ' + recv_data)
if recv_data == 'quit':
break
clientSocketObj.close()
# 1. 创建服务端 TCP 套接字
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 重复使用绑定的信息,即解决端口已占用的问题
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址和端口(IP:port)
server.bind(('0.0.0.0', 9997))
# 3. 监听是否有客户端连接
server.listen(5)
print('server start .........')
while True:
# 4.接收客户端的连接accept
clientSocketObj, clientAddress = server.accept()
# dealWithClient(clientSocketObj)
# 4.1 多进程服务器 (处理并发)
p = Process(target=dealWithClient, args=(clientSocketObj, clientAddress))
p.start()
# 4.2 多线程服务器 (处理并发)
# t = Thread(target=dealWithClient, args=(clientSocketObj, clientAddress))
# t.start()
# 4.3 协程——gevent版并发服务器(TCP)
# gevent.spawn(target=dealWithClient, args=(clientSocketObj, clientAddress))
# server.close()
4.2 多线程服务器 (处理并发)
多线程服务器:每一个连接催生一个线程
代码示例如 4.2 服务端代码所示
4.3 协程——gevent版并发服务器(TCP)
代码示例如 4.2 服务端代码所示