90年代に戻るか、Javaを介してポケットベルにメッセージを送信する方法

見出しを読んだ後、あなたはおそらく私が自分のために設定したタスクの異常な性質に少し驚いたでしょう。 ただし、奇妙なことに、過去15年間に登場した他のコミュニケーション手段が豊富にあるにもかかわらず、ポケットベルは生活に役立つことがあります。 その使用の特殊なケースの1つは、(約)WiFiと携帯電話の信号を減衰させる鉄筋コンクリートの建物内にある医療機関です。 ただし、アテンダントは、何らかの場合に緊急に移動する必要がある場所に関するメッセージを何らかの方法で受信する必要があります。 この問題を解決するために、この場合の機関の管理は高価なステーションとして設定し、ポケットベルをすべての従業員に配布しました。 したがって、私と同僚はそれらを送信することがタスクでした。



ページャーにテキストを送信するために、最初に電話ノードから眠い少女と話をしなければならなかった時はすでに過ぎていました。 これで、ステーションに到達して、トーンモードで加入者の番号とメッセージをダイヤルするだけで十分です。 武器庫は非常に限られています:数字、文字*および#、場合によっては文字ABCDのみを送信できます。 ただし、たとえば、部屋番号またはエラーコードを送信するだけで十分です。 これにより、タスクが大幅に簡素化され、他のユーザーと関連するようになります-たとえば、共通の会議室にダイヤルインするなど。



決定の明らかな透明性と私の経験の二次的性質にもかかわらず、トピックに関するインターネット上の情報があまりないので、私は自分の行動を詳細に説明することにしました。 一部の人にとって、このテキストは多くの時間を節約するかもしれません。



画像



ステップ1-招待


最初の段階-ページングステーションへのダイヤル-は、SIPプロトコルと適切なjain-sip Javaライブラリを使用して実装されまし 。 Habréの出版物「SIPクライアントインタラクション」で見つけたプロトコルの原則の最良の説明 パート1「および」SIPクライアントインタラクション。 パート2」 、およびジェーンに関する最も消化しやすいチュートリアルはこちらです (ただし、 ここからのサンプルのコレクションは拒否されました)。



前提条件として、クラスを作成しました:



public class SipNotificator implements SipListener
      
      





チュートリアルで示されているように、最初に初期化する必要がある必要なフィールドを使用します。



 private SipProvider sipProvider; private SipFactory sipFactory; private SdpFactory sdpFactory;//  private AddressFactory addressFactory; private HeaderFactory headerFactory; private MessageFactory messageFactory;
      
      





ルールからわかるように、まず電話にINVITEメッセージを送信する必要があります。 ToヘッダーとRequestヘッダーの宛先の記述が異なることに注意してください。 前者の場合、ヘッダーは単にグローバルな電話番号から収集されます:



 Address toNameAddress = addressFactory.createAddress( addressFactory.createTelURL(adresseenumber)); ToHeader toHeader = headerFactory.createToHeader(toNameAddress, null);
      
      





2番目の場合、通信を開始するメッセージの送信元のホストを指定する必要があります。



 URI requestURI = addressFactory.createAddress("sip:"+adresseenumber+"@"+host+";user=phone").getURI();
      
      





別の興味深い要素は、実際には、 SDPメッセージの本文です。これは、正常な通信に必要なものの説明です。 私たちの場合、次のようになりました。



 String sdpData = "v=0\r\n" + "o=4444 123456 789054 IN IP4 "+InetAddress.getLocalHost().getHostAddress() +"\r\n" + "s=phone call\r\n" + "p="+phoneusername+"\r\n" + "c=IN IP4 "+InetAddress.getLocalHost().getHostAddress() +"\r\n" + "t=0 0\r\n" + "m=audio "+localUdpPort+" RTP/AVP 0 8 18 101\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:18 G729A/8000\r\n" + "a=fmtp:18 annexb=no\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-16\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n";
      
      





属性「o」と「s」は特に重要ではありません。「p」では電話番号を記述します。 主な部分は「m」(メディア)であり、使用されるコーデック(この場合、送信者にインストールされていない可能性があります)とトピックに関する応答を受信するポートを定義します。



ステップ2-認証


正しい招待を送信できた場合、最良の場合、受信者サーバーは200のステータスで目的のOKメッセージを送信し、最悪の場合、識別で少し苦しめることを決定します。 2番目の場合、応答ステータスは401または407になります。応答を送信するコードを次に示します。 これをサポートするには、jain-sipの最新バージョン(1.2.228など)のいずれかが必要です。 引数としてResponseEvent responseEvtを受け取るprocessResponse()メソッドに配置する必要があります。



 if (status == 401|| status == 407){ AuthenticationHelper authenticationHelper = ((SipStackExt) sipProvider.getSipStack()).getAuthenticationHelper( new AccountManagerImpl(this.phoneusername, this.password), headerFactory); transaction = authenticationHelper.handleChallenge(responceEvt.getResponse(), responceEvt.getClientTransaction(), sipProvider, 15, true); dialog = transaction.getDialog(); transaction.sendRequest(); }
      
      





