Python 进阶 网络编程与多进程
一、网络编程在解决什么问题
网络 :很多台电脑用网线、WiFi、路由器等连在一起,按共同遵守的规则(协议)交换数据。
网络编程 :写程序让运行在不同电脑上的两个进程 之间能发数据、收数据。
比喻:
- IP 地址 :小区地址------确定是哪一栋楼(哪一台设备)。
- 端口号 :门牌 / 窗口号------同一栋楼里有很多房间(很多程序),端口决定数据交给哪一个程序。
- 协议 :两个人都说普通话------约定数据怎么打包、怎么确认、怎么重传,双方对得上才能通信。
一句话:IP 找机器,端口找程序,协议定规矩。
二、IP 地址与端口
IP 地址 :在网络里大致唯一标识一台设备 。同一局域网里常用形如 192.168.x.x 的形式;本机自己和自己测可以用 127.0.0.1(回环地址)。
端口 / 端口号 :一台电脑上可以同时跑浏览器、微信、你写的 Python 程序。只凭 IP 只能找到电脑,端口用来区分"这份数据交给哪个进程" 。端口一般是 0~65535 的整数,写程序时常用 1024 以上的端口,避免和系统常用服务冲突。公有端口1-1023 动态端口:1024-65535
本机小操作(Windows) :
在命令行用 ipconfig 可以看本机 IP;用 ping 某 IP 或域名 可以粗测网络通不通;ping 127.0.0.1 测本机协议栈是否正常。
三、TCP 协议
1. TCP 的三大特点
-
面向连接
真正传业务数据之前,必须先建立连接(底层就是你下面要学的三次握手)。就像打电话要先拨通再说话,不是丢完就走。
-
可靠传输
有确认、重传、按序等机制,尽量保证:你发过去的字节流,对方按顺序收到(或发现错误、连接异常)。适合文件、网页、聊天等"不能乱、不能大面积丢"的场景。
-
面向字节流
传送的是连续的字节流,没有"消息边界"帮你分包,应用层要自己约定"一条命令到哪算结束"(例如换行、固定长度、先长度再内容等)。
和 UDP 对比帮助记忆(本课代码以 TCP 为主):UDP 无连接 、不保证可靠、发出去就不管,像发短信;TCP 像打电话,更"啰嗦"但更稳。
2. 三次握手:建立连接时到底发生了什么
三次握手指的是:客户端和服务器之间连续交换三个带控制位的报文段 ,把"我要连你""我准备好了""我也准备好了"说清楚,并交换初始序号,后面的可靠传输都依赖这些序号。
可以按下面四列来记:谁发起、发什么、对方收到后明白什么、这一步的意义。
| 次序 | 方向 | 发送内容(习惯说法) | 含义(白话) |
|---|---|---|---|
| 第 1 次 | 客户端 → 服务器 | SYN | "我想建立连接,这是我的初始序号(seq)。" |
| 第 2 次 | 服务器 → 客户端 | SYN + ACK | "我收到了你的请求(ACK),我也发起我这一侧的连接(SYN),这是我的初始序号。" |
| 第 3 次 | 客户端 → 服务器 | ACK | "我收到了你的 SYN,双方序号对齐,可以开始传数据。" |
这一阶段你要抓住的重点:
-
为什么是"三次"而不是两次?
两次只能保证"一方知道另一方在线",没法可靠地统一双方的初始序号、也没法很好地处理旧的、重复的连接请求 。三次是在工程上公认的最小可用 做法:既让双方都确认"对方愿意连、自己也愿意连",又把两边的起始序号对齐,避免很多边角问题。面试时可以说:防止历史连接请求的报文扰乱本次连接、并同步双方初始序列号。
-
握手阶段还几乎不传业务数据 ,主要是同步状态 。真正
send的业务数据是在握手完成之后。 -
SYN 洪泛等攻击和 SYN 报文有关,了解即可;写应用时知道"连上之前必经历这三步"即可。
-
作用: 确认双方的发送与接收能力正常
3. 四次挥手:关闭连接时为什么往往是四步
TCP 是全双工 :同一时间里,A 可以给 B 发,B 也可以给 A 发,两个方向可以独立关闭。
关闭时常见情况是:一边先说自己"发完了" ,另一边可能还有数据没发完,所以要分步确认,不能像握手那样简单合并成三步。
| 次序 | 方向 | 发送内容(习惯说法) | 含义(白话) |
|---|---|---|---|
| 第 1 次 | 主动关闭方 → 被动方 | FIN | "我这边的数据发完了,我要关闭我这边的发送。" |
| 第 2 次 | 被动方 → 主动关闭方 | ACK | "我知道你发 FIN 了。"此时被动方还可以继续发数据给主动方。 |
| 第 3 次 | 被动方 → 主动关闭方 | FIN | "我这边也发完了,我也关闭发送。" |
| 第 4 次 | 主动关闭方 → 被动方 | ACK | "我知道你 FIN 了。"连接完全释放过程中还要处理最后的确认。 |
这一阶段你要抓住的重点:
-
为什么是四次?
被动方收到对方的 FIN 时,往往立刻回 ACK 表示"我知道你关了",但自己可能还没发完数据 ,所以要等自己这边也发完 ,再单独发自己的 FIN。因此中间的 ACK 和最后的 FIN 通常不能合二为一,就形成了四次挥手(少数情况下第二步和第三步可以合并成"带 FIN 的 ACK",那是特例)。
-
全双工:关的是"某一个方向上的发送/接收",不是一瞬间两个方向同时无声无息消失。
-
主动关连接的一方 在发完最后一个 ACK 之后会进入 TIME_WAIT 状态有一段时间------为了万一最后一个 ACK 丢了,对方重传 FIN 时你还能补 ACK。初学知道"用来兜底丢包"即可。
四、Socket 是什么
Socket(套接字)是操作系统提供给你的编程接口 :你的 Python 程序通过它,把"发网络数据、收网络数据"这些底层细节包装成几个函数调用。
- 你用
socket.socket(地址族, 类型)创建一个套接字对象。 - IPv4 + TCP 对应:
socket.AF_INET和socket.SOCK_STREAM。
可以理解为:有了一个能走 TCP、能选 IPv4 地址的通信端点。
五、TCP 编程整体流程:服务端在做什么、客户端在做什么
先记角色:
- 服务端 :在自家电脑上开好端口等人连(被动)。
- 客户端 :知道对方的 IP 和端口,主动连过去(主动)。
下面按推荐顺序把每一步讲清楚(这是你必须能默画、能讲给别人听的)。
(一)服务端:从"建套接字"到"能和一个人聊天"
| 步骤 | 函数 | 这一步在干什么(必须能口述) |
|---|---|---|
| 1 | socket.socket(AF_INET, SOCK_STREAM) |
向系统申请一个 TCP / IPv4 套接字。还没有绑定到具体 IP 和端口。 |
| 2 | bind((IP, 端口)) |
声明:我这个程序在这个 IP 的这个端口上"安家" 。别人要连服务器,就连这个地址。'' 或 0.0.0.0 常表示监听本机所有网卡 ;127.0.0.1 只有本机程序能连。参数必须是二元组 ,端口是整数。 |
| 3 | listen(backlog) |
把套接字设为被动监听 状态:backlog 表示排队等待你 accept 的连接 最多能堆多少。之后才能在这一步的套接字上 accept。 |
| 4 | accept() |
阻塞 :直到有一个客户端完成三次握手连上来。返回 (新套接字, 客户端地址) 。关键 :从此刻起你有两个套接字角色 ------原来的 继续负责"在门口接人";新的 专门和当前这位客户端收发数据。 |
| 5 | 新套接字.recv(大小) |
从当前连接 收最多这么长的字节,返回 bytes。阻塞 直到收到数据或连接关闭。要对 accept 返回的那只套接字操作 (常写成 conn.recv),不要对监听套接字 recv。 |
| 6 | 新套接字.send(字节数据) |
往当前连接发数据;Python 3 里要发字符串需先 .encode('utf-8')。同样用 accept 返回的那只套接字 (如 conn.send)。 |
| 7 | 新套接字.close() |
结束和这一位客户端的会话(底层会走关闭流程)。 |
| (可选) | 原监听套接字.close() |
若服务器程序要彻底退出、不再接任何客户,再关监听套接字。很多练习里为了继续接下一单,不关监听套接字。 |
口诀(帮助背顺序) :创建 → 绑定 → 监听 → 接电话(accept) → 听/说(recv/send) → 挂机(close)。
(二)客户端:从"建套接字"到"连上服务器"
| 步骤 | 函数 | 这一步在干什么 |
|---|---|---|
| 1 | socket.socket(AF_INET, SOCK_STREAM) |
同样创建一个 TCP / IPv4 套接字。 |
| 2 | connect((服务器IP, 端口)) |
主动 向对方的 bind 地址发起连接;底层完成三次握手 。成功后才算"线路通了"。通常会阻塞到连上或失败。 |
| 3 | send / recv |
和服务端一样,用这同一个套接字收发(客户端没有"接听套接字"和"通话套接字"的拆分)。 |
| 4 | close() |
关闭连接。 |
(三)小结:服务端与客户端"长得不一样"的地方
- 服务端多
bind、listen、accept;客户端用connect代替这三步里的"等人连"逻辑。 - 服务端
accept会多出一个新套接字 ;客户端始终就一个套接字在和服务器说话。 accept、connect、recv常常阻塞:程序会停在这一行,直到连上、直到收到数据,或连接异常------写多任务时就会明白为什么要进程/线程。
六、字符串与二进制(encode / decode)
网络里传的是 字节(bytes) ,不是 Python 里的 str。
- 发送:
"你好".encode('utf-8') - 接收:
data.decode('utf-8')
双方必须用同一种编码 ,否则就会乱码。多数项目默认 UTF-8。
七、端口被占用:端口重用
现象:服务器刚关,立刻再运行,提示地址已被占用(和 TIME_WAIT 等有关)。
做法:在 bind 之前调用:
python
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
记住:一定要在 bind 前,否则起不到你想要的"立刻重启还能绑同一端口"的效果。
八、循环 accept:一个接一个接待多个客户端
text
while True:
新套接字, 地址 = 监听套接字.accept()
# 用 新套接字 recv / send
新套接字.close()
含义:可以一个接一个地服务很多客户 ,但同一时刻通常只有一个循环在执行 ,若你在内层又用 while 跟一个人聊很久,别人还是要等 ------这就是"单线程顺序服务"。要很多人同时 传文件、同时聊天,后面要用多进程、多线程或异步 ,本课会先用多进程做入门。
九、文件上传思路(和聊天同一套 API)
- 服务器 :
accept后用wb打开要保存的文件;循环recv(一块大小),把收到的byteswrite 进文件;若recv得到空字节b''(长度为 0),一般表示对方关连接了,上传结束,跳出循环。 - 客户端 :
rb读本地文件,循环send一块 ;发完close()。
为什么要分块? 文件可能很大,一次读进内存不现实;网络也是流式的,适合边读边发、边收边写。
十、多任务:并发和并行
并发 :一段时间内多个任务在交替执行 (单核 CPU 快速切换,看起来像同时)。
并行 :多核 时,不同任务真的在同一时刻各跑各的核心。
多任务的目的:让 CPU 别闲着 ,提高整体效率。
和后面线程对比时先记一句:进程之间数据默认不共享;同进程里的线程共享内存(以后会细讲锁)。
十一、多进程:multiprocessing
进程:程序的一次运行实例;操作系统给进程分配资源(内存空间等)。打开一个软件,通常至少一个进程。
1. 创建与启动
python
import multiprocessing
def job():
print("子进程里的代码")
if __name__ == '__main__':
p = multiprocessing.Process(target=job)
p.start()
target=写函数名 ,不要写成job()(加了括号就变成"先执行函数再把返回值当 target"了)。start()才真正启动子进程。
Windows 必记 :创建进程、写 Process、start() 的代码要放在
python
if __name__ == '__main__':
里面,否则可能反复启动子进程或报错。
2. 传参:args 和 kwargs
python
def work(name, n):
...
p1 = multiprocessing.Process(target=work, args=('张三', 5))
p2 = multiprocessing.Process(target=work, kwargs={'name': '李四', 'n': 10})
args:元组,按位置传。只有一个参数时要写成(x,),不能写成(x),后者在 Python 里不是元组。kwargs:字典,键名必须和函数参数名一致。
3. 查看 PID
python
import os
import multiprocessing
os.getpid() # 当前进程号
os.getppid() # 父进程号
multiprocessing.current_process().pid
进程结束后 PID 会被系统回收,以后可能被别的进程复用;在存活期内可以把它当成该进程的标志。
4. 全局变量为什么不共享
每个子进程里是父进程内存的一份拷贝 ,模块级别的 my_list = [] 在不同进程里各有一份 。一个进程里 append,另一个进程看不到 。要在进程间传数据,需要队列、Manager、管道等(后面课程会接触);先建立"默认不共享"的直觉。
5. 守护进程 daemon
python
p = multiprocessing.Process(target=job)
p.daemon = True # 要在 start() 之前设
p.start()
守护进程 随主进程退出 而结束,适合"主程序关了,后台辅助任务也不用留了"的场景;默认则主进程会等子进程跑完再结束(行为因写法略有差异,以课堂演示为准)。
十二、容易写错的地方
bind写成bind('127.0.0.1', 8080)------少了括号,应该是bind(('127.0.0.1', 8080))。- 端口写成字符串
'8080'------应该是 整数8080。 - 用监听套接字 去
recv/send------应对accept返回的套接字操作。 - 忘了
encode/decode,或两端编码不一致。 SO_REUSEADDR写在bind后面。recv得到b''仍死循环------要判断长度为 0 表示对端关了。- Windows 下
if __name__ == '__main__':忘写。 args=(5)不是单元素元组,应args=(5,)。
今日自测
1. IP 地址的作用
在网络中唯一标识一台设备 ,让数据能到达"哪台电脑"。可记:收货地址、楼栋。
2. 端口号的作用
在一台计算机上**标识正在运行的哪个程序(进程)**接收数据。
IP 找机器,端口找程序。
3. TCP 的特点
面向连接 、可靠传输 、面向字节流。(可与 UDP 对比记忆。)
4. TCP 服务器端收发消息要走哪些步骤?
socket 创建 → bind → listen → accept 得到新套接字 → recv / send(在新套接字上) → close 新套接字;监听套接字可继续接人。
5. 什么是进程?
程序在操作系统里的一次运行实例,是资源分配的基本单位。
6. 怎么用 multiprocessing 起多进程?
import multiprocessing obj = multiprocessing.Process(target=函数名, args=..., kwargs=...) → obj.start() ;Windows 下代码放在 if __name__ == '__main__':。
7. 怎么获取 PID?
os.getpid()、os.getppid()、multiprocessing.current_process().pid。
8. args 和 kwargs 怎么用?
args 元组按位置传,单元素写 (x,) ;kwargs 字典按关键字传,键对齐参数名。
9. 进程之间共享全局变量吗?
默认不共享;各进程有独立拷贝,要共享需专门机制。
最小示例
服务端(单次接一个客户端、收一条再回一条):
python
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server.bind(('0.0.0.0', 9000))
server.listen(128)
conn, addr = server.accept()
data = conn.recv(1024).decode('utf-8')
print("收到:", data)
conn.send("服务器已收到".encode('utf-8'))
conn.close()
server.close()
客户端:
python
import socket
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(('127.0.0.1', 9000))
c.send("你好".encode('utf-8'))
print(c.recv(1024).decode('utf-8'))
c.close()
先按注释理解顺序,再改成你自己的 IP 和端口做练习。