posts - 297,  comments - 1618,  trackbacks - 0
               转载请注明出处:http://www.blogjava.net/amigoxie/archive/2009/08/03/289620.html  
一. 参考资料
      

        1. 《RTSP简单命令》:http://blog.csdn.net/feidragon319/archive/2007/08/14/1742357.aspx 
        2. http://bbs.21eic.com/dispbbs.asp?boardid=15&Id=22948
        3. 《RTSP客户端的Java实现》:http://hi.baidu.com/ssyuan/blog/item/566df6defac1dc5094ee37eb.html

二. RTSP的常用命令与解释
       其中C是客户端,S是服务端。
2.1  OPTIONS
       C->S:       OPTION request //询问S有哪些方法可用
       S->C:       OPTION response //S回应信息中包括提供的所有可用方法
      使用举例:
      客户端到服务端: 
OPTIONS rtsp://218.207.101.236:554/mobile/3/67A451E937422331 RTSP/1.0
Cseq: 1
     服务端对OPTIONS的回应:
RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
1
Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD

2.2  DESCRIBE

      C->S:      DESCRIBE request //要求得到S提供的媒体初始化描述信息
      S->C:      DESCRIBE response //S回应媒体初始化描述信息,主要是sdp

     使用举例:
     客户端到服务端:

DESCRIBE rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp RTSP/1.0
Cseq: 2

      服务端对OPTIONS的回应:

RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
2
Content
-length: 421
Date: Mon, 
03 Aug 2009 08:21:33 GMT
Expires: Mon, 
03 Aug 2009 08:21:33 GMT
Content
-Type: application/sdp
x
-Accept-Retransmit: our-retransmit
x
-Accept-Dynamic-Rate: 1
Content
-Base: rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/

v
=0
o
=MediaBox 127992 137813 IN IP4 0.0.0.0
s
=RTSP Session
i
=Starv Box Live Cast
c
=IN IP4 218.207.101.236
t
=0 0
a
=range:npt=now-
a
=control:*
m
=video 0 RTP/AVP 96
b
=AS:20
a
=rtpmap:96 MP4V-ES/1000
a
=fmtp:96 profile-level-id=8; config=000001b008000001b5090000010000000120008440fa282c2090a31f; decode_buf=12586
a
=range:npt=now-
a
=framerate:5
a
=framesize:96 176-144
a
=cliprect:0,0,144,176
a
=control:trackID=1

2.3  SETUP
        C->S:        SETUP request //设置会话的属性,以及传输模式,提醒S建立会话
        S->C:        SETUP response //S建立会话,返回会话标识符,以及会话相关信息
        客户端到服务端的请求举例:

SETUP rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/trackID=1
 RTSP/1.0
Cseq: 
3
Transport: RTP
/AVP;UNICAST;client_port=16264-16265;mode=play

       服务端对客户端的回应举例:

RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
3
Session: 
26633092229589
Date: Mon, 
03 Aug 2009 08:21:33 GMT
Expires: Mon, 
03 Aug 2009 08:21:33 GMT
Transport: RTP
/AVP;UNICAST;mode=play;client_port=16264-16265;server_port=20026-20027

2.4  PLAY
        C->S:      PLAY request //C请求播放
        S->C:      PLAY response //S回应该请求的信息
        客户端到服务端的请求举例:

PLAY rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp RTSP/1.0
Session: 26633092229589
Cseq: 
4

       服务端对客户端的回应举例:

RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
4
Session: 
26633092229589
RTP
-Info: url=rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/trackID=1;seq=0;rtptime=0

2.5  PAUSE
        C->S:      PAUSE request //C请求暂停播放
        S->C:      PAUSE response //S回应该请求的信息
        客户端到服务端的请求举例:

PAUSE rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/ RTSP/1.0
Cseq: 5
Session: 
26633092229589

       服务端对客户端的回应举例:

RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
5
Session: 
26633092229589

2.6  TEARDOWN
        C->S:        TEARDOWN request //C请求关闭会话
        S->C:        TEARDOWN response //S回应该请求
        客户端到服务端的请求举例:

TEARDOWN rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/ RTSP/1.0
Cseq: 6
User
-Agent: RealMedia Player HelixDNAClient/10.0.0.11279 (win32)
Session: 
26633092229589

       服务端对客户端的回应举例:

