查看TCP网络服务器情况和端口使用情况
[zsh@ecs-78471 vscode]$ netstat -nltp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 ::1:25 :::* LISTEN -
[zsh@ecs-78471 vscode]$
一.TCP套接字接口
1.inet_aton (和inet_addr一样,换一种方式而已)
int inet_aton(const char *cp, struct in_addr *inp);(address to net 本地字符串风格IP转网络4字节IP)cp:字符串风格IP地址。inp:转换后的存到inp中。
返回值:成功返回1;失败返回0;
注意:这类函数在转变IP风格时都会自动进行主机字节序和网络字节序之间的转换。
例:
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (
inet_aton(ip_.c_str(), &local.sin_addr));
2.listen——把套接字设置为监听状态
监听socket,为何要监听呢?——因为tcp是面向连接的!(面向 的解释:做任何工作前先做什么就是面向什么,比如面向对象就是进行任何工作前先定义对象)
面向连接:就是进行任何工作前先建立连接,让服务器在任何时候可以被客户端去连接。为了面向连接需要把套接字设置为监听状态。
将socket套接字设置为监听状态,使得套接字在任何时候都可以随时被客户端连接
man 3 listen
int listen(int socket, int backlog);
socket:要设置的文件描述符。backlog:后面再说,现在随便写,比如5。
返回值:成功返回0,失败返回-1 错误码被设置。
3.服务器获取客户端的连接 accept
man 2 accept
** int accept(int sockfd, struct sockaddr addr, socklen_t addrlen);
服务器通过特定套接字sockfd获取连接
sockfd:文件描述符。src_addr和addrlen这俩参数和recvfrom后面俩参数一模一样(客户端套接字):
addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入addr中。(addr的类型是套接字类型指针struct sockaddr,传入的网络套接字类型struct sockaddr_in需要强转成此类型指针 struct sockaddr*。)
addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)
返回值:成功返回一个新的socketfd,错误就返回-1错误码被设置。
返回值中套接字和参数中套接字的作用:
**int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); **
小故事:景点餐厅会雇一个专门迎客的人(张三),当张三把客人引进餐厅,就由服务员服务客人了。张三就走了,接着去迎客。*张三就是***int sockfd *参数中套接字-监听套接字 listensocke_,服务员就是*accept**返回值的套接字 serviceSock
监听套接字 listensocke_:参数中套接字的核心工作:获取新的连接(当然,一定是有客户来连接了! )
serviceSock:返回值中套接字的核心工作:主要是为用户提供网络服务的socket,主要是为客户提供IO服务
4.用到的部分函数
(1)strcasecmp —— 比较两个字符串,但会忽略大小写
比较两个字符串,但会忽略大小写
如果发现s1(或其前n个字节)小于s2,则返回一个小于0的整数;s1等于s2,则返回0;s1大于s2,则返回大于0的整数。
(2)int isalpha(int c); ——是否是字母,是就返回1,不是就返回0。
(3)int islower(int c);——是否是小写字母,是就返回1,不是就返回0。
(4)int toupper(int c);——转换为大写字母。返回值是转换后的字母的值,如果无法进行转换,则为c。
(5)strcasecmp
比较了两个字符串s1和s2,忽略了字符的大小写。如果发现s1分别小于、匹配或大于s2,则它返回一个小于、等于或大于0的整数。
if (strcasecmp(message.c_str(), "quit") == 0)
quit = true;
返回值: 如果发现s1(或其前n个字节)小于、匹配或大于s2,则分别返回一个小于、等于或大小大于0的整数。
5.客户端发起连接 connect
connect 作用:通过客户端指定的流式套接字sockfd,向服务器发起链接请求(先填充需要连接的远端主机的基本信息)
*int connect(int sockfd, const struct sockaddr addr,socklen_t addrlen);
sockfd:流式套接字。src_addr和addrlen这俩参数和sendto后面俩参数一模一样
addr:(输入型参数)向哪个主机发消息,套接字类型指针struct sockaddr,传入的网络套接字类型struct sockaddr需要强转成此类型指针 struct sockaddr*。
addrlen:(输入型参数)主机这个缓冲区大小。(socklen t就是unsigned int)
返回值:发送连接成功返回0,连接失败就返回-1错误码被设置
(首次调用sendto 或 connect的时候 都会自动帮我们进行bind,client会自动bind自己的ip和port)
二.TCP套接字代码
1.注意事项
(1)因为TCP是流式类型套接字,所以利用read和write进行读取和写入数据。
(2)①客户端操作系统会自动bind,但是不需要自己显示的bind。
②需要listen吗?不需要的!监听本身就是等待别的客户端去连接你,所以客户端本身不需要设置监听状态,因为没人去连你的客户端
③需要accept吗?不需要的,都无法设置监听,就更不需要获取连接。
(3)TCP服务器的工作:①创建套接字。②bind绑定。③设置套接字监听状态。④服务器获取连接 accept。⑤获取客户端基本信息。⑥提供服务,读取内容后完成转换写回。
TCP客户端的工作:①创建套接字。② connect 填充服务器信息后向服务器发起链接请求。③写入数据后读出服务器转化的数据
(4)易错:服务器中
①accpet失败日志设置warnning,并continue重新获取客户端的连接
③memset(&local,0,sizeof(local));漏写(不写也行,但是写了给自己带来确定性,更好一些)
②inbuffer[s]='\0'; 漏写。
客户端中:
①volatile bool quit 漏写
② message.clear(); 和 message.resize(1024); 漏写。
2.封装服务为多进程,多线程版本
我们封装服务为多进程,多线程版本的目的:多个客户端访问时,让第一个客户端访服务器时,服务器上通过子进程为客户端提供服务,然后父进程就可以继续while循环,进行下一次阻塞式获取下一个客户端的链接并为他提供服务,是并发是进行的,
小提示:为什么不用waitpid()waitpid(); 默认是阻塞等待!我们本身就是追求多进程并发,阻塞相当于还是串行了,所以我们不能用waitpid()。那WNOHANG可以吗?——答:可以是可以,但是很麻烦,需要把各个子进程的pid保存进一个vector中,每次非阻塞等待需要轮询检测子进程pid看子进程是否退出,很麻烦,我们不选择这种方法。
(1)单进程(原始版本)
只能给一个客户提供服务,当为一个客户提供服务进入transService后,transService是死循环,除非提供完毕,否则函数不返回,则主执行流无法继续为其他客户提供服务
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服务, echo -> 小写 -> 大写
// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
transService(serviceSock, peerIp, peerPort);
close(serviceSock); //这一步是一定要做的!
}
}
(2)多进程版本1
利用signal(SIGCHLD, SIG_IGN); 父进程调用signal/sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
注意:①父进程打开的文件会被子进程继承,所以子进程中本身用不到“接客”的listenSock_,所以建议关掉此文件描述符。close(listenSock_); //建议(类似管道关闭不需要的读写端一样)
②父进程accept创建的提供服务的文件描述符serviceSock就是让子进程继承使用的,那么子进程已经继承serviceSock后,父进程就用不到了,就需要关闭父进程对应的serviceSock。close(serviceSock); //这一步是一定要做的!(类似管道关闭不需要的读写端一样)
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服务, echo -> 小写 -> 大写
// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
// transService(serviceSock, peerIp, peerPort);
// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
close(listenSock_); //建议
//子进程
transService(serviceSock, peerIp, peerPort);
exit(0); // 进入僵尸
}
// 父进程
close(serviceSock); //这一步是一定要做的!
}
}
监控脚本:
(3)多进程版本2
通过创建孙子进程,孙子进程的爸爸直接终止,所以孙子进程是孤儿进程,孙子进程被系统领养,他的回收问题就交给了系统来回收。而爸爸进程通过爷爷进程来阻塞等待释放。
void loop()
{
// signal(SIGCHLD, SIG_IGN); // only Linux
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5.1 v1.1 版本 -- 多进程版本 -- 也是可以的
//爷爷进程
pid_t id = fork();
if(id == 0)
{
// 爸爸进程
close(listenSock_);//建议
// 又进行了一次fork,让 爸爸进程直接终止
if(fork() > 0) exit(0);
// 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
transService(serviceSock, peerIp, peerPort);
exit(0);
}
// 父进程
close(serviceSock); //这一步是一定要做的!
// 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
assert(ret > 0);
(void)ret;
}
}
(4)多线程版本
利用多线程去服务客户,首先创造一个ThreadData类,方便函数方法调用transService传参。
class ServerTcp; // 申明一下ServerTcp
class ThreadData
{
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
ServerTcp *this_;
public:
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clinetIp_(ip), sock_(sock),this_(ts)
{}
};
————————上面是类外,下面是类内
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); //设置线程分离
ThreadData *td = static_cast<ThreadData*>(args);
td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
delete td;
return nullptr;
}
void loop()
{
// signal(SIGCHLD, SIG_IGN); // only Linux
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服务, echo -> 小写 -> 大写
// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
// 5.2 v2 版本 -- 多线程
// 这里不需要进行关闭文件描述符吗??不需要啦
// 多线程是会共享文件描述符表的!
ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// logMessage(DEBUG, "server 提供 service start ...");
// sleep(1);
}
}
2.代码
clientTcp.cc
#include "util.hpp"
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!
volatile bool quit = false;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1. 创建socket SOCK_STREAM
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
// 2.1 先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 发起请求,connect 会自动帮我们进行bind!
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(CONN_ERR);
}
std::cout << "info : connect success: " << sock << std::endl;
std::string message;
while (!quit)
{
message.clear();
std::cout << "请输入你的消息>>> ";
std::getline(std::cin, message);
if (strcasecmp(message.c_str(), "quit") == 0)
quit = true;
ssize_t s = write(sock, message.c_str(), message.size());
if (s > 0)
{
message.resize(1024);
ssize_t s = read(sock, (char *)(message.c_str()), 1024);
if (s > 0)
message[s] = 0;
std::cout << "Server Echo>>> " << message << std::endl;
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
serverTcp.cc
#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
class ServerTcp; // 申明一下ServerTcp
class ThreadData
{
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
ServerTcp *this_;
public:
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clinetIp_(ip), sock_(sock),this_(ts)
{}
};
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1)
{
}
~ServerTcp()
{
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "socket: %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);
// 2. bind绑定
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind: %s", strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5 /*后面再说*/) < 0)
{
logMessage(FATAL, "listen: %s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
// 运行别人来连接你了
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); //设置线程分离
ThreadData *td = static_cast<ThreadData*>(args);
td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
delete td;
return nullptr;
}
void loop()
{
// signal(SIGCHLD, SIG_IGN); // only Linux
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服务, echo -> 小写 -> 大写
// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
// transService(serviceSock, peerIp, peerPort);
// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
// pid_t id = fork();
// assert(id != -1);
// if(id == 0)
// {
// close(listenSock_); //建议
// //子进程
// transService(serviceSock, peerIp, peerPort);
// exit(0); // 进入僵尸
// }
// // 父进程
// close(serviceSock); //这一步是一定要做的!
// 5.1 v1.1 版本 -- 多进程版本 -- 也是可以的
// 爷爷进程
// pid_t id = fork();
// if(id == 0)
// {
// // 爸爸进程
// close(listenSock_);//建议
// // 又进行了一次fork,让 爸爸进程
// if(fork() > 0) exit(0);
// // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
// transService(serviceSock, peerIp, peerPort);
// exit(0);
// }
// // 父进程
// close(serviceSock); //这一步是一定要做的!
// // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
// assert(ret > 0);
// (void)ret;
// 5.2 v2 版本 -- 多线程
// 这里不需要进行关闭文件描述符吗??不需要啦
// 多线程是会共享文件描述符表的!
ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// waitpid(); 默认是阻塞等待!WNOHANG
// 方案1
// logMessage(DEBUG, "server 提供 service start ...");
// sleep(1);
}
}
// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char inbuffer[BUFFER_SIZE];
while (true)
{
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串
if (s > 0)
{
// read success
inbuffer[s] = '\0';
if(strcasecmp(inbuffer, "quit") == 0)
{
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
// 可以进行大小写转化了
for(int i = 0; i < s; i++)
{
if(isalpha(inbuffer[i]) && islower(inbuffer[i]))
inbuffer[i] = toupper(inbuffer[i]);
}
logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
write(sock, inbuffer, strlen(inbuffer));
}
else if (s == 0)
{
// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
// s == 0: 代表对方关闭,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到这里,一定是client退出了,服务到此结束
close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
};
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if(argc != 2 && argc != 3 )
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if(argc == 3) ip = argv[2];
ServerTcp svr(port, ip);
svr.init();
svr.loop();
return 0;
}
util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
版权归原作者 beyond.myself 所有, 如有侵权,请联系我们删除。