NAT穿透解決
1.各種網路環境下的P2P通訊解決方法:
(1)如果通訊雙方在同一個區域網內,這種情況下可以不借助任何外力直接通過內網地址通訊即可; (2)如果通訊雙方都在有獨立的公網地址,這種情況下當然可以不借助任何外力直接通訊即可;(3)如果通訊雙方一方擁有獨立的公網地址另一方在NAT後面,那麼可以由位於NAT後面的一方主動發起通訊請求;
(4)如果通訊雙方都位於NAT後面,且雙方的NAT型別都是cone NAT,那麼可以通過一個STUN伺服器發現自己的NAT型別以及內網和外網傳輸地址對映資訊,然後通過Signaling(信令伺服器,實現了SIP協議的主機)交換彼此的NAT型別及內網和外網傳輸地址對映資訊,然後通過UDP打洞的方式建立通訊連線;
2.協議及用到的相關技術介紹:
SDP(Session Description Protocol) 當初始化多媒體電視會議、IP電話、視訊流等會話的時候,參與者之間會要求傳送媒介的詳細、傳輸地址和其他會話描述元資料等資訊;SDP為這些資訊提供一種和傳輸方式無關的標準的表現形式。也就是說SDP僅僅只是一種描述會話資訊的格式。它主要被各種不同的傳輸協議作為一種資訊交換的格式使用列如:HTTP、RTSP、SIP、Email等各種協議。 如ICE裡面的SDP內容為:v=0 o=ice4j.org 0 0 IN IP4 192.168.106.215 s=- t=0 0 a=ice-options:trickle a=ice-ufrag:bc01a a=ice-pwd:1boove7ehnpo1lqho7unefni36 m=audio 3030 RTP/AVP 0 c=IN 192.168.106.215 IP4 a=mid:audio a=candidate:1 1 udp 2130706431 192.168.106.215 3030 typ host a=candidate:2 1 udp 1694498815 121.15.130.xxx 64923 typ srflx raddr 192.168.106.215 rport 3030
STUN(Session Traversal Utilities for NAT)
NAT會話穿透工具;STUN提供了一種方式使一個端點能夠確定NAT分配的和本地私有IP地址和埠相對應的公網IP地址和埠以及NAT的型別資訊。它也為端點提供了一種方式保持一個NAT繫結不過期。NAT繫結過期則表示為相同的內網地址重新分配外網地址也就是埠號。
TURN(Traversal Using Relay NAT)
TURN是STUN協議的擴充套件,在實際應用中他也可以充當STUN的角色;如果一個位於NAT後面的裝置想要和另外一個位於NAT後面的裝置建立通訊,當採用UDP打洞技術不能改實現的時候就必須要一臺中間伺服器扮演資料包轉發的角色,這臺TURN伺服器需要擁有公網的IP地址;
ICE(Interactive Connectivity Establishment)
是實現NAT穿透的一種技術方案;ICE是一種NAT穿透技術,通過offer/answer模型建立基於UDP的媒介流。ICE是offer/answer模型的擴充套件,通過在offer和answer的SDP裡面包含多種IP地址和埠,然後對本地SDP和遠端SDP裡面的IP地址進行配對,然後通過P2P連通性檢查進行連通性測試工作,如果測試通過即表明該傳輸地址對可以建立連線。其中IP地址和埠(也就是地址)有以下幾種:本機地址、通過STUN伺服器反射後獲取的server-reflexive地址(內網地址被NAT對映後的地址)、relayed地址(和TURN轉發伺服器相對應的地址)及Peer reflexive地址等。
3.ICE進行NAT穿透的基本過程: 在通常的ICE部署環境中,我們有兩個客服端想要建立通訊連線,他們可以直接通過signaling伺服器(如SIP伺服器)執行offer/answer過程來交換SDP訊息。 在ICE過程開始的時候,客服端忽略他們各自的網路拓撲結構,不管是不是在NAT裝置後面或者多個NAT後面,ICE允許客服端發現他們的所在網路的拓撲結構的資訊,然後找出一個或者更多的可以建立通訊連線的路徑。 下圖顯示了一個典型的ICE部署環境,客服端L和R都在各自的NAT裝置後面,下面簡單描述下ICE建立通訊的過程: (1)L和R先分別通過STUN和TURN伺服器獲取自己的host address,server-reflexive address、relayed address(和TURN轉發伺服器相對應的地址),其中server-reflexive address和relayed address通過定時重新整理保證地址不過期。這些地址通常叫做candinate地址。 (2)給這些candinate地址分配優先順序排序並格式化成SDP格式,通過SIP伺服器交換彼此的SDP; (3)交換完成後根據一定的原則把本地的候選和遠端的候選進行配對,每一對都有自己的優先順序並根據優先順序進行排序後放入Check列表裡面(兩邊都會有相同的Check列表)。 (4)然後進行連線性測試,測試前會選擇一個客服端扮演Controlled角色和另一個扮演Controling角色,連通性檢查完成後扮演Controling角色的客服端負責在有效的Candinate對列表裡面選擇一個作為一個被選中的傳輸通道並通知Controlled的客服端。 (5)利用被選中的candinate地址對進行通訊。 4.ICE JAVA實現程式碼 我這裡的樣例程式碼採用ICE4J來實現,ICE4J的API文件可以參考 http://bluejimp.com/jitsi/ice4j/javadoc/,在這個實現裡面沒有利用SIP伺服器進行SDP資訊的交換而是採用手動輸入的方式,在生產環境中可以部署一臺socket.io或者其他SIP伺服器/** * Copyright (c) 2014 All Rights Reserved. * TODO */ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.ice4j.Transport; import org.ice4j.TransportAddress; import org.ice4j.ice.Agent; import org.ice4j.ice.Component; import org.ice4j.ice.IceMediaStream; import org.ice4j.ice.IceProcessingState; import org.ice4j.ice.LocalCandidate; import org.ice4j.ice.NominationStrategy; import org.ice4j.ice.RemoteCandidate; import org.ice4j.ice.harvest.StunCandidateHarvester; import org.ice4j.ice.harvest.TurnCandidateHarvester; import org.ice4j.security.LongTermCredential; import test.SdpUtils; public class IceClient { private int port; private String streamName; private Agent agent; private String localSdp; private String remoteSdp; private String[] turnServers = new String[] { "stun.jitsi.net:3478" }; private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" }; private String username = "guest"; private String password = "anonymouspower!!"; private IceProcessingListener listener; static Logger log = Logger.getLogger(IceClient.class); public IceClient(int port, String streamName) { this.port = port; this.streamName = streamName; this.listener = new IceProcessingListener(); } public void init() throws Throwable { agent = createAgent(port, streamName); agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO); agent.addStateChangeListener(listener); agent.setControlling(false); agent.setTa(10000); localSdp = SdpUtils.createSDPDescription(agent); log.info("=================== feed the following" + " to the remote agent ==================="); System.out.println(localSdp); log.info("======================================" + "========================================\n"); } public DatagramSocket getDatagramSocket() throws Throwable { LocalCandidate localCandidate = agent .getSelectedLocalCandidate(streamName); IceMediaStream stream = agent.getStream(streamName); List<Component> components = stream.getComponents(); for (Component c : components) { log.info(c); } log.info(localCandidate.toString()); LocalCandidate candidate = (LocalCandidate) localCandidate; return candidate.getDatagramSocket(); } public SocketAddress getRemotePeerSocketAddress() { RemoteCandidate remoteCandidate = agent .getSelectedRemoteCandidate(streamName); log.info("Remote candinate transport address:" + remoteCandidate.getTransportAddress()); log.info("Remote candinate host address:" + remoteCandidate.getHostAddress()); log.info("Remote candinate mapped address:" + remoteCandidate.getMappedAddress()); log.info("Remote candinate relayed address:" + remoteCandidate.getRelayedAddress()); log.info("Remote candinate reflexive address:" + remoteCandidate.getReflexiveAddress()); return remoteCandidate.getTransportAddress(); } /** * Reads an SDP description from the standard input.In production * environment that we can exchange SDP with peer through signaling * server(SIP server) */ public void exchangeSdpWithPeer() throws Throwable { log.info("Paste remote SDP here. Enter an empty line to proceed:"); BufferedReader reader = new BufferedReader(new InputStreamReader( System.in)); StringBuilder buff = new StringBuilder(); String line = new String(); while ((line = reader.readLine()) != null) { line = line.trim(); if (line.length() == 0) { break; } buff.append(line); buff.append("\r\n"); } remoteSdp = buff.toString(); SdpUtils.parseSDP(agent, remoteSdp); } public void startConnect() throws InterruptedException { if (StringUtils.isBlank(remoteSdp)) { throw new NullPointerException( "Please exchange sdp information with peer before start connect! "); } agent.startConnectivityEstablishment(); // agent.runInStunKeepAliveThread(); synchronized (listener) { listener.wait(); } } private Agent createAgent(int rtpPort, String streamName) throws Throwable { return createAgent(rtpPort, streamName, false); } private Agent createAgent(int rtpPort, String streamName, boolean isTrickling) throws Throwable { long startTime = System.currentTimeMillis(); Agent agent = new Agent(); agent.setTrickling(isTrickling); // STUN for (String server : stunServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new StunCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP))); } // TURN LongTermCredential longTermCredential = new LongTermCredential(username, password); for (String server : turnServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new TurnCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP), longTermCredential)); } // STREAMS createStream(rtpPort, streamName, agent); long endTime = System.currentTimeMillis(); long total = endTime - startTime; log.info("Total harvesting time: " + total + "ms."); return agent; } private IceMediaStream createStream(int rtpPort, String streamName, Agent agent) throws Throwable { long startTime = System.currentTimeMillis(); IceMediaStream stream = agent.createMediaStream(streamName); // rtp Component component = agent.createComponent(stream, Transport.UDP, rtpPort, rtpPort, rtpPort + 100); long endTime = System.currentTimeMillis(); log.info("Component Name:" + component.getName()); log.info("RTP Component created in " + (endTime - startTime) + " ms"); return stream; } /** * Receive notify event when ice processing state has changed. */ public static final class IceProcessingListener implements PropertyChangeListener { private long startTime = System.currentTimeMillis(); public void propertyChange(PropertyChangeEvent event) { Object state = event.getNewValue(); log.info("Agent entered the " + state + " state."); if (state == IceProcessingState.COMPLETED) { long processingEndTime = System.currentTimeMillis(); log.info("Total ICE processing time: " + (processingEndTime - startTime) + "ms"); Agent agent = (Agent) event.getSource(); List<IceMediaStream> streams = agent.getStreams(); for (IceMediaStream stream : streams) { log.info("Stream name: " + stream.getName()); List<Component> components = stream.getComponents(); for (Component c : components) { log.info("------------------------------------------"); log.info("Component of stream:" + c.getName() + ",selected of pair:" + c.getSelectedPair()); log.info("------------------------------------------"); } } log.info("Printing the completed check lists:"); for (IceMediaStream stream : streams) { log.info("Check list for stream: " + stream.getName()); log.info("nominated check list:" + stream.getCheckList()); } synchronized (this) { this.notifyAll(); } } else if (state == IceProcessingState.TERMINATED) { log.info("ice processing TERMINATED"); } else if (state == IceProcessingState.FAILED) { log.info("ice processing FAILED"); ((Agent) event.getSource()).free(); } } } } import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.concurrent.TimeUnit; public class PeerA { public static void main(String[] args) throws Throwable { try { IceClient client = new IceClient(2020, "audio"); client.init(); client.exchangeSdpWithPeer(); client.startConnect(); final DatagramSocket socket = client.getDatagramSocket(); final SocketAddress remoteAddress = client .getRemotePeerSocketAddress(); System.out.println(socket.toString()); new Thread(new Runnable() { public void run() { while (true) { try { byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); System.out.println("receive:" + new String(packet.getData(), 0, packet .getLength())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { public void run() { int count = 1; while (true) { try { byte[] buf = ("send msg " + count++ + "").getBytes(); DatagramPacket packet = new DatagramPacket(buf, buf.length); packet.setSocketAddress(remoteAddress); socket.send(packet); System.out.println("send msg"); TimeUnit.SECONDS.sleep(10); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
5.參考資料 ICE: https://tools.ietf.org/html/rfc5245 SDP: http://tools.ietf.org/html/rfc4566 SIP: http://tools.ietf.org/html/rfc3261 NAT: http://en.wikipedia.org/wiki/Network_address_translation STUN: http://tools.ietf.org/html/rfc5389 TURN: http://tools.ietf.org/html/rfc5766 ICE4J: http://code.google.com/p/ice4j/
1.各種網路環境下的P2P通訊解決方法:
(1)如果通訊雙方在同一個區域網內,這種情況下可以不借助任何外力直接通過內網地址通訊即可; (2)如果通訊雙方都在有獨立的公網地址,這種情況下當然可以不借助任何外力直接通訊即可;(3)如果通訊雙方一方擁有獨立的公網地址另一方在NAT後面,那麼可以由位於NAT後面的一方主動發起通訊請求;
(4)如果通訊雙方都位於NAT後面,且雙方的NAT型別都是cone NAT,那麼可以通過一個STUN伺服器發現自己的NAT型別以及內網和外網傳輸地址對映資訊,然後通過Signaling(信令伺服器,實現了SIP協議的主機)交換彼此的NAT型別及內網和外網傳輸地址對映資訊,然後通過UDP打洞的方式建立通訊連線;
(5)如果通訊雙方有一方的NAT型別是Symmetric NAT,則無法直接建立P2P連線,這個時候就需要藉助TURN(Traversal Using Relay NAT)即轉發伺服器來實現間接通訊;2.協議及用到的相關技術介紹:
SDP(Session Description Protocol) 當初始化多媒體電視會議、IP電話、視訊流等會話的時候,參與者之間會要求傳送媒介的詳細、傳輸地址和其他會話描述元資料等資訊;SDP為這些資訊提供一種和傳輸方式無關的標準的表現形式。也就是說SDP僅僅只是一種描述會話資訊的格式。它主要被各種不同的傳輸協議作為一種資訊交換的格式使用列如:HTTP、RTSP、SIP、Email等各種協議。 如ICE裡面的SDP內容為:v=0 o=ice4j.org 0 0 IN IP4 192.168.106.215 s=- t=0 0 a=ice-options:trickle a=ice-ufrag:bc01a a=ice-pwd:1boove7ehnpo1lqho7unefni36 m=audio 3030 RTP/AVP 0 c=IN 192.168.106.215 IP4 a=mid:audio a=candidate:1 1 udp 2130706431 192.168.106.215 3030 typ host a=candidate:2 1 udp 1694498815 121.15.130.xxx 64923 typ srflx raddr 192.168.106.215 rport 3030
STUN(Session Traversal Utilities for NAT)
NAT會話穿透工具;STUN提供了一種方式使一個端點能夠確定NAT分配的和本地私有IP地址和埠相對應的公網IP地址和埠以及NAT的型別資訊。它也為端點提供了一種方式保持一個NAT繫結不過期。NAT繫結過期則表示為相同的內網地址重新分配外網地址也就是埠號。
TURN(Traversal Using Relay NAT)
TURN是STUN協議的擴充套件,在實際應用中他也可以充當STUN的角色;如果一個位於NAT後面的裝置想要和另外一個位於NAT後面的裝置建立通訊,當採用UDP打洞技術不能改實現的時候就必須要一臺中間伺服器扮演資料包轉發的角色,這臺TURN伺服器需要擁有公網的IP地址;
SIP(Session Initiation Protocol) 是一種Signaling(信令)通訊協議;有許多網際網路應用需要建立有多個參與者的會話和管理參與者之間相互的資料交換,然而如果這些工作讓應用的參與者來實現是比較複雜的如:使用者也許在端點之間移動、通過多個名稱定址和也許同時使用幾種不同的媒介通訊。有許多協議能夠實現各種形式的多媒體會話進行資料傳送例如聲音、視訊或者文字訊息。SIP能夠和這些協議一同合作,使一個客服端能夠發現參與這個會話的其他客服端並共享同一會話。為了定位後面加入會話的參與者等功能,SIP能夠為代理伺服器建立基礎設施,客服端可以通過這個代理伺服器實現會話註冊、邀請參與會話等功能。SIP是一個建立、修改和終止會話的靈活的多種用途的工具,不依賴於底層的傳輸協議並且不依賴於被建立的會話型別。ICE(Interactive Connectivity Establishment)
是實現NAT穿透的一種技術方案;ICE是一種NAT穿透技術,通過offer/answer模型建立基於UDP的媒介流。ICE是offer/answer模型的擴充套件,通過在offer和answer的SDP裡面包含多種IP地址和埠,然後對本地SDP和遠端SDP裡面的IP地址進行配對,然後通過P2P連通性檢查進行連通性測試工作,如果測試通過即表明該傳輸地址對可以建立連線。其中IP地址和埠(也就是地址)有以下幾種:本機地址、通過STUN伺服器反射後獲取的server-reflexive地址(內網地址被NAT對映後的地址)、relayed地址(和TURN轉發伺服器相對應的地址)及Peer reflexive地址等。
3.ICE進行NAT穿透的基本過程: 在通常的ICE部署環境中,我們有兩個客服端想要建立通訊連線,他們可以直接通過signaling伺服器(如SIP伺服器)執行offer/answer過程來交換SDP訊息。 在ICE過程開始的時候,客服端忽略他們各自的網路拓撲結構,不管是不是在NAT裝置後面或者多個NAT後面,ICE允許客服端發現他們的所在網路的拓撲結構的資訊,然後找出一個或者更多的可以建立通訊連線的路徑。 下圖顯示了一個典型的ICE部署環境,客服端L和R都在各自的NAT裝置後面,下面簡單描述下ICE建立通訊的過程: (1)L和R先分別通過STUN和TURN伺服器獲取自己的host address,server-reflexive address、relayed address(和TURN轉發伺服器相對應的地址),其中server-reflexive address和relayed address通過定時重新整理保證地址不過期。這些地址通常叫做candinate地址。 (2)給這些candinate地址分配優先順序排序並格式化成SDP格式,通過SIP伺服器交換彼此的SDP; (3)交換完成後根據一定的原則把本地的候選和遠端的候選進行配對,每一對都有自己的優先順序並根據優先順序進行排序後放入Check列表裡面(兩邊都會有相同的Check列表)。 (4)然後進行連線性測試,測試前會選擇一個客服端扮演Controlled角色和另一個扮演Controling角色,連通性檢查完成後扮演Controling角色的客服端負責在有效的Candinate對列表裡面選擇一個作為一個被選中的傳輸通道並通知Controlled的客服端。 (5)利用被選中的candinate地址對進行通訊。 4.ICE JAVA實現程式碼 我這裡的樣例程式碼採用ICE4J來實現,ICE4J的API文件可以參考 http://bluejimp.com/jitsi/ice4j/javadoc/,在這個實現裡面沒有利用SIP伺服器進行SDP資訊的交換而是採用手動輸入的方式,在生產環境中可以部署一臺socket.io或者其他SIP伺服器/** * Copyright (c) 2014 All Rights Reserved. * TODO */ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.ice4j.Transport; import org.ice4j.TransportAddress; import org.ice4j.ice.Agent; import org.ice4j.ice.Component; import org.ice4j.ice.IceMediaStream; import org.ice4j.ice.IceProcessingState; import org.ice4j.ice.LocalCandidate; import org.ice4j.ice.NominationStrategy; import org.ice4j.ice.RemoteCandidate; import org.ice4j.ice.harvest.StunCandidateHarvester; import org.ice4j.ice.harvest.TurnCandidateHarvester; import org.ice4j.security.LongTermCredential; import test.SdpUtils; public class IceClient { private int port; private String streamName; private Agent agent; private String localSdp; private String remoteSdp; private String[] turnServers = new String[] { "stun.jitsi.net:3478" }; private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" }; private String username = "guest"; private String password = "anonymouspower!!"; private IceProcessingListener listener; static Logger log = Logger.getLogger(IceClient.class); public IceClient(int port, String streamName) { this.port = port; this.streamName = streamName; this.listener = new IceProcessingListener(); } public void init() throws Throwable { agent = createAgent(port, streamName); agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO); agent.addStateChangeListener(listener); agent.setControlling(false); agent.setTa(10000); localSdp = SdpUtils.createSDPDescription(agent); log.info("=================== feed the following" + " to the remote agent ==================="); System.out.println(localSdp); log.info("======================================" + "========================================\n"); } public DatagramSocket getDatagramSocket() throws Throwable { LocalCandidate localCandidate = agent .getSelectedLocalCandidate(streamName); IceMediaStream stream = agent.getStream(streamName); List<Component> components = stream.getComponents(); for (Component c : components) { log.info(c); } log.info(localCandidate.toString()); LocalCandidate candidate = (LocalCandidate) localCandidate; return candidate.getDatagramSocket(); } public SocketAddress getRemotePeerSocketAddress() { RemoteCandidate remoteCandidate = agent .getSelectedRemoteCandidate(streamName); log.info("Remote candinate transport address:" + remoteCandidate.getTransportAddress()); log.info("Remote candinate host address:" + remoteCandidate.getHostAddress()); log.info("Remote candinate mapped address:" + remoteCandidate.getMappedAddress()); log.info("Remote candinate relayed address:" + remoteCandidate.getRelayedAddress()); log.info("Remote candinate reflexive address:" + remoteCandidate.getReflexiveAddress()); return remoteCandidate.getTransportAddress(); } /** * Reads an SDP description from the standard input.In production * environment that we can exchange SDP with peer through signaling * server(SIP server) */ public void exchangeSdpWithPeer() throws Throwable { log.info("Paste remote SDP here. Enter an empty line to proceed:"); BufferedReader reader = new BufferedReader(new InputStreamReader( System.in)); StringBuilder buff = new StringBuilder(); String line = new String(); while ((line = reader.readLine()) != null) { line = line.trim(); if (line.length() == 0) { break; } buff.append(line); buff.append("\r\n"); } remoteSdp = buff.toString(); SdpUtils.parseSDP(agent, remoteSdp); } public void startConnect() throws InterruptedException { if (StringUtils.isBlank(remoteSdp)) { throw new NullPointerException( "Please exchange sdp information with peer before start connect! "); } agent.startConnectivityEstablishment(); // agent.runInStunKeepAliveThread(); synchronized (listener) { listener.wait(); } } private Agent createAgent(int rtpPort, String streamName) throws Throwable { return createAgent(rtpPort, streamName, false); } private Agent createAgent(int rtpPort, String streamName, boolean isTrickling) throws Throwable { long startTime = System.currentTimeMillis(); Agent agent = new Agent(); agent.setTrickling(isTrickling); // STUN for (String server : stunServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new StunCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP))); } // TURN LongTermCredential longTermCredential = new LongTermCredential(username, password); for (String server : turnServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new TurnCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP), longTermCredential)); } // STREAMS createStream(rtpPort, streamName, agent); long endTime = System.currentTimeMillis(); long total = endTime - startTime; log.info("Total harvesting time: " + total + "ms."); return agent; } private IceMediaStream createStream(int rtpPort, String streamName, Agent agent) throws Throwable { long startTime = System.currentTimeMillis(); IceMediaStream stream = agent.createMediaStream(streamName); // rtp Component component = agent.createComponent(stream, Transport.UDP, rtpPort, rtpPort, rtpPort + 100); long endTime = System.currentTimeMillis(); log.info("Component Name:" + component.getName()); log.info("RTP Component created in " + (endTime - startTime) + " ms"); return stream; } /** * Receive notify event when ice processing state has changed. */ public static final class IceProcessingListener implements PropertyChangeListener { private long startTime = System.currentTimeMillis(); public void propertyChange(PropertyChangeEvent event) { Object state = event.getNewValue(); log.info("Agent entered the " + state + " state."); if (state == IceProcessingState.COMPLETED) { long processingEndTime = System.currentTimeMillis(); log.info("Total ICE processing time: " + (processingEndTime - startTime) + "ms"); Agent agent = (Agent) event.getSource(); List<IceMediaStream> streams = agent.getStreams(); for (IceMediaStream stream : streams) { log.info("Stream name: " + stream.getName()); List<Component> components = stream.getComponents(); for (Component c : components) { log.info("------------------------------------------"); log.info("Component of stream:" + c.getName() + ",selected of pair:" + c.getSelectedPair()); log.info("------------------------------------------"); } } log.info("Printing the completed check lists:"); for (IceMediaStream stream : streams) { log.info("Check list for stream: " + stream.getName()); log.info("nominated check list:" + stream.getCheckList()); } synchronized (this) { this.notifyAll(); } } else if (state == IceProcessingState.TERMINATED) { log.info("ice processing TERMINATED"); } else if (state == IceProcessingState.FAILED) { log.info("ice processing FAILED"); ((Agent) event.getSource()).free(); } } } } import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.concurrent.TimeUnit; public class PeerA { public static void main(String[] args) throws Throwable { try { IceClient client = new IceClient(2020, "audio"); client.init(); client.exchangeSdpWithPeer(); client.startConnect(); final DatagramSocket socket = client.getDatagramSocket(); final SocketAddress remoteAddress = client .getRemotePeerSocketAddress(); System.out.println(socket.toString()); new Thread(new Runnable() { public void run() { while (true) { try { byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); System.out.println("receive:" + new String(packet.getData(), 0, packet .getLength())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { public void run() { int count = 1; while (true) { try { byte[] buf = ("send msg " + count++ + "").getBytes(); DatagramPacket packet = new DatagramPacket(buf, buf.length); packet.setSocketAddress(remoteAddress); socket.send(packet); System.out.println("send msg"); TimeUnit.SECONDS.sleep(10); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
5.參考資料 ICE: https://tools.ietf.org/html/rfc5245 SDP: http://tools.ietf.org/html/rfc4566 SIP: http://tools.ietf.org/html/rfc3261 NAT: http://en.wikipedia.org/wiki/Network_address_translation STUN: http://tools.ietf.org/html/rfc5389 TURN: http://tools.ietf.org/html/rfc5766 ICE4J: http://code.google.com/p/ice4j/