重头戏!ZeroMQ的请求-响应模式详解:ZMQ_REP、ZMQ_REQ

2024-02-16 03:30

本文主要是介绍重头戏!ZeroMQ的请求-响应模式详解:ZMQ_REP、ZMQ_REQ,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、ØMQ模式总览

  • ØMQ支持多种模式,具体可以参阅:https://blog.csdn.net/qq_41453285/article/details/106865539
  • 本文介绍ØMQ的“请求-响应”模式

二、请求-响应模式

  • 请求-响应模式由http://rfc.zeromq.org/spec:28正式定义
  • 请求-应答模式应该是最常见的交互模式,如果连接之后,服务器终止,那么客户端也终止,从崩溃的过程中恢复不太容易
  • 因此,做一个可靠的请求-应答模式很复杂,在很后面我们会有一部分系列文章介绍“可靠的请求-应答模式”
  • 请求-响应模型”支持的套接字类型有4种:
    • ZMQ_REP
    • ZMQ_REQ
    • ZMQ_DEALER
    • ZMQ_ROUTER

三、“REQ-REP”套接字类型

  • 请求-响应模式用于将请求从ZMQ_REQ客户端发送到一个或多个ZMQ_REP服务,并接收对每个发送的请求的后续答复
  • REQ-REP套接字对是步调一致的。它们两者的次序必须有规则,不能同时发送或接收,否则无效果

ZMQ_REQ

  • 客户端使用ZMQ_REQ类型的套接字向服务发送请求并从服务接收答复
  • 此套接字类型仅允许zmq_send(request)和后续zmq_recv(reply)调用交替序列。发送的每个请求都在所有服务中轮流轮询,并且收到的每个答复都与最后发出的请求匹配
  • 如果没有可用的服务,则套接字上的任何发送操作都应阻塞,直到至少有一项服务可用为止。REQ套接字不会丢弃消息
                                                                             ZMQ_REQ特性摘要 
兼容的对等套接字ZMQ_REP、ZMQ_ROUTER
方向双向
发送/接收模式发送、接收、发送、接收......
入网路由策略最后一位(Last peer)

外发路由策略

轮询
静音状态下的操作阻塞

ZMQ_REP

  • 服务使用ZMQ_REP类型的套接字来接收来自客户端的请求并向客户端发送回复
  • 此套接字类型仅允许zmq_recv(request)和后续zmq_send(reply)调用的交替序列。接收到的每个请求都从所有客户端中公平排队,并且发送的每个回复都路由到发出最后一个请求的客户端
  • 如果原始请求者不再存在,则答复将被静默丢弃
                                                                           ZMQ_REP特性摘要 
兼容的对等套接字ZMQ_REQ、ZMQ_DEALER
方向双向
发送/接收模式接收、发送、接收、发送......
入网路由策略公平排队

外发路由策略

最后一位(Last peer)
  • 演示案例如下:
    • 服务端创建REP套接字,阻塞等待客户端消息的到达,当客户端有消息达到时给客户端回送“World”字符串
    • 客户端创建REP套接字,向服务端发送字符串“Hello”,然后等待服务端回送消息

  • 服务端代码如下:
// https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/hwserver.c
// hwserver.c
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <zmq.h>// 向socket发送数据, 数据为string
static int s_send(void *socket, char *string);
// 从socket接收数据, 并将数据以字符串的形式返回
static char *s_recv(void *socket);int main()
{// 1.创建上下文void *context = zmq_ctx_new();// 2.创建、绑定套接字void *responder = zmq_socket(context, ZMQ_REP);zmq_bind(responder, "tcp://*:5555");int rc;// 3.循环接收数据、发送数据while(1){// 4.接收数据char *request = s_recv(responder);assert(request != NULL);printf("Request: %s\n", request);free(request);// 休眠1秒再继续回复sleep(1);// 5.回送数据char *reply = "World";rc = s_send(responder, reply);assert(rc > 0);}// 6.关闭套接字、销毁上下文zmq_close(responder);zmq_ctx_destroy(context);return 0;
}static int s_send(void *socket, char *string)
{int rc;zmq_msg_t msg;zmq_msg_init_size(&msg, 5);memcpy(zmq_msg_data(&msg), string, strlen(string));rc = zmq_msg_send(&msg, socket, 0);zmq_msg_close(&msg);return rc;
}static char *s_recv(void *socket)
{int rc;zmq_msg_t msg;zmq_msg_init(&msg);rc = zmq_msg_recv(&msg, socket, 0);if(rc == -1)return NULL;char *string = (char*)malloc(rc + 1);memcpy(string, zmq_msg_data(&msg), rc);zmq_msg_close(&msg);string[rc] = 0;return string;
}
  • 还有一个使用C++ API编写的服务端,可参阅:https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/hwserver.cpp
  • 客户端代码如下:
// https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/hwclient.c
// hwclient.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <zmq.h>// 向socket发送数据, 数据为string
static int s_send(void *socket, char *string);
// 从socket接收数据, 并将数据以字符串的形式返回
static char *s_recv(void *socket);int main()
{// 1.创建上下文void *context = zmq_ctx_new();// 2.创建套接字、连接服务器void *requester = zmq_socket(context, ZMQ_REQ);zmq_connect(requester, "tcp://localhost:5555");int rc;// 3.循环发送数据、接收数据while(1){// 4.发送数据char *request = "Hello";rc = s_send(requester, request);assert(rc > 0);// 5.接收回复数据char *reply = s_recv(requester);assert(reply != NULL);printf("Reply: %s\n", reply);free(reply);}// 6.关闭套接字、销毁上下文zmq_close(requester);zmq_ctx_destroy(context);return 0;
}static int s_send(void *socket, char *string)
{int rc;zmq_msg_t msg;zmq_msg_init_size(&msg, 5);memcpy(zmq_msg_data(&msg), string, strlen(string));rc = zmq_msg_send(&msg, socket, 0);zmq_msg_close(&msg);return rc;
}static char *s_recv(void *socket)
{int rc;zmq_msg_t msg;zmq_msg_init(&msg);rc = zmq_msg_recv(&msg, socket, 0);if(rc == -1)return NULL;char *string = (char*)malloc(rc + 1);memcpy(string, zmq_msg_data(&msg), rc);zmq_msg_close(&msg);string[rc] = 0;return string;
}
  • 编译并运行如下,左侧为服务端,右侧为客户端:
gcc -o hwserver hwserver.c -lzmq
gcc -o hwclient hwclient.c -lzmq

四、“DEALER-ROUTER”套接字类型

  • 本文介绍“DEALER-ROUTER”的语法和代理演示案例,在后面的一个专题中我们将介绍如何使用“DEALER-ROUTER”来构建各种异步请求-应答流

ZMQ_DEALER

  • ZMQ_DEALER类型的套接字是用于扩展“请求/应答”套接字的高级模式
  • 发送消息时:当ZMQ_DEALER套接字由于已达到所有对等点的最高水位而进入静音状态时,或者如果根本没有任何对等点,则套接字上的任何zmq_send()操作都应阻塞,直到静音状态结束或至少一个对等方变得可以发送;消息不会被丢弃
  • 接收消息时:发送的每条消息都是在所有连接的对等方之间进行轮询,并且收到的每条消息都是从所有连接的对等方进行公平排队的
  • 将ZMQ_DEALER套接字连接到ZMQ_REP套接字时,发送的每个消息都必须包含一个空的消息部分,定界符以及一个或多个主体部分
                                                                             ZMQ_DEALER特性摘要 
兼容的对等套接字ZMQ_ROUTER、ZMQ_REP、ZMQ_DEALER
方向双向
发送/接收模式无限制
入网路由策略公平排队

外发路由策略

轮询
静音状态下的操作阻塞

