文章目录
- 一、SIP通信方法介绍
- 1.1 核心方法(Core Methods)
- 1.2 扩展方法(Extension Methods)
- 二、源码分析
- 2.1 SIP协议处理过程
- 2.2 查询设备信息的sip过程(CmdType=DeviceInfo)
- 2.2.1 摄像机注册成功后,wvp会发命令查询设备信息
- 2.2.2 查询信令发出
- 2.2.3 处理设备查询返回的XML
- 2.3 查询设备通道(目录)的sip过程(CmdType=Catalog)
2024年6月20日下载的wvp-GB28181-pro,版本号为2.7.2,使用ZLMediakit主干版本。
本节阐述sip通信步骤对应到wvp-gb28181-Pro中的代码。
一、SIP通信方法介绍
sip通信主要有6个核心方法和8个扩展方法。
1.1 核心方法(Core Methods)
SIP请求消息(方法Method) | SIP操作 |
---|---|
INVITE | 会话邀请 |
ACK | 确认会话邀请 |
CANCEL | 取消会话邀请 |
BYE | 结束会话 |
REGISTER | 注册 |
OPTIONS | 查询服务器能力 |
1.2 扩展方法(Extension Methods)
SIP请求消息(方法Method) | SIP操作 |
---|---|
Subscribe | 与所使用的用户代理商建立订阅,获取有关特定事件的通知 |
NOTIFY | NOTIFY是用来由用户代理传达特定事件的发生 |
PUBLISH | PUBLISH用于由用户代理发送的事件的状态信息 |
REFER | REFER用于由一个用户代理来指另一个用户代理访问URI的对话框 |
INFO | INFO所使用的用户代理发送呼叫信令信息,与它建立了一个媒体会话其他用户代理 |
UPDATE | UPDATE用于修改会话的状态不改变对话的状态 |
PRACK | PRACK用于确认收到临时响应(1XX)可靠传输 |
MESSAGE | 用来发送即时消息 |
二、源码分析
2.1 SIP协议处理过程
wvp针对这14个方法(注意wvp并没有全部实现14个方法),相应写了处理对象,如下图
将这些处理器RegisterRequestProcessor、MessageRequestProcessor等等Processor加入到SIPProcessorObserver观察者的容器中。
每一个processor中都有这个方法,用来添加消息处理的订阅。processor方法实现了InitializingBean类(不懂的可以去查),通过afterPropertiesSet()方法完成了消息的订阅。
@Override
public void afterPropertiesSet() throws Exception {
// 添加消息处理的订阅
sipProcessorObserver.addRequestProcessor(method, this);
}
当有相应方法的sip消息到来时,根据是Request还是Response,观察者将调用相应的Processor进行处理。
观察者SIPProcessorObserver类,在如下的包路径下
com.genersoft.iot.vmp.gb28181.transmit
观察者中的处理Request源码如下
public void processRequest(RequestEvent requestEvent) {
String method = requestEvent.getRequest().getMethod();
ISIPRequestProcessor sipRequestProcessor = requestProcessorMap.get(method);
if (sipRequestProcessor == null) {
logger.warn("不支持方法{}的request", method);
// TODO 回复错误玛
return;
}
requestProcessorMap.get(method).process(requestEvent);
}
处理Response的源码如下
@Override
@Async("taskExecutor")
public void processResponse(ResponseEvent responseEvent) {
Response response = responseEvent.getResponse();
int status = response.getStatusCode();
// Success
if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) {
CSeqHeader cseqHeader = (CSeqHeader) responseEvent.getResponse().getHeader(CSeqHeader.NAME);
String method = cseqHeader.getMethod();
ISIPResponseProcessor sipRequestProcessor = responseProcessorMap.get(method);
if (sipRequestProcessor != null) {
sipRequestProcessor.process(responseEvent);
}
if (status != Response.UNAUTHORIZED && responseEvent.getResponse() != null && sipSubscribe.getOkSubscribesSize() > 0 ) {
CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
if (callIdHeader != null) {
SipSubscribe.Event subscribe = sipSubscribe.getOkSubscribe(callIdHeader.getCallId());
if (subscribe != null) {
SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
sipSubscribe.removeOkSubscribe(callIdHeader.getCallId());
subscribe.response(eventResult);
}
}
}
} else if ((status >= Response.TRYING) && (status < Response.OK)) {
// 增加其它无需回复的响应,如101、180等
} else {
logger.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase());
if (responseEvent.getResponse() != null && sipSubscribe.getErrorSubscribesSize() > 0 ) {
CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
if (callIdHeader != null) {
SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId());
if (subscribe != null) {
SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
subscribe.response(eventResult);
sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId());
}
}
}
if (responseEvent.getDialog() != null) {
responseEvent.getDialog().delete();
}
}
}
由于MessageRequestProcessor类型的消息较多,wvp将Message再分为多个handler,分别在对应的包下面,然后根据cmdType调用对应的handler。如cmdType = “Catalog”、cmdType = “DeviceInfo”。
2.2 查询设备信息的sip过程(CmdType=DeviceInfo)
设备信息查询流程示意图如下:
对应《GBT 28181-2016 公共安全视频监控联网系统信息传输、交换、控制技术要求》文件中的J.10.1到J.10.8节。如果需要GB文件,可以评论区留下邮箱,我看到就会发送(私信不发)
2.2.1 摄像机注册成功后,wvp会发命令查询设备信息
在RegisterRequestProcessor中会调用online方法,这里看不明白的话,可以去看 wvp-GB28181-pro 源码分析-服务启动流程及IPC注册(一)这篇文章。
online方法将device信息写到redis和mysql
2.2.2 查询信令发出
deviceInfoQuery方法会发出一个message请求,代码如下:
/**
* 查询设备信息
*
* @param device 视频设备
*/
@Override
public void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException {
StringBuffer catalogXml = new StringBuffer(200);
String charset = device.getCharset();
catalogXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
catalogXml.append("<Query>\r\n");
catalogXml.append("<CmdType>DeviceInfo</CmdType>\r\n");
catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
catalogXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
catalogXml.append("</Query>\r\n");
Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request);
}
2.2.3 处理设备查询返回的XML
DeviceInfoResponseMessageHandler处理类在如下包路径下
com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd
代码如下:
@Override
public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
logger.debug("接收到DeviceInfo应答消息");
// 检查设备是否存在, 不存在则不回复
if (device == null || !device.isOnLine()) {
logger.warn("[接收到DeviceInfo应答消息,但是设备已经离线]:" + (device != null ? device.getDeviceId():"" ));
return;
}
SIPRequest request = (SIPRequest) evt.getRequest();
try {
rootElement = getRootElement(evt, device.getCharset());
if (rootElement == null) {
logger.warn("[ 接收到DeviceInfo应答消息 ] content cannot be null, {}", evt.getRequest());
try {
responseAck((SIPRequest) evt.getRequest(), Response.BAD_REQUEST);
} catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] DeviceInfo应答消息 BAD_REQUEST: {}", e.getMessage());
}
return;
}
Element deviceIdElement = rootElement.element("DeviceID");
String channelId = deviceIdElement.getTextTrim();
String key = DeferredResultHolder.CALLBACK_CMD_DEVICEINFO + device.getDeviceId() + channelId;
device.setName(getText(rootElement, "DeviceName"));
device.setManufacturer(getText(rootElement, "Manufacturer"));
device.setModel(getText(rootElement, "Model"));
device.setFirmware(getText(rootElement, "Firmware"));
if (ObjectUtils.isEmpty(device.getStreamMode())) {
device.setStreamMode("UDP");
}
deviceService.updateDevice(device);
RequestMessage msg = new RequestMessage();
msg.setKey(key);
msg.setData(device);
deferredResultHolder.invokeAllResult(msg);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
try {
// 回复200 OK
responseAck(request, Response.OK);
} catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] DeviceInfo应答消息 200: {}", e.getMessage());
}
}
设备信息 通过 deviceService.updateDevice(device),更新到redis和mysql,具体可以自己跟一下。
2.3 查询设备通道(目录)的sip过程(CmdType=Catalog)
流程示意图如下:
同样,在摄像机注册成功后,wvp发起查询设备的目录信息请求,同样在DeviceServiceImpl类中的online方法体中发起。
@Override
public void sync(Device device) {
if (catalogResponseMessageHandler.isSyncRunning(device.getDeviceId())) {
logger.info("开启同步时发现同步已经存在");
return;
}
int sn = (int)((Math.random()*9+1)*100000);
catalogResponseMessageHandler.setChannelSyncReady(device, sn);
try {
sipCommander.catalogQuery(device, sn, event -> {
String errorMsg = String.format("同步通道失败,错误码: %s, %s", event.statusCode, event.msg);
catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), errorMsg);
});
} catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[同步通道], 信令发送失败:{}", e.getMessage() );
String errorMsg = String.format("同步通道失败,信令发送失败: %s", e.getMessage());
catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), errorMsg);
}
}
在CatalogResponseMessageHandler类中处理返回的XML 信令
@Override
public void handForDevice(RequestEvent evt, Device device, Element element) {
taskQueue.offer(new HandlerCatchData(evt, device, element));
// 回复200 OK
try {
responseAck((SIPRequest) evt.getRequest(), Response.OK);
} catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage());
}
// 已经开启消息处理则跳过
if (processing.compareAndSet(false, true)) {
taskExecutor.execute(() -> {
while (!taskQueue.isEmpty()) {
// 全局异常捕获,保证下一条可以得到处理
try {
HandlerCatchData take = taskQueue.poll();
Element rootElement = null;
try {
rootElement = getRootElement(take.getEvt(), take.getDevice().getCharset());
} catch (DocumentException e) {
logger.error("[xml解析] 失败: ", e);
continue;
}
if (rootElement == null) {
logger.warn("[ 收到通道 ] content cannot be null, {}", evt.getRequest());
continue;
}
Element deviceListElement = rootElement.element("DeviceList");
Element sumNumElement = rootElement.element("SumNum");
Element snElement = rootElement.element("SN");
int sumNum = Integer.parseInt(sumNumElement.getText());
if (sumNum == 0) {
logger.info("[收到通道]设备:{}的: 0个", take.getDevice().getDeviceId());
// 数据已经完整接收
storager.cleanChannelsForDevice(take.getDevice().getDeviceId());
catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
} else {
Iterator<Element> deviceListIterator = deviceListElement.elementIterator();
if (deviceListIterator != null) {
List<DeviceChannel> channelList = new ArrayList<>();
List<String> parentChannelIds = new ArrayList<>();
// 遍历DeviceList
while (deviceListIterator.hasNext()) {
Element itemDevice = deviceListIterator.next();
Element channelDeviceElement = itemDevice.element("DeviceID");
if (channelDeviceElement == null) {
continue;
}
DeviceChannel channel = XmlUtil.channelContentHandler(itemDevice, device, null);
if (channel == null) {
logger.info("[收到目录订阅]:但是解析失败 {}", new String(evt.getRequest().getRawContent()));
continue;
}
if (channel.getParentId() != null && channel.getParentId().equals(sipConfig.getId())) {
channel.setParentId(null);
}
SipUtils.updateGps(channel, device.getGeoCoordSys());
channel.setDeviceId(take.getDevice().getDeviceId());
channelList.add(channel);
}
int sn = Integer.parseInt(snElement.getText());
catalogDataCatch.put(take.getDevice().getDeviceId(), sn, sumNum, take.getDevice(), channelList);
logger.info("[收到通道]设备: {} -> {}个,{}/{}", take.getDevice().getDeviceId(), channelList.size(), catalogDataCatch.get(take.getDevice().getDeviceId()) == null ? 0 : catalogDataCatch.get(take.getDevice().getDeviceId()).size(), sumNum);
if (catalogDataCatch.get(take.getDevice().getDeviceId()).size() == sumNum) {
// 数据已经完整接收, 此时可能存在某个设备离线变上线的情况,但是考虑到性能,此处不做处理,
// 目前支持设备通道上线通知时和设备上线时向上级通知
boolean resetChannelsResult = storager.resetChannels(take.getDevice().getDeviceId(), catalogDataCatch.get(take.getDevice().getDeviceId()));
if (!resetChannelsResult) {
String errorMsg = "接收成功,写入失败,共" + sumNum + "条,已接收" + catalogDataCatch.get(take.getDevice().getDeviceId()).size() + "条";
catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), errorMsg);
} else {
catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
}
}
}
}
} catch (Exception e) {
logger.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest());
logger.error("[收到通道] 异常内容: ", e);
}
}
processing.set(false);
});
}
}
本节设备信息查询和通道查询流程就梳理完了。