http://blog.csdn.net/tigerjibo/article/details/6775534

一.Linux下TCP编程框架

TCP网络编程的流程包含服务器和客户端两种模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。

1.服务器端程序包括

Ø  建立套接字( socket())

Ø  套接字与端口的绑定(bind())

Ø  设置服务器的侦听连接(listen())

Ø  接收客户端连接(accept())

Ø  接收和发送数据(send(),recv())

Ø  关闭套接字(close())

2.说明

1>套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。按照用户定义的网络类型,协议类型和具体的协议标号等参数来定以socket()函数。系统根据用户的需求生成一个套接字文件描述符供用户使用。

2>套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,套接字所代表IP地址和端口地址及协议类型等参数按照绑定值进行操作。

3>由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务器端排队队列的长度。

4>在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。

5>在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。

6>当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接

2.客户端程序包括

Ø  建立套接字(socket())

Ø  连接服务器(connect())

Ø  读写网络数据(send(),recv())

Ø  关闭套接字(close())

客户端程序和服务器端程序不同之处是客户端在建立套接字之后可以不进行地址绑定,而是直接连接服务器端。

服务器端有listen()和accept()两个函数,而客户端不需要这两个函数。

二.基于Linux的TCP套接字函数

1. socket

1> 函数原型:

int socket(int domain,int type,int protocol)

2> 函数功能:

函数socket()用于创建一个套接字文件描述符。

3> 形参:

Ø  domain:用于指定创建套接字所使用的协议族,在头文件

<linux/socket.h>中定义。有时候程序中会使用PF_INET,在头文件中AF_INET和PF_INET的数值是一致的。

常见的协议族如下:

AF_UNIX:创建只在本机内进行通信的套接字。

AF_INET:使用IPv4TCP/IP协议

AF_INET6:使用IPv6 TCP/IP协议

说明:

AF_UNIX只能用于单一的UNIX系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程主机之间通信。一般把它赋为AF_INET。

Ø  type:指明套接字通信的类型,对应的参数如下

SOCK_STREAM:创建TCP流套接字

SOCK_DGRAM:创建UDP数据报套接字

SOCK_RAW:创建原始套接字

Ø  protocol:指定某个协议的特定类型

参数protocol通常设置为0,表示通过参数domain指定的协议族和参数type指定的套接字类型来确定使用的协议。当为原始套接字时,系统无法唯一的确定协议,此时就需要使用使用该参数指定所使用的协议。

4> 返回值:执行成功后返回一个新创建的套接字;若有错误发生则返回一个-1,错误代码存入errno中。

5> 举例:调用socket函数创建一个UDP套接字

int sock_fd;

sock_fd = socket(AF_INET,SOCK_DGRAM,0);

if(sock_fd< 0){

perror(“socket”);

exit(1);

}

2. bind

1> 函数原型:

int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen)

2> 函数功能

函数bind()的作用是将一个套接字文件描述符与地址和端口绑定。

3> 形参:

Ø  sockfd:sockfd是调用socket函数返回的文件描述符;

Ø  addrlen是sockaddr结构的长度。

Ø  my_addr: 是一个指向sockaddr结构的指针,它保存着本地套接字的地址(即端口和IP地址)信息。不过由于系统兼容性的问题,一般不使用这个结构,而使用另外一个结构(struct sockaddr_in)来代替

4> 套接字地址结构:

(1)struct sockaddr:

结构struct  sockaddr定义了一种通用的套接字地址,它在

sys/socket.h 中定义。

struct sockaddr{

unsigned short  sa_family;/*地址类型,AF_XXX*/

char          sa_data[14];/*14字节的协议地址*/

}

a. sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.

b. sa_data:存储具体的协议地址。

(2)sockaddr_in

每种协议族都有自己的协议地址格式,TCP/IP协议组的地址格式为结构体struct sockaddr_in,它在netinet/in.h头文件中定义。

structsockaddr_in{

unsigned short  sin_family;/*地址类型*/

unsigned short  sin_port;/*端口号*/

struct in_addr   sin_addr;/*IP地址*/

unsigned char  sin_zero[8];/*填充字节,一般赋值为0*/

}

a. sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.

b. sin_port:是端口号

c. sin_addr:用来存储32位的IP地址。

d. 数组sin_zero为填充字段,一般赋值为0.

e. struct in_addr的定义如下:

structin_addr{

unsigned long s_addr;

}

