网络编程之服务器模型与UDP编程
一、服务器模型
在网络通信中,通常要求一个服务器连接多个客户端
为了处理多个客户端的请求,通常有多种表现形式
1、循环服务器模型
一个服务器可以连接多个客户端,但同一时间只能连接并处理一个客户的请求
socket()
结构体
bind()
listen()
while(1)
{
accept();
while(1)
{
recv()
}
}
2、并发服务器模型
一个服务器可以同时处理多个客户端请求
2.1 多线程
每有一个客户端连接就去创建一个新的线程用于通信
为什么要使用多线程?-->解决服务器不能同时与多个客户通信
什么时间创建新的子线程?-->在accept之后(因为accept可以循环等待与多个客户建立连接,但无法同时与多个客户通信)
主线程 ---->用于循环等待客户的连接
子线程 ----->用于与客户通信
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
// $$ 服务器端 $$ void *handler_thread(void *arg)
{char buf[128] = "";int ret;int acceptfd = *(int *)arg;while (1){/*接收消息*/ret = recv(acceptfd, buf, 128, 0); // 0-->相当于read(acceptfd,buf,128)if (ret < 0){perror("recv err");return NULL;}else if (ret == 0){printf("客户退出\n");break;}else{printf("%s 接收成功\n", buf);memset(buf, 0, sizeof(buf));}}close(acceptfd);pthread_exit(NULL);
}int main(int argc, char const *argv[])
{/*创建流式套接字*/int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("创建套接字成功\n");}/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/// 服务器的网络信息通过一个系统定义好的结构体来描述struct sockaddr_in saddr; // 定义一个结构体变量saddr.sin_family = AF_INET; // 确定协议族-->IPv4saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址/*绑定套接字*/int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (t1 < 0){perror("bind err");return -1;}else{printf("绑定套接字成功\n");}/*监听*/int t2 = listen(sockfd, 6); // 将默认的主动套接字变为被动套接字if (t2 < 0){printf("listen err");return -1;}else{printf("监听中\n");}pthread_t tid;int acceptfd;// 定义一个结构体变量来存接收到的客户信息struct sockaddr_in caddr;// len是记录客户信息的结构体的大小int len = sizeof(caddr);while (1){/*阻塞等待接收客户端的连接请求,并将连接成功的客户端信息写入到结构体变量caddr中*/acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){printf("accept err");return -1;}else{printf("等待接收客户端请求\n");}printf("客户IP:%s 端口号:%d \n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));pthread_create(&tid, NULL, handler_thread, &acceptfd);pthread_detach(tid);}/* 关闭套接字 */close(sockfd);return 0;
}
2.1 多进程
每有一个客户端连接就去创建一个新的进程用于通信
为什么要使用多进程?-->解决服务器不能同时与多个客户通信
什么时间创建新的子进程?-->在accept之后(因为accept可以循环等待与多个客户建立连接,但无法同时与多个客户通信)
主进程 ---->用于循环等待客户的连接
子进程 ----->用于与客户通信
2.3 IO多路复用
创建两个表fd_set rfds, tempfds
清空表 FD_ZREO(&rfds) ; FD_ZREO(&tempfds);
创建流式套接字socket() 得到sockfd
指明网络信息struct sockaddr_in saddr;
绑定套接字bind()
监听listen()
将想要监听的文件描述符放入表中 FD_SET(sockfd,&rfds);FD_SET(0,&rfds);
//设置变量max用来保存文件描述符最大值
while(1){
tempfds=rfds;
//使用select轮询监听tempfds表
select(max+1,&tempfds,......)
//判断是哪个文件描述符发生了变化,并做出相应处理
if(FD_ISSTE(sockfd,&tempfds);)
{
acceptfd=accept();
if(acceptfd>max)
max=acceptfd;
FD_SET(acceptfd,&rfds);
}
for(int i=0;i<max;i++)
{
if(FD_ISSET(acceptfd))
{
//通信
if(出错)
{
return -1;
}
else if(有客户退出)
{
close(i);
FD_CLR(i,&rfds);
while(FD_ISSET(max,&rfds)==0) //看是否需要更新max
max--;
}
else
{
收发消息
}
}
}
}
3、UDP
3.1通信流程
3.2 函数接口
(1) 接收信息recvfrom()
recvfrom的最后连两个参数与accept最后两个参数起到了相同的作用--->获取消息发送方的信息
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:sockfd:套接字描述符buf:接收缓存区的首地址len:接收缓存区的大小flags:0src_addr:发送端的网络信息结构体的指针addrlen:发送端的网络信息结构体的大小的指针
返回值:成功接收的字节个数失败:-10:客户端退出
(2) 发送信息 sendto()
sendto的最后连两个参数与connect最后两个参数传参一样---->确定消息要发给谁
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:sockfd:套接字描述符buf:发送缓存区的首地址len:发送缓存区的大小flags:0src_addr:接收端的网络信息结构体的指针addrlen:接收端的网络信息结构体的大小
返回值: 成功发送的字节个数失败:-1
普通通信:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>/*消息发送方(无bind)*/int main(int argc, char const *argv[])
{/*创建数据报套接字*/int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("创建套接字成功\n");}/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/// 服务器的网络信息通过一个系统定义好的结构体来描述struct sockaddr_in saddr; // 定义一个结构体变量saddr.sin_family = AF_INET; // 确定协议族-->IPv4saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址char buf[128]="";while (1){fgets(buf,sizeof(buf),stdin);if(buf[strlen(buf)-1]=='\n')buf[strlen(buf)-1]='\0';if (strcmp(buf,"quit")==0){break;}sendto(sockfd, buf, 128, 0,(struct sockaddr *)&saddr,sizeof(saddr)); // 0-->相当于read(acceptfd,buf,128)}return 0;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>/*消息接收方(有bind)*/int main(int argc, char const *argv[])
{/*创建数据报套接字*/int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("创建套接字成功\n");}/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/// 服务器的网络信息通过一个系统定义好的结构体来描述struct sockaddr_in saddr; // 定义一个结构体变量saddr.sin_family = AF_INET; // 确定协议族-->IPv4saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 确定服务器IP地址/*绑定套接字*/int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (t1 < 0){perror("bind err");return -1;}else{printf("绑定套接字成功\n");}struct sockaddr_in caddr;int len=sizeof(caddr);char buf[128]="";int ret;while (1){ret = recvfrom(sockfd, buf, 128, 0,(struct sockaddr *)&caddr,&len); // 0-->相当于read(acceptfd,buf,128)if (ret < 0){perror("recv err");return -1;}else{printf("IP为:%s,端口号为:%d的客户发来了:%s \n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buf);memset(buf, 0, sizeof(buf));}}return 0;
}
4、广播与组播
广播:
● 前面介绍的数据包发送方式只有一个接受方,称为单播
● 如果同时发给局域网中的所有主机,称为广播
● 只有用户数据报(使用UDP协议)套接字才能广播
● 一般被设计成局域网搜索协议
● 广播地址:局域网中主机号最大的一个
注意要在同一网段下!!!
缺点:
广播方式发给所有的主机,过多的广播会大量的占用网络带宽,造成广播风暴,影响正常的通信
广播风暴: 网络长时间被大量的广播数据包所占用,使正常的点对点通信无法正常进行,其外在表现为网络速度奇慢无比,甚至导致网络瘫痪
补充:
int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen)
功能:获得/设置套接字属性
参数:sockfd:套接字描述符level:协议层optname:选项名optval:选项值optlen:选项值大小
返回值: 成功 0 失败-1
set:设置 sock:套接字 option:属性 选项
socket属性(int类型,允许则为非0,不允许为0)
流程:
接收者
1. 创建套接字(socket)
2. 指定网络信息
3. 绑定套接字(bind)
4. 接收消息(recvfrom)
5. 关闭套接字(close)
发送者
1. 创建套接字(socket)
2. 由于原本的套接字不支持广播,所以要给套接字设置广播属性
3. 指定网络(服务器)信息
4. 发送消息(sendto)
5. 关闭套接字(close)
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>/*UDP广播*/int main(int argc, char const *argv[])
{/*创建数据报套接字*/int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("创建套接字成功\n");}//为套接字设置广播属性int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval));//指定网络信息struct sockaddr_in saddr; // 定义一个结构体变量saddr.sin_family = AF_INET; // 确定协议族-->IPv4saddr.sin_port = htons(atoi(argv[1])); // 确定使用的端口号saddr.sin_addr.s_addr = inet_addr("192.168.50.255"); // IP使用该网段广播地址//通信char buf[128] = "";while (1){fgets(buf, sizeof(buf), stdin);if (buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';if (strcmp(buf, "quit") == 0){break;}sendto(sockfd, buf, 128, 0, (struct sockaddr *)&saddr, sizeof(saddr)); }return 0;
}
组播:
又名多播
● 多播是一个人发送后,只有加入到多播组的人接收数据。
● 多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
多播地址:D类IP:224.0.0.1~239.255.255.254
流程
接收者
1. 创建套接字(socket)
2. 设置多播属性,将自己的IP加入到多播组中
3. 指定网络信息
4. 绑定套接字(bind)
5. 接收消息(recvfrom)
6. 关闭套接字(close)
发送者
1. 创建套接字(socket)
2. 指定网络(服务器)信息
3. 发送消息(sendto)
4. 关闭套接字(close)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>/*消息接收方(有bind)*/int main(int argc, char const *argv[])
{/*创建数据报套接字*/int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("创建套接字成功\n");}//设置多播属性,将自己加入多播组(绑定自己的ip和组播ip)struct ip_mreq mreq;mreq.imr_multiaddr.s_addr=inet_addr(argv[1]);//组播ipmreq.imr_interface.s_addr=INADDR_ANY;//自己的ipsetsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));/*指定服务器网络信息 使用的协议族(IPv4-->AF_INET)、IP地址、端口号等*/// 服务器的网络信息通过一个系统定义好的结构体来描述struct sockaddr_in saddr; // 定义一个结构体变量saddr.sin_family = AF_INET; // 确定协议族-->IPv4saddr.sin_port = htons(atoi(argv[2])); // 确定使用的端口号saddr.sin_addr.s_addr = INADDR_ANY; // 服务器地址/*绑定套接字*/int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (t1 < 0){perror("bind err");return -1;}else{printf("绑定套接字成功\n");}struct sockaddr_in caddr;int len=sizeof(caddr);char buf[128]="";int ret;while (1){ret = recvfrom(sockfd, buf, 128, 0,(struct sockaddr *)&caddr,&len); if (ret < 0){perror("recv err");return -1;}else{printf("IP为:%s,端口号为:%d的客户发来了:%s \n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buf);memset(buf, 0, sizeof(buf));}}return 0;
}
相关文章:

