Python 九阳神功之肆:网络编程 · Socket 从入门到实战

第 4 篇 · 兵器谱:网络编程 · Socket 从入门到实战

行走江湖,得有趁手的兵器。
IP 是江湖坐标,端口是暗号,TCP/UDP 是传音入密的内功心法。
本篇为你打造网络编程的「屠龙刀」,客户端与服务端通信,一剑封喉!


1. 网络设备的三要素

  • IP地址:网络环境中电脑的唯一标识,通过 IP 可以找到某台计算机
  • 端口:一台服务器上会有 N 个应用,通过唯一的端口映射服务器中某个应用(服务)
  • 协议:网络编程协议 --- TCP / UDP

1.1 域名解析

输入 www.baidu.com 的时候,会进入 DNS 解析:

  • 本地 host 文件 → 本地 DNS 解析 → 远程 DNS 解析 → 获取 IP 地址 → 找到服务器

8.8.8.8 是比较靠谱的 DNS 解析器

绝大部分网站的端口是 80 端口,默认不需要写


1.2 IP 版本

  • IPv4:IP 地址量约 2 的 32 次方 ≈ 43 亿,已接近耗尽
  • IPv6:IP 地址量约 2 的 128 次方,几乎无穷无尽

1.3 端口可用范围

端口号的可用范围是 0 ~ 65535,作用是帮你找到在计算机上独一无二的服务。

不要去抢占知名端口号:0 ~ 1023。但也有一些不在这个范围内,比如 MySQL 的 3306。

IP 地址帮你锁定网络中的计算机设备,端口号帮你在这个设备上找到对应的服务。


2. TCP 与 UDP 的区别

TCP(传输控制协议)面向连接、可靠 ;UDP(用户数据报协议)无连接、不可靠,都属于传输层协议。

2.1 核心对比

对比项 TCP UDP
连接特性 面向连接,三次握手建立连接,四次挥手断开连接 无连接,发数据前不需要建立连接,直接发包
可靠性 可靠传输:确认应答、超时重传、丢包重传;不丢包、不乱序 不可靠:不保证到达,不保证顺序,可能丢包、乱序
流量/拥塞控制 有流量控制(滑动窗口)、拥塞控制 无,只管发送,不感知网络拥堵
报文 面向字节流,数据是流,没有报文边界 面向数据包,保留报文边界,一个 UDP 包就是一条消息
头部开销 头部 20~60 字节,开销大 头部固定 8 字节,开销小
速度 较慢,握手、重传、确认带来额外开销 速度快,延迟低,开销小
适用场景 文件传输、HTTP/HTTPS、SSH、数据库访问,要求数据不能丢 直播、视频通话、游戏、DNS,允许少量丢包,追求低延迟

2.2 形象比喻

  • TCP = 打电话:拨号(三次握手)→ 通话(可靠传输)→ 挂断(四次挥手);说话对方一定能收到,顺序不变,但是流程多
  • UDP = 寄明信片:直接扔出去,不用先联系对方;明信片可能丢失、顺序乱掉,但是发得快,成本低

3. TCP 的三次握手

客户端主动发起连接,服务端被动接收,一共 3 次报文交互。

3.1 三次握手流程

  1. 第一次握手:客户端 → 服务端【SYN】
    客户端发送 SYN=1,随机初始序列号 seq=x,客户端进入 SYN_SENT 状态。
    含义:客户端请求建立连接。
  2. 第二次握手:服务端 → 客户端【SYN+ACK】
    服务端收到 SYN,回复 SYN=1,ACK=1ack=x+1(确认客户端),自己随机初始序列号 seq=y;服务端进入 SYN_RCVD 状态。
    含义:同意连接,同时也向客户端发起连接请求。
  3. 第三次握手:客户端 → 服务端【ACK】
    客户端收到 SYN+ACK,回复 ACK=1ack=y+1;客户端进入 ESTABLISHED(已连接) ;服务端收到这个 ACK 后,也进入 ESTABLISHED,连接正式建立,可以传数据。

3.2 为什么是三次,不是两次?

  • 两次握手:服务端收到 SYN 就直接建立连接。如果客户端旧的失效 SYN 延迟到达服务器,服务器会建立无效连接,白白占用资源。
  • 三次握手:双方收发能力都得到验证:客户端能发能收、服务端能发能收。第三次 ACK 可以拒绝过期的历史连接。

SYN=1 表示握手连接

客户端发 SYN=1,发 seq=x

服务端回 SYN=1,发 ack=x+1,seq=y

客户端回 ack=y+1


4. TCP 的四次挥手

任何一方都可以主动关闭,下面以客户端主动关闭举例。

