求助解读一段ping程序 ( 积分: 62 )

  • 主题发起人 主题发起人 RoyT
  • 开始时间 开始时间
R

RoyT

Unregistered / Unconfirmed
GUEST, unregistred user!
我本人不是学c的,对网络这部分更是难理解。今拿到一段代码,欲知道每行的意思,求哪位高手给解答一下。希望尽量把有函数的地方表明一下什么意思!不胜感激
#include <stdio.h>
#include "Ping.h"
#include "com_adminet_protocol_icmp_winPing.h"

/*
* Checksum routine for Internet Protocol family headers (C Version)
*/
unsigned short CPing::in_cksum(unsigned short *addr, int len)
{
register int nleft = len;
register u_short *w = addr;
register u_short answer;
register int sum = 0;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum),
* we add sequential 16 bit words to it, and at the end, fold
* back all the carry bits from the top 16 bits into the lower
* 16 bits.
*/
while( nleft > 1 ) {
sum += *w++;
nleft -= 2;
}
/* mop up an odd byte, if necessary */
if( nleft == 1 ) {
u_short u = 0;
*(unsigned char *)(&amp;u) = *(unsigned char *)w
sum += u;
}
/*
* add back carry outs from top 16 bits to low 16 bits
*/
sum = (sum >> 16) + (sum &amp;
0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
answer = ~sum; /* truncate to 16 bits */
return (answer);
}

int CPing::SendEchoRequest(SOCKET s,LPSOCKADDR_IN lpstToAddr,int nData)
{
ECHOREQUEST echoReq;
int nId = 1;
int nSeq = 1;
int nRet;
char* cData=(char*)malloc(nData);
// Fill in echo request
echoReq.icmpHdr.Type = ICMP_ECHOREQ;
echoReq.icmpHdr.Code = 0;
echoReq.icmpHdr.Checksum = 0;
echoReq.icmpHdr.ID = (USHORT)GetCurrentThreadId();
echoReq.icmpHdr.Seq = (USHORT)GetCurrentProcessId();
// Fill in some data to send
for (nRet = 0;
nRet < nData;
nRet++)
cData[nRet] = ' '+nRet;
echoReq.cData=cData;
// Save tick count when sent
echoReq.dwTime = GetTickCount();
// Put data in packet and compute checksum
echoReq.icmpHdr.Checksum = in_cksum((u_short *)&amp;echoReq, sizeof(ECHOREQUEST));
// Send the echo request
nRet = sendto(s, /* socket */
(LPSTR)&amp;echoReq, /* buffer */
sizeof(ECHOREQUEST),
0, /* flags */
(LPSOCKADDR)lpstToAddr, /* destination */
sizeof(SOCKADDR_IN));
/* address length */
free(cData);
if (nRet == SOCKET_ERROR)
return -1;
return (nRet);
}

DWORD CPing::RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, unsigned char *pTTL)
{
ECHOREPLY echoReply;
int nRet;
int nAddrLen = sizeof(struct sockaddr_in);
// Receive the echo reply
nRet = recvfrom(s, // socket
(LPSTR)&amp;echoReply, // buffer
sizeof(ECHOREPLY), // size of buffer
0, // flags
(LPSOCKADDR)lpsaFrom, // From address
&amp;nAddrLen); // pointer to address len
// Check return value
if (nRet == SOCKET_ERROR)
return 0;
if (echoReply.echoRequest.icmpHdr.ID!=(USHORT)GetCurrentThreadId())
return 0;
if (echoReply.echoRequest.icmpHdr.Seq!=(USHORT)GetCurrentProcessId())
return 0;
// return time sent and IP TTL
*pTTL = echoReply.ipHdr.TTL;
return(echoReply.echoRequest.dwTime);

}

int CPing::OpenRawSocket()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2,2), &amp;wsaData) != 0)
{
return -1;
// WSAStartup error!
}
// Create a Raw socket
rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawSocket == SOCKET_ERROR)
{
return 0;
}
return 1;
}
int CPing::CloseRawSocket()
{
int nRet;
if (rawSocket != INVALID_SOCKET)
nRet=closesocket(rawSocket);


if (nRet == SOCKET_ERROR)
return 0;
WSACleanup();
return 1;
}
int CPing::IcmpPing(LPCSTR pstrHost,int nTimeOut,int nData, char szResult[256])
{
PHOSTENT lpHost;
int nRet;
struct sockaddr_in saDest;
struct sockaddr_in saSrc;
DWORD dwTimeSent;
DWORD dwElapsed;
unsigned char cTTL;

if (rawSocket == INVALID_SOCKET)
return -1;
if (nData<=0)
nData=REQ_DATASIZE;
if (nTimeOut<=0)
nTimeOut=1000;

/*
nRet = setsockopt(rawSocket,
SOL_SOCKET,
SO_SNDTIMEO,
(char*)&amp;nTimeOut,
sizeof(nTimeOut));
if(nRet == SOCKET_ERROR)
{
return -1;
}
*/
// Lookup host
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
return -1;
}

