欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

十分钟带你了解服务化框架

程序员文章站 2022-06-12 13:29:24
...

在此之前

在此之前,你需要知道中间件的概念,可能在过往的从业生涯这个名词无数次的从你的眼前、耳畔都留下了足记,但是它的样子依然很模糊。
今天要说的服务化框架其实就是中间件的范畴,我们来看下,什么是中间件:

中间件是为软件应用提供了操作系统所提供的服务之外的服务,可以把中间件描述为“软件胶水”。中间件不是操作系统的一部分,不是数据库管理系统,也不是软件应用的一部分,而是能够让软件开发者方便的处理通讯、输入和输出,能够专注在他们自己应用的部分。

从这段定义来看,我们要通俗易懂的描述中间件这个概念实在有些困难。所以我们首先借助我们了解的操作系统、数据库管理系统的概念,将中间件与他们撇清关系,而后通过一段较为抽象的说明对中间件下了个定义。

举个例子,如果你知道消息队列,比如我们常用的RabbitMQ,它就是中间件,它可以为我们削峰,为我们提供异步处理业务的可能,它就是名副其实的软件胶水。下面我们从另外一个侧面——服务化框架来体会下中间件是一种什么样的存在。

服务化框架是怎么来的

《大型网站的自强之路》中我们看到了大型网站的一步步演化,从单应用到多应用,从单库到分库分表,所有这些演化都是源于业务、访问量、并发量的增加。
十分钟带你了解服务化框架

这样一个网站结构简单又清晰,一般来说可以满足常规需求。但是随着业务越来越复杂,网站规模也日益扩大,原本清晰划分的应用模块A、B和C上加上了很多不属于他们的代码。这样的状况持续的时间越长,网站的结构就变的越来越没有边界,也就在高内聚低耦合的路上渐行渐远。

举例来说,某天我们在应用A上加的无关代码太多以至于我们不能再冠之以“应用A”的名号。假设应用A表示的是商品系统,我们慢慢的在其基础上加入了商品的展示功能,商品的添加、商品的更新等等功能,代码不断的增加,维护的人也越来越多,让这个应用模块变的愈加臃肿复杂。

这时候我们需要做应用级别的拆分,对于用户管理系统我们可以单独提出一块,订单系统提出一块,商品系统作为一块……慢慢的,我们的网站结构图就成了这样
十分钟带你了解服务化框架

现在从功能划分这个角度来看,我们的系统相较于之前着实清晰了不少,再也不用很多不相干的代码挤在一起了。但是在实际使用过程中,我们发现有些东西我们可以作进一步的抽象,比如,在一个电商网站中,在订单系统和交易系统中,我们都有依赖用户系统。虽然各个应用模块看似分工明确,但实际上他们之间还是有些藕断丝连。

我们进一步抽象,将这些被多个应用模块的应用抽象为一个服务,添加一个服务层,使得各大应用之间的交集演变为服务层的一个服务,这样对于公共的服务抽象出来,应用层只需要调用相应的服务即可,再也不用自己重复造*了。
十分钟带你了解服务化框架

这样我们就得到了服务化的框架,这个框架有它自身的好处:

  • 结构清晰 应用层和服务层以及底层基础层结构清晰明了
  • 稳定性 通过服务层的隔离,使得应用层不在直接操作接触底层服务如DB缓存等,提供了系统的稳定性
  • 解耦 使得原来还交错依赖的应用模块耦合度降低
  • 高扩展性 如果需要接入新的服务或者应用,直接水平扩展即可,前面的解耦为高可扩展提供了支持

现如今,我们经常提到的微服务,也就是这种思想。一个模块就是一个完整的服务,内部通过调用底层的基础服务,然后对外提供服务,这样耦合性低且易于维护。

服务化框架是怎么用的

我想应该没有哪个有位青年,在当时学到Socket编程的时候能够克制自己的好奇心,对于聊天室这个小东西无动于衷,甚至都不愿意多看它一眼。
总之对于聊天之略知一二的应该就了解他实际上使用的Socket编程。通过启动一个Server端,然后监听一个端口,再起一个客户端,客户端向服务端发送请求,服务端接受到请求后,做出相应的回复并将内容通过网络传输到客户端,这时候客户端就可以看到服务端回复的内容。

