标题:[原创]JAVA实现tftp服务端 -- 我博客中的一篇文章
只看楼主
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
 问题点数:0 回复次数:10 
[原创]JAVA实现tftp服务端 -- 我博客中的一篇文章

这几天看了一下tftp协议,非常简单,这个学期选修的JAVA,
想趁这个机会练习一下,写一个tftp server的,刚刚把demo完成了,
用windows2003的tftp客户端测试过了,也用我的ADSL (嵌入式linux)里面的tftp测试过,demo版本还是挺好的,至少可以用 。。。
=================================================

源代码可以在这里下载: UploadFiles/2007-5/53552715.zip

文章在博客中的地址: http://blog.bc-cn.net/user1/37/archives/2007/5202.shtml
=================================================

因为tftp协议本身就很简单,所以我的代码就用了三个类文件,
Main.java --> 这个是启动代码,这个demo版本中,
他只是建立一个ServerAgent的对象,然后启动这个对象
tftpServerAgent.java --> 这是Server,它一直监听UDP Port 69,
来了新的连接,它就创建一个ClientAgent,
自己继续等待。
tftpClientAgent.java --> 这是与ftp客户端通信的类,它处理RRQ和WRQ
请求。
这两个Agent都继承了Thread,因为我想在以后的版本中加入Server的管理功能。

============================================
主要代码:
=================================

Main.java
===========================================

public class Main {

/** Creates a new instance of Main */
public Main() {
}

public static void main(String[] args) throws Exception {
// TODO code application logic here
tftpServerAgent tftp_srv = new tftpServerAgent();
tftp_srv.setDaemon(true);
tftp_srv.start();

tftp_srv.join(); //暂时先这么处理吧
}

}

=======================================

tftpServerAgent.java
===========================================

public class tftpServerAgent extends Thread {

//over the parent's run methord
public void run() {
try {
DatagramSocket tftpd = new DatagramSocket(69); //tftp server socket
byte[] buf = new byte[516]; //a buffer for UDP packet
DatagramPacket dp = new DatagramPacket(buf, 516); //a UDP packet

DataInputStream din = null;
tftpClientAgent newClient = null;
short tftp_opcode = 0; //opcode: the 2 bytes in the front of tftp packet
String tftp_filename = null;
String tftp_mode = null;
while (true) {
tftpd.receive(dp); //wait for a client
buf = dp.getData(); //get the UDP packet data
din = new DataInputStream(new ByteArrayInputStream(buf));
tftp_opcode = din.readShort(); //get the opcode
{ //get the filename
int fnoffset = 2;
int fnlen = 0;
while (din.readByte() != 0) {
fnlen++; //filename will end with a null char('\0')
}
tftp_filename = new String(buf, fnoffset, fnlen);

//get the mode
int mdnoffset = fnoffset + fnlen + 2;
int mdnlen = 0;
while (din.readByte() != 0) {
mdnlen++; //filename will end with a null char('\0')
}
tftp_mode = new String(buf, mdnoffset, mdnlen);
}

switch (tftp_opcode) {
case 1:
//RRQ
case 2:
//WRQ
newClient = new tftpClientAgent(dp.getAddress(), dp.getPort(), tftp_opcode, tftp_filename, tftp_mode);
newClient.setDaemon(true);
newClient.start();

this.m_hashClients.put(dp.getAddress().toString()+dp.getPort(), newClient);
//System.out.println("debug: Main.main() --> a RRQ thread start ....");
break;
}
}
} catch (Exception e) {

}
}

}

=========================================
tftpClientAgent.java
=========================================

public class tftpClientAgent extends Thread {
public tftpClientAgent(InetAddress ip, int port, short opcode, String fname, String mode) {
this.m_ClientAddress = ip;
this.m_ClientPort = port;
this.m_curopcode = opcode;
this.m_filename = fname;
this.m_mode = mode;
}

public void run() {
int nfail = 100;
while (nfail-- > 0) {
//try getFreePort 100 times
try {
this.m_so_tftp = new DatagramSocket(this.getFreePort());
break; //get a random port number
} catch (SocketException e) {
}
}

//ok, the UDP socket is ready, response to the client
switch (this.m_curopcode) {
case 1:
//RRQ
this.RRQ();
break;
case 2:
//WRQ
this.WRQ();
//System.out.println("debug: tftpClientAgent.run() --> a WRQ ended ...");
break;
}

this.m_so_tftp.close();

}

}