// Setup destination socket address
saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;
// Send ICMP echo request
SendEchoRequest(rawSocket, &amp;saDest,nData);
int nTimeOutTemp=nTimeOut;
struct timeval Timeout;
fd_set readfds;
readfds.fd_count = 1;
readfds.fd_array[0] = rawSocket;

while(nTimeOutTemp>0)
{
Timeout.tv_sec = nTimeOutTemp/1000;
Timeout.tv_usec = nTimeOutTemp;

dwElapsed=GetTickCount();
nRet=select(1, &amp;readfds, NULL, NULL, &amp;Timeout);
if (nRet == SOCKET_ERROR)
{
return -1;
}
if (!nRet)
{
//Request time out
break;
}
else
{
// Receive reply
dwTimeSent = RecvEchoReply(rawSocket, &amp;saSrc, &amp;cTTL);
dwElapsed = GetTickCount() - dwElapsed;
if (dwTimeSent>0)
{
sprintf(szResult,"Reply from: %s: bytes=%d time=%ldms TTL=%d",
inet_ntoa(saSrc.sin_addr),
REQ_DATASIZE,
GetTickCount()-dwTimeSent,
cTTL);
return 1;
}
else
{
nTimeOutTemp=nTimeOutTemp-dwElapsed;
continue;
}
}
}
sprintf(szResult,"Request timed out!");
return 0;
}
int CPing::Ping(LPCSTR pstrHost,int nRetries,int nTimeOut)
{
SOCKET rawIcmpSocket;
LPHOSTENT lpHost;
int nLoop;
int nRet;
struct sockaddr_in saDest;
struct sockaddr_in saSrc;
DWORD dwTimeSent;
DWORD dwElapsed;
unsigned char cTTL;

int nData=REQ_DATASIZE;
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2,2), &amp;wsaData) != 0)
{
return -1;
// WSAStartup error!
}
if (nTimeOut<=0)
nTimeOut=1000;
// Create a Raw socket
rawIcmpSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawIcmpSocket == INVALID_SOCKET)
{
WSACleanup();
return -1;
}
/*
nRet = setsockopt(rawIcmpSocket,
SOL_SOCKET,
SO_SNDTIMEO,
(char*)&amp;nTimeOut,
sizeof(nTimeOut));
if(nRet == SOCKET_ERROR)
{
return -1;
}
*/
// Lookup host
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
closesocket(rawIcmpSocket);
WSACleanup();
return -1;
}

// Setup destination socket address
saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;
// Ping multiple times
for (nLoop = 0;
nLoop < nRetries;
nLoop++)
{
// Send ICMP echo request
SendEchoRequest(rawIcmpSocket, &amp;saDest,nData);
struct timeval Timeout;
fd_set readfds;
readfds.fd_count = 1;
readfds.fd_array[0] = rawIcmpSocket;
int nTimeOutTemp=nTimeOut;
while(nTimeOutTemp>0)
{
Timeout.tv_sec = nTimeOutTemp/1000;
Timeout.tv_usec = nTimeOutTemp;
dwElapsed=GetTickCount();
nRet=select(1, &amp;readfds, NULL, NULL, &amp;Timeout);

if (nRet == SOCKET_ERROR)
{
closesocket(rawIcmpSocket);
WSACleanup();
return -1;
}
if (!nRet)
{
//Request time out
break;
}
else
{
// Receive reply
dwTimeSent = RecvEchoReply(rawIcmpSocket, &amp;saSrc, &amp;cTTL);
dwElapsed = GetTickCount() - dwElapsed;

if (dwTimeSent>0)
{
closesocket(rawIcmpSocket);
WSACleanup();
return 1;
}
else
{
nTimeOutTemp=nTimeOutTemp-dwElapsed;
continue;
}
}
}
}
closesocket(rawIcmpSocket);
WSACleanup();
return 0;
}

/*
* Class: com_winPing
* Method: ping
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2
(JNIEnv * env, jobject obj, jstring IP)
{
CPing ping;
const char* szIP=env->GetStringUTFChars(IP,0);
return ping.Ping((LPCSTR)szIP,3,1000);
}
/*
* Class: com_dcnetworks_protocol_icmp_IcmpPing
* Method: simplePing
* Signature: (Ljava/lang/String;II)I
*/

JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2II
(JNIEnv* env, jobject obj, jstring IP, jint retrys, jint timeout)
{
if (retrys<1)
retrys=3;
if (timeout<1)
timeout=1000;
CPing ping;
const char* szIP=env->GetStringUTFChars(IP,0);
return ping.Ping((LPCSTR)szIP,(int)retrys,(int)timeout);
}
 
我本人不是学c的,对网络这部分更是难理解。今拿到一段代码,欲知道每行的意思,求哪位高手给解答一下。希望尽量把有函数的地方表明一下什么意思!不胜感激
#include <stdio.h>
#include "Ping.h"
#include "com_adminet_protocol_icmp_winPing.h"

/*
* Checksum routine for Internet Protocol family headers (C Version)
*/
unsigned short CPing::in_cksum(unsigned short *addr, int len)
{
register int nleft = len;
register u_short *w = addr;
register u_short answer;
register int sum = 0;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum),
* we add sequential 16 bit words to it, and at the end, fold
* back all the carry bits from the top 16 bits into the lower
* 16 bits.
*/
while( nleft > 1 ) {
sum += *w++;
nleft -= 2;
}
/* mop up an odd byte, if necessary */
if( nleft == 1 ) {
u_short u = 0;
*(unsigned char *)(&amp;u) = *(unsigned char *)w
sum += u;
}
/*
* add back carry outs from top 16 bits to low 16 bits
*/
sum = (sum >> 16) + (sum &amp;
0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
answer = ~sum; /* truncate to 16 bits */
return (answer);
}

int CPing::SendEchoRequest(SOCKET s,LPSOCKADDR_IN lpstToAddr,int nData)
{
ECHOREQUEST echoReq;
int nId = 1;
int nSeq = 1;
int nRet;
char* cData=(char*)malloc(nData);
// Fill in echo request
echoReq.icmpHdr.Type = ICMP_ECHOREQ;
echoReq.icmpHdr.Code = 0;
echoReq.icmpHdr.Checksum = 0;
echoReq.icmpHdr.ID = (USHORT)GetCurrentThreadId();
echoReq.icmpHdr.Seq = (USHORT)GetCurrentProcessId();
// Fill in some data to send
for (nRet = 0;
nRet < nData;
nRet++)
cData[nRet] = ' '+nRet;
echoReq.cData=cData;
// Save tick count when sent
echoReq.dwTime = GetTickCount();
// Put data in packet and compute checksum
echoReq.icmpHdr.Checksum = in_cksum((u_short *)&amp;echoReq, sizeof(ECHOREQUEST));
// Send the echo request
nRet = sendto(s, /* socket */
(LPSTR)&amp;echoReq, /* buffer */
sizeof(ECHOREQUEST),
0, /* flags */
(LPSOCKADDR)lpstToAddr, /* destination */
sizeof(SOCKADDR_IN));
/* address length */
free(cData);
if (nRet == SOCKET_ERROR)
return -1;
return (nRet);
}

DWORD CPing::RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, unsigned char *pTTL)
{
ECHOREPLY echoReply;
int nRet;
int nAddrLen = sizeof(struct sockaddr_in);
// Receive the echo reply
nRet = recvfrom(s, // socket
(LPSTR)&amp;echoReply, // buffer
sizeof(ECHOREPLY), // size of buffer
0, // flags
(LPSOCKADDR)lpsaFrom, // From address
&amp;nAddrLen); // pointer to address len
// Check return value
if (nRet == SOCKET_ERROR)
return 0;
if (echoReply.echoRequest.icmpHdr.ID!=(USHORT)GetCurrentThreadId())
return 0;
if (echoReply.echoRequest.icmpHdr.Seq!=(USHORT)GetCurrentProcessId())
return 0;
// return time sent and IP TTL
*pTTL = echoReply.ipHdr.TTL;
return(echoReply.echoRequest.dwTime);

}