网络编程之服务器模型与UDP编程
一、服务器模型 在网络通信中,通常要求一个服务器连接多个客户端 为了处理多个客户端的请求,通常有多种表现形式 1、循环服务器模型 一个服务器可以连接多个客户端,但同一时间只能连接并处理一个客户的请求 socket() 结构体 bind() listen() …...

Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测
Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测 目录 Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五…...

阿里云服务器安装nginx并配置前端资源路径(前后端部署到一台服务器并成功访问)
运行以下命令,安装Nginx相关依赖。 yum install -y gcc-c yum install -y pcre pcre-devel yum install -y zlib zlib-devel yum install -y openssl openssl-devel 运行wget命令下载Nginx 1.21.6。 您可以通过Nginx开源社区直接获取对应版本的安装包URL&…...
Ubuntu 下开机自动执行命令的方法
Ubuntu 下开机自动执行命令的方法(使用 crontab) 在日常使用 Ubuntu 或其他 Linux 系统时,我们常常需要让某些程序或脚本在系统启动后自动运行。例如:启动 Clash 代理、初始化服务、定时同步数据等。 本文将介绍一种简单且常用的…...

C++11新增重要标准(下)
前言 一,forward(完美转发) 二,可变参数模板 三,emplace系列接口 四,新增类功能 五,default与delete 六,lambda表达式 七,包装器 八,bind 在C11中新增…...

