2013年9月7日星期六

再谈SipDroid

研究了SipDroid2.7,自己对它的理解也渐渐的清晰了。
那它是怎样实现电话拨打以及电话监听的?它的音频接收以及发送是怎么实现的?它的视频又是怎么一回事?它在模拟器上的端口为什么总是变化的?它又是如何处理登陆超时以及通话出错的?
带着这些疑问进入它的代码思想境界!
使用yate搭配服务器,然后使用了一个yate与SipDroid客户端进行通话!~至于怎么搭配服务器以及SipDroid的配置设置,此处就不讨论了!~ 
登陆后的标识效果如图:
然后我使用了yate客户端程序拨打了SipDroid程序段上的帐号(banketree),如图:
接通后的效果图像如图:
好了,进入我们的主题了!~
它是怎样实现电话拨打以及电话监听的?
程序进入时会进行服务注册,如下:
[java] view plaincopy
  1. // 实例化一个引擎 注册模式    由登陆时进行……  
  2. /   Receiver.engine(this).registerMore();  

我们知道Receiver.engine(this) 进行了SipUA引擎的实例化,并开启SipUA引擎,关键就SipUA引擎了!~
[java] view plaincopy
  1. // 构造引擎  
  2. public static synchronized SipdroidEngine engine(Context context)  
  3. {  
  4.     // 构造引擎的条件  
  5.     if (mContext == null  
  6.             || !context.getClass().getName()  
  7.                     .contains("ReceiverRestrictedContext"))  
  8.     {  
  9.         mContext = context;  
  10.     }  
  11.   
  12.     // 为空则构造  
  13.     if (mSipdroidEngine == null)  
  14.     {  
  15.         mSipdroidEngine = new SipdroidEngine();  
  16.   
  17.         // 开始   
  18.         mSipdroidEngine.StartEngine();  
  19.   
  20.         // 开启蓝牙服务  
  21.         if (Integer.parseInt(Build.VERSION.SDK) >= 8)  
  22.         {  
  23.             Bluetooth.init();  
  24.         }  
  25.     } else  
  26.     {  
  27.         // 引擎已经存在  
  28.         mSipdroidEngine.CheckEngine();  
  29.     }  
  30.   
  31.     // 开启服务  
  32.     context.startService(new Intent(context, RegisterService.class));  
  33.   
  34.     return mSipdroidEngine;  
  35. }  
而StartEngine()里有开启监听!~
[java] view plaincopy
  1. //注册  在此向服务器发送请求。  
  2. register();  
  3.   
  4. //实例化一个ExtendedCall 循环监听指定端口   
  5. listen();  

至于里面是怎么实现的,那就涉及到了SipUA封装的一些类以及消息了!~ 
消息以及协议的通信都是SipProvider类由完成的!~
[java] view plaincopy
  1. public void onReceivedMessage(Transport transport, Message msg)  
  2. {  
  3.     if (pm == null)  
  4.     {  
  5.         pm = (PowerManager) Receiver.mContext.getSystemService(Context.POWER_SERVICE);  
  6.         wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Sipdroid.SipProvider");  
  7.     }  
  8.     wl.acquire(); // modified  
  9.     //处理接收消息   当程序进行了监听,就会向此类添加一个监听事件标识   
  10.     //处理消息就是根据标识进行区分的,例如来电、接听、视频等等!~~~  
  11.     processReceivedMessage(msg);    
  12.     wl.release();  
  13. }  

向服务器发送信息(发送协议进行登陆以及获取对方信息等等)也是由该类完成的!~如下:
[java] view plaincopy
  1. /** 
  2.      * Sends a Message, specifing the transport portocol, nexthop address and 
  3.      * port. 
  4.      * 发送一条消息,指定传输协议、地址、以及端口。 
  5.      */  
  6.     private ConnectionIdentifier sendMessage(Message msg, String proto, IpAddress dest_ipaddr, int dest_port, int ttl)  
  7.     {  
  8.         if(bDebug)  
  9.         {  
  10.             android.util.Log.i("SipProvider发送消息""msg:"+msg.toString());  
  11.         }  
  12.           
  13.         ConnectionIdentifier conn_id = new ConnectionIdentifier(proto, dest_ipaddr, dest_port);  
  14.         if (log_all_packets || msg.getLength() > MIN_MESSAGE_LENGTH)  
  15.             printLog("Sending message to " + conn_id, LogLevel.MEDIUM);  
  16.   
  17.         if (transport_udp && proto.equals(PROTO_UDP))  
  18.         {   
  19.             // UDP  
  20.             // printLog("using UDP",LogLevel.LOW);  
  21.             conn_id = null;  
  22.             try  
  23.             {   
  24.                 // if (ttl>0 && multicast_address) do something?  
  25.                 udp.sendMessage(msg, dest_ipaddr, dest_port);  
  26.             } catch (IOException e)  
  27.             {  
  28.                 printException(e, LogLevel.HIGH);  
  29.                 return null;  
  30.             }  
  31.         } else if (transport_tcp && proto.equals(PROTO_TCP))  
  32.         {   
  33.             // TCP  
  34.             // printLog("using TCP",LogLevel.LOW);  
  35.             if (connections == null || !connections.containsKey(conn_id))  
  36.             {   
  37.                 // modified  
  38.                 printLog("no active connection found matching " + conn_id, LogLevel.MEDIUM);  
  39.                 printLog("open " + proto + " connection to " + dest_ipaddr + ":" + dest_port, LogLevel.MEDIUM);  
  40.                 TcpTransport conn = null;  
  41.                 try  
  42.                 {  
  43.                     conn = new TcpTransport(dest_ipaddr, dest_port, this);  
  44.                 } catch (Exception e)  
  45.                 {  
  46.                     printLog("connection setup FAILED", LogLevel.HIGH);  
  47.                     return null;  
  48.                 }  
  49.                   
  50.                 printLog("connection " + conn + " opened", LogLevel.HIGH);  
  51.                 addConnection(conn);  
  52.                 if (!msg.isRegister())  
  53.                     Receiver.engine(Receiver.mContext).register(); // modified  
  54.             } else  
  55.             {  
  56.                 printLog("active connection found matching " + conn_id, LogLevel.MEDIUM);  
  57.             }  
  58.             ConnectedTransport conn = (ConnectedTransport) connections.get(conn_id);  
  59.             if (conn != null)  
  60.             {  
  61.                 printLog("sending data through conn " + conn, LogLevel.MEDIUM);  
  62.                 try  
  63.                 {  
  64.                     conn.sendMessage(msg);  
  65.                     conn_id = new ConnectionIdentifier(conn);  
  66.                 } catch (IOException e)  
  67.                 {  
  68.                     printException(e, LogLevel.HIGH);  
  69.                     return null;  
  70.                 }  
  71.             } else  
  72.             {   
  73.                 // this point has not to be reached这一点还没有达到  
  74.                 printLog("ERROR: conn " + conn_id + " not found: abort.", LogLevel.MEDIUM);  
  75.                 return null;  
  76.             }  
  77.         } else  
  78.         { // otherwise  
  79.             printWarning("Unsupported protocol (" + proto + "): Message discarded", LogLevel.HIGH);  
  80.             return null;  
  81.         }  
  82.         // logs  
  83.         String dest_addr = dest_ipaddr.toString();  
  84.         printMessageLog(proto, dest_addr, dest_port, msg.getLength(), msg, "sent");  
  85.         return conn_id;  
  86.     }  

消息的信息格式如下:
……
回到问题中来,监听电话已经明白了,那是如何实现拨打的?
当用户登陆后就会把自己的信息发送给服务端,服务端记录下来,等用户需要时就返回给他,比如我打banketree电话,我没有他的信息我怎么打呀,是吧!~
拨打电话的关键在UserAgent中,其它的都是封装好处理信息的类!~
[java] view plaincopy
  1. public boolean call(String target_url, boolean send_anonymous)  
  2. {  
  3.   
  4.     if (Receiver.call_state != UA_STATE_IDLE)  
  5.     {  
  6.         // We can initiate or terminate a call only when  
  7.         // we are in an idle state  
  8.         //只有当我们处于闲置状态,我们可以开始或结束通话  
  9.         printLog("Call attempted in state" + this.getSessionDescriptor()  
  10.                 + " : Failing Request", LogLevel.HIGH);  
  11.         return false;  
  12.     }  
  13.       
  14.     //挂断  
  15.     hangup(); // modified  
  16.       
  17.     //改变状态  
  18.     changeStatus(UA_STATE_OUTGOING_CALL, target_url);  
  19.   
  20.     String from_url;  
  21.   
  22.     if (!send_anonymous)  
  23.     {  
  24.         from_url = user_profile.from_url;  
  25.     } else  
  26.     {  
  27.         from_url = "sip:anonymous@anonymous.com";  
  28.     }  
  29.   
  30.     // change start multi codecs  更改启动多编解码器  
  31.     createOffer();  
  32.     // change end  
  33.       
  34.     call = new ExtendedCall(sip_provider, from_url,  
  35.             user_profile.contact_url, user_profile.username,  
  36.             user_profile.realm, user_profile.passwd, this);  
  37.   
  38.     // in case of incomplete url (e.g. only 'user' is present), try to  
  39.     // complete it  
  40.     //在不完整的URL(例如,“用户”是存在的话)的情况下,尽量去完成它  
  41.     if (target_url.indexOf("@") < 0)  
  42.     {  
  43.         if (user_profile.realm.equals(Settings.DEFAULT_SERVER))  
  44.             target_url = "&" + target_url;  
  45.           
  46.         target_url = target_url + "@" + realm; // modified  
  47.     }  
  48.   
  49.     // MMTel addition to define MMTel ICSI to be included in INVITE (added  
  50.     // by mandrajg)  要包含在INVITE MMTel除了定义MMTel ICSI  
  51.     String icsi = null;  
  52.     if (user_profile.mmtel == true)  
  53.     {  
  54.         icsi = "\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";  
  55.     }  
  56.   
  57.     //从服务器中获得对方的地址  
  58.     target_url = sip_provider.completeNameAddress(target_url).toString();  
  59.       
  60.     //真的拨打电话  
  61.     if (user_profile.no_offer)  
  62.     {  
  63.         call.call(target_url);  
  64.     } else  
  65.     {  
  66.         call.call(target_url, local_session, icsi); // modified by mandrajg  
  67.     }  
  68.   
  69.     return true;  
  70. }  