一个Server端和Client端的通讯Java版本的实现大致是这样的
Server服务端:

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static final int PORT = 12345;//监听的端口号   

    public static void main(String[] args) {  
        System.out.println("服务器启动...\n");  
        Server server = new Server();  
        server.init();  
    }  

    public void init() {  
        try {  
            ServerSocket serverSocket = new ServerSocket(PORT);  
            while (true) {  
                // 一旦有堵塞, 则表示服务器与客户端获得了连接  
                Socket client = serverSocket.accept();  
                // 处理这次连接  
                new HandlerThread(client);  
            }  
        } catch (Exception e) {  
            System.out.println("服务器异常: " + e.getMessage());  
        }  
    }  

    private class HandlerThread implements Runnable {  
        private Socket socket;  
        public HandlerThread(Socket client) {  
            socket = client;  
            new Thread(this).start();  
        }  

        public void run() {  
            try {  
                // 读取客户端数据  
                DataInputStream input = new DataInputStream(socket.getInputStream());
                String clientInputStr = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException
                // 处理客户端数据  
                System.out.println("客户端发过来的内容:" + clientInputStr);  

                // 向客户端回复信息  
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
                System.out.print("请输入:\t");  
                // 发送键盘输入的一行  
                String s = new BufferedReader(new InputStreamReader(System.in)).readLine();  
                out.writeUTF(s);  

                out.close();  
                input.close();  
            } catch (Exception e) {  
                System.out.println("服务器 run 异常: " + e.getMessage());  
            } finally {  
                if (socket != null) {  
                    try {  
                        socket.close();  
                    } catch (Exception e) {  
                        socket = null;  
                        System.out.println("服务端 finally 异常:" + e.getMessage());  
                    }  
                }  
            } 
        }  
    }  
}

Client端

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class Client {
    public static final String IP_ADDR = "localhost";//服务器地址 
    public static final int PORT = 12345;//服务器端口号  

    public static void main(String[] args) {  
        System.out.println("客户端启动...");  
        System.out.println("当接收到服务器端字符为 \"OK\" 的时候, 客户端将终止\n"); 
        while (true) {  
            Socket socket = null;
            try {
                //创建一个流套接字并将其连接到指定主机上的指定端口号
                socket = new Socket(IP_ADDR, PORT);  

                //读取服务器端数据  
                DataInputStream input = new DataInputStream(socket.getInputStream());  
                //向服务器端发送数据  
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
                System.out.print("请输入: \t");  
                String str = new BufferedReader(new InputStreamReader(System.in)).readLine();  
                out.writeUTF(str);  

                String ret = input.readUTF();   
                System.out.println("服务器端返回过来的是: " + ret);  
                // 如接收到 "OK" 则断开连接  
                if ("OK".equals(ret)) {  
                    System.out.println("客户端将关闭连接");  
                    Thread.sleep(500);  
                    break;  
                }  

                out.close();
                input.close();
            } catch (Exception e) {
                System.out.println("客户端异常:" + e.getMessage()); 
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        socket = null; 
                        System.out.println("客户端 finally 异常:" + e.getMessage()); 
                    }
                }
            }
        }  
    }  
}

从代码中,我们可以发现,这个Socket通讯的例子中无论是服务端还是客户端都是放在localhost上的,这个就是典型的单机版本的通讯。
单集版的聊天室实在太简陋,以至于服务端和客户端都是你的电脑localhost,很多中型、大型的网站和应用需要有自己的服务端,好比你安装的QQ或是微信是无法把服务端安装到你手机上的,你的手机其实只是充当了一个客户端的角色。下面我们来看看服务化框架是如何从集中式走向分布式的。

抛开单机版的电脑,迈向分布式的服务化

现在一个财务系统中有一块需要计算每个月的工资,这时候有一个SalaryCalculator类,其中有一个很多方法,我们关心的是其中的一个计算本月实得工资的方法,类如下所示

public class SalaryCalculator{
    public BigDecimal getTotalSalary(BigDecimal baseSalary, BigDecimal performanceSalary) {
        return baseSalary + performanceSalary;
    }
}

现在我们来算一下小王这个月的工资

public static void getSalary() {
    SalaryCalculator salaryCalculato = new SalaryCalculator();
    System.out.println("Mr Wang earn totalSalary: " + salaryCalculator.getTotalSalary(10000 + 199.9))
}

或者我们通过依赖注入的方式直接注入SalaryCalculator,然后调用他的getTotalSalary()方法。

我们太熟悉这样调用一个类的方法了,但是引入服务化的思想,我们该想想如果现在结算工资的模块已经抽象成一个服务,单独打包并部署在一台tomcat的容器上,这时候我们该如何调用,还是直接new或者注入?
跳出了你的服务端和客户端二合一的电脑,在分布式的服务化框架下我们压根就不知道这个结算服务在哪台机子上,甚至不知道要调用的是哪个方法。

分布式大环境下,我们需要换一种思维

远程调用区别于本地调用主要多了寻址(找到服务所在地址列表)和Socket通讯(好比上面提到的聊天室)。

客户端
这时候如果我们还想要调用到这个结算工资的方法,我们需要分为如下几步:

  • 获取可用服务地址列表
List<String> l = getAvailableServiceAddresses("SalaryCalculator.getTotalSalary")
  • 确定要调用服务的目标机器
String address = chhoseTarget(l)
  • 建立连接
Socket s = new Socket(address)
  • 请求的序列化
byte[] request = getRequest(baseSalary, performanceSalary)
  • 接受结果
byte[] response = new byte[10240];
s.getInputStream().read(response)
  • 解析结果
