netty通信框架,如果考虑使用protubuf进行通讯,则protubuf的设计的封装非常重要,对于单机,于分布式项目来考虑,protubuf有不同的设计方案。
可以考虑,根据枚举+配置文件,进行路由封装消息体。考虑请求于响应,通知封装在一个消息体。
syntax = "proto3";
option java_multiple_files = false;
option java_package = "com.example.demo.message";
option java_outer_classname = "GDM";
package example;
enum MSG {
Test_Request=0;
Test_Response=1;
Login_Request = 2;
Login_Response = 3;
Logout_Request = 4;
Logout_Response = 5;
Keepalive_Request = 6;
Keepalive_Response = 7;
Get_Friends_Request = 8;
Get_Friends_Response = 9;
Send_Message_Request = 10;
Send_Message_Response = 11;
Friend_Notification = 12;
Message_Notification = 13;
Welcome_Notification = 14;
}
/*下面定义具体的消息内容,MSG枚举中的每个消息ID,如果有消息体,则会对应一个message 定义,如果无消息体则不必要*/
/*Login_Request 消息ID对应的消息名称为LoginRequest ; 规则为取掉下划线,有利于某些自动化编码工具编写自动化代码*/
message LoginRequest
{
string username = 1;
string password = 2;
}
message LoginResponse
{
fixed32 ttl = 1;
}
message TestRequest {
string data = 1;
}
message TestResponse {
string result = 1;
}
/*没有对应的MSG id,则为其它 消息的字段,作为子消息,可以消息嵌套定义,也可以放在外面,个人习惯放在外部。*/
message Friend
{
bytes name = 1;
bool online = 2;
}
message GetFriendsResponse
{
Friend friends = 1;
}
message SendMessageRequest
{
bytes receiver = 1;
bytes text = 2;
}
message FriendNotification
{
bytes name = 1;
bool online = 2;
}
message MessageNotification
{
bytes sender = 1;
bytes text = 2;
string timestamp = 3;
}
message WelcomeNotification
{
bytes text = 1;
}
/*请求消息集合,把所有的 XxxxxRequest消息全部集合在一起,使用起来类似于C语言的联合体,全部使用optional字段,任何时刻根据MSG 的id值,最多只有一个有效性, 从程序的逻辑上去保证,编译器(不管是protoc还是具体语言的编译器都无法保证)*/
message Request
{
oneof request_oneof {
LoginRequest login = 1;
TestRequest test_request = 2;
SendMessageRequest send_message = 3;
}
}
/*与Request作用相同,把所有的XxxResponse消息集合在一起,当作联合体使用,不过额外多了几个字段用于表示应答的结果*/
message Response
{
bool result = 1; //true表示应答成功,false表示应答失败
bool last_response = 2;// 一个请求可以包含多个应答,用于指示是否为最后一个应答
bytes error_describe = 3;// result == false时,用于描述错误信息
LoginResponse login = 4;
TestResponse test_response = 5;
GetFriendsResponse get_friends = 6;
}
/*与Request相同,把所有的XxxxxNotification消息集合在一起当作联合体使用.*/
message Notification
{
FriendNotification friend = 1;
MessageNotification msg = 2;
WelcomeNotification welcome = 3;
}
/*顶层消息,包含所有的Request,Response,Notification,
具体包含哪个消息又 MSG msg_type字段决定,程序逻辑去保证msg_type和具体的消息进行匹配*/
message Message
{
MSG msg_type = 1;//根据消息序号确定特定消息
fixed32 sequence = 2;//消息系列号,主要用于Request和Response,Response的值必须和Request相同,使得发送端可以进行事务匹配处理
fixed32 session_id = 3;
Request request = 4;
Response response = 5;
Notification notification = 6;
}
service XxxService {
rpc getXXX (TestRequest) returns (TestResponse) {}
}
netty服务器编写
package com.example.demo.rpc;
import com.example.demo.message.GDM;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.logging.LoggingHandler;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Component
public class NettyServer {
public NettyServer() throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
try {
start(8888);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();
}
public void start(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
// 指定Channel
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
.option(ChannelOption.SO_BACKLOG, 1024)
//设置TCP长连接,一般如果两个小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true)
//将小的数据包包装成更大的帧进行传送,提高网络的负载,即TCP延迟传输
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 添加长度解码器
p.addLast(new ProtobufVarint32FrameDecoder());
// 添加ProtoBuf解码器
p.addLast(new
ProtobufDecoder(GDM.Message.getDefaultInstance()));
p.addLast(new ProtobufVarint32LengthFieldPrepender());
// 添加ProtoBuf编码器
p.addLast(new ProtobufEncoder());
// 添加自定义的业务处理器
p.addLast(new MyServerHandler());
}
});
ChannelFuture f = b.bind().sync();
System.out.println("=====服务器启动成功=====");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
MyServerHandler处理。
package com.example.demo.rpc;
import com.example.demo.annotation.RouteExecution;
import com.example.demo.interfaceT.monitor1.EventPublisher;
import com.example.demo.message.GDM;
import com.example.demo.BeanTool;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class MyServerHandler extends SimpleChannelInboundHandler<GDM.Message> implements Process {
private static final Logger logger = LoggerFactory.getLogger(MyServerHandler.class);
private EventPublisher eventPublisher =eventPublisher = BeanTool.getBean(EventPublisher.class);;
private RouteExecution scanner = BeanTool.getBean(RouteExecution.class);
/**
* 服务端读取消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, GDM.Message msg) throws Exception {
GDM.Request request = msg.getRequest();
System.out.println("====服务器收到信息======");
route(ctx, msg);
}
/**
* 服务端异常处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 记录异常到日志
System.out.println(ctx + "====服务器收到异常======" + cause);
// 关闭连接,释放资源
ctx.close();
// 注意:通常不需要调用super.exceptionCaught(ctx, cause),因为ChannelInboundHandlerAdapter的此方法为空实现
}
@Override
public void route(ChannelHandlerContext ctx, GDM.Message message) throws Exception {
GDM.Request request = message.getRequest();
GDM.MSG msgType = message.getMsgType();
String nameClass = msgType.name();
//执行方法
scanner.getBeanAndExecute(nameClass, ctx, request);
}
}
路由处理,对应分布式项目,这里可以使用负载均衡指定找到对应的服务器,然后再找到的对应的处理器即可。但是对于单机来说仅仅使用反射,找到对应的处理器即可。
这里使用单机演示,这里使用了自定义注解,方便业务的快速开发。
package com.example.demo.annotation;
import com.example.demo.annotation.route.RouteServer;
import com.example.demo.BeanTool;
import com.example.demo.message.GDM;
import io.netty.channel.ChannelHandlerContext;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Component
public class RouteExecution {
private static final Map<String, String> ENUMTOCLASSMAP = new HashMap<>();
private static String HANDLERPATH ="";
static {
try {
// 使用ClassLoader来加载资源
InputStream inputStream = RouteExecution.class.getClassLoader().getResourceAsStream("route.xml");
if (inputStream == null) {
throw new RuntimeException("Resource not found: route.xml");
}
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(inputStream);
doc.getDocumentElement().normalize();
NodeList entries = doc.getElementsByTagName("entry");
for (int i = 0; i < entries.getLength(); i++) {
Element entry = (Element) entries.item(i);
String key = entry.getAttribute("key");
String value = entry.getTextContent().trim(); // 可能需要trim()来去除前后的空白字符
ENUMTOCLASSMAP.put(key, value);
}
NodeList handlerPath = doc.getElementsByTagName("handlerPath");
Node item = handlerPath.item(0);
HANDLERPATH = item.getTextContent().trim();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void getBeanAndExecute(String param, ChannelHandlerContext ctx, GDM.Request request) {
Reflections reflections = new Reflections(new ConfigurationBuilder()
.setUrls(ClasspathHelper.forPackage(HANDLERPATH))
.setScanners(new SubTypesScanner(false)));
Set<Class<? extends RouteServer>> classes = reflections.getSubTypesOf(RouteServer.class);
for (Class<?> clazz : classes) {
if (clazz.isAnnotationPresent(ToServer.class)) {
ToServer toServer = clazz.getAnnotation(ToServer.class);
// 注解参数对比
String methodName = toServer.value();
String orDefault = ENUMTOCLASSMAP.getOrDefault(param, null);
if (orDefault == null) {
continue;
}
if (!methodName.equals(orDefault)) {
continue;
}
Object bean = BeanTool.getBean(clazz);
// 查找并调用方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(ToMethod.class)) {
ToMethod annotation = method.getAnnotation(ToMethod.class);
if (!annotation.value().equals(param)) {
continue;
}
try {
method.setAccessible(true);
method.invoke(bean, ctx, request);
ctx.close();
return;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
}
注解实现
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ToMethod {
String value();
}
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // 类级别注解
@Retention(RetentionPolicy.RUNTIME)
public @interface ToServer {
String value();
}
最终测试
package com.example.demo.handler;
import com.example.demo.annotation.ToMethod;
import com.example.demo.annotation.ToServer;
import com.example.demo.annotation.route.RouteServer;
import com.example.demo.message.GDM;
import io.netty.channel.ChannelHandlerContext;
import org.springframework.stereotype.Component;
@Component
@ToServer("Test")
public class Test extends RouteServer {
@ToMethod("Login_Request")
public void p( ChannelHandlerContext ctx, GDM.Request request) {
GDM.LoginRequest login = request.getLogin();
System.out.println(ctx + "oooooooooooooo" + login);
}
@ToMethod("Test_Request")
public void ff( ChannelHandlerContext ctx, GDM.Request request) {
GDM.TestRequest testRequest = request.getTestRequest();
System.out.println(ctx + "oooooooooooooo" + testRequest);
}
@ToMethod("Send_Message_Request")
public void ddd( ChannelHandlerContext ctx, GDM.Request request) {
GDM.SendMessageRequest sendMessage = request.getSendMessage();
System.out.println(ctx + "oooooooooooooo" + sendMessage);
}
}
对于分布式来说仅仅是,添加一个网关的处理逻辑即可。