ZMQ_ROUTER

  • ZMQ_ROUTER类型的套接字是用于扩展请求/答复套接字的高级套接字类型
  • 当收到消息时:ZMQ_ROUTER套接字在将消息传递给应用程序之前,应在消息部分之前包含消息的始发对等方的路由ID。接收到的消息从所有连接的同级之间公平排队
  • 发送消息时:
    • ZMQ_ROUTER套接字应删除消息的第一部分,并使用它来确定消息应路由到的对等方的_routing id _。如果该对等点不再存在或从未存在,则该消息将被静默丢弃
    • 但是,如果ZMQ_ROUTER_MANDATORY套接字选项设置为1,这两种情况下套接字都将失败并显示EHOSTUNREACH
  • 高水位标记影响:
    • 当ZMQ_ROUTER套接字由于达到所有同位体的高水位线而进入静音状态时,发送到该套接字的任何消息都将被丢弃,直到静音状态结束为止。同样,任何路由到已达到单个高水位标记的对等方的消息也将被丢弃
    • 如果ZMQ_ROUTER_MANDATORY套接字选项设置为1,则在两种情况下套接字都应阻塞或返回EAGAIN
  • ZMQ_ROUTER_MANDATORY套接字选项:
    • 当ZMQ_ROUTER套接字的ZMQ_ROUTER_MANDATORY标志设置为1时,套接字应在接收到来自一个或多个对等方的消息后生成ZMQ_POLLIN事件
    • 同样,当至少一个消息可以发送给一个或多个对等方时,套接字将生成ZMQ_POLLOUT事件
  • 当ZMQ_REQ套接字连接到ZMQ_ROUTER套接字时,除了始发对等方的路由ID外,每个收到的消息都应包含一个空的定界符消息部分。因此,由应用程序看到的每个接收到的消息的整个结构变为:一个或多个路由ID部分,定界符部分,一个或多个主体部分。将回复发送到ZMQ_REQ套接字时,应用程序必须包括定界符部分
                                                                             ZMQ_ROUTER特性摘要 
兼容的对等套接字ZMQ_DEALER、ZMQ_REQ、ZMQ_ROUTER
方向双向
发送/接收模式无限制
入网路由策略公平排队

外发路由策略

看上面介绍
静音状态下的操作丢弃(见上面介绍)

共享队列/代理

  • 在“REP-REQ”的演示案例中,我们只有一个客户端和一个服务端进行交流。但是实际中,我们通常允许多个客户端与多个服务端之间相互交流
  • 将多个客户端连接到多个服务器的方法有两种:
    • 方法①:将每个客户端都连接到多个服务端点
    • 方法②:使用代理

方法①:

  • 原理:一种是将每个客户端套接字连接到多个服务端点。REQ套接字随后会将请求发送到服务端上。比如说一个客户端连接了三个服务端点:A、B、C,之后发送请求R1、R4到服务A上,发送请求R2到服务B上、发送请求R3到服务C上(如下图所示)
  • 对于这种设计来说,服务器属于静态部分,客户端属于动态部分,客户端的增减无所谓,但是服务器的增减确实致命的。假设现在有100个客户端都连接了服务器,如果此时新增了三台服务器,为了让客户端识别这新增的三台服务器,那么就需要将所有的客户端都停止重新配置然后再重新启动

方法②:

  • 我们可以编写一个小型消息排队代理,使我们具备灵活性
  • 原理:该代理绑定到了两个端点,一个用于客户端的前端(ZMQ_ROUTER),另一个用于服务的后端(ZMQ_DEALER)。然后带来使用zmq_poll()来轮询这两个套接字的活动,当有消息时,代理会将消息在两个套接字之间频繁地输送
  • 该代理其实并不显式管理任何队列,其只负责消息的传送,ØMQ会自动将消息在每个套接字上进行排队
  • 使用zmq_poll()配合DEALER-ROUTER:
    • 在上面我们使用REQ-REP套接字时,会有一个严格同步的请求-响应对话,就是必须客户端先发送一个请求,然后服务端读取请求并发送应答,最后客户端读取应答,如此反复。如果客户端或服务端尝试破坏这种约束(例如,连续发送两个请求,而没有等待响应),那么将返回一个错误
    • 我们的代理必须是非阻塞的,可以使用zmq_poll()来轮询任何一个套接字上的活动,但我们不能使用REQ-REQ。幸运地是,有两个称为DEALER和ROUTER的套接字,它们能我们可以执行无阻塞的请求-响应

  • 代理演示案例如下所示:我们扩展了上面的“REQ-REP”演示案例:REQ和ROUTER交流,DEALER与REP交流。代理节点从一个套接字读取消息,并将消息转发到其他套接字

  • 客户端的代码如下:将REQ套接字连接到代理的ROUTER节点上,向ROUTER节点发送“Hello”,接收到“World”的回复