【第六篇】 SpringBoot的日志基础操作
简介 日志系统在软件开发中至关重要,用于调试代码、记录运行信息及错误堆栈。本篇文章不仅详细介绍了日志对象的创建及快速使用,还说明了日志持久化的两种配置方式和滚动日志的设置。实际开发需根据场景选择合适的日志级别和存储策略。文章内容若存在错误…...

Pluto论文阅读笔记
主要还是参考了这一篇论文笔记:https://zhuanlan.zhihu.com/p/18319150220 Pluto主要有三个创新点: 横向纵向用lane的query来做将轨迹投回栅格化地图,计算碰撞loss对数据进行正增强和负增强,让正增强的结果也无增强的结果相近&a…...
ubuntu显示器未知
在Ubuntu系统中,当外接显示器被识别为“未知设备”时,可通过以下日志文件进行问题诊断,结合Xorg日志和内核日志综合分析: 🔍 一、查看Xorg显示服务日志(核心) Xorg日志记录了图形界面的详细事…...
Faiss向量数据库全面解析:从原理到实战
Faiss向量数据库全面解析:从原理到实战 引言:向量搜索的时代需求 在AI技术爆发的今天,向量数据已成为表示文本、图像、音视频等内容的核心形式。Facebook AI研究院开源的Faiss(Facebook AI Similarity Search)作为高…...

