手敲一个简易dubbo(一)

引子

为了了解dubbo框架,我们最终还是走上了手敲的路。

先搂一眼最终的成品:

pom:

<dependencies>
	<dependency>
		<groupId>org.apache.tomcat.embed</groupId>
		<artifactId>tomcat-embed-core</artifactId>
		<version>9.0.12</version>
	</dependency>

	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-io</artifactId>
		<version>1.3.2</version>
	</dependency>

	<dependency>
		<groupId>io.netty</groupId>
		<artifactId>netty-all</artifactId>
		<version>4.1.16.Final</version>
	</dependency>

	

	<dependency>
		<groupId>org.apache.dubbo</groupId>
		<artifactId>dubbo-common</artifactId>
		<version>2.7.0</version>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>
</dependencies>

提供的服务及如何调用

public interface HelloService {
	String sayHello(String username);
}


public class HelloServiceImpl implements HelloService {
	@Override
	public String sayHello(String username) {
		return "hello " + username;
	}
}



一个sayHello的艰巨服务。

本地调用的话,直接用实现类就行了。

远程调用,我要用你这个sayHello的服务,我要知道接口名字方法名字方法参数方法参数的类型,如此就能精确定位到HelloServicesayHello(String username)方法。

于是,我们写一个类来封装这四个要素:

public class Invocation implements Serializable {
	private String interfaceName;
	private String methodName;
	private Class[] paramTypes;
	private Object[] params;

	public Invocation(String interfaceName, String methodName, Class[] paramTypes, Object[] params) {
		this.interfaceName = interfaceName;
		this.methodName = methodName;
		this.paramTypes = paramTypes;
		this.params = params;
	}

	public String getInterfaceName() {
		return interfaceName;
	}

	public void setInterfaceName(String interfaceName) {
		this.interfaceName = interfaceName;
	}

	public String getMethodName() {
		return methodName;
	}

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

	public Class[] getParamTypes() {
		return paramTypes;
	}

	public void setParamTypes(Class[] paramTypes) {
		this.paramTypes = paramTypes;
	}

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

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



由于要网络传输,所以实现了Serializable接口。

本地注册和远程注册

服务提供者肯定要进行本地注册和远程注册。

dubbo-demo的例子中:

  <bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>

    <dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService"/>



是本地注册。

 <dubbo:registry address="zookeeper://127.0.0.1:2181"/>


是远程注册。


先考虑本地注册。

要索取sayHello的服务,拿到实现类就算成功了。

public class LocalRegister {
	private static Map<String,Class> map = new HashMap<>();

	public static void register(String interfaceName, Class implClass){
		map.put(interfaceName,implClass);
	}

	public static Class get(String interfaceName){
		return map.get(interfaceName);
	}
}



我们定义一个map来存储本地注册的服务。key是接口名,value是实现类。

所以如果你要用的话,提供个接口名,拿到实现类就可以了。

public class RemoteRegister {

	private static Map<String , List<URL>> REGISTER = new HashMap<>();


	public static void register(String interfaceName, URL url){

		List<URL> list = Collections.singletonList(url);

		REGISTER.put(interfaceName,list);

		saveFile();
	}
	}




private static void saveFile(){
		try{
			FileOutputStream fileOutputStream = new FileOutputStream("./temp.txt");
			ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
			objectOutputStream.writeObject(REGISTER);

			objectOutputStream.close();
			fileOutputStream.close();
		}catch (Exception e){
			e.printStackTrace();
		}
	}


远程注册的话,我们要提供服务接口名和自己的url地址。

这个URL对象是我们自己封装的:

public class URL implements Serializable {
	private String hostname;
	private Integer port;

	public String getHostname() {
		return hostname;
	}

	public void setHostname(String hostname) {
		this.hostname = hostname;
	}

	public Integer getPort() {
		return port;
	}

	public void setPort(Integer port) {
		this.port = port;
	}

	public URL(String hostname, Integer port) {
		this.hostname = hostname;
		this.port = port;
	}
}


我这里用了saveFile这个方法将REGISTER这个map存到了本地文件中。因为我们之后的consumer会用到REGISTER,会根据接口名找到url,但是consumer是另一个进程。它如何能找到REGISTER呢?暂时的办法就是通过本地文件的形式。

private static Map<String, List<URL>> getFile(){
		try{
			FileInputStream fileInputStream = new FileInputStream("./temp.txt");
			ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

			return (Map<String, List<URL>>) objectInputStream.readObject();


		}catch (Exception e){
			e.printStackTrace();
		}
		return null;
	}

getFile就是给consumer用的。

负载均衡

另一方面,我们要做好负载均衡。

根据服务名,我们将选择一个url暴露给consumer。

public static URL randomChoose(String interfaceName){
		REGISTER = getFile();

		List<URL> urls = REGISTER.get(interfaceName);
		Random random = new Random();
		int index = random.nextInt(urls.size());
		return urls.get(index);
	}

这里选择的算法就是随机选一个url。

http协议

server

http协议是以tomcat作为server的。

我们要用代码启动一个tomcat:

public class TomcatHttpServer {

