Netty手动实现Dubbo(含视频教程)

为什么要使用Dubbo

随着Internet的快速发展,web应用的规模不断扩大,传统的垂直架构(单片)已经无法满足这一需求。分布式服务体系结构和流计算体系结构是必须的,并且迫切需要一个治理系统来确保体系结构的有序发展

Dubbo架构

在这里插入图片描述

  • 1.服务提供者服务启动则异步向注册中心中注册服务
  • 2.消费者启动之后会异步向注册中心订阅服务
  • 3.注册中心会异步的将订阅的接口和地址映射返回给服务消费者
  • 4.服务调研时,消费者通过负载均衡算法选择其中一个ip&port发送PRC请求
  • 5.服务提供者收到请求同步处理并将处理结果序列化之后传输给消费者
    netty在这中间重要起到了序列化编解码和网络请求的作用
    注册中心则负责服务的注册订阅和消息分发

技术选型

RPC框架使用netty实现
注册中心使用zk
使用zk的watch机制了监控数据的变更,变更之后会异步通知服务消费者
注册中心也可以使用redis,基于redis的发布/订阅模式通知数据变更

实现方案

项目的目录结构

├─dubbo
│  ├─.idea
│  ├─dubbo-client
│  │  ├─src
│  │  │  ├─main
│  │  │  │  ├─java
│  │  │  │  │  └─com
│  │  │  │  │      └─mrduan
│  │  │  │  │          ├─bean
│  │  │  │  │          ├─loadbalance
│  │  │  │  │          ├─proxy
│  │  │  │  │          └─registry
│  ├─dubbo-common
│  │  ├─src
│  │  │  ├─main
│  │  │  │  ├─java
│  │  │  │  │  └─com
│  │  │  │  │      └─mrduan
│  │  │  │  │          ├─annotation
│  │  │  │  │          ├─bean
│  │  │  │  │          └─service
│  ├─dubbo-server
│  │  ├─src
│  │  │  ├─main
│  │  │  │  ├─java
│  │  │  │  │  └─com
│  │  │  │  │      └─mrduan
│  │  │  │  │          ├─registry
│  │  │  │  │          └─service
│  │  │  │  └─resources

项目依赖
主要添加zk操作依赖和netty依赖

<dependencies>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>2.5.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>2.5.0</version>
    </dependency>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.32.Final</version>
    </dependency>
</dependencies>

1.实现服务注册

在dubbo-server中实现服务注册功能
1.配置类ZkConfig

package com.mrduan.registry;

public class ZkConfig {
    public static final String CONNECTION_STR = "127.0.0.1:2181";
    public static final String ZK_REGISTER_PATH = "/registrys";
}

2.服务注册中心接口IRegisterCenter

package com.mrduan.registry;

public interface IRegisterCenter {
    /**
     * 服务注册
     * @param serverName 服务名称(实现方法路径)
     * @param serviceAddress 服务地址
     */
    void registry(String serverName,String serviceAddress);
}

3.服务注册中心实现

package com.mrduan.registry;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

public class RegisterCenterImpl implements IRegisterCenter {

