Swift的網絡框架采用了jboss的netty庫,沒有采用后期的netty標準庫,它實現了純異步的Thrift服務器和客戶端框架,本篇主要介紹服務端的源碼。
1、以一個簡單的例子開始
我們以一個簡單的服務器端例子開始:
public static void main(String[] args) {
//SwiftScribe scribeService = new SwiftScribe();
ExecutorService taskWorkerExecutor;
ThriftServer server;
CountDownLatch latch;
ExecutorService bossExecutor;
ExecutorService ioWorkerExecutor;
ThriftServiceProcessor processor = new ThriftServiceProcessor(
new ThriftCodecManager(),
ImmutableList.<ThriftEventHandler>of(),
new SwiftScribe()
);
taskWorkerExecutor = newFixedThreadPool(1);
ThriftServerDef serverDef = ThriftServerDef.newBuilder()
.listen(8899)
.withProcessor(processor)
.using(taskWorkerExecutor)
.build();
bossExecutor = newCachedThreadPool();
ioWorkerExecutor = newCachedThreadPool();
NettyServerConfig serverConfig = NettyServerConfig.newBuilder()
.setBossThreadExecutor(bossExecutor)
.setWorkerThreadExecutor(ioWorkerExecutor)
.build();
server = new ThriftServer(serverConfig, serverDef) ;
server.start();
}
我們首先封裝了ThriftServerDef和NettyServerConfig,由這兩個參數來構造一個ThriftServer對象,然后啟動Thrift服務。對外就可以提供端口號為8899的高并發的Thrift協議服務能力。
該服務對外提供的服務協議在SwiftScribe中,SwfitScribe的框架類是通過scribe.thrift文件生成,
namespace java com.facebook.swift.service.scribe
enum ResultCode
{
OK,
TRY_LATER
}
struct LogEntry
{
1: string category,
2: string message
}
service scribe
{
ResultCode Log(1: list<LogEntry> messages);
}
生成框架的命令
java -jar .\swift-generator-cli-0.19.2-standalone.jar scribe.thrift -out ..\java
2、類說明
- ThriftCodecManager類
ThriftCodecManager包含了所有已知的ThriftCodec的索引,并且可以按需為未知類型創建編解碼器。既然編解碼器的創建耗費資源, 所以每個類只有一個實例會被創建,并被ThriftCodecManager類管理起來。
目前包括的類型有:
public enum ThriftProtocolType
{
UNKNOWN((byte) 0),
BOOL((byte) 2),
BYTE((byte) 3),
DOUBLE((byte) 4),
I16((byte) 6),
I32((byte) 8),
I64((byte) 10),
STRING((byte) 11),
STRUCT((byte) 12),
MAP((byte) 13),
SET((byte) 14),
LIST((byte) 15),
ENUM((byte) 8), // same as I32 type
BINARY((byte) 11); // same as STRING type
private final byte type;
}
在ThriftCodecManager中,通過typeCodecs管理所有數據類型的協議編解碼功能。
ThriftCodecManager通過Guawa庫中的LoadingCache來緩存每種Thrift類型的編解碼器,LoadingCache的原理這里大概說一下,如果能從緩存中找到到數據,那么就直接返回,否則會調用構造器中傳入的Callable對象的call方法,來根據key值計算出value,并緩存起來。然后才返回數據。
addBuiltinCodec(new BooleanThriftCodec());
addBuiltinCodec(new ByteThriftCodec());
addBuiltinCodec(new ShortThriftCodec());
addBuiltinCodec(new IntegerThriftCodec());
addBuiltinCodec(new LongThriftCodec());
addBuiltinCodec(new DoubleThriftCodec());
addBuiltinCodec(new ByteBufferThriftCodec());
addBuiltinCodec(new StringThriftCodec());
addBuiltinCodec(new VoidThriftCodec());
addBuiltinCodec(new BooleanArrayThriftCodec());
addBuiltinCodec(new ShortArrayThriftCodec());
addBuiltinCodec(new IntArrayThriftCodec());
addBuiltinCodec(new LongArrayThriftCodec());
addBuiltinCodec(new DoubleArrayThriftCodec());
以上是內在的類型以及相應的編解碼器,所以內在的類型是不需要計算,對于自定義的復雜的類型,就需要計算了,如:enum, set, map, struct等,由于初始化的時候,這些類型都是沒有放入編解碼器緩存的,所以第一次查詢肯定查詢不到。
這時候就需要計算:
typeCodecs = CacheBuilder.newBuilder().build(new CacheLoader<ThriftType, ThriftCodec<?>>()
{
public ThriftCodec<?> load(ThriftType type)
throws Exception
{
try {
// When we need to load a codec for a type the first time, we push it on the
// thread-local stack before starting the load, and pop it off afterwards,
// so that we can detect recursive loads.
stack.get().push(type);
switch (type.getProtocolType()) {
case STRUCT: {
return factory.generateThriftTypeCodec(ThriftCodecManager.this, type.getStructMetadata());
}
case MAP: {
return new MapThriftCodec<>(type, getElementCodec(type.getKeyTypeReference()), getElementCodec(type.getValueTypeReference()));
}
case SET: {
return new SetThriftCodec<>(type, getElementCodec(type.getValueTypeReference()));
}
case LIST: {
return new ListThriftCodec<>(type, getElementCodec(type.getValueTypeReference()));
}
case ENUM: {
return new EnumThriftCodec<>(type);
}
default:
if (type.isCoerced()) {
ThriftCodec<?> codec = getCodec(type.getUncoercedType());
TypeCoercion coercion = catalog.getDefaultCoercion(type.getJavaType());
return new CoercionThriftCodec<>(codec, coercion);
}
throw new IllegalArgumentException("Unsupported Thrift type " + type);
}
}
finally {
ThriftType top = stack.get().pop();
checkState(type.equals(top),
"ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s",
type,
top);
}
}
});
從ThriftCodecManager中獲取編解碼器主要有兩種方法:getCodec和getCodecIfpresent,前者不存在會調用前面的builder方法構建,后者不存在返回null。
ThriftCodecManager包含一個 ThriftCatalog対像,ThriftCatalog包含了所有已知的structs, enums,和類型隱含轉換器。因為元數據抽取是非常消耗資源的,所有catalog是單實例。
ThriftCatalog的構造函數如下,它采用DefaultJavaCorecions類來加載隱式類型轉換器。
@VisibleForTesting
public ThriftCatalog(Monitor monitor)
{
this.monitor = monitor;
addDefaultCoercions(DefaultJavaCoercions.class);
}
可以用來轉換一些原始數據類型:int,long,double,float,byte,byte[]
在DefaultJavaCoercions類中,包含了一些方法,用來轉換原始類型和對應的封裝類型,這些方法都有注解,注解有兩種類型:FromThrift和ToThrift。
在addDefaultCoercions中,根據不同的注解,會把相應的方法,放到
Map<ThriftType, Method> toThriftCoercions = new HashMap<>();
Map<ThriftType, Method> fromThriftCoercions = new HashMap<>();
兩個結構體中。最后把每種類型的的FromThrift方法和ToThrift方法封裝成一個類型轉換器,存放到隱式轉換映射中。
Map<Type, TypeCoercion> coercions = new HashMap<>();
for (Map.Entry<ThriftType, Method> entry : toThriftCoercions.entrySet()) {
ThriftType type = entry.getKey();
Method toThriftMethod = entry.getValue();
Method fromThriftMethod = fromThriftCoercions.get(type);
TypeCoercion coercion = new TypeCoercion(type, toThriftMethod, fromThriftMethod);
coercions.put(type.getJavaType(), coercion);
}
this.coercions.putAll(coercions);
- ThriftServiceProcessor類
用來封裝一個Thrift服務。
private final Map<String, ThriftMethodProcessor> methods;
private final List<ThriftEventHandler> eventHandlers;
methods存儲每個方法的處理器對象,而eventHandlers則是用于給外部監控用,類的定義如下所示:
public abstract class ThriftEventHandler
{
public Object getContext(String methodName, RequestContext requestContext)
{
return null;
}
public void preRead(Object context, String methodName) {}
public void postRead(Object context, String methodName, Object[] args) {}
public void preWrite(Object context, String methodName, Object result) {}
public void preWriteException(Object context, String methodName, Throwable t) {}
public void postWrite(Object context, String methodName, Object result) {}
public void postWriteException(Object context, String methodName, Throwable t) {}
public void declaredUserException(Object o, String methodName, Throwable t, ThriftCodec<?> exceptionCodec) {}
public void undeclaredUserException(Object o, String methodName, Throwable t) {}
public void done(Object context, String methodName) {}
}
這里說明一下process函數
1)讀取Thrift的消息開始信息
2)獲取Thrift方法名和SequenceId(這兩個信息都在開始信息里面,還包括信息類型是CALL還是ONEWAY)
3)根據Thrift方法名,獲取對應的處理器:ThriftMethodProcessor
4)生成ContextChain對象
5)調用方法處理器進行處理。
6)添加回調。這里的回調是基于google的Futures來做的。
但是這個回調感覺對于那些需要直接返回的函數,會增加額外的開銷,因為回調的處理,需要放到另外一個執行器中處理(線程池),然后才會返回。
- ThriftServiceMetadata類
成員說明:
private final String name; // 服務名,通過@ThriftService("名字")注解讀取出來的名字。
private final Map<String, ThriftMethodMetadata> methods; // 類所有的Thrift方法,包括父類和所有接口的
Thrift方法
private final Map<String, ThriftMethodMetadata> declaredMethods; // 類本身自己定義的Thrift方法
private final ImmutableList<ThriftServiceMetadata> parentServices; // 父類或者接口定義的元數據
private final ImmutableList<String> documentation; // 通過ThriftDocumentation注解的文檔
- ThriftMethodMetadata類
成員說明:
private final String name; // 方法名
private final String qualifiedName; // 服務名.方法名
private final ThriftType returnType;
private final List<ThriftFieldMetadata> parameters;
private final Method method;
private final ImmutableMap<Short, ThriftType> exceptions;
private final ImmutableList<String> documentation;
private final boolean oneway;
類的構造過程:(以例子中的Scribe.log為例進行說明)
1)獲取參數編號,一般為index+1
2)獲取參數名稱
3)根據返回值和參數的Java類型獲取(或者按需生成)對應的ThriftType【ThriftType thriftType = catalog.getThriftType(parameterType);】
獲取的邏輯:如果緩存中有,則從緩存中獲取,否則調用 buildThriftType-->buildThriftTypeInternal來生成。
buildThriftTypeInternal的邏輯大致為:
i)獲取類型的裸類型Class<?> rawType = TypeToken.of(javaType).getRawType();
關于裸類型與元素類型:
舉個栗子:java.util.List<String>的裸類型是java.util.List,元素類型是:String
ii)如果裸類型是原始類型返回對應的ThriftType,如:DOUBLE,STRING,I32, I64等。
iii)如果是ByteBuffer返回BINARY
iv)如果是Enum生成相應的ThriftEnumMetadata,并據此創建對應的Enum的Thrift類型,具體邏輯略
v)如果是數組,則生成具體類型的數組thrift類型,具體邏輯略
vi)如果是Map,獲取key和value的具體類型,生成相應的thrifttype,在根據這兩thrifttype生成map Thrifttype。
vii)如果是void,返回VOID
viii)如果是結構
ix)如果是Future類型(牛逼,THrift居然有這種東東)
x)其他情況,這時候,將采用隱式轉換類型,來獲取相應的ThriftType。
關于隱式轉換前面已經提到了,系統在初始化時,把DefaultJavaCoercions類作為隱式轉換的初始化對象。主要是一些java原始類型與其包裝類之間的相互轉換。另外在ThriftType中有對應的
這里細節先就先不提了,以后另外專門分一個章節來講解。
4)構建異常信息
5)單方向信息
- ThriftMethodProcessor類
該類負責執行一個客戶端的Thrift請求,它封裝了該方法的元數據和編解碼器相關信息。
這里先看看他的process方法的主要流程:
1)讀取參數信息
2)讀取消息結束標志
【注意:讀取消息開始,以及讀取消息名,都在前面ThriftServiceProcessor類的process函數中處理過了】
3) 調用方法 : Object response = method.invoke(service, args); 這個很簡單。
在這里,它對于立即返回和Future返回值的函數做了統一處理。
try {
Object response = method.invoke(service, args);
if (response instanceof ListenableFuture) {
return (ListenableFuture<?>) response;
}
return Futures.immediateFuture(response);
}
catch (IllegalAccessException | IllegalArgumentException e) {
// These really should never happen, since the method metadata should have prevented it
return Futures.immediateFailedFuture(e);
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause != null) {
return Futures.immediateFailedFuture(cause);
}
return Futures.immediateFailedFuture(e);
}
4)返回數據
注意,這里有兩塊:成功的和失敗的。
成功的情況下,會返回一個
- ThriftServer類
負責服務器的相關元素的封裝和服務器的啟動,它主要依賴于NettyServerTransport類來完成服務器的相關功能。
1、首先看看編解碼工廠類
有兩塊:
1)、DEFAULT_FRAME_CODEC_FACTORIES 存放了兩個基于Framed的編解碼:buffered和framed
2)、DEFAULT_PROTOCOL_FACTORIES 存放了兩種協議解析binary和compact,
binary:簡單的二進制編碼
compact:以一種更加緊湊的編碼方式編碼,
注意:Thrift也可以使用Json來編碼,不過Swfit不支持
- NettyServerTransport類
這是一個核心的通道類,用于解析framed 的thrift消息,分到給指定的TProcessor,然后把返回的消息打包返回都Thrift棧。
1、啟動服務
public void start(ServerChannelFactory serverChannelFactory)
{
bootstrap = new ServerBootstrap(serverChannelFactory);
bootstrap.setOptions(nettyServerConfig.getBootstrapOptions());
bootstrap.setPipelineFactory(pipelineFactory);
serverChannel = bootstrap.bind(new InetSocketAddress(requestedPort));
InetSocketAddress actualSocket = (InetSocketAddress) serverChannel.getLocalAddress();
actualPort = actualSocket.getPort();
Preconditions.checkState(actualPort != 0 && (actualPort == requestedPort || requestedPort == 0));
log.info("started transport %s:%s", def.getName(), actualPort);
}
2、協議棧
this.pipelineFactory = new ChannelPipelineFactory()
{
@Override
public ChannelPipeline getPipeline()
throws Exception
{
ChannelPipeline cp = Channels.pipeline();
TProtocolFactory inputProtocolFactory = def.getDuplexProtocolFactory().getInputProtocolFactory();
NiftySecurityHandlers securityHandlers = def.getSecurityFactory().getSecurityHandlers(def, nettyServerConfig);
cp.addLast("connectionContext", new ConnectionContextHandler());
cp.addLast("connectionLimiter", connectionLimiter);
cp.addLast(ChannelStatistics.NAME, channelStatistics);
cp.addLast("encryptionHandler", securityHandlers.getEncryptionHandler());
cp.addLast("frameCodec", def.getThriftFrameCodecFactory().create(def.getMaxFrameSize(),
inputProtocolFactory));
if (def.getClientIdleTimeout() != null) {
// Add handlers to detect idle client connections and disconnect them
cp.addLast("idleTimeoutHandler", new IdleStateHandler(nettyServerConfig.getTimer(),
def.getClientIdleTimeout().toMillis(),
NO_WRITER_IDLE_TIMEOUT,
NO_ALL_IDLE_TIMEOUT,
TimeUnit.MILLISECONDS));
cp.addLast("idleDisconnectHandler", new IdleDisconnectHandler());
}
cp.addLast("authHandler", securityHandlers.getAuthenticationHandler());
cp.addLast("dispatcher", new NiftyDispatcher(def, nettyServerConfig.getTimer()));
cp.addLast("exceptionLogger", new NiftyExceptionLogger());
return cp;
}
};
1)ConnectionContextHandler 負責在連接建立成功的時候,創建NiftyConnectionContext上線文對象,附著在ctx中【ConnectionHandlerContext】
2)ConnectionLimiter 負責對會話打開和關閉是對連接數進行計數,并判斷是否超過最大連接數,超出的會直接關閉連接。
3)ChannelStatistics 負責統計打開的channel數、上下行流量。
4)安全方面的Handler,這里先不分析了。
5)DefaultThriftFrameCodec,負責Thrift的協議頭的解析。把字節數據變成ThriftMessage對象。從而會觸發到NiftyDispatcher處理器。
對于Framed的消息,Thrift是4個字節的長度+對應長度的消息體。
6)IdleStateHandler用來檢查讀寫或者兩者一起Idle的情況,如果發生了超時現象,就會向上匯報IDLE事件。
7)IdleDisconnectHandler用于處理Idle事件,收到Idle事件后,IdleDisconnectHandler負責關閉連接。
8)認證處理器
9)NiftyDispatcher負責消息的分發
10)NiftyExceptionLogger:負責記錄網絡中的異常數據到日志中。
**NiftyDispatcher類
負責Thrift消息的處理、分發和返回處理。
1、消息處理部分
前面在協議棧已經分析過了,在DefaultThriftFrameCodec處理器處理過后,就會把字節數據變成ThriftMessage,從而觸發NiftyDispatcher的處理,如下所示:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception
{
if (e.getMessage() instanceof ThriftMessage) {
ThriftMessage message = (ThriftMessage) e.getMessage();
message.setProcessStartTimeMillis(System.currentTimeMillis());
checkResponseOrderingRequirements(ctx, message);
TNiftyTransport messageTransport = new TNiftyTransport(ctx.getChannel(), message);
TTransportPair transportPair = TTransportPair.fromSingleTransport(messageTransport);
TProtocolPair protocolPair = duplexProtocolFactory.getProtocolPair(transportPair); // 活的雙線協議處理器,這里會根據TThriftServerDef中定義的,缺省值為:this.duplexProtocolFactory = TDuplexProtocolFactory.fromSingleFactory(new TBinaryProtocol.Factory(true, true));
TProtocol inProtocol = protocolPair.getInputProtocol();
TProtocol outProtocol = protocolPair.getOutputProtocol();
processRequest(ctx, message, messageTransport, inProtocol, outProtocol);
}
else {
ctx.sendUpstream(e);
}
}
從上面代碼中,協議的出入棧打包器在這里創建了出來,TNiftyTransport類似乎繼承和實現了原生Thrift的TTransport類。(注意看ThriftTransport類的in和out成員,in成員是傳入的TThriftMessage的消息體,而out則是新城成的一個ChannelBuffer)
2、消息分發
NiftyDispatcher的exec成員是一個Executor對象,它負責分發消息執行,收到的ThriftMessage會被放到工作線程池中執行,而不是在io線程中。
exe.execute(new Runnable() {
@Override
public void run() {
ListenableFuture<Boolean> processFuture;
final AtomicBoolean responseSent = new AtomicBoolean(false);
// Use AtomicReference as a generic holder class to be able to mark it final
// and pass into inner classes. Since we only use .get() and .set(), we don't
// actually do any atomic operations.
final AtomicReference<Timeout> expireTimeout = new AtomicReference<>(null);
try {
try {
// 超時處理
long timeRemaining = 0;
long timeElapsed = System.currentTimeMillis() - message.getProcessStartTimeMillis();
if (queueTimeoutMillis > 0) {
if (timeElapsed >= queueTimeoutMillis) {
TApplicationException taskTimeoutException = new TApplicationException(
TApplicationException.INTERNAL_ERROR,
"Task stayed on the queue for " + timeElapsed +
" milliseconds, exceeding configured queue timeout of " + queueTimeoutMillis +
" milliseconds."
);
sendTApplicationException(taskTimeoutException, ctx, message, requestSequenceId, messageTransport,
inProtocol, outProtocol);
return;
}
} else if (taskTimeoutMillis > 0) {
if (timeElapsed >= taskTimeoutMillis) {
TApplicationException taskTimeoutException = new TApplicationException(
TApplicationException.INTERNAL_ERROR,
"Task stayed on the queue for " + timeElapsed +
" milliseconds, exceeding configured task timeout of " + taskTimeoutMillis +
" milliseconds."
);
sendTApplicationException(taskTimeoutException, ctx, message, requestSequenceId, messageTransport,
inProtocol, outProtocol);
return;
} else {
timeRemaining = taskTimeoutMillis - timeElapsed;
}
}
if (timeRemaining > 0) {
expireTimeout.set(taskTimeoutTimer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// The immediateFuture returned by processors isn't cancellable, cancel() and
// isCanceled() always return false. Use a flag to detect task expiration.
if (responseSent.compareAndSet(false, true)) { // 只能被發送一次返回消息
TApplicationException ex = new TApplicationException(
TApplicationException.INTERNAL_ERROR,
"Task timed out while executing."
);
// Create a temporary transport to send the exception
ChannelBuffer duplicateBuffer = message.getBuffer().duplicate();
duplicateBuffer.resetReaderIndex();
TNiftyTransport temporaryTransport = new TNiftyTransport(
ctx.getChannel(),
duplicateBuffer,
message.getTransportType());
TProtocolPair protocolPair = duplexProtocolFactory.getProtocolPair(
TTransportPair.fromSingleTransport(temporaryTransport));
sendTApplicationException(ex, ctx, message,
requestSequenceId,
temporaryTransport,
protocolPair.getInputProtocol(),
protocolPair.getOutputProtocol());
}
}
}, timeRemaining, TimeUnit.MILLISECONDS));
}
ConnectionContext connectionContext = ConnectionContexts.getContext(ctx.getChannel());
RequestContext requestContext = new NiftyRequestContext(connectionContext, inProtocol, outProtocol, messageTransport);
RequestContexts.setCurrentContext(requestContext);
processFuture = processorFactory.getProcessor(messageTransport).process(inProtocol, outProtocol, requestContext);
} finally {
// RequestContext does NOT stay set while we are waiting for the process
// future to complete. This is by design because we'll might move on to the
// next request using this thread before this one is completed. If you need
// the context throughout an asynchronous handler, you need to read and store
// it before returning a future.
RequestContexts.clearCurrentContext();
}
Futures.addCallback(
processFuture,
new FutureCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
deleteExpirationTimer(expireTimeout.get());
try {
// Only write response if the client is still there and the task timeout
// hasn't expired.
if (ctx.getChannel().isConnected() && responseSent.compareAndSet(false, true)) { // 只能被發送一次返回消息,防止跟超時消息搞重
ThriftMessage response = message.getMessageFactory().create(
messageTransport.getOutputBuffer());
writeResponse(ctx, response, requestSequenceId,
DispatcherContext.isResponseOrderingRequired(ctx));
}
} catch (Throwable t) {
onDispatchException(ctx, t);
}
}
@Override
public void onFailure(Throwable t) {
deleteExpirationTimer(expireTimeout.get());
onDispatchException(ctx, t);
}
}
);
} catch (TException e) {
onDispatchException(ctx, e);
}
}
});
3、流程說明
1、初始化ThriftServiceProcessor
這個過程充分利用了java的反射原理,基于一個Thrift的服務處理類來構建Thrift服務處理器。
逐步步驟為:
1)生成ThriftCodecManager對象,用于對Thrift協議中的各種數據類型進行編解碼處理。
這個步驟有點特殊,將獨立說明,見:ThriftCodecManager的構建。
2)構建一個服務器處理對象,例子中是一個SwfitScribe対象
3)分析服務器處理対像,提出處理函數,綁定到服務處理器中。
2、協議解析以及消息分發機制