	public void start(String addr, int port){
		Tomcat tomcat = new Tomcat();

		Server server = tomcat.getServer();
		Service service = server.findService("Tomcat");

		Connector connector = new Connector();
		connector.setPort(port);

		Engine engine = new StandardEngine();
		engine.setDefaultHost(addr);

		Host host = new StandardHost();
		host.setName(addr);

		String contextPath = "";
		Context context = new StandardContext();
		context.setPath(contextPath);
		context.addLifecycleListener(new Tomcat.FixContextListener());

		host.addChild(context);
		engine.addChild(host);

		service.setContainer(engine);
		service.addConnector(connector);

		tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet());
		context.addServletMappingDecoded("/*","dispatcher");

		try{
			tomcat.start();
			tomcat.getServer().await();
		}catch (Exception e){
			e.printStackTrace();
		}
	}
}

我们给tomcat添加了分发处理请求的DispatcherServlet

客户端的请求会打到DispatcherServlet上:

public class DispatcherServlet extends HttpServlet {
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		new HttpServerHandler().handleHttp(req,resp);
	}
}

为了代码的层级清晰,我们把具体的处理转交给HttpServerHandler

/**
 * receive data and process request from httpclient
 */
public class HttpServerHandler {

	public void handleHttp(HttpServletRequest req, HttpServletResponse resp){
		try{
			InputStream inputStream = req.getInputStream();
			ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);

			Invocation invocation = (Invocation) objectInputStream.readObject();
			Class implClass = LocalRegister.get(invocation.getInterfaceName());
			Method method = implClass.getMethod(invocation.getMethodName(),invocation.getParamTypes());
			String result = (String) method.invoke(implClass.newInstance(), invocation.getParams());

			IOUtils.write(result,resp.getOutputStream());
		}catch (Exception e){
			e.printStackTrace();
		}


	}
}

consumer会提供需要调用的接口名,我们将其封装成Invocation通过http client传过来。

所以我们先拿到输入流读取Invocation。然后取到Invocation中的接口名。在本地注册表中,用接口名去拿实现类,再用反射invoke方法(sayHello)。

sayHello返回的值我们用IOUtils(apache的工具类)传给client。

client
/**
 * connect to server and send data
 *and
 * receive result from server
 *
 *
 */
public class HttpClient {


	public String send(String hostname, int port, Invocation invocation) {
		try {
			URL url = new java.net.URL("http",
					hostname,
					port,
					"/");
			HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();

			httpConnection.setRequestMethod("POST");
			httpConnection.setDoOutput(true);

			OutputStream outputStream = httpConnection.getOutputStream();
			ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

			objectOutputStream.writeObject(invocation);
			objectOutputStream.flush();
			objectOutputStream.close();
			outputStream.close();

			InputStream inputStream = httpConnection.getInputStream();
			String result = IOUtils.toString(inputStream);

			return result;

		}
		catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

}



client要做的事情就是根据服务端的ip地址和端口号和服务端连接。

然后把consumer委托过来的Invovation传给服务端。

服务端方法调用好了会把返回值回传过来,那么client就接收一下,再return给consumer。

启动服务提供者

上面铺垫好后,我们看服务提供者(provider)的代码。

public class Provider {

	public static void main(String[] args) {

		//local register
		LocalRegister.register(HelloService.class.getName(), HelloServiceImpl.class);


		//remote register
		URL url = new URL("localhost",8080);
		RemoteRegister.register(HelloService.class.getName(),url);

		//start tomcat
		TomcatHttpServer tomcatHttpServer = new TomcatHttpServer();
		tomcatHttpServer.start("localhost",8080);

//		NettyServer nettyServer = new NettyServer();
//		nettyServer.openServer("localhost",8080);
	}

}

它这里做了三个事情。本地注册,远程注册,启动tomcat。

意思就是说,我provider已经能提供服务了。你们consumer快来调用啊。

代理模式

记得dubbo-demo中consumer是如何调用服务的吗?

 DemoService demoService = context.getBean("demoService", DemoService.class);
        
 System.err.println(demoService.sayHello("ocean"));


这个demoService一定是个代理对象。

所以我们也要模仿使用代理模式。

public class ProxyFactory {

	public static <T> T getProxy(Class interfaceCls) {
		return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[] {interfaceCls}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				HttpClient httpClient = new HttpClient();
				Invocation invocation = new Invocation(interfaceCls.getName(), method.getName(), method.getParameterTypes(), args);

				URL url = RemoteRegister.randomChoose(interfaceCls.getName());
				String result = httpClient.send(url.getHostname(), url.getPort(), invocation);

				return  result;
			}
		});

	}

}



consumer会提供接口名,我们用该接口生成一个代理返回给consumer。

当consumer调用代理对象的sayHello方法时,会被InvocationHandler拦截到,并执行invoke方法。

invoke方法将理清所有的逻辑:

  1. new一个client
  2. 根据consumer要调用的接口和接口中的方法封装成Invocation
  3. 根据接口名(服务名)在远程注册中心找到一个具体的服务提供者(使用负载均衡)的地址。
  4. client根据这个地址和server连接,把Invocation传过去。
  5. server端拿到Invocation在本地注册表中找到它的实现类并调用,调用结果返回给client。
  6. 方法执行结果返回给consumer。

consumer

讲了这么多,终于要亮出consumer 了。

public class Consumer {

	public static void main(String[] args) {
		HelloService helloService = ProxyFactory.getProxy(HelloService.class);
		System.out.println(helloService.sayHello("ocean"));
	}
}

它就说:“我要调用HelloService这个服务的sayHello方法”。

测试

先启动Provider,再启动Consumer

控制台打印了hello ocean,说明consumer调用成功了。

后续,我们将讲基于netty的dubbo协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值