RTSP/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
6
Session: 
26633092229589
Connection: Close


三. RTSP客户端的Java实现
3.1  接口IEvent.java

      接口IEvent.java的代码如下:
package com.amigo.rtsp;

import java.io.IOException;
import java.nio.channels.SelectionKey;

/**
* IEvent.java 网络事件处理器,当Selector可以进行操作时,调用这个接口中的方法.
* 2007-3-22 下午03:35:51
@author sycheng
@version 1.0
*/

public interface IEvent {
    
/**
    * 当channel得到connect事件时调用这个方法.
    * 
@param key
    * 
@throws IOException
    
*/

    
void connect(SelectionKey key) throws IOException;

    
/**
    * 当channel可读时调用这个方法.
    * 
@param key
    * 
@throws IOException
    
*/

    
void read(SelectionKey key) throws IOException;

    
/**
    * 当channel可写时调用这个方法.
    * 
@throws IOException
    
*/

    
void write() throws IOException;

    
/**
    * 当channel发生错误时调用.
    * 
@param e
    
*/

    
void error(Exception e);
}

3.2  RTSP的测试类:RTSPClient.java
        RTSP的测试类RTSPClient.java类的代码如下所示:
package com.amigo.rtsp;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;

public class RTSPClient extends Thread implements IEvent {

    
private static final String VERSION = " RTSP/1.0\r\n";
    
private static final String RTSP_OK = "RTSP/1.0 200 OK";

    
/** 远程地址 */
    
private final InetSocketAddress remoteAddress;

    
/** * 本地地址 */
    
private final InetSocketAddress localAddress;

    
/** * 连接通道 */
    
private SocketChannel socketChannel;

    
/** 发送缓冲区 */
    
private final ByteBuffer sendBuf;

    
/** 接收缓冲区 */
    
private final ByteBuffer receiveBuf;

    
private static final int BUFFER_SIZE = 8192;

    
/** 端口选择器 */
    
private Selector selector;

    
private String address;

    
private Status sysStatus;

    
private String sessionid;

    
/** 线程是否结束的标志 */
    
private AtomicBoolean shutdown;
    
    
private int seq=1;
    
    
private boolean isSended;
    
    
private String trackInfo;
    

    
private enum Status {
        init, options, describe, setup, play, pause, teardown
    }


    
public RTSPClient(InetSocketAddress remoteAddress,
            InetSocketAddress localAddress, String address) 
{
        
this.remoteAddress = remoteAddress;
        
this.localAddress = localAddress;
        
this.address = address;

        
// 初始化缓冲区
        sendBuf = ByteBuffer.allocateDirect(BUFFER_SIZE);
        receiveBuf 
= ByteBuffer.allocateDirect(BUFFER_SIZE);
        
if (selector == null{
            
// 创建新的Selector
            try {
                selector 
= Selector.open();
            }
 catch (final IOException e) {
                e.printStackTrace();
            }

        }


        startup();
        sysStatus 
= Status.init;
        shutdown
=new AtomicBoolean(false);
        isSended
=false;
    }


    
public void startup() {
        
try {
            
// 打开通道
            socketChannel = SocketChannel.open();
            
// 绑定到本地端口
            socketChannel.socket().setSoTimeout(30000);
            socketChannel.configureBlocking(
false);
            socketChannel.socket().bind(localAddress);
            
if (socketChannel.connect(remoteAddress)) {
                System.out.println(
"开始建立连接:" + remoteAddress);
            }

            socketChannel.register(selector, SelectionKey.OP_CONNECT
                    
| SelectionKey.OP_READ | SelectionKey.OP_WRITE, this);
            System.out.println(
"端口打开成功");

        }
 catch (final IOException e1) {
            e1.printStackTrace();
        }

    }


    
public void send(byte[] out) {
        
if (out == null || out.length < 1{
            
return;
        }

        
synchronized (sendBuf) {
            sendBuf.clear();
            sendBuf.put(out);
            sendBuf.flip();
        }


        
// 发送出去
        try {
            write();
            isSended
=true;
        }
 catch (final IOException e) {
            e.printStackTrace();
        }

    }


    
public void write() throws IOException {
        
if (isConnected()) {
            
try {
                socketChannel.write(sendBuf);
            }
 catch (final IOException e) {
            }

        }
 else {
            System.out.println(
"通道为空或者没有连接上");
        }

    }


    
public byte[] recieve() {
        
if (isConnected()) {
            
try {
                
int len = 0;
                
int readBytes = 0;

                
synchronized (receiveBuf) {
                    receiveBuf.clear();
                    
try {
                        
while ((len = socketChannel.read(receiveBuf)) > 0{
                            readBytes 
+= len;
                        }

                    }
 finally {
                        receiveBuf.flip();
                    }

                    
if (readBytes > 0{
                        
final byte[] tmp = new byte[readBytes];
                        receiveBuf.get(tmp);
                        
return tmp;
                    }
 else {
                        System.out.println(
"接收到数据为空,重新启动连接");
                        
return null;
                    }

                }

            }
 catch (final IOException e) {
                System.out.println(
"接收消息错误:");
            }

        }
 else {
            System.out.println(
"端口没有连接");
        }

        
return null;
    }


    
public boolean isConnected() {
        
return socketChannel != null && socketChannel.isConnected();
    }


    
private void select() {
        
int n = 0;
        
try {
            
if (selector == null{
                
return;
            }

            n 
= selector.select(1000);

        }
 catch (final Exception e) {
            e.printStackTrace();
        }


        
// 如果select返回大于0,处理事件
        if (n > 0{
            
for (final Iterator<SelectionKey> i = selector.selectedKeys()
                    .iterator(); i.hasNext();) 
{
                
// 得到下一个Key
                final SelectionKey sk = i.next();
                i.remove();
                
// 检查其是否还有效
                if (!sk.isValid()) {
                    
continue;
                }


                
// 处理事件
                final IEvent handler = (IEvent) sk.attachment();
                
try {
                    
if (sk.isConnectable()) {
                        handler.connect(sk);
                    }
 else if (sk.isReadable()) {
                        handler.read(sk);
                    }
 else {
                        
// System.err.println("Ooops");
                    }

                }
 catch (final Exception e) {
                    handler.error(e);
                    sk.cancel();
                }

            }

        }

    }


    
public void shutdown() {
        
if (isConnected()) {
            
try {
                socketChannel.close();
                System.out.println(
"端口关闭成功");
            }
 catch (final IOException e) {
                System.out.println(
"端口关闭错误:");
            }
 finally {
                socketChannel 
= null;
            }

        }
 else {
            System.out.println(
"通道为空或者没有连接");
        }

    }


    @Override
    
public void run() {
        
// 启动主循环流程
        while (!shutdown.get()) {
            
try {
                
if (isConnected()&&(!isSended)) {
                    
switch (sysStatus) {
                    
case init:
                        doOption();
                        
break;
                    
case options:
                        doDescribe();
                        
break;
                    
case describe:
                        doSetup();
                        
break;
                    
case setup:
                        
if(sessionid==null&&sessionid.length()>0){
                            System.out.println(
"setup还没有正常返回");
                        }
else{
                            doPlay();
                        }

                        
break;
                    
case play:
                        doPause();
                        
break;
                        
                    
case pause:
                        doTeardown();
                        
break;
                    
default:
                        
break;
                    }

                }

                
// do select
                select();
                
try {
                    Thread.sleep(
1000);
                }
 catch (final Exception e) {
                }

            }
 catch (final Exception e) {
                e.printStackTrace();
            }

        }

        
        shutdown();
    }


    
public void connect(SelectionKey key) throws IOException {
        
if (isConnected()) {
            
return;
        }

        
// 完成SocketChannel的连接
        socketChannel.finishConnect();
        
while (!socketChannel.isConnected()) {
            
try {
                Thread.sleep(
300);
            }
 catch (final InterruptedException e) {
                e.printStackTrace();
            }

            socketChannel.finishConnect();
        }


    }


    
public void error(Exception e) {
        e.printStackTrace();
    }


    
public void read(SelectionKey key) throws IOException {
        
// 接收消息
        final byte[] msg = recieve();
        
if (msg != null{
            handle(msg);
        }
 else {
            key.cancel();
        }

    }


    
private void handle(byte[] msg) {
        String tmp 
= new String(msg);
        System.out.println(
"返回内容:");
        System.out.println(tmp);
        
if (tmp.startsWith(RTSP_OK)) {
            
switch (sysStatus) {
            
case init:
                sysStatus 
= Status.options;
                
break;
            
case options:
                sysStatus 
= Status.describe;
                trackInfo
=tmp.substring(tmp.indexOf("trackID"));
                
break;
            
case describe:
                sessionid 
= tmp.substring(tmp.indexOf("Session: "+ 9, tmp
                        .indexOf(
"Date:"));
                
if(sessionid!=null&&sessionid.length()>0){
                    sysStatus 
= Status.setup;
                }

                
break;
            
case setup:
                sysStatus 
= Status.play;
                
break;
            
case play:
                sysStatus 
= Status.pause;
                
break;
            
case pause:
                sysStatus 
= Status.teardown;
                shutdown.set(
true);
                
break;
            
case teardown:
                sysStatus 
= Status.init;
                
break;
            
default:
                
break;
            }

            isSended
=false;
        }
 else {
            System.out.println(
"返回错误:" + tmp);
        }


    }


    
private void doTeardown() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"TEARDOWN ");
        sb.append(
this.address);
        sb.append(
"/");
        sb.append(VERSION);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"User-Agent: RealMedia Player HelixDNAClient/10.0.0.11279 (win32)\r\n");
        sb.append(
"Session: ");
        sb.append(sessionid);
        sb.append(
"\r\n");
        send(sb.toString().getBytes());
        System.out.println(sb.toString());
    }


    
private void doPlay() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"PLAY ");
        sb.append(
this.address);
        sb.append(VERSION);
        sb.append(
"Session: ");
        sb.append(sessionid);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"\r\n");
        System.out.println(sb.toString());
        send(sb.toString().getBytes());

    }


    
private void doSetup() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"SETUP ");
        sb.append(
this.address);
        sb.append(
"/");
        sb.append(trackInfo);
        sb.append(VERSION);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"Transport: RTP/AVP;UNICAST;client_port=16264-16265;mode=play\r\n");
        sb.append(
"\r\n");
        System.out.println(sb.toString());
        send(sb.toString().getBytes());
    }


    