结构体sockaddr的长度为16字节,结构体sockaddr_in的长度为16字节。可以将参数my_addr的sin_addr设置为INADDR_ANY而不是某个确定的IP地址就可以绑定到任何网络接口。对于只有一IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于多宿主主机(拥有多个网卡),INADDR_ANY表示本服务器程序将处理来自所有网络接口上相应端口的连接请求

5> 返回值:

函数成功后返回0,当有错误发生时则返回-1,错误代码存入errno中。

6>举例:调用socket函数创建一个UDP套接字(htonl将主机字节序数据转化为网络字节序数据)

struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/

memset(&serv_addr,0,sizeof(structsockaddr_in));

addr_serv.sin_family= AF_INET;/*协议族*/

addr_serv.sin_port= htons(SERV_PORT);/*本地端口号*/

addr_serv.sin_addr.s_addr= htonl(INADDR_ANY); /*任意本地地址*/
/*套接字绑定*/

if(bind(sock_fd,(structsockaddr *)&addr_serv),sizeof(struct sockaddr_in)) <0)

{

perror(“bind”);

exit(1);

}

3. 监听本地端口listen()

1>函数功能:函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度由listen()函数来定义。

2>函数原型:

#includ<sys/socket.h>

int listen(int sockfd,int backlog);

3>形参

Ø  sockfd: sockfd是调用socket函数返回的文件描述符

Ø  backlog:指定该连接队列的最大长度。如果连接队列已经达到最大,之后的连接请求被服务器拒绝。大多数系统的设置为20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。

4>返回值:当listen()函数成功运行时,返回值为0;当运行失败时,它的返回值为-1,错误代码存入errno中。

5>.listen()函数的例子:

#define SERV_PORT 3000

int main(int argc,char *argv[])

{

int sock_fd;

struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/

sock_fd = socket(AF_INET,SOCK_DGRAM,0);

if(sock_fd< 0){

perror(“socket”);

exit(1);

}

memset(&serv_addr,0,sizeof(struct sockaddr_in));

addr_serv.sin_family= AF_INET;/*协议族*/

addr_serv.sin_port= htons(SERV_PORT);/*本地端口号*/

addr_serv.sin_addr.s_addr= htonl(INADDR_ANY); /*任意本地地址*/
/*套接字绑定*/

if(bind(sock_fd,(structsockaddr *)&addr_serv),sizeof(struct sockaddr_in)) <0)

{

perror(“bind”);

exit(1);

}

//设置服务器侦听队列的长度

if(listen(sock_fd,5) <0){

perror(“listen”);

exit(1);

}

4. accept(接收一个网络请求)

1>函数功能:

当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,知道使用服务器处理接收请求。

函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的连接请求后,会有两个文件描述符,老的套接字文件描述符用来接受客户端的连接请求,函数send()和recv()通过新的文件描述符进行数据收发。

2>函数原型:

#include<sys/types.h>

#include<sys/socket.h>

int accept(int sock_fd,struct sockaddr*addr,socklen_t *addrlen);

3>形参

Ø  sock_fd:是由函数socket创建,经函数bind绑定到本地某一端口上,然后通过函数listen转化而来的监听套接字。

Ø  addr:用来保存发起连接请求的主机的地址和端口。

Ø  addrlen是addr 所指向的结构体的大小。

4>返回值:accept()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符。如果accept()函数发生错误,accept()会返回-1,通过errno可以得到错误值。

5>如果参数sock_fd所指定的套接字被设置为阻塞方式(Linux下的默认方式),且连接请求队列为空,则accept()将被阻塞直到有连接请求到此为止;如果参数s所指定的套接字被设置为非阻塞方式,如果队列为空,accept将立即返回-1,errno被设置为EAGAIN.

6>实例:

int client_fd;

int client_len;

struct sockaddr_in  client_addr;

client_len = sizeof(struct sockaddr_in);

client_fd = accept(sock_fd,(struct sockaddr *)&client_addr,&client_len);

if(conn_fd< 0){

perror(“accept”);

exit(1);

}

5. connect(连接目标网络服务器)

1>函数功能:

客户端在建立套接字之后,不需要进行地址绑定,就可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址,端口号。

如果是TCP编程,则connect()函数用于客户端发出连接请求,服务器的IP地址和端口号由 参数serv_addr指定。

如果是UDP编程,则connect函数并不建立真正的连接,它只是告诉内核与该套接字进行通信的目的地址(由第二个参数指定),只有该目的地址发来的数据才会被该socket接收。调用connect函数的好处是不必在每次发送和接收数据时都指定目的地址。

2>函数原型:

#include<sys/types.h>

#include<sys/socket.h>

int connect(int sock_fd,struct sockaddr  *serv_addr,socklen_taddrlen);

