使用Java实现简单gRPC开发
1. gRPC简介
gRPC(全称为gRPC Remote Procedure Call)是一种高性能、开源和通用的远程过程调用(RPC)框架,由Google开发。gRPC基于HTTP/2协议设计,使用ProtoBuf(全称Protocol Buffers)作为接口描述语言。gRPC原生方面支持了C++, Java, Python, Go, Ruby等语言,不同语言的应用程序能够方便地进行通信。
ProtoBuf是一种由Google开发的轻量级、高效的二进制数据序列化格式,用于结构化数据的序列化和反序列化。 ProtoBuf采用二进制格式,相比于文本格式,序列化后的数据更紧凑,解析速度更快,占用的空间更小。这使得它特别适用于对性能要求较高的场景。使用ProtoBuf语言定义文件来描述数据结构和消息格式。这些文件可以用于生成不同编程语言的代码,使得在不同平台和语言之间进行数据交换更为方便。
在gRPC中,需要使用ProtoBuf定义好消息与服务。这是一种轻量级、高效的二进制序列化格式。ProtoBuf允许你定义数据结构和服务接口,而且提供了代码生成工具,可以生成Server端和Client端的代码。在后续开发中,Server端需要实现定义好的接口,而在Client端,只需要调用定义好的方法即可完成传参。
gRPC支持四种调用方式,包括简单调用、服务器流式调用、客户端流式调用和双向流式调用,满足不同应用场景的需求。
- 简单调用,客户端发起一次请求,服务端响应一个数据。
当client发起调用后,提交数据,并且等待 服务端响应。
在开发过程中,主要采用简单调用的这种通信方式
rpc fun1(Request) returns (Response) {}
- 服务器流式调用,一个请求对象,服务端可以回传多个结果对象。
rpc fun2(Request) returns (stream Response) {}
- 客户端流式调用,客户端发送多个请求对象,服务端只返回一个结果。
rpc fun3(stream Request) returns (Response) {}
- 双向流式调用,客户端可以发送多个请求消息,服务端响应多个响应消息。
应用场景:聊天室
rpc fun4(stream Request) returns (stream Response) {}
2. 开发环境的搭建
整体项目结构如下
引入相关gRPC依赖
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.60.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.60.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.60.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
</dependencies>
3. 实现
3.1 定义服务
在proto文件夹下创建Chat.proto文件,定义消息格式为sender、content,定义服务包括SendMessage、JoinChat。
syntax = "proto3";
option java_multiple_files = false;
option java_package = "com.jy";
option java_outer_classname = "ChatProto";
message ChatMessage {
string sender = 1;
string content = 2;
}
service ChatService {
rpc SendMessage(ChatMessage) returns (ChatMessage);
rpc JoinChat(ChatMessage) returns (ChatMessage);
}
3.2 生成Java代码
在pom.xml文件中加入以下配置,即可使用Maven中的compile与compile-custom生成代码。compile用于生成消息相关类,compile-custom生成接口。
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.24.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.60.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
将生成的代码复制到/src/main/java/com.jy中
3.3 Server端
package com.jy;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
public class ChatServer {
private final int port;
private final Server server;
// 构造函数,初始化端口和 gRPC 服务器
public ChatServer(int port) {
this.port = port;
this.server = ServerBuilder.forPort(port)
.addService(new ChatServiceImpl())
.build();
}
// 启动服务器的方法
public void start() throws Exception {
server.start();
System.out.println("服务器已启动,端口号:" + port);
// 注册 JVM 关闭钩子,用于在 JVM 关闭时停止服务器
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("正在关闭服务器");
ChatServer.this.stop();
}));
}
// 停止服务器的方法
private void stop() {
if (server != null) {
server.shutdown();
}
}
// 内部类,实现 gRPC 服务接口
private static class ChatServiceImpl extends ChatServiceGrpc.ChatServiceImplBase {
// 处理客户端发送消息的方法
@Override
public void sendMessage(ChatProto.ChatMessage request, StreamObserver<ChatProto.ChatMessage> responseObserver) {
System.out.println("收到来自 " + request.getSender() + " 的消息:" + request.getContent());
// 构建服务器回复消息
ChatProto.ChatMessage response = ChatProto.ChatMessage.newBuilder()
.setSender("服务器")
.setContent("已成功接收您的消息")
.build();
// 发送回复消息给客户端
responseObserver.onNext(response);
responseObserver.onCompleted();
}
// 处理客户端加入聊天的方法
@Override
public void joinChat(ChatProto.ChatMessage request, StreamObserver<ChatProto.ChatMessage> responseObserver) {
System.out.println(request.getSender() + " 加入了聊天");
// 构建服务器欢迎消息
ChatProto.ChatMessage response = ChatProto.ChatMessage.newBuilder()
.setSender("服务器")
.setContent("欢迎加入聊天," + request.getSender() + "!")
.build();
// 发送欢迎消息给客户端
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
// 主函数,启动服务器并等待终止
public static void main(String[] args) throws Exception {
int port = 50051;
ChatServer chatServer = new ChatServer(port);
chatServer.start();
chatServer.server.awaitTermination();
}
}
3.4 Client端
package com.jy;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import java.util.Scanner;
public class ChatClient {
private final ManagedChannel channel;
private final ChatServiceGrpc.ChatServiceStub chatStub;
// 构造函数,初始化 gRPC 通道和服务存根
public ChatClient(String host, int port) {
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
this.chatStub = ChatServiceGrpc.newStub(channel);
}
// 关闭客户端的方法
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
}
// 启动聊天的方法
public void startChat(String username) {
// 创建用于接收服务器消息的观察者
StreamObserver<ChatProto.ChatMessage> responseObserver = new StreamObserver<>() {
@Override
public void onNext(ChatProto.ChatMessage message) {
System.out.println(message.getSender() + " " + message.getContent());
}
@Override
public void onError(Throwable throwable) {
// 处理错误
}
@Override
public void onCompleted() {
// 处理完成事件
}
};
// 创建加入聊天的请求消息
ChatProto.ChatMessage joinRequest = ChatProto.ChatMessage.newBuilder()
.setSender(username)
.setContent("加入聊天")
.build();
// 发送加入聊天请求
chatStub.joinChat(joinRequest, responseObserver);
// 启动新线程用于发送用户输入的消息
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String message = scanner.nextLine();
// 创建聊天消息请求
ChatProto.ChatMessage chatRequest = ChatProto.ChatMessage.newBuilder()
.setSender(username)
.setContent(message)
.build();
// 发送聊天消息请求
chatStub.sendMessage(chatRequest, responseObserver);
}
}).start();
}
// 主函数,用于启动客户端
public static void main(String[] args) {
String host = "localhost";
int port = 50051;
ChatClient chatClient = new ChatClient(host, port);
try {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入您的用户名:");
String username = scanner.nextLine();
// 启动聊天
chatClient.startChat(username);
// 主线程等待
Thread.currentThread().join();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
// 关闭客户端
chatClient.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4. 运行结果
Server端与Client端的代码均正常执行,Server端可以正常接受请求以及向客户端发送消息。
5. 课程总结
网络程序设计这门课程中,我学习了很多东西,不同于其他课程,我相信这门课可以对我求职起到很大的作用。
在这门课程中,首先学习了在浏览器端和服务器端使用JavaScript进行网络开发的方法,这包括使用Ajax进行异步通信、处理JSON数据、以及利用WebSocket实现实时通信等;学习了epoll并发编程,这是一种在Linux系统上高效处理大量并发连接的方法,通过epoll,我学到了如何在非阻塞模式下管理多个套接字,提高了程序的性能和可伸缩性;学习gRPC让我接触到了远程过程调用(RPC)框架,通过定义服务和消息类型,能够轻松地生成跨语言的客户端和服务器端代码,实现这些方法;学习Linux内核网络协议栈,我了解了网络协议的实现原理,包括TCP/IP协议栈、套接字接口等。通过深入研究内核层面的网络机制,我对操作系统如何管理和处理网络通信有了更全面的认识。
孟宁老师每次上课都会和同学们积极互动,调动大家的积极性。这是一门十分有趣的课程!