当前位置: 首页 > news >正文

Java SpringBoot 对接FreeSwitch

1.增加Maven依赖

        <dependency><groupId>org.freeswitch.esl.client</groupId><artifactId>org.freeswitch.esl.client</artifactId><version>0.9.2</version></dependency><!-- XML-RPC --><dependency><groupId>org.apache.xmlrpc</groupId><artifactId>xmlrpc-client</artifactId><version>3.1.3</version></dependency>

2.封装FreeSWITCH 工具类


import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslMessage;import javax.annotation.PreDestroy;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.stream.Collectors;/*** FreeSWITCH 工具类* @author luo* @date 2025-07-11*/
@Slf4j
public class FreeswitchUtil {private final String freeswitchHost;private final int eslPort;private final String eslPassword;private final int connectionTimeout;private final ReentrantLock connectionLock = new ReentrantLock();private final AtomicBoolean isConnecting = new AtomicBoolean(false);@Getter private volatile Client client;private IEslEventListener eventListener;private final String sipProfilesPath;public FreeswitchUtil(String eslHost, int eslPort, String eslPassword) {this(eslHost, eslPort, eslPassword, 10, "/usr/local/freeswitch/conf/directory/default");}public FreeswitchUtil(String eslHost, int eslPort, String eslPassword, int connectionTimeout) {this(eslHost, eslPort, eslPassword, connectionTimeout, "/usr/local/freeswitch/conf/directory/default");}public FreeswitchUtil(String eslHost, int eslPort, String eslPassword, int connectionTimeout, String sipProfilesPath) {this.freeswitchHost = eslHost;this.eslPort = eslPort;this.eslPassword = eslPassword;this.connectionTimeout = connectionTimeout;this.sipProfilesPath = sipProfilesPath;this.client = new Client();}public void setEventListener(IEslEventListener eventListener) {this.eventListener = eventListener;}public boolean connect() {connectionLock.lock();try {if (isClientConnected()) {log.info("FreeSWITCH ESL 已连接,无需重复连接");return true;}if (isConnecting.getAndSet(true)) {log.info("正在进行连接,当前线程等待连接完成");waitForConnection();return isClientConnected();}try {log.info("尝试连接到 FreeSWITCH ESL: {}:{}", freeswitchHost, eslPort);client = new Client();client.connect(freeswitchHost, eslPort, eslPassword, connectionTimeout);log.info("成功连接到 FreeSWITCH ESL");registerEventListener();subscribeEvents();return true;} catch (InboundConnectionFailure e) {log.error("连接 FreeSWITCH ESL 失败: {}", e.getMessage());disconnect();return false;} finally {isConnecting.set(false);}} finally {connectionLock.unlock();}}private void registerEventListener() {if (client != null && eventListener != null) {client.addEventListener(eventListener);} else if (client != null) {client.addEventListener(new DefaultEslEventListener());}}private void subscribeEvents() {if (client != null) {try {client.setEventSubscriptions("plain", "all");} catch (Exception e) {log.error("订阅 FreeSWITCH 事件失败: {}", e.getMessage());}}}public boolean isClientConnected() {return client != null && client.canSend();}private void waitForConnection() {int attempts = 0;while (isConnecting.get() && attempts < 10) {try {Thread.sleep(500);attempts++;} catch (InterruptedException e) {Thread.currentThread().interrupt();break;}}}@PreDestroypublic void disconnect() {Client localClient = this.client;this.client = null;if (localClient != null) {try {log.info("断开 FreeSWITCH ESL 连接");localClient.close();} catch (Exception e) {log.error("关闭 FreeSWITCH 客户端失败: {}", e.getMessage());}}}public synchronized void reConnect() {if (!isClientConnected()) {log.info("检测到 FreeSWITCH 连接断开,尝试重新连接");disconnect();connect();}}/*** 检查用户是否在线* @param username 用户名/分机号* @return 用户是否在线*/public boolean isUserOnline(String username) {if (!isClientConnected()) {log.warn("FreeSWITCH 未连接,无法检查用户在线状态");return false;}try {// 修改为使用 sendSyncApiCommand 返回 EslMessageEslMessage message = client.sendSyncApiCommand("sofia", "status profile internal reg " + username);String response = message.getBodyLines().toString();return response != null && !response.contains("0 registrations found");} catch (Exception e) {log.error("检查用户在线状态失败: {}", e.getMessage());return false;}}/*** 获取用户注册信息* @param username 用户名/分机号* @return 用户注册信息,如果不在线则返回空*/public String getUserRegistrationInfo(String username) {if (!isClientConnected()) {log.warn("FreeSWITCH 未连接,无法获取用户注册信息");return "";}try {// 修改为使用 sendSyncApiCommand 返回 EslMessageEslMessage message = client.sendSyncApiCommand("sofia", "status profile internal reg " + username);return message.getBodyLines().toString();} catch (Exception e) {log.error("获取用户注册信息失败: {}", e.getMessage());return "";}}/*** 注销用户* @param username 用户名/分机号* @return 操作是否成功*/public boolean unregisterUser(String username) {if (!isClientConnected()) {log.warn("FreeSWITCH 未连接,无法注销用户");return false;}try {// 修改为使用 sendSyncApiCommand 返回 EslMessageEslMessage message = client.sendSyncApiCommand("sofia", "killreg " + username);String response = message.getBodyLines().toString();return response != null && response.contains("removed");} catch (Exception e) {log.error("注销用户失败: {}", e.getMessage());return false;}}/*** 重启Sofia模块(使配置生效)* @param profileName Sofia配置文件名称,如internal* @return 操作是否成功*/public boolean restartSofiaProfile(String profileName) {if (!isClientConnected()) {log.warn("FreeSWITCH 未连接,无法重启Sofia配置");return false;}try {// 修改为使用 sendSyncApiCommand 返回 EslMessageEslMessage message = client.sendSyncApiCommand("sofia", "profile " + profileName + " restart");String response = message.getBodyLines().toString();return response != null && response.contains("Starting");} catch (Exception e) {log.error("重启Sofia配置失败: {}", e.getMessage());return false;}}/*** 创建新用户* @param username 用户名/分机号* @param password 密码* @param displayName 显示名称* @param domain 域名* @return 创建结果*/public boolean createUser(String username, String password, String displayName,String domain) {if (!isValidUsername(username)) {log.error("无效的用户名: {}", username);return false;}Path userFilePath = Paths.get(sipProfilesPath, username + ".xml");if (Files.exists(userFilePath)) {log.info("用户已存在: {},无需重复创建", username);return true;}String userConfig = generateUserConfig(username, password, displayName,domain);try (BufferedWriter writer = new BufferedWriter(new FileWriter(userFilePath.toFile()))) {writer.write(userConfig);log.info("用户配置文件已创建: {}", userFilePath);// 重载目录return reloadDirectory();} catch (IOException e) {log.error("创建用户配置文件失败: {}", e.getMessage());return false;}}/*** 删除用户* @param username 用户名/分机号* @return 删除结果*/public boolean deleteUser(String username) {if (!isValidUsername(username)) {log.error("无效的用户名: {}", username);return false;}Path userFilePath = Paths.get(sipProfilesPath, username + ".xml");if (!Files.exists(userFilePath)) {log.error("用户不存在: {}", username);return false;}try {Files.delete(userFilePath);log.info("用户配置文件已删除: {}", userFilePath);// 重载目录return reloadDirectory();} catch (IOException e) {log.error("删除用户配置文件失败: {}", e.getMessage());return false;}}/*** 修改用户密码* @param username 用户名/分机号* @param newPassword 新密码* @return 修改结果*/public boolean changeUserPassword(String username, String newPassword) {if (!isValidUsername(username)) {log.error("无效的用户名: {}", username);return false;}Path userFilePath = Paths.get(sipProfilesPath, username + ".xml");if (!Files.exists(userFilePath)) {log.error("用户不存在: {}", username);return false;}try {// 使用Java 8兼容的文件读取方式String content = Files.lines(userFilePath, StandardCharsets.UTF_8).collect(Collectors.joining(System.lineSeparator()));String updatedContent = content.replaceAll("<param name=\"password\" value=\".*?\"/>","<param name=\"password\" value=\"" + newPassword + "\"/>");// 使用Java 8兼容的文件写入方式try (BufferedWriter writer = Files.newBufferedWriter(userFilePath, StandardCharsets.UTF_8)) {writer.write(updatedContent);}log.info("用户密码已更新: {}", username);// 重载目录return reloadDirectory();} catch (IOException e) {log.error("修改用户密码失败: {}", e.getMessage());return false;}}private boolean reloadDirectory() {if (!isClientConnected()) {log.warn("FreeSWITCH 未连接,无法重载目录");return false;}try {// 修改为使用 sendSyncApiCommand 返回 EslMessageEslMessage message = client.sendSyncApiCommand("reloadxml", "");String response = message.getBodyLines().toString();EslMessage directoryMessage = client.sendSyncApiCommand("reload", "directory");String directoryResponse = directoryMessage.getBodyLines().toString();return response.contains("OK") && directoryResponse.contains("OK");} catch (Exception e) {log.error("重载目录失败: {}", e.getMessage());return false;}}/*** 生成用户配置文件内容* @param username 用户名/分机号* @param password 密码* @param displayName 显示名称* @return 用户配置文件内容*/private String generateUserConfig(String username, String password, String displayName, String domain) {return "<include>\n" +"  <user id=\"" + username + "\">\n" +"    <params>\n" +"      <param name=\"password\" value=\"" + password + "\"/>\n" +"    </params>\n" +"    <variables>\n" +"      <variable name=\"user_context\" value=\"default\"/>\n" +"      <variable name=\"domain\" value=\"" + domain + "\"/>\n" +  // 绑定指定域名"      <variable name=\"display_name\" value=\"" + displayName + "\"/>\n" +"      <variable name=\"toll_allow\" value=\"domestic,international,local\"/>\n" +"      <variable name=\"accountcode\" value=\"" + username + "\"/>\n" +"      <variable name=\"effective_caller_id_name\" value=\"" + displayName + "\"/>\n" +"      <variable name=\"effective_caller_id_number\" value=\"" + username + "\"/>\n" +"      <variable name=\"outbound_caller_id_name\" value=\"$${outbound_caller_name}\"/>\n" +  // 修正为全局变量引用($${})"      <variable name=\"outbound_caller_id_number\" value=\"$${outbound_caller_id}\"/>\n" +  // 修正为全局变量引用($${})"      <variable name=\"callgroup\" value=\"techsupport\"/>\n" +  // 补充默认呼叫组变量"    </variables>\n" +"  </user>\n" +"</include>";}/*** 校验用户名是否有效* @param username* @return*/private boolean isValidUsername(String username) {// 用户名只能包含数字和字母,长度1-20return Pattern.matches("^[a-zA-Z0-9]{1,20}$", username);}/*** ESL监听程序*/private static class DefaultEslEventListener implements IEslEventListener {@Overridepublic void eventReceived(EslEvent event) {log.debug("收到 FreeSWITCH 事件: {}", event.getEventName());}@Overridepublic void backgroundJobResultReceived(EslEvent event) {log.debug("收到 FreeSWITCH 后台任务结果: {}", event.getEventName());}}
}

3.FreeSWITCH用户服务类

import com.gnss.helmet.utils.FreeswitchUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;/*** FreeSWITCH用户服务类* @author Luo* @date 2025-07-11*/
@Service
@Slf4j
public class FreeswitchUserServer {private final FreeswitchUtil freeswitchUtil;private final ConcurrentMap<String, UserInfo> userCache = new ConcurrentHashMap<>();private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();private final int cacheRefreshInterval;private final int reconnectDelay;/*** 构造器注入:Spring会先解析@Value获取配置值,再传入构造方法* @param freeswitchHost* @param eslPort* @param eslPassword* @param directoryPath* @param defaultCacheRefreshInterval* @param reconnectDelay*/public FreeswitchUserServer(@Value("${gnss.freeswitch.host}") String freeswitchHost,@Value("${gnss.freeswitch.port}") int eslPort,@Value("${gnss.freeswitch.password}") String eslPassword,@Value("${gnss.freeswitch.path:/usr/local/freeswitch/conf/directory/default}") String directoryPath,@Value("${gnss.freeswitch.refresh-interval:300}") int defaultCacheRefreshInterval,@Value("${gnss.freeswitch.delay:5}") int reconnectDelay) {this.reconnectDelay = reconnectDelay;// 初始化缓存刷新间隔this.cacheRefreshInterval = defaultCacheRefreshInterval;// 此时所有参数均已通过@Value注入,可安全创建FreeswitchUtilthis.freeswitchUtil = new FreeswitchUtil(freeswitchHost,eslPort,eslPassword,10,directoryPath);}@PostConstructpublic void init() {connect();startCacheRefreshTask();startConnectionMonitor();}@PreDestroypublic void destroy() {scheduler.shutdownNow();disconnect();}/*** 连接 FreeSWITCH ESL*/private void connect() {try {if (!freeswitchUtil.isClientConnected()) {log.info("正在连接到 FreeSWITCH ESL...");boolean connected = freeswitchUtil.connect();if (connected) {log.info("成功连接到 FreeSWITCH ESL");refreshUserCache();} else {log.error("连接 FreeSWITCH ESL 失败,将在 {} 秒后重试", reconnectDelay);scheduler.schedule(this::connect, reconnectDelay, TimeUnit.SECONDS);}}} catch (Exception e) {log.error("连接 FreeSWITCH ESL 时发生异常: {}", e.getMessage(), e);scheduler.schedule(this::connect, reconnectDelay, TimeUnit.SECONDS);}}/*** 断开 FreeSWITCH ESL 连接*/private void disconnect() {try {if (freeswitchUtil != null) {freeswitchUtil.disconnect();log.info("已断开与 FreeSWITCH ESL 的连接");}} catch (Exception e) {log.error("断开 FreeSWITCH ESL 连接时发生异常: {}", e.getMessage(), e);}}/*** 刷新用户缓存*/private void startCacheRefreshTask() {scheduler.scheduleAtFixedRate(() -> {try {refreshUserCache();} catch (Exception e) {log.error("刷新用户缓存时发生异常: {}", e.getMessage(), e);}}, cacheRefreshInterval, cacheRefreshInterval, TimeUnit.SECONDS);log.info("用户缓存刷新任务已启动,间隔: {} 秒", cacheRefreshInterval);}/*** 连接监控任务*/private void startConnectionMonitor() {scheduler.scheduleAtFixedRate(() -> {try {if (!freeswitchUtil.isClientConnected()) {log.warn("检测到 FreeSWITCH 连接断开,尝试重新连接");connect();}} catch (Exception e) {log.error("连接监控过程中发生异常: {}", e.getMessage(), e);}}, 30, 30, TimeUnit.SECONDS);log.info("连接监控任务已启动,间隔: 30 秒");}/*** 创建新用户* @param username* @param password* @return*/public boolean createUser(String username, String password,String domain) {UserInfo userInfo = new UserInfo();userInfo.setUsername(username);userInfo.setPassword(password);userInfo.setDisplayName(username);userInfo.setDomain(domain);return createUser(userInfo);}/*** 创建新用户* @param userInfo 用户信息* @return 创建结果*/public boolean createUser(UserInfo userInfo) {if (!ensureConnected()) {return false;}try {boolean result = freeswitchUtil.createUser(userInfo.getUsername(),userInfo.getPassword(),userInfo.getDisplayName(),userInfo.getDomain());if (result) {userCache.put(userInfo.getUsername(), userInfo);log.info("成功创建用户: {}", userInfo.getUsername());} else {log.error("创建用户失败: {}", userInfo.getUsername());}return result;} catch (Exception e) {log.error("创建用户时发生异常: {}", e.getMessage(), e);return false;}}/*** 删除用户* @param username 用户名* @return 删除结果*/public boolean deleteUser(String username) {if (!ensureConnected()) {return false;}try {boolean result = freeswitchUtil.deleteUser(username);if (result) {userCache.remove(username);log.info("成功删除用户: {}", username);} else {log.error("删除用户失败: {}", username);}return result;} catch (Exception e) {log.error("删除用户时发生异常: {}", e.getMessage(), e);return false;}}/*** 修改用户密码* @param username 用户名* @param newPassword 新密码* @return 修改结果*/public boolean changePassword(String username, String newPassword) {if (!ensureConnected()) {return false;}try {boolean result = freeswitchUtil.changeUserPassword(username, newPassword);if (result) {UserInfo userInfo = userCache.get(username);if (userInfo != null) {userInfo.setPassword(newPassword);}log.info("成功修改用户密码: {}", username);} else {log.error("修改用户密码失败: {}", username);}return result;} catch (Exception e) {log.error("修改用户密码时发生异常: {}", e.getMessage(), e);return false;}}/*** 检查用户是否在线* @param username 用户名* @return 用户在线状态*/public boolean isUserOnline(String username) {if (!ensureConnected()) {return false;}try {boolean online = freeswitchUtil.isUserOnline(username);UserInfo userInfo = userCache.get(username);if (userInfo != null) {userInfo.setOnline(online);if (online) {userInfo.setRegistrationTime(System.currentTimeMillis());}}return online;} catch (Exception e) {log.error("检查用户在线状态时发生异常: {}", e.getMessage(), e);return false;}}/*** 获取用户信息* @param username 用户名* @return 用户信息,如果不存在则返回null*/public UserInfo getUser(String username) {UserInfo userInfo = userCache.get(username);if (userInfo != null) {userInfo.setOnline(isUserOnline(username));}return userInfo;}/*** 获取所有用户信息* @return 所有用户信息的映射*/public ConcurrentMap<String, UserInfo> getAllUsers() {refreshOnlineStatus();return userCache;}/*** 刷新用户缓存*/public void refreshUserCache() {if (!ensureConnected()) {return;}try {log.info("开始刷新用户缓存...");// 在实际应用中,这里应该从配置文件或数据库加载用户// 简化示例,仅刷新在线状态refreshOnlineStatus();log.info("用户缓存刷新完成,当前用户数: {}", userCache.size());} catch (Exception e) {log.error("刷新用户缓存时发生异常: {}", e.getMessage(), e);}}private void refreshOnlineStatus() {userCache.forEach((username, userInfo) -> {boolean online = isUserOnline(username);userInfo.setOnline(online);if (online) {userInfo.setRegistrationTime(System.currentTimeMillis());}});}private boolean ensureConnected() {if (!freeswitchUtil.isClientConnected()) {log.warn("FreeSWITCH 未连接,尝试重新连接...");connect();return freeswitchUtil.isClientConnected();}return true;}@Getterpublic static class UserInfo {private String username;private String password;private String displayName;private String domain;private boolean online;private long registrationTime;private String sipProfile;private String userContext;public UserInfo setUsername(String username) {this.username = username;return this;}public UserInfo setPassword(String password) {this.password = password;return this;}public UserInfo setDisplayName(String displayName) {this.displayName = displayName;return this;}public UserInfo setDomain(String domain) {this.domain = domain;return this;}public UserInfo setOnline(boolean online) {this.online = online;return this;}public UserInfo setRegistrationTime(long registrationTime) {this.registrationTime = registrationTime;return this;}public UserInfo setSipProfile(String sipProfile) {this.sipProfile = sipProfile;return this;}public UserInfo setUserContext(String userContext) {this.userContext = userContext;return this;}}
}

http://www.dtcms.com/a/289174.html

相关文章:

  • 【图像处理基石】什么是畸变校正?
  • AG32:解锁MCU+FPGA应用新姿势,功能与实战全解析
  • JavaScript 语言基础详解
  • 金融大前端中的 AI 应用:智能投资顾问与风险评估
  • SSE和WebSocket区别到底是什么
  • FAN-UNET:用于生物医学图像分割增强模型
  • Python 中的闭包:原理、应用与实践
  • 2025.7.20总结-实战演讲
  • 单细胞空间多组学揭示肿瘤相关成纤维细胞的保守空间亚型和细胞邻域-空间细胞亚群细分代码实现
  • 常用的三种加密算法
  • 金融工程、金融与经济学知识点
  • ICT模拟零件测试方法--电容测试
  • 算法讲解--复写零
  • 【OpenGL 渲染器开发笔记】5 顶点数据
  • LeetCode第337题_打家劫舍III
  • Spring Boot 配置文件解析
  • 《深入C++多态机制:从虚函数表到运行时类型识别》​
  • 牛客NC14661 简单的数据结构(deque双端队列)
  • python学智能算法(二十六)|SVM-拉格朗日函数构造
  • 非广告!! 【实用工具推荐】自用多功能视频播放器-РotРlayer详细图文安装使用教程
  • 【安卓笔记】RecyclerView之ItemDecoration实现吸顶效果
  • codepen使用
  • FFmpeg 图片处理
  • 数据结构 | 栈:构建高效数据处理的基石
  • 【高等数学】第四章 不定积分——第三节 分部积分法
  • 【深度学习新浪潮】什么是robotaxi?
  • 【设计模式C#】享元模式(用于解决多次创建对象而导致的性能问题)
  • MPLS转发
  • windows C#-本地函数
  • Docker Compose 配置