private void doOption() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"OPTIONS ");
        sb.append(
this.address.substring(0, address.lastIndexOf("/")));
        sb.append(VERSION);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"\r\n");
        System.out.println(sb.toString());
        send(sb.toString().getBytes());
    }


    
private void doDescribe() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"DESCRIBE ");
        sb.append(
this.address);
        sb.append(VERSION);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"\r\n");
        System.out.println(sb.toString());
        send(sb.toString().getBytes());
    }

    
    
private void doPause() {
        StringBuilder sb 
= new StringBuilder();
        sb.append(
"PAUSE ");
        sb.append(
this.address);
        sb.append(
"/");
        sb.append(VERSION);
        sb.append(
"Cseq: ");
        sb.append(seq
++);
        sb.append(
"\r\n");
        sb.append(
"Session: ");
        sb.append(sessionid);
        sb.append(
"\r\n");
        send(sb.toString().getBytes());
        System.out.println(sb.toString());
    }

    
    
public static void main(String[] args) {
        
try {
            
// RTSPClient(InetSocketAddress remoteAddress,
            
// InetSocketAddress localAddress, String address)
            RTSPClient client = new RTSPClient(
                    
new InetSocketAddress("218.207.101.236"554),
                    
new InetSocketAddress("192.168.2.28"0),
                    
"rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp");
            client.start();
        }
 catch (Exception e) {
            e.printStackTrace();
        }

    }

}

       其中:rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp为我在网上找到的一个rtsp的sdp地址,读者可自行更换,RTSP的默认端口为554.