那它的音频接收以及发送是怎么实现的?
管理音频有一个类,它是JAudioLauncher,当实例化它的时候,它会开启两个线程,一个是RtpStreamReceiver,另一个是RtpStreamReceiver!~
看标题就知道它们一个是发送的,一个是接收的!~
先来看下接收是怎么实现的!~
[java] view plaincopy
  1.     public void run()  
  2.     {  
  3.         boolean nodata = PreferenceManager.getDefaultSharedPreferences(  
  4.                 Receiver.mContext).getBoolean(  
  5.                 org.sipdroid.sipua.ui.Settings.PREF_NODATA,  
  6.                 org.sipdroid.sipua.ui.Settings.DEFAULT_NODATA);  
  7.         keepon = PreferenceManager.getDefaultSharedPreferences(  
  8.                 Receiver.mContext).getBoolean(  
  9.                 org.sipdroid.sipua.ui.Settings.PREF_KEEPON,  
  10.                 org.sipdroid.sipua.ui.Settings.DEFAULT_KEEPON);  
  11.   
  12.         if (rtp_socket == null)  
  13.         {  
  14.             if (DEBUG)  
  15.             {  
  16.                 println("ERROR: RTP socket is null(出错:rtp接受套接字出错!)");  
  17.             }  
  18.             return;  
  19.         }  
  20.   
  21.         //发送包 缓冲区  
  22.         byte[] buffer = new byte[BUFFER_SIZE + 12];  
  23.           
  24.         //构建包实例  
  25.         rtp_packet = new RtpPacket(buffer, 0);  
  26.   
  27.         if (DEBUG)  
  28.         {  
  29.             println("Reading blocks of max (读取快的最大值) = " + buffer.length + " bytes");  
  30.         }  
  31.   
  32.         running = true;  
  33.           
  34.         //开启蓝牙  
  35.         enableBluetooth(PreferenceManager.getDefaultSharedPreferences(  
  36.                 Receiver.mContext).getBoolean(  
  37.                 org.sipdroid.sipua.ui.Settings.PREF_BLUETOOTH,  
  38.                 org.sipdroid.sipua.ui.Settings.DEFAULT_BLUETOOTH));  
  39.           
  40.         restored = false;  
  41.   
  42.         //设置线程权限  
  43.         android.os.Process  
  44.                 .setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);  
  45.           
  46.         am = (AudioManager) Receiver.mContext  
  47.                 .getSystemService(Context.AUDIO_SERVICE);  
  48.         cr = Receiver.mContext.getContentResolver();  
  49.           
  50.         //保存设置  
  51.         saveSettings();  
  52.         Settings.System.putInt(cr, Settings.System.WIFI_SLEEP_POLICY,  
  53.                 Settings.System.WIFI_SLEEP_POLICY_NEVER);  
  54.         am.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER,  
  55.                 AudioManager.VIBRATE_SETTING_OFF);  
  56.         am.setVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION,  
  57.                 AudioManager.VIBRATE_SETTING_OFF);  
  58.         if (oldvol == -1)  
  59.         {  
  60.             oldvol = am.getStreamVolume(AudioManager.STREAM_MUSIC);  
  61.         }  
  62.   
  63.         //初始化模式  
  64.         initMode();  
  65.           
  66.         //设置音频编解码器  
  67.         setCodec();  
  68.   
  69.         //编辑音频  由发送包解码后放到此处 然后调整后放入到声音播放器中  
  70.         short lin[] = new short[BUFFER_SIZE];  
  71.         //它是负责清空lin的  
  72.         short lin2[] = new short[BUFFER_SIZE];  
  73.         int server, headroom, todo, len = 0, m = 1, expseq, getseq, vm = 1, gap, gseq;  
  74.           
  75.         ToneGenerator tg = new ToneGenerator(  
  76.                 AudioManager.STREAM_VOICE_CALL,  
  77.                 (int) (ToneGenerator.MAX_VOLUME * 2 * org.sipdroid.sipua.ui.Settings  
  78.                         .getEarGain()));  
  79.           
  80.         //播放  
  81.         track.play();  
  82.           
  83.         //指示虚拟机运行垃圾收集器,这将是一个好时机。  
  84.         //请注意,这仅是一个提示。有没有保证,垃圾收集器将实际运行。  
  85.         System.gc();  
  86.           
  87.         //清空  
  88.         empty();  
  89.           
  90.         lockFirst = true;  
  91.   
  92.         while (running)  
  93.         {  
  94.             lock(true);  
  95.               
  96.             if (Receiver.call_state == UserAgent.UA_STATE_HOLD)  
  97.             {  
  98.                 lock(false);  
  99.                   
  100.                 //停止  
  101.                 tg.stopTone();  
  102.                   
  103.                 //暂停  
  104.                 track.pause();  
  105.                   
  106.                 while (running  
  107.                         && Receiver.call_state == UserAgent.UA_STATE_HOLD)  
  108.                 {  
  109.                     try  
  110.                     {  
  111.                         sleep(1000);  
  112.                     } catch (InterruptedException e1)  
  113.                     {  
  114.                     }  
  115.                 }  
  116.   
  117.                 track.play();  
  118.                 System.gc();  
  119.                 timeout = 1;  
  120.                 luser = luser2 = -8000 * mu;  
  121.             }  
  122.               
  123.             try  
  124.             {  
  125.                 // 接受数据  
  126.                 rtp_socket.receive(rtp_packet);  
  127.   
  128.                 //超时  
  129.                 if (timeout != 0)  
  130.                 {  
  131.                     //停止音乐  
  132.                     tg.stopTone();  
  133.                       
  134.                     //暂停声音  
  135.                     track.pause();  
  136.                       
  137.                     //清空  
  138.                     for (int i = maxjitter * 2; i > 0; i -= BUFFER_SIZE)  
  139.                     {  
  140.                         write(lin2, 0, i > BUFFER_SIZE ? BUFFER_SIZE : i);  
  141.                     }  
  142.   
  143.                     cnt += maxjitter * 2;  
  144.                       
  145.                     //声音播放  
  146.                     track.play();  
  147.                     empty();  
  148.                 }  
  149.                 timeout = 0;  
  150.             } catch (IOException e)  
  151.             {  
  152.                 if (timeout == 0 && nodata)  
  153.                 {  
  154.                     tg.startTone(ToneGenerator.TONE_SUP_RINGTONE);  
  155.                 }  
  156.   
  157.                 //异常者 断开  
  158.                 rtp_socket.getDatagramSocket().disconnect();  
  159.                   
  160.                 if (++timeout > 60)  
  161.                 {  
  162.                     Receiver.engine(Receiver.mContext).rejectcall();  
  163.                     break;  
  164.                 }  
  165.             }  
  166.               
  167.             //在运行 且未超时  
  168.             if (running && timeout == 0)  
  169.             {  
  170.                 //得到数字字符  
  171.                 gseq = rtp_packet.getSequenceNumber();  
  172.                   
  173.                 //得到当前接受包的大小  
  174.                 if (seq == gseq)  
  175.                 {  
  176.                     m++;  
  177.                     continue;  
  178.                 }  
  179.                   
  180.                   
  181.                 gap = (gseq - seq) & 0xff;  
  182.                   
  183.                 if (gap > 240)  
  184.                 {  
  185.                     continue;  
  186.                 }  
  187.                   
  188.                 //返回帧中表示播放头位置。  
  189.                 server = track.getPlaybackHeadPosition();  
  190.                   
  191.                 //接受包的总大小 - 当前播放的位置  
  192.                 headroom = user - server;  
  193.   
  194.                 if (headroom > 2 * jitter)  
  195.                 {  
  196.                     cnt += len;  
  197.                 } else  
  198.                 {  
  199.                     cnt = 0;  
  200.                 }  
  201.   
  202.                 if (lserver == server)  
  203.                 {  
  204.                     cnt2++;  
  205.                 } else  
  206.                 {  
  207.                     cnt2 = 0;  
  208.                 }  
  209.   
  210.                 if (cnt <= 500 * mu || cnt2 >= 2 || headroom - jitter < len  
  211.                         || p_type.codec.number() != 8  
  212.                         || p_type.codec.number() != 0)  
  213.                 {  
  214.                     //有效负荷类型||改变类型  
  215.                     if (rtp_packet.getPayloadType() != p_type.number  
  216.                             && p_type.change(rtp_packet.getPayloadType()))  
  217.                     {  
  218.                         //保留声量  
  219.                         saveVolume();  
  220.                           
  221.                         //设置编解码器  
  222.                         setCodec();  
  223.                           
  224.                         //恢复声量  
  225.                         restoreVolume();  
  226.                           
  227.                         //头信息  
  228.                         codec = p_type.codec.getTitle();  
  229.                     }  
  230.                       
  231.                     //得到有效负荷长度  
  232.                     len = p_type.codec.decode(buffer, lin,  
  233.                             rtp_packet.getPayloadLength());  
  234.   
  235.                     // Call recording: Save incoming.Data is in buffer lin, from 0 to len.  
  236.                     //通话记录:保存传入。数据是在缓冲区林,从0到LEN。  
  237.                     if (call_recorder != null)  
  238.                     {  
  239.                         //写入  
  240.                         call_recorder.writeIncoming(lin, 0, len);  
  241.                     }  
  242.   
  243.                     //声音改变效果显示  
  244.                     if (speakermode == AudioManager.MODE_NORMAL)  
  245.                     {  
  246.                         calc(lin, 0, len);  
  247.                     } else if (gain > 1)  
  248.                     {  
  249.                         calc2(lin, 0, len);  
  250.                     }  
  251.                 }  
  252.   
  253.                 //  
  254.                 avgheadroom = avgheadroom * 0.99 + (double) headroom * 0.01;  
  255.                 if (avgcnt++ > 300)  
  256.                 {  
  257.                     devheadroom = devheadroom * 0.999  
  258.                             + Math.pow(Math.abs(headroom - avgheadroom), 2)  
  259.                             * 0.001;  
  260.                 }  
  261.                   
  262.                 //头大小  
  263.                 if (headroom < 250 * mu)  
  264.                 {  
  265.                     late++;  
  266.                     newjitter(true);  
  267. //                  System.out.println("RTP:underflow "  
  268. //                          + (int) Math.sqrt(devheadroom));  
  269.                     todo = jitter - headroom;  
  270.                     write(lin2, 0, todo > BUFFER_SIZE ? BUFFER_SIZE : todo);  
  271.                 }  
  272.   
  273.                 //  
  274.                 if (cnt > 500 * mu && cnt2 < 2)  
  275.                 {  
  276.                     todo = headroom - jitter;  
  277.                     if (todo < len)  
  278.                     {  
  279.                         write(lin, todo, len - todo);  
  280.                     }  
  281.                 } else  
  282.                 {  
  283.                     write(lin, 0, len);  
  284.                 }  
  285.   
  286.                 //丢失包统计  
  287.                 if (seq != 0)  
  288.                 {  
  289.                     getseq = gseq & 0xff;  
  290.                     expseq = ++seq & 0xff;  
  291.                     if (m == RtpStreamSender.m)  
  292.                     {  
  293.                         vm = m;  
  294.                     }  
  295.                       
  296.                     gap = (getseq - expseq) & 0xff;  
  297.                     if (gap > 0)  
  298.                     {  
  299.                     //  System.out.println("RTP:lost");  
  300.                         if (gap > 100)  
  301.                         {  
  302.                             gap = 1;  
  303.                         }  
  304.                         loss += gap;  
  305.                         lost += gap;  
  306.                         good += gap - 1;  
  307.                         loss2++;  
  308.                     } else  
  309.                     {  
  310.                         if (m < vm)  
  311.                         {  
  312.                             loss++;  
  313.                             loss2++;  
  314.                         }  
  315.                     }  
  316.                     good++;  
  317.                     if (good > 110)  
  318.                     {  
  319.                         good *= 0.99;  
  320.                         lost *= 0.99;  
  321.                         loss *= 0.99;  
  322.                         loss2 *= 0.99;  
  323.                         late *= 0.99;  
  324.                     }  
  325.                 }  
  326.                 m = 1;  
  327.                 seq = gseq;  
  328.   
  329.                 if (user >= luser + 8000 * mu  
  330.                         && (Receiver.call_state == UserAgent.UA_STATE_INCALL || Receiver.call_state == UserAgent.UA_STATE_OUTGOING_CALL))  
  331.                 {  
  332.                     if (luser == -8000 * mu || getMode() != speakermode)  
  333.                     {  
  334.                         //保留声量  
  335.                         saveVolume();  
  336.                           
  337.                         //设置模式  
  338.                         setMode(speakermode);  
  339.                           
  340.                         //恢复声量  
  341.                         restoreVolume();  
  342.                     }  
  343.                   
  344.                     luser = user;  
  345.                       
  346.                     if (user >= luser2 + 160000 * mu)  
  347.                     {  
  348.                         //抖动  
  349.                         newjitter(false);  
  350.                     }  
  351.                 }  
  352.                 lserver = server;  
  353.             }  
  354.         }  

