thrift 入门(1/2)

一、thrift 定义

“什么是 thrift” 这个问题,我曾问过别人,也有人拿来问过我。
无论是官网、百度、谷歌还是博客,得到的答案都差不多,挨个补齐定语无非就是:

  • thrift 是一个服务框架。
  • thrift 是一个 rpc 服务框架。
  • thrift 是一个跨语言的 rpc 服务框架。
  • thrift 是一个 Facebook 公布的开源跨语言的 rpc 服务框架。

那 “rpc 又是什么” 呢?


二、什么是 RPC?

RPC 定义

  • RPC = Remote Procedure Call,远程过程调用。是一种进程间通信方式,允许像调用本地服务一样调用远程服务。
  • 比较另辟蹊径的解释是:RPC 是一种编程模式,把对服务器的调用抽象为过程调用,通常还伴随着框架代码自动生成等功能。
  • 为了便于理解,先把“远程过程调用”和“本地过程调用”做个对比。
rpc与本地过程调用对比

“远程过程调用(RPC)” 与 “本地过程调用” 对比
由上图可以看出:

  • 本地过程调用,所有的过程都在同一个服务器上,依次调用即可。
  • 远程过程调用(即 RPC),部分过程在 Client 部分过程在 Server,Client 想要使用 Server 中的过程并没有本地过程调用那样简单,需要两个端(Client 和 Server)交互。

RPC 与本地过程调用的的差异具体体现在两方面:

  1. RPC 要求双方具有网络通信能力。网络传输开销增加。
  2. RPC 要求双方统一数据结构和传输协议,因为远程过程与主调方运行在完全不同的地址空间中,无法通过传递指针直接调用。编程复杂度增高。

这就很容易让人想到基于 HTTP 的各种 webService 实现,因为它完全满足前述两个要求。

HTTP 和 RPC
这里单独说一下我的理解。

  • “基于 HTTP 的 webService 实现”,广义上可以算是一种 RPC 的实现方式,因为不同 webService 之间可以通过 HTTP 接口相互通信。
  • 但是 RPC 又不仅限于使用 HTTP 协议实现,HTTP 仅仅是 RPC 实现可选的一种传输协议,除了 HTTP 协议外,还可以选择 TCP/MQ 等其他方式。

回到正题,接下来看 thrift 作为一个 “RPC框架” 到底做了什么,或者说能做什么。


三、从 sayHi 开始

为了便于理解,我们从一个非常简单的小需求入手:要求是客户端输入姓名(例如:小明),服务端输出打招呼的句子(例如:Hi,小明!)。

PS:请不要把这里的 demo 当范本“摘抄”,仅仅是为了说明问题,写的略挫 ==

3.1 原生 Socket 裸写 sayHi

以 java 语言为例,用原生 jdk 类库提供的能力实现 sayHi 功能。

定义三个类:

  • V1SayHiService.java,sayHi 服务类。
  • V1SocketDemoServer.java,服务端。
  • V1SocketDemoClient.java,客户端。
// V1SayHiService.java
package com.ann.javas.projects.javacores.socket.demo;

public class V1SayHiService {
  public static String sayHi(String name) {
    return "Hi," + name + "!";
  }
}
// V1SocketDemoServer.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by liyanling on 2018/6/27.
 * ************************************
 * <p>
 * 交互方式(短连接)
 * <p>
 * 1、服务端启动监听。
 * 2、服务端【每】获得一个客户端连接,处理完一条消息后即【关闭连接】,并【等待下一个客户端连接】。
 * 3、服务端按行读取消息内容,作为姓名参数,回写 sayHi 消息
 */
public class V1SocketDemoServer {

  private static Logger logger = LoggerFactory.getLogger(V1SocketDemoServer.class);

  public static final int PORT = 9091;
  private static int count = 1;

  public static void main(String[] args) throws Exception {
    startServer();
  }

