thrift java_Thrift(Java版)到网络编程(一)

本文旨在通过Thrift Java实例介绍网络编程基础知识,适合网络编程初学者和Thrift新手。文章讨论了Thrift的架构、关键组件如TTransport、TProtocol和TProcessor,并介绍了TSimpleServer及单线程同步模式。通过示例代码展示了Thrift服务器和客户端的创建,强调了连接超时和backlog参数的重要性,并通过Wireshark抓包分析了Thrift协议的工作流程。
摘要由CSDN通过智能技术生成

Thrift(Java版)到网络编程(一)

写在前面

在面试和平时交流过程中,发现很多同学对于网络编程这块关注度较低(做业务开发的同学居多)。作者认为当今互联网界做工程,是不可能饶过这一块的。当然本人对这块理解也很浅。这也是写本文的动机和初衷。本文试图通过基于Thrift的一些实际案例,把网络编程过程中遇到的常见的问题和知识点总结一下。文中一些链接,同学们也可以多去点一点,大部分都是写得不错的。

选择Thrift作为主要的参考学习对象,一是Thrift自身已经很成熟了,对基本的网络通信模型支持都不错;二是thrift相对来说是一个非常轻量级的框架,相信对于入门级同学有较大的学习价值。

希望本文对于以下同学有些参考价值:1)网络编程入门玩家;2)Thrift入门玩家(Java党最好)

一、 Thrift简介

本文不是《Thrift 30min入门到精通》之类的。所以不会对Thrift做过多的介绍。大家可以到官网或者参考Apache THRIFT: A Much Needed Tutorial(推荐)。

个人觉得Thrift最大的优势有:高性能(跟Protocol

Buffer近似),可压缩(相对纯文本协议),多语言支持(相对Dubbo),配套RPC框架(不仅仅是IDL支持)等。当然从个人喜好角度,更喜欢其轻量化设计和强大的扩展能力。Thrift目前在我厂各种微服务框架中得到了较为广泛的应用。

有些基本概念还是要简单讲一下(本文源码是基于V0.9.3)。首先看一下Thrift整体架构图(协议栈)。如图1所示,带颜色的部分是Thrift框架范围。结合图2网络分层模型我们可以确定:Thrift是一个应用层协议。

图1. Thrift整体架构图

图2. 网络分层模型

你会发现分层的思想无处不在。不仅是网络,如组织架构,建筑,软件架构等都离不开这个基本思想。所以该分层时候千万不要排斥。分层的好处是每一层各执其职,术有专攻。当然对下层服务能力和上层需求要有足够的了解,才能把本职工作做好。本文更多是在应用层徘徊,socket往下基本不涉及。如果有机会作者也希望能够往下再探一探。

下面大致讲一下Thrift一些基本概念。

TTransport

TTransport是对下层传输的一个抽象。只提供open/close,read/write, flush等少数接口。目前实现了Socket、HTTP、Zlib等多重传输方式。

图3. TTransport

在server端Thrift同时提供了TServerTransport接口。主要提供了Listen, accept, close等接口。实现有TServerSocket和TNonblockingServerSocket。一个用于阻塞通信,一个用于非阻塞通信。

图4. TServerTransport

说到这里,已经很难回避一个东西了,那就是socket。相信绝大部分同学对这个概念已经非常熟悉了。当然想把这块东西都说明白,估计又得再写一篇长博文了。还好之前有人已经努力过了,这里贴个参考链接,感兴趣的同学可以去看看。

Socket是一种技术规范,最早用于Unix系统。Socket

API是由操作系统提供的编程接口。Socket位于传输层和应用层之间。Java提供了对Socket JDK层面的支持。具体可以参考java.net.Socket和java.net.ServerSocket。面向连接的Socket(TCP)通信主要流程请参考图5(来源),这里就不再展开了。

图5. TCP Socket通信过程

大家可以看看TSocket和TServerSocket源码。他们内部就是封装了Socket和ServerSocket,同时实现了TTransport和TServerSocket接口。

TProtocol

TProtocol主要负责序列化和反序列化工作。估计这个就不用过多的罗嗦了。目前Thrfit主要支持binary,compact,JSON等方式,如图6。其中MultiplexedXX是为了支持一个进程支持多个TServer而引入的。

图6. TProtocol接口

细心的同学会发现图中没有列出接口。不是不想,而是太多了。TProtocol提供了对于thrift支持的所有数据结构的R/W操作。那有哪些数据结构呢。可以参考TType定义。其中最特殊的是STOP,这其实是个占位符。传输过程中一般都是用‘0’表示的,表示分段结束。

图7. TType

Protocol其实就是业务层的一个协议,用于通信双方有效的交换信息。从TLV,PB,JSON都是在做这个事情。我们自然也可以自己约定个协议。只要C/S两端能识别就没问题。Thrift在这方面扩展性也是很强大的。必要的时候大家可以尝试下。

TProcessor

TProcessor负责从一个input

protocol读取一份数据,将数据转发给相应的handler,然后把结果写入到output

protocol中。一个Service对应的Processor是通过thrift gen自动生成的。而这个handler就是我们需要实现的部分。

TProcessor接口简单的不能再简单了(图8)。其实现可以参考TBaseProcessor.process()。因为Thrift是一个RPC框架,所以真正process过程都是交给具体接口来完成的。具体可以参考ProcessFunction.process().

图8. TProcessor接口

TServer

TServer负责把上述processor,protocol,transport等等组合在一起,协同实现一个Thrift服务器。主要实现类如图9所示。因为这个是后续讨论重点。也希望有时间能把这些Server一一都过一下。这里先不展开。

图9. TServer

Thrift IDL

IDL(Interface Description Language)顾名思义,主要是用于描述thrift接口的。根据IDL文件,我们可以通过thrift --gen生成不同语言的版本的Processor等。下面是本文会用到的IDL文件:

//hello world service

namespace java com.itegel

service Hello {

string sayHello(1:string name, 3:i32 age);

}

根据此文件生成的类如图10。其中 Processor和AsyncProcessor,就是上面说的TProcessor的实现,一个用于同步模式,一个用于异步模式。同步异步这个话题后面会单独再讨论。Client是对客户端逻辑的一个封装。Clinet继承自TServiceClient,实现了IFace接口。具体代码还请各位同学自行看一下,不多。本文不想把代码一行一行都讲一遍。但是每个例子里都希望能把一些核心代码拿出来share一下。

图10. 自动生成Processor

二、 TSimpleServer及单线程同步模式

写了这么久,终于可以切入主题了(此处小兴奋)。

TSimpleServer是一个单线程的server。所以线上很少能用得到。但是作为教学Server再好不过了。

服务端代码

Hello.Iface handler;

Hello.Processor processor;

handler = new

HelloHandler();

processor = new

Hello.Processor<>(handler);

TServerSocket.ServerSocketTransportArgs transportArgs

= new TServerSocket.ServerSocketTransportArgs()

.backlog(backlog)

.port(port)

.clientTimeout(timeout);

TServerTransport serverTransport = new TServerSocket(transportArgs);

TSimpleServer.Args args = new TSimpleServer.Args(serverTransport)

.processor(helloProcessor)

.protocolFactory(new

TBinaryProtocol.Factory());

TServer tServer = new TSimpleServer(args);

tServer.serve();

其中Processor是自动生成的。

Handler是我们实现的代码。本文只是简单返回个字符串:

public class HelloHandler implements

Hello.Iface {

@Override

public String

sayHello(String name, int age)

{

System.out.print(new

Date() + ": " + name + ",

" + age + "\n");

try {

if (name.equals("Itegel"))

{

Thread.sleep(300000);

}

} catch (InterruptedException

e) {

e.printStackTrace();

}

return "Hello

" + name + ", you are " +

age + ".";

}

}

这里sleep部分先忽略,那是为了演示用的 。

服务端transport用了TServerSocket,protocol用了TBinaryProtocol。然后把这两个逐层传递给了TSimpleServer。

我们首先看一下TServerSocket都干了啥。看下构造器:

public TServerSocket(ServerSocketTransportArgs

args) throws TTransportException {

clientTimeout_ =

args.clientTimeout;

if (args.serverSocket

!= null) {

this.serverSocket_

= args.serverSocket;

return;

}

try {

// Make server socket

serverSocket_ = new

ServerSocket();

// Prevent 2MSL delay

problem on server restarts

serverSocket_.setReuseAddress(true);

// Bind to listening port

serverSocket_.bind(args.bindAddr, args.backlog);

} catch

(IOException ioe) {

close();

throw new TTransportException("Could

not create ServerSocket on address " + args.bindAddr.toString()

+ ".", ioe);

}

}

因为参数里,我们没带下来serverSocket,所以会new

ServerSocket()。好吧,这事儿基本就这样了,毫无花哨的东东。这里有两个参数非常重要。

1.timeout:

这里只有一个值,但实际上socket编程里有两个timeout。先看看最终调用的setTimeout方法吧。

public void setTimeout(int

timeout) {

this.setConnectTimeout(timeout);

this.setSocketTimeout(timeout);

}

所以一个叫connectTimeout,一个叫socketTimeout。

ConnectTimeout顾名思义是客户端过来建立连接过程的耗时。也就是三次握手开始到成功建立连接的时间。如果超过这个时间还是未能建立连接,服务端会主动rst当前半连接。

SocketTimeout这里实际上是socket里的readtimeout。注意一点,read timeout是针对读取单个包的timeout。数据分包场景下不等于整个结果获取的时间。

Timeout为啥会这么重要呢?我们经常提雪崩,大部分都是因为下游某个服务故障加上上游不断重试,导致本服务资源耗尽所致。这里一个很重要的资源是连接。如果超时设置不合理,就会导致当下游故障时,不能及时释放连接资源,导致不断建立新连接。故障服务相关连接把整个连接池都占用了,其他可以正常响应的请求就无法建立连接。

这里还提到了重试次数设置。这个也很重要。一般建议是如果上游有重试机制,中间服务要谨慎加重试或直接不重试,以便有效的保护下游。

本服务加些必要的限流措施也是很有必要的。这样在一些极端情况下不至于所有服务都挂掉。

2.backlog:

Linux内核中有两个队列,一个叫syns

queue, 一个叫accept queue。服务端收到客户端syn消息后,会把相应的请求放到syns(SYN_SENT?)队列(半连接状态)。而连接established的连接会放到accept queue中等待accept()消费。Backlog长度就是syns队列的长度。当该队列满之后,客户端在想连接就会直接拒绝。

Thrift里backlog被默认设置为50。那到底设置多少合适呢?太小了,那么对于一些尖峰流量就没有容忍度了。太大会有啥问题?关键要看处理能力。举个例子,上游超时设置为1s,本服务处理能力是100QPS。那么把backlog设置为大于100会有个问题:当发生挤压,处理队尾连接是很可能已经被客户端断开了。FIFO顺序执行可能会有更悲剧的事情发生。当然还有一些资源占用方面的考虑。参考下小节。

public static abstract class AbstractServerTransportArgs

AbstractServerTransportArgs>

{

int backlog

= 0; // A value of 0 means the

default value will be used (currently set at 50)

int clientTimeout

= 0;

}

3.连接数:

我们一般是不用特别设置这个值的,那他受哪些限制呢?第一个就是上面说到的backlog会限制半连接的个数。一般工作线程大小限制established状态的连接个数。更大的限制是系统内存(包含JVM配置)和系统句柄数限制。Linux下:用户级别限制可以通过ulimit

–n查看;系统级别限制通过以下命令查看:sysctl -a|grep file-max.

关于protocol,这里先不占开,后面抓包章节再看。

客户端代码

客户端代码更简单:

TSocket tSocket = new TSocket(hostname, port);

tSocket.setTimeout(timeout);

TTransport tTransport = tSocket;

TProtocol tProtocol = new TBinaryProtocol(tTransport);

Hello.Client client = new Hello.Client(tProtocol);

tTransport.open();

System.out.println("RESPONSE: " + client.sayHello(name, age));

tTransport.close();

这里client代码是自动生成的。Transport用的是socket,protocol用的是TBinaryProtocol(需要跟服务端一致)。看一下transport.open:

public void open() throws TTransportException {

……

if (socket_ == null) {

initSocket();

}

try {

socket_.connect(new InetSocketAddress(host_, port_), connectTimeout_);

inputStream_ = new BufferedInputStream(socket_.getInputStream(), 1024);

outputStream_ = new BufferedOutputStream(socket_.getOutputStream(), 1024);

} catch (IOException iox) {

close();

throw new TTransportException(TTransportException.NOT_OPEN, iox);

}

}

可以看到,底层用的是http://java.net的socket.connect()方法。除了host和port,还有个timeout参数,这里就不再展开了。

作者贴这两段代码,不是想给大家一个完整的tutorial。官网和google,有一堆tutorial。以上小sample主要是为了下面讨论做铺垫。当然完全没接触过thrift的同学相信看完之后也能大概知道在做什么。

抓个包

当年烧单板时候养成一个习惯:所有涉及网络的事情,首先都希望抓个包看看。在RPC环境下,即使不抓包,把输入输出打出来看看还是必要的。

抓包当然是需要工具的。这里推荐两个工具。第一个必然是tcpdump了。估计也不用多介绍。另一个神器是WireShark。Tcpdump大家自行谷哥吧。实际应用中,可以通过tcpdump抓包,然后拿用wireshark分析。本文中因为作者只是在自己pc上做个demo,所以直接用wireshark抓的包。

针对以上sample,我们抓个正常交互的报文如下:

图11. SimpleServer + BinaryProtocol 抓包

作者惊喜的发现,wireshark竟然支持thrift协议(仅限于binary protocol,其他暂时没看到相应插件等支持)。

一次完整的RPC过程,总共发了10条消息。前三条就是传说中的三次握手。最后三条对应关闭连接的四次握手消息(第二条把ACK和FIN在同一条消息里发送)。这部分大家请参考下节。

这里核心是蓝色部分,客户端发送一个消息,服务端返回ACK。服务端响应了一条消息,客户端也发送了相应的ACK。

在CALL sayHello时候到底发送了什么呢?请看下面截图:

图12. sayHello RPC报文

从报文结构能够看到二层以太网协议包着ip协议->tcp->thrift,很清晰。因为篇幅没法一个一个点开给各位客官看了。有兴趣可以自行抓包看看。以太网已下,我就看不懂了...

回到Thrift协议,我们通过抓包去,可以帮助我们了解一下TProtocol是怎么work的。

首先客户端的rpc调用,实际调用是Client下的send_sayHello方法。如下:

public void send_sayHello(String

name, int age) throws org.apache.thrift.TException

{

sayHello_args args = new

sayHello_args();

args.setName(name);

args.setAge(age);

sendBase("sayHello", args);

}

而sendBase实际上是调用protocol的相应方法,组装报文:

private void sendBase(String

methodName, TBase,?>

args, byte type) throws TException

{

oprot_.writeMessageBegin(new

TMessage(methodName, type, ++seqid_));

args.write(oprot_);

oprot_.writeMessageEnd();

oprot_.getTransport().flush();

}

DATA部分,我们可以参考下

sayHello_argsStandardScheme.write()方法实现:

public void write(org.apache.thrift.protocol.TProtocol

oprot, sayHello_args struct) throws

org.apache.thrift.TException {

struct.validate();

oprot.writeStructBegin(STRUCT_DESC);

if (struct.name

!= null) {

oprot.writeFieldBegin(NAME_FIELD_DESC);

oprot.writeString(struct.name);

oprot.writeFieldEnd();

}

oprot.writeFieldBegin(AGE_FIELD_DESC);

oprot.writeI32(struct.age);

oprot.writeFieldEnd();

oprot.writeFieldStop();

oprot.writeStructEnd();

}

这里的oprot就是out protocol的实例。我们可以看出,TProtocol就是在负责怎么把具体的字段(包括自身协议头和协议body)转换成实际传输的二进制数据。一般我们会把这个过程叫做序列化。

而Read的过程是反过来的(反序列化)。当然Thrift支持in和out的protocol可以设置成不同的协议。

有兴趣的同学结合上面抓包结果和thrift代码对比多看看。相信很快你就会有自行定义个协议的冲动。当然多种数据类型的支持是这里的关键点。这个过程非常繁杂。这可能也是为什么thrift没有支持嵌套的struct和enum等的原因吧。

TCP连接

上面提到了很多连接相关东西。本节简单说一下。首先看一TCP连接状态图,参考博客:

图13.1 TCP连接状态图

图13.2 TCP连接状态图 VS建立和关闭连接

图13.1大家应该很熟悉了,本科计算机网络课本上应该就有。个人觉得图13.2更容易记忆。详细的讨论请大家点上面“来源”连接。作者写得比较细。

通过netstat –nat|grep ‘port’可以看到该端口相关连接状态。 这里就不再罗嗦了。

有几个状态,大家可能要多注意。

1.服务端大量CLOSE_WAIT:

这个可能是一个比较常见的情况。从图13可以看出,当客户端主动关闭,但是服务端没有发送FIN,那么就会处在一个CLOSE_WAIT状态。而这个是怎么产生的呢?实际中,可能原因很多。一种常见的情况是,因为服务端处理速度跟不上,导致客户端timeout,主动close了。这时候因为服务器还在处理业务逻辑,没法及时响应close请求,从而连接处于CLOSE_WAIT状态。我们可以简单模拟,比如client设置timeout为200ms,服务端处理线程sleep个1s,就会出现服务端CLOSE_WAIT。此时客户端一般处于FIN_WAIT_2:

jstack看下,服务端刚启动:

服务线程进入sleep:

如果出现CLOSE_WAIT,可以先排查有没有长耗时的业务,有没有业务死循环,死锁等。当然很多时候事情并没有如此简单,具体问题具体分析吧。Jstack总是要会看的。当然线上一般不会给你这个机会。大家有问题时候首先想到的就是先重启恢复服务。所以我们一般会定时把jstack信息打到日志文件里,已备后续分析。当然线上有问题,留个环境这种意识最好也有。比如有几十台机器,留一台有问题的机器,一般也不会影响业务的。

2.客户端大量TIME_WAIT:

TIME_WAIT实际上并不可怕,这个是TCP协议本身决定的。四次握手时,主动关闭方,发送ACK后,自己进入TIME_WAIT状态,保持2MSL(Maximum Segment Lifetime)时间。目的是当被动关闭方发送FIN后因超时,再次发起FIN请求。这样主动发起方,还有机会再发送一遍ACK。

但是实际中如果出现大量TIME_WAIT,很可能是网络或者服务端原因造成的。可以考虑通过setReuseAddress和setSoLinger设置来达到忽略或缩短这个时间的目的。具体情况还需要具体分析,更多内容可以继续谷哥。

线程状态

细心的朋友会发现,jstack里可以看到有个线程状态。现在还不想发散,后面有机会再看看。附上一幅图(TIMED_WAITING就是下图sleeping):

图14. Java线程状态图

感觉这张图还是很清晰了,就不展开了。多线程问题,作者希望下一篇中能够更多的展开些。大家可以先参考

我们用jstack定位问题时候,通常可能需要多执行几次,看有没有线程夯住等情况。

这里有个各种常用工具组合介绍,很全,给原作者点赞!

写在后面

写了不少了,发现这块如果真想说清楚(当然还要先搞清楚),还是需要很大篇幅的。最近懒惰综合征又犯了,所以第一篇先写到这里。

下一篇希望能把多线程、各种pool、阻塞非阻塞这些能尽量说明白。至于NIO这些估计很可能得下下篇了。

P.S.微信"搜一搜","看一看"亮瞎眼。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值