Swift Server端源码分析

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、协议解析以及消息分发机制

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

推荐阅读更多精彩内容