引言

  本文是围绕Linux udp api 构建一个简易的多人聊天室.重点看思路,帮助我们加深

对udp开发中一些api了解.相对而言udp socket开发相比tcp socket开发注意的细节要少很多.

但是水也很深. 本文就当是一个demo整合帮助开发者回顾和继续了解 linux udp开发的基本流程.

首先我们来看看 linux udp 和 tcp的异同.

/*
这里简单比较一下TCP和UDP在编程实现上的一些区别: TCP流程
建立一个TCP连接需要三次握手,而断开一个TCP则需要四个分节。当某个应用进程调用close(主动端)后
(可以是服务器端,也可以是客户 端),这一端的TCP发送一个FIN,表示数据发送完毕;另一端(被动端)发送一
个确认,当被动端待处理的应用进程都处理完毕后,发送一个FIN到主动端,并关闭套接口,主动端接收到这个
FIN后再发送一个确认,到此为止这个TCP连接被断开。 UDP套接口
  UDP套接口是无连接的、不可靠的数据报协议;既然他不可靠为什么还要用呢?
  其一:当应用程序使用广播或多播是只能使用UDP协议;
  其二:由于它是无连接的,所以速度快。因为UDP套接口是无连接的,如果一方的数据报丢失,那另一方将无
限等待,解决办法是设置一个超时。在编写UDP套接口程序时,有几点要注意:建立套接口时socket函
数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口;由于UDP是无连接的,所以服务器端
并不需要listen或accept函数;当UDP套接口调用connect函数时,内核只记录连接放的IP地址 和端
口,并立即返回给调用进程.
*/

参照

linux udp api简介   http://blog.csdn.net/wocjj/article/details/8315559

     tcp 和udp区别    http://www.cnblogs.com/Jessy/p/3536163.html

这里简单引述一下 udp相比tcp 用到的两个api .  recvfrom()/sendto() 具体细节如下

#include <sys/types.h>
#include <sys/socket.h> /*
* 这两个函数基本等同于 一个 send 和 recv . 详细参数解释如下
* s : 文件描述符,等同于 socket返回的值
* buf : 数据其实地址
* len : 发送数据长度或接受数据缓冲区最大长度
* flags : 发送标识,默认就用O.带外数据使用 MSG_OOB, 偷窥用MSG_PEEK .....
* addr : 发送的网络地址或接收的网络地址
* alen : sento标识地址长度做输入参数, recvfrom表示输入和输出参数.可以为NULL此时addr也要为NULL
* : 返回0表示执行成功,否则返回<0 . 更多细节查询man手册
*/
extern int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *addr, int alen);
extern int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *addr, int *alen);

上面就是两个函数的大致用法. 具体可以查看linux api帮助手册. 最好就用 man sendto / man recvfrom 把那一系列函数都看看.

现在很多文章都是转载,但是找不见转载的地址, 下面会举一个简易的UDP回显服务器的demo加深理解.

前言

首先看设计图

