在此之前,你需要知道中间件的概念,可能在过往的从业生涯这个名词无数次的从你的眼前、耳畔都留下了足记,但是它的样子依然很模糊。
今天要说的服务化框架其实就是中间件的范畴,我们来看下,什么是中间件:
中间件是为软件应用提供了操作系统所提供的服务之外的服务,可以把中间件描述为 "软件胶水"。中间件不是操作系统的一部分,不是数据库管理系统,也不是软件应用的一部分,而是能够让软件开发者方便的处理通讯、输入和输出,能够专注在他们自己应用的部分。
从这段定义来看,我们要通俗易懂的描述中间件这个概念实在有些困难。所以我们首先借助我们了解的操作系统、数据库管理系统的概念,将中间件与他们撇清关系,而后通过一段较为抽象的说明对中间件下了个定义。
举个例子,如果你知道消息队列,比如我们常用的 RabbitMQ,它就是中间件,它可以为我们削峰,为我们提供异步处理业务的可能,它就是名副其实的软件胶水。下面我们从另外一个侧面——服务化框架来体会下中间件是一种什么样的存在。
在《大型网站的自强之路》中我们看到了大型网站的一步步演化,从单应用到多应用,从单库到分库分表,所有这些演化都是源于业务、访问量、并发量的增加。
这样一个网站结构简单又清晰,一般来说可以满足常规需求。但是随着业务越来越复杂,网站规模也日益扩大,原本清晰划分的应用模块 A、B 和 C 上加上了很多不属于他们的代码。这样的状况持续的时间越长,网站的结构就变的越来越没有边界,也就在高内聚低耦合的路上渐行渐远。
举例来说,某天我们在应用 A 上加的无关代码太多以至于我们不能再冠之以 "应用 A" 的名号。假设应用 A 表示的是商品系统,我们慢慢的在其基础上加入了商品的展示功能,商品的添加、商品的更新等等功能,代码不断的增加,维护的人也越来越多,让这个应用模块变的愈加臃肿复杂。
这时候我们需要做应用级别的拆分,对于用户管理系统我们可以单独提出一块,订单系统提出一块,商品系统作为一块…… 慢慢的,我们的网站结构图就成了这样
现在从功能划分这个角度来看,我们的系统相较于之前着实清晰了不少,再也不用很多不相干的代码挤在一起了。但是在实际使用过程中,我们发现有些东西我们可以作进一步的抽象,比如,在一个电商网站中,在订单系统和交易系统中,我们都有依赖用户系统。虽然各个应用模块看似分工明确,但实际上他们之间还是有些藕断丝连。
我们进一步抽象,将这些被多个应用模块的应用抽象为一个服务,添加一个服务层,使得各大应用之间的交集演变为服务层的一个服务,这样对于公共的服务抽象出来,应用层只需要调用相应的服务即可,再也不用自己重复造轮子了。
这样我们就得到了服务化的框架,这个框架有它自身的好处:
现如今,我们经常提到的微服务,也就是这种思想。一个模块就是一个完整的服务,内部通过调用底层的基础服务,然后对外提供服务,这样耦合性低且易于维护。
我想应该没有哪个有位青年,在当时学到 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
,很多中型、大型的网站和应用需要有自己的服务端,好比你安装的 QQ 或是微信是无法把服务端安装到你手机上的,你的手机其实只是充当了一个客户端的角色。下面我们来看看服务化框架是如何从集中式走向分布式的。
- localhost
现在一个财务系统中有一块需要计算每个月的工资,这时候有一个 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 可以认为是一个通用对象,是完成本地和远程服务的桥梁,具体的配置主要包含了以下几个属性:
图中有调用方也有服务方,当然,实际场景中的调用方和服务方的数量并不仅仅是图中的两个。那调用方怎么知道提供服务的有几个,都是谁,这时候我们需要有一个目录查询的角色,通过查找服务注册中心,调用方就知道当前有谁,并如何找到它。当然了,切实到具体选择那台机器,那又是负载均衡的事儿了,可以采用的策略如随机(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 中间件实现》
来源: http://www.cnblogs.com/bigdataZJ/p/ServiceFramework.html