    private CuratorFramework curatorFramework;
    {
        curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(ZkConfig.CONNECTION_STR).sessionTimeoutMs(4000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 10)).build();
        curatorFramework.start();
    }
    @Override
    public void registry(String serverName, String serviceAddress) {
        String serverPath = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serverName);
        try {
            if (curatorFramework.checkExists().forPath(serverPath) == null) {
                curatorFramework.create().creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT).forPath(serverPath, "0".getBytes());
            }
            String addr = serverPath.concat("/").concat(serviceAddress);
            String rsNode = curatorFramework.create().withMode(CreateMode.EPHEMERAL)
                    .forPath(addr, "0".getBytes());
            System.out.println("服务注册成功," + rsNode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

定义一个CuratorFramework对象,通过代码块来实例化该对象,并通过curatorFramework.start();来连接ZKConfig中配置好的地址连接ZK。

​ 使用zk作为注册中心,我们了解下ZK的存储结构。zookeeper的命名空间的结构和文件系统很像。一个名字和文件一样使用/的路径表现,zookeeper的每个节点都是被路径唯一标识的
在这里插入图片描述
这里/registrys是我们注册的根节点:类型是永久类型,服务停机了也不删除CreateMode.PERSISTENT
/com.mrduan.service.IDemoRpcService是注册的接口,也是用的CreateMode.PERSISTENT模式
/127.0.0.1是注册的服务接口的地址:类型是暂时类型:CreateMode.EPHEMERAL,当服务注册之后就会生产,服务停机之后大约10s左右就会被删除

4.编写RpcServer,用户发布服务

package com.mrduan;

import com.mrduan.annotation.RpcAnnotation;
import com.mrduan.registry.IRegisterCenter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.util.HashMap;
import java.util.Map;

public class RpcServer {

    private IRegisterCenter registerCenter;
    private String serviceAddress;
    private Map<String, Object> handlerMap = new HashMap<>(16);

    public RpcServer(IRegisterCenter registerCenter, String serviceAddress) {
        this.registerCenter = registerCenter;
        this.serviceAddress = serviceAddress;
    }

    /**
     * 发布服务
     */
    public void publisher() {
        for (String serviceName : handlerMap.keySet()) {
            registerCenter.registry(serviceName, serviceAddress);
        }
        try {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            //启动netty服务
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    ChannelPipeline channelPipeline = channel.pipeline();
                    channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
                    channelPipeline.addLast(new ObjectEncoder());
                    channelPipeline.addLast(new RpcServerHandler(handlerMap));
                }
            }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
            String[] addr = serviceAddress.split(":");
            String ip = addr[0];
            int port = Integer.valueOf(addr[1]);
            ChannelFuture future = bootstrap.bind(ip, port).sync();
            System.out.println("服务启动,成功。");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 子对象的实现
     *
     * @param services 对象实现类
     */
    public void bind(Object... services) {
        //将实现类通过注解获取实现类的名称、实现类的实现放入map集合中。
        for (Object service : services) {
            RpcAnnotation annotation = service.getClass().getAnnotation(RpcAnnotation.class);
            String serviceName = annotation.value().getName();
            handlerMap.put(serviceName, service);
        }
    }
}

通过bind绑定服务和服务地址的映射
ChannelFuture future = bootstrap.bind(ip, port).sync();
future.channel().closeFuture().sync();
发布服务已经服务请求处理的同步调用

5.编写RpcServerHandler用户处理来自客户端的远程调用

package com.mrduan;

import com.mrduan.bean.RpcRequest;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 远程服务处理器
 */
public class RpcServerHandler extends ChannelInboundHandlerAdapter {
    private Map<String, Object> handlerMap = new HashMap<>();

    public RpcServerHandler(Map<String, Object> handlerMap) {
        this.handlerMap = handlerMap;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws UnsupportedEncodingException {
        System.out.println("channelActive:" + ctx.channel().remoteAddress());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务端接收到消息:" + msg);
        RpcRequest rpcRequest = (RpcRequest) msg;
        Object result = new Object();
        if (handlerMap.containsKey(rpcRequest.getClassName())) {
            Object clazz = handlerMap.get(rpcRequest.getClassName());
            Method method = clazz.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getTypes());
            result = method.invoke(clazz, rpcRequest.getParams());
        }
        ctx.write(result);
        ctx.flush();
        ctx.close();
    }
}

重新channelRead方法,通过msg接受请求参数
通过反射原理调用服务端方法
通过ctx将数据写到客户端

6.编写服务发布类,验证服务

package com.mrduan;

import com.mrduan.registry.IRegisterCenter;
import com.mrduan.registry.RegisterCenterImpl;
import com.mrduan.service.DemoRpcServiceImpl;
import com.mrduan.service.IDemoRpcService;

import java.io.IOException;

public class RpcServerStarter {
    public static void main(String[] args) throws IOException {
        IDemoRpcService service = new DemoRpcServiceImpl();
        IRegisterCenter registerCenter = new RegisterCenterImpl();
        RpcServer server = new RpcServer(registerCenter,"127.0.0.1:7070");
        server.bind(service);
        server.publisher();
    }
}

为了验证功能,我们需要在dubbo-common项目中编写客户端和服务端都需要使用到的代码
7.编写RpcAnnotation用于区分哪些服务需要被注册到注册中心

package com.mrduan.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 RpcAnnotation {

    Class<?> value();

}

8.编写RpcRequest用于客户端和服务端通讯的封装

package com.mrduan.bean;

import java.io.Serializable;

public class RpcRequest implements Serializable {

    private String methodName;

    private String className;

    private Class<?>[] types;

    private Object[] params;

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public Class<?>[] getTypes() {
        return types;
    }

    public void setTypes(Class<?>[] types) {
        this.types = types;
    }

    public Object[] getParams() {
        return params;
    }

    public void setParams(Object[] params) {
        this.params = params;
    }
}

9.编写服务接口
为了方便服务端和客户端都能使用到接口因此我们在common项目中创建了接口

package com.mrduan.service;

import com.mrduan.annotation.RpcAnnotation;

public interface IDemoRpcService {
    public String sayHello(String name);
}

10.此时在返回到dubbo-server项目中编写IDemoRpcService的接口实现

package com.mrduan.service;

import com.mrduan.annotation.RpcAnnotation;

@RpcAnnotation(IDemoRpcService.class)
public class DemoRpcServiceImpl implements IDemoRpcService{
    @Override
    public String sayHello(String name) {
        return "hello: " + name;
    }
}

启动RpcServerStarter你看到以下功能则说明启动成功
在这里插入图片描述
为了确定是否成功我们在取zk中查看一下
在这里插入图片描述
发现我们刚刚注册的服务确实已经记录到了

2.服务发现&调用

在客户端编写代码
1.配置文件(此文件完全可以写在common中,因为大家只有使用相同的注册中心才能发现服务)

package com.mrduan.registry;

public class ZkConfig {
    public static final String CONNECTION_STR = "127.0.0.1:2181";
    public static final String ZK_REGISTER_PATH = "/registrys";
}

2.服务发现接口IServiceDiscovery

package com.mrduan.registry;

public interface IServiceDiscovery {
    public String discover(String ServiceName);
}

3.发现接口实现ZkServiceDiscoveryImpl

package com.mrduan.registry;

import com.mrduan.loadbalance.LoadBalance;
import com.mrduan.loadbalance.RandomLoadBalance;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;

import java.util.ArrayList;
import java.util.List;

public class ZkServiceDiscoveryImpl implements IServiceDiscovery{

    List<String> repos = new ArrayList<String>();
    private CuratorFramework curatorFramework;

    public ZkServiceDiscoveryImpl() {
        curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(ZkConfig.CONNECTION_STR).sessionTimeoutMs(4000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 10))
                .build();
        curatorFramework.start();
    }

    @Override
    public String discover(String serviceName) {
        String path = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serviceName);
        try {
            repos = curatorFramework.getChildren().forPath(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        registerWatch(path);
        LoadBalance loadBalance = new RandomLoadBalance();
        return loadBalance.select(repos);
    }

    /**
     * 监听ZK节点内容刷新
     *
     * @param path 路径
     */
    private void registerWatch(final String path) {
        PathChildrenCache childrenCache = new PathChildrenCache(curatorFramework, path, true);
        PathChildrenCacheListener childrenCacheListener = new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                repos = curatorFramework.getChildren().forPath(path);
            }
        };
        childrenCache.getListenable().addListener(childrenCacheListener);
        try {
            childrenCache.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

和服务注册同样定义CuratorFramework对象,并通过curatorFramework.start();连接ZK。
连接成功后通过zk注册的根节点加服务名称,获取该服务的服务地址。
​获取的服务地址有可能不是最新的服务地址,我们需要监听zk节点的内容刷新,通过调用registerWatch方法,监听该节点的数据变化。
​最后,将获取到的地址集合,通过LoadBalance随机选出一个地址,实现该服务。

4.负载均衡

package com.mrduan.loadbalance;

import java.util.List;

public interface LoadBalance {

    public String select(List<String> repos);
}

5.负载均衡实现,这里使用随机算法,当然也可以实现接口做其他算法

package com.mrduan.loadbalance;

import java.util.List;
import java.util.Random;

public class RandomLoadBalance implements LoadBalance {
    @Override
    public String select(List<String> repos) {
        int len = repos.size();
        if (len == 0)
            throw new RuntimeException("未发现注册的服务。");
        Random random = new Random();
        return repos.get(random.nextInt(len));
    }
}

6.编写客户端代理
因为客户端只需要接口,然后需要完成一个将请求发送服务提供者,将返回结果处理成结构能识别的即可,一次需要使用到代理

package com.mrduan.proxy;

import com.mrduan.registry.IServiceDiscovery;

import java.lang.reflect.Proxy;

public class RpcClientProxy {

    private IServiceDiscovery serviceDiscovery;

    public RpcClientProxy (IServiceDiscovery serviceDiscovery){
        this.serviceDiscovery = serviceDiscovery;
    }

    public void setServiceDiscovery(IServiceDiscovery serviceDiscovery) {
        this.serviceDiscovery = serviceDiscovery;
    }

    public <T> T create(final Class<T> interfaceClass){
        ClassLoader classLoader = interfaceClass.getClassLoader();
        Class<?>[] classes = {interfaceClass};
        RpcClientInvocationHandler invocationHandler = new RpcClientInvocationHandler(serviceDiscovery, interfaceClass);
        Object o = Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        return (T) o;
    }
}

通过动态代理newProxyInstance方法,传入待调用的接口对象,获取getClassLoader后,实现invoke方法

7.客户端代理处理器

package com.mrduan.proxy;

import com.mrduan.bean.RpcRequest;
import com.mrduan.registry.IServiceDiscovery;
import com.mrduan.service.IDemoRpcService;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class RpcClientInvocationHandler implements InvocationHandler {

    private IServiceDiscovery serviceDiscovery;

    private Class<?> interfaceClass;

    public RpcClientInvocationHandler(IServiceDiscovery serviceDiscovery,Class<?> interfaceClass) {
        this.serviceDiscovery = serviceDiscovery;
        this.interfaceClass = interfaceClass;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setTypes(method.getParameterTypes());
        rpcRequest.setParams(args);
        //服务发现,zk进行通讯
        String serviceName = interfaceClass.getName();
        //获取服务实现url地址
        String serviceAddress = serviceDiscovery.discover(serviceName);
        //解析ip和port
        System.out.println("服务端实现地址:" + serviceAddress);
        String[] arrs = serviceAddress.split(":");
        String host = arrs[0];
        int port = Integer.parseInt(arrs[1]);
        System.out.println("服务实现ip:" + host);
        System.out.println("服务实现port:" + port);
        final RpcProxyHandler rpcProxyHandler = new RpcProxyHandler();
        //通过netty方式进行连接发送数据
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline channelPipeline = socketChannel.pipeline();
                            channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
                            channelPipeline.addLast(new ObjectEncoder());
                            //netty实现代码
                            channelPipeline.addLast(rpcProxyHandler);
                        }
                    });
            ChannelFuture future = bootstrap.connect(host, port).sync();
            //将封装好的对象写入
            future.channel().writeAndFlush(rpcRequest);
            future.channel().closeFuture().sync();
        } catch (Exception e) {

        } finally {
            group.shutdownGracefully();
        }
        return rpcProxyHandler.getResponse();
    }
}