4.1 四次挥手流程

  1. 第一次挥手:客户端 → 服务端【FIN】
    客户端发送 FIN=1seq=u,客户端进入 FIN_WAIT_1
    含义:客户端不再发送数据,但还可以接收数据。
  2. 第二次挥手:服务端 → 客户端【ACK】
    服务端回复 ACK=1ack=u+1seq=v;服务端进入 CLOSE_WAIT ;客户端收到 ACK 进入 FIN_WAIT_2
    含义:收到关闭请求;此时服务端还可以继续向客户端发送剩余数据
  3. 第三次挥手:服务端 → 客户端【FIN】
    服务端数据全部发送完毕,发送 FIN=1ACK=1seq=wack=u+1;服务端进入 LAST_ACK
    含义:服务端也没有数据要发了,请求关闭。
  4. 第四次挥手:客户端 → 服务端【ACK】
    客户端回复 ACK=1ack=w+1seq=u+1;客户端进入 TIME_WAIT (等待 2MSL);服务端收到 ACK,立刻进入 CLOSED ,连接关闭。客户端等待 2MSL 之后,也进入 CLOSED。

FIN=1 表示挥手连接

客户端发 FIN=1,seq=u

服务端回 ack=u+1 --- 把剩余的数据传输完成

服务端发 FIN=1,ack=u+1,seq=w

客户端回 ACK=1,ack=w+1

4.2 为什么挥手要 4 次,握手只要 3 次?

握手时 SYN 和 ACK 可以合并成一个报文;挥手时,服务端收到 FIN,可能还有业务数据没发完,不能立刻发 FIN,只能先回复 ACK,等数据发完再发 FIN,所以拆成两次,总共 4 次。


5. Socket 套接字

5.1 Socket 套接字对象介绍

使用 socket 来进行网络通信编程工具。

python 复制代码
# 引入socket模块
import socket

# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 地址族选用 IPv4,传输类型选用字节流

print(server_socket)
  • 客户端(学名 Client):前端 / HTML / CSS / JS / Android / iOS / 小程序 / Flutter / RN
  • 服务端(学名 Server):Python / Java / Go / Rust / Node.js / C#

5.2 Socket 客户端和服务端通信流程分析

服务端流程

  1. 创建一个 socket 对象 --- 大管家(不负责和客户端直接对话)
  2. bind() 绑定 IP 和端口号 --- 表示通过这个地址和端口能找到该服务
  3. listen(5) --- 设置最大挂起的 socket 数量,默认值为 1,最大为 128,一般设置为 5
  4. accept() --- 接收客户端发起连接,生成和客户端对话的 socket 对象,返回客户端信息
    accept 是阻塞的:它不走完,下面谁也别走
  5. send() --- 发送数据
  6. recv() --- 接收数据(发送和接收没有强制顺序要求,但发送的数据一定是字节流)
  7. close() --- 释放当前和客户端对话的 socket

5.3 二进制编解码操作

python 复制代码
str = "rucy,今天是个好日子"

print(str.encode("gbk").decode("gbk"))
print(str.encode("utf-8").decode("utf-8"))
  • GBK 编码:一个中文占 2 个字节,英文字符占 1 个字节,支持简体 + 繁体,部分日文
  • UTF-8 编码:一个中文占 3 个字节,英文字符占 1 个字节,支持全球所有字符

如果内容中不包含中文,直接在字符串前加 b 即表示该字符为 bytes


5.4 Socket 套接字对象创建(服务端)

注意:socket 对象默认是阻塞模式,阻塞的方法有:

  • accept()(服务端专用)
  • recv(bufsize)(客户端/服务端)
  • connect()(客户端专用)

阻塞:当前代码未执行完毕,绝不可能执行下一行代码

python 复制代码
import socket

print("1.创建socket服务器对象")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print("2.设置端口号在进程结束立刻释放")
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

print("3.绑定端口号和ip地址")
server_socket.bind(("127.0.0.1", 9999))

print("4.设置监听并设置挂起最大监听量")
server_socket.listen(5)

print("5.等待客户端连接")
client_socket, client_info = server_socket.accept()

print("6.客户端连接成功", client_info)

print("7.给客户端发送数据")
client_socket.send("我是你的服务器对象".encode("utf-8"))

print("8.接收客户端消息")
data = client_socket.recv(1024)
print(f"收到客户端发的消息:{data.decode('utf-8')}")

print("9.关闭和客户端通讯的连接")
client_socket.close()

# server_socket 一般不关

5.5 Socket 套接字对象创建(客户端)

python 复制代码
import socket

print("1.创建客户端socket对象")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print("2.通过ip端口连接服务器")
client_socket.connect(("127.0.0.1", 9999))

print("3.连接成功")