有点low. 简单看看吧. 那我们先看 客户端代码  udpclt.c 代码

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define _SHORT_PORT (8088) //
// udp client heoo
//
int main(int argc, char * argv[]) {
int fd, len;
struct sockaddr_in ar = { AF_INET };
socklen_t al = sizeof (struct sockaddr_in);
char msg[BUFSIZ] = ":) 谁也不会喜欢工作狂 ~"; // 创建 client socket
if ((fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -) {
perror("main socket dgram");
exit(EXIT_FAILURE);
} ar.sin_port = htons(_SHORT_PORT);
// 开始发送消息包到服务器, 默认走 INADDR_ANY
sendto(fd, msg, sizeof msg - , , (struct sockaddr *)&ar, al); // 开始接收服务器回过来的报文
len = recvfrom(fd, msg, sizeof msg - , , (struct sockaddr *)&ar, &al);
if (len == -) {
perror("main recvfrom");
exit(EXIT_FAILURE);
}
msg[len] = '\0';
printf("[%s:%hd] -> %s\n", inet_ntoa(ar.sin_addr), ntohs(ar.sin_port), msg); // 程序结束
return close(fd);
}

编译是

gcc -g -Wall -o udpclt.out udpclt.c

udp 服务器 udpsrv.c

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define _SHORT_PORT (8088) //
// udp server heoo
//
int main(int argc, char * argv[]) {
int fd, len;
struct sockaddr_in ar = { AF_INET };
socklen_t al = sizeof (struct sockaddr_in);
char msg[BUFSIZ]; if ((fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -) {
perror("main socket dgram");
exit(EXIT_FAILURE);
} printf("udp server start [%d][0.0.0.0][%hd] ...\n", fd, _SHORT_PORT); // 服务器绑定地址
ar.sin_port = htons(_SHORT_PORT);
if (bind(fd, (struct sockaddr *)&ar, al) == -) {
perror("main bind INADDR_ANY");
exit(EXIT_FAILURE);
} // 阻塞的接收不同客户端数据来回搞
while ((len = recvfrom(fd, msg, sizeof msg - , , (struct sockaddr *)&ar, &al)) > ) {
msg[len] = '\0';
printf("[%s:%hd] -> %s\n", inet_ntoa(ar.sin_addr), ntohs(ar.sin_port), msg); // 回显继续发送给客户端
sendto(fd, msg, len, , (struct sockaddr *)&ar, al);
} return close(fd);
}

编译是

gcc -g -Wall -o udpsrv.out udpsrv.c

后面运行结果如下 udp服务器如下 (Ctrl + C 退出)

udp 客户端如下(修改了一版本, 当前版本更加简单, 容易理解.)

到这里将上面代码 敲一遍基本上 udp  一套 api 就会使用了. 后面进入正题设计聊天室代码.

正文

  首先看客户端设计代码. 主要思路是子进程处理数据的输出, 父进程处理服务器数据的接收. 具体设计如下(画的图有点low就不画了.../(ㄒoㄒ)/~~)

udpmulclt.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h> // 名字长度包含'\0'
#define _INT_NAME (64)
// 报文最大长度,包含'\0'
#define _INT_TEXT (512) //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
* 简单的Linux上API错误判断检测宏, 好用值得使用
*/
#define IF_CHECK(code) \
if((code) < ) \
CERR_EXIT(#code) // 发送和接收的信息体
struct umsg{
char type; //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
char name[_INT_NAME]; //保存用户名字
char text[_INT_TEXT]; //得到文本信息,空间换时间
}; /*
* udp聊天室的客户端, 子进程发送信息,父进程接受信息
*/
int main(int argc, char* argv[]) {
int sd, rt;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
pid_t pid;
struct umsg msg = { '' }; // 这里简单检测
if(argc != ) {
fprintf(stderr, "uage : %s [ip] [port] [name]\n", argv[]);
exit(-);
}
// 下面对接数据
if((rt = atoi(argv[]))< || rt > )
CERR("atoi port = %s is error!", argv[]);
// 接着判断ip数据
IF_CHECK(inet_aton(argv[], &addr.sin_addr));
addr.sin_port = htons(rt);
// 这里拼接用户名字
strncpy(msg.name, argv[], _INT_NAME - ); //创建socket 连接
IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, ));
// 这里就是发送登录信息给udp聊天服务器了
IF_CHECK(sendto(sd, &msg, sizeof msg, , (struct sockaddr*)&addr, alen)); //开启一个进程, 子进程处理发送信息, 父进程接收信息
IF_CHECK(pid = fork());
if(pid == ) { //子进程,先忽略退出处理防止成为僵尸进程
signal(SIGCHLD, SIG_IGN);
while(fgets(msg.text, _INT_TEXT, stdin)){
if(strcasecmp(msg.text, "quit\n") == ){ //表示退出
msg.type = '';
// 发送数据并检测
IF_CHECK(sendto(sd, &msg, sizeof msg, , (struct sockaddr*)&addr, alen));
break;
}
// 洗唛按发送普通信息
msg.type = '';
IF_CHECK(sendto(sd, &msg, sizeof msg, , (struct sockaddr*)&addr, alen));
}
// 处理结算操作,并杀死父进程
close(sd);
kill(getppid(), SIGKILL);
exit();
}
// 这里是父进程处理数据的读取
for(;;){
bzero(&msg, sizeof msg);
IF_CHECK(recvfrom(sd, &msg, sizeof msg, , (struct sockaddr*)&addr, &alen));
msg.name[_INT_NAME-] = msg.text[_INT_TEXT-] = '\0';
switch(msg.type){
case '':printf("%s 登录了聊天室!\n", msg.name);break;
case '':printf("%s 说了: %s\n", msg.name, msg.text);break;
case '':printf("%s 退出了聊天室!\n", msg.name);break;
default://未识别的异常报文,程序直接退出
fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
goto __exit;
}
} __exit:
// 杀死并等待子进程退出
close(sd);
kill(pid, SIGKILL);
waitpid(pid, NULL, -); return ;
}

这里主要需要注意的是

// 发送和接收的信息体
struct umsg{
char type; //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
char name[_INT_NAME]; //保存用户名字
char text[_INT_TEXT]; //得到文本信息,空间换时间
};

传输和接收的数据格式, type表示协议或行为. 我这里细心了处理 name, text最后一个字符必须是 '\0'. 其它都是业务代码.再扯一点

struct sockaddr_in addr = { AF_INET };

等价于

struct sockaddr_in addr;
memset(&addr, , sizeof addr);
addr.sin_family = AF_INET;

也是一个C开发中技巧吧. 再扯一点linux上提供 bzero函数, 但是window上没有. 写了个通用的如下

//7.0 置空操作
#ifndef BZERO
//v必须是个变量
#define BZERO(v) \
memset(&v,,sizeof(v))
#endif/* !BZERO */

可以试试吧毕竟跨平台....

好了那我们说 udp 聊天室的服务器设计思路. 就是服务器会维护一个客户端链表. 有信息来就广播. 好简单吧.就是这样.正常的事都简单.

简单的是美的. 好了看代码总设计和实现. udpmulsrv.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h> // 名字长度包含'\0'
#define _INT_NAME (64)
// 报文最大长度,包含'\0'
#define _INT_TEXT (512) //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
* 简单的Linux上API错误判断检测宏, 好用值得使用
*/
#define IF_CHECK(code) \
if((code) < ) \
CERR_EXIT(#code) // 发送和接收的信息体
struct umsg{
char type; //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
char name[_INT_NAME]; //保存用户名字
char text[_INT_TEXT]; //得到文本信息,空间换时间
}; // 维护一个客户端链表信息,记录登录信息
typedef struct ucnode {
struct sockaddr_in addr;
struct ucnode* next;
} *ucnode_t ; // 新建一个结点对象
static inline ucnode_t _new_ucnode(struct sockaddr_in* pa){
ucnode_t node = calloc(sizeof(struct ucnode), );
if(NULL == node)
CERR_EXIT("calloc sizeof struct ucnode is error. ");
node->addr = *pa;
return node;
} // 插入数据,这里head默认头结点是当前服务器结点
static inline void _insert_ucnode(ucnode_t head, struct sockaddr_in* pa) {
ucnode_t node = _new_ucnode(pa);
node->next = head->next;
head->next = node;
} // 这里是有用户登录处理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
_insert_ucnode(head, pa);
head = head->next;
// 从此之后才为以前的链表
while(head->next){
head = head->next;
IF_CHECK(sendto(sd, msg, sizeof(*msg), , (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
} // 信息广播
static void _broadcast_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
int flag = ; //1表示已经找到了
while(head->next) {
head = head->next;
if((flag) || !(flag=memcmp(pa, &head->addr, sizeof(struct sockaddr_in))==)){
IF_CHECK(sendto(sd, msg, sizeof(*msg), , (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
}
} // 有人退出群聊
static void _quit_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
int flag = ;//1表示已经找到
while(head->next) {
if((flag) || !(flag = memcmp(pa, &head->next->addr, sizeof(struct sockaddr_in))==)){
IF_CHECK(sendto(sd, msg, sizeof(*msg), , (struct sockaddr*)&head->next->addr, sizeof(struct sockaddr_in)));
head = head->next;
}
else { //删除这个退出的用户
ucnode_t tmp = head->next;
head->next = tmp->next;
free(tmp);
}
}
} // 销毁维护的对象池,没有往复杂的考虑了简单处理退出了
static void _destroy_ucnode(ucnode_t* phead) {
ucnode_t head;
if((!phead) || !(head=*phead)) return;
while(head){
ucnode_t tmp = head->next;
free(head);
head = tmp;
} *phead = NULL;
} /*
* udp聊天室的服务器, 子进程广播信息,父进程接受信息
*/
int main(int argc, char* argv[]) {
int sd, rt;
struct sockaddr_in addr = { AF_INET };
socklen_t alen = sizeof addr;
struct umsg msg;
ucnode_t head; // 这里简单检测
if(argc != ) {
fprintf(stderr, "uage : %s [ip] [port]\n", argv[]);
exit(-);
}
// 下面对接数据
if((rt = atoi(argv[]))< || rt > )
CERR("atoi port = %s is error!", argv[]);
// 接着判断ip数据
IF_CHECK(inet_aton(argv[], &addr.sin_addr));
addr.sin_port = htons(rt); //端口要采用网络字节序
// 创建socket
IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, ));
// 这里bind绑定设置的地址
IF_CHECK(bind(sd, (struct sockaddr*)&addr, alen)); //开始监听了
head = _new_ucnode(&addr);
for(;;){
bzero(&msg, sizeof msg);
IF_CHECK(recvfrom(sd, &msg, sizeof msg, , (struct sockaddr*)&addr, &alen));
msg.name[_INT_NAME-] = msg.text[_INT_TEXT-] = '\0';
fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
// 开始判断处理
switch(msg.type) {
case '':_login_ucnode(head, sd, &addr, &msg);break;
case '':_broadcast_ucnode(head, sd, &addr, &msg);break;
case '':_quit_ucnode(head, sd, &addr, &msg);break;
default://未识别的异常报文,程序把其踢走
fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port), msg.type, msg.name, msg.text);
_quit_ucnode(head, sd, &addr, &msg);
break;
}
} // 这段代码是不会执行到这的, 可以加一些控制让其走到这. 看人
close(sd);
_destroy_ucnode(&head);
return ;
}

这里主要围绕的结构就是

// 维护一个客户端链表信息,记录登录信息
typedef struct ucnode {
struct sockaddr_in addr;
struct ucnode* next;
} *ucnode_t ;

注册添加登录广播退出等.这里再扯一下. 关于C static开发技巧. C中有一种 *.h 开发模式, 全部采用static 内嵌代码段. 这样

可以省略*.c 文件. 小巧的封装可以使用. 继续扯一点. 开发也写C++,虽然鄙视. C++ 中有个 *.hpp文件. 比较好. 它表达的意思

是这个代码是开源的. 全部采用充血模型. 类中代码都放在类中实现.非常值得提倡. 这也是学boost的时候学到的. 很实在.

好了说代码吧. 也比较随大流. 看看也都明白了. 简单分析一处吧

// 这里是有用户登录处理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
_insert_ucnode(head, pa);
head = head->next;
// 从此之后才为以前的链表
while(head->next){
head = head->next;
IF_CHECK(sendto(sd, msg, sizeof(*msg), , (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
}
}

因为我采用的头查法. 那就除了刚插入的头的下一个结点都需要发送登录信息. 比较精巧.

好看编译命令

gcc -g -Wall -o udpmulsrv.out udpmulsrv.c
gcc -g -Wall -o udpmulclt.out udpmulclt.c

最后测试截图如下

很好玩,欢迎尝试.到这里基本上udp基础api 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.

下次有机会 要么分享开源的网络库,要么分享数据库开发.

后记

   错误是难免的,欢迎吐槽交流. ( ^_^ )/~~拜拜

     
别董大(其一)
        高适
    千里黄云白日曛,
    北风吹雁雪纷纷。
    莫愁前路无知己,
    天下谁人不识君。
 
(作者注: 别董大意思是 分别了我的朋友董大)

  

C 基于UDP实现一个简易的聊天室的更多相关文章

  1. UDP实现一个简易的聊天室 (Unity&&C#完成)

    效果展示(尚未完善) UDP User Data Protocol 用户数据报协议 概述 UDP是不连接的数据报模式.即传输数据之前源端和终端不建立连接.使用尽最大努力交付原则,即不保证可靠交付. 数 ...

  2. 与众不同 windows phone (31) - Communication(通信)之基于 Socket UDP 开发一个多人聊天室

    原文:与众不同 windows phone (31) - Communication(通信)之基于 Socket UDP 开发一个多人聊天室 [索引页][源码下载] 与众不同 windows phon ...

  3. TCP实现一个简易的聊天室 (Unity&&C#完成)

    效果展示 TCP Transmission Control Protocol 传输控制协议 TCP是面向连接的流模式(俗称:网络流).即传输数据之前源端和终端建立可靠的连接,保证数据传输的正确性. 流 ...

  4. 计算机网络课设之基于UDP协议的简易聊天机器人

    前言:2017年6月份计算机网络的课设任务,在同学的帮助和自学下基本搞懂了,基于UDP协议的基本聊天的实现方法.实现起来很简单,原理也很简单,主要是由于老师必须要求使用C语言来写,所以特别麻烦,而且C ...

  5. 与众不同 windows phone (30) - Communication(通信)之基于 Socket TCP 开发一个多人聊天室

    原文:与众不同 windows phone (30) - Communication(通信)之基于 Socket TCP 开发一个多人聊天室 [索引页][源码下载] 与众不同 windows phon ...

  6. 基于websocket实现的一个简单的聊天室

    本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊.是基于websocket的注解方式编写的.(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知 ...

  7. Java进阶:基于TCP通信的网络实时聊天室

    目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...

  8. Android基于XMPP Smack openfire 开发的聊天室

    Android基于XMPP Smack openfire 开发的聊天室(一)[会议服务.聊天室列表.加入] http://blog.csdn.net/lnb333666/article/details ...

  9. [SignalR]一个简单的聊天室

    原文:[SignalR]一个简单的聊天室 1.说明 开发环境:Microsoft Visual Studio 2010 以及需要安装NuGet. 2.添加SignalR所需要的类库以及脚本文件: 3. ...

随机推荐

  1. js对象3--工厂方法加深引出原型--杂志

    继续上一章的案例讲解: <script type="text/javascript"> function createPreason(name,sex){ //他的怪癖 ...

  2. 洛谷P1466 集合 Subset Sums

    P1466 集合 Subset Sums 162通过 308提交 题目提供者该用户不存在 标签USACO 难度普及/提高- 提交  讨论  题解 最新讨论 暂时没有讨论 题目描述 对于从1到N (1 ...

  3. python字典的常用操作方法

    Python字典是另一种可变容器模型(无序),且可存储任意类型对象,如字符串.数字.元组等其他容器模型.本文章主要介绍Python中字典(Dict)的详解操作方法,包含创建.访问.删除.其它操作等,需 ...

  4. STL源码分析-AVL树-RB树

    AVL树 不平衡情况 插入节点位于左子节点的左子树(左左) 插入节点位于左子节点的右子树(左右) 插入节点位于右子节点的左子树(右左) 插入节点位于右子节点的右子树(右右) 左左.右右为外侧插入,左右 ...

  5. windbg配置问题汇总

    .loadby sos.dll mscorwks.symfix c:\windows\symbols windbg配置问题汇总 1.Failed to find runtime DLL (clr.dl ...

  6. PHP实现物流查询(通过快递网API实现)

    物流查询实现 引 言:目前快递公司太多了,不可能一个一个去申请api查询.这个时候,就可以通过合作,找一些中间商合作.我试了两家,一家是快递100,一家是快递网. 他们都需要申请key.但是快递100 ...

  7. _func_

    __func__标识符 引用:http://blog.csdn.net/zhoujunyi/article/details/1572325 __func__是C99标准里面预定义标识符, 它是一个st ...

  8. [转]基于AnyCAD的准双曲面齿轮建模

    基于AnyCAD的准双曲面齿轮建模 作者:谨阳 (文章来源:http://www.opencascade.net/ask/?/article/6) 摘要:根据准双面齿轮的加工方法和传动特性,对准双面齿 ...

  9. Android开发教程 录音和播放

    首先要了解andriod开发中andriod多媒体框架包含了什么,它包含了获取和编码多种音频格式的支持,因此你几耍轻松把音频合并到你的应用中,若设备支持,使用MediaRecorder APIs便可以 ...

  10. 在mac系统上安装Eclipse,编写java程序

    第一步:安装java jre(java 运行环境). 如图所示: 登陆Oralce官网,点击Download选项,找到如图所示界面: 选择Java Runtime Environment(JRE),打 ...