private InetAddress m_ClientAddress; //ip of the client
private int m_ClientPort; //port of the client
private DatagramSocket m_so_tftp; //the socket object send or get message
private short m_curopcode; //the current opcode( wrq/rrq )
private String m_filename;
private String m_mode;
private final int m_MAX_nTimeOut = 5

[此贴子已经被作者于2007-5-3 21:08:20编辑过]

搜索更多相关主题的帖子: tftp JAVA 博客 服务端 blog 
2007-05-03 21:05
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
得分:0 
[原创]JAVA实现tftp服务端 -- RRQ
这里是RRQ请求的处理代码:
=================================================

源代码可以在这里下载: UploadFiles/2007-5/53552715.zip

文章在博客中的地址: http://blog.bc-cn.net/user1/37/archives/2007/5202.shtml
=================================================

tftpClientAgent.java
=====================================

public void RRQ() {
int ntimeout = this.m_MAX_nTimeOut;
try {
short nblock = 1;
//send the #0 block ACK to start the transfer
if (!this.SendFile(nblock, this.m_filename, this.m_mode)) {
this.SendERROR((short)0, "无法读取文件");
return ;
}

//wait for the ACK packet
while (ntimeout > 0) {
DatagramPacket dp;
dp = this.waitForData();

if (dp == null) {
//this.SendERROR((short)0, "超时了,连接被服务器断开 ...");
//System.out.println("debug: tftpClientagent.RRQ() --> timeout: " + ntimeout);
ntimeout--;
} else {
//ok, get a packet, check the ip and port
if (!((dp.getAddress().equals(this.m_ClientAddress))
&& (dp.getPort() == this.m_ClientPort))) {
//ip or port has a mistake
//this.SendERROR((short)0, "我不认识你");
//System.out.println("debug: RRQ() --> ip or port error ...");
ntimeout--;
} else { //right ip and port
//get the opcode
DataInputStream din = new DataInputStream(new ByteArrayInputStream(dp.getData()));
int opcode = din.readShort();
//System.out.println("debug: the opcode is " + opcode);
if (opcode != 4) {
//不是04号ACK包
if (opcode == 1 && nblock == 1) {
//是01号RRQ包,且正在等待#1号ACK包
//此时收到RRQ,说明#1号data有可能失败了,重发
if (!this.SendFile(nblock, this.m_filename, this.m_mode)) {
this.SendERROR((short)0, "无法读取文件");
return ;
}
} else {
this.SendERROR((short)4, "非法的TFTP操作");
}

ntimeout--;
} else { //is an ACK packet
//System.out.println("debug: get a ACK packet");
int nblk = din.readShort();
//System.out.println("debug: the waiting for #" + nblock + " -- the curBlock is #" + nblk);
if (nblk != nblock) {
//System.out.println("debug: re-SendFile(" + nblock + ")");
//不是期待的块号,重发data包,超时计数减1
if (!this.SendFile(nblock, this.m_filename, this.m_mode)) {
this.SendERROR((short)0, "无法读取文件");
return ;
}
ntimeout--;
} else { //get the right ACK packet
//the rest of the special file
long restSize = this.getRestOfFile(nblock, this.m_filename);
//System.out.println("debug: the restSize = " + restSize);
if (restSize < 0) {
//ok, 上次已经把剩余的发送完毕了,结束
ntimeout = 0;
} else {
//send the next block data
nblock++;
//System.out.println("debug: start sending ...");
if (!this.SendFile(nblock, this.m_filename, this.m_mode)) {
//System.out.println("debug: ERROR while sending the #" + nblock);
this.SendERROR((short)0, "无法读取文件");
ntimeout = 0;
} else {
//send succ
//System.out.println("debug: send the #" + nblock);
ntimeout = this.m_MAX_nTimeOut; //重置超时记数
//continue wait for ACK of the current block
}
}
}
}
}
}
}
} catch (Exception e) {
//System.out.println("Exception in tftpClientAgent.RRQ() --> " + e.getMessage());
}
}

