首页 > 其他分享 >研究gRPC所给的helloworld例子

研究gRPC所给的helloworld例子

时间:2024-07-12 16:55:54浏览次数:21  
标签:set proto gRPC helloworld echo grpc 例子 include

这里我以编写一个远程过程调用,客户端传过来请求,远程过程调用就可以返回当前时间。(daytime服务器熟知端口是13,这里并不是搭建daytime,只是为了测试远程过程调用是否成功)

CMakeLists.txt文件的编写

cmake_minimum_required(VERSION 3.8)
project(HelloWorld C CXX)
include(../cmake/common.cmake)

前两句都是标准的CMakeLists.txt的开头,后面的include​为本项目引入了一个叫common.cmake​的文件,那这个文件里面有什么东西?

common.cmake的内容解析

cmake_minimum_required(VERSION 3.8)
if(MSVC)
	add_definitions(-D_WIN32_WINNT=0x600)
endif()
find_package(Threads REQUIRED)

这里好像探测了一下是否是在Windows平台下的MSVC编译的,并且为MSVC编译器定义了某个参数。

后面又去找包,这个package它cmake怎么找到的,我不记得自己安装过这样的库

find_package(Threads REQUIRED)会让cmake依据不同的平台(Linux、Windows等)查找能使用的线程库,比如:Linux下的POSIX,Windows的线程库。

CMake会自动为我们配置项目以使用系统的线程库。一旦成功,你就可以在你的CMakeLists.txt中使用由Threads模块提供的变量和命令来编写使用线程的代码。

但这样我有个问题,不同平台的线程库接口API应该不同吧!该如何进行兼容?

if(GRPC_AS_SUBMODULE)
	add_subdirectory(../../.. ${CMAKE_CURRENT_BINARY_DIR}/grpc EXCLUDE_FROM_ALL)
	message(STATUS "Using gRPC via add_subdirectory.")
	set(_PROTOBUF_LIBPROTOBUF libprotobuf)
	set(_REFLECTION grpc++_reflection)
	if(CMAKE_CROSSCOMPILING)
    	find_program(_PROTOBUF_PROTOC protoc)
  	else()
    	set(_PROTOBUF_PROTOC $<TARGET_FILE:protobuf::protoc>)
  	endif()
  	set(_GRPC_GRPCPP grpc++)
  	if(CMAKE_CROSSCOMPILING)
    	find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)
  	else()
    	set(_GRPC_CPP_PLUGIN_EXECUTABLE $<TARGET_FILE:grpc_cpp_plugin>)
  	endif()
elseif(GRPC_FETCHCONTENT)
	message(STATUS "Using gRPC via add_subdirectory (FetchContent).")
    include(FetchContent)
	  FetchContent_Declare(
	    grpc
	    GIT_REPOSITORY https://github.com/grpc/grpc.git
	    GIT_TAG        vGRPC_TAG_VERSION_OF_YOUR_CHOICE)
	  FetchContent_MakeAvailable(grpc)
  	set(_PROTOBUF_LIBPROTOBUF libprotobuf)
	set(_REFLECTION grpc++_reflection)
	set(_PROTOBUF_PROTOC $<TARGET_FILE:protoc>)
	set(_GRPC_GRPCPP grpc++)
	if(CMAKE_CROSSCOMPILING)
	  find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)
	else()
	  set(_GRPC_CPP_PLUGIN_EXECUTABLE $<TARGET_FILE:grpc_cpp_plugin>)
	endif()
else()
  option(protobuf_MODULE_COMPATIBLE TRUE)
  find_package(Protobuf CONFIG REQUIRED)
  message(STATUS "Using protobuf ${Protobuf_VERSION}")

  set(_PROTOBUF_LIBPROTOBUF protobuf::libprotobuf)
  set(_REFLECTION gRPC::grpc++_reflection)
  if(CMAKE_CROSSCOMPILING)
    find_program(_PROTOBUF_PROTOC protoc)
  else()
    set(_PROTOBUF_PROTOC $<TARGET_FILE:protobuf::protoc>)
  endif()
  # Find gRPC installation
  # Looks for gRPCConfig.cmake file installed by gRPC's cmake installation.
  find_package(gRPC CONFIG REQUIRED)
  message(STATUS "Using gRPC ${gRPC_VERSION}")

  set(_GRPC_GRPCPP gRPC::grpc++)
  if(CMAKE_CROSSCOMPILING)
    find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)
  else()
    set(_GRPC_CPP_PLUGIN_EXECUTABLE $<TARGET_FILE:gRPC::grpc_cpp_plugin>)
  endif()