定义RpcRequest对象,封装请求参数。通过interfaceClass对象获取服务实现名称,调用discover方法获取服务提供者的地址信息,netty通过该信息连接服务,并将RpcRequest对象发送到服务端,服务端解析对象,获取接口请求参数等信息,执行方法,并将结果返回到客户端RpcProxyHandler对象接收返回结果。

8.代理处理器

import io.netty.channel.ChannelInboundHandlerAdapter;

public class RpcProxyHandler extends ChannelInboundHandlerAdapter {
    private Object response;

    public Object getResponse() {
        return response;
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //将服务端返回的内容返回
        response = msg;
    }
}

9.编写测试类测试服务发现和PRC远程调用

package com.mrduan.registrys;

import com.mrduan.proxy.RpcClientProxy;
import com.mrduan.registry.IServiceDiscovery;
import com.mrduan.registry.ZkServiceDiscoveryImpl;
import com.mrduan.service.IDemoRpcService;

public class ClinetTest {

    public static void main(String[] args) {
        IServiceDiscovery discovery = new ZkServiceDiscoveryImpl();
        String url = discovery.discover("com.mrduan.service.IDemoRpcService");
        long start = System.currentTimeMillis();
        RpcClientProxy proxy = new RpcClientProxy(discovery);
        IDemoRpcService service = proxy.create(IDemoRpcService.class);
        String bruce = service.sayHello("Bruce");
        System.out.println(bruce);
        long end = System.currentTimeMillis();
        System.out.println("请求耗时:"+ (end-start)+"ms");
    }
}

至此基于netty实现dubbo服务功能完成
我们测试一下效果
在这里插入图片描述
大家可以看到这个客户端是没有接口IDemoRpcService实现的

参考链接
参考源码
视频教程

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值