全網第一篇SIP協議之GB28181註冊(JAVA版本)
鑑於網上大部分關於SIP註冊伺服器編寫都是C/C++/python,故開此貼,JAVA實現也貼出分享
GB28181定義了了 基於SIP架構的 視訊監控互聯規範,而對於多數私有協議實現的監控系統如果想接入SIP架構,就要藉助閘道器,GB28181 規範了實現 SIP 監控域與非SIP 監控域互聯。
以下是我在實際使用過程中總結的一些問題:
1. 當客戶端第一次接入時,客戶端將持續向Server端傳送REGISTER訊息,直到Server端回覆"200 OK"後結束;
2. GB28181的註冊流程牽扯使用者認證,所以相對比較複雜,不過這也是安防通訊安全方面的一個亮點;
它的註冊流程如下圖:
用抓包工具看,如下圖所示
註冊流程:
1. 客戶端向伺服器無限期傳送Register訊息:
這裡客戶端期初發送的Register訊息為最簡單的訊息
2.當伺服器接收到訊息後,回送一個 401 訊息“Unauthorized”,並在訊息包頭新增如下欄位:
如下所示,這就是客戶端接到401-Unauthorized之後再次發來的REGISTER訊息,並且還附帶了Auth欄位, 而第一次REGISTER訊息是沒有這個欄位的:
完整的401回覆如下(通過抓包工具Wireshark抓到的):
Via: SIP/2.0/UDP 172.24.20.109:5060;rport=5060;received=172.24.20.109;branch=z9hG4bK352707374
From: <sip: [email protected]:5060>;tag=2109371333
To: <sip:[email protected]:5060>;tag=888
Call-ID: [email protected]
CSeq: 1 REGISTER
WWW-Authenticate: Digest realm="3402000000",nonce="1677f194104d46aea6c9f8aebe507017"
Content-Length: 0
第二次REGISTER,也就是附帶了Auth欄位的報文:
ìKC8¯)à¯Eóß[email protected] @×Ǭm¬ÄÄßyÁREGISTER sip:[email protected]:5060 SIP/2.0
Via: SIP/2.0/UDP 172.24.20.109:5060;rport;branch=z9hG4bK742316145
Route: <sip:[email protected]:5060;lr>
From: <sip:[email protected]:5060>;tag=2109371333
To: <sip:[email protected]:5060>
Call-ID: [email protected]
CSeq: 2 REGISTER
Contact: <sip:[email protected]:5060>
Authorization: Digest username="34020000001320000002", realm="3402000000", nonce="1677f194104d46aea6c9f8aebe507017", uri="sip:[email protected]:5060", response="dca920f418cecae456bc1566c5ac7da5", algorithm=MD5
Max-Forwards: 70
User-Agent: SIP UAS V2.1.2.438058
Expires: 3600
Content-Length: 0
驗證演算法如下:
HA1=MD5(username:realm:passwd) #username和realm在欄位“Authorization”中可以找到,passwd這個是由客戶端和伺服器協商得到的,一般情況下UAC端存一個UAS也知道的密碼就行了
HA2=MD5(Method:Uri) #Method一般有INVITE, ACK, OPTIONS, BYE, CANCEL, REGISTER;Uri可以在欄位“Authorization”找到
response = MD5(HA1:nonce:HA2)
演算法來源:http://tools.ietf.org/html/rfc2069 [Page 6]
關鍵認證演算法的JAVA實現(注意冒號是必須要的):
public static void main(String[] args) throws Exception {
String ha1 = md5("34020000001320000002" + ":" + "3402000000" + ":" + "admin123", ""); //HA1=MD5(username:realm:passwd)
String ha2 = md5("REGISTER" + ":" + "sip:[email protected]:5060", ""); //HA2=MD5(Method:Uri)
String response = ha1 + ":" + "326d59f91b6e448fa461fcacd9161abe" + ":" + ha2;
System.out.println("MD5加密後的字串為:encodeStr="+md5(response, ""));
}
MD5工具類
import org.apache.commons.codec.digest.DigestUtils;
public class MD5Utils {
/**
* MD5方法
*
* @param text 明文
* @param key 金鑰
* @return 密文
* @throws Exception
*/
public static String md5(String text, String key) {
//加密後的字串
String encodeStr=DigestUtils.md5Hex(text + key);
return encodeStr;
}
/**
* MD5驗證方法
*
* @param text 明文
* @param key 金鑰
* @param md5 密文
* @return true/false
* @throws Exception
*/
public static boolean verify(String text, String key, String md5) throws Exception {
//根據傳入的金鑰進行驗證
String md5Text = md5(text, key);
if(md5Text.equalsIgnoreCase(md5)) {
System.out.println("MD5驗證通過");
return true;
}
return false;
}
}
最後執行流程如下:
註冊伺服器核心程式碼:
import java.text.ParseException;
import java.util.TooManyListenersException;
import javax.sip.InvalidArgumentException;
import javax.sip.ObjectInUseException;
import javax.sip.PeerUnavailableException;
import javax.sip.SipException;
import javax.sip.TransportNotSupportedException;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SIPMain {
protected Logger logger = LoggerFactory.getLogger(SIPMain.class);
public void run() {
//使用者名稱,IP地址,埠
try {
int port = 5060;
SipLayer sipLayer = new SipLayer("admin" , "172.24.20.26" , port); //本地
//SipLayer sipLayer = new SipLayer("admin","xx.xx.xx.xx",port); //阿里雲上的IP VECS01532
sipLayer.setMessageProcessor(new MessageProcessorImpl());
System.out.println("服務啟動完畢, 已經在"+port+"埠監聽訊息\n\n");
} catch (PeerUnavailableException e) {
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
} catch (TransportNotSupportedException e) {
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
} catch (ObjectInUseException e) {
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
} catch (InvalidArgumentException e) {
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
} catch (TooManyListenersException e) {
e.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(e));
}
}
/**
* 這個方法暫時用不上,目前系統沒有需要主動傳送訊息給SIP終端裝置的業務場景
* @throws InvalidArgumentException
* @throws TooManyListenersException
* @throws ParseException
* @throws SipException
*/
public void sendMsg() throws InvalidArgumentException, TooManyListenersException, ParseException, SipException{
SipLayer sipLayer = new SipLayer("admin","127.0.0.1",5060);
sipLayer.sendMessage(sipLayer.getUsername(), sipLayer.getHost(), "test message");
}
}
SipLayer.java程式碼:
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.TooManyListenersException;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.ObjectInUseException;
import javax.sip.PeerUnavailableException;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.TransportNotSupportedException;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;
public class SipLayer implements SipListener {
private MessageProcessor messageProcessor;
private String username;
private SipStack sipStack;
private SipFactory sipFactory;
private AddressFactory addressFactory;
private HeaderFactory headerFactory;
private MessageFactory messageFactory;
private SipProvider sipProvider;
/** Here we initialize the SIP stack. */
@SuppressWarnings("deprecation")
public SipLayer(String username, String ip, int port) throws PeerUnavailableException,
TransportNotSupportedException, InvalidArgumentException, ObjectInUseException, TooManyListenersException {
setUsername(username);
sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", "cameraReg");
properties.setProperty("javax.sip.IP_ADDRESS", ip);
/**
* sip_server_log.log 和 sip_debug_log.log
* public static final int TRACE_NONE = 0;
public static final int TRACE_MESSAGES = 16;
public static final int TRACE_EXCEPTION = 17;
public static final int TRACE_DEBUG = 32;
*/
properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "16");
properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sip_server_log");
properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sip_debug_log");
sipStack = sipFactory.createSipStack(properties);
headerFactory = sipFactory.createHeaderFactory();
addressFactory = sipFactory.createAddressFactory();
messageFactory = sipFactory.createMessageFactory();
ListeningPoint tcp = sipStack.createListeningPoint(port, "tcp");
ListeningPoint udp = sipStack.createListeningPoint(port, "udp");
sipProvider = sipStack.createSipProvider(tcp);
sipProvider.addSipListener(this);
sipProvider = sipStack.createSipProvider(udp);
sipProvider.addSipListener(this);
}
/**
* This method uses the SIP stack to send a message. 第一個引數:使用者名稱 第二個引數:IP地址
* 第三個引數:訊息內容
*/
public void sendMessage(String username, String address, String message)
throws ParseException, InvalidArgumentException, SipException {
SipURI from = addressFactory.createSipURI(getUsername(), getHost() + ":" + getPort());
Address fromNameAddress = addressFactory.createAddress(from);
fromNameAddress.setDisplayName(getUsername());
FromHeader fromHeader = headerFactory.createFromHeader(fromNameAddress, "cameraReg1.0");
SipURI toAddress = addressFactory.createSipURI(username, address);
Address toNameAddress = addressFactory.createAddress(toAddress);
toNameAddress.setDisplayName(username);
ToHeader toHeader = headerFactory.createToHeader(toNameAddress, null);
SipURI requestURI = addressFactory.createSipURI(username, address);
requestURI.setTransportParam("udp");
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = headerFactory.createViaHeader(getHost(), getPort(), "udp", "branch1");
viaHeaders.add(viaHeader);
CallIdHeader callIdHeader = sipProvider.getNewCallId();
@SuppressWarnings("deprecation")
CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1, Request.MESSAGE);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
Request request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader,
fromHeader, toHeader, viaHeaders, maxForwards);
SipURI contactURI = addressFactory.createSipURI(getUsername(), getHost());
contactURI.setPort(getPort());
Address contactAddress = addressFactory.createAddress(contactURI);
contactAddress.setDisplayName(getUsername());
ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress);
request.addHeader(contactHeader);
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("text", "plain");
request.setContent(message, contentTypeHeader);
sipProvider.sendRequest(request);
}
/** This method is called by the SIP stack when a response arrives. */
public void processResponse(ResponseEvent evt) {
Response response = evt.getResponse();
int status = response.getStatusCode();
if ((status >= 200) && (status < 300)) { // Success!
messageProcessor.processInfo("--Sent");
return;
}
messageProcessor.processError("Previous message not sent: " + status);
}
/**
* SIP服務端接收訊息的方法
* Content 裡面是GBK編碼
* This method is called by the SIP stack when a new request arrives.
*/
public void processRequest(RequestEvent evt) {
Request req = evt.getRequest();
messageProcessor.processMessage(req,messageFactory,sipProvider);
}
/**
* This method is called by the SIP stack when there's no answer to a
* message. Note that this is treated differently from an error message.
*/
public void processTimeout(TimeoutEvent evt) {
messageProcessor.processError("Previous message not sent: " + "timeout");
}
/**
* This method is called by the SIP stack when there's an asynchronous
* message transmission error.
*/
public void processIOException(IOExceptionEvent evt) {
messageProcessor.processError("Previous message not sent: " + "I/O Exception");
}
/**
* This method is called by the SIP stack when a dialog (session) ends.
*/
public void processDialogTerminated(DialogTerminatedEvent evt) {
}
/**
* This method is called by the SIP stack when a transaction ends.
*/
public void processTransactionTerminated(TransactionTerminatedEvent evt) {
}
@SuppressWarnings("deprecation")
public String getHost() {
String host = sipStack.getIPAddress();
return host;
}
@SuppressWarnings("deprecation")
public int getPort() {
int port = sipProvider.getListeningPoint().getPort();
return port;
}
public String getUsername() {
return username;
}
public void setUsername(String newUsername) {
username = newUsername;
}
public MessageProcessor getMessageProcessor() {
return messageProcessor;
}
public void setMessageProcessor(MessageProcessor newMessageProcessor) {
messageProcessor = newMessageProcessor;
}
}
MessageProcessor介面:
import javax.sip.SipProvider;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
/**
* 訊息處理回撥函式介面
*/
public interface MessageProcessor {
/**
* 接收IPCamera發來的SIP協議訊息的時候產生的回撥函式
*/
public void processMessage(Request req,MessageFactory messageFactory, SipProvider sipProvider);
public void processError(String errorMessage);
public void processInfo(String infoMessage);
}
MessageProcessor實現類這裡不給出,因為裡面包含了很多本公司SIP註冊業務的具體細節
需要提示的一點是,需要安裝一個反編譯工具去閱讀Request原始碼裡面的屬性和方法 ,以獲取SIP報文裡面的內容
比如獲取我想獲取sender和method欄位
FromHeader fromHeader = (FromHeader) req.getHeader("From");
String sender = fromHeader.getAddress().toString();
String method = req.getMethod();
再比如我想獲取Contact欄位
Contact contact = (Contact) req.getHeader("Contact");
類似於這樣,關鍵是去通過eclipse點進去看一下這個Request的原始碼
最後,給出JAVA SIP協議的支援包MAVEN POM依賴:
<!-- SPI協議相關的包 -->
<dependency>
<groupId>javax.sip</groupId>
<artifactId>jain-sip-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.sip</groupId>
<artifactId>jain-sip-ri</artifactId>
<version>1.2</version>
</dependency>