endif()

对于第一个分支,具有以下注释:

构建使用gRPC的项目的一种方式是只通过add_subdirectory()​将整个gRPC项目包含起来。

这个方法使用起来很简单,但存在以下问题:

  • 它将gRPC的CMakeLists.txt直接包含到你的构建脚本中而不使用,这可能使得gRPC在内部干扰你自己的构建
  • 取决于你的系统上安装了什么,子模块的内容gRPC的third_party/*中的可能需要是可用的(并且可能有构建它们所需的前置条件)

添加对gRPC的依赖性的一种更健壮的方法是使用cmake的ExternamlProject_Add()​详细请参考cmake的相关文档

对于第二个分支,具有以下注释:

另一种方法是使用CMake的FetchContent模块克隆gRPC在配置时间里。

这使得gRPC的源代码可用于你的项目,类似与git子模块

此分支假定gRPC及其所有依赖项都已安装
因此可以通过find_package()找到它们。

查找Protobuf安装
查找由protobuf的cmake安装程序安装的protobuf-config.cmake文件。

我在使用的时候,为了避免麻烦的环境配置,可以直接使用第二种,如果已经完成了麻烦的配置,使用第三种无疑是性能最好的。

总结:common.cmake文件介绍了如何使用CMake为CPP项目引入gRPC依赖的方法,总共介绍了三种。

get_filename_component(hw_proto "../../protos/hellowrold.proto" ABSOLUTE)
get_filename_component(hw_proto_path "hw_proto" PATH)

这两语句为项目引入了一个helloworld.proto​文件,大概是为了让本项目可以直接使用该文件,或者利用该文件做出某些操作。

set(hw_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.pb.cc")
set(hw_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.pb.h")
set(hw_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.grpc.pb.cc")
set(hw_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.grpc.pb.h")

最后,将某些文件定义为了一个变量,这些文件应该在后面需要进行一些操作,注意:由于这些文件不是一开始就存在于BINARY_DIR中的,需要CMake执行完配置之后才会出现。


add_custom_command(
      OUTPUT "${hw_proto_srcs}" "${hw_proto_hdrs}" "${hw_grpc_srcs}" "${hw_grpc_hdrs}"
      COMMAND ${_PROTOBUF_PROTOC}
      ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}"
        --cpp_out "${CMAKE_CURRENT_BINARY_DIR}"
        -I "${hw_proto_path}"
        --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
        "${hw_proto}"
      DEPENDS "${hw_proto}")

在common.cmake中,会找出Protocol Buffers的编译器protoc,并将其设置到变量_PROTOBUF_PROTOC​上面,也就是说这一步执行的是protoc的编译操作。

我并不知道add_custom_command()​命令的功能,所以需要查询相关资料:

add_custion_command()允许你向构建系统中添加自定义的编译规则。这个命令主要用于以下几个场景:

  1. 生成源文件:在编译之前生成源代码文件,这些文件可由其他工具(如脚本、代码生成器等)生成的
  2. 预处理或后处理:在编译前或后执行特定的预处理或后处理
  3. 创建自定义的构建步骤

add_custom_command()可以通过多种方式被调用,包括指定目标(TARGET)、源文件(SOURCE)或独立于任何目标的自定义命令(COMMAND)

如果没有指定TARGET或SOURCE,将定义一个独立的自定义命令。这些命令的执行时机取决于它们是如何被调用的。例如,你可以使用add_custom_target()创建一个自定义的目标,并在该目标中调用这些自定义命令,从而控制它们的执行时机。


include_directories("${CMAKE_CURRENT_BINARY_DIR}")

包含通过protoc生成的头文件


add_library(hw_grpc_proto
  ${hw_grpc_srcs}
  ${hw_grpc_hdrs}
  ${hw_proto_srcs}
  ${hw_proto_hdrs})

利用protoc生成的文件,生成一个库,生成这些库不需要我们自己编写的C++代码,只是需要一个.proto​文件


target_link_libraries(hw_grpc_proto
  absl::check
  ${_REFLECTION}
  ${_GRPC_GRPCPP}
  ${_PROTOBUF_LIBPROTOBUF})

这个库需要连接一些依赖库,使用到的变量在common.cmake中被定义


foreach(_target
  greeter_client greeter_server
  greeter_callback_client greeter_callback_server
  greeter_async_client greeter_async_client2 greeter_async_server)
  add_executable(${_target} "${_target}.cc")
  target_link_libraries(${_target}
    hw_grpc_proto
    absl::check
    absl::flags
    absl::flags_parse
    absl::log
    ${_REFLECTION}
    ${_GRPC_GRPCPP}
    ${_PROTOBUF_LIBPROTOBUF})
endforeach()

使用foreach()批量生成可执行文件

protoc编译生成了什么东西?

helloworld.pb.h​文件中,首先定义了一个google::protobuf::internal::AnyMetadata​注意,命令空间google::protobuf::internal​里面只有一个AnyMetadata​而且这个类中啥玩意没有。

namespace google {
namespace protobuf {
namespace internal {
class AnyMetadata;
}  // namespace internal
}  // namespace protobuf
}  // namespace google

然后这一堆我也看不懂:

// Internal implementation detail -- do not use these members.
struct TableStruct_helloworld_2eproto {
  static const ::uint32_t offsets[];
};
extern const ::google::protobuf::internal::DescriptorTable
    descriptor_table_helloworld_2eproto;

重点是知道在helloworld.proto​文件中定义了一个package名为:helloworld

所以这里生成了类好像也放到了名为helloworld​的namespace中

namespace helloworld {
// HelloReply是在proto文件中定义好的传回参数
class HelloReply;
struct HelloReplyDefaultTypeInternal; // 这个我也不太懂是什么玩意
extern HelloReplyDefaultTypeInternal _HelloReply_default_instance_; // 生成一个上面的结构体的实例并暴露给外界,虽然我不知道有什么用

// HelloRequest是在proto文件中定义好的传入参数
class HelloRequest;
struct HelloRequestDefaultTypeInternal;
extern HelloRequestDefaultTypeInternal _HelloRequest_default_instance_;
}  // namespace helloworld

最后就是为我们生成了HelloReply和HelloRequest这两个类的实现代码,这两个类的父类都是:google::protobuf::Message​。

然后在这之上实现了一些方法,不过都是自动生成的,我暂时只需要关心如何使用这些生成的方法,了解它们有什么功能即可。

那么helloworld.pb.cc​很大概率就是helloworld.pb.h​对应的实现文件。


那么我现在好奇的是,helloworld.grpc.pb.h​这里定义了什么东西。

查看代码的结构可以发现,该文件定义了一个Greeter​类,该类中定义了Stub​和Service​(这两个都是类,但定义在Greeter内部)也就是说客户端和服务端都定义好了。还定义了一些在proto文件中定义好的方法,如:SayHello​。观看源码发现,还提供了一些异步的方法,应该是用于处理异步请求和回复。

理解上完全没有困难的好吧,只需要知道如何引入gRPC剩下的就是利用这些生成好的代码进行服务的搭建。

使用gRPC编写自定义的远程过程调用

利用examples中的分支3引入对gRPC的依赖,来搭建项目

不论是分支1直接导入整个项目过于巨大,而分支2导入gRPC由于网络问题和项目大小几乎是不可行

所以,使用分支3,也就是在本地安装库然后导入。

那么是否需要自己写内容到CMakeLists.txt文件来导入呢?

完全不需要,因为在官方所给的例子中,有给一个common.cmake​这个就是导入gRPC依赖的标准解决方案。我们要做的,无非在自己的CMakeLists.txt中定义变量,选择不同的分支(导入方式),如果是选择分支3,那么什么变量也不需要定义,因为这就是默认的导入方式。

在编写proto​文件,并利用protoc​和其gRPC插件编译proto​文件中我发现:

如果我编写的是echo.proto​文件,则编译后会产生:四个同名文件,即:

  • echo.pb.cc
  • echo.pb.h​。声明消息类
  • echo.grpc.pb.cc
  • echo.grpc.pb.h​。声明服务Service和Stub

也就是说,生成的文件名只和.proto​的文件名有关,而与该文件的具体内容无关。

但是生成的文件内容是和.proto​文件的内容有关的。

比如,我编写的echo.proto​文件内容如下:

syntax = "proto3";

package EchoTime;

service Echo {
    rpc  Time(TimeRequest) returns (TimeResponse);
}

message TimeRequest {
}

message TimeResponse {
    string time = 1;
}

那么,在echo.pb.h​中就会声明EchoTime::TimeRequest​,EchoTime::TimeResponse​前面的命名空间是由package​来指定的。这些类的一些方法将在echo.pb.cc​中得到实现。

echo.grpc.pb.h​中,EchoTime::Echo​会得到声明,而在EchoTime::Echo​中,由会有两个重要的类:

  1. EchoTime::Echo::Stub​.这是一个实现类,它本身会实现一个我在proto文件中定义的Time()​方法(该方法似乎是同步的)。这个实现了中实现了异步的方法,放在了EchoTime::Echo::async​中,可以类似于Netty设置监听者的方式,异步处理事件。
  2. EchoTime::Echo::Service​。这是一个接口类,我们需要自己实现在echo.proto​中定义的Time()​方法。具体做法就是:定义一个实现类ServiceImpl,然后实现Time()方法。

CMakeLists.txt

cmake_minimum_required(VERSION 3.22)
project(gRPC_try)

set(CMAKE_CXX_STANDARD 20)

include(common.cmake)

get_filename_component(echo_proto "./echo.proto" ABSOLUTE)
get_filename_component(echo_proto_path "${echo_proto}" PATH)

set(echo_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/echo.pb.cc") # 用ProtoBuf生成的消息类的实现
set(echo_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/echo.pb.h") # 用ProtoBuf生成的消息类的声明
set(echo_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/echo.grpc.pb.cc") # gRPC的Service和Stub的实现
set(echo_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/echo.grpc.pb.h") # gRPC的Service和Stub的声明
# 这里唯一的疑问就是,如何知道该command输出文件的顺序?
add_custom_command(
        OUTPUT "${echo_proto_srcs}" "${echo_proto_hdrs}" "${echo_grpc_srcs}" "${echo_grpc_hdrs}"
        COMMAND ${_PROTOBUF_PROTOC}
        ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}"
        --cpp_out "${CMAKE_CURRENT_BINARY_DIR}"
        -I ${echo_proto_path}
        --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
        "${echo_proto}"
        DEPENDS "${echo_proto}"
)
include_directories("${CMAKE_CURRENT_BINARY_DIR}")

add_library(echo_grpc_proto
    ${echo_grpc_srcs} ${echo_grpc_hdrs}
    ${echo_proto_srcs} ${echo_proto_hdrs}
)

target_link_libraries(echo_grpc_proto
    absl::check
    ${_REFLECTION}
    ${_GRPC_GRPCPP}
    ${_PROTOBUF_LIBPROTOBUF}
)

foreach (_target echo_server)
    add_executable(${_target} "${_target}.cpp")
    target_link_libraries(${_target}
        echo_grpc_proto
        absl::check
        absl::flags
        absl::flags_parse
        absl::log
        ${_REFLECTION}
        ${_GRPC_GRPCPP}
        ${_PROTOBUF_LIBPROTOBUF}
    )
endforeach ()

如何编写同步的服务器

#include <sstream>
#include <memory>
#include <chrono>
#include <string>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "grpcpp/ext/proto_server_reflection_plugin.h"
#include "grpcpp/grpcpp.h"
#include "grpcpp/health_check_service_interface.h"


#include "echo.grpc.pb.h"

using EchoTime::Echo;
using EchoTime::TimeRequest;
using EchoTime::TimeResponse;
using grpc::Status; // 这个不是头文件,不会暴露给外界,所以不用担心
using grpc::Server;
using grpc::ServerContext;

// 定义全局变量,并交给FLAG管理,这样可以在不将变量暴露给全局的情况下使用它
// 想要查看怎么实现的可以去研究一下源码,有兴趣就去看
ABSL_FLAG(uint16_t, port, 10000, "Server port for the service");

// 实现EchoTime::Service
class EchoServiceImpl final : public Echo::Service {
public:
    Status Time(ServerContext* context, const TimeRequest* request, TimeResponse* response) override {
        std::stringstream ss;
        // 获取当前时间点
        auto now = std::chrono::system_clock::now();
        // 将时间点转换为time_t类型,以便使用ctime库函数
        std::time_t now_c = std::chrono::system_clock::to_time_t(now);
        // 转换为本地时间
        std::tm* now_tm = std::localtime(&now_c);
        // 格式化时间输出
        ss << (now_tm->tm_year + 1900) << '-'
                  << (now_tm->tm_mon + 1) << '-'
                  << now_tm->tm_mday
                  << " " << now_tm->tm_hour << ":"
                  << now_tm->tm_min << ":"
                  << now_tm->tm_sec << '\n';
        response->set_time(ss.str()); // 这个time是在proto文件定义好的名字
        return Status::OK;
    }
};

void RunServer(uint16_t port) {
    std::string server_address = absl::StrFormat("0.0.0.0:%d", port);
    EchoServiceImpl service;
    // 以下两语句我没研究明白,官方的例子中要这样写咱也这样写,遵守别人的规定
    grpc::EnableDefaultHealthCheckService(true); // 来自"grpcpp/grpcpp.h"
    grpc::reflection::InitProtoReflectionServerBuilderPlugin();

    // 后面的就是利用Builder来创建一个Server的故事了,基本上没有理解障碍
    grpc::ServerBuilder builder;
    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
    builder.RegisterService(&service);
    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on" << server_address << std::endl;
    server->Wait();
}


int main(int argc, char** argv) {
    absl::ParseCommandLine(argc, argv);
    RunServer(absl::GetFlag(FLAGS_port));
    return 0;
}

服务器成功运转,接下来就是编写使用该服务器的客户端。

总结:

对于RPC中服务器端的编写主要步骤为:

  1. 以继承的方式,重写Service中的我们所定义的方法
  2. 完成上述方法的业务逻辑
  3. 使用ServerBuilder生成Service

客户端的编写

#include <memory>
#include <iostream>
#include <string>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"

#include <grpcpp/grpcpp.h>

#include "echo.grpc.pb.h"

ABSL_FLAG(std::string, target, "localhost:10000", "Server address");

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using EchoTime::Echo;
using EchoTime::TimeRequest;
using EchoTime::TimeResponse;

class ClockClient {
public:
    ClockClient(std::shared_ptr<Channel> channel)  : stub_(Echo::NewStub(channel)) {}
    std::string Time() {
        TimeRequest request;
        TimeResponse response;
        ClientContext context;
        Status status = stub_->Time(&context, request, &response);
        if(status.ok()) {
            return response.time();
        }
        else {
            std::cerr << status.error_code() << ": " << status.error_message() << std::endl;
            return "RPC failed";
        }
    }

private:
    std::unique_ptr<Echo::Stub> stub_;
};

int main(int argc, char** argv) {
    absl::ParseCommandLine(argc, argv);
    std::string target_str = absl::GetFlag(FLAGS_target);
    ClockClient clock(CreateChannel(target_str, grpc::InsecureChannelCredentials()));
    std::string now = clock.Time();
    std::cout << "Now time is " << now << std::endl;
    return 0;
}

成功运行,运行结果的时间我对照了一下当前时间,基本可以确定它是正确的。

总结:

客户端重点就是:准备Stub,然后准备请求的参数,准备好用来接收返回值的变量,调用方法即可。基本上跟普通的函数调用是一回事。

标签:set,proto,gRPC,helloworld,echo,grpc,例子,include
From: https://www.cnblogs.com/Aderversa/p/18298868

相关文章

  • 在C++中使用gRPC框架
    概览在gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同机器上的服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多RPC系统类似,gRPC也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包括参数和返回类型)。在服务器端实现这个接口,并运行一个gRPC......
  • PixiJS源码分析系列: 第一章 从最简单的例子入手
    从最简单的例子入手分析PixiJS源码我一般是以使用角度作为切入点查看分析源码,例子中用到什么类,什么方法,再入源码。高屋建瓴的角度咱也做不到啊,毕竟水平有限pixijs的源码之前折腾了半天都运行不起来,文档也没有明确说明如何调式我在github上看到过也有歪果仁在问如何本地......
  • 前端面试题28(Vue3的Teleport功能在什么场景下特别有用?能给个例子吗?)
    Vue3的Teleport功能在需要将组件的渲染结果放置在DOM树中与当前组件位置无关的任意位置时特别有用。这通常涉及到需要将某些UI元素(如模态框、弹出菜单、通知、工具提示等)从其逻辑上的父级组件中“提取”出来,放置到页面的更高层级或完全不同的位置,以避免样式冲突或层......
  • 目标检测小例子
    YOLO(YouOnlyLookOnce)是一种流行的目标检测算法,它以其快速和高效而闻名。YOLOv5是YOLO系列的第五个版本,它在性能和速度上都有所改进。以下是使用YOLOv5进行目标检测的一个基本示例代码,假设你已经安装了Python和必要的库,比如PyTorch和OpenCV。首先,你需要安装YOLOv5的库。......
  • 强化学习与控制模型结合例子
    强化学习与模型控制结合强化学习(ReinforcementLearning,RL)与控制模型结合,可以通过整合传统控制理论和现代RL算法,利用控制模型提供的动态信息和稳定性保障,同时利用RL的学习能力优化控制策略。这种结合的方式被称为模型辅助强化学习(Model-AssistedReinforcementLearning)......
  • OpenCV GPU解码简单例子
    基于GPU/cuda的运算能够极大解放CPU的负担,特别是针对复杂图像处理的场景中。该例子主要展示利用GPU的硬解码模块,对本地和网络视频流进行解码和本地显示。环境如下,ubuntu20.04+opencv4.10.0+cuda12.5.代码逻辑比较简单,不涉及复杂逻辑和算法,直接看代码。GPU解码本地视频并进行显......
  • if-else结构嵌套多逻辑太复杂?来试试责任链模式 一个例子教会你责任链模式
     复杂的逻辑结构小伙伴们在刚开始学习if—else结构是不是觉得也不过如此,但是有一天看到了下面一个关于汽车租赁服务的代码你又是什么感受呢do{System.out.println("请选择租车类型:1、轿车2、客车3、卡车");Scannerinput=newScanner(System.in);intch......
  • 引用个数为什么会影响内存泄漏 c++例子
    在C++中,内存泄漏通常与手动管理内存有关,而不是直接由引用计数引起,因为C++标准库本身并不提供自动的引用计数功能。但是,我们可以通过一个例子来间接说明引用(或指针)管理不当如何导致内存泄漏,尤其是当涉及复杂对象结构和所有权关系时,这种管理不当往往体现在循环引用上。基本概念......
  • 解决nacos报错 Caused by: io.grpc.netty.shaded.io.netty.channel.unix.Errors$Nati
    报错信息:org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)atorg.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)atorg......
  • java使用Netty实现TCP收发消息的例子,多线程并且含断线自动重连
    需求:有一个TCP的服务,需要使用Netty开发一个TCP连接并收发消息的程序。要求多线程并且含断线自动重连能力。组织结构,使用JavaMaven编程方式功能还包含读取配置文件和log4j2写日志部分 完整代码:App.javapackagecom.LSpbxServer;importorg.slf4j.Logger;import......