3>形参:

Ø  sock_fd:建立套接字时返回的套接字文件描述符,调用socket()返回的。

Ø  serv_addr:是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的IP地址和端口号。

Ø  addrlen:表示了第二了参数的大小,可以使用sizeof(struct sockaddr)

4>执行成功后返回0,有错误发生则返回-1,错误代码存入errno中。

5>实例:

int sock_fd;

struct sockaddr_in serv_addr;
if(-1 == (sock_fd == socket(AF_INET,SOCK_STREAM,0))){

printf(“Error: Unable to createsocket(%i)…\n”,errno);

perror(“sockets”);

exit(1);

}

memset(&serv_addr,0,sizeof(structsockaddr_in));

serv_addr.sin_family= AF_INET;

serv_addr.sin_port= htons(DEST_PORT);

serv_addr.sin_addr.s_addr= inet(DEST_IP_ADDRESS);

if(-1== connect(sock_fd,(struct sockaddr *)&serv_add,sizeof(struct sockaddr))){

printf(“Error:unable to the establishconnection to socket(%i)…\n”,errno);

perror(“socks”);

close(sock_fd);

exit(1);

}

6. send(发送数据)

1>函数功能:函数send用来在TCP套接字上发送数据,send只能对处于连接状态的套接字使用。

2>函数原型

#include<sys/types.h>

#include<sys/socket.h>

ssize_t send(int conn_fd,const void *msg,size_t len, int flags);

3>函数形参:

Ø  conn_fd:为已建立好连接的套接字描述符,即调用accept()函数后返回的套接字描述符。

Ø  msg:存放发送数据的缓冲区。

Ø  len:发送缓冲区的长度

Ø  flags:为控制选项,一般设置为0,或取以下值:

²  MSG_OOB:在指定的套接字上发送带外数据(out-of-band data),该类型的套接字必须支持带外数据(如:SOCK_STREAM).

²  MSG_DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置。

4>返回值:

执行成功返回实际发送数据的字节数,出错则返回-1,错误代码存入errno中。

执行成功只是说明数据写入套接字的缓冲区中,并不表示数据已经成功地通过网络发送到目的地。

5>实例:

#define  BUFFERSIZE  1500

char  send_buf[BUFFERSIZE];

……

if(send(conn_fd,send_buf,len,0)< 0){

perror(“send”);

exit(1);

}

7. recv(接收数据)

1>函数功能:recv()用来在TCP套接字上接收数据,函数recv从指定的套接字文件描述符上接收数据并保存到指定buf中。

2>函数原型

#include<sys/types.h>

#include<sys/socket.h>

ssize_t recv(int conn_fd,void *buf,size_t len,int flags);

3>函数形参:

Ø  conn_fd: 为已建立好连接的套接字描述符,即调用accept()函数后返回的套接字描述符

Ø  buf:接收缓冲区

Ø  len:接收缓冲区的大小

Ø  flags:为控制选项,一般设置为0或取以下数值

²  MSG_OOB:请求接收带外数据

²  MSG_PEEK:只查看数据而不读出

²  MSG_WAITALL:只在接收缓冲区满时才返回。

4>函数返回值

函数执行成功返回接收到的数据字节数,出错返回-1,错误代码存入errno中。

5>实例:

#define  BUFFERSIZE  1500

char recv_buf[BUFFERSIZE];

……

if(recv(conn_fd,recv_buf,sizeof(recv_buf),0)< 0){

perror(“recv”);

exit(1);

}

8. close

1>函数原型:

int  close(int fd);

2>函数功能:

函数close用来关闭一个套接字描述符。

3>函数形参:

Ø  参数fd为一个套接字描述符。

4>返回值:

执行成功返回0,出错则返回-1.错误代码存入errno中。

说明:close()函数的头文件是#include<unistd.h>.

三.基于Linux的TCP套接字编程实例

1.实例程序分为服务器端和客户端,客户端把Hello tigerjibo发送给服务器端;服务器端接收到字符串后,发送接收到的总字符串个数给客户端;

2.服务器端程序:

客户端代码:

#include <stdlib.h>
#include <string.h>
#include <errno.h> #include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>//struct sockaddr_in
#include <arpa/inet.h> #define DEST_PORT 8000
#define DEST_IP_ADDRESS "10.180.30.143" void process_info(int s)
{
int send_num;
int recv_num;
char send_buf[]="caonimade";
char recv_buf[]; while()
{
printf("begin send\n");
send_num=send(s,send_buf,sizeof(send_buf),);
if(send_num<)
{
perror("send");
exit();
}
else
{
printf("send success:%s\n",send_buf);
}
printf("begin recv:\n");
recv_num=recv(s,recv_buf,sizeof(recv_buf),);
if(recv_num<)
{
perror("recv");
exit();
}
else
{
recv_buf[recv_num]='\0';
printf("recv success:%s\n",recv_buf);
}
}
} int main()
{
int sock_fd;
struct sockaddr_in addr_serv;//服务器端地址 sock_fd=socket(AF_INET,SOCK_STREAM,);
if(sock_fd<)
{
perror("sock");
exit();
}
else
{
printf("sock successfully:\n");
} memset(&addr_serv,,sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_port = htons(DEST_PORT);
addr_serv.sin_addr.s_addr = inet_addr(DEST_IP_ADDRESS);
if( connect(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr)) < ){
perror("connect");
printf("connect (%d)\n",errno);
exit();
} else {
printf("connect sucessful\n");
} /*
memset(&addr_serv,0,sizeof(addr_serv));
addr_serv.sin_family=AF_INET;
addr_serv.sin_port=htons(DEST_PORT);
addr_serv.sin_addr.s_addr=inet_addr(DEST_IP_ADDRESS);
if(connect(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr))<0)
{
printf("failed\n");
perror("connect");
printf("connect(%d)\n",errno);
exit(1);
}
else
{
printf("connect successful\n");
}
*/
process_info(sock_fd);
close(sock_fd);
}

服务器端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h> #include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>//struct sockaddr_in
#include <arpa/inet.h>
#define QUEUE_LINE 12
#define SOURCE_PORT 8000
#define SOURCE_IP_ADDRESS "192.168.1.6" void process_info(int s)
{
int recv_num;
int send_num;
char recv_buf[];
char send_buf[];
while()
{
printf("begin recv:\n");
recv_num=recv(s,recv_buf,sizeof(recv_buf),);
if(recv_num<)
{
perror("recv");
exit();
}
else
{
recv_buf[recv_num]='\0';
printf("recv sucessful:%s\n",recv_buf);
}
sprintf(send_buf,"recv %d numbers bytes\n",recv_num);//把格式化的数据写入send_buf
printf("begin send\n");
send_num=send(s,send_buf,sizeof(send_buf),);
if(send_num<)
{
perror("sned");
exit();
}
else
{
printf("send success\n");
}
}
} int main()
{
int sock_fd,conn_fd;
int client_len;
pid_t pid;
struct sockaddr_in addr_serv,addr_client;//TCP,IP协议组的地址格式
sock_fd=socket(AF_INET,SOCK_STREAM,);
if(sock_fd<)
{
perror("socket");//头文件stdio
exit();
}
else
{
printf("sock successful\n");
}
//初始化服务器端地址
memset(&addr_serv,,sizeof(addr_serv));
addr_serv.sin_family=AF_INET;
addr_serv.sin_port=htons(SOURCE_PORT);//将无符号短整型的主机数值转为网络字节顺序
addr_serv.sin_addr.s_addr=htonl(INADDR_ANY);
//addr_serv.sin_addr.s_addr=inet_addr("127.0.0.1");
//addr_serv.sin_addr.s_addr=inet_addr(SOURCE_IP_ADDRESS);//inet_addr将IP地址转为网络序
client_len=sizeof(struct sockaddr_in);
if(bind(sock_fd,(struct sockaddr*)&addr_serv,sizeof(struct sockaddr_in))<)
{
perror("bind");
exit();
}
else
{
printf("bind success\n");
}
if(listen(sock_fd,QUEUE_LINE)<)//初始化服务器可连接队列
{
perror("listen");
exit();
}
else
{
printf("listen successful\n");
} while()
{
printf("begin accept:\n");
conn_fd=accept(sock_fd,(struct sockaddr *)&addr_client,&client_len);
if(conn_fd<)
{
perror("accept");
exit();
}
printf("accept a new client,ip:%s\n",inet_ntoa(addr_client.sin_addr));
pid=fork();
if(==pid)//子进程
{
close(sock_fd);//在子进程中关闭服务器的侦听
process_info(conn_fd);//处理信息
}
else
{
close(conn_fd);//在父进程中关闭客户端连接
} }
}

注意:在同一台电脑上测试时,本机ip为127.0.0.1,开俩进程