  private static void startServer() throws Exception {
    try {

      ServerSocket serverSocket = new ServerSocket(PORT);
      logger.info("{}:服务端(端口:{})启动监听,等待客户端连接", count++, PORT);

      while (true) {
        Socket socket = serverSocket.accept();
        logger.info("{}:服务端已与客户端建立连接,开始接收客户端消息...", count++);

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String name = reader.readLine();
        logger.info("{}:服务端获取输入流,读取客户端信息:{}", count++, name);

        String sayHiResult = V1SayHiService.sayHi(name);
        PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
        writer.write(sayHiResult + "\n");
        writer.flush();
        logger.info("{}:服务端获取输出流,响应客户端请求,回写:{}", count++, sayHiResult);

        writer.close();
        reader.close();
        socket.close();
//        serverSocket.close();// 这里不要把 serverSocket 关了。还得继续监听呢
        logger.info("{}:服务端关闭资源", count++);
        logger.info("{}:等待下一个客户端连接", count++);
      }

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

}

// V1SocketDemoClient.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * Created by liyanling on 2018/6/27.
 */
public class V1SocketDemoClient {

  private static      Logger logger = LoggerFactory.getLogger(V1SocketDemoClient.class);
  public static final String IP     = "127.0.0.1";

  public static Scanner scanner;
  public static int count = 1;


  public static void main(String[] args) throws Exception {
    scanner = new Scanner(System.in);

    logger.info("{}:demo1-----------",count++);
    startClient(IP, V1SocketDemoServer.PORT);
    Thread.sleep(1000);

    logger.info("{}:demo2-----------",count++);
    startClient(IP, V1SocketDemoServer.PORT);

    scanner.close();
  }

  public static void startClient(String IP, int PORT) throws Exception {
    try {
      logger.info("{}:准备连接服务端(IP:{},PORT:{})", count++, IP, PORT);

      Socket socket = new Socket(IP, PORT);
      logger.info("{}:服务端已连接成功(IP:{},PORT:{})", count++, IP, PORT);

      PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
      logger.info("{}:客户端获取输出流,向服务端发送信息:", count++);

      logger.info("{}:请输入姓名:", count++);
      String name = scanner.nextLine();

      writer.write(name + "\n");
      writer.flush();

      BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      logger.info("{}:客户端获取输入流,读取服务端返回信息:{}", count++, reader.readLine());

      reader.close();
      writer.close();
      socket.close();
      logger.info("{}:关闭资源", count++);

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

}

先启动 Server 再启动 Client,然后在客户端终端输入“小明”并回车,两端分别输出的日志如下:

// V1SocketDemoServer.java 日志
00:28:02.023 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 1:服务端(端口:9091)启动监听,等待客户端连接
00:28:06.032 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 2:服务端已与客户端建立连接,开始接收客户端消息...
00:28:09.817 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 3:服务端获取输入流,读取客户端信息:小明
00:28:09.822 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 4:服务端获取输出流,响应客户端请求,回写:Hi,小明!
00:28:09.823 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 5:服务端关闭资源
00:28:09.823 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 6:等待下一个客户端连接
00:28:10.828 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - 7:服务端已与客户端建立连接,开始接收客户端消息...

// V1SocketDemoClient.java 日志
00:28:06.028 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 1:demo1-----------
00:28:06.029 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 2:准备连接服务端(IP:127.0.0.1,PORT:9091)
00:28:06.032 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 3:服务端已连接成功(IP:127.0.0.1,PORT:9091)
00:28:06.033 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 4:客户端获取输出流,向服务端发送信息:
00:28:06.033 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 5:请输入姓名:
小明
00:28:09.822 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 6:客户端获取输入流,读取服务端返回信息:Hi,小明!
00:28:09.824 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 7:关闭资源
00:28:10.827 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 8:demo2-----------
00:28:10.828 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 9:准备连接服务端(IP:127.0.0.1,PORT:9091)
00:28:10.828 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 10:服务端已连接成功(IP:127.0.0.1,PORT:9091)
00:28:10.829 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 11:客户端获取输出流,向服务端发送信息:
00:28:10.829 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoClient - 12:请输入姓名:

这是原生 Socket 实现的 sayHi,非常简单,100多行代码而已。

双端通信和服务规则如下:

  1. Server 仅提供 sayHi 功能。
  2. sayHi 功能仅一个姓名参数。
  3. 客户端->服务端 逐行写入参数,行数据即姓名。
  4. 服务端->客户端 逐行回写返回值,行数据即 sayHi 消息。

3.2 sayHi 迭代升级

接下来,我们对 sayHi 功能升级一下:

  1. 加“性别”参数,男性称呼为“xx先生”,女性称呼为“xx女士”。
  2. 加“体重”参数,超过 200 斤则提醒:“你该减肥啦”。

到这儿,前面的一部分规则就要被打破了,一次请求不能仅仅带一个“姓名”参数,还要带上性别和体重,重新制定规则如下:

  1. Server 仅提供 sayHi 功能。
  2. sayHi 功能三个参数:姓名、性别、体重。
  3. 客户端->服务端 逐行写入参数,行数据按姓名、性别、体重顺序带三个参数,参数用“,”分隔。
  4. 服务端->客户端 逐行回写返回值,行数据即 sayHi 消息。

在原来的基础上,做个 V2 版本:

  • V2SayHiService.java,sayHi 服务类。
  • V2SayHiParam.java,sayHi 参数。
  • V2SocketDemoServer.java,服务端。
  • V2SocketDemoClient.java,客户端。
// V2SayHiService.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by liyanling on 2018/9/9.
 */
public class V2SayHiService {

  private static       Logger logger          = LoggerFactory.getLogger(V1SocketDemoServer.class);
  private final static String SAY_HI_TEMPLATE = "Hi,%s%s!%s";

  public static String sayHi(String params) {
    try {
      return sayHi(V2SayHiParam.decode(params));
    } catch (Throwable t) {
      logger.error("caught exception,t=", t);
      return "sayHi参数格式错误:" + params;
    }
  }

  private static String sayHi(V2SayHiParam v2SayHiParam) {
    String name = v2SayHiParam.getName();
    String nameSuffix = V2SayHiParam.Gender.BOY == v2SayHiParam.getGender() ? "先生" : "女士";
    String weightNotice = v2SayHiParam.getWeight() >= 200 ? "你该减肥啦~" : "";
    return String.format(SAY_HI_TEMPLATE, name, nameSuffix, weightNotice);
  }

}


// V2SayHiParam.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class V2SayHiParam {

  private static Logger logger = LoggerFactory.getLogger(V2SayHiParam.class);

  public String name;
  public Gender gender;
  public int    weight;

  public enum Gender {
    GIRL, BOY
  }

  public V2SayHiParam() {
  }

  public V2SayHiParam(String name, Gender gender, int weight) {
    this.name = name;
    this.gender = gender;
    this.weight = weight;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Gender getGender() {
    return gender;
  }

  public void setGender(Gender gender) {
    this.gender = gender;
  }

  public int getWeight() {
    return weight;
  }

  public void setWeight(int weight) {
    this.weight = weight;
  }

  @Override
  public String toString() {
    return "V2SayHiParam{" + "name='" + name + '\'' + ", gender=" + gender + ", weight=" + weight + '}';
  }


  // String -> V2SayHiParam
  public static V2SayHiParam decode(String params) {
    int separatorLocate1 = params.indexOf(',');
    int separatorLocate2 = params.indexOf(',', separatorLocate1 + 1);
    int length = params.length();

    String name = params.substring(0, separatorLocate1);
    String gender = params.substring(separatorLocate1 + 1, separatorLocate2);
    int weight = Integer.valueOf(params.substring(separatorLocate2 + 1, length));

    return new V2SayHiParam(name, Gender.valueOf(gender), weight);
  }


  // V2SayHiParam -> String
  public static String encode(V2SayHiParam v2SayHiParam) {
    return v2SayHiParam.getName() + "," + v2SayHiParam.getGender() + "," + v2SayHiParam.getWeight();
  }


}

// V2SocketDemoServer.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by liyanling on 2018/6/27.
 */
public class V2SocketDemoServer {

  private static Logger logger = LoggerFactory.getLogger(V2SocketDemoServer.class);

  public static final int PORT = 9092;
  private static int count = 1;

  public static void main(String[] args) throws Exception {
    startServer();
  }

  private static void startServer() throws Exception {
    try {

      ServerSocket serverSocket = new ServerSocket(PORT);
      logger.info("{}:服务端(端口:{})启动监听,等待客户端连接", count++, PORT);

      while (true) {
        Socket socket = serverSocket.accept();
        logger.info("{}:服务端已与客户端建立连接,开始接收客户端消息...", count++);

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String params = reader.readLine();
        logger.info("{}:服务端获取输入流,读取客户端信息:{}", count++, params);

        String sayHiResult = V2SayHiService.sayHi(params);

        PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
        writer.write(sayHiResult + "\n");
        writer.flush();
        logger.info("{}:服务端获取输出流,响应客户端请求,回写:{}", count++, sayHiResult);

        writer.close();
        reader.close();
        socket.close();
//        serverSocket.close();// 这里不要把 serverSocket 关了。还得继续监听呢
        logger.info("{}:服务端关闭资源", count++);
        logger.info("{}:等待下一个客户端连接", count++);
      }

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

}

// V2SocketDemoClient.java
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * Created by liyanling on 2018/6/27.
 */
public class V2SocketDemoClient {

  private static      Logger logger = LoggerFactory.getLogger(V2SocketDemoClient.class);
  public static final String IP     = "127.0.0.1";

  public static Scanner scanner;
  public static int count = 1;

  public static void main(String[] args) throws Exception {
    scanner = new Scanner(System.in);

    logger.info("{}:demo1-----------",count++);
    startClient(IP, V2SocketDemoServer.PORT);
    Thread.sleep(1000);
    logger.info("{}:demo2-----------",count++);
    startClient(IP, V2SocketDemoServer.PORT);

    scanner.close();
  }

  public static void startClient(String IP, int PORT) throws Exception {
    try {
      logger.info("{}:准备连接服务端(IP:{},PORT:{})", count++, IP, PORT);

      Socket socket = new Socket(IP, PORT);
      logger.info("{}:服务端已连接成功(IP:{},PORT:{})", count++, IP, PORT);

      PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
      logger.info("{}:客户端获取输出流,向服务端发送信息:", count++);

      logger.info("{}:请输入姓名:", count++);
      String name = scanner.nextLine();
      logger.info("{}:请输入性别(GIRL / BOY):", count++);
      String gender = scanner.nextLine();
      logger.info("{}:请输入体重(单位:斤):", count++);
      Integer weight = Integer.valueOf(scanner.nextLine());

      V2SayHiParam v2SayHiParam = new V2SayHiParam(name, V2SayHiParam.Gender.valueOf(gender), weight);

      writer.write(V2SayHiParam.encode(v2SayHiParam) + "\n");
      writer.flush();

      BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      logger.info("{}:客户端获取输入流,读取服务端返回信息:{}", count++, reader.readLine());

      reader.close();
      writer.close();
      socket.close();
      logger.info("{}:关闭资源", count++);

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }
}

仍旧是先启动服务端,再启动客户端,在客户端终端输入:大熊、BOY、220,分别是姓名、性别、体重。输出日志如下:

// V2SocketDemoServer.java 日志
00:30:02.588 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 1:服务端(端口:9092)启动监听,等待客户端连接
00:30:05.646 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 2:服务端已与客户端建立连接,开始接收客户端消息...
00:30:17.821 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 3:服务端获取输入流,读取客户端信息:大熊,BOY,220
00:30:17.829 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 4:服务端获取输出流,响应客户端请求,回写:Hi,大熊先生!你该减肥啦~
00:30:17.830 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 5:服务端关闭资源
00:30:17.830 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 6:等待下一个客户端连接
00:30:18.835 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoServer - 7:服务端已与客户端建立连接,开始接收客户端消息...

// V2SocketDemoClient.java 日志
00:30:05.639 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 1:demo1-----------
00:30:05.641 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 2:准备连接服务端(IP:127.0.0.1,PORT:9092)
00:30:05.646 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 3:服务端已连接成功(IP:127.0.0.1,PORT:9092)
00:30:05.648 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 4:客户端获取输出流,向服务端发送信息:
00:30:05.648 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 5:请输入姓名:
大熊
00:30:12.951 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 6:请输入性别(GIRL / BOY):
BOY
00:30:15.267 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 7:请输入体重(单位:斤):
220
00:30:17.829 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 8:客户端获取输入流,读取服务端返回信息:Hi,大熊先生!你该减肥啦~
00:30:17.830 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 9:关闭资源
00:30:18.834 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 10:demo2-----------
00:30:18.834 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 11:准备连接服务端(IP:127.0.0.1,PORT:9092)
00:30:18.835 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 12:服务端已连接成功(IP:127.0.0.1,PORT:9092)
00:30:18.835 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 13:客户端获取输出流,向服务端发送信息:
00:30:18.835 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V2SocketDemoClient - 14:请输入姓名:

3.3 能力扩展

再来个升级,除 sayHi 功能外,还需要:

  1. 新增 locate 功能,用于定位用户位置。
  2. 新增 adsRecommendation 功能,用于推送广告。

前面的实现默认只提供 sayHi 功能,要新增 locate 和 adsRecommendation 支持,再次修改规则为:

  1. Server 仅提供 sayHi、locate、adsRecommendation功能。
  2. sayHi 功能三个参数:姓名、性别、体重。
  3. locate 和 adsRecommendation 无参数。
  4. 客户端->服务端 逐行写入功能名和参数,参数仍旧按顺序“,”分隔,功能名和参数之间用“|”分隔。
  5. 服务端->客户端 逐行回写返回值。

类定义和源码如下:

  • V3DemoService.java,demo 服务类,提供 sayHi、locate、adsRecommendation 功能。
  • V3SayHiParam.java,sayHi 参数。
  • V3ocketDemoServer.java,服务端。
  • V3SocketDemoClient.java,客户端。
// V3DemoService.java 
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * Created by liyanling on 2018/9/9.
 */
public class V3DemoService {

  private static       Logger logger          = LoggerFactory.getLogger(V1SocketDemoServer.class);

  private final static String SAY_HI_TEMPLATE = "Hi,%s%s!%s";

  private final static List<String> CITIES = Arrays.asList("上海", "北京", "广州", "成都", "内蒙古", "香港", "河北", "云南");

  private final static List<String> ADS = Arrays.asList("坐红旗车,走中国路", "要想皮肤好,早晚用大宝", "喝汇源果汁,走健康之路", "送礼就送脑白金",
                                                        "飘柔,就是这么自信");

  public static String locate() {
    int citySize = CITIES.size();
    int randomIndex = Math.abs((new Random(System.currentTimeMillis())).nextInt()) % citySize;
    return CITIES.get(randomIndex);
  }

  public static String adsRecommendation() {
    int adsSize = ADS.size();
    int randomIndex = Math.abs((new Random(System.currentTimeMillis())).nextInt()) % adsSize;
    return ADS.get(randomIndex);
  }

  public static String sayHi(String params) {
    try {
      return sayHi(V3SayHiParam.decode(params));
    } catch (Throwable t) {
      logger.error("caught exception,t=", t);
      return "sayHi参数格式错误:" + params;
    }
  }

  private static String sayHi(V3SayHiParam v3SayHiParam) {
    String name = v3SayHiParam.getName();
    String nameSuffix = V3SayHiParam.Gender.BOY == v3SayHiParam.getGender() ? "先生" : "女士";
    String weightNotice = v3SayHiParam.getWeight() >= 200 ? "你该减肥啦~" : "";
    return String.format(SAY_HI_TEMPLATE, name, nameSuffix, weightNotice);
  }

  public static String doSomething(String params) {
    try {
      int separatorLocate = params.indexOf('|');
      int length = params.length();

      String function = params.substring(0, separatorLocate);

      String functionParams = params.substring(separatorLocate + 1, length);

      logger.info("demoService function:{} functionParams:{}",function,functionParams);
      if ("sayHi".equals(function)) {
        return sayHi(functionParams);
      } else if ("locate".equals(function)) {
        return locate();
      } else if ("adsRecommendation".equals(function)) {
        return adsRecommendation();
      } else {
        return "不支持" + function + "功能";
      }

    } catch (Throwable t) {
      logger.error("caught error,t=", t);
      return "参数格式错误:" + params;
    }

  }
}


// V3SayHiParam.java 
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class V3SayHiParam {

  private static Logger logger = LoggerFactory.getLogger(V3SayHiParam.class);

  public String name;
  public Gender gender;
  public int    weight;

  public enum Gender {
    GIRL, BOY
  }

  public V3SayHiParam() {
  }

  public V3SayHiParam(String name, Gender gender, int weight) {
    this.name = name;
    this.gender = gender;
    this.weight = weight;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Gender getGender() {
    return gender;
  }

  public void setGender(Gender gender) {
    this.gender = gender;
  }

  public int getWeight() {
    return weight;
  }

  public void setWeight(int weight) {
    this.weight = weight;
  }

  @Override
  public String toString() {
    return "V3SayHiParam{" + "name='" + name + '\'' + ", gender=" + gender + ", weight=" + weight + '}';
  }


  // String -> V3SayHiParam
  public static V3SayHiParam decode(String params) {
    int separatorLocate1 = params.indexOf(',');
    int separatorLocate2 = params.indexOf(',', separatorLocate1 + 1);
    int length = params.length();

    String name = params.substring(0, separatorLocate1);
    String gender = params.substring(separatorLocate1 + 1, separatorLocate2);
    int weight = Integer.valueOf(params.substring(separatorLocate2 + 1, length));

    return new V3SayHiParam(name, Gender.valueOf(gender), weight);
  }


  // V3SayHiParam -> String
  public static String encode(V3SayHiParam v3SayHiParam) {
    return v3SayHiParam.getName() + "," + v3SayHiParam.getGender() + "," + v3SayHiParam.getWeight();
  }


}

// V3ocketDemoServer.java 
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by liyanling on 2018/6/27.
 */
public class V3SocketDemoServer {

  private static Logger logger = LoggerFactory.getLogger(V3SocketDemoServer.class);

  public static final int PORT = 9093;
  private static int count = 1;

  public static void main(String[] args) throws Exception {
    startServer();
  }

  private static void startServer() throws Exception {
    try {

      ServerSocket serverSocket = new ServerSocket(PORT);
      logger.info("{}:服务端(端口:{})启动监听,等待客户端连接", count++, PORT);

      while (true) {
        Socket socket = serverSocket.accept();
        logger.info("{}:服务端已与客户端建立连接,开始接收客户端消息...", count++);

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String params = reader.readLine();
        logger.info("{}:服务端获取输入流,读取客户端信息:{}", count++, params);

        String sayHiResult = V3DemoService.doSomething(params);

        PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
        writer.write(sayHiResult + "\n");
        writer.flush();
        logger.info("{}:服务端获取输出流,响应客户端请求,回写:{}", count++, sayHiResult);

        writer.close();
        reader.close();
        socket.close();
//        serverSocket.close();// 这里不要把 serverSocket 关了。还得继续监听呢
        logger.info("{}:服务端关闭资源", count++);
        logger.info("{}:等待下一个客户端连接", count++);
      }

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

}

// V3SocketDemoClient.java 
package com.ann.javas.projects.javacores.socket.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * Created by liyanling on 2018/6/27.
 */
public class V3SocketDemoClient {

  private static      Logger logger = LoggerFactory.getLogger(V3SocketDemoClient.class);
  public static final String IP     = "127.0.0.1";

  public static Scanner scanner;
  public static int count = 1;

  public static void main(String[] args) throws Exception {
    scanner = new Scanner(System.in);

    logger.info("{}:demo1-----------", count++);
    startClient(IP, V3SocketDemoServer.PORT);
    Thread.sleep(1000);
    logger.info("{}:demo2-----------", count++);
    startClient(IP, V3SocketDemoServer.PORT);

    Thread.sleep(1000);
    logger.info("{}:demo3-----------", count++);
    startClient(IP, V3SocketDemoServer.PORT);

    scanner.close();
  }

  public static void startClient(String IP, int PORT) throws Exception {
    try {
      logger.info("{}:准备连接服务端(IP:{},PORT:{})", count++, IP, PORT);

      Socket socket = new Socket(IP, PORT);
      logger.info("{}:服务端已连接成功(IP:{},PORT:{})", count++, IP, PORT);

      PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
      logger.info("{}:客户端获取输出流,向服务端发送信息:", count++);

      logger.info("{}:请输入功能名(sayHi / locate / adsRecommendation):", count++);
      String function = scanner.nextLine();
      String line = "";



      if ("sayHi".equals(function)) {
        logger.info("{}:请输入姓名:", count++);
        String name = scanner.nextLine();
        logger.info("{}:请输入性别(GIRL / BOY):", count++);
        String gender = scanner.nextLine();
        logger.info("{}:请输入体重(单位:斤):", count++);
        Integer weight = Integer.valueOf(scanner.nextLine());

        V3SayHiParam v3SayHiParam = new V3SayHiParam(name, V3SayHiParam.Gender.valueOf(gender), weight);
        line = function + "|" + V3SayHiParam.encode(v3SayHiParam);
      } else {
        line = function + "|";
      }

      writer.write(line + "\n");
      writer.flush();
      BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      logger.info("{}:客户端获取输入流,读取服务端返回信息:{}", count++, reader.readLine());

      reader.close();
      writer.close();
      socket.close();
      logger.info("{}:关闭资源", count++);

    } catch (Throwable t) {
      t.printStackTrace();
    }
  }
}

分别测试 locate、adsRecommendation、sayHi 功能,输出日志如下:

// V3SocketDemoServer.java 日志
00:31:37.510 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 1:服务端(端口:9093)启动监听,等待客户端连接
00:31:51.403 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 2:服务端已与客户端建立连接,开始接收客户端消息...
00:32:00.525 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 3:服务端获取输入流,读取客户端信息:locate|
00:32:00.532 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - demoService function:locate functionParams:
00:32:00.534 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 4:服务端获取输出流,响应客户端请求,回写:上海
00:32:00.535 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 5:服务端关闭资源
00:32:00.535 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 6:等待下一个客户端连接
00:32:01.539 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 7:服务端已与客户端建立连接,开始接收客户端消息...
00:32:08.982 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 8:服务端获取输入流,读取客户端信息:adsRecommendation|
00:32:08.982 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - demoService function:adsRecommendation functionParams:
00:32:08.983 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 9:服务端获取输出流,响应客户端请求,回写:飘柔,就是这么自信
00:32:08.983 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 10:服务端关闭资源
00:32:08.983 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 11:等待下一个客户端连接
00:32:09.989 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 12:服务端已与客户端建立连接,开始接收客户端消息...
00:32:25.251 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 13:服务端获取输入流,读取客户端信息:sayHi|小明,GIRL,210
00:32:25.252 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V1SocketDemoServer - demoService function:sayHi functionParams:小明,GIRL,210
00:32:25.254 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 14:服务端获取输出流,响应客户端请求,回写:Hi,小明女士!你该减肥啦~
00:32:25.254 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 15:服务端关闭资源
00:32:25.255 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoServer - 16:等待下一个客户端连接

// V3SocketDemoClient.java 日志
00:31:51.398 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 1:demo1-----------
00:31:51.399 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 2:准备连接服务端(IP:127.0.0.1,PORT:9093)
00:31:51.404 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 3:服务端已连接成功(IP:127.0.0.1,PORT:9093)
00:31:51.406 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 4:客户端获取输出流,向服务端发送信息:
00:31:51.407 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 5:请输入功能名(sayHi / locate / adsRecommendation):
locate
00:32:00.535 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 6:客户端获取输入流,读取服务端返回信息:上海
00:32:00.535 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 7:关闭资源
00:32:01.538 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 8:demo2-----------
00:32:01.538 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 9:准备连接服务端(IP:127.0.0.1,PORT:9093)
00:32:01.539 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 10:服务端已连接成功(IP:127.0.0.1,PORT:9093)
00:32:01.539 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 11:客户端获取输出流,向服务端发送信息:
00:32:01.539 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 12:请输入功能名(sayHi / locate / adsRecommendation):
adsRecommendation
00:32:08.983 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 13:客户端获取输入流,读取服务端返回信息:飘柔,就是这么自信
00:32:08.983 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 14:关闭资源
00:32:09.988 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 15:demo3-----------
00:32:09.988 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 16:准备连接服务端(IP:127.0.0.1,PORT:9093)
00:32:09.991 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 17:服务端已连接成功(IP:127.0.0.1,PORT:9093)
00:32:09.991 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 18:客户端获取输出流,向服务端发送信息:
00:32:09.991 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 19:请输入功能名(sayHi / locate / adsRecommendation):
sayHi
00:32:12.661 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 20:请输入姓名:
小明
00:32:16.647 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 21:请输入性别(GIRL / BOY):
GIRL
00:32:22.979 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 22:请输入体重(单位:斤):
210
00:32:25.254 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 23:客户端获取输入流,读取服务端返回信息:Hi,小明女士!你该减肥啦~
00:32:25.254 [main] INFO  com.ann.javas.projects.javacores.socket.demo.V3SocketDemoClient - 24:关闭资源

3.4 小结

从 V1 到 V2 再到 V3,你会发现一个很痛的点是:随便来个新需求,都会导致客户端与服务端的“大换血”,可扩展性非常差,说到底,还是“序列化与反序列化规则”做的不够好,单单行数据、","分隔、"|"分隔 这种“口头”约定,是远远不够的。

从这个角度入手,我们来看 thrift 是怎么做的。


下接:thrift 入门(2/2)
PS:我也不想拆,但是放一起文章太长无法发布。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容

  • 转自:http://blog.csdn.net/kesonyk/article/details/50924489 ...
    晴天哥_王志阅读 24,817评论 2 38
  • 上接:thrift 入门(1/2)PS:我也不想拆,但是放一起文章太长无法发布。。。 四、thrift 入门 4....
    李眼镜阅读 1,071评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,678评论 18 139
  • Thrift是什么? Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,...
    jiangmo阅读 9,421评论 0 6
  • 现在有很多家长,花很多很多的时间陪孩子,但一般都在做两件事:1.下指令,纠偏。指导孩子做作业,然后挑出错误的,要求...
    高宇_Betty阅读 414评论 1 5