content = client_socket.recv(1024)
print(f"服务器给你说:{content.decode('utf-8')}")

send_content = input("跟服务说:")
client_socket.send(send_content.encode("utf-8"))

print("关闭客户端连接")
client_socket.close()

5.6 实现无限聊天版本 --- 服务端

python 复制代码
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(("127.0.0.1", 9999))
server_socket.listen(5)

# 阻塞方法
client_socket, client_info = server_socket.accept()
print(f"和客户端{client_info}连接成功")

client_socket.send("你好,我是rucy,欢迎你来到我的聊天房间".encode("utf-8"))

# accept recv
while True:
    data = client_socket.recv(1024)
    if not data:
        break
    print(f"收到客户端:{client_info}的消息:{data.decode('utf-8')}")

client_socket.close()

5.7 实现无限聊天 --- 客户端

python 复制代码
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 9999))

data = client_socket.recv(1024)
print(f"服务器对你说:{data.decode('utf-8')}")

while True:
    send_content = input("对服务器说(quit退出)")
    if send_content == 'quit':
        break
    client_socket.send(send_content.encode('utf-8'))

client_socket.close()

5.8 模拟多任务实现多人版本聊天(服务端)

支持多人聊天的服务端。

python 复制代码
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(("192.168.25.170", 9999))
server_socket.listen(5)

print("完成服务端初始化所有功能")

# 模拟多任务 --- 多进程 > 多线程 > 多协程
while True:
    client_socket, client_info = server_socket.accept()
    client_socket.send("欢迎你兄弟来连接rucy的电脑".encode("utf-8"))
    data = client_socket.recv(1024)
    print(f"收到客户端{client_info}消息:{data.decode('utf-8')}")
    client_socket.close()

5.9 模拟多任务多人版本聊天(客户端)

python 复制代码
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 8888))

data = client_socket.recv(1024)
print(f"收到服务端消息:{data.decode('utf-8')}")

content = input("跟rucy说:")
client_socket.send(content.encode("utf-8"))

client_socket.close()

6. 扩展功能

6.1 文件选择器对象

需求:当输入 img 的时候,自动弹出文件选择器,自动过滤文件,只选择图片。

python 复制代码
import tkinter
from tkinter import filedialog

content = input("对服务器说(img选择图片):")

if content == 'img':
    print("选择图片")
    root = tkinter.Tk()
    root.withdraw()  # 防止弹出多余小窗口
    file_path = filedialog.askopenfilename(
        title="请选择图片",
        filetypes=[("图片", "*.png;*.jpg;*.jpeg;*.gif")]
    )
    print(file_path)
    root.destroy()

6.2 解析图片获取图片的字节流及详细信息

python 复制代码
import os
import tkinter
from tkinter import filedialog

content = input("输入img选择图片")
if content == 'img':
    root = tkinter.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="请选择图片",
        filetypes=[("图片", "*.jpg *.jpeg *.png")]
    )
    ext = os.path.splitext(file_path)[1][1:]  # 拿到图片的扩展名
    with open(file_path, "rb") as f:
        img_data = f.read()  # 拿到图片的二进制数据

    print(f"拿到了图片的扩展名{ext}, 长度大小:{len(img_data)}, 二进制流数据:{img_data}")

# TCP协议 二进制字节流  jpeg|42196|image_data

6.3 选择一张图片存储到 data 目录下

6.3.1 快速创建目录

python 复制代码
import os

# 在当前目录下创建一个abc目录,如果存在则不创建
os.makedirs("./abc", exist_ok=True)

6.3.2 查询某个文件夹目录下的文件个数

python 复制代码
import os

os.makedirs("./abc", exist_ok=True)
print(len(os.listdir("./")))  # 返回当前目录下所有的文件列表

6.3.3 完整需求实现

  1. 当用户输入 img 的时候,弹出选择图片窗口
  2. 选择完毕图片解析图片,得到图片的后缀、长度、二进制
  3. 在当前目录下创建一个 data 目录,如果存在则不创建
  4. 在 data 目录下写入刚刚读取到的图片,保留原来的后缀,图片的命名为 图片_1、图片_2......
python 复制代码
import os
import tkinter
from tkinter import filedialog

content = input("请输入您的内容(img)")
if content == 'img':
    root = tkinter.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="选择图片",
        filetypes=[("图片", "*.png *.jpeg *.jpg")]
    )
    ext = os.path.splitext(file_path)[1][1:]  # 得到后缀
    with open(file_path, "rb") as f:
        img_data = f.read()
    size = len(img_data)

    os.makedirs("./data", exist_ok=True)
    flag = len(os.listdir("./data")) + 1

    with open(f"./data/图片_{flag}.{ext}", "wb") as f:
        f.write(img_data)

    print(f"图片已经存储到对应的位置:路径:./data/图片_{flag}.{ext}")

