什么是KCP_Hello,C++!的博客-kcp是什么意思

最后更新 :2023-02-23 06:01:53

一、什么是KCP KCP是一种网络传输协议(A Fast and Reliable ARQ Protocol),可以视它为TCP的代替品,但是它运行于用户空间,它不管底层的发送与接收,只是个纯算法实现可靠传输,它的特点是牺牲带宽来降低延迟。因为TCP协议的大公无私,经常牺牲自己速度来减少网络拥塞,它是从大局上考虑的。而KCP是自私的,它只顾自己的传输效率,从不管整个网络的拥塞情况。举个例子,TCP检测到丢包的时候,首先想到的是网络拥塞了,要放慢自己的速度别让网络更糟,而KCP想到的赶紧重传别耽误事。

TCP的特点是可靠传输(累积确认、超时重传、选择确认)、流量控制(滑动窗口)、拥塞控制(慢开始、拥塞避免、快重传、快恢复)、面向连接。KCP对这些参数基本都可配,也没用建立/关闭连接的过程。

其实KCP并不神秘,因为TCP的高度自治(很多东西都不可配),满足不了如今各种速度需求。而KCP就是基于UDP协议,再将一些TCP经典的机制移植过来,变成参数可配。

二、技术特性 RTO翻倍vs不翻倍:TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。 选择性重传 vs 全部重传:TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。 快速重传:发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。 延迟ACK vs 非延迟ACK :TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大RTT时间,延长了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。 UNA vs ACK+UNA :ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),KCP有单独ACK,且数据包和ACK包都带UNA信息,有效降低ACK丢失成本。 非退让流控:KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

三、怎么使用 KCP只有两个文件,分别是ikcp.c和ikcp.h,代码行数1300左右。使用KCP和使用TCP有些不同,所以上手之前需要先了解下KCP如何使用,需要时间成本。

第一步,就是创建一个kcp实例,相当于一个句柄。

ikcpcb* ikcp_create(IUINT32 conv, void *user)

第二步,设置发送数据的接口,底层用哪种socket都没问题,只要能把数据发送出去,建议使用UDP,比较简单。

int output(const char *buf, int len, ikcpcb *kcp, void *user)

第三步,更新KCP状态。KCP运行于用户空间,所以需要手动去更新每个实例的状态,其实主要就是检测哪些数据包该重传了。

void ikcp_update(ikcpcb *kcp, IUINT32 current)

第四步,发送数据。调用ikcp_send之后,KCP最后会使用上面设置的output函数来将发送数据(KCP自己并不关心如何发送数据)。

int ikcp_send(ikcpcb *kcp, const char *buffer, int len)

第五步,预接收数据。先手动预接收数据,然后再调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。

int ikcp_input(ikcpcb *kcp, const char *data, long size)

第六步,接收数据。此时收到的数据才是真正的数据,重组操作在调用ikcp_recv之前就完成了。

int ikcp_recv(ikcpcb *kcp, char *buffer, int len)

总体上还是容易理解的,以前我们是直接使用各种socket和对端通信,各种功能由自己控制。现在是在socket之上使用了一个中间件KCP,帮忙实现快速可靠传输功能。注意一下KCP有模式的区分,不同模式下的速度表现不一样,建议把参数配好之后再使用,否则使用的都是默认的参数。

四、协议配置 协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:

工作模式

int ikcp_nodelay(ikcpcb *kcp,int nodelay,int interval,int resend,int nc);