// rrclient.c
// https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/rrclient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zmq.h>// 向socket发送数据, 数据为string
static int s_send(void *socket, char *string);
// 从socket接收数据, 并将数据以字符串的形式返回
static char *s_recv(void *socket);int main()
{int rc;// 1.初始化上下文void *context = zmq_ctx_new();// 2.创建套接字、连接代理的ROUTER端void *requester = zmq_socket(context, ZMQ_REQ);rc = zmq_connect(requester, "tcp://localhost:5559");if(rc == -1){perror("zmq_connect");zmq_close(requester);zmq_ctx_destroy(context);return -1;}// 3.循环发送、接收数据(10次)int request_nbr;for(request_nbr = 0; request_nbr < 10; request_nbr++){// 4.先发送数据rc = s_send(requester, "Hello");if(rc < 0){perror("s_send");zmq_close(requester);zmq_ctx_destroy(context);return -1;}// 5.等待响应char *reply = s_recv(requester);if(reply == NULL){perror("s_recv");free(reply);zmq_close(requester);zmq_ctx_destroy(context);return -1;}printf("Reply[%d]: %s\n", request_nbr + 1, reply);free(reply);}// 6.关闭套接字、销毁上下文zmq_close(requester);zmq_ctx_destroy(context);return 0;
}static int s_send(void *socket, char *string)
{int rc;zmq_msg_t msg;zmq_msg_init_size(&msg, 5);memcpy(zmq_msg_data(&msg), string, strlen(string));rc = zmq_msg_send(&msg, socket, 0);zmq_msg_close(&msg);return rc;
}static char *s_recv(void *socket)
{int rc;zmq_msg_t msg;zmq_msg_init(&msg);rc = zmq_msg_recv(&msg, socket, 0);if(rc == -1)return NULL;char *string = (char*)malloc(rc + 1);memcpy(string, zmq_msg_data(&msg), rc);zmq_msg_close(&msg);string[rc] = 0;return string;
}
  • 服务端的代码如下:将REP套接字连接到代理的DEALER节点上
// rrworker.c
// https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/rrworker.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <zmq.h>// 向socket发送数据, 数据为string
static int s_send(void *socket, char *string);
// 从socket接收数据, 并将数据以字符串的形式返回
static char *s_recv(void *socket);int main()
{int rc;// 1.初始化上下文void *context = zmq_ctx_new();// 2.创建套接字、连接代理的DEALER端void *responder = zmq_socket(context, ZMQ_REP);rc = zmq_connect(responder, "tcp://localhost:5560");if(rc == -1){perror("zmq_connect");zmq_close(responder);zmq_ctx_destroy(context);return -1;}// 3.循环接收、响应while(1){// 4.先等待接收数据char *request = s_recv(responder);if(request == NULL){perror("s_recv");free(request);zmq_close(responder);zmq_ctx_destroy(context);return -1;}printf("Request: %s\n", request);free(request);// 休眠1秒再进行响应sleep(1);// 5.响应rc = s_send(responder, "World");if(rc < 0){perror("s_send");zmq_close(responder);zmq_ctx_destroy(context);return -1;}}// 6.关闭套接字、销毁上下文zmq_close(responder);zmq_ctx_destroy(context);return 0;
}static int s_send(void *socket, char *string)
{int rc;zmq_msg_t msg;zmq_msg_init_size(&msg, 5);memcpy(zmq_msg_data(&msg), string, strlen(string));rc = zmq_msg_send(&msg, socket, 0);zmq_msg_close(&msg);return rc;
}static char *s_recv(void *socket)
{int rc;zmq_msg_t msg;zmq_msg_init(&msg);rc = zmq_msg_recv(&msg, socket, 0);if(rc == -1)return NULL;char *string = (char*)malloc(rc + 1);memcpy(string, zmq_msg_data(&msg), rc);zmq_msg_close(&msg);string[rc] = 0;return string;
}
  • 代理端的代码如下:
    • 创建一个ROUTER套接字与客户端相连接,创建一个DEALER套接字与服务端相连接
    • ROUTER套接字从客户端接收请求数据,并把请求数据发送给服务端
    • DEALER套接字从服务端接收响应数据,并把响应数据发送给客户端