int CPing::OpenRawSocket()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2,2), &amp;wsaData) != 0)
{
return -1;
// WSAStartup error!
}
// Create a Raw socket
rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawSocket == SOCKET_ERROR)
{
return 0;
}
return 1;
}
int CPing::CloseRawSocket()
{
int nRet;
if (rawSocket != INVALID_SOCKET)
nRet=closesocket(rawSocket);


if (nRet == SOCKET_ERROR)
return 0;
WSACleanup();
return 1;
}
int CPing::IcmpPing(LPCSTR pstrHost,int nTimeOut,int nData, char szResult[256])
{
PHOSTENT lpHost;
int nRet;
struct sockaddr_in saDest;
struct sockaddr_in saSrc;
DWORD dwTimeSent;
DWORD dwElapsed;
unsigned char cTTL;

if (rawSocket == INVALID_SOCKET)
return -1;
if (nData<=0)
nData=REQ_DATASIZE;
if (nTimeOut<=0)
nTimeOut=1000;

/*
nRet = setsockopt(rawSocket,
SOL_SOCKET,
SO_SNDTIMEO,
(char*)&amp;nTimeOut,
sizeof(nTimeOut));
if(nRet == SOCKET_ERROR)
{
return -1;
}
*/
// Lookup host
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
return -1;
}

// Setup destination socket address
saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;
// Send ICMP echo request
SendEchoRequest(rawSocket, &amp;saDest,nData);
int nTimeOutTemp=nTimeOut;
struct timeval Timeout;
fd_set readfds;
readfds.fd_count = 1;
readfds.fd_array[0] = rawSocket;

while(nTimeOutTemp>0)
{
Timeout.tv_sec = nTimeOutTemp/1000;
Timeout.tv_usec = nTimeOutTemp;

dwElapsed=GetTickCount();
nRet=select(1, &amp;readfds, NULL, NULL, &amp;Timeout);
if (nRet == SOCKET_ERROR)
{
return -1;
}
if (!nRet)
{
//Request time out
break;
}
else
{
// Receive reply
dwTimeSent = RecvEchoReply(rawSocket, &amp;saSrc, &amp;cTTL);
dwElapsed = GetTickCount() - dwElapsed;
if (dwTimeSent>0)
{
sprintf(szResult,"Reply from: %s: bytes=%d time=%ldms TTL=%d",
inet_ntoa(saSrc.sin_addr),
REQ_DATASIZE,
GetTickCount()-dwTimeSent,
cTTL);
return 1;
}
else
{
nTimeOutTemp=nTimeOutTemp-dwElapsed;
continue;
}
}
}
sprintf(szResult,"Request timed out!");
return 0;
}
int CPing::Ping(LPCSTR pstrHost,int nRetries,int nTimeOut)
{
SOCKET rawIcmpSocket;
LPHOSTENT lpHost;
int nLoop;
int nRet;
struct sockaddr_in saDest;
struct sockaddr_in saSrc;
DWORD dwTimeSent;
DWORD dwElapsed;
unsigned char cTTL;

int nData=REQ_DATASIZE;
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2,2), &amp;wsaData) != 0)
{
return -1;
// WSAStartup error!
}
if (nTimeOut<=0)
nTimeOut=1000;
// Create a Raw socket
rawIcmpSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawIcmpSocket == INVALID_SOCKET)
{
WSACleanup();
return -1;
}
/*
nRet = setsockopt(rawIcmpSocket,
SOL_SOCKET,
SO_SNDTIMEO,
(char*)&amp;nTimeOut,
sizeof(nTimeOut));
if(nRet == SOCKET_ERROR)
{
return -1;
}
*/
// Lookup host
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
closesocket(rawIcmpSocket);
WSACleanup();
return -1;
}

// Setup destination socket address
saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;
// Ping multiple times
for (nLoop = 0;
nLoop < nRetries;
nLoop++)
{
// Send ICMP echo request
SendEchoRequest(rawIcmpSocket, &amp;saDest,nData);
struct timeval Timeout;
fd_set readfds;
readfds.fd_count = 1;
readfds.fd_array[0] = rawIcmpSocket;
int nTimeOutTemp=nTimeOut;
while(nTimeOutTemp>0)
{
Timeout.tv_sec = nTimeOutTemp/1000;
Timeout.tv_usec = nTimeOutTemp;
dwElapsed=GetTickCount();
nRet=select(1, &amp;readfds, NULL, NULL, &amp;Timeout);

if (nRet == SOCKET_ERROR)
{
closesocket(rawIcmpSocket);
WSACleanup();
return -1;
}
if (!nRet)
{
//Request time out
break;
}
else
{
// Receive reply
dwTimeSent = RecvEchoReply(rawIcmpSocket, &amp;saSrc, &amp;cTTL);
dwElapsed = GetTickCount() - dwElapsed;

if (dwTimeSent>0)
{
closesocket(rawIcmpSocket);
WSACleanup();
return 1;
}
else
{
nTimeOutTemp=nTimeOutTemp-dwElapsed;
continue;
}
}
}
}
closesocket(rawIcmpSocket);
WSACleanup();
return 0;
}

