【设计模式——学习笔记】23种设计模式——桥接模式Bridge(原理讲解+应用场景介绍+案例介绍+Java代码实现)
问题引入
现在对不同手机类型的不同品牌实现操作编程(比如:开机、关机、上网,打电话等),如图
【对应类图】
【分析】
- 扩展性问题(类爆炸),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加。
- 违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本
- 解决方案:使用桥接模式
介绍
基础介绍
- 桥接模式(Bridge模式)是指: 将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
- Bridge 的意思是“桥梁”。就像在现实世界中,桥梁的功能是将河流的两侧连接起来一样Bridge 模式的作用也是将两样东西连接起来,它们分别是类的功能层次结构和类的实现层次结构。
- 是一种结构型设计模式
- Bridge模式基于
类的最小设计原则
(实现功能的同时,让类尽可能少),通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(lmplementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展
类的功能层次结构
假设现在有一个类Something。当我们想在Something中增加新功能时(想增加一个具体方法时),会编写一个Something类的子类(派生类),如SomethingGood类。这样就构成了一个小小的类层次结构。
父类本身具备一些基本功能,子类在继承父类的功能之外,还可以添加新的功能。这种层次结构被称为“类的功能层次结构”。如果需要在SomethingGood的基础上继续增加新的功能,再写SomethingGood的子类即可。
注意:类的层次结构关系不应当过深
类的实现层次结构
抽象类声明了一些抽象方法,定义了接口(API),然后子类负责去实现这些抽象方法。父类的任务是通过声明抽象方法的方式定义接口(API),而子类的任务是实现抽象方法。正是由于父类和子类的这种任务分担,我们才可以编写出具有高可替换性的类。
当子类Concreteclass实现了父类Abstractclass类的抽象方法时,它们之间就构成了一个小小的层次结构。
这种类的层次结构(类的实现层次结构)并非用于增加功能,并不方便我们增加新的方法。它的真正作用是帮助我们实现下面这样的任务分担
- 类通过声明抽象方法来定义接口(API)
- 子类通过实现具体方法来实现接口(API)
一个抽象类可以有多种子实现类
层次结构分离
当类的层次结构只有一层时,功能层次结构与实现层次结构是混杂在一个层次结构中的。这样很容易使类的层次结构变得复杂,也难以透彻地理解类的层次结构。因为自己难以确定究竟应该在类的哪一个层次结构中去增加子类。
因此,我们需要将“类的功能层次结构”与“类的实现层次结构”分离为两个独立的类层次结构。当然,如果只是简单地将它们分开,两者之间必然会缺少联系。所以我们还需要在它们之间搭建一座桥梁,这就是桥接模式。
类图
抽象类和接口是聚合关系,也是调用和被调用关系
应用场景
对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用,如以下场景:
- JDBC驱动程序
- 银行转账系统
- 转账分类: 网上转账,柜台转账,AMT转账
- 转账用户类型: 普通用户,银卡用户,金卡用户
- 消息管理
- 消息类型: 即时消息,延时消息
- 消息分类: 手机短信,邮件消息,QQ消息…
案例
案例一
类图
代码实现
【接口】
package com.atguigu.bridge;/*** 接口*/
public interface Brand {void open();void close();void call();
}
【接口实现类:小米手机】
package com.atguigu.bridge;public class XiaoMi implements Brand {@Overridepublic void open() {System.out.println(" 小米手机开机 ");}@Overridepublic void close() {System.out.println(" 小米手机关机 ");}@Overridepublic void call() {System.out.println(" 小米手机打电话 ");}}
【接口实现类:Vivo手机】
package com.atguigu.bridge;public class Vivo implements Brand {@Overridepublic void open() {System.out.println(" Vivo手机开机 ");}@Overridepublic void close() {System.out.println(" Vivo手机关机 ");}@Overridepublic void call() {System.out.println(" Vivo手机打电话 ");}}
【抽象类】
package com.atguigu.bridge;public abstract class Phone {/*** 聚合品牌*/private Brand brand;/*** 构造器* @param brand*/public Phone(Brand brand) {super();this.brand = brand;}protected void open() {this.brand.open();}protected void close() {this.brand.close();}protected void call() {this.brand.call();}}
【抽象类子类:折叠手机】
package com.atguigu.bridge;/*** 折叠式手机类,继承 抽象类 Phone*/
public class FoldPhone extends Phone {//构造器public FoldPhone(Brand brand) {super(brand);}public void open() {System.out.println(" 折叠样式手机 ");// 实际上调用的是具体品牌(如Xiaomi)的开机方法,抽象类Phone充当桥接作用super.open();}public void close() {System.out.println(" 折叠样式手机 ");super.close();}public void call() {System.out.println(" 折叠样式手机 ");super.call();}
}
【抽象类子类:直立手机】
package com.atguigu.bridge;public class UpRightPhone extends Phone {//构造器public UpRightPhone(Brand brand) {super(brand);}public void open() {System.out.println(" 直立样式手机 ");super.open();}public void close() {System.out.println(" 直立样式手机 ");super.close();}public void call() {System.out.println(" 直立样式手机 ");super.call();}
}
【客户端】
package com.atguigu.bridge;public class Client {public static void main(String[] args) {//获取折叠式手机 (样式 + 品牌 才是具体的手机)Phone phone1 = new FoldPhone(new XiaoMi());phone1.open();phone1.call();phone1.close();System.out.println("==============");Phone phone2 = new FoldPhone(new Vivo());phone2.open();phone2.call();phone2.close();System.out.println("==============");UpRightPhone phone3 = new UpRightPhone(new XiaoMi());phone3.open();phone3.call();phone3.close();System.out.println("==============");UpRightPhone phone4 = new UpRightPhone(new Vivo());phone4.open();phone4.call();phone4.close();}}
【运行】
折叠样式手机 小米手机开机 折叠样式手机 小米手机打电话 折叠样式手机 小米手机关机
==============折叠样式手机 Vivo手机开机 折叠样式手机 Vivo手机打电话 折叠样式手机 Vivo手机关机
==============直立样式手机 小米手机开机 直立样式手机 小米手机打电话 直立样式手机 小米手机关机
==============直立样式手机 Vivo手机开机 直立样式手机 Vivo手机打电话 直立样式手机 Vivo手机关机 Process finished with exit code 0
分析
- 无论增加一个样式或者增加一个新的手机品牌,都只需要增加一个类
案例二
类图
Display使用DispalyImpl的方法来完成功能,StringDisplayImpl负责DispalyImpl方法的具体实现,CountDisplay用来完成更多的功能
代码实现
【类的功能层次结构:Display】
package com.atguigu.bridge.Sample;/*** 类的功能层次最上层*/
public class Display {/*** 实现了Display类的具体功能的实例 (桥梁)*/private DisplayImpl impl;public Display(DisplayImpl impl) {this.impl = impl;}/*** 显示前的处理*/public void open() {impl.rawOpen();}/*** 显示处理*/public void print() {impl.rawPrint();}/*** 显示后的处理*/public void close() {impl.rawClose();}/*** 调用上面的三个方法来进行显示*/public final void display() {open();print(); close();}
}
private DisplayImpl impl;
使用了“委托”关系,而不是使用“继承”关系。继承是强关联关系,委托是弱关联关系。使用委托更方便扩展。
【类的功能层次结构:CountDisplay】
package com.atguigu.bridge.Sample;/*** 在Display类的基础上增加新功能*/
public class CountDisplay extends Display {public CountDisplay(DisplayImpl impl) {super(impl);}/*** 循环显示times次** @param times*/public void multiDisplay(int times) {open();for (int i = 0; i < times; i++) {print();}close();}
}
【类的实现层次结构:上层】
package com.atguigu.bridge.Sample;/*** 类的实现层次最上层*/
public abstract class DisplayImpl {public abstract void rawOpen();public abstract void rawPrint();public abstract void rawClose();
}
【类的实现层次结构:下层】
package com.atguigu.bridge.Sample;/*** 类的实现层次结构,具体实现类*/
public class StringDisplayImpl extends DisplayImpl {/*** 要显示的字符串*/private String string;/*** 以字节单位计算出的字符串的宽度*/private int width;/*** 构造方法** @param string 构造函数接收要显示的字符串string*/public StringDisplayImpl(String string) {// 将它保存在字段中this.string = string;// 把字符串的宽度也保存在字段中,以供使用this.width = string.getBytes().length;}public void rawOpen() {printLine();}public void rawPrint() {// 前后加上"|"并显示System.out.println("|" + string + "|");}public void rawClose() {printLine();}private void printLine() {// 显示用来表示方框的角的"+"System.out.print("+");// 显示width个"-"for (int i = 0; i < width; i++) {// 将其用作方框的边框System.out.print("-"); }// 显示用来表示方框的角的"+"System.out.println("+");}
}
【主类】
package com.atguigu.bridge.Sample;public class Main {public static void main(String[] args) {Display d1 = new Display(new StringDisplayImpl("Hello, China."));Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World."));CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe."));d1.display();System.out.println();d2.display();System.out.println();d3.display();d3.multiDisplay(5);}
}
【运行】
+-------------+
|Hello, China.|
+-------------++-------------+
|Hello, World.|
+-------------++----------------+
|Hello, Universe.|
+----------------+
+----------------+
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
+----------------+Process finished with exit code 0
拓展一
在上述示例程序中增加一个类,实现“显示字符串若干(随机)次”的功能。
【新增类】
package com.atguigu.bridge.A1;import java.util.Random;public class RandomCountDisplay extends CountDisplay {private Random random = new Random();public RandomCountDisplay(DisplayImpl impl) {super(impl);}public void randomDisplay(int times) {multiDisplay(random.nextInt(times));}
}
【主类】
package com.atguigu.bridge.A1;public class Main {public static void main(String[] args) {RandomCountDisplay d = new RandomCountDisplay(new StringDisplayImpl("Hello, China."));d.randomDisplay(10);}
}
扩展二
在上述示例程序中增加一个类,实现“显示文本文件的内容”的功能。
【新增类】
package com.atguigu.bridge.A2;import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;public class FileDisplayImpl extends DisplayImpl {private String filename;private BufferedReader reader;/*** 循环显示的极限(缓存大小限制)*/private final int MAX_READAHEAD_LIMIT = 4096; public FileDisplayImpl(String filename) {this.filename = filename;}public void rawOpen() {try {reader = new BufferedReader(new FileReader(filename));reader.mark(MAX_READAHEAD_LIMIT);} catch (IOException e) {e.printStackTrace();}// 装饰框System.out.println("=-=-=-=-=-= " + filename + " =-=-=-=-=-="); }public void rawPrint() {try {String line;reader.reset(); // 回到mark的位置while ((line = reader.readLine()) != null) {System.out.println("> " + line);}} catch (IOException e) {e.printStackTrace();}}public void rawClose() {// 装饰框System.out.println("=-=-=-=-=-= "); try {reader.close();} catch (IOException e) {e.printStackTrace();}}
}
【主类】
package com.atguigu.bridge.A2;public class Main {public static void main(String[] args) {CountDisplay d = new CountDisplay(new FileDisplayImpl("star.txt"));d.multiDisplay(3);}
}
登场角色
- Abstraction(抽象化):位于
类的功能层次结构
的最上层。它使用Implementor角色的方法定义了基本的功能。该角色中保存了Implementor 角色的实例 - RefinedAbstraction(改善后的抽象化):在Abstraction角色的基础上增加了新功能
- Implementor(实现者):位于“类的实现层次结构”的最上层。它定义了用于实现Abstraction 角色的接口(API)的方法
- Concretelmplementor( 具体实现者):负责实现在Implementor 角色中定义的接口(API)
桥接模式在JDBC源码中的应用
总结
- 实现了抽象和实现部分的分离,从而极大的
提高了系统的灵活性
,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统 - 分离之后代码的扩展性更强,当想要增加功能时,只需要在“类的功能层次结构”一侧增加类即可,不必对“类的实现层次结构”做任何修改。而且,增加后的功能可以被“所有的实现”使用(例如,我们可以将“类的功能层次结构”应用于软件所运行的操作系统上。如果我们将某个程序中依赖于操作系统的部分划分为 Windows版、Macintosh 版、Unix 版,那么我们就可以用 Bridge模式中的“类的实现层次结构”来表现这些依赖于操作系统的部分。也就是说,我们需要编写一个定义这些操作系统的共同接口(API)的Implementor角色,然后编写Windows版、Macintosh版Unix版的3个Concretelmplementor角色。这样一来,无论在“类的功能层次结构”中增加多少个功能,它们都可以工作于这3个操作系统上。)
- 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成
桥接模式替代多层继承方案
,可以减少子类的个数,降低系统的管理和维护成本- 桥接模式的引入增加了系统的理解和设计难度(较难分析出哪些是抽象层,哪些是实现层),由于聚合关联关系建立在抽象层要求开发者针对抽象进行设计和编程
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性,即需要有这样的应用场景
文章说明
- 本文章为本人学习尚硅谷的学习笔记,文章中大部分内容来源于尚硅谷视频(点击学习尚硅谷相关课程),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对尚硅谷的优质课程表示感谢。
- 本人还同步阅读《图解设计模式》书籍(图解设计模式/(日)结城浩著;杨文轩译–北京:人民邮电出版社,2017.1),进而综合两者的内容,让知识点更加全面
相关文章:

【设计模式——学习笔记】23种设计模式——桥接模式Bridge(原理讲解+应用场景介绍+案例介绍+Java代码实现)
问题引入 现在对不同手机类型的不同品牌实现操作编程(比如:开机、关机、上网,打电话等),如图 【对应类图】 【分析】 扩展性问题(类爆炸),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们…...

文档翻译软件那么多,哪个能满足你的多语言需求?
想象一下,你手中拿着一份外文文件,上面记录着珍贵的知识和信息,但是语言的障碍让你无法领略其中的内容。而此时,一位翻译大师闪亮登场!他的翻译技巧犹如一把魔法笔,能够将文字的魅力和意境完美传递。无论是…...

MySQL 中NULL和空值的区别
MySQL 中NULL和空值的区别? 简介NULL也就是在字段中存储NULL值,空值也就是字段中存储空字符(’’)。区别 1、空值不占空间,NULL值占空间。当字段不为NULL时,也可以插入空值。 2、当使用 IS NOT NULL 或者 IS NULL 时࿰…...

阿里云容器镜像仓库(ACR)的创建和使用
天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…...

工业的相机与镜头(简单选型)
面阵相机,需要多大的分辨率?多少帧数? 前提条件: 1.被检测的物体大小 2.要求检测的精度是多少 3.物体是否在运动过程中进行检测,速度是多少 线阵相机选择(分辨率、扫描行数) 行频:每秒扫描多少行…...

numpy广播机制介绍
广播 广播机制的意义:广播描述了在算术运算期间NumPy如何处理具有不同形状的数组。受某些约束条件的限制,较小的数组会在较大的数组中“广播”,以便它们具有兼容的形状。 在对两个数组进行操作时,NumPy按元素对它们的形状进行比…...

RocketMQ 5.0 无状态实时性消费详解
作者:绍舒 背景 RocketMQ 5.0 版本引入了 Proxy 模块、无状态 pop 消费机制和 gRPC 协议等创新功能,同时还推出了一种全新的客户端类型:SimpleConsumer。 SimpleConsumer 客户端采用了无状态的 pop 机制,彻底解决了在客户端发布…...

本地 IDC 中的 K8s 集群如何以 Serverless 方式使用云上计算资源
作者:庄宇 在前一篇文章《应对突发流量,如何快速为自建 K8s 添加云上弹性能力》中,我们介绍了如何为 IDC 中 K8s 集群添加云上节点,应对业务流量的增长,通过多级弹性调度,灵活使用云上资源,并通…...

MySQL - 安装、连接、简单介绍
1、安装 MySQL8.0 安装MySQL 8.0的步骤,以 Windows 为例: 1.1 下载MySQL Installer: 需要从MySQL官方网站下载MySQL Installer。在下载页面中,选择适用于Windows的MySQL Installer并下载。 1.2 运行MySQL Installer࿱…...
【算法】求欧拉函数(包括完整的证明以及代码模板,建议收藏)
求欧拉函数 前置知识 互质:互质是公约数只有1的两个整数,叫做互质整数。 欧拉函数定义 1 ∼ N − 1 1∼N-1 1∼N−1中与N互质的数的个数被称为欧拉函数,记为 ϕ ( N ) \phi(N) ϕ(N)。 若在算数基本定理中, N p 1 a 1 p 2 a 2 .…...

Ceph的应用
文章目录 一、创建 CephFS 文件系统 MDS 接口1)在管理节点创建 mds 服务2)查看各个节点的 mds 服务3)创建存储池,启用 ceph 文件系统4)查看mds状态,一个up,其余两个待命,目前的工作的…...