// rrbroker.c
// https://github.com/dongyusheng/csdn-code/blob/master/ZeroMQ/rrbroker.c
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zmq.h>int main()
{int rc;// 1.初始化上下文void *context = zmq_ctx_new();// 2.创建、绑定套接字void *frontend = zmq_socket(context, ZMQ_ROUTER);void *backend = zmq_socket(context, ZMQ_DEALER);// ZMQ_ROUTER绑定到5559, 接收客户端的请求rc = zmq_bind(frontend, "tcp://*:5559");if(rc == -1){perror("zmq_bind");zmq_close(frontend);zmq_close(backend);zmq_ctx_destroy(context);return -1;}// ZMQ_DEALER绑定到5560, 接收服务端的回复rc = zmq_bind(backend, "tcp://*:5560");if(rc == -1){perror("zmq_bind");zmq_close(frontend);zmq_close(backend);zmq_ctx_destroy(context);return -1;}// 3.初始化轮询集合zmq_pollitem_t items[] = {{ frontend, 0, ZMQ_POLLIN, 0 },{ backend, 0, ZMQ_POLLIN, 0 }};// 4.在套接字上切换消息while(1){zmq_msg_t msg;//多部分消息检测int more;     // 5.调用zmq_poll轮询消息rc = zmq_poll(items, 2, -1);//zmq_poll出错if(rc == -1)     {perror("zmq_poll");zmq_close(frontend);zmq_close(backend);zmq_ctx_destroy(context);return -1;}//zmq_poll超时else if(rc == 0) continue;else{// 6.如果ROUTER套接字有数据来if(items[0].revents & ZMQ_POLLIN){while(1){// 从ROUTER上接收数据, 这么数据是客户端发送过来的"Hello"zmq_msg_init(&msg);zmq_msg_recv(&msg, frontend, 0);// 查看是否是接收多部分消息, 如果后面还有数据要接收, 那么more会被置为1size_t more_size = sizeof(more);zmq_getsockopt(frontend, ZMQ_RCVMORE, &more, &more_size);// 接收"Hello"之后, 将数据发送到DEALER上, DEALER会将"Hello"发送给服务端zmq_msg_send(&msg, backend, more ? ZMQ_SNDMORE : 0);zmq_msg_close(&msg);// 如果没有多部分数据可以接收了, 那么退出循环if(!more)break;}}// 7.如果DEALER套接字有数据来if(items[1].revents & ZMQ_POLLIN){while(1){// 接收服务端的响应"World"zmq_msg_init(&msg);zmq_msg_recv(&msg, backend, 0);// 查看是否是接收多部分消息, 如果后面还有数据要接收, 那么more会被置为1size_t more_size = sizeof(more);zmq_getsockopt(backend, ZMQ_RCVMORE, &more, &more_size);// 接收"World"之后, 将数据发送到ROUTER上, ROUTER会将"World"发送给客户端zmq_msg_send(&msg, frontend, more ? ZMQ_SNDMORE : 0);zmq_msg_close(&msg);// 如果没有多部分数据可以接收了, 那么退出循环if(!more)break;}}}}// 8.关闭套接字、销毁上下文zmq_close(frontend);zmq_close(backend);zmq_ctx_destroy(context);return 0;
}
  • 编译如下:
gcc -o rrclient rrclient.c -lzmq
gcc -o rrworker rrworker.c -lzmq
gcc -o rrbroker rrbroker.c -lzmq
  • 一次运行如下,左侧为客户端,中间为代理,右侧为服务端

  • 下面运行两个客户端,0为代理,1、2为客户端,3位服务端。可以看到客户端的消息是有顺序到达客户端的,消息会自动进行排队

  • ØMQ自己提供了代理的接口zmq_proxy(),可以省略上面代码的书写,详情可参阅:https://blog.csdn.net/qq_41453285/article/details/106887035。

  • 我是小董,V公众点击"笔记白嫖"解锁更多【ZeroMQ】资料内容。

这篇关于重头戏!ZeroMQ的请求-响应模式详解:ZMQ_REP、ZMQ_REQ的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/713364

相关文章

Java中switch-case结构的使用方法举例详解

《Java中switch-case结构的使用方法举例详解》:本文主要介绍Java中switch-case结构使用的相关资料,switch-case结构是Java中处理多个分支条件的一种有效方式,它... 目录前言一、switch-case结构的基本语法二、使用示例三、注意事项四、总结前言对于Java初学者

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

详解Java中的敏感信息处理

《详解Java中的敏感信息处理》平时开发中常常会遇到像用户的手机号、姓名、身份证等敏感信息需要处理,这篇文章主要为大家整理了一些常用的方法,希望对大家有所帮助... 目录前后端传输AES 对称加密RSA 非对称加密混合加密数据库加密MD5 + Salt/SHA + SaltAES 加密平时开发中遇到像用户的

Springboot使用RabbitMQ实现关闭超时订单(示例详解)

《Springboot使用RabbitMQ实现关闭超时订单(示例详解)》介绍了如何在SpringBoot项目中使用RabbitMQ实现订单的延时处理和超时关闭,通过配置RabbitMQ的交换机、队列和... 目录1.maven中引入rabbitmq的依赖:2.application.yml中进行rabbit

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Python绘制土地利用和土地覆盖类型图示例详解

《Python绘制土地利用和土地覆盖类型图示例详解》本文介绍了如何使用Python绘制土地利用和土地覆盖类型图,并提供了详细的代码示例,通过安装所需的库,准备地理数据,使用geopandas和matp... 目录一、所需库的安装二、数据准备三、绘制土地利用和土地覆盖类型图四、代码解释五、其他可视化形式1.

SpringBoot使用Apache POI库读取Excel文件的操作详解

《SpringBoot使用ApachePOI库读取Excel文件的操作详解》在日常开发中,我们经常需要处理Excel文件中的数据,无论是从数据库导入数据、处理数据报表,还是批量生成数据,都可能会遇到... 目录项目背景依赖导入读取Excel模板的实现代码实现代码解析ExcelDemoInfoDTO 数据传输

如何用Java结合经纬度位置计算目标点的日出日落时间详解

《如何用Java结合经纬度位置计算目标点的日出日落时间详解》这篇文章主详细讲解了如何基于目标点的经纬度计算日出日落时间,提供了在线API和Java库两种计算方法,并通过实际案例展示了其应用,需要的朋友... 目录前言一、应用示例1、天安门升旗时间2、湖南省日出日落信息二、Java日出日落计算1、在线API2

使用Spring Cache时设置缓存键的注意事项详解

《使用SpringCache时设置缓存键的注意事项详解》在现代的Web应用中,缓存是提高系统性能和响应速度的重要手段之一,Spring框架提供了强大的缓存支持,通过​​@Cacheable​​、​​... 目录引言1. 缓存键的基本概念2. 默认缓存键生成器3. 自定义缓存键3.1 使用​​@Cacheab

详解Java中如何使用JFreeChart生成甘特图

《详解Java中如何使用JFreeChart生成甘特图》甘特图是一种流行的项目管理工具,用于显示项目的进度和任务分配,在Java开发中,JFreeChart是一个强大的开源图表库,能够生成各种类型的图... 目录引言一、JFreeChart简介二、准备工作三、创建甘特图1. 定义数据集2. 创建甘特图3.