/*
* Class: com_winPing
* Method: ping
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2
(JNIEnv * env, jobject obj, jstring IP)
{
CPing ping;
const char* szIP=env->GetStringUTFChars(IP,0);
return ping.Ping((LPCSTR)szIP,3,1000);
}
/*
* Class: com_dcnetworks_protocol_icmp_IcmpPing
* Method: simplePing
* Signature: (Ljava/lang/String;II)I
*/

JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2II
(JNIEnv* env, jobject obj, jstring IP, jint retrys, jint timeout)
{
if (retrys<1)
retrys=3;
if (timeout<1)
timeout=1000;
CPing ping;
const char* szIP=env->GetStringUTFChars(IP,0);
return ping.Ping((LPCSTR)szIP,(int)retrys,(int)timeout);
}
 
这是它的两个.h文件《ping.h》
#include <winsock2.h>
#define ICMP_ECHOREPLY 0
#define ICMP_ECHOREQ 8
#define REQ_DATASIZE 32 // Echo Request Data size

class CPing
{
private:
SOCKET rawSocket;
public:
int OpenRawSocket();
int CloseRawSocket();
int IcmpPing(LPCSTR pstrHost,int nTimeOut,int nData, char szResult[256]);
int Ping(LPCSTR pstrHost,int nRetries,int nTimeOut);

private:

unsigned short in_cksum(unsigned short *addr, int len);
int SendEchoRequest(SOCKET ,LPSOCKADDR_IN ,int );
DWORD RecvEchoReply(SOCKET , LPSOCKADDR_IN , unsigned char *);
int WaitForEchoReply(SOCKET s,int nTimeOut);
};
// IP Header -- RFC 791
typedef struct tagIPHDR
{
unsigned char VIHL; // Version and IHL
unsigned char TOS; // Type Of Service
short TotLen; // Total Length
short ID; // Identification
short FlagOff; // Flags and Fragment Offset
unsigned char TTL; // Time To Live
unsigned char Protocol; // Protocol
unsigned short Checksum; // Checksum
struct in_addr iaSrc; // Internet Address - Source
struct in_addr iaDst; // Internet Address - Destination
}IPHDR, *PIPHDR;
////////////////////////com_adminet_protocol_icmp_winPing.h
// ICMP Header - RFC 792
typedef struct tagICMPHDR
{
unsigned char Type; // Type
unsigned char Code; // Code
unsigned short Checksum; // Checksum
unsigned short ID; // Identification
unsigned short Seq; // Sequence
char Data; // Data
}ICMPHDR, *PICMPHDR;

// ICMP Echo Request
typedef struct tagECHOREQUEST
{
ICMPHDR icmpHdr;
DWORD dwTime;
char* cData;
}ECHOREQUEST, *PECHOREQUEST;

// ICMP Echo Reply
typedef struct tagECHOREPLY
{
IPHDR ipHdr;
ECHOREQUEST echoRequest;
char cFiller[256];
}ECHOREPLY, *PECHOREPLY;

/*do
NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_adminet_protocol_icmp_winPing */
#ifndef _Included_com_adminet_protocol_icmp_winPing
#define _Included_com_adminet_protocol_icmp_winPing
#ifdef __cplusplus
extern "C" {
#endif
#undef com_adminet_protocol_icmp_winPing_OK
#define com_adminet_protocol_icmp_winPing_OK 1L
/*
* Class: com_adminet_protocol_icmp_winPing
* Method: ping
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2
(JNIEnv *, jobject, jstring);
/*
* Class: com_adminet_protocol_icmp_winPing
* Method: ping
* Signature: (Ljava/lang/String;II)I
*/
JNIEXPORT jint JNICALL Java_com_adminet_protocol_icmp_winPing_ping__Ljava_lang_String_2II
(JNIEnv *, jobject, jstring, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
 
找一本基础c语言教程学习一下吧,
心急吃不了热豆腐
 
真是佩服呀,把Java里面的代码拿出来研究了。
Java代码中其实没有对底层数据封包操作的方法,因此它必须通过C来实现,最后再通过原生接口进行调用。对于楼主贴的第二部分代码,个人建议不要过分研究,因为它本身有协议进行描述的。
对于ICMP协议,在TCP网络教程中有一定的描述,通过构造TCP头部数据实现ICMP协议。目前在TCPIP协议详解卷二第11章有它的代码描述。
不过楼主的C语言确实需要学习下,否则拿着书能够看明白流程,但不明白如何实现也不行。
 
后退
顶部