sip 监听创建/销毁

支持 hotReload
This commit is contained in:
shikong 2023-08-09 16:09:23 +08:00
parent 5483effad4
commit f2c78c4b78
15 changed files with 691 additions and 0 deletions

View File

@ -18,4 +18,16 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -80,6 +80,11 @@
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,56 @@
package cn.skcks.docking.gb28181.common.json;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@SuppressWarnings("unused")
@Schema(title = "返回结果")
public class JsonResponse<T> {
@Schema(title = "状态码")
private int code;
@Schema(title = "响应消息")
private String msg;
@Schema(title = "响应数据")
private T data;
public JsonResponse(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> JsonResponse<T> success(T data) {
return JsonResponse.build(data, ResponseStatus.OK);
}
public static <T> JsonResponse<T> success(T data, String message) {
return JsonResponse.build(data, ResponseStatus.OK.getCode(), message);
}
public static <T> JsonResponse<T> error(T data) {
return JsonResponse.build(data, ResponseStatus.INTERNAL_SERVER_ERROR);
}
public static <T> JsonResponse<T> error(T data, String message) {
return JsonResponse.build(data, ResponseStatus.INTERNAL_SERVER_ERROR.getCode(), message);
}
public static <T> JsonResponse<T> build(ResponseStatus status) {
return new JsonResponse<>(status.getCode(), status.getMessage(),null);
}
public static <T> JsonResponse<T> build(T data, ResponseStatus status) {
return new JsonResponse<>(status.getCode(), status.getMessage(), data);
}
public static <T> JsonResponse<T> build(ResponseStatus status,String message) {
return new JsonResponse<>(status.getCode(), message, null);
}
public static <T> JsonResponse<T> build(T data, int status, String msg) {
return new JsonResponse<>(status, msg, data);
}
}

View File

@ -0,0 +1,81 @@
package cn.skcks.docking.gb28181.common.json;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@SuppressWarnings("unused")
public class JsonUtils {
public static final ObjectMapper mapper;
public static final ObjectMapper compressMapper;
static {
mapper = new ObjectMapper();
compressMapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
compressMapper.disable(SerializationFeature.INDENT_OUTPUT);
// 返回内容中 不含 值为 null 的字段
// mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
compressMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 如果json中有新增的字段并且是实体类类中不存在的不报错
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
compressMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
compressMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
// 允许出现特殊字符和转义符
mapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
compressMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
// 允许出现单引号
mapper.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES.mappedFeature(), true);
compressMapper.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES.mappedFeature(), true);
}
public static <T> T parse(String json, Class<T> clazz) {
try {
return mapper.readValue(json, clazz);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static <T> T parse(String json, TypeReference<T> clazz) {
try {
return mapper.readValue(json, clazz);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String toCompressJson(Object obj) {
try {
return compressMapper.writeValueAsString(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String toJson(Object obj) {
try {
return mapper.writeValueAsString(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static <T> T convert(Object object, Class<T> clazz) {
return JsonUtils.parse(JsonUtils.toJson(object), clazz);
}
}

View File

@ -0,0 +1,60 @@
package cn.skcks.docking.gb28181.common.json;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
@SuppressWarnings("unused")
public enum ResponseStatus {
UNKNOWN(-1, "Unknown"),
UNDEFINED(0, "Undefined"),
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NO_CONTENT(204, "No Content"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
FOUND(302, "Found"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
URI_TOO_LONG(414, "URI Too Long"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
RANGE_NOT_SATISFIABLE(416, "Range Not Satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
TOO_MANY_REQUESTS(429, "Too Many Requests"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable");
private final int code;
private final String message;
public static ResponseStatus getByCode(int code) {
for (ResponseStatus status : values()) {
if (status.getCode() == code) {
return status;
}
}
return null;
}
public static ResponseStatus valueOf(int code) {
return getByCode(code);
}
}

View File

@ -19,6 +19,17 @@
</properties>
<dependencies>
<dependency>
<groupId>cn.skcks.docking.gb28181</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- sip协议栈 -->
<dependency>
<groupId>javax.sip</groupId>

View File

@ -0,0 +1,42 @@
package cn.skcks.docking.gb28181.config.sip;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "gb28181.sip", ignoreInvalidFields = true)
@Order(0)
@Data
public class SipConfig {
private List<String> ip;
private List<String> showIp;
private Integer port;
private String domain;
private String id;
private String password;
Integer ptzSpeed = 50;
Integer registerTimeInterval = 120;
private boolean alarm;
public List<String> getShowIp() {
if (this.showIp == null) {
return this.ip;
}
return showIp;
}
}

View File

@ -0,0 +1,6 @@
package cn.skcks.docking.gb28181.core.sip;
public interface SipService {
void run();
void stop();
}

View File

@ -0,0 +1,88 @@
package cn.skcks.docking.gb28181.core.sip;
import cn.skcks.docking.gb28181.config.sip.SipConfig;
import cn.skcks.docking.gb28181.core.sip.properties.DefaultProperties;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.sip.*;
import java.util.ArrayList;
import java.util.List;
import java.util.TooManyListenersException;
@Slf4j
@RequiredArgsConstructor
@Data
@Service
public class SipServiceImpl implements SipService{
private final SipFactory sipFactory = SipFactory.getInstance();
private final SipConfig sipConfig;
private final List<SipProviderImpl> pool = new ArrayList<>(2);
private SipStackImpl sipStack;
@Override
public void run() {
sipFactory.setPathName("gov.nist");
sipConfig.getIp().parallelStream().forEach(ip->{
listen(ip, sipConfig.getPort());
});
}
@Override
public void stop() {
if(sipStack == null){
return;
}
sipStack.closeAllSockets();
pool.parallelStream().forEach(sipProvider -> {
ListeningPoint listen = sipProvider.getListeningPoint();
log.debug("移除监听 {}://{}:{}",listen.getTransport(),listen.getIPAddress(),listen.getPort());
sipProvider.removeListeningPoints();
try{
sipStack.deleteListeningPoint(listen);
sipStack.deleteSipProvider(sipProvider);
} catch (Exception ignore){
}
});
pool.clear();
}
public void listen(String ip, int port){
try{
sipStack = (SipStackImpl)sipFactory.createSipStack(DefaultProperties.getProperties("GB28181_SIP_LOG",true));
try {
ListeningPoint tcpListen = sipStack.createListeningPoint(ip, port, "TCP");
SipProviderImpl tcpSipProvider = (SipProviderImpl) sipStack.createSipProvider(tcpListen);
tcpSipProvider.setDialogErrorsAutomaticallyHandled();
pool.add(tcpSipProvider);
log.info("[sip] 监听 tcp://{}:{}", ip, port);
} catch (TransportNotSupportedException
| ObjectInUseException
| InvalidArgumentException e) {
log.error("[sip] tcp://{}:{} 监听失败, 请检查端口是否被占用, 错误信息 => {}", ip, port, e.getMessage());
}
try {
ListeningPoint udpListen = sipStack.createListeningPoint(ip, port, "UDP");
SipProviderImpl udpSipProvider = (SipProviderImpl) sipStack.createSipProvider(udpListen);
pool.add(udpSipProvider);
log.info("[sip] 监听 udp://{}:{}", ip, port);
} catch (TransportNotSupportedException
| ObjectInUseException
| InvalidArgumentException e) {
log.error("[sip] udp://{}:{} 监听失败, 请检查端口是否被占用, 错误信息 => {}", ip, port, e.getMessage());
}
} catch (Exception e){
log.error("[sip] {}:{} 监听失败, 请检查端口是否被占用, 错误信息 => {}",ip,port, e.getMessage());
}
}
}

View File

@ -0,0 +1,79 @@
package cn.skcks.docking.gb28181.core.sip.logger;
import gov.nist.core.CommonLogger;
import gov.nist.core.ServerLogger;
import gov.nist.core.StackLogger;
import gov.nist.javax.sip.message.SIPMessage;
import gov.nist.javax.sip.stack.SIPTransactionStack;
import javax.sip.SipStack;
import java.util.Properties;
public class ServerLoggerImpl implements ServerLogger {
private boolean showLog = true;
protected StackLogger stackLogger;
@Override
public void closeLogFile() {
}
@Override
public void logMessage(SIPMessage message, String from, String to, boolean sender, long time) {
if (!showLog) {
return;
}
String log = (sender ? "发送: 目标 =>" + from : "接收: 来自 =>" + to) + "\r\n" + message;
this.stackLogger.logInfo(log);
}
@Override
public void logMessage(SIPMessage message, String from, String to, String status, boolean sender, long time) {
if (!showLog) {
return;
}
String log = (sender ? "发送: 目标 =>" + from : "接收: 来自 =>" + to) + "\r\n" + message;
this.stackLogger.logInfo(log);
}
@Override
public void logMessage(SIPMessage message, String from, String to, String status, boolean sender) {
if (!showLog) {
return;
}
String log = (sender ? "发送: 目标 =>" + from : "接收: 来自 =>" + to) + "\r\n" + message;
this.stackLogger.logInfo(log);
}
@Override
public void logException(Exception ex) {
if (!showLog) {
return;
}
this.stackLogger.logException(ex);
}
@Override
public void setStackProperties(Properties stackProperties) {
if (!showLog) {
return;
}
String TRACE_LEVEL = stackProperties.getProperty("gov.nist.javax.sip.TRACE_LEVEL");
if (TRACE_LEVEL != null) {
showLog = true;
}
}
@Override
public void setSipStack(SipStack sipStack) {
if (!showLog) {
return;
}
if(sipStack instanceof SIPTransactionStack) {
SIPTransactionStack sipStack1 = (SIPTransactionStack) sipStack;
this.stackLogger = CommonLogger.getLogger(SIPTransactionStack.class);
}
}
}

View File

@ -0,0 +1,105 @@
package cn.skcks.docking.gb28181.core.sip.logger;
import gov.nist.core.StackLogger;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Properties;
@Slf4j
@Component
public class StackLoggerImpl implements StackLogger {
private final static Logger logger = LoggerFactory.getLogger(StackLoggerImpl.class);
@Override
public void logStackTrace() {}
@Override
public void logStackTrace(int traceLevel) {
System.out.println("traceLevel: " + traceLevel);
}
@Override
public int getLineCount() {
return 0;
}
@Override
public void logException(Throwable ex) {
}
@Override
public void logDebug(String message) {
}
@Override
public void logDebug(String message, Exception ex) {
}
@Override
public void logTrace(String message) {
logger.trace(message);
}
@Override
public void logFatalError(String message) {
}
@Override
public void logError(String message) {
}
@Override
public boolean isLoggingEnabled() {
return true;
}
@Override
public boolean isLoggingEnabled(int logLevel) {
return true;
}
@Override
public void logError(String message, Exception ex) {
// logger.error(message);
}
@Override
public void logWarning(String message) {
logger.warn(message);
}
@Override
public void logInfo(String message) {
logger.info(message);
}
@Override
public void disableLogging() {
}
@Override
public void enableLogging() {
}
@Override
public void setBuildTimeStamp(String buildTimeStamp) {
}
@Override
public void setStackProperties(Properties stackProperties) {
}
@Override
public String getLoggerName() {
return null;
}
}

View File

@ -0,0 +1,62 @@
package cn.skcks.docking.gb28181.core.sip.properties;
import cn.skcks.docking.gb28181.config.sip.SipConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
/**
* 获取sip默认配置
*/
public class DefaultProperties {
public static Properties getProperties(String name, boolean sipLog) {
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", name);
// properties.setProperty("javax.sip.IP_ADDRESS", ip);
// 关闭自动会话
properties.setProperty("javax.sip.AUTOMATIC_DIALOG_SUPPORT", "off");
/**
* 完整配置参考 gov.nist.javax.sip.SipStackImpl需要下载源码
* gov/nist/javax/sip/SipStackImpl.class
* sip消息的解析在 gov.nist.javax.sip.stack.UDPMessageChannel的processIncomingDataPacket方法
*/
// * gov/nist/javax/sip/SipStackImpl.class
// 接收所有notify请求即使没有订阅
properties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true");
properties.setProperty("gov.nist.javax.sip.AUTOMATIC_DIALOG_ERROR_HANDLING", "false");
properties.setProperty("gov.nist.javax.sip.CANCEL_CLIENT_TRANSACTION_CHECKED", "true");
// 为_NULL _对话框传递_终止的_事件
properties.setProperty("gov.nist.javax.sip.DELIVER_TERMINATED_EVENT_FOR_NULL_DIALOG", "true");
// 是否自动计算content length的实际长度默认不计算
properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true");
// 会话清理策略
properties.setProperty("gov.nist.javax.sip.RELEASE_REFERENCES_STRATEGY", "Normal");
// 处理由该服务器处理的基于底层TCP的保持生存超时
properties.setProperty("gov.nist.javax.sip.RELIABLE_CONNECTION_KEEP_ALIVE_TIMEOUT", "60");
// 获取实际内容长度不使用header中的长度信息
properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true");
// 线程可重入
properties.setProperty("gov.nist.javax.sip.REENTRANT_LISTENER", "true");
// 定义应用程序打算多久审计一次 SIP 堆栈了解其内部线程的健康状况该属性指定连续审计之间的时间以毫秒为单位
properties.setProperty("gov.nist.javax.sip.THREAD_AUDIT_INTERVAL_IN_MILLISECS", "30000");
// properties.setProperty("gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", "gov.nist.javax.sip.stack.NioMessageProcessorFactory");
/**
* sip_server_log.log sip_debug_log.log ERROR, INFO, WARNING, OFF, DEBUG, TRACE
*/
Logger logger = LoggerFactory.getLogger(SipConfig.class);
if (sipLog) {
properties.setProperty("gov.nist.javax.sip.STACK_LOGGER", "cn.skcks.docking.gb28181.core.sip.logger.StackLoggerImpl");
properties.setProperty("gov.nist.javax.sip.SERVER_LOGGER", "cn.skcks.docking.gb28181.core.sip.logger.ServerLoggerImpl");
properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "true");
logger.info("[SIP日志]已开启");
}else {
logger.info("[SIP日志]已关闭");
}
return properties;
}
}

View File

@ -0,0 +1,53 @@
package cn.skcks.docking.gb28181.starter;
import cn.skcks.docking.gb28181.common.json.JsonUtils;
import cn.skcks.docking.gb28181.config.sip.SipConfig;
import cn.skcks.docking.gb28181.core.sip.SipService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Order(0)
@Slf4j
@RequiredArgsConstructor
@Component
public class SipStarter implements SmartLifecycle {
private final SipConfig sipConfig;
private final SipService sipService;
private boolean isRunning;
@Override
public void start() {
if(checkConfig()){
isRunning = true;
log.debug("sip 服务 启动");
sipService.run();
}
}
@Override
public void stop() {
log.debug("sip 服务 关闭");
sipService.stop();
isRunning = false;
}
@Override
public boolean isRunning() {
return isRunning;
}
public boolean checkConfig(){
log.debug("sip 配置信息 => \n{}", JsonUtils.toJson(sipConfig));
if(CollectionUtils.isEmpty(sipConfig.getIp())){
log.error("sip ip 配置错误, 请检查配置是否正确");
return false;
}
return true;
}
}

View File

@ -21,6 +21,12 @@
</properties>
<dependencies>
<dependency>
<groupId>cn.skcks.docking.gb28181</groupId>
<artifactId>gb28181-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.skcks.docking.gb28181</groupId>
<artifactId>common</artifactId>

View File

@ -0,0 +1,25 @@
server:
port: 28181
gb28181:
# 作为28181服务器的配置
sip:
# [必须修改] 本机的IP对应你的网卡监听什么ip就是使用什么网卡
# 如果不明白就使用0.0.0.0,大部分情况都是可以的
# 请不要使用127.0.0.1任何包括localhost在内的域名都是不可以的。
ip:
- 10.10.10.20
- 10.27.0.6
# [可选] 28181服务监听的端口
port: 5060
# 根据国标6.1.2中规定domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码由省级、市级、区级、基层编号组成参照GB/T 2260-2007
# 后两位为行业编码定义参照附录D.3
# 3701020049标识山东济南历下区 信息行业接入
# [可选]
domain: 4405010000
# [可选]
id: 44050100002000000002
# [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验
password: 123456
# 是否存储alarm信息
alarm: true