mac m1 触控栏TouchBar功能栏异常
电脑可能在高温下运行时间过长,导致TouchBar之前正常显示的调整屏幕亮度与调整声音等功能的按钮均丢失,然后看了一眼键盘设置,设置也是正常的,已勾选显示功能栏 下面请看 如何在MacBook Pro(macOS Monterey࿰…...

“奢侈品”价格的“快消品”,竹叶青这么想赚年轻人的“茶水钱”?
文 | 螳螂观察 作者 | 青月 或许是受养生焦虑的影响,这届年轻人似乎爱上了喝茶。 《抖音电商茶行业洞察报告》数据显示, 年轻客群已经成为了抖音电商茶行业的增长极,在茶叶、茶具、茶文化书籍等方面,18-30岁消费者是当之无愧消…...
【Matlab】基于随机森林算法的时间序列预测(Excel可直接替换数据)
【Matlab】基于随机森林算法的时间序列预测(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.分块代码6.完整代码7.运行结果1.模型原理 基于随机森林算法的时间序列预测是一种利用随机森林模型来解决时间序列预测问题的方法。在传统的随机森林算法中,对于…...

vue 中断请求
1 背景:针对一些请求时间较长,组件销毁后即中断请求; 2 方法: data(){return {//用于取消请求abortController:new AbortController(), } }, created(){//请求接口this.groundAcquisition(); }, beforeDestroy(){//中断请求this.…...

Jwt(Json web token)——从Http协议到session+cookie到Token Jwt介绍 Jwt的应用:登陆验证的流程
目录 引出从Http协议到session&cookie到TokenHTTP协议session & cookiesessioncookie为什么需要session & cookie? JavaEE传统解决长连接方案问题:分布式不适用解决方案:令牌Token Jwt,Json web tokenjwt的结构Header加密算法Ba…...

Java使用 java.util.regex.Pattern 正则表达式校验参数值是否规范
场景: java中我们可以利用 Pattern 注解对某个入参进行规则校验,但有些特殊参数在接口入口处不方便校验,需要在代码中校验 一、使用 Pattern 注解校验 Pattern(regexp "^[a-zA-Z0-9]$", message "xxx号限输入字母、…...

HDFS基本操作命令
这里写目录标题 HDFS Shell CLI客户端说明常用命令hadoop fs -mkdir [-p] <path>hadoop fs -ls [-h] [-R] [<path>...]上传文件到指定目录下方法一:hadoop fs -put [-f] [-p] <localsrc>.....<dst>方法二:hadoop fs -moveFromLocal <loc…...

git 实操
首先有安装好的git,安装好后,会在任一目录下右键出现git bash和git gui两个选项 打开git bash,设置好全局变量,用户名和邮箱,设置方法为: git config -- global user.name "xxx" git config --global user.email "xxxxxx.com" 1.创建版本库 git init 命…...

Visual Studio Code Python 扩展中的包管理
排版:Alan Wang Python 凭借其简单的语法和强大的库,目前已成为最流行的编程语言之一,也是最适合那些刚接触编程的人们的语言。但是,随着项目复杂性和规模的增长,管理依赖项的复杂性也会增加。当新用户不断承接更成熟的…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...