int result = getResult(response);
return result;

从以上各个步骤,我们可以看到首先我们需要根据调用的服务名称来获取提供服务的机器列表,并进一步确定提供服务的目标机器的信息,如地址端口号等。这个过程就可以简单的理解为一个路由寻址,找到提供服务的机器的信息。后面就是客户端建立连接以及通讯的过程了。

服务端
以上是客户端需要做的操作,那么作为响应和接收并处理请求的服务端需要做些什么,大概分为以下几步:

  • 接收请求,从请求中拿到一些标识信息,比如服务的名称,要调用的方法和入参等
  • 定位需要提供的服务,根据客户端传来的标识信息,服务端决定在本地如果提供服务
  • 具体响应处理,这时候调用响应的方法并返回结果
  • 传输结果,方法调用完成,通过网络传回结果数据

细说服务提供方和服务调用方

服务调用方
在分布式的服务框架中,我们不能像集中式的环境中那么随意,通过new一个对象,从而调用类中的方法属性等。在分布式环境下,我们要考虑到网络传输,当然就需要序列化和反序列化。除此以外,我们还需要根据一些规则找到我们需要调用的服务,最终完成调用,然后通过网络传输返回结果。

调用服务既然需要规则,那么我们就需要配置规则,大家最为属性的可能就是类似于Spring中的xml格式的配置了。好比这样

<bean id = "salaryCalculator" class="com.jackie.ServiceFramework.ConsumerBean">
    <property name = "interfaceName">
        <value>com.jackie.SalaryCalculator</value>
    </propperty>
    <property name = "version">
        <value>1.0.0</value>
    </propperty>
    <property name = "group">
        <value>Salary</value>
    </propperty>
</bean>

这里的ConsumerBean可以认为是一个通用对象,是完成本地和远程服务的桥梁,具体的配置主要包含了以下几个属性:

  • interfaceName 接口名称,通过该属性,我们就知道要访问调用的接口是哪个。上面的ConsumerBean在明确要调用哪个接口后就会生成这个接口的代理,从而再调用具体的方法供本地使用

  • version 版本号,如果你有稍稍接触这种服务框架,就应该知道我们的服务框架是在不断完善的,修改了某个模块后需要对外发布上线,这时候就需要修改版本。对于需要使用新功能的,我们则需要引用新版本的服务,也就是通过这里的version指定

  • group 分组,我们在请求某个接口时,可能该接口部署在不同机器上,我们通过group这个属性将这些机器分组,那么调用者就可以根据分组名来调用服务了,这么做的好处就是通过分组名将调用者隔离了。其实上面的version属性也是一种隔离,是将不同版本的服务隔离。

  • 调用方通过这样的配置企图找到需要的接口或者方法以供本地调用。那么我们又是如何真正的从调用端走向服务端,中间是如何找到我们在配置文件中指定配置信息对应的服务呢,这里介绍一种方式,见图
    十分钟带你了解服务化框架

图中有调用方也有服务方,当然,实际场景中的调用方和服务方的数量并不仅仅是图中的两个。那调用方怎么知道提供服务的有几个,都是谁,这时候我们需要有一个目录查询的角色,通过查找服务注册中心,调用方就知道当前有谁,并如何找到它。当然了,切实到具体选择那台机器,那又是负载均衡的事儿了,可以采用的策略如随机(random)、轮询(round-robin)或者权重等方式。

服务提供方

上面提到了调用方是如何完成自身配置并通过一些规则和策略找到自己想要的服务。这里我们看看服务提供方通过怎样的方式对外提供服务。
调用方有自己的配置来表明要调用的服务是长什么样,对应的,服务提供方自然已有自己的配置来供调用方识别。好比这样

<bean id = "salaryCalculator" class="com.jackie.ServiceFramework.ProviderBean">
    <property name = "interfaceName">
        <value>com.jackie.SalaryCalculator</value>
    </propperty>
    <property name = "target">
        <ref>salaryCalculatorImpl</ref>
    </propperty>
    <property name = "version">
        <value>1.0.0</value>
    </propperty>
    <property name = "group">
        <value>Salary</value>
    </propperty>
</bean>

这个配置我们已经很熟悉了,相较于调用方的配置,我们一眼就发现这里多了个target属性。这个属性主要是告诉调用方具体要调用的实现类是哪个。另外,还有一个不同就是这里的ConsumerBean变成了ProviderBean,这个主要职责是将自己的服务注册到上图中的服务注册查找中心,这样就是告诉别人我能提供什么服务,可以通过什么方式找到我。

十分钟到了,你了解了么

switch(your status) {
    case: get it
        give like;
        break;
    case: ambiguous
        share your question under comment area;
        break;
    case: still no idea about it
        repeat read from head and refer to other guides
        break;
    default:
        you win!!!
        break;
}

参考文献:《大型网站系统与Java中间件实现》

转载于:https://www.cnblogs.com/bigdataZJ/p/ServiceFramework.html