handleChallenge()メソッドの4番目の引数に注意してください。これがないと、メッセージ形式が変更され、不適切になり、認証が失敗します。



AccountManagerImplクラスと必要なUserCredentialsImplクラスを追加する必要がありますここで紹介するモデルに従って作成しまし



登録データを送信した後、希望する200 OKを安全に期待できます。ACKの送信を忘れないでください。 このタイプのメッセージは非常にシンプルになっています。



 Request ackRequest = dialog.createAck( ((CSeqHeader) responseEvt.getResponse().getHeader(CSeqHeader.NAME)).getSeqNumber() ); //dialog -  
      
      







ステップ3-SIP INFO


次に、楽しみが始まります-DTMF信号(トーンモードで同じトーン)を送信します。 グローバルに、これはSIPRTPの 2つの異なるプロトコルを介して実行できます。 当然、最初は抵抗が最も少ない経路をたどることが決定されました。 各キャラクターについて、そのようなリクエストが生成され、サーバーに送信する必要がありました:



 Request info = dialog.createRequest(Request.INFO); String sdpData = "Signal="+digit+"\r\n" + "Duration=200"; byte[] contents = sdpData.getBytes(); ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "dtmf-relay"); info.setContent(contents, contentTypeHeader); ClientTransaction transaction = sipProvider.getNewClientTransaction(info); Dialog dialog = transaction.getDialog(); dialog.sendRequest(transaction);
      
      





送信手順自体は少し奇妙に見えますが(「ZhmerinkaからParisへ」モデル上に構築されているようです)、それ以外の場合は何も機能しませんでした。 一般的に、このライブラリは少しバグが多いように見えました。基本的に同じように見えるいくつかのソリューションの1つが機能しなかったことが非常に多くありました。



何が言えますか? このステップを実装した後、すべてのVoIPサーバーが同等に友好的ではないことが判明しました。一部のサーバーはSIPを介して十分な信号を送信しましたが、他のサーバーは音声信号を生成しないため気付かないため、十分な信号がありませんでした。 当然、意地悪の法則によれば、私の目標は2番目のタイプのサーバーでした。 したがって...



ステップ4. RTPパケットの形成


一般に、1つのSIPでは問題を解決できないことに気付いたとき、少なくともDTMF信号を簡単に送信できる別のライブラリを使用できることを望みました。 しかし、そこにありました。 通常、「Javaを介したRTP」と言う場合、 JMFを意味します。 しかし、最初に、それはすでに古く、特にサポートされていません。 第二に、より複雑なメディアの転送に適しています。 第三に、私が見つけたチュートリアルはあまり賢明ではありませんでした。 ここにドキュメントからの1つの例を示します。その中には、特定のrtpSessionがポップアップしますが、検索の最初の数分ではまったく見つかりませんでした。



もう1つのオプションはlibjitsiライブラリで、これは完全なコミュニケーターです。 素晴らしいsendDTMFメソッドまたはそのようなものがありますが、そこから何も借りることができません。 コードの構造は、完全に取られるか、まったく取られないようになっています。 その結果、人間のパケットを作成し、UDPソケットを介して送信することが通常の方法で決定されました。



そのため、ここにRtpPacketクラスの重要なフラグメントがあります。その主要フィールドと、DTMFの送信に適した値を持つコンストラクタです。 これらすべてが意味することは、多くの場所に書かれているので、繰り返しません。 原則としてssrcパラメーターの値は役割を果たしませんが、同じセッションで送信されるすべてのパケットについては一致する必要があることに注意してください。 ペイロードタイプのペイロードタイプは101です(SIP通信を開始したときに登録しました)。



  private int version; private boolean padding; private boolean extension; private int csrcCount; private boolean marker; private int payloadType; private int sequenceNumber; private long timestamp; private long ssrc; private long[] csrcList; private byte[] data; public RtpPacket(){ this.setVersion(2); this.setPadding(false); this.setExtension(false); this.setCsrcCount(0); long[] list = {}; this.setCsrcList(list); }
      
      







パッケージを作成する上で最も重要なステップは、バイトデータ配列の入力です。 DTMFには独自の形式があります:実際には、最初のバイトは送信信号の値(0から16)、2番目のバイトの前半はさまざまな市場(通常0)、2番目のバイトの後半はボリューム(標準値は10)、残りは2は期間です(デフォルトは160です)。



