计算机网络课设实战:从协议设计到并发实现的电子图书馆系统
简介计算机网络课程设计中的电子图书馆完整设计方案面向网络工程或计算机相关专业学生可用于课设报告撰写、网络拓扑设计与仿真配置参考。方案针对1000M主干网、100M到点的校园级站点要求规划100个以上站点、划分4个以上子网并围绕DNS、DHCP、WEB、FTP等网络服务展开同时兼顾吞吐量、冗余度和网络存储架构强调电子图书馆在大并发访问与长期数据保存场景下的特殊需求。压缩包内含1份doc格式文档约637KB目录涵盖前言、需求分析、拓扑结构、硬件选型、服务软件配置与测试等模块层次清晰可直接对照章节完成设计任务文档中还整理了设备选型与配置命令片段方便快速搭建实验环境并验证方案。这份资料已有941人学习下载适合需要从零搭建电子图书馆网络方案或补充课设细节的学生使用。1. 先把课程设计题目读透电子图书馆不是网页是一台会“说话”的服务器如果你正在做计算机网络课设并且选题是“电子图书馆设计”我猜你第一反应是打开前端模板先画一个漂亮的网页。但我要泼一盆冷水计算机网络课设的评分点不在页面而在你的程序有没有真的“在网络上交换数据”。电子图书馆设计的本质是让一台服务器同时服务多个客户端完成登录、查询、借阅、归还这一套业务流程——客户端和服务端之间通过你自定义的协议通信而不是大家共用一台电脑上的同一个程序。换句话说你要交付的不是一个网站而是一套基于 TCP/IP 的 C/S 架构系统。这篇文章我按自己做过的一套方案来讲从协议设计、选型理由到能跑起来的最小代码、并发和数据落地的细节再到课设答辩时老师最爱追问的坑最后给你几个加分项。适合正在写计算机网络课设、需要交源码和文档又不想只糊一个网页的同学。2. 先定协议再做功能为什么我选 TCP 和自绘文本协议2.1 电子图书馆为什么必须用 TCP借阅操作不能丢包很多课设题目里只写了“电子图书馆设计”没规定传输层协议。这时候第一选择应当是 TCP。原因很简单图书馆系统的核心操作是借书和还书这两个操作在业务上属于“有状态变更”——借出一本书数据库里的库存要减一还回来库存要加一。如果用 UDP客户端发出“借阅《计算机网络》”的报文后报文在网络中丢了服务端没有收到客户端还以为自己借成功了。这在演示环节会直接翻车老师看到你点击借阅后图书列表里那本书还在而你自己的借阅记录里却多了一条。TCP 的可靠传输正好解决这个问题。它自带确认、重传、排序机制客户端发出去的数据服务端只要回了确认客户端就知道服务端确实处理了。从计算机网络课程的角度看这也能让你在答辩时站得住脚你能讲清楚为什么选 TCP 而不是 UDP能提到三次握手建立连接、四次挥手释放连接能解释序列号和确认号的作用。这些都是《计算机网络自顶向下》和《计算机网络》王道复习里反复出现的考点现在落到了实处。2.2 自绘应用层协议还是直接套 HTTP两种方案的真实取舍接下来是应用层协议。这里有个常见的误区——很多同学觉得自己用 Java 或 Python 写 Socket 通信协议就得自己发明一套。其实不一定。电子图书馆课设完全可以套 HTTP 协议让浏览器直接访问服务端返回 HTML 或 JSON。这种做法开发效率高前端资源丰富调试也方便。但它有两个问题第一HTTP 请求的格式是固定的你得处理请求头、请求体、状态码这些细节代码量会偏大第二答辩时老师可能会问“HTTP 协议本身属于哪一层”“它还依赖哪一层协议”如果你答不上来反而扣分。我自己的做法是折中服务端监听一个固定端口客户端用 Java/Python 原生 Socket 连接应用层协议用自定义的文本协议——每条消息以换行符结尾第一行是请求命令后续行是参数以空行结束。这样有几个好处报文格式自己定义你能把字段设计讲清楚调试时用“网络调试助手”这种工具直接敲文本就能测不需要写完整客户端最重要的是答辩时你能完整讲出“从传输层到应用层我在哪一层做了什么”。对于课程设计这个场景这种“自绘文本协议”其实是最稳妥的选型。方案开发效率答辩深度代码量课设推荐度纯 HTTP 浏览器高低老师可问的少中不推荐HTTP JSON 接口高中需解释 HTTP 细节中可选TCP 自绘文本协议中高完整覆盖传输层与应用层低推荐2.3 消息格式设计让报文能被老师一眼看懂协议设计是这个课设的核心交付物之一。我建议这样设计报文LOGIN|student001|123456 BOOK_QUERY|计算机网络 BORROW|BOOK001|student001 RETURN|BOOK001|student001每条报文以换行结束字段用竖线分隔第一个字段是操作码后面是参数。服务端返回也一样比如QUERY_OK|BOOK001|计算机网络|谢希仁|3|可借。这么做的好处是报文本身就是文本用抓包工具 Wireshark 能看到明文内容调试和答辩演示都很直观字段分隔符统一解析逻辑简单不需要处理复杂编码。协议字段的设计我一般固定在“操作码 参数列表”这个格式上不放校验和、长度字段这些——因为 TCP 已经保证了传输可靠性应用层不需要重复设计。但我会在报文末尾加一个\r\n作为结束标识同时在解析时忽略空行这样即使客户端多发了几个空白行服务端也不会解析出错。这套设计在答辩时能讲五分钟从传输层为什么可靠到应用层为什么只关心内容。3. 从零搭一个能跑的最小系统登录、检索、借阅三条主链路3.1 服务端主循环一个单线程版先跑通逻辑我先把最核心的服务端代码给你。这个版本不做并发只做单线程循环目的是先把业务流程跑通。理解了它再往上加多线程。import socket # 服务端配置 SERVER_HOST 0.0.0.0 # 监听所有网卡方便局域网内其他机器连入 SERVER_PORT 8080 # 端口号建议用 1024 以上的非特权端口 BUFFER_SIZE 4096 # 单次 recv 缓冲区大小单位字节 def handle_request(data: str) - str: 根据客户端发来的文本请求返回对应的响应报文 parts data.strip().split(|) cmd parts[0] if cmd LOGIN: # 第三部分用户名第四部分密码 if parts[1] student001 and parts[2] 123456: return LOGIN_OK|欢迎登录 return LOGIN_FAIL|用户名或密码错误 if cmd BOOK_QUERY: # 模拟图书数据查得到就返回数量 if parts[1] 计算机网络: return QUERY_OK|BOOK001|计算机网络|谢希仁|3本可借 return QUERY_OK|无此图书 if cmd BORROW: return BORROW_OK|借阅成功 return UNKNOWN_CMD|未知操作 # 创建 TCP socket 并绑定端口 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((SERVER_HOST, SERVER_PORT)) server.listen(5) # 最大等待队列长度超过则拒绝新连接 print(f电子图书馆服务端已启动监听端口 {SERVER_PORT}) while True: conn, addr server.accept() print(f客户端连接: {addr}) with conn: raw conn.recv(BUFFER_SIZE).decode(utf-8) if not raw: break response handle_request(raw) conn.sendall(response.encode(utf-8)) conn.close()这段代码的逻辑链路是服务端启动后进入死循环accept()阻塞等待客户端连接拿到连接后用recv()读取客户端发来的报文交给handle_request()解析处理再把结果sendall()回给客户端。注意recv()的返回值是字节数组必须decode(utf-8)转成字符串再解析发送时反过来字符串要先encode(utf-8)再发。参数说明里有两个地方容易出错。SERVER_HOST用0.0.0.0而不是127.0.0.1是为了让局域网内其他机器也能连接答辩现场如果老师用自己的手机或另一台笔记本连你的电脑做演示这个设置就是必需的如果只在本机测试也可以改成127.0.0.1。BUFFER_SIZE设成 4096 一般够用图书查询请求不会超过这个长度——但如果你后面加了“图书简介”这种长文本字段就要改成更大值或者做消息边界处理否则长报文会被截断解析报错。这个问题我在第 5 章会再细讲。3.2 客户端封装把请求和响应做成一个可复用函数服务端有了客户端不能每次都用调试助手去连。我给你一个最小客户端它可以独立运行也可以被后续的 GUI 界面调用。import socket SERVER_HOST 127.0.0.1 SERVER_PORT 8080 def send_request(payload: str) - str: 发送一条请求报文并接收服务端响应 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client: client.settimeout(5) # 5 秒超时防止服务端无响应时卡死 client.connect((SERVER_HOST, SERVER_PORT)) client.sendall(payload.encode(utf-8)) response client.recv(4096).decode(utf-8) return response if __name__ __main__: # 测试登录 login_resp send_request(LOGIN|student001|123456) print(登录响应:, login_resp) # 测试查询 query_resp send_request(BOOK_QUERY|计算机网络) print(查询响应:, query_resp)这个客户端每调用一次send_request()就建立一次 TCP 连接、发送数据、接收数据、关闭连接。每次请求都走完整的“三次握手-数据传输-四次挥手”虽然效率不如长连接但逻辑简单且每次连接的状态自然结束不会出现连接未关闭导致服务端资源泄漏。settimeout(5)这个参数很重要如果没有它服务端崩溃或网络不通时recv()会一直阻塞程序像死了一样。加了超时5 秒后抛出socket.timeout异常你可以捕获后在界面上提示“服务端无响应”。代码里的with socket.socket(...) as client写法保证执行完请求后 socket 自动关闭。这是 Python 的习惯用法比手动client.close()更不容易漏。在这里你会直观体会到“TCP 连接是有状态的”——每次connect()对应accept()每次sendall()对应recv()这个对应关系在计算机网络实验一里你反复做过现在完整落到一套业务系统里。这里你可以用自己的账号密码后面替换来测。3.3 数据库表结构课设文档里不能缺失的五张表单靠内存模拟数据课程设计文档写不厚答辩时也经不起“重启后数据还在吗”这种追问。所以数据库必须上连接数据库我建议用 MySQL理由有三点它是网络数据库服务端和数据库可以分两台机器跑这样你能在文档里写“系统采用三层架构”其次数据库课程设计 mysql是很多院校指定环境兼容性不用操心最后MySQL 的表结构设计你用标准 SQL 就能建不用额外引入文件型数据库的本地路径概念。我给出最小但完整的表结构五张表覆盖图书、用户、借阅三块业务。-- 图书表 CREATE TABLE book ( id CHAR(8) PRIMARY KEY, -- 图书编号如 BK000001 title VARCHAR(100) NOT NULL, -- 书名 author VARCHAR(50) NOT NULL, -- 作者 total INT DEFAULT 1, -- 总藏书量 available INT DEFAULT 1 -- 当前可借数量必须 total ); -- 用户表 CREATE TABLE user ( user_id VARCHAR(20) PRIMARY KEY, -- 学号/工号 password VARCHAR(50) NOT NULL, -- 密码明文存储仅课设够用 name VARCHAR(50) NOT NULL -- 姓名 ); -- 借阅记录表 CREATE TABLE borrow ( id INT AUTO_INCREMENT PRIMARY KEY, -- 流水号自增 book_id CHAR(8) NOT NULL, -- 外键指向 book.id user_id VARCHAR(20) NOT NULL, -- 外键指向 user.user_id borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 借出时间 return_time DATETIME NULL, -- 归还时间NULL 表示未还 UNIQUE KEY uk_open (book_id, user_id, return_time) -- 防止重复借同一本未还的 ); -- 管理员表可选 CREATE TABLE admin ( admin_id VARCHAR(20) PRIMARY KEY, password VARCHAR(50) NOT NULL ); -- 分类表可选用于按分类浏览 CREATE TABLE category ( cid INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE );这几张表的关联逻辑很关键borrow表是book和user的关联表一个学生可以借多本书一本书可以被多个学生借只要库存够这是典型的多对多关系。available字段是冗余设计的产物——它可以通过total减去未归还的借阅记录算出来但我建议保留这个字段因为每次借书时查一次available 0再更新比每次实时统计借阅记录要快得多代码也更简单。UNIQUE KEY uk_open是为了防止同一学生对同一本未归还图书重复借阅这个约束在并发场景下尤其重要第 4 章我会讲为什么。4. 并发与数据落地线程模型、超时参数和三个必调的连接缺陷4.1 单线程转多线程用线程池而不是每连接一线程你现在的服务端是单线程的while True循环里accept()一个就处理一个处理完才接受下一个连接。这在演示时看不出问题但只要老师拿两台电脑同时登录第二个客户端的连接请求就会阻塞——第一个连接没关闭accept()就一直在等待它的数据。解决办法是多线程但多线程也有两种写法我直接推荐线程池。from concurrent.futures import ThreadPoolExecutor SERVER_PORT 8080 MAX_WORKERS 10 # 最大同时处理的客户端连接数 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((0.0.0.0, SERVER_PORT)) server.listen(128) # 请求等待队列调大避免高峰期丢连接 # 线程池最多同时 10 个连接在处理 pool ThreadPoolExecutor(max_workersMAX_WORKERS) def handle_connection(conn, addr): with conn: while True: raw conn.recv(4096).decode(utf-8) if not raw: break response handle_request(raw) conn.sendall(response.encode(utf-8)) while True: conn, addr server.accept() pool.submit(handle_connection, conn, addr)关键差异在pool.submit()——它把处理任务丢给线程池主线程立刻回到accept()继续等待新连接。MAX_WORKERS10意味着同一时刻最多 10 个连接在并发处理超过的会排队这避免了“每连接一线程”在大量连接时创建几百个线程拖垮系统的问题。listen(128)也相应调大它代表内核中尚未被accept()接收的连接队列长度如果演示现场有很多同学同时连你的服务端这个值小了会直接丢连接。这里要强调 TCP 粘包与消息边界的问题我上面的代码里handle_connection是while True循环读理论上客户端连续发两条BORROW|...报文TCP 可能把它们合并成一个包送到服务端recv()一次就拿到了两条报文也可能只拿到半条。这是计算机网络课设里最经典的坑俗称粘包。我的解决办法是协议规定每条报文以换行符结尾服务端把recv()到的数据先拼进缓冲区遇到\n才解析一条完整消息。4.2 数据库操作要加事务否则并发借阅会让库存变成负数多线程写数据库时会遇到并发问题。假设一本书只有 1 本可借两个学生同时提交借阅请求两个线程都先执行SELECT available FROM book WHERE idBK001都查到available1都认为可以借然后都执行UPDATE book SET availableavailable-1。结果库存变成 -1但两个人都借成功了。这在课设演示时就是致命翻车现场。解决办法是数据库事务加行锁让“检查库存”和“扣减库存”成为一个原子操作。MySQL 的 InnoDB 引擎支持行级锁用SELECT ... FOR UPDATE就能实现。START TRANSACTION; -- 锁定该图书的行其他事务必须等待 SELECT available FROM book WHERE id BK001 FOR UPDATE; -- 如果 available 0则插入借阅记录并扣减库存 INSERT INTO borrow (book_id, user_id) VALUES (BK001, student001); UPDATE book SET available available - 1 WHERE id BK001; COMMIT;这段 SQL 里最关键的是FOR UPDATE它在事务内锁住了book表中idBK001这一行。另一个线程执行同一条查询时会被阻塞直到第一个事务提交或回滚。这样两个借阅请求就串行化了不会出现同时读到available1的情况。注意INSERT INTO borrow那一步又触发了UNIQUE KEY uk_open约束——如果这个学生已经借了这本书且未还这里会直接报主键冲突事务回滚库存不会扣减。这是双重保险。我在课设文档里会建议把“借书”和“还书”两个操作都封装成存储过程或者至少写成固定 SQL 片段而不是在 Python 代码里拼接字符串。一是防 SQL 注入二是方便你在文档里截图“事务隔离级别”的设置过程答辩时老师能看到你是真的理解了并发控制。4.3 超时与心跳防止僵死连接占用线程池线程池只有 10 个 worker如果某个客户端连上后不发数据也不断开它的recv()会一直阻塞这个线程就被白占了。更麻烦的是有的客户端程序异常退出TCP 没来得及发 FIN 报文服务端还以为连接活着。这两个情况都会让线程池慢慢耗尽最后所有新连接全部排队服务端“卡死”。我的习惯是在客户端send_request()里保留settimeout(5)并且服务端也设置接收超时。def handle_connection(conn, addr): conn.settimeout(30) # 30 秒内没有收到任何数据判定超时 with conn: try: while True: raw conn.recv(4096).decode(utf-8) if not raw: break response handle_request(raw) conn.sendall(response.encode(utf-8)) except socket.timeout: print(f连接超时关闭: {addr})conn.settimeout(30)是服务端最后的防线如果 30 秒内客户端没有任何数据过来recv()抛socket.timeout异常捕获后退出循环、关闭连接线程池释放一个 worker。这个参数在答辩演示时尤其重要——老师可能拿着你的客户端程序到处点或者连着 WiFi 信号不好连接假死没有超时机制你的服务端就会越跑越慢。注意超时时间别设太短学生可能输密码要思考一会儿但 30 秒足够宽松了。4.4 参数配置清单把下面的值写进你的课程设计文档参数推荐值设置位置说明与踩坑提醒服务端监听地址0.0.0.0服务端 bind只填127.0.0.1则只能本机访问端口号8080 及以上服务端 bind低于 1024 需要 root 权限演示机经常没权限接收缓冲区4096 / 8192服务端 recv协议报文变长时缓冲区不足会截断出现“半包”客户端超时5 秒客户端 settimeout设太长卡界面设太短服务端稍慢就误报故障服务端超时30 秒服务端 settimeout防止死连接占光线程池线程池大小10ThreadPoolExecutor课设规模 10 够用不要设 100listen 队列128服务端 listen演示现场人多时默认 5 会丢连接MySQL 事务隔离READ COMMITTED数据库REPEATABLE READ 会导致间隙锁问题这张表可以直接复制到你的课程设计说明书“系统参数配置”一节每一项都能讲出理由。尤其是端口号这条我曾经见过同学在 Linux 实验室的机器上选 80 端口结果普通用户没有权限绑定启动直接报Permission denied换 8080 才好——这个细节写在文档里老师会觉得你确实踩过坑。5. 课设排错与避坑五个高频翻车点照着查就能救回来5.1 客户端连不上服务端防火墙、IP 和0.0.0.0三连问现象客户端运行后提示“连接被拒绝Connection refused”或“超时”服务端窗口没有打印任何新连接日志。原因最常见的有三个按概率排序第一服务端bind()的是127.0.0.1客户端连的是局域网 IP跨主机访问被拒第二Windows/Windows 防火墙拦了 8080 端口的入站连接服务端程序自己的弹窗没点“允许”第三两台机器不在同一网段或实验室 WiFi 开了“AP 隔离”。解决先把服务端监听地址改成0.0.0.0重启再关掉防火墙测试或者进“高级设置”放行 TCP 8080 端口最后在客户端机器上用命令ping 服务端IP验证网络通不通。注意不能用“关闭防火墙”当最终方案课设文档里要写“已配置防火墙入站规则放行 8080 端口”这是加分项。我遇到过一次最诡异的两台电脑 IP 都能 ping 通但客户端连不上排查半天发现是服务端监听的是 IPv6 的::而客户端解析域名时只走了 IPv4。5.2 字符串编码不一致recv()出来的是乱码现象客户端发送的登录用户名是student001服务端收到的却是student001前面多了一个\ufeff之类的不可见字符导致数据库查不到、登录失败。原因客户端的 Python 文件保存时用了带 BOM 的 UTF-8 编码或者某个字段从文件读取时带了 BOM 头也可能是客户端用gbk编码发送服务端用utf-8解码。解决统一所有源文件用 UTF-8 无 BOM 格式保存在 Python 里对收到的数据先解码再用解码前可以raw.encode(latin-1, errorsignore)过滤掉非法字符。最稳妥的做法是在协议明文里约定“所有文本一律 UTF-8”然后在客户端sendall()和服务端decode()处写死编码不要用系统默认编码。我们实验室的机器默认编码是GBK在我自己电脑上跑得好好的代码拷到实验室电脑上第一次运行就乱码这就是编码玄学不写死必翻车。5.3 recv 缓冲区不够导致报文截断查询结果只剩一半现象客户端发BOOK_QUERY服务端返回的图书信息很长客户端只收到前半段解析时因为字段数不够直接报IndexError。原因recv(4096)的单次接收上限小于服务端sendall()发出去的报文长度。TCP 是流协议sendall()发 8000 字节recv()一次最多接 4096剩下的留在内核缓冲区里等下一次 recv。如果客户端只 recv 一次就处理必然半包。解决两个方向要么把BUFFER_SIZE提到 8192 或 16384要么在应用层做消息边界处理——按换行符切割积累到完整一行再返回。我建议直接用后者因为图书简介这种字段会越来越长缓冲区设多大都不保险。代码里维护一个buffer变量每次recv()后拼进去循环判断\n在不在里面在就切出来完整消息处理。5.4 并发借阅把库存借成负数可用数量的“竞态条件”现象演示时两台电脑同时借同一本书服务端日志显示两次都借阅成功数据库里available变成了 -1。原因服务端代码里“先查 available 再更新 available”是两步操作多线程环境下两个线程同时执行查询都得到available1然后都执行减一最后变成 -1。这是典型的“读改写”竞态条件。解决用第 4 章的SELECT ... FOR UPDATE加事务把两步合并成原子操作或者更简单直接把UPDATE book SET available available - 1 WHERE id BK001 AND available 0作为条件更新这个 SQL 本身带原子性不需要显式锁行。条件更新返回受影响行数为 0 就说明库存不足借阅失败。这个方法我在课设文档里重点写过老师在这个问题上追问的概率很高。5.5 服务端端口被占用前一次调试的程序没退干净现象服务端启动时报OSError: [Errno 98] Address already in use但你觉得没有其他程序在跑。原因之前有一次服务端异常退出比如按了 CtrlC操作系统的 TIME_WAIT 状态让端口仍在保留期或者一个残留的 Python 进程还占着进程。解决Linux 上先ss -lntp | grep 8080找到进程 PIDkill -9 PID杀掉Windows 上先netstat -ano | findstr :8080再用taskkill /PID 进程号 /F。如果你只是自己调试可以在 bind 之前加一行server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)允许端口立即重用。但注意这个设置解决的是 TIME_WAIT 问题如果你有两个服务端进程同时在监听它救不了你——还是得先杀进程。6. 给验收老师看的加分项用 Wireshark 抓包给协议挑毛病到了最后一步功能都跑通、坑也踩得差不多了但课设的分数高低往往取决于你有没有“超出题目要求的自觉”。我自己最推荐的加分动作是用 Wireshark 抓包验证你的协议设计并针对抓包结果做一次小重构。打开 Wireshark选择客户端连着服务端的那个网卡过滤条件填tcp.port 8080然后让客户端执行一次完整的“登录 → 查询 → 借阅”操作。你会看到三组明显的 TCP 流每组流的开头都能看到三次握手的SYN、SYN-ACK、ACK报文。点击其中一条右侧面板能看到 TCP 层的序列号、确认号、窗口大小。这时候你可以截一张图写进课程设计报告“系统验证”章节配一行说明“由图可见客户端每次请求都建立新的 TCP 连接符合预期设计。三次握手过程正常数据段携带应用层报文。”这句话足以证明你是真的理解传输层协议而不是只调通了代码。抓包还会暴露一个我之前提过的问题粘包。如果客户端连续快速发两条请求Wireshark 里可能看到两个 HTTP 式报文的PSH, ACK合并成一个包发送。这说明我的协议设计有改进空间——在每条报文前加一个Content-Length头服务端先读长度再读内容。你可以把这个重构作为“系统改进与展望”写在文档里表示你已认识到问题并给出了方案。这在答辩时是很有力的主动表现。最后一个能立住的加分技巧是写一个简单的压力测试脚本启动 20 个线程每个线程循环执行 50 次图书查询记录平均响应时间和错误率。你会发现线程池设置为 10 时平均响应时间可能在几十毫秒到几百毫秒之间波动这是正常的——排队等待占了大头。把这个结果做成表格附在文档里就能回答老师常问的“你有考虑过系统性能吗”。我自己当年把结果跑出来时还比较意外20 个并发客户端下线程池 10 比线程池 30 的响应时间还稳定因为线程切换开销更小——这种反直觉的数据往往比一个漂亮界面更能让老师记住你的课设。希望帮到你祝你的电子图书馆顺利通过验收。本文还有配套的精品资源点击获取