Thrift

跨语言rpc框架Thrift实战

一.Thrift介绍

1.Thrift的定义

**Thrift是一个轻量级、跨语言的RPC框架,主要用于各个服务之间的RPC通信,**最初由Facebook于2007 年开发,2008年进入Apache开源项目。它通过自身的IDL中间语言, 并借助代码生成引擎生成各种主流 语言的RPC服务端/客户端模板代码。Thrift支持多种不同的编程语言,包括C++, Java, Python, PHP, Ruby, Erlang, Haskell, C#, Cocoa, Javascript, Node.js, Smalltalk, OCaml, Golang等,本系列主要讲述 基于Java语言的Thrift的配置方式和具体使用。

RPC 全称 Remote Procedure Call——远程过程调用。RPC技术简单说就是为了解决远程调用服务 的一种技术,使得调用者像调用本地服务一样方便透明

2.Thrift的架构

Thrift技术栈分层从下向上分别为:传输层(Transport Layer)、协议层(Protocol Layer)、处理层 (Processor Layer)和服务层(Server Layer)

  • 传输层(Transport Layer):传输层负责直接从网络中读取和写入数据,它定义了具体的网络传输 协议;比如说TCP/IP传输等。
  • 协议层(Protocol Layer):协议层定义了数据传输格式,负责网络传输数据的序列化和反序列化; 比如说JSON、XML、二进制数据等。
  • 处理层(Processor Layer):处理层是由具体的IDL(接口描述语言)生成的,封装了具体的底层网络传输和序列化方式,并委托给用户实现的Handler进行处理。
  • 服务层(Server Layer):整合上述组件,提供具体的网络IO模型(单线程/多线程/事件驱动),形成 最终的服务

针对采用TCP/IP作为更底层的通信协议的话,整个通信过程如下图所示:

2.Thrift的特性

(1)开发速度快

通过编写RPC接口Thrift IDL文件,利用编译生成器自动生成服务端骨架(Skeletons)和客户端桩(Stubs)。 从而省去开发者自定义和维护接口编解码、消息传输、服务器多线程模型等基础工作。

服务端:只需要按照服务骨架即接口,编写好具体的业务处理程序(Handler)即实现类即可。

客户端:只需要拷贝IDL定义好的客户端桩和服务对象,然后就像调用本地对象的方法一样调用远端服 务。

(2)接口维护简单

通过维护Thrift格式的IDL(接口描述语言)文件(注意写好注释),即可作为给Client使用的接口文档 使用,也自动生成接口代码,始终保持代码和文档的一致性。且Thrift协议可灵活支持接口的可扩展性。

(3)学习成本低

因为其来自Google Protobuf开发团队,所以其IDL文件风格类似Google Protobuf,且更加易读易懂; 特别是RPC服务接口的风格就像写一个面向对象的Class一样简单。

初学者只需参照:http://thrift.apache.org/,一个多小时就可以理解Thrift IDL文件的语法使用。

(4)多语言/跨语言支持

Thrift支持C++、 Java、Python、PHP、Ruby、Erlang、Perl、Haskell、C#、Cocoa、JavaScript、 Node.js、Smalltalk等多种语言,即可生成上述语言的服务器端和客户端程序。

(5)稳定/广泛使用

Thrift在很多开源项目中已经被验证是稳定和高效的,例如Cassandra、Hadoop、HBase等;国外在 Facebook中有广泛使用,国内包括百度、美团小米、和饿了么等公司。

二.IDL详解

Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。既然客户端和 服务端能使用不同的语言开发,那么一定就要有一种中间语言来关联客户端和服务端的语言,这种语言 就是IDL (InterfaceDescription Language)

Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过Thrift提供的编 译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。

1.IDL语法

(1)基本类型(Base Types)

基本类型就是:不管哪一种语言,都支持的数据形式表现。Apache Thrift中支持以下几种基本类型:

(2)特殊类型(Special Types)

binary: 未编码的字节序列,是string的一种特殊形式;这种类型主要是方便某些场景下JAVA调用。JAVA 中对应的是java.nio.ByteBuffer类型,GO中是[]byte

(3)集合容器(Containers)

有3种可用容器类型:

在使用容器类型时必须指定泛型,否则无法编译idl文件。其次,泛型中的基本类型,JAVA语言中会被替 换为对应的包装类型。

集合中的元素可以是除了service之外的任何类型,包括exception。

struct Test {
 1: map<string, User> usermap,
 2: set<i32> intset,
 3: list<double> doublelist
}

(4)常量及类型别名(Const&&Typedef)

