第 4 篇 · 兵器谱网络编程 · Socket 从入门到实战行走江湖得有趁手的兵器。IP 是江湖坐标端口是暗号TCP/UDP 是传音入密的内功心法。本篇为你打造网络编程的「屠龙刀」客户端与服务端通信一剑封喉1. 网络设备的三要素IP地址网络环境中电脑的唯一标识通过 IP 可以找到某台计算机端口一台服务器上会有 N 个应用通过唯一的端口映射服务器中某个应用服务协议网络编程协议 — TCP / UDP1.1 域名解析输入www.baidu.com的时候会进入 DNS 解析本地 host 文件 → 本地 DNS 解析 → 远程 DNS 解析 → 获取 IP 地址 → 找到服务器8.8.8.8 是比较靠谱的 DNS 解析器绝大部分网站的端口是 80 端口默认不需要写1.2 IP 版本IPv4IP 地址量约 2 的 32 次方 ≈ 43 亿已接近耗尽IPv6IP 地址量约 2 的 128 次方几乎无穷无尽1.3 端口可用范围端口号的可用范围是0 ~ 65535作用是帮你找到在计算机上独一无二的服务。不要去抢占知名端口号0 ~ 1023。但也有一些不在这个范围内比如 MySQL 的 3306。IP 地址帮你锁定网络中的计算机设备端口号帮你在这个设备上找到对应的服务。2. TCP 与 UDP 的区别TCP传输控制协议面向连接、可靠UDP用户数据报协议无连接、不可靠都属于传输层协议。2.1 核心对比对比项TCPUDP连接特性面向连接三次握手建立连接四次挥手断开连接无连接发数据前不需要建立连接直接发包可靠性可靠传输确认应答、超时重传、丢包重传不丢包、不乱序不可靠不保证到达不保证顺序可能丢包、乱序流量/拥塞控制有流量控制滑动窗口、拥塞控制无只管发送不感知网络拥堵报文面向字节流数据是流没有报文边界面向数据包保留报文边界一个 UDP 包就是一条消息头部开销头部 20~60 字节开销大头部固定 8 字节开销小速度较慢握手、重传、确认带来额外开销速度快延迟低开销小适用场景文件传输、HTTP/HTTPS、SSH、数据库访问要求数据不能丢直播、视频通话、游戏、DNS允许少量丢包追求低延迟2.2 形象比喻TCP 打电话拨号三次握手→ 通话可靠传输→ 挂断四次挥手说话对方一定能收到顺序不变但是流程多UDP 寄明信片直接扔出去不用先联系对方明信片可能丢失、顺序乱掉但是发得快成本低3. TCP 的三次握手客户端主动发起连接服务端被动接收一共 3 次报文交互。3.1 三次握手流程第一次握手客户端 → 服务端【SYN】客户端发送SYN1随机初始序列号seqx客户端进入SYN_SENT状态。含义客户端请求建立连接。第二次握手服务端 → 客户端【SYNACK】服务端收到 SYN回复SYN1ACK1ackx1确认客户端自己随机初始序列号seqy服务端进入SYN_RCVD状态。含义同意连接同时也向客户端发起连接请求。第三次握手客户端 → 服务端【ACK】客户端收到 SYNACK回复ACK1acky1客户端进入ESTABLISHED已连接服务端收到这个 ACK 后也进入ESTABLISHED连接正式建立可以传数据。3.2 为什么是三次不是两次两次握手服务端收到 SYN 就直接建立连接。如果客户端旧的失效 SYN 延迟到达服务器服务器会建立无效连接白白占用资源。三次握手双方收发能力都得到验证客户端能发能收、服务端能发能收。第三次 ACK 可以拒绝过期的历史连接。SYN1 表示握手连接客户端发 SYN1发 seqx服务端回 SYN1发 ackx1seqy客户端回 acky14. TCP 的四次挥手任何一方都可以主动关闭下面以客户端主动关闭举例。4.1 四次挥手流程第一次挥手客户端 → 服务端【FIN】客户端发送FIN1sequ客户端进入FIN_WAIT_1。含义客户端不再发送数据但还可以接收数据。第二次挥手服务端 → 客户端【ACK】服务端回复ACK1acku1seqv服务端进入CLOSE_WAIT客户端收到 ACK 进入FIN_WAIT_2。含义收到关闭请求此时服务端还可以继续向客户端发送剩余数据。第三次挥手服务端 → 客户端【FIN】服务端数据全部发送完毕发送FIN1ACK1seqwacku1服务端进入LAST_ACK。含义服务端也没有数据要发了请求关闭。第四次挥手客户端 → 服务端【ACK】客户端回复ACK1ackw1sequ1客户端进入TIME_WAIT等待 2MSL服务端收到 ACK立刻进入CLOSED连接关闭。客户端等待2MSL之后也进入 CLOSED。FIN1 表示挥手连接客户端发 FIN1sequ服务端回 acku1 — 把剩余的数据传输完成服务端发 FIN1acku1seqw客户端回 ACK1ackw14.2 为什么挥手要 4 次握手只要 3 次握手时 SYN 和 ACK 可以合并成一个报文挥手时服务端收到 FIN可能还有业务数据没发完不能立刻发 FIN只能先回复 ACK等数据发完再发 FIN所以拆成两次总共 4 次。5. Socket 套接字5.1 Socket 套接字对象介绍使用 socket 来进行网络通信编程工具。# 引入socket模块importsocket# 创建一个socket对象server_socketsocket.socket(socket.AF_INET,socket.SOCK_STREAM)# 地址族选用 IPv4传输类型选用字节流print(server_socket)客户端学名 Client前端 / HTML / CSS / JS / Android / iOS / 小程序 / Flutter / RN服务端学名 ServerPython / Java / Go / Rust / Node.js / C#5.2 Socket 客户端和服务端通信流程分析服务端流程创建一个 socket 对象 — 大管家不负责和客户端直接对话bind()绑定 IP 和端口号 — 表示通过这个地址和端口能找到该服务listen(5)— 设置最大挂起的 socket 数量默认值为 1最大为 128一般设置为 5accept()— 接收客户端发起连接生成和客户端对话的 socket 对象返回客户端信息accept 是阻塞的它不走完下面谁也别走send()— 发送数据recv()— 接收数据发送和接收没有强制顺序要求但发送的数据一定是字节流close()— 释放当前和客户端对话的 socket5.3 二进制编解码操作strrucy今天是个好日子print(str.encode(gbk).decode(gbk))print(str.encode(utf-8).decode(utf-8))GBK 编码一个中文占 2 个字节英文字符占 1 个字节支持简体 繁体部分日文UTF-8 编码一个中文占 3 个字节英文字符占 1 个字节支持全球所有字符如果内容中不包含中文直接在字符串前加b即表示该字符为 bytes5.4 Socket 套接字对象创建服务端注意socket 对象默认是阻塞模式阻塞的方法有accept()服务端专用recv(bufsize)客户端/服务端connect()客户端专用阻塞当前代码未执行完毕绝不可能执行下一行代码importsocketprint(1.创建socket服务器对象)server_socketsocket.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_infoserver_socket.accept()print(6.客户端连接成功,client_info)print(7.给客户端发送数据)client_socket.send(我是你的服务器对象.encode(utf-8))print(8.接收客户端消息)dataclient_socket.recv(1024)print(f收到客户端发的消息:{data.decode(utf-8)})print(9.关闭和客户端通讯的连接)client_socket.close()# server_socket 一般不关5.5 Socket 套接字对象创建客户端importsocketprint(1.创建客户端socket对象)client_socketsocket.socket(socket.AF_INET,socket.SOCK_STREAM)print(2.通过ip端口连接服务器)client_socket.connect((127.0.0.1,9999))print(3.连接成功)contentclient_socket.recv(1024)print(f服务器给你说:{content.decode(utf-8)})send_contentinput(跟服务说)client_socket.send(send_content.encode(utf-8))print(关闭客户端连接)client_socket.close()5.6 实现无限聊天版本 — 服务端importsocket server_socketsocket.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_infoserver_socket.accept()print(f和客户端{client_info}连接成功)client_socket.send(你好我是rucy欢迎你来到我的聊天房间.encode(utf-8))# accept recvwhileTrue:dataclient_socket.recv(1024)ifnotdata:breakprint(f收到客户端:{client_info}的消息{data.decode(utf-8)})client_socket.close()5.7 实现无限聊天 — 客户端importsocket client_socketsocket.socket(socket.AF_INET,socket.SOCK_STREAM)client_socket.connect((127.0.0.1,9999))dataclient_socket.recv(1024)print(f服务器对你说{data.decode(utf-8)})whileTrue:send_contentinput(对服务器说(quit退出))ifsend_contentquit:breakclient_socket.send(send_content.encode(utf-8))client_socket.close()5.8 模拟多任务实现多人版本聊天服务端支持多人聊天的服务端。importsocket server_socketsocket.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(完成服务端初始化所有功能)# 模拟多任务 — 多进程 多线程 多协程whileTrue:client_socket,client_infoserver_socket.accept()client_socket.send(欢迎你兄弟来连接rucy的电脑.encode(utf-8))dataclient_socket.recv(1024)print(f收到客户端{client_info}消息:{data.decode(utf-8)})client_socket.close()5.9 模拟多任务多人版本聊天客户端importsocket client_socketsocket.socket(socket.AF_INET,socket.SOCK_STREAM)client_socket.connect((127.0.0.1,8888))dataclient_socket.recv(1024)print(f收到服务端消息{data.decode(utf-8)})contentinput(跟rucy说)client_socket.send(content.encode(utf-8))client_socket.close()6. 扩展功能6.1 文件选择器对象需求当输入 img 的时候自动弹出文件选择器自动过滤文件只选择图片。importtkinterfromtkinterimportfiledialog contentinput(对服务器说(img选择图片):)ifcontentimg:print(选择图片)roottkinter.Tk()root.withdraw()# 防止弹出多余小窗口file_pathfiledialog.askopenfilename(title请选择图片,filetypes[(图片,*.png;*.jpg;*.jpeg;*.gif)])print(file_path)root.destroy()6.2 解析图片获取图片的字节流及详细信息importosimporttkinterfromtkinterimportfiledialog contentinput(输入img选择图片)ifcontentimg:roottkinter.Tk()root.withdraw()file_pathfiledialog.askopenfilename(title请选择图片,filetypes[(图片,*.jpg *.jpeg *.png)])extos.path.splitext(file_path)[1][1:]# 拿到图片的扩展名withopen(file_path,rb)asf:img_dataf.read()# 拿到图片的二进制数据print(f拿到了图片的扩展名{ext}, 长度大小:{len(img_data)}, 二进制流数据:{img_data})# TCP协议 二进制字节流 jpeg|42196|image_data6.3 选择一张图片存储到 data 目录下6.3.1 快速创建目录importos# 在当前目录下创建一个abc目录如果存在则不创建os.makedirs(./abc,exist_okTrue)6.3.2 查询某个文件夹目录下的文件个数importos os.makedirs(./abc,exist_okTrue)print(len(os.listdir(./)))# 返回当前目录下所有的文件列表6.3.3 完整需求实现当用户输入 img 的时候弹出选择图片窗口选择完毕图片解析图片得到图片的后缀、长度、二进制在当前目录下创建一个 data 目录如果存在则不创建在 data 目录下写入刚刚读取到的图片保留原来的后缀图片的命名为 图片_1、图片_2……importosimporttkinterfromtkinterimportfiledialog contentinput(请输入您的内容(img))ifcontentimg:roottkinter.Tk()root.withdraw()file_pathfiledialog.askopenfilename(title选择图片,filetypes[(图片,*.png *.jpeg *.jpg)])extos.path.splitext(file_path)[1][1:]# 得到后缀withopen(file_path,rb)asf:img_dataf.read()sizelen(img_data)os.makedirs(./data,exist_okTrue)flaglen(os.listdir(./data))1withopen(f./data/图片_{flag}.{ext},wb)asf:f.write(img_data)print(f图片已经存储到对应的位置路径./data/图片_{flag}.{ext})6.4 选择图片通过 Socket客户端importos.pathimportsocketimporttkinterfromtkinterimportfiledialogprint(1.创建客户端对象)client_socketsocket.socket(socket.AF_INET,socket.SOCK_STREAM)client_socket.connect((127.0.0.1,9999))print(2.客户端连接成功)contentinput(请输入您要的内容(img传图片))ifcontentimg:roottkinter.Tk()root.withdraw()file_pathfiledialog.askopenfilename(title请选择图片,filetypes[(图片,*.jpg *.jpeg *.png)])extos.path.splitext(file_path)[1][1:]withopen(file_path,rb)asf:img_dataf.read()sizelen(img_data)# 不但要传字节流还要传后缀、长度client_socket.sendall(fIMG|{ext}|{size}.encode(utf-8)img_data)# 等待服务端图片接收完成给一个图片上传成功的消息print(client_socket.recv(1024).decode(utf-8))client_socket.close()6.5 选择图片通过 Socket服务端importosimportsocketprint(1.创建服务端socket对象)server_socketsocket.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_infoserver_socket.accept()print(6.客户端连接成功,client_info)# 阻塞方法: accept recv connect# 第一次接收得到一定是图片的元信息: IMG|jpeg|10086|x01first_dataclient_socket.recv(1024)iffirst_data.startswith(bIMG|):partsfirst_data.split(b|,maxsplit3)extparts[1].decode(utf-8)sizeint(parts[2])img_dataparts[3]# 循环接收直到收完所有图片数据whilelen(img_data)size:img_dataclient_socket.recv(1024)os.makedirs(./data,exist_okTrue)flaglen(os.listdir(./data))1withopen(f./data/图片_{flag}.{ext},wb)asf:f.write(img_data)client_socket.send(f图片上传成功图片路径./data/图片_{flag}.{ext}.encode(utf-8))client_socket.close()7. Socket 编程基础模板必背7.1 服务端基础模板pythonimport socket # 1. 创建 socket 对象IPv4TCP 字节流 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 客户端基础模板pythonimport socket # 1. 创建 socket 对象IPv4TCP 字节流 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()