在springboot中实现WebSocket协议通信
前面介绍了使用netty实现websocket通信,有些时候,如果我们的服务并不复杂或者连接数并不高,单独搭建一个websocket服务端有些浪费资源,这时候我们就可以在web服务内提供简单的websocket连接支持。其实springboot已经支持了websocket通信协议,只需要几步简单的配置就可以实现。
老规矩,首先需要引入相关的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>provided</scope>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version>
</dependency>
springboot的配置文件application.yaml不需要额外内容,简单指定一下端口号和服务名称就可以了:
server:port: 8081shutdown: gracefulspring:application:name: test-ws
由于我这里使用了日志,简单配置一下日志文件logback-spring.xml输出内容:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false"><contextName>api-logger-server</contextName><!-- 控制台 --><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}|%thread|[%-5level]|%logger{36}.%method|%msg%n</pattern><charset>UTF-8</charset></encoder></appender><!--业务日志 文件--><appender name="msg" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${user.dir}/logs/msg.log</file><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}|%thread|[%-5level]|%logger{36}.%method|%msg%n</pattern><charset>UTF-8</charset></encoder><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><FileNamePattern>${user.dir}/logs/msg.log.%d{yyyy-MM-dd}</FileNamePattern></rollingPolicy></appender><logger name="msg" level="ERROR" additivity="false"><appender-ref ref="msg"/></logger><!--收集除error级别以外的日志--><appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>DENY</onMatch><onMismatch>ACCEPT</onMismatch></filter><encoder><pattern>%d|%t|%-5p|%c|%m%n</pattern><charset>UTF-8</charset></encoder><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!--路径--><fileNamePattern>${user.dir}/logs/info/%d.%i.log</fileNamePattern><maxFileSize>100MB</maxFileSize><!--日志文件保留天数--><maxHistory>15</maxHistory><!--超过该大小,删除旧文件--><totalSizeCap>10GB</totalSizeCap></rollingPolicy></appender><appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>ERROR</level></filter><encoder><pattern>%d|%t|%-5p|%c|%m%n</pattern><charset>UTF-8</charset></encoder><!--滚动策略--><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!--路径--><fileNamePattern>${user.dir}/logs/error/%d.%i.log</fileNamePattern><maxFileSize>100MB</maxFileSize><!--日志文件保留天数--><maxHistory>15</maxHistory><!--超过该大小,删除旧文件--><totalSizeCap>10GB</totalSizeCap></rollingPolicy></appender><root level="INFO"><appender-ref ref="console"/><appender-ref ref="INFO"/><appender-ref ref="ERROR"/></root>
</configuration>
本项目只是简单演示在springboot中使用websocket功能,所以没有涉及到复杂的业务逻辑,但还是需要定义一个用户服务类,用来存储用户身份信息和登录时的身份校验。
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap;/*** 用户服务类** @Author xingo* @Date 2023/11/22*/
@Slf4j
@Service
public class UserService {static final ConcurrentHashMap<String, User> USER_MAP = new ConcurrentHashMap<>();static final ConcurrentHashMap<String, String> TOKEN_MAP = new ConcurrentHashMap<>();/*** 启动时存入信息*/@PostConstructpublic void run() {User user1 = User.builder().userName("zhangsan").nickName("张三").build();User user2 = User.builder().userName("lisi").nickName("李四").build();// 用户信息集合USER_MAP.put(user1.getUserName(), user1);USER_MAP.put(user2.getUserName(), user2);// 模拟用户登录成功,将身份认证的token放入集合String random1 = "token_" + RandomStringUtils.random(18, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");String random2 = "token_" + RandomStringUtils.random(18, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");log.info("用户身份信息|{}|{}", user1.getUserName(), random1);log.info("用户身份信息|{}|{}", user2.getUserName(), random2);TOKEN_MAP.put(random1, user1.getUserName());TOKEN_MAP.put(random2, user2.getUserName());}/*** 根据用户名获取用户信息*/public User getUserByUserName(String userName) {return USER_MAP.get(userName);}/*** 校验token和用户是否匹配*/public boolean checkToken(String token, String userName) {return userName.equals(TOKEN_MAP.get(token));}/*** 用户信息实体类*/@Data@Builderpublic static final class User {private String userName;private String nickName;}
}
接下来就是websocket相关注入到容器中,首先需要注入的是ServerEndpointExporter,这个类用来扫描ServerEndpoint相关内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** 注入ServerEndpointExporter,用来扫描ServerEndpoint相关注解** @author xingo* @Date 2023/11/22*/
@Configuration
public class WebsocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
接下来需要再注入一个Bean,这个Bean需要添加ServerEndpoint注解,主要用来处理websocket连接。注意这个Bean是多例的,每个websocket连接都会新建一个实例。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;/*** websocket服务类* 连接ws服务这里要两个参数:userName 和 token* userName 用于用户身份标识* token 用于用户身份认证,用户每次登录进入系统都有可能不同** @author xingo* @Date 2023/11/22*/
@Slf4j
@Component
@ServerEndpoint("/{userName}/{token}")
public class WebSocketEndpoint {/*** 存放所有在线的客户端:键为用户名,值为用户的所有连接*/public static final Map<String, List<Session>> USER_SESSIONS = new ConcurrentHashMap<>();/*** 存放连接最近一次写数据的时间戳*/public static final Map<Session, Long> LAST_REQUEST_TIME = new ConcurrentHashMap<>();// ServerEndpoint 是多例的,需要设置为静态的类成员,否则程序运行会出错private static UserService userService;// 只能通过属性的set方法注入@Autowiredpublic void setUserService(UserService userService) {WebSocketEndpoint.userService = userService;}/*** 客户端连接* @param session*/@OnOpenpublic void onOpen(Session session, EndpointConfig config, @PathParam("userName") String userName, @PathParam("token") String token) {System.out.println("客户端连接|" + userName + "|" + token + "|" + session);System.out.println(this);System.out.println(userService);LAST_REQUEST_TIME.put(session, System.currentTimeMillis());if(StringUtils.isNotBlank(userName) && StringUtils.isNotBlank(token)) {boolean flag = false;boolean check = userService.checkToken(token, userName);if(check) {UserService.User user = userService.getUserByUserName(userName);if(user != null) {if(!USER_SESSIONS.containsKey(userName)) {USER_SESSIONS.put(userName, new ArrayList<>());}USER_SESSIONS.get(userName).add(session);flag = true;}}if(flag) {session.getAsyncRemote().sendText("连接服务端成功");} else {session.getAsyncRemote().sendText("用户信息认证失败,连接服务端失败");}} else {session.getAsyncRemote().sendText("未获取到用户身份验证信息");}}/*** 客户端关闭* @param session session*/@OnClosepublic void onClose(Session session, CloseReason closeReason, @PathParam("userName") String userName, @PathParam("token") String token) {System.out.println("客户端断开|" + userName + "|" + token + "|" + session);if(StringUtils.isNotBlank(userName)) {USER_SESSIONS.get(userName).remove(session);LAST_REQUEST_TIME.remove(session);}LAST_REQUEST_TIME.remove(session);}/*** 发生错误* @param throwable e*/@OnErrorpublic void onError(Session session, Throwable throwable) {throwable.printStackTrace();}/*** 收到客户端发来消息* @param message 消息对象*/@OnMessagepublic void onMessage(Session session, String message, @PathParam("userName") String userName, @PathParam("token") String token) {log.info("接收到客户端消息|{}|{}|{}|{}", userName, token, session.getId(), message);LAST_REQUEST_TIME.put(session, System.currentTimeMillis());String resp = null;try {if("PING".equals(message)) {resp = "PONG";} else if("PONG".equals(message)) {log.info("客户端响应心跳|{}", session.getId());} else {resp = "服务端收到信息 : " + message;}} catch (Exception e) {e.printStackTrace();}if(resp != null) {sendMessage(userName, resp);}}/*** 发送消息* @param userName 用户名* @param data 数据体*/public static void sendMessage(String userName, String data) {List<Session> sessions = USER_SESSIONS.get(userName);if(sessions != null && !sessions.isEmpty()) {sessions.forEach(session -> session.getAsyncRemote().sendText(data));} else {log.error("客户端未连接|{}", userName);}}/*** 初始化方法执行标识*/public static final AtomicBoolean INIT_RUN = new AtomicBoolean(false);/*** 处理长时间没有与服务器进行通信的连接*/@PostConstructpublic void run() {if(INIT_RUN.compareAndSet(false, true)) {log.info("检查连接定时任务启动");ScheduledExecutorService service = Executors.newScheduledThreadPool(1);service.scheduleAtFixedRate(() -> {// 超时关闭时间:超过5分钟未更新时间long closeTimeout = System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);// 心跳包时间:超过2分钟未更新时间long heartbeatTimeout = System.currentTimeMillis() - TimeUnit.MICROSECONDS.convert(2, TimeUnit.MINUTES);Iterator<Map.Entry<Session, Long>> iterator = LAST_REQUEST_TIME.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<Session, Long> next = iterator.next();Session session = next.getKey();long lastTimestamp = next.getValue();if(lastTimestamp < closeTimeout) { // 超时链接关闭log.error("关闭超时连接|{}", session.getId());try {session.close();iterator.remove();USER_SESSIONS.entrySet().forEach(entry -> entry.getValue().remove(session));} catch (IOException e) {e.printStackTrace();}} else if(lastTimestamp < heartbeatTimeout) { // 发送心跳包log.info("发送心跳包|{}", session.getId());session.getAsyncRemote().sendText("PING");}}}, 5, 10, TimeUnit.SECONDS);}}
}
对于上面的Bean需要几点说明:
- 该Bean是多例的,每个websocket连接都会创建一个实例。在上面连接建立的方法里面输出当前实例对象的内容每个连接输出的内容都不同:
客户端连接|zhangsan|token_JTrFGlBW01gHxFZHFG|org.apache.tomcat.websocket.WsSession@7ef1b79f
org.example.websocket.WebSocketEndpoint@33141901
org.example.service.UserService@46db8a12
客户端断开|zhangsan|token_JTrFGlBW01gHxFZHFG|org.apache.tomcat.websocket.WsSession@7ef1b79f
客户端连接|zhangsan|token_JTrFGlBW01gHxFZHFG|org.apache.tomcat.websocket.WsSession@7116a4f3
org.example.websocket.WebSocketEndpoint@341424b5
org.example.service.UserService@46db8a12
客户端断开|zhangsan|token_JTrFGlBW01gHxFZHFG|org.apache.tomcat.websocket.WsSession@7116a4f3
客户端连接|zhangsan|token_JTrFGlBW01gHxFZHFG|org.apache.tomcat.websocket.WsSession@737a3e9b
org.example.websocket.WebSocketEndpoint@3678be90
org.example.service.UserService@46db8a12
- 在该类中注入其他的Bean要设置为静态属性,并且注入要通过set方法,否则注入失败,之前在项目中使用时就出现过这种问题,将属性定义为成员变量并且直接在属性上面添加@Autowired注解,导致该属性一直是null。
比如我的UserService服务就是通过这种方式注入的:
private static UserService userService;@Autowired
public void setUserService(UserService userService) {WebSocketEndpoint.userService = userService;
}
上面几个类定义好后就实现了在springboot中使用websocket,添加启动类就可以进行前后通信:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** 应用启动类* * @Author xingo* @Date 2023/11/22*/
@SpringBootApplication
public class WsApplication {public static void main(String[] args) {SpringApplication.run(WsApplication.class, args);}
}
为了方便测试,再添加一个controller用于接收消息并将消息转发到客户端:
import org.example.websocket.WebSocketEndpoint;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** @Author xingo* @Date 2023/11/22*/
@RestController
public class MessageController {/*** 发送信息*/@GetMapping("/sendmessage")public String sendMessage(String userName, String message) {WebSocketEndpoint.sendMessage(userName, message);return "ok";}
}
测试服务是否正常。我这里选择使用postman进行测试,通过postman建立连接并发送消息。

连接建立成功,并且正常的发送和接收到数据。
下面测试一下通过http发送数据到服务端,服务端根据用户名查找到对应连接将消息转发到客户端。


这种模拟了服务端主动推送数据给客户端场景,实现了双向通信。
以上就是使用springboot搭建websocket的全部内容,发现还是非常简单,最主要的是可以与现有的项目实行完全融合,不需要做太多的改变。
上面这种方式只是单体服务简单的实现,对于稍微有一点规模的应用都会采用集群化部署,用一个nginx做反向代理后端搭配几个应用服务器组成集群模式,对于集群服务就会涉及到服务间通信的问题,需要将消息转发到用户正在连接的服务上面发送给客户端。后面会讲一下如何通过redis作为中心服务实现服务发现和请求转发的功能。
相关文章:
在springboot中实现WebSocket协议通信
前面介绍了使用netty实现websocket通信,有些时候,如果我们的服务并不复杂或者连接数并不高,单独搭建一个websocket服务端有些浪费资源,这时候我们就可以在web服务内提供简单的websocket连接支持。其实springboot已经支持了websock…...
云原生Docker系列 | Docker私有镜像仓库公有镜像仓库使用
云原生Docker系列 | Docker私有镜像仓库&公有镜像仓库使用 1. 使用公有云镜像仓库1.1. 阿里云镜像仓库1.2. 华为云镜像仓库1.3. 腾讯云镜像仓库2. 使用Docker Hub镜像仓库3. 使用Harbor构建私有镜像仓库4. 搭建本地Registry镜像仓库1. 使用公有云镜像仓库 1.1. 阿里云镜像…...
用于 syslog 收集的协议:TCP、UDP、RELP
系统日志是从 Linux/Unix 设备和其他网络设备(如交换机、路由器和防火墙)生成的日志 可以通过将 syslog 聚合到称为 syslog 服务器、syslog 守护程序或 syslogd 的服务器来集中 syslog。在TCP、UDP和RELP协议的帮助下,系统日志从设备传输到系…...
OpenAI创始人山姆·阿尔特曼重返公司;LLM持续学习
🦉 AI新闻 🚀 OpenAI创始人山姆阿尔特曼重返公司并与微软建立合作伙伴关系 摘要:OpenAI创始人山姆阿尔特曼回归OpenAI,担任首席执行官,并与微软建立牢固的合作伙伴关系。这解决了近期的争论,微软对OpenAI…...
Ant Design Pro生产环境部署
Ant Design Pro是通过URL路径前缀/api访问后端服务器,因此在nginx配置以下代理即可。 location / {index.html } location /api {proxy_pass: api.mydomain.com }...
Altium Designer学习笔记10
再次根据图纸进行布局走线: 这个MT2492 建议的布局走线。 那我这边应该是尽量按照该图进行布局: 其中我看到C1的电容的封装使用的是电感的封装,需要进行更换处理: 执行Validate Changes和Execute Changes操作,更新&a…...
ubuntu cutecom串口调试工具使用方法(图形界面)
文章目录 Ubuntu下使用CuteCom进行串口调试使用指南什么是CuteCom?主要特点 安装CuteCom使用APT包管理器从源码编译安装 配置串口CuteCom界面解析(启动cutecom)使用CuteCom进行数据发送和接收配置串口参数数据接收数据发送 高级功能和技巧流控…...
flink 1.17.1的pom.xml模板
flink 1.17.1的pom.xml模板 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apa…...
MySql的数据类型和隐式转换
文章目录 一、数据类型1、数值类型1.1、整数类型1.2、浮点1.3、定点DECIMAL 2、时间类型2.1、日期和时间类型占用的存储空间2.2、日期和时间类型表示的范围2.3、日期和时间类型的零值表示 3、文本类型 二、隐式转换参考文章 一、数据类型 1、数值类型 1.1、整数类型 整数名称…...
【开源】基于JAVA的在线课程教学系统
项目编号: S 014 ,文末获取源码。 \color{red}{项目编号:S014,文末获取源码。} 项目编号:S014,文末获取源码。 目录 一、摘要1.1 系统介绍1.2 项目录屏 二、研究内容2.1 课程类型管理模块2.2 课程管理模块2…...
【Linux】权限理解【文件权限以及目录权限详解、以及umsk程序掩码知识详解】
权限理解 一、Linux权限的概念二、su [用户名] : 切换用户三、Linux权限管理文件(一)文件访问者的分类(人)(二)文件类型和访问权限(事物属性)(1)第…...
Leetcode—1410.HTML实体解析器【中等】
2023每日刷题(三十八) Leetcode—1410.HTML实体解析器 算法思想 实现代码 typedef struct entityChar {char* entity;char rechar; }entity;entity matches[] {{""", "},{"'", \},{"&"…...
golang指针学习
package mainimport "fmt"func main() {name:"飞雪无情"nameP:&name//取地址fmt.Println("name变量的内存地址为:",&name)fmt.Println("name变量的值为:",name)fmt.Println("name变量的内存地址为:",nameP)fmt.Prin…...
c语言:用迭代法解决递归问题
题目: 解释:题目的意思就是用迭代法的空间和时间复杂的太高了,需要我们减小空间与时间的复杂度,我就想到了迭代法,思路和代码如下: #include <stdio.h> //这里是递归法转迭代法 int main() {int x,i…...
服务器数据恢复—OCFS2下raid5磁盘损坏导致阵列崩溃的数据恢复案例
服务器数据恢复环境: IBM某型号存储,6块sas硬盘组建一组raid5,划分一个lun分配给Linux服务器并格式化为OCFS2文件系统,共享给虚拟化使用,存放的数据包括24台liunx和windows虚拟机、压缩包文件和配置文件。 服务器故障…...
YOLO目标检测——卫星遥感多类别检测数据集下载分享【含对应voc、coco和yolo三种格式标签】
实际项目应用:卫星遥感目标检测数据集说明:卫星遥感多类别检测数据集,真实场景的高质量图片数据,数据场景丰富,含网球场、棒球场、篮球场、田径场、储罐、车辆、桥、飞机、船等类别标签说明:使用lableimg标…...
基于Towers of Binary Fields的succinct arguments
1. 引言 Ulvetanna团队Benjamin E. Diamond和Jim Posen 2023年论文《Succinct Arguments over Towers of Binary Fields》,开源代码见: https://github.com/recmo/binius(Rust Sage)【基于plonky3等库】 在该论文中࿱…...
【LeetCode刷题笔记】DFSBFS(一)
51. N 皇后 解题思路: DFS + 回溯 :由于 NxN 个格子放 N 个皇后, 同一行不能放置 2 个皇后,所以皇后必然放置在不同行 。 因此,可以从第 0 行开始,逐行地尝试,在每一个 i...
Amazon Generative AI 新世界 | 基于 Amazon 扩散模型原理的代码实践之采样篇
以前通过论文介绍 Amazon 生成式 AI 和大语言模型(LLMs)的主要原理之外,在代码实践环节主要还是局限于是引入预训练模型、在预训练模型基础上做微调、使用 API 等等。很多开发人员觉得还不过瘾,希望内容可以更加深入。因此&#x…...
使用C语言统计一个字符串中每个字母出现的次数
每日一言 Wishing is not enough; we must do. 光是许愿望是不够的; 我们必须行动。 题目 输入一个字符串,统计在该字符串中每个字母出现的次数 例如: 输入:i am a student 输出:a:2 d:1 e:1 i:1 m:1 n:1 s:1 t:2 u:1 大体思路…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