//常量定义
const i32 MALE_INT = 1
const map<i32, string> GENDER_MAP = {1: "male", 2: "female"}
//某些数据类型比较长可以用别名简化
typedef map<i32, string> gmp

(5)struct类型

在面向对象语言中,表现为“类定义”;在弱类型语言、动态语言中,表现为“结构/结构体”。定义格式如 下:

struct <结构体名称> {
     <序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
}

例如:

struct User{
 1: required string name, //该字段必须填写
 2: optional i32 age = 0; //默认值
 3: bool gender //默认字段类型为optional
}
struct bean{
   1: i32 number=10,
   2: i64 bigNumber,
   3: double decimals,
   4: string name="thrifty"
}

struct有以下一些约束:

1.struct不能继承,但是可以嵌套,不能嵌套自己。

2.其成员都是有明确类型。

3.成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。

4.成员分割符可以是逗号(,)或是分号(;),而且可以混用。

5.字段会有optional和required之分和protobuf一样,但是如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去,optional是不填充则不序列化,required是必须填充也必须序列化。

6.每个字段可以设置默认值

7.同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。

(6)枚举(enum)

Thrift不支持枚举类嵌套,枚举常量必须是32位的正整数

enum HttpStatus {
 OK = 200,
 NOTFOUND=404
}

(7)异常(Exceptions)

异常在语法和功能上类似于结构体,差别是异常使用关键字exception,而且异常是继承每种语言的基础 异常类。

exception MyException {
   1: i32 errorCode
   2: string message
}
service ExampleService {
 string GetName() throws (1: MyException e)
}

(8)Service (服务定义类型)

服务的定义方法在语义上等同于面向对象语言中的接口。

service HelloService {
   i32 sayInt(1:i32 param)
   string sayString(1:string param)
   bool sayBoolean(1:bool param)
   void sayVoid()
}

编译后的Java代码

public class HelloService {
 public interface Iface {
   public int sayInt(int param) throws org.apache.thrift.TException;
   public java.lang.String sayString(java.lang.String param) throws 
org.apache.thrift.TException;
   public boolean sayBoolean(boolean param) throws 
org.apache.thrift.TException;
   public void sayVoid() throws org.apache.thrift.TException;
 }
 // ... 省略很多代码
}

(9)Namespace (名字空间)

Thrift中的命名空间类似于C++中的namespace和java中的package,它们提供了一种组织(隔离)代码 的简便方式。名字空间也可以用于解决类型定义中的名字冲突。 由于每种语言均有自己的命名空间定义方式(如python中有module), thrift允许开发者针对特定语言 定义namespace。

namespace java com.example.test

转化成

package com.example.test
namespace java com.tuling // 命名空间定义,规范:namespace + 语言 + 包路径
service Hello{ // 接口定义,类似Java接口定义
   string getWord(), // 方法定义,类似Java接口定义
   void writeWold(1:string words) //参数类型指定

(10)Comment (注释)

Thrift支持C多行风格和Java/C++单行风格。

/** 
 * This is a multi-line comment. 
 * Just like in C. 
 */
 // C++/Java style single-line comments work just as well.

(11)Include

便于管理、重用和提高模块性/组织性,我们常常分割Thrift定义在不同的文件中。包含文件搜索方式与 c++一样。Thrift允许文件包含其它thrift文件,用户需要使用thrift文件名作为前缀访问被包含的对像。

include "test.thrift"
...
struct StSearchResult {
1: in32 uid;
...
}

thrift文件名要用双引号包含,末尾没有逗号或者分号

2.thrift编译器的安装

参考文档:https://thrift.apache.org/docs/install/

(1)windows 安装

下载地址:https://thrift.apache.org/download

(2)centos 安装

参考文档:https://thrift.apache.org/docs/install/centos.html

3.IDL文件编译

DL文件可以直接用来生成各种语言的代码,下面给出常用的各种不同语言的代码生成命令:

# 生成java
thrift -gen java user.thrift
# 生成c++
thrift -gen cpp user.thrift
# 生成php
thrift -gen php user.thrift
# 生成node.js
thrift -gen js:node user.thrift
#可以通过以下命令查看生成命令的格式
thrift -help
//指定输出目录
thrift --gen java -o target user.thrift

三.Thrift的协议

Thrift可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本(text)和 二进制(binary)传输协议。为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数, 有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求。常用协议有以下几种:

  • TBinaryProtocol:二进制编码格式进行数据传输
  • TCompactProtocol:高效率的、密集的二进制编码格式进行数据传输
  • TJSONProtocol: 使用JSON文本的数据编码协议进行数据传输
  • TSimpleJSONProtocol:只提供JSON只写的协议,适用于通过脚本语言解析

四.Thrift的传输层

常用的传输层有以下几种:

  • TSocket:使用阻塞式I/O进行传输,是最常见的模式
  • TNonblockingTransport:使用非阻塞方式,用于构建异步客户端
  • TFramedTransport:使用非阻塞方式,按块的大小进行传输,类似于Java中的NIO

五.Thrift快速开始

1.编写user.thrift文件

namespace java com.tuling
struct User{
   1:i32 id
   2:string name
   3:i32 age=0
}
service UserService {
 User getById(1:i32 id)
 bool isExist(1:string name)
}

2.通过编译器编译user.thrift文件,生成java接口类文件

#编译user.thrift
thrift -gen java user.thrift

由于未指定代码生成的目标目录,生成的类文件默认存放在 gen-java 目录下

对于开发人员而言,使用原生的Thrift框架,仅需要关注以下四个核心内部接口/类:Iface, AsyncIface, Client和AsyncClient。

  • Iface:服务端通过实现UserService.Iface接口,向客户端的提供具体的同步业务逻辑。
  • AsyncIface:服务端通过实现UserService.Iface接口,向客户端的提供具体的异步业务逻辑。
  • Client:客户端通过UserService.Client的实例对象,以同步的方式访问服务端提供的服务方法。
  • AsyncClient:客户端通过UserService.AsyncClient的实例对象,以异步的方式访问服务端提供的 服务方法。

(1)单线程同步阻塞demo

新建maven工程,引入thrift依赖

<dependency>
   <groupId>org.apache.thrift</groupId>
   <artifactId>libthrift</artifactId>
   <version>0.15.0</version>
</dependency>

将生成类的UserService.java源文件拷贝进项目源文件目录中,并实现UserServiceService.Iface的定义 的getById()方法。

public class UserServiceImpl implements UserService.Iface {
    @Override
    public User getById(int id) throws TException {
        System.out.println("=====调用getById=====");
        //todo 模拟业务调用
        User user = new User();
        user.setId(id);
        user.setName("fox");
        user.setAge(30);
        return user;
   }
    @Override
    public boolean isExist(String name) throws TException {
    return false;
   }
}

4.服务器端程序编写

public class SimpleService {
    public static void main(String[] args) {
        try {
            TServerTransport serverTransport = new TServerSocket(9090);
            //获取processor
            UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
            //指定TBinaryProtocol
            TBinaryProtocol.Factory protocolFactory = new 
TBinaryProtocol.Factory();
            TSimpleServer.Args targs = new TSimpleServer.Args(serverTransport);
            targs.processor(processor);
            targs.protocolFactory(protocolFactory);
            //单线程服务模型,一般用于测试
            TServer server = new TSimpleServer(targs);
            System.out.println("Starting the simple server...");
            server.serve();
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}

运行服务端程序,服务端在指定端口监听客户端的连接请求

5.客户端程序编写

public class SimpleClient {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            // 使用阻塞io
            transport = new TSocket("localhost", 9090);
           //指定二进制编码格式
            TProtocol protocol = new TBinaryProtocol(transport);
            UserService.Client client = new UserService.Client(protocol);
            transport.open();
            //发起rpc调用
            User result = client.getById(1);
            System.out.println("Result : " + result);
       } catch (TException e) {
            e.printStackTrace();
       } finally {
            if (null != transport) {
                transport.close();
           }
       }
   }
}

运行客户端程序,客户端通过网络请求HelloWorldService的say()方法的具体实现,控制台输出返回结 果

这里使用的一个基于单线程同步的简单服务模型,一般仅用于入门学习和测试!

(2)python跨语言调用测试

1.通过编译器编译user.thrift文件,生成python代码

thrift -gen py user.thrift

然后将生成的 python 代码 和 文件,放到新建的 python 项目中

2.python中使用thrift需要安装thrift模块

pip install thrift

3.创建python客户端程序

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from com.tuling import UserService
# Make socket
transport = TSocket.TSocket('localhost', 9090)
transport.setTimeout(600)
# Buffering is critical. Raw sockets are very slow
transport = TTransport.TBufferedTransport(transport)
# Wrap in a protocol
protocol = TBinaryProtocol.TBinaryProtocol(transport)
# Create a client to use the protocol encoder
client = UserService.Client(protocol)
# Connect!
transport.open()
result = client.getById(1)
print(result)

六.网络服务模型详解

Thrift提供的网络服务模型:单线程、多线程、事件驱动,从另一个角度划分为:阻塞服务模型、非阻塞 服务模型。

  • 阻塞服务模型:TSimpleServer、TThreadPoolServer
  • 非阻塞服务模型:TNonblockingServer、THsHaServer和TThreadedSelectorServer

1.TServer

TServer定义了静态内部类Args,Args继承自抽象类AbstractServerArgs。AbstractServerArgs采用了建造者模式,向TServer提供各种工厂:

2.TSimpleServer

TSimpleServer的工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和 处理一个socket连接,效率比较低。它主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。

(1)工作流程

(2)服务端使用

public class SimpleService {
    public static void main(String[] args) {
        try {
            TServerTransport serverTransport = new TServerSocket(9090);
            //获取processor
            UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
            //指定TBinaryProtocol
            TBinaryProtocol.Factory protocolFactory = new 
TBinaryProtocol.Factory();
            TSimpleServer.Args targs = new TSimpleServer.Args(serverTransport);
            targs.processor(processor);
            targs.protocolFactory(protocolFactory);
            //单线程服务模型,一般用于测试
            TServer server = new TSimpleServer(targs);
            System.out.println("Starting the simple server...");
            server.serve();
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}

(3)客户端使用

public class SimpleClient {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            // 使用阻塞io
            transport = new TSocket("localhost", 9090);
            //指定二进制编码格式
            TProtocol protocol = new TBinaryProtocol(transport);
            UserService.Client client = new UserService.Client(protocol);
            //建立连接
            transport.open();
            //发起rpc调用
            User result = client.getById(1);
            System.out.println("Result : " + result);
       } catch (TException e) {
            e.printStackTrace();
       } finally {
            if (null != transport) {
               transport.close();
           }
       }
   }
}

3.TThreadPoolServer

(1)工作流程

(2)服务端使用

TServerTransport serverTransport = new TServerSocket(9090);
//获取processor
UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
//指定TBinaryProtocol
TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
TThreadPoolServer.Args targs = new TThreadPoolServer.Args(serverTransport);
targs.processor(processor);
targs.protocolFactory(protocolFactory);
// 线程池服务模型 使用标准的阻塞式IO 预先创建一组线程处理请求
TServer server = new TThreadPoolServer(targs);
System.out.println("Starting ThreadPool server...");
server.serve();

ThreadPoolServer解决了TSimpleServer不支持并发和多连接的问题,引入了线程池。实现的模型是 One Thread Per Connection。

(3)优缺点

3.1 TThreadPoolServer模式的优点

拆分了监听线程(Accept Thread)和处理客户端连接的工作线程(Worker Thread),数据读取和业务处理 都交给线程池处理。因此在并发量较大时新连接也能够被及时接受。

线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池 及时处理,性能也非常高。

3.2 TThreadPoolServer模式的缺点

线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只 能排队等待。

默认线程池允许创建的最大线程数量为Integer.MAX_VALUE,可能会创建出大量线程,导致OOM(内存 溢出)

4.TNonblockingServer

TNonblockingServer模式也是单线程工作,但是采用NIO的模式,利用io多路复用模型处理socket就绪 事件,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送操作,对于 监听socket则产生一个新业务socket并将其注册到selector上。

注意:TNonblockingServer要求底层的传输通道必须使用TFramedTransport。

(1)工作流程

(2)服务端使用

public class NonblockingServer {
    public static void main(String[] args) {
        try {
            TNonblockingServerSocket serverTransport = new 
TNonblockingServerSocket(9090);
            //获取processor
            UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
            //指定TCompactProtocol
            TCompactProtocol.Factory protocolFactory = new 
TCompactProtocol.Factory();
            //指定TFramedTransport
            TFramedTransport.Factory tTransport = new 
TFramedTransport.Factory();
            TNonblockingServer.Args targs = new 
TNonblockingServer.Args(serverTransport);
            targs.processor(processor);
            targs.protocolFactory(protocolFactory);
            targs.transportFactory(tTransport);
            // 使用NIO服务端和客户端需要指定TFramedTransport数据传输的方式
            TServer server = new TNonblockingServer(targs);
            System.out.println("Starting Non-blocking server...");
            server.serve();
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}

(3)客户端使用

public class NonblockingClient {
    public static void main(String[] args) {
        TTransport transport = null;
        try {
            // 使用非阻塞
            transport = new TFramedTransport(new TSocket("localhost", 9090));
            //协议和服务端一致
            TProtocol protocol = new TCompactProtocol(transport);
            UserService.Client client = new UserService.Client(protocol);
            //建立连接
            transport.open();
            //发起rpc调用
            User result = client.getById(1);
            System.out.println("Result : " + result);
            
       } catch (TException e) {
            e.printStackTrace();
       } finally {
            if (null != transport) {
                transport.close();
           }
       }
   }
}

(4)优缺点

4.1 TNonblockingServer模式优点

相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,对 accept/read/write等IO事件进行监控和处理,同时监控多个socket的状态变化。

4.2 TNonblockingServer模式缺点

TNonblockingServer模式在业务处理上还是采用单线程顺序来完成。在业务处理比较复杂、耗时的时 候,例如某些接口函数需要读取数据库执行时间较长,会导致整个服务被阻塞住,此时该模式效率也不 高,因为多个调用请求任务依然是顺序一个接一个执行.

5.THsHaServer

鉴于TNonblockingServer的缺点,THsHaServer继承于TNonblockingServer,引入了线程池提高了任 务处理的并发能力。

注意:THsHaServer和TNonblockingServer一样,要求底层的传输通道必须使用 TFramedTransport。

(1)工作流程

(2)服务端使用

public class HsHaServer {
    public static void main(String[] args) {
        try {
            TNonblockingServerSocket serverTransport = new 
TNonblockingServerSocket(9090);
            //获取processor
            UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
            //指定TCompactProtocol
            TCompactProtocol.Factory protocolFactory = new 
TCompactProtocol.Factory();
            //指定TFramedTransport
            TFramedTransport.Factory tTransport = new 
TFramedTransport.Factory();
            THsHaServer.Args targs = new THsHaServer.Args(serverTransport);
            targs.processor(processor);
            targs.protocolFactory(protocolFactory);
            targs.transportFactory(tTransport);
            // 使用NIO服务端和客户端需要指定TFramedTransport数据传输的方式
            TServer server = new THsHaServer(targs);
            System.out.println("Starting HsHa server...");
            server.serve();
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}

(3)优缺点

3.1 THsHaServer的优点

THsHaServer与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交 由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。

3.2 THsHaServer的缺点

主线程仍然需要完成所有socket的监听接收、数据读取和数据写入操作。当并发请求数较大时,且发送 数据量较多时,监听socket上新连接请求不能被及时接受。

6.TThreadedSelectorServer

TThreadedSelectorServer是对THsHaServer的一种扩充,它将selector中的读写IO事件(read/write)从 主线程中分离出来。同时引入worker工作线程池。

TThreadedSelectorServer模式是目前Thrift提供的最高级的线程服务模型,它内部有如果几个部分构 成:

  1. 一个AcceptThread专门用于处理监听socket上的新连接。
  2. 若干个SelectorThread专门用于处理业务socket的网络I/O读写操作,所有网络数据的读写均是有这些线程来完成。
  3. 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。
  4. 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求数据读取之后,交给ExecutorService线程池中的线程完成此次调用的具体执行。主要用于处理每个rpc请求的handler回调处理。

(1)工作流程

(2)服务端使用

public class ThreadedSelectorServer {
    public static void main(String[] args) {
        try {
            TNonblockingServerSocket serverTransport = new 
TNonblockingServerSocket(9090);
            //获取processor
            UserService.Processor processor = new UserService.Processor(new 
UserServiceImpl());
            //指定TCompactProtocol
            TCompactProtocol.Factory protocolFactory = new 
TCompactProtocol.Factory();
            //指定TFramedTransport
            TFramedTransport.Factory tTransport = new 
TFramedTransport.Factory();
            TThreadedSelectorServer.Args targs = new 
TThreadedSelectorServer.Args(serverTransport);
            targs.processor(processor);
            targs.protocolFactory(protocolFactory);
            targs.transportFactory(tTransport);
            // NIO 引入线程池处理业务
            TServer server = new TThreadedSelectorServer(targs);
            System.out.println("Starting ThreadedSelector server...");
            server.serve();
       } catch (Exception e) {
            e.printStackTrace();
       }
   }
}

(3)客户端使用

public class ThreadedSelectorClient {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                handle();
           }).start();
       }
   }
    public static void handle(){
        TTransport transport = null;
        try {
            // 设置传输通道,对于NIO需要使用TFramedTransport(用于将数据分块发送)
            transport = new TFramedTransport(new TSocket("localhost", 9090));
            //协议和服务端一致
            TProtocol protocol = new TCompactProtocol(transport);
            UserService.Client client = new UserService.Client(protocol);
            transport.open();
            //RPC调用
            User result = client.getById(1);
            System.out.println("Result =: " + result);
            transport.close();
       } catch (Exception e) {
            e.printStackTrace();
       } finally {
            // 关闭传输通道
            transport.close();
       }
   }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值