matlab 2024a 工具箱Aerospsce Toolbox报错
Matlab R2024a中Aerospsce Toolbox报错 警告:Aerospace Toolbox and Aerospace Blockset licenses are required in ‘built-in/Spacecraft Dynamics’ 找到安装路径\MATLAB\R2024a\licenses文件夹license_****_R2024a.lic 里面工具箱名称出错,手动修改…...

使用有限计算实现视频生成模型的高效训练
大家读完觉得有帮助记得关注和点赞!!! 抽象 视频生成的最新进展需要越来越高效的训练配方,以减轻不断上升的计算成本。在本报告中,我们介绍了 ContentV,这是一种 8B 参数文本到视频模型,在 256 …...

Server2003 B-1 Windows操作系统渗透
任务环境说明: 服务器场景:Server2003(开放链接) 服务器场景操作系统:Windows7 1.通过本地PC中渗透测试平台Kali对服务器场景Windows进行系统服务及版本扫描渗透测试,并将该操作显示结果中Telnet服务对应的…...

一次Oracle的非正常关闭
数据库自己会关闭吗? 从现象来说Oracle MySQL Redis等都会出现进程意外停止的情况。而这些停止都是非人为正常关闭或者暴力关闭(abort或者kill 进程) 一次测试环境的非关闭 一般遇到这种情况先看一下错误日志吧。 2025-06-01T06:26:06.35…...
AI不会杀死创作,但会杀死平庸
作为一个敲了8年Java代码的普通本科程序员,日常主要泡在会议后台管理系统的开发里。从2023年底被朋友拽着试了第一把AI工具到现在,电脑手机上的AI软件比外卖App还多——写代码的Copilot、画时序图的工具、聊天的ChatGPT、Deepseek,基本市面上…...
JeecgBoot低代码管理平台
一、一句话理解 JeecgBoot JeecgBoot 是一个基于 Java 技术栈(主要是 Spring Boot 和 Vue)的快速开发脚手架。它的核心理念是:通过代码生成器和一系列预置模块,极大地减少程序员在开发企业级后台管理系统时重复的、模板化的工作&…...
Fetch与Axios:区别、联系、优缺点及使用差异
Fetch与Axios:区别、联系、优缺点及使用差异 文章目录 Fetch与Axios:区别、联系、优缺点及使用差异一、联系二、区别1. 浏览器支持与兼容性2. 响应处理3. 请求拦截和响应拦截4. 错误处理 三、优缺点1. Fetch API优点缺点 2. Axios优点缺点 四、使用上的差…...