6.4 选择图片通过 Socket(客户端)

python 复制代码
import os.path
import socket
import tkinter
from tkinter import filedialog

print("1.创建客户端对象")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 9999))
print("2.客户端连接成功")

content = input("请输入您要的内容(img传图片)")
if content == 'img':
    root = tkinter.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename(
        title="请选择图片",
        filetypes=[("图片", "*.jpg *.jpeg *.png")]
    )
    ext = os.path.splitext(file_path)[1][1:]
    with open(file_path, "rb") as f:
        img_data = f.read()
    size = len(img_data)

    # 不但要传字节流,还要传后缀、长度
    client_socket.sendall(f"IMG|{ext}|{size}".encode("utf-8") + img_data)

    # 等待服务端图片接收完成,给一个图片上传成功的消息
    print(client_socket.recv(1024).decode("utf-8"))
    client_socket.close()

6.5 选择图片通过 Socket(服务端)

python 复制代码
import os
import socket

print("1.创建服务端socket对象")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print("2.设置端口释放")
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

print("3.绑定端口")
server_socket.bind(("127.0.0.1", 9999))

print("4.监听")
server_socket.listen(5)

print("5.接受客户端连接")
client_socket, client_info = server_socket.accept()
print("6.客户端连接成功", client_info)

# 阻塞方法: accept recv connect

# 第一次接收,得到一定是图片的元信息: IMG|jpeg|10086|x01
first_data = client_socket.recv(1024)

if first_data.startswith(b"IMG|"):
    parts = first_data.split(b"|", maxsplit=3)
    ext = parts[1].decode("utf-8")
    size = int(parts[2])
    img_data = parts[3]

    # 循环接收直到收完所有图片数据
    while len(img_data) < size:
        img_data += client_socket.recv(1024)

    os.makedirs("./data", exist_ok=True)
    flag = len(os.listdir("./data")) + 1

    with open(f"./data/图片_{flag}.{ext}", "wb") as f:
        f.write(img_data)

client_socket.send(f"图片上传成功,图片路径:./data/图片_{flag}.{ext}".encode("utf-8"))
client_socket.close()

7. Socket 编程基础模板(必背)


7.1 服务端基础模板

python

plain 复制代码
import socket

# 1. 创建 socket 对象(IPv4,TCP 字节流)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 设置端口复用(防止程序崩溃后端口被占用)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

# 3. 绑定 IP 和端口
server_socket.bind(("127.0.0.1", 9999))

# 4. 开启监听,设置最大挂起连接数
server_socket.listen(5)

# 5. 等待客户端连接(阻塞)
client_socket, client_info = server_socket.accept()

# 6. 接收数据(阻塞)
data = client_socket.recv(1024)
print(data.decode("utf-8"))

# 7. 发送数据
client_socket.send("消息内容".encode("utf-8"))

# 8. 关闭连接
client_socket.close()
# server_socket.close()  # 服务端一般不关,如需关闭则取消注释

7.2 客户端基础模板

python

plain 复制代码
import socket

# 1. 创建 socket 对象(IPv4,TCP 字节流)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 连接服务器
client_socket.connect(("127.0.0.1", 9999))

# 3. 发送数据
client_socket.send("消息内容".encode("utf-8"))

# 4. 接收数据(阻塞)
data = client_socket.recv(1024)
print(data.decode("utf-8"))

# 5. 关闭连接
client_socket.close()
相关推荐
j7~1 小时前
【C++】《C++二叉搜索树(BST)从入门到精通:概念、实现与Key/Value模型全解析》
开发语言·c++·学习·二叉搜索树
hehelm1 小时前
仿muduo库实现高并发服务器—Channel类
linux·服务器·开发语言·网络·c++
末代iOS程序员华仔1 小时前
Codex + Figma 生成 Objective‑C (UIKit) 完整工作流
c语言·开发语言·figma
ybdesire1 小时前
注入型漏洞以及agent-audit的检测原理
网络·安全·风险·智能体·ai安全
SomeB1oody1 小时前
【RustyML入门】7.3. 性能调优与并行
开发语言·后端·机器学习·rust·教程
Java后端的Ai之路1 小时前
01、Python - 设计模式介绍
java·人工智能·python·设计模式·通用
李白你好1 小时前
今日网络技术热点:【计算机网络 | 数据链路层8:CSMA/CD
网络·计算机网络
hehelm1 小时前
仿muduo库实现高并发服务器—EventLoop
linux·服务器·网络·c++
余额瞒着我当琳1 小时前
C++STL--list底层实现,迭代器分类,模拟list的迭代器封装、实现
java·开发语言·c++