信号ごとに約10個のパケットが作成されます(数は異なる場合があります)。



-最初のイニシャルはマーカー= 1、残りは0です。

-最後の3つは最終、マーカー= 0、データブロックの2番目のバイトの最初のビット=1。信号1を送信するための非最終パケットのデータブロックは次のようになります。



 0000 0001 0000 1010 0000 0000 1010 0000   
      
      





そして最終的には、このように:



 0000 0001 1000 1010 0000 0000 1010 0000  end  
      
      





同じ信号に属するすべてのDTMFパケットのタイムスタンプは同じままでかまいません(Tと仮定)。 ただし、次のパッケージの時間は次のとおりです。



 T+( DTMF- *  )
      
      







ステップ5. RTPチャネル


メッセージを送受信するためのソケットを1つだけ作成できます。 ただし、ホストと宛先ポートがわからなければ、いずれにしても、送信は自然に行われません。 これは、SIP通信が通過したデータではありません。 電話サーバーは、ダイヤラー中に応答メッセージでその座標を送信します。 これらを取得するには、次のコードをprocessResponce()メソッドに挿入します(ここで、sdpFactoryを以前に初期化した理由を確認できます)。



 Response resp = responceEvent.getResponse(); int remoteHost; int remotePort; if (resp.getRawContent()!=null){ String sdpContent = new String(resp.getRawContent()); SessionDescription requestSDP = sdpFactory.createSessionDescription(sdpContent); remoteHost = requestSDP.getConnection().getAddress();//   Connection Information Vector<MediaDescription> media = requestSDP.getMediaDescriptions(false); for (MediaDescription m:media){ if (m.getMedia()!=null ) remotePort =m.getMedia().getMediaPort();//     media } }
      
      





さらに、私は単純に考えたように、自分のバイトからDatagramPacketsを実行し、それらをソケットに入れてサーバーにプルすることしかできませんでした。 しかし、そこにありました。 これに応じて、サーバーは何も受信していないかのように、一目で通信を中断し続けました。 そして、Wiresharkは、原則として、RTPのメッセージを受け取らず、単純なUDPとして表示しました。



ステップ6. RTP通信


どの方向に進むべきかを理解するのに長い時間がかかりました。 利用可能なすべての仕様を読み直し、パッケージの正確性を100回確認することに多大な労力を費やしました。 7日目に、私の目のVision Visionは、標準のRTP通信がDTMFデータの送信ですぐに開始されず、サーバーとの短いパケット交換が先行することに気づきました。

ヘッダーで宣言されたペイロード形式は0で、データはありませんが、実際にはペイロード自体があり、160バイトかかります。 このバイトセットは、すべての着信メッセージと発信メッセージで異なり、まったくランダムに構成されます。 どういうわけか、それがどのように形成されるべきかについての情報を見つけることができなかったので、毎回それを乱数で打ちました。



各DTMF信号の前にこれらの補助パケットの送信を開始した後、Wiresharkは最終的にRTP形式を認識しました。 すべてが良く見えましたが、通信はまだ中断されましたが、サーバーは今では喜びのために「ペイロード」パケットを私に浴びせ始めました。



他に何ができるのかわかりませんでしたが、RTPは不可分の兄弟であるRTCPであることを思い出しました。 どうやら、問題は本当にそこにあった。サーバーは何かを送ろうとしていたが、彼は対応するポートが閉じられているというメッセージを絶えず受け取っていた。 RTCPパケットの送信を気にしたくないので、ポートチャクラを開くことから始めました。



 DatagramSocket socket = new DatagramSocket(localUdpPort, InetAddress.getLocalHost()); DatagramSocket controlSocket = new DatagramSocket(localUdpPort+1, InetAddress.getLocalHost());//    1 ,   RTP
      
      





これには決定的な効果がありました。加入者はポケットベルで「305 * 1 * 66」というメッセージを受け取りました。



おわりに


カートの最後の行で、これがHabréの最初の投稿であることを強調したいので、厳密に判断しないでください。 私は自分自身をテレマティクスの第一人者や他の何者でもないと考えています。 ソースコードを書くとき、情報の検索に多くの時間が費やされたというだけです。 私は仕様書に何かを見つけました。最初から最後まで一度に習得することは困難で、何かは普通の言語で記述されていましたが、マージンの柔らかい印刷で何とかして、私はランダムに何かをしました。 そのため、ある時点で、すべてがうまくいったら、すべてのアクションを1か所で説明し、インターネット上のどこかにインデックスを作成することにしました。



だから、少なくとも誰かが私の記事を役に立つと思うか、少なくとも面白そうに見えることを本当に願っています。



All Articles