3.3  运行结果
       运行RTSPClient.java,运行结果如下所示:
端口打开成功
OPTIONS rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331 RTSP/1.0
Cseq: 1


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
1
Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD


DESCRIBE rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp RTSP/1.0
Cseq: 2


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
2
Content
-length: 421
Date: Mon, 
03 Aug 2009 08:50:36 GMT
Expires: Mon, 
03 Aug 2009 08:50:36 GMT
Content
-Type: application/sdp
x
-Accept-Retransmit: our-retransmit
x
-Accept-Dynamic-Rate: 1
Content
-Base: rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/

v
=0
o
=MediaBox 127992 137813 IN IP4 0.0.0.0
s
=RTSP Session
i
=Starv Box Live Cast
c
=IN IP4 218.207.101.236
t
=0 0
a
=range:npt=now-
a
=control:*
m
=video 0 RTP/AVP 96
b
=AS:20
a
=rtpmap:96 MP4V-ES/1000
a
=fmtp:96 profile-level-id=8; config=000001b008000001b5090000010000000120008440fa282c2090a31f; decode_buf=12586
a
=range:npt=now-
a
=framerate:5
a
=framesize:96 176-144
a
=cliprect:0,0,144,176
a
=control:trackID=1

SETUP rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/trackID=1
 RTSP/1.0