【转】基于Linux下的TCP编程的更多相关文章

  1. 基于Linux下的UDP编程

    一. Linux下UDP编程框架 使用UDP进行程序设计可以分为客户端和服务器端两部分. 1.服务器端程序包括: Ø  建立套接字 Ø  将套接字地址结构进行绑定 Ø  读写数据 Ø  关闭套接字 2 ...

  2. Linux下TCP网络编程与基于Windows下C#socket编程间通信

    一.linux下TCP网络编程基础,需要了解相关函数 Socket():用于套接字初始化. Bind():将 socket 与本机上的一个端口绑定,就可以在该端口监听服务请求. Listen():使s ...

  3. Linux下的C编程实战

    Linux下的C编程实战(一) ――开发平台搭建 1.引言 Linux操作系统在服务器领域的应用和普及已经有较长的历史,这源于它的开源特点以及其超越Windows的安全性和稳定性.而近年来, Linu ...

  4. Linux基础与Linux下C语言编程基础

    Linux基础 1 Linux命令 如果使用GUI,Linux和Windows没有什么区别.Linux学习应用的一个特点是通过命令行进行使用. 登录Linux后,我们就可以在#或$符后面去输入命令,有 ...

  5. 【转】Linux基础与Linux下C语言编程基础

    原文:https://www.cnblogs.com/huyufeng/p/4841232.html ------------------------------------------------- ...

  6. 基于Linux下Iptables限制BT下载的研究

    基于Linux下Iptables限制BT下载的研究   摘要:     当前BT下载技术和软件飞速发展,给人们网上冲浪获取资源带来了极大的便利, 但同时BT占用大量的网络带宽等资源也给网络和网络管理员 ...

  7. linux下的tcp连接超时

    最近需要写一个linux下的通信程序, 通信模块用的是Qt的QTcpSocket. 最后程序需要增加一个断网检测, 在windows下调试没问题, 拔网线, 断网口都能马上检测到, 但到了部署到lin ...

  8. Linux下C语言编程实现spwd函数

    Linux下C语言编程实现spwd函数 介绍 spwd函数 功能:显示当前目录路径 实现:通过编译执行该代码,可在终端中输出当前路径 代码实现 代码链接 代码托管链接:spwd.c 所需结构体.函数. ...

  9. linux下的c编程

    linux下的c编程 Linux 系统上可用的 C 编译器是 GNU C 编译器, 它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布.GNU  C 对标准 C 进行一系列扩展,以增强标准 ...

随机推荐

  1. 简述SVN服务器配置和客户端操作

    有关SVN服务器的搭建见:http://www.cnblogs.com/DwyaneTalk/p/3997688.html 搭建好环境之后,服务器端需要完成如下操作: 1.需要新建Repository ...

  2. Struts1 MVC框架的工作原理

    MVC英文及Model-View-Controller,分别是模型(Model),视图(View)和控制(Controller).MVC模式的目的是实现web系统的职能分工. View:即用户交互界面 ...

  3. ubunut在线音乐比方软件

    今天安装了一个音乐在线播放软件,忍不住要来赞一下, 之前一直都是用网页在线的qq音乐听的,这样就有点感觉不爽了, 今天突然想起来好像在网上看到的在ubuntu下有用网易云音乐的,就上网看了一下 还真的 ...

  4. SweetAlert如何实现点击Confirm之后自动关闭

    swal({ title: "Are you sure?", text: "You will not be able to recover this imaginary ...

  5. ar - 建立, 修改档案或从档案中抽取成员.

    总览 ar [-]{dmpqrtx}[abcfilNoPsSuvV] [membername] [count] archive files... 描述 GNU 组织的ar 程序 用于建立, 修改档案 ...

  6. JS中二进制与十进制的相互转换

    今天在做题目的时候遇到了需要十进制转换为二进制,这个我知道用toString可以,但是二进制转换为十进制我一下子就想不起来,网上搜了下,才知道是parseInt可以实现,特此记录下. 十进制转换为二进 ...

  7. php简单实用的调试工具类

    <?php /* * 调试类 */ class Common_Debug { //打开错误报告 public static function showError($debug = true) { ...

  8. 利用jQuery对li标签操作

    <ul class="con" id="products"> <li i=" class=""> < ...

  9. MongoDB中mapReduce的使用

    MongoDB中mapReduce的使用 制作人:全心全意 mapReduce的功能和group by的功能类似,但比group by处理的数据量更大 使用示例: var map = function ...

  10. eclipse导入项目时报错不能运行问题的一个记录

    一直用学校的云桌面,但是还是有一些地方不是很方便,必须要校园网以及需要离线保存: 碰到的问题:重新安装和云桌面一样版本的jdk9.0.4,以及tomcat9.0.12,以及eclipse-oxygen ...