nodelay :是否启用 nodelay模式,0不启用;1启用。 interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传) nc :是否关闭流控,默认是0代表不关闭,1带包关闭。 普通模式:`ikcp_nodelay(kcp, 0, 40, 0, 0); 极速模式: ikcp_nodelay(kcp, 0, 10, 2, 1);

最大窗口:int ikcp_wndsize(ikcpcb *kcp,int sndwnd,int rcvwnd);该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32。

最大传输单元:纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用 ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。

最小RTO:不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下该值为30ms,可以手动更改该值:kcp->rx_minrto =10;

内存分配器 默认KCP协议使用 malloc/free进行内存分配释放,如果应用层接管了内存分配,可以用ikcp_allocator来设置新的内存分配器,注意要在一开始设置:ikcp_allocator(my_new_malloc, my_new_free);

前向纠错注意 为了进一步提高传输速度,下层协议也许会使用前向纠错技术。需要注意,前向纠错会根据冗余信息解出原始数据包。相同的原始数据包不要两次input到KCP,否则将会导致kcp以为对方重发了,这样会产生更多的ack占用额外带宽。

比如下层协议使用最简单的冗余包:单个数据包除了自己外,还会重复存储一次上一个数据包,以及上上一个数据包的内容:

Fn=(Pn,Pn-1,Pn-2)
P0 =(0, X, X)
P1 =(1,0, X)
P2 =(2,1,0)
P3 =(3,2,1)

这样几个包发送出去,接收方对于单个原始包都可能被解出3次来(后面两个包任然会重复该包内容),那么这里需要记录一下,一个下层数据包只会input给kcp一次,避免过多重复ack带来的浪费。

五、快在哪里 没用使用任何系统调用接口 无需建立/关闭连接(就KCP本身来说) 很多影响速度的参数都可配

六、使用场景 丢包率高的网络环境下KCP的优点才会显示出来。如果不丢包,那么TCP和KCP的效率不会差别很大,可能就是少了连接建立/关闭而已。一般来讲,在公网上传输的都可以使用,特别是对实时性要求较高的程序,如LOL。

KCP demo

kcpClient.c

#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "ikcp.c"

#include <sys/time.h>
#include <sys/wait.h>
#include <arpa/inet.h>

//=====================================================================
//
// kcp demo
//
// 说明:
// ikcp_input、ikcp_send返回值0 为正常
// 前期通信异常原因:
// 1.sendto(send->sockfd, buf, len, 0, (struct sockaddr *)&send->CientAddr,sizeof(struct sockaddr_in));
	// 参数buf 写成send->buff
// 2.ret = ikcp_send(send->pkcp, send->buff,sizeof(send->buff) );//第三个参数 正确应该是strlen(send->buff)+1
//   send->buff实际长度14字节,却传输512字节,后面全是0,kcp对这512字节数据全部进行封包,由UDP发送,导致kcp  input处理出错
//  send中的 buff[488]最大长度为488,传输正常,(488+24=512)[实际长度488+24kcp头部],感觉kcp_input处理超过512的数据就出错
//=====================================================================

static int number = 0;

 typedef struct {
	unsigned char *ipstr;
	int port;
	
	ikcpcb *pkcp;
	
	int sockfd;
	struct sockaddr_in addr;//存放服务器的结构体
	
	char buff[488];//存放收发的消息
	
}kcpObj;


/* get system time */
void itimeofday(long *sec, long *usec)
{
	#if defined(__unix)
	struct timeval time;
	gettimeofday(&time, NULL);
	if (sec) *sec = time.tv_sec;
	if (usec) *usec = time.tv_usec;
	#else
	static long mode = 0, addsec = 0;
	BOOL retval;
	static IINT64 freq = 1;
	IINT64 qpc;
	if (mode == 0) {
		retval = QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
		freq = (freq == 0)? 1 : freq;
		retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
		addsec = (long)time(NULL);
		addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
		mode = 1;
	}
	retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
	retval = retval * 2;
	if (sec) *sec = (long)(qpc / freq) + addsec;
	if (usec) *usec = (long)((qpc % freq) * 1000000 / freq);
	#endif
}

/* get clock in millisecond 64 */
IINT64 iclock64(void)
{
	long s, u;
	IINT64 value;
	itimeofday(&s, &u);
	value = ((IINT64)s) * 1000 + (u / 1000);
	return value;
}

IUINT32 iclock()
{
	return (IUINT32)(iclock64() & 0xfffffffful);
}

/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
	#ifdef __unix 	/* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
	#elif defined(_WIN32)
	Sleep(millisecond);
	#endif
}



int udpOutPut(const char *buf, int len, ikcpcb *kcp, void *user){
   
 //  printf("使用udpOutPut发送数据\n");
   
    kcpObj *send = (kcpObj *)user;

	//发送信息
    int n = sendto(send->sockfd, buf, len, 0,(struct sockaddr *) &send->addr,sizeof(struct sockaddr_in));//【】
    if (n >= 0) 
	{       
		//会重复发送,因此牺牲带宽
		//printf("udpOutPut-send: 字节 =%d bytes   内容=[%s]\n", n ,buf+24);//24字节的KCP头部
        return n;
    } 
	else 
	{
        printf("udpOutPut: %d bytes send, error\n", n);
        return -1;
    }
}


int init(kcpObj *send)
{	
	send->sockfd = socket(AF_INET,SOCK_DGRAM,0);
	
	if(send->sockfd < 0)
	{
		perror("socket error!");
		exit(1);
	}
	
	bzero(&send->addr, sizeof(send->addr));
	
	//设置服务器ip、port
	send->addr.sin_family=AF_INET;
    send->addr.sin_addr.s_addr = inet_addr((char*)send->ipstr);
    send->addr.sin_port = htons(send->port);
	
	printf("sockfd = %d ip = %s  port = %d\n",send->sockfd,send->ipstr,send->port);
	
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n,ret;

	while(1)
	{
		isleep(1);
		
		//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用

		char buf[512]={0};

		//处理收消息
		n = recvfrom(send->sockfd,buf,512,MSG_DONTWAIT,(struct sockaddr *) &send->addr,&len);
	
		if(n < 0)//检测是否有UDP数据包
			continue;
			
	printf("UDP接收到数据包  大小= %d   buf =%s\n",n,buf+24);		
		
		//预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。 
		//kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式
		ret = ikcp_input(send->pkcp, buf, n);	
		if(ret < 0)//检测ikcp_input是否提取到真正的数据
		{
			//printf("ikcp_input ret = %d\n",ret);
			continue;			
		}
		
//		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
		
		while(1)
		{	
			//kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据		
			ret = ikcp_recv(send->pkcp, buf, n);		
			if(ret < 0)//检测ikcp_recv提取到的数据	
			{
				//printf("ikcp_recv ret = %d\n",ret);
				break;
			}
		}
		
		printf("数据交互  ip = %s  port = %d\n",inet_ntoa(send->addr.sin_addr),ntohs(send->addr.sin_port));	
		
		if(strcmp(buf,"Conn-OK") == 0)
		{
			//kcp收到确认连接包,则进行交互
			printf("Data from Server-> %s\n",buf);	
			//把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。
			ret = ikcp_send(send->pkcp, send->buff,sizeof(send->buff) );//strlen(send->buff)+1
			printf("Client reply -> 内容[%s] 字节[%d]  ret = %d\n",send->buff,(int)sizeof(send->buff),ret);//发送成功的
			number++;
			printf("第[%d]次发\n",number);					
		}
		
		//发消息
		if(strcmp(buf,"Server:Hello!") == 0)
		{		
			//kcp收到确认连接包,则进行交互
			printf("Data from Server-> %s\n",buf);
			
			//kcp收到交互包,则回复	
			//把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。
			ikcp_send(send->pkcp, send->buff, sizeof(send->buff));				
			number++;		
			printf("第[%d]次发\n",number);
		}	
	}
	
}

int main(int argc,char *argv[])
{
	//printf("this is kcpClient,请输入服务器 ip地址和端口号:\n");
	if(argc != 3)
	{
		printf("请输入服务器ip地址和端口号\n");
		return -1;
	}
	printf("this is kcpClient\n");
	
	unsigned char *ipstr = (unsigned char *)argv[1];
	unsigned char *port  = (unsigned char *)argv[2];
	
	kcpObj send;
	send.ipstr = ipstr;
	send.port = atoi(argv[2]);
	
	init(&send);//初始化send,主要是设置与服务器通信的套接字对象
	
	bzero(send.buff,sizeof(send.buff));
	char Msg[] = "Client:Hello!";//与服务器后续交互	
	memcpy(send.buff,Msg,sizeof(Msg));
	
	ikcpcb *kcp = ikcp_create(0x1, (void *)&send);//创建kcp对象把send传给kcp的user变量
	kcp->output = udpOutPut;//设置kcp对象的回调函数
	ikcp_nodelay(kcp,0, 10, 0, 0);//(kcp1, 0, 10, 0, 0); 1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);
	
	send.pkcp = kcp;
	
	char temp[] = "Conn";//与服务器初次通信		
	int	ret = ikcp_send(send.pkcp,temp,(int)sizeof(temp)); 
		printf("ikcp_send发送连接请求: [%s] len=%d ret = %d\n",temp,(int)sizeof(temp),ret);//发送成功的
		

	loop(&send);//循环处理
	
	return 0;	
}

kcpServer.c

#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "ikcp.c"

#include <string.h>

#include <sys/time.h>
#include <sys/wait.h>
#include <arpa/inet.h>


//=====================================================================
//
// kcp demo
//
// 说明:
// ikcp_input、ikcp_send返回值0 为正常
// 前期通信异常原因:
// 1.sendto(send->sockfd, buf, len, 0, (struct sockaddr *)&send->CientAddr,sizeof(struct sockaddr_in));
	// 参数buf 写成send->buff
// 2.ret = ikcp_send(send->pkcp, send->buff,sizeof(send->buff) );//第三个参数 正确应该是strlen(send->buff)+1
//   send->buff实际长度14字节,却传输512字节,后面全是0,kcp对这512字节数据全部进行封包,由UDP发送,导致kcp  input处理出错
//  send中的 buff[488]最大长度为488,传输正常,(488+24=512)[实际长度488+24kcp头部],感觉kcp_input处理超过512的数据就出错
//=====================================================================

static int number = 0;

 typedef struct {
	unsigned char *ipstr;
	int port;
	
	ikcpcb *pkcp;
	
	int sockfd;
	
	struct sockaddr_in addr;//存放服务器信息的结构体
	struct sockaddr_in CientAddr;//存放客户机信息的结构体
	
	char buff[488];//存放收发的消息
	
}kcpObj;


/* get system time */
void itimeofday(long *sec, long *usec)
{
	#if defined(__unix)
	struct timeval time;
	gettimeofday(&time, NULL);
	if (sec) *sec = time.tv_sec;
	if (usec) *usec = time.tv_usec;
	#else
	static long mode = 0, addsec = 0;
	BOOL retval;
	static IINT64 freq = 1;
	IINT64 qpc;
	if (mode == 0) {
		retval = QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
		freq = (freq == 0)? 1 : freq;
		retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
		addsec = (long)time(NULL);
		addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
		mode = 1;
	}
	retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
	retval = retval * 2;
	if (sec) *sec = (long)(qpc / freq) + addsec;
	if (usec) *usec = (long)((qpc % freq) * 1000000 / freq);
	#endif
}

/* get clock in millisecond 64 */
IINT64 iclock64(void)
{
	long s, u;
	IINT64 value;
	itimeofday(&s, &u);
	value = ((IINT64)s) * 1000 + (u / 1000);
	return value;
}

IUINT32 iclock()
{
	return (IUINT32)(iclock64() & 0xfffffffful);
}

/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
	#ifdef __unix 	/* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
	#elif defined(_WIN32)
	Sleep(millisecond);
	#endif
}


int udpOutPut(const char *buf, int len, ikcpcb *kcp, void *user){
   
    kcpObj *send = (kcpObj *)user;

	//发送信息
    int n = sendto(send->sockfd, buf, len, 0, (struct sockaddr *)&send->CientAddr,sizeof(struct sockaddr_in));
    if (n >= 0)       
   	{       
		//会重复发送,因此牺牲带宽
		printf("udpOutPut-send: 字节 =%d bytes   内容=[%s]\n", n ,buf+24);//24字节的KCP头部
        return n;
    } 
	else 
	{
        printf("error: %d bytes send, error\n", n);
        return -1;
    }
}


int init(kcpObj *send)
{	
	send->sockfd = socket(AF_INET,SOCK_DGRAM,0);
	
	if(send->sockfd<0)
	{
		perror("socket error!");
		exit(1);
	}
	
	bzero(&send->addr, sizeof(send->addr));
	
	send->addr.sin_family = AF_INET;
	send->addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY
	send->addr.sin_port = htons(send->port);
		
	printf("服务器socket: %d  port:%d\n",send->sockfd,send->port);
	
	if(send->sockfd<0){
		perror("socket error!");
		exit(1);
	}
	
	if(bind(send->sockfd,(struct sockaddr *)&(send->addr),sizeof(struct sockaddr_in))<0)
	{
		perror("bind");
		exit(1);
	}
	
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n,ret;	
	//接收到第一个包就开始循环处理
	
	while(1)
	{
		isleep(1);		
		ikcp_update(send->pkcp,iclock());
		
		char buf[1024]={0};
	
		//处理收消息
    	n = recvfrom(send->sockfd,buf,512,MSG_DONTWAIT,(struct sockaddr *)&send->CientAddr,&len);			
		
		if(n < 0)//检测是否有UDP数据包: kcp头部+data
			continue;
		
printf("UDP接收到数据包  大小= %d   \n",n);
	
		//预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。 
		//kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式
		ret = ikcp_input(send->pkcp, buf, n);
		
		// if(ret < 0)//检测ikcp_input对 buf 是否提取到真正的数据	
		// {	
			// printf("ikcp_input error ret = %d\n",ret);
			// continue;
		// }

		while(1)	
		{			
			//kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据		
			ret = ikcp_recv(send->pkcp, buf, n);//从 buf中 提取真正数据,返回提取到的数据大小
			if(ret < 0)//检测ikcp_recv提取到的数据	
				break;
		}

		printf("数据交互  ip = %s  port = %d\n",inet_ntoa(send->CientAddr.sin_addr),ntohs(send->CientAddr.sin_port));
		
		//发消息
		if(strcmp(buf,"Conn") == 0)
		{
			//kcp提取到真正的数据	
			printf("[Conn]  Data from Client-> %s\n",buf);
			
			//kcp收到连接请求包,则回复确认连接包		
			char temp[] = "Conn-OK";	
			
			//ikcp_send只是把数据存入发送队列,没有对数据加封kcp头部数据
			//应该是在kcp_update里面加封kcp头部数据
			//ikcp_send把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。
			ret = ikcp_send(send->pkcp,temp,(int)sizeof(temp));			
			printf("Server reply -> 内容[%s] 字节[%d] ret = %d\n",temp,(int)sizeof(temp),ret);
			
			number++;
			printf("第[%d]次发\n",number);			
		}	
		
		if(strcmp(buf,"Client:Hello!") == 0)
		{				
			//kcp提取到真正的数据	
			printf("[Hello]  Data from Client-> %s\n",buf);
			//kcp收到交互包,则回复			
			ikcp_send(send->pkcp, send->buff,sizeof(send->buff));				
			number++;		
			printf("第[%d]次发\n",number);
		}


	}	
}

int main(int argc,char *argv[])
{
	printf("this is kcpServer\n");
	if(argc <2 )
	{
		printf("请输入服务器端口号\n");
		return -1;
	}
	
	kcpObj send;
	send.port = atoi(argv[1]);
	send.pkcp = NULL;
	
	bzero(send.buff,sizeof(send.buff));
	char Msg[] = "Server:Hello!";//与客户机后续交互	
	memcpy(send.buff,Msg,sizeof(Msg));

	ikcpcb *kcp = ikcp_create(0x1, (void *)&send);//创建kcp对象把send传给kcp的user变量
	kcp->output = udpOutPut;//设置kcp对象的回调函数
	ikcp_nodelay(kcp, 0, 10, 0, 0);//1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);
	
	send.pkcp = kcp;

	init(&send);//服务器初始化套接字
	loop(&send);//循环处理
	
	return 0;	
}

在这里插入图片描述

- END -

原来廖凡也是“背景王”,父亲是著名老戏骨,妻子让周星驰都佩服_作品_演技_廖凡父亲

原标题:原来廖凡也是“背景王”,父亲是著名老戏骨,妻子让周星驰都佩服跟大家说到影帝廖凡你...

中国面积最大的火车站前十:广州南站面积最大(61.5万平方米)_

中国地广物博,关于各种基础设施的建设也是最为完备的,而在一些建筑上,也是足够展现大国风采...