YOLO11解决方案之分析
概述 Ultralytics提供了一系列的解决方案,利用YOLO11解决现实世界的问题,包括物体计数、模糊处理、热力图、安防系统、速度估计、物体追踪等多个方面的应用。 Ultralytics提供了三种基本的数据可视化类型:折线图(面积图…...

yolov11与双目测距结合,实现目标的识别和定位测距(onnx版本)
一、yolov11双目测距基本流程 yolov11 双目测距的大致流程就是: 双目标定 --> 立体校正(含消除畸变) --> 立体匹配 --> 视差计算 --> 深度计算(3D坐标)计算 --> 目标检测 --> 目标距离计算及可视化 下面将分别阐述每…...

基于51单片机和8X8点阵屏、独立按键的填充消除类小游戏
目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、8X8点阵屏2、独立按键3、定时器04、定时器1 四、主函数总结 系列文章目录 前言 使用的是普中A2开发板。 【单片机】STC89C52RC 【频率】12T11.0592MHz 【外设】8X8点阵屏、独立按键 效果查看/操作演示&#x…...
将数据库表导出为C#实体对象
数据库方式 use 数据库;declare TableName sysname 表名 declare Result varchar(max) /// <summary> /// TableName /// </summary> public class TableName {select Result Result /// <summary>/// CONVERT(NVARCHAR(500), ISNULL(ColN…...

物联网技术发展与应用研究分析
文章目录 引言一、物联网的基本架构(一)感知层(二)网络层(三)平台层(四)应用层 二、物联网的关键技术(一)传感器技术(二)通信技术&…...

金融系统渗透测试
金融系统渗透测试是保障金融机构网络安全的核心环节,它的核心目标是通过模拟攻击手段主动发现系统漏洞,防范数据泄露、资金盗取等重大风险。 一、金融系统渗透测试的核心框架 合规性驱动 需严格遵循《网络安全法》《数据安全法》及金融行业监管要求&am…...
C++ 信息学奥赛总复习题
第一章 C 基础语法 一、填空题 C 源文件的扩展名通常是______。C 程序的入口函数是______。在 C 中,注释有两种形式,分别是______和______。声明一个整型变量 a 的语句是______。输出语句的关键字是______。 二、判断题 C 区分大小写。( …...

9.进程间通信
1.简介 为啥要有进程间通信? 如果未来进程之间要协同呢?一个进程要把自己的数据交给另一个进程!进程是具有独立性的,所以把一个进程的数据交给另一个进程----基本不可能!必须通信起来,就必须要有另一个人…...
性能剖析:在 ABP 框架中集成 MiniProfiler 实现性能可视化诊断
🚀 性能剖析:在 ABP 框架中集成 MiniProfiler 实现性能可视化诊断 📚 目录 🚀 性能剖析:在 ABP 框架中集成 MiniProfiler 实现性能可视化诊断一、为什么选择 MiniProfiler? 🧐二、集成 MiniProf…...

React 基础入门笔记
一、JSX语法规则 1. 定义虚拟DOM时,不要写引号 2.标签中混入JS表达式时要用 {} (1).JS表达式与JS语句(代码)的区别 (2).使用案例 3.样式的类名指定不要用class,要用className 4.内…...
C++.OpenGL (12/64)光照贴图(Lightmaps)
光照贴图(Lightmaps) 静态光照烘焙技术 #mermaid-svg-1vJKLLr1zSCp1ASH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-1vJKLLr1zSCp1ASH .error-icon{fill:#552222;}#mermaid-svg-1vJKLLr1zSCp1ASH .error-text…...

压测软件-Jmeter
1 下载和安装 1.1 检查运行环境 Jmeter需要运行在java环境(JRE 或 JDK)中 在window的"命令提示窗"查看安装的java版本: java -version 1.2 下载Jmeter 从Apache官网下载Jmeter安装包 1.3 解压和运行 解压后,进入bin文件夹,双击jmeter.bat即可…...
Linux 常用命令语法总结
Linux 常用命令语法总结 1. 文件和目录操作 1.1 基本文件操作 # 列出文件和目录 ls # 列出当前目录内容 ls -l # 详细列表格式 ls -la # 显示隐藏文件 ls -lh # 人性化显示文件大小 ls...
青少年编程与数学 01-011 系统软件简介 02 UNIX操作系统
青少年编程与数学 01-011 系统软件简介 02 UNIX操作系统 一、UNIX的历史沿革二、UNIX系统架构三、UNIX主要特性四、UNIX分支与变种五、UNIX设计哲学六、UNIX的影响与遗产 **摘要:**UNIX操作系统是现代计算领域最具影响力的操作系统之一,其设计哲学和技术…...