Cseq: 
3
Transport: RTP
/AVP;UNICAST;client_port=16264-16265;mode=play


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
3
Session: 
15470472221769
Date: Mon, 
03 Aug 2009 08:50:36 GMT
Expires: Mon, 
03 Aug 2009 08:50:36 GMT
Transport: RTP
/AVP;UNICAST;mode=play;client_port=16264-16265;server_port=20080-20081


PLAY rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp RTSP/1.0
Session: 15470472221769
Cseq: 
4


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
4
Session: 
15470472221769
RTP
-Info: url=rtsp://218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/trackID=1;seq=0;rtptime=0


PAUSE rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/ RTSP/1.0
Cseq: 5
Session: 
15470472221769


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
5
Session: 
15470472221769


TEARDOWN rtsp:
//218.207.101.236:554/mobile/3/67A451E937422331/8jH5QPU5GWS07Ugn.sdp/ RTSP/1.0
Cseq: 6
User
-Agent: RealMedia Player HelixDNAClient/10.0.0.11279 (win32)
Session: 
15470472221769


返回内容:
RTSP
/1.0 200 OK
Server: PVSS
/1.4.8 (Build/20090111; Platform/Win32; Release/StarValley; )
Cseq: 
6
Session: 
15470472221769
Connection: Close


端口关闭成功

     对照运行结果,读者可以熟悉RTSP的常用命令.
posted on 2009-08-03 16:53 阿蜜果 阅读(8932) 评论(8)  编辑  收藏 所属分类: 协议


FeedBack:
# re: RTSP实例
2009-08-04 11:39 | 99读书人
不错啊   回复  更多评论
  
# re: RTSP实例
2009-08-04 12:18 | 99读书人
好东西啊~~  回复  更多评论
  
# re: RTSP实例
2009-09-30 17:51 | wyf20030725
有服务器端的代码吗?  回复  更多评论
  
# re: 蜜果私塾:RTSP实例
2012-04-06 11:45 | #
急求服务器端的代码~~!!limp87@163.com  回复  更多评论
  
# re: 蜜果私塾:RTSP实例
2012-05-08 05:32 | 33
怎么运行啊  回复  更多评论
  
# re: 蜜果私塾:RTSP实例
2012-05-13 00:19 | 二小
流媒体的用户验证怎么发送命令?  回复  更多评论
  
# re: 蜜果私塾:RTSP实例
2015-04-01 14:38 | 冷树
在发送SETUP 的时候报错   回复  更多评论
  
# re: 蜜果私塾:RTSP实例
2015-04-01 14:39 | 冷树
接收消息错误:java.io.IOException: 远程主机强迫关闭了一个现有的连接。
在SETUP的时候  回复  更多评论
  

只有注册用户登录后才能发表评论。


网站导航:
 
<2009年8月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
303112345

      生活将我们磨圆,是为了让我们滚得更远——“圆”来如此。
      我的作品:
      玩转Axure RP  (2015年12月出版)
      

      Power Designer系统分析与建模实战  (2015年7月出版)
      
     Struts2+Hibernate3+Spring2   (2010年5月出版)
     

留言簿(262)

随笔分类

随笔档案

文章分类

相册

关注blog

积分与排名

  • 积分 - 2279819
  • 排名 - 3

最新评论

阅读排行榜

评论排行榜