引子
为了了解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
的服务,我要知道接口名字,方法名字,方法参数,方法参数的类型,如此就能精确定位到HelloService
的sayHello(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
方法将理清所有的逻辑:
- new一个client
- 根据consumer要调用的接口和接口中的方法封装成
Invocation
- 根据接口名(服务名)在远程注册中心找到一个具体的服务提供者(使用负载均衡)的地址。
- client根据这个地址和server连接,把
Invocation
传过去。 - server端拿到
Invocation
在本地注册表中找到它的实现类并调用,调用结果返回给client。 - 方法执行结果返回给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协议。