網路穿透與音視訊技術(4)——NAT對映檢測和常見網路穿越方法論(NAT檢測實踐1)
2.2、檢測過程實戰——伺服器端
要進行NAT對映檢測,按照上文提到的檢測方式,我們就需要一個服務端檢測程式。並將服務端檢測程式部署到具有兩個外網IP的硬體環境下。
2.2.1、檢測要求
服務端程式至少需要做到以下功能:
-
檢測客戶端和當前伺服器端之間是否至少有一級NAT裝置:這是因為在檢測準備階段,如果判定當前客戶端和服務檢測端之間沒有任何NAT裝置,則無需進行後續的檢測了
-
輔助客戶端完成Symmetric NAT檢測:當進行Symmetric NAT檢測時,伺服器端的兩對IP + PORT在分別收到客戶端IP + PORT發來的檢測請求後,只需要將自己收到的客戶端的IP + PORT資訊封裝成響應內容,返回給客戶端即可。注意:實際上這和“檢測客戶端和當前伺服器端之間是否至少有一級NAT裝置”的工作原理是一樣的,但是為了說明整個過程,這裡還是分開進行介紹和程式碼編寫。
-
輔助客戶端完成Full Cone NAT檢測:伺服器端的一組IP + PORT將接收到客戶端發來的檢測請求,然後用伺服器端的另一組IP + PORT傳送給客戶端。傳送的資訊同樣是“一組伺服器端IP + PORT收到的客戶端的IP + PORT資訊和其它主要資訊”。
-
Address Restricted Cone NAT/Port Restricted Cone NAT 檢測:伺服器端的一組IP + PORT在收到客戶端的檢測請求後,伺服器端會使用當前IP + 另一個PORT的方式封裝資料進行響應返回。返回的內容同樣是“一組伺服器端IP + PORT收到的客戶端的IP + PORT資訊和其它主要資訊”。
-
這裡,我們只討論伺服器端的情況,客戶端傳送和接受響應的處理過程在後文“檢測過程實戰——客戶端”在進行說明。另外,通過這裡的分析我們可以看到,無論是哪一步檢測過程伺服器端起的都是輔助作用,都是將收到檢測請求時真實獲取到的客戶端IP + PORT通過自己這組IP + PORT或者伺服器上的另一組IP + PORT又或者本IP + 另一個PORT重新發送回客戶端即可。
2.2.2、檢測程式設計思路
那麼基於以上所述的檢測功能要求,我們得到一種伺服器端程式的設計思路如下圖所示:
-
如上圖所示我們一共使用了兩組IP + PORT,其中一組是主要的IP + PORT(如上圖藍色部分所示)還帶有一個輔助的PORT,這個輔助的PORT是在進行第四步(Address Restricted Cone NAT/Port Restricted Cone NAT 檢測)驗證時使用。
-
每組IP + PORT都由兩個執行緒,其中一個執行緒用於進行檢測請求接收,另一個執行緒用於進行檢測結果的傳送。兩個執行緒間之間使用阻塞佇列(BlockingQueue)進行資料通訊。這樣一來只要主IP + PORT需要另一組IP + PORT傳送響應資訊時,只需要向對應的訊息佇列中推入訊息即可。
-
注意:在程式碼實現細節方面,由於檢測思路我們經常使用一組IP + PORT接收UDP資料報,又要同時使用相同的IP + PORT傳送響應資料報。所以這組IP + PORT不能是阻塞工作方式,只能使用多路複用的工作方式。
2.2.3、主要程式碼
- 以下是檢測訊息接收執行緒
/**
* 檢測服務的UDP資料報接收執行緒,這個執行緒將啟動兩個,分別為兩個獨立的ip + port工作
* 還要指定當前接收執行緒所服務的ip + port是否為master,因為在進行Full Cone NAT檢測時需要
* @author Administrator
*/
private static class CheckServer_Acceptor implements Runnable {
private Selector selector;
private BlockingQueue<JSONObject> messagesQueue;
/**
* 可能指定的輔助檢測執行緒所使用的訊息佇列
* 它將在進行Full Cone NAT時產生作用
*/
private BlockingQueue<JSONObject> slaveMessagesQueue;
private boolean isMaster = false;
public CheckServer_Acceptor(Selector selector , BlockingQueue<JSONObject> messagesQueue) {
this.selector = selector;
this.messagesQueue = messagesQueue;
}
public CheckServer_Acceptor(Selector selector , BlockingQueue<JSONObject> messagesQueue , boolean isMaster , BlockingQueue<JSONObject> slaveMessagesQueue) {
this(selector, messagesQueue);
this.isMaster = isMaster;
if(isMaster == true && slaveMessagesQueue == null) {
throw new IllegalArgumentException("設定成master後,一定要設定slaveQueueChannel");
}
this.slaveMessagesQueue = slaveMessagesQueue;
}
@Override
public void run() {
/*
* 處理過程為:
* 1、首先在指定的IP + 埠位置,進行持續的UDP檢測請求監聽
* 2、一旦有UDP資料到達,就進行資料->json的轉換,並且進行校驗,還要獲取當前UDP資料報的來源IP和PORT
* 3、根據傳來的type資訊,決定要進行的檢測型別
* 3.1、type=1,輔助檢測客戶端和服務端之間有無NAT裝置,這時通知所屬的messagesQueue即可
* 3.2、type=2,輔助檢測SymmetricNat,這時通知所屬的messagesQueue即可
* 3.3、type=3,輔助檢測Full Cone NAT,這時只有當前服務執行緒的isMaster == true,才起作用
* 且不但要通知所屬的messagesQueue,還要通知slaveMessagesQueue
* 3.4、type=4,最後輔助檢測Address Restricted Cone NAT/Port Restricted Cone NAT,
* 這時需要通知所屬的messagesQueue,且消費者一側會根據type進行特殊處理
* */
while(true) {
try {
doHandle();
} catch(Exception e) {
LOGGER.error(e.getMessage() , e);
}
}
}
private void doHandle() throws IOException {
// 1、=============
ByteBuffer bb = ByteBuffer.allocateDirect(2048);
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while(keys.hasNext()) {
SelectionKey sk = keys.next();
keys.remove();
if(sk.isReadable()) {
DatagramChannel curdc = (DatagramChannel) sk.channel();
InetSocketAddress senderSocketAddress = null;
try {
senderSocketAddress = (InetSocketAddress) curdc.receive(bb);
} catch(Exception e) {
LOGGER.warn(e.getMessage() , e);
continue;
}
bb.flip();
byte[] peerbs = new byte[bb.limit()];
for(int i=0;i<bb.limit();i++){
peerbs[i]=bb.get(i);
}
// 2、=============
String receStr = new String(peerbs);
JSONObject requestObject = null;
Integer type = null;
try {
requestObject = JSONObject.parseObject(receStr);
if(requestObject == null) {
continue;
}
type = requestObject.getInteger("type");
if(type == null) {
continue;
}
} catch(Exception e) {
LOGGER.error(e.getMessage() , e);
} finally {
bb.clear();
}
// 獲得傳送方的ip+port
Inet4Address resouceIp4 = (Inet4Address)senderSocketAddress.getAddress();
String resouceIp = resouceIp4.getHostAddress();
Integer resoucePort = senderSocketAddress.getPort();
requestObject.put("resouceIp", resouceIp);
requestObject.put("resoucePort", resoucePort);
requestObject.put("ack", true);
LOGGER.info("=========接收到檢測請求,來自於[" + resouceIp + ":" + resoucePort + "] " + receStr);
// 3、============
try {
switch (type) {
// 3.1
case 1:
messagesQueue.put(requestObject);
break;
// 3.2
case 2:
messagesQueue.put(requestObject);
break;
// 3.3
case 3:
if(this.isMaster) {
messagesQueue.put(requestObject);
slaveMessagesQueue.put(requestObject);
}
break;
// 3.4
case 4:
if(this.isMaster) {
messagesQueue.put(requestObject);
}
break;
default:
break;
}
} catch(InterruptedException e) {
LOGGER.info("CheckServer_Acceptor收到終止訊號,停止執行!!");
return;
}
}
}
}
}
- 以下是檢測結果訊息傳送執行緒
/**
* 檢測後向檢測請求發起者回執訊息的執行緒<br>
* 它和指定的接收執行緒間,通過messagesQueue進行訊息互動
* @author yinwenjie
*/
private static class CheckServer_Sender implements Runnable {
/**
* 本傳送執行緒主要使用的UDP Channel
*/
private DatagramChannel udpChannel;
/**
* 輔助的UDP傳送channel
* 再進行type==4的處理時會使用到
*/
private DatagramChannel assistChannel;
/**
* 當前channel已經和哪些傳送目標建立的連線,這裡要進行記錄
* 以避免重複的連線
*/
private Map<String, String> connectedMap = new ConcurrentHashMap<>(20);
private Map<String, String> assistConnectedMap = new ConcurrentHashMap<>(20);
/**
* 本傳送執行緒使用的主要訊息佇列
*/
private LinkedBlockingQueue<JSONObject> messagesQueue;
public CheckServer_Sender(DatagramChannel udpChannel , LinkedBlockingQueue<JSONObject> messagesQueue) {
this.udpChannel = udpChannel;
this.messagesQueue = messagesQueue;
}
public CheckServer_Sender(DatagramChannel udpChannel , LinkedBlockingQueue<JSONObject> messagesQueue , DatagramChannel assistChannel) {
this(udpChannel, messagesQueue);
this.assistChannel = assistChannel;
}
@Override
public void run() {
/*
* 處理過程如下:
* 1、首先一直監控messagesQueue中的訊息資訊
* 2、一旦獲取訊息,就要根據type的情況,準備傳送資訊
* 2.1、type == 1 || type == 2 || type == 3,只需要增加當前傳送方的ip + port
* (記為targetIp和targetPort,這是從client的角度出發)
* 2.2、type == 4、這是使用輔助的assistChannel進行傳送
* 如果沒有設定assistChannel,就不進行操作
* */
while(true) {
try {
doHandle();
} catch(Exception e) {
LOGGER.error(e.getMessage() , e);
}
}
}
private void doHandle() throws IOException {
// 1、============
JSONObject jsonObject;
try {
jsonObject = messagesQueue.take();
} catch (InterruptedException e) {
LOGGER.error(e.getMessage() , e);
return;
}
String resouceIp = jsonObject.getString("resouceIp");
Integer resoucePort = jsonObject.getInteger("resoucePort");
if(StringUtils.isBlank(resouceIp) || resoucePort == null) {
return;
}
Integer type = jsonObject.getInteger("type");
if(type == null) {
return;
}
// 2、===========
DatagramSocket udpSocket = null;
byte[] jsonBytes = null;
Integer curentLocalPort = null;
String currentLocalIp = null;
if(type == 1 || type == 2 || type == 3) {
udpSocket = udpChannel.socket();
curentLocalPort = udpSocket.getLocalPort();
currentLocalIp = udpSocket.getLocalAddress().getHostAddress();
jsonObject.put("targetIp", currentLocalIp);
jsonObject.put("targetPort", curentLocalPort);
} else if (type == 4 && assistChannel != null) {
udpSocket = assistChannel.socket();
curentLocalPort = udpSocket.getLocalPort();
currentLocalIp = udpSocket.getLocalAddress().getHostAddress();
jsonObject.put("targetIp", currentLocalIp);
jsonObject.put("targetPort", curentLocalPort);
} else {
return;
}
// 準備傳送,根據不同的type,使用不同的channel進行傳送
String jsonContext = jsonObject.toJSONString();
jsonBytes = jsonContext.getBytes();
DatagramChannel currentChannel = null;
if(type == 1 || type == 2 || type == 3) {
if(connectedMap.get(resouceIp + ":" + resoucePort) == null) {
connectedMap.put(resouceIp + ":" + resoucePort , "true");
try {
udpChannel.connect(new InetSocketAddress(resouceIp, resoucePort));
} catch(Exception e) {
LOGGER.warn(e.getMessage());
return;
}
}
currentChannel = udpChannel;
} else {
if(assistConnectedMap.get(resouceIp + ":" + resoucePort) == null) {
assistConnectedMap.put(resouceIp + ":" + resoucePort , "true");
try {
assistChannel.connect(new InetSocketAddress(resouceIp, resoucePort));
} catch(Exception e) {
LOGGER.warn(e.getMessage());
return;
}
}
currentChannel = assistChannel;
}
// 傳送
LOGGER.info("伺服器基於[" + currentLocalIp + ":" + curentLocalPort + "]"
+ "向檢測請求者[" + resouceIp + ":" + resoucePort + "]傳送檢測結果===:" + jsonContext);
ByteBuffer conentBytes = ByteBuffer.allocateDirect(jsonBytes.length);
try {
conentBytes.put(jsonBytes);
conentBytes.flip();
currentChannel.write(conentBytes);
} finally {
conentBytes.clear();
}
}
}
- 以下是檢測程式的啟動程式碼片段
/**
* 這個檢測服務覆蓋的服務型別包括:
* 1、輔助檢測客戶端和服務端之間有無NAT裝置
* 2、輔助檢測SymmetricNat
* 3、輔助檢測Full Cone NAT
* 4、最後輔助檢測Address Restricted Cone NAT/Port Restricted Cone NAT
* @author yinwenjie
*/
public class CheckServer2 {
/**
* 日誌
*/
private static Logger LOGGER = LoggerFactory.getLogger(CheckServer2.class);
private static LinkedBlockingQueue<JSONObject> messagesQueue = new LinkedBlockingQueue<>();
private static LinkedBlockingQueue<JSONObject> slaveMessagesQueue = new LinkedBlockingQueue<>();
/**
* 用於描述本地IP + port和channel的對應關係
*/
private static Map<String, DatagramChannel> localChannelMaps = new HashMap<>();
static {
BasicConfigurator.configure();
}
public static void main(String[] args) throws IOException {
// 得到兩對可用的 ip + port(第一對IP + PORT將作為主要的IP + PORT),還有一個為檢查Address Restricted Cone NAT設定的輔助port
String currentIp1 = args[0];
String currentPort1Value = args[1];
String currentIp2 = args[2];
String currentPort2Value = args[3];
String assistPortValue = args[4];
Integer currentPort1 = Integer.parseInt(currentPort1Value);
Integer currentPort2 = Integer.parseInt(currentPort2Value);
Integer assistPort = Integer.parseInt(assistPortValue);
// 初始化NIO-UDP Server
// 主IP + PORT
Selector selector = Selector.open();
DatagramChannel udpChannel1 = DatagramChannel.open();
udpChannel1.configureBlocking(false);
LOGGER.info("正在進行UDP服務監聽繫結:[" + currentIp1 + ":" + currentPort1 + "]");
udpChannel1.socket().bind(new InetSocketAddress(currentIp1 , currentPort1));
udpChannel1.register(selector, SelectionKey.OP_READ);
localChannelMaps.put(currentIp1 + ":" + currentPort1, udpChannel1);
// 副IP + PORT
DatagramChannel udpChannel2 = DatagramChannel.open();
udpChannel2.configureBlocking(false);
LOGGER.info("正在進行UDP服務監聽繫結:[" + currentIp2 + ":" + currentPort2 + "]");
udpChannel2.socket().bind(new InetSocketAddress(currentIp2 , currentPort2));
udpChannel2.register(selector, SelectionKey.OP_READ);
localChannelMaps.put(currentIp2 + ":" + currentPort2, udpChannel2);
// 輔助的PORT
DatagramChannel assistChannel = DatagramChannel.open();
assistChannel.configureBlocking(false);
LOGGER.info("正在進行UDP服務監聽繫結:[" + currentIp1 + ":" + assistPort + "]");
assistChannel.socket().bind(new InetSocketAddress(currentIp1 , assistPort));
assistChannel.register(selector, SelectionKey.OP_READ)
相關推薦
網路穿透與音視訊技術(4)——NAT對映檢測和常見網路穿越方法論(NAT檢測實踐1)
2.2、檢測過程實戰——伺服器端
要進行NAT對映檢測,按照上文提到的檢測方式,我們就需要一個服務端檢測程式。並將服務端檢測程式部署到具有兩個外網IP的硬體環境下。
2.2.1、檢測要求
服務端程式至少需要做到以下功能:
檢測客戶端和當前伺服器端之間是否至
網路穿透與音視訊技術(1)——NAT的概念及工作模式(上)
(這個專題我們將介紹網路穿透的基本知識,以及建立在此基礎上的實時視訊語音通訊技術。不只是介紹理論知識,還介紹實際案例 )
1、概念介紹
1.1、NAT基本概念
NAT英文全稱是“Network Address Translation”,中文意思是“網路
網路穿透與音視訊技術(2)——NAT的概念及工作模式(下)
3、四種NAT對映實現方式
上文中我們已經提到三種NAT對映模式,它們是靜態對映(Static NAT)、動態對映(Pooled NAT)和網路地址埠對映(NAPT/PAT),又由於NAPT/PAT對映模式的靈活性和複用性最好,所以它又是目前應用最廣泛的一種對
網路穿透與音視訊技術(3)——NAT對映檢測和常見網路穿越方法論(NAT檢測)
1、什麼是網路穿透
1.1、伺服器高負載狀態下的通訊問題
想想一下這種情況,多個處於不同內部網路的終端同時進行大檔案傳輸工作。我們最直接的思維模式能夠想到的就是這些終端首先將檔案傳輸到某各都能訪問到的伺服器上,再由伺服器進行中轉傳輸。是的,這個方式最簡單直接,
LiveVideoStack線上交流分享 ( 五 ) —— 線上教育音視訊技術探索與應用
為了給大家提供一個學習,交流的平臺,暢聊音視訊技術開發新趨勢,新實踐。我們推出了LiveVideoStack線上交流分享活動,在每週四晚19:30,邀請1名業內資深技術專家進行線上分享技術乾貨,解答熱點問題。你可以通過以下方式參與:
關注LiveVideoStack公眾號【
線上教育音視訊技術探索與應用
隨著實時音視訊通訊技術的發展,1對1,1對多直播等線上教育形式不斷的滿足個人定製化的學習需求。掌門1對1音視訊負責人 曾小偉在LiveVideoStack 線上交流分享中介紹了線上教育中音視訊技術的應用現狀、挑戰以及未來的發展。本文由LiveVideoStack整理而
實時音視訊技術(WebRTC/voip/Linphone/P2P)
視訊社交與語音社交???
實時視訊(直播)/語音通訊。多媒體技術團隊在音視訊編解碼、前後處理、傳輸等技術;
在語音社交、視訊社交、遊戲語音和互動直播等領域,關於在語音視訊實時傳輸中實現低延遲這個議題,已經有不少的文章提出各種方案。絕大部分方案的思路都是“優化”,
1小時學會:最簡單的iOS直播推流(九)flv 編碼與音視訊時間戳同步
最簡單的iOS 推流程式碼,視訊捕獲,軟編碼(faac,x264),硬編碼(aac,h264),美顏,flv編碼,rtmp協議,陸續更新程式碼解析,你想學的知識這裡都有,願意懂直播技術的同學快來看!!
前文介紹瞭如何獲取音視訊的aac/h2
快手科技音視訊技術亮相ChinaMM 首次公開多媒體傳輸協議KTP
在中國多媒體大會產業前沿論壇,快手科技演算法科學家周超博士發表題為《多媒體傳輸演算法應用和展望》的演講,首次對外公開了其多媒體傳輸協議KTP(Kwai Transport Protocol,快手傳輸協議),該協議解決了重要的內容傳輸問題。以下為周超博士演講的主要內容。
快手的核心理念就是記錄,力
使用ONVIF Device Test Tool獲取網路攝像頭的音/視訊
軟/硬體準備
1、一個網路攝像頭(IPC),品牌必須支援ONVIF協議,具體哪些品牌支援不作為本教程介紹的重點,大家可自行度娘,我知道的有品牌大華和海康威視; 2、ONVIF Device Test Tool軟體下載,筆者使用的版本為15.06, 下載地址:https://downl
開源實時音視訊技術WebRTC中RTP/RTCP資料傳輸協議的應用
1、前言
RTP/RTCP協議是流媒體通訊的基石。RTP協議定義流媒體資料在網際網路上傳輸的資料包格式,而RTCP協議則負責可靠傳輸、流量控制和擁塞控制等服務質量保證。在WebRTC專案中,RTP/RTCP模組作為傳輸模組的一部分,負責對傳送端採集到的媒體資料進行進行封包,然後交給上層網路模組
LiveVideoStackCon音視訊技術大會首次來到上海
音視訊技術生態盛宴——LiveVideoStackCon將在2019年來到上海,並從即日起開啟招募講師與出品人。
文 / 包研
2019年4月12-13日,將迎來LiveVideoStackCon上海大會。這是第三次LiveVideoStackC
音視訊技術開發週刊 75期
『音視訊技術開發週刊』由LiveVideoStack團隊出品,專注在音視訊技術領域,縱覽相關技術領域的乾貨和新聞投稿,每週一期。點選『閱讀原文』,瀏覽第75期內容,祝您閱讀愉快。
架構
Netflix媒體資料庫:媒體時間線資料模
音視訊技術開發週刊 74期
『音視訊技術開發週刊』由LiveVideoStack團隊出品,專注在音視訊技術領域,縱覽相關技術領域的乾貨和新聞投稿,每週一期。點選『閱讀原文』,瀏覽第74期內容,祝您閱讀愉快。
架構
VMAF:未畢之旅
本文來自N
打造專遞課堂,即構成為希沃專遞課堂實時音視訊技術唯一提供方
日前,在南昌舉辦的第75屆中國教育裝備展上,希沃和即構zego打造的互動錄播方案亮相。現場將展廳設定為授課教室,廣州、贛州、南昌三個分會場為聽課教室,以每分鐘一場的高頻次互動演示,模擬了身處不同地區的4個教室的互動教學,現場效果令人震撼。 據瞭解,該方案也稱“專遞課堂”,目前已在江西、雲南
【雲棲TechDay】音視訊技術開發實戰專場沙龍,邀您參加
【時間】2018-12-20 下午13:40-18:00【地點】浙江省杭州市蕭山區啟迪路198號杭州灣資訊港A座負一樓國際報告廳【主辦單位】雲棲techday 阿里雲視訊雲團隊
簡介
音視訊技術是當前非常活躍、發展十分迅速的技術領域。近年來,數字化潮流正在迅猛衝擊模擬領域,數字技術促進了音視訊
音視訊技術總結
1. 常用的基本知識
基本概念
編解碼
編解碼器(codec)指的是一個能夠對一個訊號或者一個數據流進行變換的裝置或者 程式。這裡指的變換既包括將訊號或者資料流進行編碼(通常是為了傳輸、儲存或者加密)或者提取得到一個編碼流的操作,也包括為了觀察或者處理從這個
音視訊技術開發週刊 77期
『音視訊技術開發週刊』由LiveVideoStack團隊出品,專注在音視訊技術領域,縱覽相關技術領域的乾貨和新聞投稿,每週一期。點選『閱讀原文』,瀏覽第77期內容,祝您閱讀愉快。
架構
基於FFmpeg的運動視訊分析
本文
LiveVideoStack音視訊技術2018年度評獎揭曉
經過一個月的投票與評審,LiveVideoStack評出了音視訊技術2018年度獲獎者。
一個月前,LiveVideoStack啟動音視訊技術2018年度評獎,總共獲得393份有效問卷。考慮到一些故意的刷票行為,對這部分投票實行了降權處理。儘管如此,我
即構科技金健忠:回顧20年音視訊技術演進
多媒體技術是一個傳統行業,從模擬到數字,VCD到藍光,從窄帶到寬頻,標清到高清,技術演進讓人的視聽體驗發生了顛覆式的改變。LiveVideoStack採訪了即構科技CTO金健忠,他回顧了過去20年多媒體技術的發展,並展望了未來的技術趨勢。
文 / 金健忠
策劃 /