再看下发送包是怎么实现的,发送声音肯定是先录制,后读取,再发送!~如下:
[java] view plaincopy
  1.     public void run()  
  2.     {  
  3.         //测试音频数据  
  4. //      testVoiceFile mvoiceFile = new testVoiceFile("voice");  
  5. //      testVoiceFile mvoiceRtpFile = new testVoiceFile("rtpdate");  
  6.           
  7.         //wifi管理  
  8.         WifiManager wm = (WifiManager) Receiver.mContext  
  9.                 .getSystemService(Context.WIFI_SERVICE);  
  10.         long lastscan = 0, lastsent = 0;  
  11.   
  12.         if (rtp_socket == null)  
  13.             return;  
  14.         int seqn = 0;  
  15.         long time = 0;  
  16.         double p = 0;  
  17.           
  18.         //改善  
  19.         boolean improve = PreferenceManager.getDefaultSharedPreferences(  
  20.                 Receiver.mContext).getBoolean(Settings.PREF_IMPROVE,  
  21.                 Settings.DEFAULT_IMPROVE);  
  22.           
  23.         //选择wifi  
  24.         boolean selectWifi = PreferenceManager.getDefaultSharedPreferences(  
  25.                 Receiver.mContext).getBoolean(  
  26.                 org.sipdroid.sipua.ui.Settings.PREF_SELECTWIFI,  
  27.                 org.sipdroid.sipua.ui.Settings.DEFAULT_SELECTWIFI);  
  28.           
  29.         int micgain = 0;  
  30.         long last_tx_time = 0;  
  31.         long next_tx_delay;  
  32.         long now;  
  33.         running = true;  
  34.         m = 1;  
  35.         int dtframesize = 4;  
  36.   
  37.         //改变线程权限  
  38.         android.os.Process  
  39.                 .setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);  
  40.           
  41.         //解码速率  
  42.         mu = p_type.codec.samp_rate() / 8000;  
  43.         int min = AudioRecord.getMinBufferSize(p_type.codec.samp_rate(),  
  44.                 AudioFormat.CHANNEL_CONFIGURATION_MONO,  
  45.                 AudioFormat.ENCODING_PCM_16BIT);  //最小缓冲大小  
  46.           
  47.         if (min == 640)  
  48.         {  
  49.             if (frame_size == 960)  
  50.                 frame_size = 320;  
  51.             if (frame_size == 1024)  
  52.                 frame_size = 160;  
  53.             min = 4096 * 3 / 2;  
  54.         } else if (min < 4096)  
  55.         {  
  56.             if (min <= 2048 && frame_size == 1024)  
  57.                 frame_size /= 2;  
  58.             min = 4096 * 3 / 2;  
  59.         } else if (min == 4096)  
  60.         {  
  61.             min *= 3 / 2;  
  62.             if (frame_size == 960)  
  63.                 frame_size = 320;  
  64.         } else  
  65.         {  
  66.             if (frame_size == 960)  
  67.                 frame_size = 320;  
  68.             if (frame_size == 1024)  
  69.                 frame_size *= 2;  
  70.         }  
  71.           
  72.         //速率  
  73.         frame_rate = p_type.codec.samp_rate() / frame_size;  
  74.         long frame_period = 1000 / frame_rate;  
  75.         frame_rate *= 1.5;  
  76.           
  77.         //发送包缓冲区  
  78.         byte[] buffer = new byte[frame_size + 12];  //  
  79.           
  80.         //实例化包  
  81.         RtpPacket rtp_packet = new RtpPacket(buffer, 0);  
  82.           
  83.         //设置有效复合  
  84.         rtp_packet.setPayloadType(p_type.number);  
  85.           
  86.         //调式输出信息  
  87.         if (DEBUG)  
  88.             println("Reading blocks of (读取块大小)" + buffer.length + " bytes");  
  89.   
  90.         println("Sample rate (采样率) = " + p_type.codec.samp_rate());  
  91.         println("Buffer size(缓冲区大小) = " + min);  
  92.   
  93.         //录制  
  94.         AudioRecord record = null;  
  95.   
  96.         //获得声音内容变量  
  97.         short[] lin = new short[frame_size * (frame_rate + 1)];  
  98.         int num, ring = 0, pos;  
  99.           
  100.         //随机数  
  101.         random = new Random();  
  102.           
  103.         //输入流  
  104.         InputStream alerting = null;  
  105.         try  
  106.         {  
  107.             //接受  
  108.             alerting = Receiver.mContext.getAssets().open("alerting");  
  109.         } catch (IOException e2)  
  110.         {  
  111.             if (!Sipdroid.release)  
  112.                 e2.printStackTrace();  
  113.         }  
  114.           
  115.         //初始化音频编码  
  116.         p_type.codec.init();  
  117.           
  118.         if(Sipdroid.VoiceDebug)  
  119.         {  
  120.             Log.i("RtpStreamSender""音频发送开始啦");  
  121.         }  
  122.           
  123.         //循环发送  先录制 然后发送  
  124.         while (running)  
  125.         {  
  126.             //录制没有 或已经改变  
  127.             if (changed || record == null)  
  128.             {  
  129.                 if (record != null)  
  130.                 {  
  131.                     //释放操作  
  132.                     record.stop();  
  133.                     record.release();  
  134.                     if (RtpStreamReceiver.samsung)  
  135.                     {  
  136.                         AudioManager am = (AudioManager) Receiver.mContext  
  137.                                 .getSystemService(Context.AUDIO_SERVICE);  
  138.                         am.setMode(AudioManager.MODE_IN_CALL);  
  139.                         am.setMode(AudioManager.MODE_NORMAL);  
  140.                     }  
  141.                 }  
  142.                   
  143.                 //未改变  
  144.                 changed = false;  
  145.                   
  146.                 //实例化录制  
  147.                 record = new AudioRecord(MediaRecorder.AudioSource.MIC,  
  148.                         p_type.codec.samp_rate(),  
  149.                         AudioFormat.CHANNEL_CONFIGURATION_MONO,  
  150.                         AudioFormat.ENCODING_PCM_16BIT, min);  
  151.                   
  152.                 if(Sipdroid.VoiceDebug)  
  153.                 {  
  154.                     Log.i("RtpStreamSender""构造音频录制实例");  
  155.                 }  
  156.                   
  157.                 //得到录制类型  
  158.                 if (record.getState() != AudioRecord.STATE_INITIALIZED)  
  159.                 {  
  160.                     //拒接  
  161.                     Receiver.engine(Receiver.mContext).rejectcall();  
  162.                     record = null;  
  163.                     break;  
  164.                 }  
  165.                   
  166.                 //开始录制  
  167.                 record.startRecording();  
  168.                   
  169.                 //计算  
  170.                 micgain = (int) (Settings.getMicGain() * 10);  
  171.             }  
  172.               
  173.               
  174.             //静音或挂断 则停止  
  175.             if (muted || Receiver.call_state == UserAgent.UA_STATE_HOLD)  
  176.             {  
  177.                 //挂断  
  178.                 if (Receiver.call_state == UserAgent.UA_STATE_HOLD)  
  179.                 {  
  180.                     //恢复模式  
  181.                     RtpStreamReceiver.restoreMode();  
  182.                 }  
  183.                   
  184.                 if(Sipdroid.VoiceDebug)  
  185.                 {  
  186.                     Log.i("RtpStreamSender""挂断电话则停止录制");  
  187.                 }  
  188.                   
  189.                 //停止录音  
  190.                 record.stop();  
  191.               
  192.                 //延时  
  193.                 while (running  
  194.                         && (muted || Receiver.call_state == UserAgent.UA_STATE_HOLD))  
  195.                 {  
  196.                     try  
  197.                     {  
  198.                         sleep(1000);  
  199.                     } catch (InterruptedException e1)  
  200.                     {  
  201.                     }  
  202.                 }  
  203.                   
  204.                 //开始录制  
  205.                 record.startRecording();  
  206.             }  
  207.               
  208.             // DTMF change start  
  209.             if (dtmf.length() != 0)  
  210.             {  
  211.                 //构造包  
  212.                 byte[] dtmfbuf = new byte[dtframesize + 12];  
  213.                 RtpPacket dt_packet = new RtpPacket(dtmfbuf, 0);  
  214.                   
  215.                 //设置有效负荷  
  216.                 dt_packet.setPayloadType(dtmf_payload_type);  
  217.                   
  218.                 //设置大小  
  219.                 dt_packet.setPayloadLength(dtframesize);  
  220.                 dt_packet.setSscr(rtp_packet.getSscr());  
  221.                 long dttime = time;  
  222.                 int duration;  
  223.   
  224.                 for (int i = 0; i < 6; i++)  
  225.                 {  
  226.                     time += 160;  
  227.                     duration = (int) (time - dttime);  
  228.                     dt_packet.setSequenceNumber(seqn++);  
  229.                     dt_packet.setTimestamp(dttime);  
  230.                     dtmfbuf[12] = rtpEventMap.get(dtmf.charAt(0));  
  231.                     dtmfbuf[13] = (byte0x0a;  
  232.                     dtmfbuf[14] = (byte) (duration >> 8);  
  233.                     dtmfbuf[15] = (byte) duration;  
  234.                     try  
  235.                     {  
  236.                         //发送包 声音包  
  237.                         rtp_socket.send(dt_packet);  
  238.                           
  239.                         if (Sipdroid.VoiceDebug)  
  240.                         {  
  241.                             Log.i("RtpStreamSender",  
  242.                                     "第一次发送声音包  大小" + dt_packet.getLength()  
  243.                                             + " " + dt_packet.getPacket());  
  244.                         }  
  245.                           
  246.                         if(bShowVoiceDecodeData)  
  247.                         {  
  248. //                          mvoiceRtpFile.write(dt_packet.getPacket());  
  249.                         }  
  250.   
  251.                         sleep(20);  
  252.                     } catch (Exception e1)  
  253.                     {  
  254.                     }  
  255.                 }  
  256.                   
  257.                 //发送回音?  
  258.                 for (int i = 0; i < 3; i++)  
  259.                 {  
  260.                     duration = (int) (time - dttime);  
  261.                     dt_packet.setSequenceNumber(seqn);  
  262.                     dt_packet.setTimestamp(dttime);  
  263.                     dtmfbuf[12] = rtpEventMap.get(dtmf.charAt(0));  
  264.                     dtmfbuf[13] = (byte0x8a;  
  265.                     dtmfbuf[14] = (byte) (duration >> 8);  
  266.                     dtmfbuf[15] = (byte) duration;  
  267.                     try  
  268.                     {  
  269.                         //发送包  
  270.                         rtp_socket.send(dt_packet);  
  271.                           
  272.                         if (Sipdroid.VoiceDebug)  
  273.                         {  
  274.                             Log.i("RtpStreamSender",  
  275.                                     "第二次发送声音包  大小" + dt_packet.getLength()  
  276.                                             + " " + dt_packet.getPacket());  
  277.                         }  
  278.                           
  279.                         if(bShowVoiceDecodeData)  
  280.                         {  
  281. //                          mvoiceRtpFile.write(dt_packet.getPacket());  
  282.                         }  
  283.                     } catch (Exception e1)  
  284.                     {  
  285.                     }  
  286.                 }  
  287.                 time += 160;  
  288.                 seqn++;  
  289.                 dtmf = dtmf.substring(1);  
  290.             }  
  291.             // DTMF change end  
  292.   
  293.             if (frame_size < 480)  
  294.             {  
  295.                 now = System.currentTimeMillis();  
  296.                 next_tx_delay = frame_period - (now - last_tx_time);  
  297.                 last_tx_time = now;  
  298.                 if (next_tx_delay > 0)  
  299.                 {  
  300.                     try  
  301.                     {  
  302.                         sleep(next_tx_delay);  
  303.                     } catch (InterruptedException e1)  
  304.                     {  
  305.                     }  
  306.                     last_tx_time += next_tx_delay - sync_adj;  
  307.                 }  
  308.             }  
  309.               
  310.             //获得发送的位置  
  311.             pos = (ring + delay * frame_rate * frame_size)  
  312.                     % (frame_size * (frame_rate + 1));  
  313.               
  314.             //得到大小  
  315.             num = record.read(lin, pos, frame_size);  
  316.               
  317.             if (num <= 0)  
  318.                 continue;  
  319.               
  320.             //是否有效  
  321.             if (!p_type.codec.isValid())  
  322.                 continue;  
  323.   
  324.             // Call recording: Save the frame to the CallRecorder.  
  325.             //通话记录:框架保存的CallRecorder的。  新的录制   
  326.             if (call_recorder != null)  
  327.             {  
  328.                 //写入 输出  
  329.                 call_recorder.writeOutgoing(lin, pos, num);  
  330.             }  
  331.   
  332.             if (RtpStreamReceiver.speakermode == AudioManager.MODE_NORMAL)  
  333.             {  
  334.                 calc(lin, pos, num);  
  335.                 if (RtpStreamReceiver.nearend != 0  
  336.                         && RtpStreamReceiver.down_time == 0)  
  337.                 {  
  338.                     noise(lin, pos, num, p / 2);  
  339.                 }  
  340.                 else if (nearend == 0)  
  341.                 {  
  342.                     p = 0.9 * p + 0.1 * s;  
  343.                 }  
  344.             } else  
  345.             {  
  346.                 switch (micgain)  
  347.                 {  
  348.                 case 1:  
  349.                     calc1(lin, pos, num);  
  350.                     break;  
  351.                 case 2:  
  352.                     calc2(lin, pos, num);  
  353.                     break;  
  354.                 case 10:  
  355.                     calc10(lin, pos, num);  
  356.                     break;  
  357.                 }  
  358.             }  
  359.               
  360.             iCount++;  
  361.               
  362.             //通话中  
  363.             if (Receiver.call_state != UserAgent.UA_STATE_INCALL  
  364.                     && Receiver.call_state != UserAgent.UA_STATE_OUTGOING_CALL  
  365.                     && alerting != null)  
  366.             {  
  367.                 try  
  368.                 {  
  369.                     if (alerting.available() < num / mu)  
  370.                     {  
  371.                         alerting.reset();  
  372.                     }  
  373.                     alerting.read(buffer, 12, num / mu);  
  374.                 } catch (IOException e)  
  375.                 {  
  376.                     if (!Sipdroid.release)  
  377.                     {  
  378.                         e.printStackTrace();  
  379.                     }  
  380.                 }  
  381.                 if (p_type.codec.number() != 8)  
  382.                 {  
  383.                     G711.alaw2linear(buffer, lin, num, mu);  
  384.                     num = p_type.codec.encode(lin, 0, buffer, num);  
  385.                       
  386. //                  if(bShowVoiceDecodeData)  
  387. //                  {  
  388. //                      byte[] sdf = lin.toString().getBytes();  
  389. //                      mvoiceFile.write(sdf);  
  390. //                        
  391. //                      String string = "";  
  392. //                        
  393. //                      for(short i:buffer)  
  394. //                          string+=i;  
  395. //                                
  396. //                      Log.i("p_type.codec.encode",iCount +"编码后的数据(buffer):"+string);  
  397. //                  }  
  398.                 }  
  399.             } else  
  400.             {  
  401.                 num = p_type.codec.encode(lin, ring  
  402.                         % (frame_size * (frame_rate + 1)), buffer, num); //进行了编码  
  403.                   
  404. //              if(bShowVoiceDecodeData)  
  405. //              {  
  406. //                  mvoiceFile.write(buffer);  
  407. //                    
  408. //                  String string = "";  
  409. //                    
  410. //                  for(short i:buffer)  
  411. //                      string+=i;  
  412. //                            
  413. //                  Log.i("p_type.codec.encode",iCount +"编码后的数据(buffer):"+string);  
  414. //              }  
  415.             }  
  416.               
  417.               
  418.   
  419.             //大小  
  420.             ring += frame_size;  
  421.             rtp_packet.setSequenceNumber(seqn++);  
  422.             rtp_packet.setTimestamp(time);  
  423.             rtp_packet.setPayloadLength(num);  
  424.               
  425.             //记录时间  
  426.             now = SystemClock.elapsedRealtime();              
  427.               
  428.             if (RtpStreamReceiver.timeout == 0 || Receiver.on_wlan  
  429.                     || now - lastsent > 500)  
  430.             {  
  431.                 try  
  432.                 {  
  433.                     lastsent = now;  
  434.                     rtp_socket.send(rtp_packet);  
  435.                   
  436.                     if (m > 1  
  437.                             && (RtpStreamReceiver.timeout == 0 || Receiver.on_wlan))  
  438.                     {  
  439.                         for (int i = 1; i < m; i++)  
  440.                             rtp_socket.send(rtp_packet);  
  441.                     }  
  442.                       
  443.                     if (Sipdroid.VoiceDebug)  
  444.                     {  
  445.                         Log.i("RtpStreamSender",  
  446.                                 "第三次发送声音包  大小" + rtp_packet.getLength()  
  447.                                         + " " + rtp_packet.getPacket());  
  448.                     }  
  449.                       
  450. //                  if(bShowVoiceDecodeData)  
  451. //                  {  
  452. //                      mvoiceRtpFile.write(rtp_packet.getPacket());  
  453. //                  }  
  454.                 } catch (Exception e)  
  455.                 {  
  456.                 }  
  457.             }  
  458.               
  459.             //编码数  
  460.             if (p_type.codec.number() == 9)  
  461.             {  
  462.                 time += frame_size / 2;  
  463.             }  
  464.             else  
  465.             {  
  466.                 time += frame_size;  
  467.             }  
  468.               
  469.             if (RtpStreamReceiver.good != 0  
  470.                     && RtpStreamReceiver.loss2 / RtpStreamReceiver.good > 0.01)  
  471.             {  
  472.                 if (selectWifi && Receiver.on_wlan && now - lastscan > 10000)  
  473.                 {  
  474.                     wm.startScan();  
  475.                     lastscan = now;  
  476.                 }  
  477.                 if (improve  
  478.                         && delay == 0  
  479.                         && (p_type.codec.number() == 0  
  480.                                 || p_type.codec.number() == 8 || p_type.codec  
  481.                                 .number() == 9))  
  482.                 {  
  483.                     m = 2;  
  484.                 }  
  485.                 else  
  486.                 {  
  487.                     m = 1;  
  488.                 }  
  489.             } else  
  490.             {  
  491.                 m = 1;  
  492.             }  
  493.         }  
好了,进入下一个问题,它的视频又是怎么一回事?此处不谈它的视频编码,直接介绍涉及它的流程!~
涉及视频的类有:VideoCamera、VideoCameraNew、VideoCameraNew2、VideoPreview!~ 而是否使用视频,关键在CallScreen类,CallScreen类是通话的界面!~
如下是否使用视频的代码,该代码在CallScreen中!~
[java] view plaincopy
  1. //是否开启视频流程关键地方     必须是在 电话中并且端口等设置正确   
  2. if (Receiver.call_state == UserAgent.UA_STATE_INCALL  
  3.         && socket == null  
  4.         && Receiver.engine(mContext).getLocalVideo() != 0  
  5.         && Receiver.engine(mContext).getRemoteVideo() != 0  
  6.         && PreferenceManager  
  7.                 .getDefaultSharedPreferences(this)  
  8.                 .getString(org.sipdroid.sipua.ui.Settings.PREF_SERVER,  
  9.                         org.sipdroid.sipua.ui.Settings.DEFAULT_SERVER)  
  10.                 .equals(org.sipdroid.sipua.ui.Settings.DEFAULT_SERVER))  
  11. {  
  12.     (new Thread()  
  13.     {  
  14.         public void run()  
  15.         {                     
  16.             // 视频包 在线包  
  17.             RtpPacket keepalive = new RtpPacket(new byte[12], 0);  
  18.             RtpPacket videopacket = new RtpPacket(new byte[1000], 0);  
  19.   
  20.             try  
  21.             {  
  22.                 if (intent == null || rtp_socket == null)  
  23.                 {  
  24.                     // 新建一个套接字  
  25.                     rtp_socket = new RtpSocket(  
  26.   
  27.                     // 初始化套接字  
  28.                             socket = new SipdroidSocket(Receiver  
  29.                                     .engine(mContext).getLocalVideo()),  
  30.   
  31.                             // 设置网络地址  
  32.                             InetAddress.getByName(Receiver.engine(  
  33.                                     mContext).getRemoteAddr()),  
  34.   
  35.                             // 接受远程视频  
  36.                             Receiver.engine(mContext).getRemoteVideo());  
  37.                     sleep(3000);  
  38.                 } else  
  39.                 {  
  40.                     // 接受数据  
  41.                     socket = rtp_socket.getDatagramSocket();  
  42.                 }  
  43.                 // 接受数据  
  44.                 rtp_socket.getDatagramSocket().setSoTimeout(15000);  
  45.             } catch (Exception e)  
  46.             {  
  47.                 if (!Sipdroid.release)  
  48.                 {  
  49.                     e.printStackTrace();  
  50.                 }  
  51.                 return;  
  52.             }  
  53.   
  54.             // 设置有效载荷类型  
  55.             keepalive.setPayloadType(126);  
  56.   
  57.             try  
  58.             {  
  59.                 // 发送数据  
  60.                 rtp_socket.send(keepalive);  
  61.             } catch (Exception e1)  
  62.             {  
  63.                 return;  
  64.             }  
  65.             for (;;)  
  66.             {  
  67.                 try  
  68.                 {  
  69.                     // 循环接收数据  
  70.                     rtp_socket.receive(videopacket);  
  71.                 } catch (IOException e)  
  72.                 {  
  73.                     // 异常则断开  
  74.                     rtp_socket.getDatagramSocket().disconnect();  
  75.                     try  
  76.                     {  
  77.                         // 发送在线包  
  78.                         rtp_socket.send(keepalive);  
  79.                     } catch (IOException e1)  
  80.                     {  
  81.                         return;  
  82.                     }  
  83.                 }  
  84.   
  85.                 // 得到有效负荷长度  
  86.                 if (videopacket.getPayloadLength() > 200)  
  87.                 {  
  88.                     if (intent != null)  
  89.                     {  
  90.                         // 发送数据  
  91.                         intent.putExtra("justplay"true);  
  92.                         mHandler.sendEmptyMessage(0);  
  93.                     } else  
  94.                     {  
  95.                         // 否则播放  
  96.                         Intent i = new Intent(mContext,  
  97.                                 org.sipdroid.sipua.ui.VideoCamera.class);  
  98.                         i.putExtra("justplay"true);  
  99.                         startActivity(i);  
  100.                     }  
  101.                     return;  
  102.                 }  
  103.             }  
  104.         }  
  105.     }).start();  
  106. }  

之后就进入到涉及视频的类VideoCamera中了!~
它在模拟器上的端口为什么总是变化的?
抓包分析下,如图:
yate服务端第一次记录了我的rport为1849,从图中发现它是与服务器通信的端口!~服务器中也把它当做端口记录!~而SipUA客户端是使用了UDP套接字自定义了一个37850端口,这个端口一直到退出才改变,而yate服务端第一次记录了我的rport为2251,也就是说服务器记录的rport的端口是一直在发生改变的!~所以下次用户拨打对方,向服务器索取对方信息时出错,就有可能会直接挂掉!~
rport在Sip中的定义是rport方式主要是对sip信令中Via字头的扩展,不过同时也要求SIP Proxy支持该功能。NAT之后的sip client在发送请求的时候在via字头中添加rport字段,该消息经发出后路由到SIP Proxy,SIP Proxy通过检查消息的源地址和Via字段中的地址,得知该client处于NAT之后,并且基于已有的rport,将消息的真实地址即公网上的地址通过received和rport字段返回给client端,这样client就知道自己真实的公网地址,可以解决信令穿越的问题。
而有网友提出,使用Android模拟器通过路由器时端口会发生变化!~ 不知道这是不是真的!

它又是如何处理登陆超时以及通话出错的?
这就涉及到封装处理Sip协议的类了!~涉及的类不多,感兴趣的童鞋就自己研究了,我累了!~
有些问题需要讨论!~~欢迎高手指点!~

2013年9月6日星期五

Install OpenFire 3.8.1 on CentOS 6.4 x86_64

Install Centos 6.4 x86_64
  • Be sure to only do the “minimal” install — use the minimal ISO from here 
Login to the server as root
This installs the dependencies:
yum -y install wget java glibc.i686
Change to the /tmp directory
cd /tmp
Download the RPM from this page  -or- use this direct link  for the RPM.
wget http://download.igniterealtime.org/openfire/openfire-3.8.1-1.i386.rpm
Run and install the RPM
yum -y install ./openfire*rpm
Once the RPM has been installed, start the openfire service
service openfire start
Ensure that openfire will boot with your server
chkconfig –level 235 openfire on
For this instance, you’ll want to make sure iptables is not running, you can configure the iptables rules how you need them later.
service iptables stop
Install mysql server
yum -y install mysql-server
Start mysql server
/sbin/service mysqld start
Ensure mysql starts when the server does
/sbin/chkconfig mysqld on
Start the basic mysql secure install
/usr/bin/mysql_secure_installation
hit “enter” to give no password
generate and save a new root password
hit “enter” aka “Y” on the rest of the questions
Restart mysql server
/sbin/service mysqld restart
Login to mysql as root (use the new root password you just entered)
/usr/bin/mysql -u root -p
Create database
CREATE DATABASE openfire;
Create user (update *username* and *securepassword* below)
INSERT INTO mysql.user (User,Host,Password) VALUES(‘username’,’127.0.0.1′,PASSWORD(‘securepassword’));
Save your changes
FLUSH PRIVILEGES;
Give the user access to the database
GRANT ALL PRIVILEGES ON openfire.* to openfire@127.0.0.1;
Save your changes
FLUSH PRIVILEGES;
Exit mysql
exit
Navigate in your browser to
http://yourdomain.com:9090
Follow the steps, enter in the mysql information that you created when prompted for a database. You are now good to go! Don’t forget to update your iptables, learn more 

基于openfire+smack的Android、Web、PC开发(一)


前言

Java领域的即时通信的解决方案可以考虑openfire+spark+smack。当然也有其他的选择。
Openfire 是基于Jabber协议(XMPP)实现的即时通信服务器端版本,目前建议使用3.8.1版本,这个版本是当前最新的版本,而且网上可以找到下载的源代码。
即时通信客户端可使用spark2.6.3,这个版本是目前最新的release版本,经过测试发现上一版本在视频支持,msn网关支持上可能有问题,所以选择openfire3.8.1+spark2.6.3是最合适的选择。
Smack是即时通信客户端编程库,目前我已测通通过jsp使用smack的API向openfire注册用户发送消息,并且可以通过监听器获得此用户的应答消息。
通过smack向msn用户和QQ用户,gtalk用户发送消息应该也可以实现,关于向gtalk用户发送消息的例子较多。这个留待以后研究。至少目前可以通过程序向spark发消息了。对于局域网内部应用足以解决系统与用户交互的问题,甚至可以做自动应答机器人。 
Spark支持聊天,语音,视频,会议,文件收发,截屏,连接msn等功能。
同时在Android移动端和Web端也有相应的编程库支持,在后面的章节会为大家逐步更新介绍,接下来我们首先要对几个重点的技术环节做些必要的讲解。

1.什么是XMPP

XMPP (Extensible Messaging and Presence Protocol),简单的来讲,它就是一个发送接收处理消息的协议,但是这个协议发送的消息,既不是二进制的东东也不是字符串,而是XML。正是因为使用了XML作为消息传递的中介,Extensible 才谈的上。XMPP的前身是Jabber,一个开源形式组织产生的网络即时通信协议。XMPP目前被IETF国际标准组织完成了标准化工作。  

2.IM

Instant Messenger,及时通信软件,就是大家使用的QQ、MSN Messenger和Gtalk等等。其中Gtalk 就是基于XMPP 协议的一个实现,其他的则不是。当前IM 几乎作为每个上网者必然使用的工具,在国外的大型企业中有一些企业级的IM应用,但是其商业价值还没完全发挥出来。设想既然XMPP 协议是一个公开的协议,那么每个企业都可以利用它来开发适合本身企业工作,提高自身生产效率的IM;甚至,你还可以在网络游戏中集成这种通信软件,不但让你可以边游戏边聊天,也可以开发出适合游戏本身的IM 应用,比如说一些游戏关键场景提醒功能,团队语音交流等等都可以基于IM来实现。

3.Spark,smack和Openfire

开源界总是有许多有趣的东东,这三个合起来就是一个完整的XMPP IM 实现。包括服务器端——Openfire,客户端——Spark,XMPP 传输协议的实现——Smack(记住,XMPP是一个协议,协议是需要实现的,Smack起到的就是这样的一个作用)。三者都是基于Java 语言的实现。
Spark 提供了客户端一个基本的实现,并提出了一个很好的插件架构,这对于开发者来说不能不说是一个福音。我强烈建议基于插件方式来实现你新增加的功能,而不是去改它的源代码,这样有利于你项目架构,把原始项目的影响降到最低。
Openfire 是基于XMPP 协议的IM 的服务器端的一个实现,虽然当两个用户连接后,可以通过点对点的方式来发送消息,但是用户还是需要连接到服务器来获取一些连接信息和通信信息的,所以服务器端是必须要实现的。Openfire 也提供了一些基本功能,但真的很基本的!庆幸的是,它也提供插件的扩展,像Spark 一样,同样强烈建议使用插件扩展的方式来增加新的功能,而不是修改人家的源代码。
Smack 是一个XMPP 协议的Java 实现,提供一套可扩展的API,不过有些时候,你还是不得不使用自己定制发送的XML 文件内容的方式来实现自己的功能
下图展示了三者之间的关系:
Spark 提供了客户端一个基本的实现,并提出了一个很好的插件架构,这对于开发者来说不能不说是一个福音。我强烈建议基于插件方式来实现你新增加的功能,而不是去改它的源代码,这样有利于你项目架构,把原始项目的影响降到最低。
Openfire 是基于XMPP 协议的IM 的服务器端的一个实现,虽然当两个用户连接后,可以通过点对点的方式来发送消息,但是用户还是需要连接到服务器来获取一些连接信息和通信信息的,所以服务器端是必须要实现的。Openfire 也提供了一些基本功能,但真的很基本的!庆幸的是,它也提供插件的扩展,像Spark 一样。
Smack 是一个XMPP 协议的Java 实现,提供一套可扩展的API,不过有些时候,你还是不得不使用自己定制发送的XML 文件内容的方式来实现自己的功能。

4.安装Openfire3.8.1

使用openfire需要配置机器的域名。如果局域网内没有安装域服务器,则需要手工为机器配置域名,打开C:WINDOWSsystem32driversetchosts文件,增加一新行:
127.0.0.1 csdn.shimiso.com  (用户根据自己的需要可配置称别的名字,但最好符合带.的域名格式)
其他机器使用域名访问openfire,也需要在C:WINDOWSsystem32driversetchosts中指定bzwang.tzsoft.com对应的ip地址,假设安装openfire的主机IP为192.168.1.10,则hosts文件中应增加一新行:
192.168.1.10 csdn.shimiso.com
通过这种方式指定主机域名,建议安装openfire的机器配置静态ip地址以免ip发生改变。
语言选择简体中文,见下图:
后续按照提示安装,点击完成启动openfire,最后出现下面的界面:
    
点”Launch Admin”,出现下面的页面,页面连接为http://127.0.0.1:9090/setup/index.jsp
语种选择中文(简体),点”Continue”:(下面的页面如果出现英文,请刷新一下页面或者重新点Launch Admin)
这个页面的Domain默认为机器名,请改为一开始设置的域名,例如csdn.shimiso.com,接着点Continue(继续):
这个页面中第一个选项,即标准数据库连接,不要选择嵌入的数据库(Embedded Database,嵌入的数据库是hsqldb数据库,生产环境不要选择此选项),然后点”继续”:
这里我用的是mysql数据库,按官方要求最好是5.0以上,确保有个名字叫openfire的库存在,配好后点继续
这个页面是配置是否使用LDAP,我们现在不配置LDAP,所以选择 Default默认即可,点继续:
这里填写邮箱和确认密码后直接点继续:
 
等待大约30秒左右,到此安装和配置操作全部完成。打开http://127.0.0.1:9090/login.jsp登录管理页面
进入用户/组新增一个用户。

5.客户端配置和调试

这里我没用使用官方的spark客户端,而是用了潘迪安 和如意通 ,配置截图如下:
    
聊天演示:

从上面的聊天记录我们发现所有的用户id全称都是:用户名@域名/资源名,这个就是我们在XMPP协议中通常说说的JID,即jabber id,它是一个xmpp协议帐号系统的通称,后面我们在使用smack编程库调试接口时会经常用到这个参数。

6.使用Smack编程库进行代码功能调试

关于Smack编程库,前面我们提到,它是面向Java端的api,主要在PC上使用,利用它我们可以向openfire服务器注册用户,发送消息,并且可以通过监听器获得此用户的应答消息,以及构建聊天室,分组,个人通讯录等等。
下面我们写几个程序小例子测试一下。
(1)登录操作
  PPConnection.DEBUG_ENABLED = true;
  AccountManager accountManager;
  final ConnectionConfiguration connectionConfig = new ConnectionConfiguration(
    "192.168.1.78", Integer.parseInt("5222"), "csdn.shimiso.com");

  // 允许自动连接
  connectionConfig.setReconnectionAllowed(true);
  connectionConfig.setSendPresence(true);

  Connection connection = new XMPPConnection(connectionConfig);
  try {
   connection.connect();// 开启连接
   accountManager = connection.getAccountManager();// 获取账户管理类
  } catch (XMPPException e) {
   throw new IllegalStateException(e);
  }

  // 登录
  connection.login("admin", "admin","SmackTest");
  System.out.println(connection.getUser()); 
  connection.getChatManager().createChat("shimiso@csdn.shimiso.com",null).sendMessage("Hello word!");
运行结果:
在login中一共有三个参数,登录名,密码,资源名,可能有人不明白资源名到底是什么意思,其实就是客户端的来源,客户端的名称,如果不写它默认就叫smack,如果你用相同的账户不同的资源名和同一个人发三条消息,那将会弹出三个窗口,而不是一个窗口。
同时smack还为我们提供了非常好的调试工具Smack Debug,利用该工具我们可以准确的捕获详细的往返报文信息。
(2)下面我们继续写个聊天的例子:
  PPConnection.DEBUG_ENABLED = true;
  AccountManager accountManager;
  final ConnectionConfiguration connectionConfig = new ConnectionConfiguration(
    "192.168.1.78", Integer.parseInt("5222"), "csdn.shimiso.com");

  // 允许自动连接
  connectionConfig.setReconnectionAllowed(true);
  connectionConfig.setSendPresence(true);

  Connection connection = new XMPPConnection(connectionConfig);
  try {
   connection.connect();// 开启连接
   accountManager = connection.getAccountManager();// 获取账户管理类
  } catch (XMPPException e) {
   throw new IllegalStateException(e);
  }

  // 登录
  connection.login("admin", "admin","SmackTest3");  
  ChatManager chatmanager = connection.getChatManager();
  Chat newChat = chatmanager.createChat("shimiso@csdn.shimiso.com", new MessageListener() {
   public void processMessage(Chat chat, Message message) {
    if (message.getBody() != null) {
     System.out.println("Received from 【"
       + message.getFrom() + "】 message: "
       + message.getBody());
    }

   }
  });
  Scanner input = new Scanner(System.in);
  while (true) {
   String message = input.nextLine(); 
   newChat.sendMessage(message);
  }
运行结果:
这里我们用Scanner来捕捉用户在控制台的键盘操作,将信息发出,同时创建了一个MessageListener监听,在其中强制实现processMessage方法即可捕获发回的信息,在初次使用上还是较为容易上手的,我们只要细心查看API即可逐步深入下去。
(3)除了聊天以外我们经常还能想到就是广播,需要给所有在线的用户发送一个通知,或者给所有在线和离线的用户全发送,我们先演示如何给在线用户发送一个广播:
  PPConnection.DEBUG_ENABLED = false;
  AccountManager accountManager;
  final ConnectionConfiguration connectionConfig = new ConnectionConfiguration(
    "192.168.1.78", Integer.parseInt("5222"), "csdn.shimiso.com");

  // 允许自动连接
  connectionConfig.setReconnectionAllowed(true);
  connectionConfig.setSendPresence(true);

  Connection connection = new XMPPConnection(connectionConfig);
  try {
   connection.connect();// 开启连接
   accountManager = connection.getAccountManager();// 获取账户管理类
  } catch (XMPPException e) {
   throw new IllegalStateException(e);
  }
  connection.login("admin", "admin","SmackTest3"); 
  Message newmsg = new Message(); 
  newmsg.setTo("shimiso@csdn.shimiso.com");
  newmsg.setSubject("重要通知");
  newmsg.setBody("今天下午2点60分有会!");
  newmsg.setType(Message.Type.headline);// normal支持离线 
  connection.sendPacket(newmsg);
  connection.disconnect();
运行结果:
将参数设置为Message.Type.normal即可支持离线广播,openfire系统会自动判断该用户是否在线,如果在线就直接发送出去,如果不在线则将信息存入ofoffline表,现在我将shimiso用户退出登录,再给它发消息,我们可以进入openfire库的ofoffline表中,非常清楚看到里面躺着一条离线消息记录是发给shimiso这个用户的
(4)那么我们如何让shimiso这个用户一登陆就取到离线消息呢?请看如下代码
  PPConnection.DEBUG_ENABLED = false;
  AccountManager accountManager;
  final ConnectionConfiguration connectionConfig = new ConnectionConfiguration(
    "192.168.1.78", Integer.parseInt("5222"), "csdn.shimiso.com");

  // 允许自动连接
  connectionConfig.setReconnectionAllowed(true);
  connectionConfig.setSendPresence(false);//不要告诉服务器自己的状态
  Connection connection = new XMPPConnection(connectionConfig);
  try {
   connection.connect();// 开启连接
   accountManager = connection.getAccountManager();// 获取账户管理类
  } catch (XMPPException e) {
   throw new IllegalStateException(e);
  } 
  connection.login("shimiso", "123","SmackTest"); 
  OfflineMessageManager offlineManager = new OfflineMessageManager(
    connection);
  try {
   Iterator<org.jivesoftware.smack.packet.Message> it = offlineManager
     .getMessages();

   System.out.println(offlineManager.supportsFlexibleRetrieval());
   System.out.println("离线消息数量: " + offlineManager.getMessageCount());

   Map<String, ArrayList<Message>> offlineMsgs = new HashMap<String, ArrayList<Message>>();

   while (it.hasNext()) {
    org.jivesoftware.smack.packet.Message message = it.next();
    System.out
      .println("收到离线消息, Received from 【" + message.getFrom()
        + "】 message: " + message.getBody());
    String fromUser = message.getFrom().split("/")[0];

    if (offlineMsgs.containsKey(fromUser)) {
     offlineMsgs.get(fromUser).add(message);
    } else {
     ArrayList<Message> temp = new ArrayList<Message>();
     temp.add(message);
     offlineMsgs.put(fromUser, temp);
    }
   }

   // 在这里进行处理离线消息集合......
   Set<String> keys = offlineMsgs.keySet();
   Iterator<String> offIt = keys.iterator();
   while (offIt.hasNext()) {
    String key = offIt.next();
    ArrayList<Message> ms = offlineMsgs.get(key);

    for (int i = 0; i < ms.size(); i++) {
     System.out.println("-->" + ms.get(i));
    }
   }

   offlineManager.deleteMessages();
  } catch (Exception e) {
   e.printStackTrace();
  }
  offlineManager.deleteMessages();//删除所有离线消息
  Presence presence = new Presence(Presence.Type.available);
          nnection.sendPacket(presence);//上线了
          nnection.disconnect();//关闭连接
运行结果:
这里我们需要特别当心的是先不要告诉openfire服务器你上线了,否则永远也拿不到离线消息,用下面老外的话将就是在你上线之前去获取离线消息,这么设计是很有道理的。
The OfflineMessageManager helps manage offline messages even before the user has sent an available presence. When a user asks for his offline messages before sending an available presence then the server will not
send a flood with all the offline messages when the user becomes online. The server will not send a flood with all the offline messages to the session that made the offline messages request or to any other session used by the user that becomes online.
同时拿到离线消息以后删除离线消息offlineManager.deleteMessages();,同是通知服务器自己上线了。
(5)下面我们来看看如何来发送文件
  PPConnection.DEBUG_ENABLED = false;
  AccountManager accountManager;
  final ConnectionConfiguration connectionConfig = new ConnectionConfiguration(
    "192.168.1.78", Integer.parseInt("5222"), "csdn.shimiso.com");

  // 允许自动连接
  connectionConfig.setReconnectionAllowed(true);
  connectionConfig.setSendPresence(true);

  Connection connection = new XMPPConnection(connectionConfig);
  try {
   connection.connect();// 开启连接
   accountManager = connection.getAccountManager();// 获取账户管理类
  } catch (XMPPException e) {
   throw new IllegalStateException(e);
  }
    connection.login("admin", "admin","Rooyee"); 
    Presence pre = connection.getRoster().getPresence("shimiso@csdn.shimiso.com");
   System.out.println(pre);
   if (pre.getType() != Presence.Type.unavailable) {
    // 创建文件传输管理器
    FileTransferManager manager = new FileTransferManager(connection);
    // 创建输出的文件传输
    OutgoingFileTransfer transfer = manager
      .createOutgoingFileTransfer(pre.getFrom());
    // 发送文件
    transfer.sendFile(new File("E:\Chrysanthemum.jpg"), "图片");
    while (!transfer.isDone()) {
     if (transfer.getStatus() == FileTransfer.Status.in_progress) {
      // 可以调用transfer.getProgress();获得传输的进度 
      System.out.println(transfer.getStatus());
      System.out.println(transfer.getProgress());
      System.out.println(transfer.isDone());
     }
    }
   }
运行结果:
在这里我们需要特别注意的是,跨资源是无法发送文件的,看connection.login(“admin”, “admin”,”Rooyee”);这个代码就明白了,必须是“域名和资源名”完全相同的两个用户才可以互发文件,否则永远都没反应,如果不清楚自己所用的客户端的资源名,可以借助前面提到的SmackDebug工具查看往返信息完整报文,在to和from中一定可以看到。
如果我们自己要写文件接收例子的话,参考代码如下:
FileTransferManager transfer = new FileTransferManager(connection);
transfer.addFileTransferListener(new RecFileTransferListener());
public class RecFileTransferListener implements FileTransferListener {

 public String getFileType(String fileFullName) {
  if (fileFullName.contains(".")) {
   return "." + fileFullName.split("//.")[1];
  } else {
   return fileFullName;
  }

 }

 @Override
 public void fileTransferRequest(FileTransferRequest request) {
  System.out.println("接收文件开始.....");
  final IncomingFileTransfer inTransfer = request.accept();
  final String fileName = request.getFileName();
  long length = request.getFileSize();
  final String fromUser = request.getRequestor().split("/")[0];
  System.out.println("文件大小:" + length + "  " + request.getRequestor());
  System.out.println("" + request.getMimeType());
  try {

   JFileChooser chooser = new JFileChooser();
   chooser.setCurrentDirectory(new File("."));

   int result = chooser.showOpenDialog(null);

   if (result == JFileChooser.APPROVE_OPTION) {
    final File file = chooser.getSelectedFile();
    System.out.println(file.getAbsolutePath());
    new Thread() {
     public void run() {
      try {

       System.out.println("接受文件: " + fileName);
       inTransfer
         .recieveFile(new File(file
           .getAbsolutePath()
           + getFileType(fileName)));

       Message message = new Message();
       message.setFrom(fromUser);
       message.setProperty("REC_SIGN", "SUCCESS");
       message.setBody("[" + fromUser + "]发送文件: "
         + fileName + "/r/n" + "存储位置: "
         + file.getAbsolutePath()
         + getFileType(fileName));
       if (Client.isChatExist(fromUser)) {
        Client.getChatRoom(fromUser)
          .messageReceiveHandler(message);
       } else {
        ChatFrameThread cft = new ChatFrameThread(
          fromUser, message);
        cft.start();

       }
      } catch (Exception e2) {
       e2.printStackTrace();
      }
     }
    }.start();
   } else {

    System.out.println("拒绝接受文件: " + fileName);

    request.reject();
    Message message = new Message();
    message.setFrom(fromUser);
    message.setBody("拒绝" + fromUser + "发送文件: " + fileName);
    message.setProperty("REC_SIGN", "REJECT");
    if (Client.isChatExist(fromUser)) {
     Client.getChatRoom(fromUser).messageReceiveHandler(message);
    } else {
     ChatFrameThread cft = new ChatFrameThread(fromUser, message);
     cft.start();
    }
   }

   /*
    * InputStream in = inTransfer.recieveFile();
    * 
    * String fileName = "r"+inTransfer.getFileName();
    * 
    * OutputStream out = new FileOutputStream(new
    * File("d:/receive/"+fileName)); byte[] b = new byte[512];
    * while(in.read(b) != -1) { out.write(b); out.flush(); }
    * 
    * in.close(); out.close();
    */
  } catch (Exception e) {
   e.printStackTrace();
  }

  System.out.println("接收文件结束.....");

 }

}
(6)用户列表
 **
  * 返回所有组信息 <RosterGroup>
  * 
  * @return List(RosterGroup)
  */
 public static List<RosterGroup> getGroups(Roster roster) {
  List<RosterGroup> groupsList = new ArrayList<RosterGroup>();
  Collection<RosterGroup> rosterGroup = roster.getGroups();
  Iterator<RosterGroup> i = rosterGroup.iterator();
  while (i.hasNext())
   groupsList.add(i.next());
  return groupsList;
 }

 /**
  * 返回相应(groupName)组里的所有用户<RosterEntry>
  * 
  * @return List(RosterEntry)
  */
 public static List<RosterEntry> getEntriesByGroup(Roster roster,
   String groupName) {
  List<RosterEntry> EntriesList = new ArrayList<RosterEntry>();
  RosterGroup rosterGroup = roster.getGroup(groupName);
  Collection<RosterEntry> rosterEntry = rosterGroup.getEntries();
  Iterator<RosterEntry> i = rosterEntry.iterator();
  while (i.hasNext())
   EntriesList.add(i.next());
  return EntriesList;
 }

 /**
  * 返回所有用户信息 <RosterEntry>
  * 
  * @return List(RosterEntry)
  */
 public static List<RosterEntry> getAllEntries(Roster roster) {
  List<RosterEntry> EntriesList = new ArrayList<RosterEntry>();
  Collection<RosterEntry> rosterEntry = roster.getEntries();
  Iterator<RosterEntry> i = rosterEntry.iterator();
  while (i.hasNext())
   EntriesList.add(i.next());
  return EntriesList;
 }
(7)用户头像的获取,使用VCard,很强大,具体自己看API吧,可以看看VCard传回来XML的组成,含有很多信息的
 **
  * 获取用户的vcard信息
  * @param connection
  * @param user
  * @return
  * @throws XMPPException
  */
 public static VCard getUserVCard(XMPPConnection connection, String user) throws XMPPException
 {
  VCard vcard = new VCard();
  vcard.load(connection, user);
  
  return vcard;
 }

 /**
  * 获取用户头像信息
  */
 public static ImageIcon getUserImage(XMPPConnection connection, String user) {
  ImageIcon ic = null;
  try {
   System.out.println("获取用户头像信息: "+user);
   VCard vcard = new VCard();
   vcard.load(connection, user);
   
   if(vcard == null || vcard.getAvatar() == null)
   {
    return null;
   }
   ByteArrayInputStream bais = new ByteArrayInputStream(
     vcard.getAvatar());
   Image image = ImageIO.read(bais);
 
   
      ic = new ImageIcon(image);
   System.out.println("图片大小:"+ic.getIconHeight()+" "+ic.getIconWidth());
  
  } catch (Exception e) {
   e.printStackTrace();
  }
  return ic;
 }
(8)组操作和用户分组操作
 **
  * 添加一个组
  */
 public static boolean addGroup(Roster roster,String groupName)
 {
  try {
   roster.createGroup(groupName);
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   return false;
  }
 }
 
 /**
  * 删除一个组
  */
 public static boolean removeGroup(Roster roster,String groupName)
 {
  return false;
 }
 
 /**
  * 添加一个好友  无分组
  */
 public static boolean addUser(Roster roster,String userName,String name)
 {
  try {
   roster.createEntry(userName, name, null);
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   return false;
  }
 }
 /**
  * 添加一个好友到分组
  * @param roster
  * @param userName
  * @param name
  * @return
  */
 public static boolean addUser(Roster roster,String userName,String name,String groupName)
 {
  try {
   roster.createEntry(userName, name,new String[]{ groupName});
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   return false;
  }
 }
 
 /**
  * 删除一个好友
  * @param roster
  * @param userName
  * @return
  */
 public static boolean removeUser(Roster roster,String userName)
 {
  try {
   
   if(userName.contains("@"))
   {
    userName = userName.split("@")[0];
   }
   RosterEntry entry = roster.getEntry(userName);
   System.out.println("删除好友:"+userName);
   System.out.println("User: "+(roster.getEntry(userName) == null));
   roster.removeEntry(entry);
   
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   return false;
  }
  
 }
(9)用户查询
public static List<UserBean> searchUsers(XMPPConnection connection,String serverDomain,String userName) throws XMPPException
 {
  List<UserBean> results = new ArrayList<UserBean>();
  System.out.println("查询开始..............."+connection.getHost()+connection.getServiceName());
  
  UserSearchManager usm = new UserSearchManager(connection);
  
  
        Form searchForm = usm.getSearchForm(serverDomain);
        Form answerForm = searchForm.createAnswerForm();
        answerForm.setAnswer("Username", true);
        answerForm.setAnswer("search", userName);
        ReportedData data = usm.getSearchResults(answerForm, serverDomain);
   
   Iterator<Row> it = data.getRows();
   Row row = null;
   UserBean user = null;
   while(it.hasNext())
   {
    user = new UserBean();
    row = it.next();
    user.setUserName(row.getValues("Username").next().toString());
    user.setName(row.getValues("Name").next().toString());
    user.setEmail(row.getValues("Email").next().toString());
    System.out.println(row.getValues("Username").next());
    System.out.println(row.getValues("Name").next());
    System.out.println(row.getValues("Email").next());
    results.add(user);
    //若存在,则有返回,UserName一定非空,其他两个若是有设,一定非空
   }
   
   return results;
 }
(10)修改自身状态,包括上线,隐身,对某人隐身,对某人上线
 ublic static void updateStateToAvailable(XMPPConnection connection)
 {
  Presence presence = new Presence(Presence.Type.available);
          nnection.sendPacket(presence);
      
 
 public static void updateStateToUnAvailable(XMPPConnection connection)
 {
  Presence presence = new Presence(Presence.Type.unavailable);
          nnection.sendPacket(presence);
     }
 
 public static void updateStateToUnAvailableToSomeone(XMPPConnection connection,String userName)
 {
  Presence presence = new Presence(Presence.Type.unavailable);
  presence.setTo(userName);
          nnection.sendPacket(presence);
 }
 public static void updateStateToAvailableToSomeone(XMPPConnection connection,String userName)
 {
  Presence presence = new Presence(Presence.Type.available);
  presence.setTo(userName);
          nnection.sendPacket(presence);

 }
(11)心情修改
 **
  * 修改心情
  * @param connection
  * @param status
  */
 public static void changeStateMessage(XMPPConnection connection,String status)
 {
  Presence presence = new Presence(Presence.Type.available);
  presence.setStatus(status);
  connection.sendPacket(presence);
 
 }
(12)修改用户头像,有点麻烦,主要是读入图片文件,编码,传输之
public static void changeImage(XMPPConnection connection,File f) throws XMPPException, IOException{
 
  VCard vcard = new VCard();
  vcard.load(connection);
  
         byte[] bytes;
       
             bytes = getFileBytes(f);
             String encodedImage = StringUtils.encodeBase64(bytes);
             vcard.setAvatar(bytes, encodedImage);
             vcard.setEncodedImage(encodedImage);
             vcard.setField("PHOTO", "<TYPE>image/jpg</TYPE><BINVAL>"
                     + encodedImage + "</BINVAL>", true);
             
             
             ByteArrayInputStream bais = new ByteArrayInputStream(
      vcard.getAvatar());
    Image image = ImageIO.read(bais);
    ImageIcon ic = new ImageIcon(image);
     
        
       
         vcard.save(connection);
        
 }
 
      private static byte[] getFileBytes(File file) throws IOException {
          BufferedInputStream bis = null;
         try {
            bis = new BufferedInputStream(new FileInputStream(file));
            int bytes = (int) file.length();
            byte[] buffer = new byte[bytes];
            int readBytes = bis.read(buffer);
            if (readBytes != buffer.length) {
                throw new IOException("Entire file not read");
            }
            return buffer;
        } finally {
            if (bis != null) {
                bis.close();
            }
        }
}
(13)用户状态的监听,即对方改变头像,状态,心情时,更新自己用户列表,其实这里已经有smack实现的监听器
  nal Roster roster = Client.getRoster();
  
  roster.addRosterListener(
   new RosterListener() {

     @Override
     public void entriesAdded(Collection<String> arg0) {
      // TODO Auto-generated method stub
      System.out.println("--------EE:"+"entriesAdded");
     }

     @Override
     public void entriesDeleted(Collection<String> arg0) {
      // TODO Auto-generated method stub
      System.out.println("--------EE:"+"entriesDeleted");
     }

     @Override
     public void entriesUpdated(Collection<String> arg0) {
      // TODO Auto-generated method stub
      System.out.println("--------EE:"+"entriesUpdated");
     }

     @Override
     public void presenceChanged(Presence arg0) {
      // TODO Auto-generated method stub
      System.out.println("--------EE:"+"presenceChanged");
     }   
     
  });
   



至此,有关openfire和smack的安装部署使用基本代码调试讲解完毕,本集源码容我稍作整理后再更新出来,请大家保持关注,同时下面会我们进入课程的重点环节,包括
如何在Openfire服务器上开发一款Android端的聊天软件。。。
如何在openfire上二次开发,如何在在web和android上接入IM模块。。。
如何在Spark基础二次开发基于java swing的PC客户端。。。
结合我之前讲的Android端服务器推送技术原理分析及XMPP简单的使用 由浅入深的对apache
MINA,XMPP协议,以及socket底层等一些难点进行讨论剖析。。。
Openfire作为一款优秀的XMPP开源服务器,整合及时通讯,视频,语音,会议室等多种服务,如何更好的让它集成到现有的系统中去才是我们研究的最终价值,否则一切都是空谈,本教程结合实战和理论,为大家提供提供一个共同研究和学习的平台,欢迎有识之士加入我们的技术交流群:173711587,共同探讨Java开发那些事儿
放些图以飨读者。。。

2013年9月5日星期四

党政一把手的“较量”

最近因为一场著名的审判,很多人在讨论官员之间的角色关系——这太复杂了,其间强弱,是公开规则、个人能力和当地官场既有力量的综合体现,很难简单概括。这篇专栏,也只能大概说说党政一把手之间的关系,算是给大家一个粗浅感觉。

地方党政一把手间的平衡,是政治正常运转的核心问题之一。
  在中国,地方党政一把手之间的平衡,是政治正常运转的核心问题之一,两人的“和”与“不和”,直接影响当地政坛生态。按规定,党政一把手之间——比如市委书记和市长——各自有不同的分工:说简单点,就是市委书记统领党的部门,主管干部调配,市长统领政府部门,负责经济发展——一个“管人”一个“管钱”,各有侧重。同时市长作为市委副书记,协助市委书记工作。
  但政治从来都是一个复杂的均衡,由此挑选合适的党政一把手“搭班子”,才会成为上级组织部门的重要任务,费尽心思。
  在县乡这个层级,党委书记往往处于强势的地位。因为一个县事务不多,党政事务也难分清;基层干部人数有限,如果县委书记在该地提拔过一轮下属官员,政治威望就会尤其牢固——此时县委书记若“自觉”还好,否则县长很容易被架空,导致县委书记说话是“规矩”,县长说话只算“意见”。
  此情况典型的是河南上蔡县原县委书记杨松泉,在他任县委书记5年中,两任县长都是任期未满就被他排挤离任。一次杨松泉主持召开县委常委会,县长讲了一些与其不吻合的意见,结果杨在会上直接厉声呵斥说:“你这县长才当几天,就想当家。”
  而后续接任的县长,杨同样施以颜色,常常在大会上公开否定县长的意见,弄得全场哄堂大笑。媒体报道说,县长下属的各局长向他汇报工作,县长会先问“书记是否知道”,若回应不知,立即面带惧色。
  实际上,县委书记的强势几乎已是公认,由此防控县委书记的“霸权”,甚至成为一个官方动作,一些省份就专门有《县委书记岗位风险预警防控办法》这样的规定。
  而到了市这一级,市委书记和市长之间的关系,就会更趋平衡。首先党政之间可以各自有更清晰的分工界限;而且到了地市这一级的官员,谁若过分“出格”,对方与省里甚至中央都有交流渠道,难有“霸权”存在。由此若碰上强势市长,面对市委书记的干涉,往往会以党政之间公开的分工规则和法律赋予政府首长的权力为依据,进行回击。
  而少数市长和市委书记之间的“交恶”,有时也更为明显。比如此前安徽阜阳市委书记王怀忠和市长肖作新之间的矛盾,几乎公开化。后来肖作新被查期间,王怀忠甚至还曾想托人到上面活动,以便“从重从快”处理。
  更甚者,此前的甘肃兰州市市委书记和市长在一次出租车罢工事件中发生冲突,市委书记在随后的人大常委会上明确表示不再支持市长的工作,由此导致该市长落选——这位被调职的市长在两年后直接向中纪委举报了市委书记,最终两人双双受审。
  情况稍有不同的是省会城市,一般省会城市的市委书记都会是省委常委,是副省级官员了,而且作为省委常委,可以直接影响市长的调动,比身为正地市级的市长权责上自然更有优势。但因为省会城市就在省委、省政府一把手眼皮底下,各方会更遵守规则。
  至于到了省这一级,省委书记和省长之间,则少有明显不和。这种级别的地方大员,由中央统一调配——组织部门肯定是通盘考虑性格、资历,基本不会让党政不和的事情出现。而且到此位置的官员,政治经验足够,相处分寸也把握熟稔。
  至于中央部委的部长和党组书记之间,则几乎不存在党政权责不清导致的“不和”——按照规定,各个部委实行部长负责制,而且大多数部委,党组书记也往往是由部长兼任。
  自己跟自己,是不会闹矛盾的。

2013年9月4日星期三

网络小贷生存模式浅析:阿里金融拍拍贷融360有利网


  投资界新芽&投资圈9月2日编者按:本文为风险投资公司专业投资人B座12楼(微信号:B1-12F)授权投资界新芽&投资圈发布。文章认为,网络小贷要解决的问题是,如何找到借款人,如何找到出租人,并将他们对接到一起。而网络小贷比传统小贷公司的优势就是网上实现信息的汇聚,实现不见面的对接。这其中就产生了难点,如何解决信任的问题。
网络小贷生存模式浅析:阿里金融拍拍贷融360有利网
  以下是全文:
  最近经常需要一些朋友问我对互联网金融的看法,这个命题太大。而且这是一个比较专业的行业,不敢随意喷观点,会被踩的半死。当然不是怕踩,主要是自己没有一个完整体系的观点。前段时间因为调研项目,研究了一下在互联网上做小贷的模式。将了解的情况整理给大家做参考,专业人士就快速拉过吧。
  市场多大,值得玩否?
  从大的市场环境来说,中国有4800万的小微企业,占企业总数的99.8%,而能得到贷款的只有30万,供求关系可见一斑——这一点实际也能解释为何各类 P2P 类借贷网站拔地而起。
  银行不愿意发展此类业务,主要是由于大部分中小企业的信贷在100万以下,而小微企业的信贷更是在几千元左右,贷款金额太低,且信贷次数较为频繁,信誉度不良,管理成本太高。
  另一方面,近些年个人贷款,不管是住房贷款还是汽车贷款,甚至是消费贷款也越来越普遍。这让网络小贷的想像空间越来越大,不断有创业者扎进来。
  阿里金额,引爆市场
  2010年6月,浙江阿里巴巴小额贷款公司成立,注册资本6亿元,获得国内首张电子商务领域的小额贷款公司营业执照。2011年,重庆市阿里巴巴小额贷款有限公司成立,注册资本10亿元。阿里金融正式诞生。
  阿里金额目前的主要产品: 淘宝小贷,针对B2C平台,为淘宝和天猫客户提供贷款,包括订单贷款和信用贷款。 阿里小贷,针对B2B平台,为阿里巴巴上的企业客户提供信用贷款。一种为循环贷:获取一定额度作为备用金,不取用不收利息,随借随还。二是固定贷:贷款额度在获贷后一次性发放。
  去年阿里金融的放贷额在 300 个亿左右,这个数据拿到一家普通公司已经是一个漂亮的成绩单了。但全国金融机构的业务放贷额则达到 60 万亿,光是小贷公司产生的放贷额就达到 6000 亿。如果以后网络小货是一种必然趋势,那么市场空间还足够容纳更多的网络小贷公司,阿里金额为大家敲开了一个缝。
  P2P:拍拍贷
  所谓 P2P,就是个人对个人;资金需求方可以发出借款需求,称之为“标”;资金供给的个人,以自己的风险判断,对“标”进行“投标”,最小投标金额为 50 元;最终以组合的方式,提供给借入者资金。没有在指定时间内凑够钱就是“流标”。成交的利息由双方决定。如拍拍贷这样的平台,提供给资金需求方和供给方进行“发标、投标和交易流程、沟通,以及贷后的催收”等平台服务。
  P2P在国外非常流行,主要有ZOPA、Prosper、Lending Club等。其中Lending Club公司刚获得一轮1.25 亿美元的融资,估值达15.5亿美元。在此轮融资之前,Lending Club 就已经获得过近 1 亿美元的融资。到目前为止,Lending Club 处理的贷款总额高达 16.5 亿美元,公司已连续 22 个季度实现正回报值。公司今年的目标是完成 20 亿美元的个人贷款额。
  但国内的社会信用体系与国外的根本就没有可比性,所以国内P2P发展都不太好,其中比较有代表性的是拍拍贷。拍拍贷成立于 2007 年 8 月,总部位于上海,是中国首家 P2P(个人对个人)小额无担保网络借贷平台。2012年11月传出获红杉资本 2500 万美元投资(拍拍贷 CEO 张俊称实际大大低于 2500 万美元,但不愿透露具体金额)。
  拍拍贷经营现状:目前尚未盈利。2012年总成交额1亿9千万元,年度总成交笔数19729。2012年之前总成交额1亿元。截至2012年累计借款人8992,累计投资人15609人,主要额度集中在10万以下。2012年借款逾期情况:逾期总人数238人,逾期总额112万,逾期率0.58%。
  拍拍贷数据真实性我不知道,但我对国内现有信用体系下的P2P不是很看好。阿里金额的核心是数据,我对网络小贷的看法是:没有完善的信用体系,就一定要有第三方担保或可靠数据来补充支撑。下面介绍两个我认为相对靠谱的模式。
  融360:说出你的需求,让信贷员来找你
  融360的业务模式:其业务主要为用户提供各种银行和出借机构贷款产品的搜索和比较服务。贷款者只需在网站上填写一张关于贷款金额、年限、收入等相关信息。融360根据用户提出的贷款需求,迅速匹配出最划算的贷款产品。在选定相应贷款后,市民将获得该贷款产品的信贷经理联系方式,开始申请该贷款产品。融360不参与双方洽谈、交易环节,只为双方解决贷款信息。
  融360的营利模式:一是把需要贷款的用户引导到信贷机构,比如银行,以及P2P公司等,收取50-100元/个,占总营收80%。二是在用户申请贷款过程中,融360 帮助用户完成整个贷款流程。贷款获批后,融360 收取贷款额的百分比作为返佣。目前,来自这部分的营收在整体营收中占比在15%左右。
  发展过程及数据:2011年10月成立的贷款推荐搜索引擎公司,2011年4季度做产品开发。2012年1季度网站上线,并很快在当年3月拿到A轮700万美元融资,由光速创投、KPCB、清科创投三家著名VC投资。 之后,融360一边建立商圈,跟银行等各大金融机构建立合作关系,一边准备大数据后端平台和移动端,流量慢慢从每天过万到每天过10万。2013年7月现在流量已超千万。再次获得 3000 万美元 B 轮投资。对于融360这个不到150人的企业来讲,近2亿人民币的融资的确不是个小数目。
  它对下游贷款客户的价值是:其一,让他们在纷繁复杂的金融产品中找到最符合自己要求的产品;其二,由于融360是向银行等金融机构收钱(按每给银行推荐一个客户收取营销服务费50-100元不等,以及收取成交后银行给的佣金),因此可以客观地比较产品。中国有1000多家银行、7000家小贷公司、近万家典当行以及20000多家担保公司,这些合法的信贷机构背后有着数十万甚至上百万的信贷员,融360的市场空间还有很大。
  有利网:线下汇聚借款人,线上汇聚出借人。
  有利网的商业模式是在线上汇聚出借人,再将其与小额贷款公司那里无法满足的借款人对接起来。
  借款来源:与小额贷款公司合作,由他们输送借款人。有利网的合作伙伴是拥有国务院颁发金融牌照的小额贷款机构,利用遍及全国的信贷人员开发、审核、担保在有利网上发布的借款需求。每笔借款均需要通过有利网及合作的小额贷款金融机构的5重审核,需提交15种材料,据称通过率仅为3%。
  互联网卖产品:投资人先通过网站查看各类贷款项目,然后直接把钱投给靠谱的借款方,再接着,就可以根据贷款的期限每月领取部分本息。投资人的每笔出借资金均可获得由合作小额贷款金融机构提供的不可撤销的100%本息担保。一旦投资人的出借资金出现借款人逾期的情况,有利网将在担保履约完成前先行赔付给投资人,不让投资人承受任何的损失。在这个过程中,有利网挣的是平台费,按每笔交易债权价格的1%收取手续费。
  小额贷款公司利益:小额贷款公司以前用自己的钱放贷,赚取利差。现在则输送借款人,并承担担保责任,赚取服务费。即有利的推荐费,借款方提供的固定中介费,以及基于信用等级的担保费。这个风险的 buffer 在 7% 到 8% 左右,应该可以抵御因坏账而需偿付本金的风险。
  经营现状:2013年02月25日有利网第一笔100%担保的理财项目上线,2013年03月28日上线一个月,有利网就累计成交金额达到1000万。2013年08月20日有利网累计成交金额达到1亿元。
  有利网商业模式的核心是所有的借款人都是线下审核过,并由有利网提供担保赔付,这就解决了借款过程中的信问题。当然有利网将担保转嫁到线下小额贷款公司身上,但对出租人是有保障的,数据证明这种众筹借款的方式是有效的。
  有没有机会了?
  网络小贷要解决的问题是,如何找到借款人,如何找到出租人,并将他们对接到一起。而网络小贷比传统小贷公司的优势就是网上实现信息的汇聚,实现不见面的对接。这其中就产生了难点,如何解决信任的问题。如前文中提及,在信用体系不完整的情况下,只有靠第三方担保、或可信数据作为支撑。融360和有利网,都通过自己的方案解决这个问题。

2013年9月3日星期二

“过桥贷”乱象:银行员工个人账户一年资金进出上亿



  前几年,一些民间资金掮客打着为中小企业融资的幌子,在各地纷纷建立民间融资性担保公司。因其大量违规经营,涉险高利贷业务,它们往往成为引发地方金融风险的“深水炸弹”。

  经过一轮轮风险事件洗礼后,跑路的跑路,进去的进去,此类不正规的小担保公司死得差不多
相关公司股票走势

建设银行4.29+0.051.18%
了。如今,资金掮客们卷土重来,盯上了已同银行建立贷款关系的中小企业,做起了“调头”、“过桥”融资的生意。据记者观察,这种灰色金融明显带有影子银行特征,其风险不亚于当年担保业乱象。

  “过桥费”奇贵

  “过桥贷”是指借款人在自有资金不足的情况下,通过民间的小贷公司、投资咨询类公司直接或通过民间融资方式筹措资金归还银行到期贷款,待重新取得银行贷款后再偿还这部分融资的一种手法。

  对苏南小微企业相对集中的某地的一家农商行进行贷后调查显示,50户有“过桥贷”的小微企业中,用此方式归还银行贷款后,再进行续贷的企业户数占全部贷款企业的70%。据对一年间的资金情况流动情况分析,这50户企业在各银行业金融机构累计贷款110多次,约7亿元,其中,以“过桥贷”的方式归还贷款超过4.84亿元,占银行累计贷款金额近七成。

  企业为按期归还银行贷款,尽快获得续贷资金,在自有资金不足的情况下,主要通过向非银行业金融机构、民间融资等第三方短期性融资还款,以防资金链断裂。“过桥贷”已成为中小企业为了维持良好信用记录和资金正常运转所采取的一种常见融资方式。

  调查显示,从50户企业银行贷款还款资金来源情况看,企业自有资金2.15亿元,占银行累计贷款的比例为30.72%;通过非银行业金融机构,如小额贷款公司、投资类公司等获得还贷资金5700万元,占银行累计贷款的比例为8.15%;通过民间融资方式获得还贷资金4.27亿元,占银行累计贷款的比例为61.13%。民间融资已成为企业“过桥贷”资金的主要来源。

  由于乞灵于“过桥贷”的企业多数急于获得短期融资,因此其成本极高。

  苏南一名做汽配生意的小老板向本报表示,他所借的“过桥贷”,100万元本金,十天利息20000元。

  来自相关部门的调查显示,这50户企业“过桥贷”平均融资成本年利率为45.58%,而某农商行一年期间加权平均利率为9.5774%。

  “过桥贷款”的巨额融资成本往往成为企业经营发展的巨大“包袱”,直接导致企业负债大幅攀升、偿债能力下降。

  某服装企业反映,它在银行年均贷款余额1.4亿元,全年支付利息1089万元,平均融资成本7.6%,而该企业为了“调头”,这一年多来通过民间融资25笔,共计2.3亿元,日均占用资金1000万元,全年支付融资利息460万元,平均融资成本50%,是银行融资成本的6.58倍。做什么生意能有这么高的利润?企业当家人说,高额的融资成本像个绞索,对企业来说是“花钱买口气”,苟延残喘。“过桥”,一不小心就成了奈何桥。

  信贷员的“灰色业务”

  由于“过桥”的紧迫性往往来自银行续贷到位前的“空白期”,撮合“过桥贷”往往成了银行信贷员的一项“灰色业务”。

  上述汽配小老板的经历很典型。由于账期因素,每当贷款到期前的一两个月,他就算有单子也不敢接。

  “做小本生意的,有一两个月账期是正常的—哪能卡得那么准,赶在还款前,就能把货款收回呢?所以,要把钱提前备足放在账上,等着银行扣。”

  即使还了款,还是有麻烦。为了发放后续贷款,银行还要重走审批流程,要十天半个月才能批下来。如果碰上额度紧,有时甚至要个把月才能批下来。这时生意还要做,急需要钱的时候,就只能找资金“调头寸”。

  据他回忆,有时,银行信贷员会有意无意地告诉你,哪家公司能提供这种短期“过桥”的钱。上述他所借的十天20000元利息的“过桥贷”,就是某信贷员介绍的。

  “信贷员答应贷款十天下来的。如果不不来,或者有意拖个几天,我就惨了。一天两千块啊。”他说。

  当地一家金融机构负责人对此评论称:“都是些短平快的生意,多发生在银行信贷人员、民间资金掮客与小企业主之间,银行工作人员利用信息不匹配进行拉郎配。”

  也正因为存在灰色地带,“过桥业务”极易诱发银行员工道德风险。无锡某大型银行在对员工与客户资金非正常往来进行飞行检查时惊愕地发现,员工与一些社会融资机构和客户存在大额非正常资金往来,其中有一银行员工在一年左右的时间,其名下的个人账户与企业间的资金往来超过了1亿元。

  调查发现,有部分员工利用与客户的良好关系,在客户资金紧张时,利用自己在银行工作的高信用度,发动“小圈子”里的朋友筹集资金代客户垫付就急。客户困难时他出手帮助,而每到季节性的考核时,客户也投桃报李,帮助拆入资金,为了便于考核,这些资金往往直接进入其个人账户。有的银行员工利用法律对个人账户查询手续复杂的限制,配合企业逃避资金流向的监控。也有的银行信贷员利用自身掌握的借贷需求信息,为民间借贷双方“牵线搭桥”,每天坐在电脑面前进行“信息配对”,从中赚取“提成”。更有甚者不惜以身涉险,赤膊上阵直接放贷。江苏丹阳就发生过一起建设银行分理处负责人充当资金掮客,从民间筹措资金后以个人名义直接与企业进行借贷交易赚快钱的案子。结果资金链断裂后远遁他方,至今音信全无。大型国有银行管理人员长时间在灰色地带游走,而上级行浑然不觉,这在一定程度上也折射出内控管理制度的失范、失察。

  (编辑:朱逸)

  作者:陈志龙来源第一财经日报)