=============================================


Member Of Qingfeng Studio 王旭华[http://][http://hi.baidu.com/rabbit5455]
2007-05-03 21:11
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
得分:0 
[原创]JAVA实现tftp服务端 -- WRQ
这里是WRQ请求的处理代码:
=================================================

源代码可以在这里下载: UploadFiles/2007-5/53552715.zip

文章在博客中的地址: http://blog.bc-cn.net/user1/37/archives/2007/5202.shtml
=================================================

tftpClientAgent.java
=====================================

public void WRQ() {
int ntimeout = this.m_MAX_nTimeOut;
try {
short nblock = 0;
//send the #0 block ACK to start the transfer
this.SendACK(nblock++);

//wait for the data packet
while (ntimeout > 0) {
DatagramPacket dp;
dp = this.waitForData();
//dp = new DatagramPacket(buf, 516);
if (dp == null) {
//this.SendERROR((short)0, "超时了,连接被服务器断开 ...");
//System.out.println("debug: tftpClientagent.WRQ() --> timeout: " + ntimeout);
ntimeout--;
} else {
//ok, get a packet, check the ip and port
if (!((dp.getAddress().equals(this.m_ClientAddress))
&& (dp.getPort() == this.m_ClientPort))) {
//ip or port has a mistake
//this.SendERROR((short)0, "我不认识你");
//System.out.println("debug: ip or port error ...");
ntimeout--;
} else { //right ip and port
//get the opcode
DataInputStream din = new DataInputStream(new ByteArrayInputStream(dp.getData()));
int opcode = din.readShort();
//System.out.println("debug: the opcode is " + opcode);
if (opcode != 3) {
//不是03号data包
if (opcode == 2 && nblock == 1) {
//是02号WRQ包,且正在等待#1号data包
//此时收到WRQ,说明#0号ACK有可能失败了,重发
this.SendACK((short)(nblock-1));
} else {
this.SendERROR((short)4, "非法的TFTP操作");
}

ntimeout--;
} else { //data packet
//System.out.println("debug: get a data packet");
int nblk = din.readShort();
//System.out.println("debug: the waiting for #" + nblock + " -- the curBlock is #" + nblk);
if (nblk != nblock) {
//System.out.println("debug: re-SendACK(" + (nblock-1) + ")");
//不是期待的块号,重发上一个包的ACK,超时计数减1
this.SendACK((short)(nblock-1));
ntimeout--;
} else { //the right packet waiting for
//this.SaveFile(this.m_filename);
if (!this.SaveFile(nblock, dp, this.m_filename, this.m_mode)) {
ntimeout = 0;
this.SendERROR((short)3, "磁盘满或超过分配的配额");
} else {
//send the current block ACK
this.SendACK(nblock);
//System.out.println("debug: dp.length = " + (dp.getLength()-4));
if (dp.getLength()-4 >= 512) {
//the transfer isn't ended
nblock++; //wait for the next block
ntimeout = this.m_MAX_nTimeOut; //重置超时记数
} else {
//ok, there is no more data, the transfer succ
ntimeout = 1;
//continue wait for current block for 1 timeout,
//in case of the last ACK's failing
}
}
}
}
}
}
}
} catch (Exception e) {
//System.out.println("Exception in tftpClientAgent.WRQ() --> " + e.getLocalizedMessage());
}
}

======================================

好了,demo的代码大部分就在这里了。

以后继续努力,


Member Of Qingfeng Studio 王旭华[http://][http://hi.baidu.com/rabbit5455]
2007-05-03 21:13
yuyunliuhen
Rank: 6Rank: 6
等 级:贵宾
威 望:20
帖 子:1435
专家分:0
注 册:2005-12-12
得分:0 
哇噻,好强啊

Go confidently in the  directions of your dreams,live the life you have imagined!Just do it!
It is no use learning without thinking!
2007-05-03 22:07
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
得分:0 
这个是demo版本的 , 写的不好,我想逐步加入一些功能和组件,
如做一个连接池,这样就不用每次都new一个ClientAgent了,
减少线程创建和销毁的开销,还有服务器的管理员控制台功能。

但对于线程池的问题,我有些疑惑:
这是一个UDP的 协议,文件传输可能持续很长时间,那么这个
ClientAgent就没法接受新的任务,如果所有的ClientAgent都在
为自己的Client服务,那么新的Client来了,怎么办呢,再new一个
ClientAgent吗?有没有其他的方法。

希望与论坛里的各位高手交流一下看法。

Member Of Qingfeng Studio 王旭华[http://][http://hi.baidu.com/rabbit5455]
2007-05-05 14:52
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
得分:0 
回复:(rabbit5455)这个是demo版本的 , 写的不好,...
1. 目的

TFTP是一个传输文件的简单协议,它其于UDP协议而实现,但是我们也不能确定有些TFTP协议是基于其它传输协议完成的。此协议设计的时候是进行小文件传输的。因此它不具备通常的FTP的许多功能,它只能从文件服务器上获得或写入文件,不能列出目录,不进行认证,它传输8位数据。传输中有三种模式:netascii,这是8位的ASCII码形式,另一种是octet,这是8位源数据类型;最后一种mail已经不再支持,它将返回的数据直接返回给用户而不是保存为文件。

2. 概况

任何传输起自一个读取或写入文件的请求,这个请求也是连接请求。如果服务器批准此请求,则服务器打开连接,数据以定长512字节传输。每个数据包包括一块数据,服务器发出下一个数据包以前必须得到客户对上一个数据包的确认。如果一个数据包的大小小于512字节,则表示传输结构。如果数据包在传输过程中丢失,发出方会在超时后重新传输最后一个未被确认的数据包。通信的双方都是数据的发出者与接收者,一方传输数据接收应答,另一方发出应答接收数据。大部分的错误会导致连接中断,错误由一个错误的数据包引起。这个包不会被确认,也不会被重新发送,因此另一方无法接收到。如果错误包丢失,则使用超时机制。错误主要是由下面三种情况引起的:不能满足请求,收到的数据包内容错误,而这种错误不能由延时或重发解释,对需要资源的访问丢失(如硬盘满)。TFTP只在一种情况下不中断连接,这种情况是源端口不正确,在这种情况下,指示错误的包会被发送到源机。这个协议限制很多,这是都是为了实现起来比较方便而进行的。

3. 与其它协议的联系

因为TFTP使用UDP,而UDP使用IP,IP可以还使用其它本地通信方法。因此一个TFTP包中会有以下几段:本地媒介头,IP头,数据报头,TFTP头,剩下的就是TFTP数据了。TFTP在IP头中不指定任何数据,但是它使用UDP中的源和目标端口以及包长度域。由TFTP使用的包标记(TID)在这里被用做端口,因此TID必须介于0到65,535之间。对它的初始化我们在后面讨论。TFTP头中包括两上字节的操作码,这个码指出了包的类型下面我们看看大体上的TFTP包格式,相关的内容我们在后面的章节中进行讨论。

---------------------------------------------------
| Local Medium | Internet | Datagram | TFTP |
---------------------------------------------------
图3-1: 包头次序

4. 初始连接

初始连接时候需要发出WRQ(请求写入远程系统)或RRQ(请求读取远程系统),收到一个确定应答,一个确定可以写出的包或应该读取的第一块数据。通常确认包包括要确认的包的包号,每个数据包都与一个块号相对应,块号从1开始而且是连续的。因此对于写入请求的确定是一个比较特殊的情况,因此它的包的包号是0。如果收到的包是一个错误的包,则这个请求被拒绝。创建连接时,通信双方随机选择一个TID,因此是随机选择的,因此两次选择同一个ID的可能性就很小了。每个包包括两个TID,发送者ID和接收者ID。这些ID用于在UDP通信时选择端口,请求主机选择ID的方法上面已经说过了,在第一次请求的时候它会将请求发到TID 69,也就是服务器的69端口上。应答时,服务器使用一个选择好的TID作为源TID,并用上一个包中的TID作为目的ID进行发送。这两个被选择的ID在随后的通信中会被一直使用。下例是一个写入的例子,其中WRQ,ACK和DATA代表写入请求,确认和数据。

1. 主机A向主机B发出WRQ,其中端口为69
2. B机向A机发出ACK,块号为0,包括B和A的TID

此时连接建立,第一个数据包以序列号1从主机开始发出。以后两台主机要保证以开始时确定的TID进行通信。如果源ID与原来确定的ID不一样,这个包会被认识为发送到了错误的地址而被抛弃。错误的包是被发送到正确端口的,但是包本身有错误。设想发送方发出一个请求,这个请求在网络的那个设备中被复制成两个包,接收方先后接收到两个包。接收方会认为为这是两个独立的请求,会返回两个应答。当这两个应答其中之一被接收到时,连接已经建立。第二个应答再到达时,这个包会被抛弃,而不会因为接收到第二个应答包而导致第一个建立的连接失败。

5. TFTP包

TFTP支持五种类型的包,我们在以上已经说明这五种类型的包:

opcode operation
1 Read request (RRQ)
2 Write request (WRQ)
3 Data (DATA)
4 Acknowledgment (ACK)
5 Error (ERROR)

包头中包括了这个包所指定的操作码。


2 bytes string 1 byte string 1 byte
------------------------------------------------
| Opcode | Filename | 0 | Mode | 0 |
------------------------------------------------

Figure 5-1: RRQ/WRQ包

RRQ和WRQ包(代码分别为1和2)的格式如上所示。文件名是NETASCII码字符,以0结束。 而MODE域包括了字符串"netascii","octet"或"mail",名称不分大小写。接收到NETASCII格式数据的主机必须将数据转换为本地格式。OCTET模式用于传输文件,这种文件在源机上以8位格式存储。假设每个机器都存在一个8位的格式,这样的假设是最一般的。比如DEC-20,这是一种36位机,我们可以假设它是4个8位外加另外4位而构成。如果机器收到OCTET格式文件,返回时必须与原来文件完全一样。在使用MAIL模式时,用户可以在FILE处使用接收人地址,这个地址可以是用户名或用户名@主机的形式,如果是后一种形式,允许主机使用电子邮件传输此文件。如果使用MAIL类型,包必须以WRQ开始,否则它与NETASCII完全一样。我们的讨论建立在发送方和接收方都在相同模式的情况下,但是双方可以以不同的模式进行传输。例如一个机器可以是一台存储服务器,这样一台服务器需要将NETASCII格式转换为自己的格式。另外,我们可以设想DEC-20这种机器,它使用36位字长,用户这边可以使用特殊的机制一次读取36位,而服务器却可以仍然使用8位格式。在这两种情况下,我们看到了两台机器使用不同格式的情况。可以在两台主机间定义其它的传输方式,但是定义要小心,因为这种传输方式不为人知,而且也没有权威机构为其指定名称或定义它的模式。

2 bytes 2 bytes n bytes
----------------------------------
| Opcode | Block # | Data |
----------------------------------
Figure 5-2: DATA包

数据在数据包中传输,其格式如上图所示。数据包的OP码为3,它还包括有一个数据块号和数据。数据块号域从1开始编码,每个数据块加1,这样接收方可以确定这个包是新数据还是已经接收过的数据。数据域从0字节到512字节。如果数据域是512字节则它不是最后一个包,如果小于512字节则表示这个包是最后一个包。除了ACK和用于中断的包外,其它的包均得到确认。发出新的数据包等于确认上次的包。WRQ和DATA包由ACK或ERROR数据包确认,而RRQ数据包由DATA或ERROR数据包确认。下图即是一个ACK包,操作码为4。其中的包号为要确认的数据包的包号。

2 bytes 2 bytes
---------------------
| Opcode | Block # |
---------------------
Figure 5-3: ACK包

WRQ数据包被ACK数据包确认,WRQ数据包的包号为0。

2 bytes 2 bytes string 1 byte
-----------------------------------------
| Opcode | ErrorCode | ErrMsg | 0 |
-----------------------------------------
Figure 5-4: ERROR包

一个ERROR包,它的操作码是5,它的格式如上所示。此包可以被其它任何类型的包确认。错误码指定错误的类型。错误的值和错误的意义在附录中。错误信息是供程序员使用的。

6. 正常终止

传输的结束由DATA数据标记,其包括0-511个字符。这个包可以被其它数据包确认。接收方在发出对最后数据包的确认后可以断开连接,当然,适当的等待是比较好的,如果最后的确定包丢失可以再次传输。如果发出确认后仍然收到最后数据包,可以确定最后的确认丢失。发送最后一个DATA包的主机必须等待对此包的确认或超时。如果响应是ACK,传输完成。如果发送方超时并不准备重新发送并且接收方有问题或网络有问题时,发送也正常结束。当然实现时也可以是非正常结束,但无论如何连接都将被关闭。

7. 早终结

如果请求不能被满足,或者在传输中发生错误,需要发送ERROR包。这仅是一种传输友好的方式,这种包不会被确认也不会被重新传输,因此这种包可能永远不会被接收到。因此需要用超时来侦测错误。

I. 附录

包头的次序

2 bytes
----------------------------------------------------------
| Local Medium | Internet | Datagram | TFTP Opcode |
----------------------------------------------------------

TFTP格式

Type Op # 没有包头的格式

2 bytes string 1 byte string 1 byte
-----------------------------------------------
RRQ/ | 01/02 | Filename | 0 | Mode | 0 |
WRQ -----------------------------------------------

2 bytes 2 bytes n bytes
---------------------------------
DATA | 03 | Block # | Data |
---------------------------------

2 bytes 2 bytes
-------------------
ACK | 04 | Block # |
--------------------

2 bytes 2 bytes string 1 byte
----------------------------------------
ERROR | 05 | ErrorCode | ErrMsg | 0 |
----------------------------------------

读文件的初始连接

1. 主机A发RRQ到A,包括源=A的ID和目的=69
2. 主机B发送DATA,其中包号=1,这个包被传送到A,源=B的ID,目的=A的ID

错误码

Value Meaning

0 未定义,请参阅错误信息(如果提示这种信息的话)
1 文件未找到
2 访问非法
3 磁盘满或超过分配的配额
4 非法的TFTP操作
5 未知的传输ID
6 文件已经存在
7 没有类似的用户

Internet用户数据报头

(TFTP不一定非要在UDP上实现。)

Format

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

域的值

Source Port 由传输发起方选择
Dest. Port 由目的地选择(如果是RRQ或WRQ,其值为69)
Length 包括UDP包头的包长度
Checksum 校验码,如果是0,则未使用校验

注意:TFTP将传输标记TID传送给UDP作为源和目的端口

安全问题

因为TFTP没有安全控制机制,因此安全问题应该多加考虑。通常TFTP允许下载数据而不允许上传数据。


Member Of Qingfeng Studio 王旭华[http://][http://hi.baidu.com/rabbit5455]
2007-06-18 16:19
rabbit5455
Rank: 2
等 级:论坛游民
帖 子:123
专家分:25
注 册:2004-4-14
得分:0 
将tftp协议的中文版放在这里吧,是在找不到地方发这个 。。。

怎么说和这个帖子也有点儿关系。。。

Member Of Qingfeng Studio 王旭华[http://][http://hi.baidu.com/rabbit5455]
2007-06-18 16:21
linminna1985
Rank: 1
等 级:新手上路
帖 子:1
专家分:0
注 册:2008-4-9
得分:0 
LZ,为什么源代码没法下载呢?
2008-04-09 22:13
xhselina
Rank: 1
等 级:新手上路
帖 子:11
专家分:0
注 册:2008-10-23
得分:0 
强,顶一下!
2008-10-23 10:22
xhselina
Rank: 1
等 级:新手上路
帖 子:11
专家分:0
注 册:2008-10-23
得分:0 
强!!!!
2008-12-08 23:52



参与讨论请移步原网站贴子:https://bbs.bccn.net/thread-136779-1-1.html




关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.325206 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved