什么是新连接接入?以及新连接接入前,Netty处于什么状态

netty的服务端NioServerSocketChannel初始化,注册在BossGroup中的一条NioEventLoop中,并且给NioServerSocketChannel中维护的jdk原生的ServerSocketChannel绑定好了端口后, EventLoop启动,开始轮询工作...

这时候 EventLoop 它在轮询什么? 其实它在轮询监听当初NioServerSocketChannel经过二次注册感兴趣的事件时, 告诉 Selector,让Selector关注自己身上可能会出现 OP_ACCEPT 事件, 这合情合理,因为对于Netty的主从Reactor线程模型中, BossGroup中的channel只关心OP_ACCEPT 也就是用户的请求建立连接事件

netty的新连接接入要做哪些工作?

看上图,netty的新连接接入,对应这个线程模型中我圈出来的部分, 主要步骤如下

  • 服务端Selector轮询到客户端请求建立连接
  • 处理请求
    • 从服务端维护的JDK 原生ServerSocketChannel中accept()客户端的channel
    • 使用new的方法 将客户端的Channel封装成 NioSocketChannel
      • 层层往上调用super(),初始化channel的组件
      • 创建channel的配置类对象 config
    • 向下传播channelRead事件
      • 给客户端的channel设置相关参数
      • 将客户端的channel注册在 workerGroup 中的轮询算法选出的 EventLoop
      • 将jdk原生的SocketChanel注册进 EventLoop中的选择器中
      • 传播channelregist事件
      • 传播channelActive事件
        • 给客户端的channel二次注册netty可以处理的感兴趣的事件

这是我总结的新连接接入的流程,从上面分析的开始检查新链接,终止的标志是,把客户端的NioSocketChannel二次注册在EventLoop上,成为Netty可以处理的chanel为止

入口:NioEventLoop处理IO事件

当服务端的事件循环检测到有io事件时,使用它的processSelectedKeys();处理,源码如下:

private void processSelectedKeys() {
// todo selectedKeys 就是经过优化后的keys(底层是数组) , 默认不为null
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

当有了新IO请求进来, jdk原生的Selector将SelectionKey放入存放感兴趣的key的集合中,而这个集合现在就是netty通过反射的方式强制替换为以数组为数据结构的selectedKeys, 数组不为空, 跟进processSelectedKeysOptimized();,源码如下: 解析写在源码下面:

private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// todo 数组输出空项, 从而允许在channel 关闭时对其进行垃圾回收
// See https://github.com/netty/netty/issues/2363
// todo 数组中当前循环对应的keys质空, 这种感兴趣的事件只处理一次就行
selectedKeys.keys[i] = null; // todo 获取出 attachment,默认情况下就是注册进Selector时,传入的第三个参数 this===> NioServerSocketChannel
// todo 一个Selector中可能被绑定上了成千上万个Channel, 通过K+attachment 的手段, 精确的取出发生指定事件的channel, 进而获取channel中的unsafe类进行下一步处理
final Object a = k.attachment();
// todo if (a instanceof AbstractNioChannel) {
// todo 进入这个方法, 传进入 感兴趣的key + NioSocketChannel
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
} if (needsToSelectAgain) {
// null out entries in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.reset(i + 1); selectAgain();
i = -1;
}
}
}

处理感兴趣的事件, 想想,需要什么他才能进一步处理呢? 需要下面两点:

  • 这个感兴趣的事件是啥?

    • 在这了就是上面的 k
  • 哪个channel出现的Selector感兴趣的事件?
    • 在这里是通过 attachment拿到的 a ,其实不就是服务端的NioServerSocketChannel ?

另外它把NioServerSocketChannel向上强转成了AbstractNioChannel这是为什么呢?

答:

第一点:

在我写的上一篇Chanel的架构体系中,我们知道,Netty的NioXXXChannel其实是netty的,基于原生的jdk的chanel的封装,而在他的整个继承体系中,这个AbstractNioChannel就负责维护jdk原生的channel, 知道了这有啥用? 当然有用,我们要去给客户端channel接生了,原生服务端channel.accept()==客户端channel

第二点:

针对数据的读写都是unsafe中,回想是哪个类中定义了读取channel中IO数据的抽象模板函数呢? AbstractNioChannel, 是它新增的内部接口,从而进客户端和服务对针对chanel的不同特化read进行不同的实现

好, 有了这两个条件,继续跟进processSelectedKey(k, (AbstractNioChannel) a);看它是如何处理, 源码如下:

  • 获取到服务端的unsafe对象(数据读写)
  • 根据k的readOps,进行计算决定执行unsafe.read();
// todo 服务端启动后,方法被用用处理新链接,  可以模拟 telnet localhost 8899 新链接的介入
// todo 处理selectedkey
// todo netty底层对数据的读写都是 unsafe完成的
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
// todo 这个unsafe 也是可channel 也是和Channel进行唯一绑定的对象
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) { // todo 确保Key的合法
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
return;
}
if (eventLoop != this || eventLoop == null) { // todo 确保多线程下的安全性
return;
}
unsafe.close(unsafe.voidPromise());
return;
}
// todo NioServerSocketChannel和selectKey都合法的话, 就进入下面的 处理阶段
try {
// todo 获取SelectedKey 的 关心的选项
int readyOps = k.readyOps();
// todo 在read() write()之前我们需要调用 finishConnect() 方法, 否则 NIO JDK抛出异常
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps( ); unsafe.finishConnect();
} // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// todo 同样是检查 readOps是否为零, 来检查是否出现了 jdk 空轮询的bug
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

下面我们进入 unsafe.read();, 直接跳进去,直接进入到了AbstractNioChannel的抽象内部类,因为上面说了,做了向上强制类型转换,我们源码如下:

    /**
* Special {@link Unsafe} sub-type which allows to access the underlying {@link SelectableChannel}
*/
public interface NioUnsafe extends Unsafe {
/**
* Return underlying {@link SelectableChannel}
*/
SelectableChannel ch(); /**
* Finish connect
*/
void finishConnect(); void forceFlush();
}

具体的实现是谁? 因为我们是服务端的channel, 所以实现类是:NioMessageUnsafe, 进入查看他的源码: 下面这段代码真的是挺长的, 它的解析我写在他的下面:

@Override
public void read() {
// todo 同样是断言, 当前的线程必须是在 EventLoop 里面的线程才有资格执行
assert eventLoop().inEventLoop( );
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
// todo 用于查看服务端接受的速率, 说白了就是控制服务端是否接着read 客户端的IO事件
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config); boolean closed = false;
Throwable exception = null;
try {
try {
do {
// todo 进入
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
//todo 对读到的连接,进行简单的计数
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
} int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
// todo 处理新的连接的逻辑来到这, 意思是让pipeline中发生事件传播,
// todo pipeline是谁的呢? 现在是NioMessageUnsafe 所以是服务端的,
// todo 事件是如何传播的呢? head-->>ServerBootStraptAcceptor-->>tail 依次传播,
// todo 传播的什么事件? ChannelRead, 也就是说,会去调用 ServerBootStraptAcceptor的ChannelRead方法,跟进去
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete(); if (exception != null) {
closed = closeOnReadError(exception); pipeline.fireExceptionCaught(exception);
} if (closed) {
inputShutdown = true;
if (isOpen()) {
close(voidPromise());
}
}
} finally {
// Check if there is a readPending which was not processed yet.
// This could be for two reasons:
// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
//
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}

read()三部曲:

针对这段代码,我们值关心下面几部分, 这三部分结束, 整个新链接的建立就完成了,

下面三部曲的 大前提都是,当前我们是在AbstractNioMessageChannel

  • doReadMessages(readBuf)
  • allocHandle.incMessagesRead(localRead);
  • pipeline.fireChannelRead(readBuf.get(i));

第一步:

如何创建出jdk原生的 客户端channel,对它做了什么?

第一步doReadMessages(readBuf) 这是AbstractNioMessageChannel的抽象方法,从chanel读取内容我们需要一个维护特化chanenl引用的对象,谁呢? 它的子类NioServerSocketChannel, 源码如下: 解析依然写在代码下面

//todo doReadMessage  其实就是 doChannel
// todo 处理新连接, 现在是在 NioServReaderSocketChannel里面
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// todo java Nio底层在这里 创建jdk底层的 原生channel
SocketChannel ch = SocketUtils.accept(javaChannel()); try {
if (ch != null) {
// todo 把java原生的channel, 封装成 Netty自定义的封装的channel , 这里的buf是list集合对象,由上一层传递过来的
// todo this -- NioServerSocketChannel
// todo ch -- SocketChnnel
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t); try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
} return 0;
}

这是个跨越性的操作, 上面的代码主要进行如下面几步工作:

  • 从原生的jdk ServerSocketChannel中 accept出 jdk原生的 SocketChanel
  • 将jdk原生的 Socket封装成Netty对它的封装类型 NioChannel

为啥,服务端的channel需要反射创建,而客户的的channel直接new?

我的理解是,netty不仅可以做 NIO编程模型的服务器, 传统的阻塞式IO,或者其他类型的服务器他也可以做, 我们传递进入的服务端Chanel的类型决定了他可以成为的服务器的类型, netty的设计者是不知道,用户想用netty做些什么的,于是设计成通过反射创建

但是,一旦服务端的channel类型确定了,对应的客户端的channel也一定知道了,直接new 就好了

NioSocketChannel的创建过程

我们跟进new NioSocketChannel(this, ch) ,继续阅读, 其中的 this,是服务端的NioServerSocketChannel , ch 是 jdk原生的 SocketChannel, 方法调用链 的源码如下:

public NioSocketChannel(Channel parent, SocketChannel socket) {
// todo 向上传递
super(parent, socket);
// todo 主要是设置 禁用了 NoDelay算法
config = new NioSocketChannelConfig(this, socket.socket());
}

跟进去, 看, 他把SelectionKey.OP_READ,传递给了他的父类, 稍后 会用这个参数进行 cannel的二次注册,使得NioSocketChannel可以被netty处理它发生的感兴趣的事件, 我们发现,和服务端的chanel明显不同的是, 服务端的NioChannel关注用户的accept,而这里的客户端的channel关注的是read事件,它标志着,服务端的Selector会关心它当中传递进客户端发送的数据,告诉Selector应该读

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}

继续跟进,到AbstractNioChannel, 他做了如下工作:

  • super(parent) 把NioServerSocketChannel设置为NioSokcetChannel的父parent
  • 自己维护原生的JDK SocketChannel
  • 保存感性趣的选项
  • 设置为非阻塞

源码如下:

*/ // todo 无论是服务端的channel 还是客户端的channel都会使用这个方法进行初始化
// // TODO: 2019/6/23 null ServerSocketChannel accept
// todo 如果是在创建NioSocketChannel parent==NioServerSocketChannel ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);// todo 继续向上跟,创建基本的组件
// todo 如果是创建NioSocketChannel 这就是在保存原生的jdkchannel
// todo 如果是创建NioServerSocketChannel 这就是在保存ServerSocketChannel
this.ch = ch;
// todo 设置上感兴趣的事件
this.readInterestOp = readInterestOp;
try {
// todo 作为服务端, ServerSocketChannel 设置为非阻塞的
// todo 作为客户端 SocketChannel 设置为非阻塞的
ch.configureBlocking(false);

第二步:

现在NioSocketChannel已经创建完成了,代码的调用栈重新返回上面的NioMessageUnsafe.read()方法,我们接着往下看

//todo 对读到的连接,进行简单的计数
allocHandle.incMessagesRead(localRead);

第三步 pipeline.fireChannelRead(readBuf.get(i));

往下传播channelRead(), 在管道中传递事件 channel, 对于服务端来说, 现在他的pipeline是怎么个状态呢?

Header --> ServerBootStraptAcceptor --> tail

channel的pipeline组件是基于双向链表实现,其中head和tail是默认的链表头和尾, 中间的ServerBootStraptAcceptor是什么呢? 其实他是在创建服务端的NioServerSocketChannel时,是在channel注册完毕之后,通过回调,将ServerBootStrapinit()函数,给channel添加channelInitializer时添加进去的; ServerBootStraptAcceptor本质上就是handler, 回顾第一个图, 他就是图中的Acceptor

ok,现在我们去直接去ServerBootStraptAcceptor中,他是ServerBootStrap的内部类,我们看它的channelRead()方法,源码如下:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
// todo 给这个来连接的通道添加 childHandler,是我在Server中添加的childHandler, 实际上是那个MyChannelInitializer , 最终目的是添加handler
child.pipeline().addLast(childHandler);
// todo 给新来的Channel设置 options 选项
setChannelOptions(child, childOptions, logger);
// todo 给新来的Channel设置 attr属性
for (Entry<AttributeKey<?>, Object> e : childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
} try {
//todo 这里这!! 把新的channel注册进 childGroup
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {

我们可以看到,如下工作:

  • 初始化属性
  • 把客户端的channel的注册进childGroup中的EventLoop

在这里补一张channelGroup的继承图

我们看这个childGroup.regist()方法, 我们知道childGroup是workerGroup,在本类中,它的类型是EventLoopGroup,这是个接口类型的变量, 我们用点进去查看源码自然跳转进接口中,但是我们需要找他的实现类, 那么,是谁重写的它的方法呢?

大家去看看上面的图,它的直接实现类只有一个MultiThreadEventGroup, 其实大家想想看,现在的任务是将原生的客户端channel,注册进WorkerGroup中的EventLoop,那第一步是啥呢? 不得先从这个 事件循环组中拿出一个事件循环吗? 好,进去看MultiThreadEventGroup是如何实现的, 源码如下:

@Override
public ChannelFuture register(Channel channel) {
// todo next() -- 就在上面-> 根据轮询算法获取一个事件的执行器 EventExecutor
// todo, 而每一个EventLoop对应一个EventExecutor 这里之所以是个组, 是因为, 我的机器内核决定我的 事件循环组有八个线程,
// todo ?? ????
// todo 但是一会的责任并没有一直循环, 难道有效的bossGroup只有一条 // todo 再进去就是SingleThreadEventLoop对此方法的实现了
return next().register(channel);
}

是的,确实在获取事件循环,我们进行跟进 next().register(channel), 现在是 eventloop.regist() ,当我们进入方法时,再次来到EventLoopGroup对这个方法的实现, ok,大家重新去看上面的图,一个eventloop.regist(),现在不再是 循环组.regist 而是 事件循环.regist, 而在图上,我们可以很轻松的看到 , 对EventLoopGroup接口的实现就是 SingleThreadEventLoop, 好,接着进去看它的实现, 源码如下:

// todo register来到这里
@Override
public ChannelFuture register(Channel channel) {
// todo ChannelPromise == channel+Executor 跟进去
// todo 再次调用 register, 就在下面
return register(new DefaultChannelPromise(channel, this));
}

调用本类的register(new DefaultChannelPromise(channel, this) ,接着进去,源码如下: 同样解析写在源码下面

@Override
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
// todo 重点来了
// todo channel() 获取通道对象
// todo unsafe() 获取仅供内部使用的unsafe对象 它定义在Channel接口中, 具体的对象是 Channel的子类, AbstractNioChannel
// todo unsafe对象进行下一步注册 register
* promise.channel().unsafe().register(this, promise);
return promise;
}
  • promise.channel() 取出的是客户端的 NioSocketChanenl
  • promise.channel().unsafe()AbstractUnsafe

来到regist()的实现类

方法调用链:

  • 本类方法register

    • 本类方法register0()

      • 本类抽象方法doRegister()
      • pipeline.fireChannelRegistered();传播channel注册事件
      • pipeline.fireChannelActive(); 传播channel Active事件
        • 二次注册事件

其中,上面的doRegister()是真正的将jdk原生的channel注册进原生的selector

pipeline.fireChannelRegistered();是在 header --> ServerBootStraptAccptor --> 用户自己添加的handler --> tail 中,挨个传递 ChannelRegistered, 就是从头开始调用它们的函数, 我们着重看下面的第三个

pipeline.fireChannelActive();其实是比较绕的,涉及到了pipeline中事件的传递,但是它的作用很大,通过传播channelActive挨个回调他们的状态,netty成功的给这条客户端的新连接注册上了netty能处理的感兴趣的事件

整体源码太长了我不一一贴出来了, 直接看关于pipeline.fireChannelActive();的源码,如下:

if (isActive()) {
if (firstRegistration) {
// todo 在pipeline中传播ChannelActive的行为,跟进去
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
// todo 可以接受客户端的数据了
beginRead();
}

第一个判断, if (isActive())针对两个channel,存在两种情况

  • 如果是服务端的channel, 只有在channel绑定完端口后,才会处于active的状态
  • 如果是客户端的channel, 注册到selector+处于连接状态, 他就是active状态

满足条件,进入第一个分支判断,同样满足第一次注册的条件,开始传播事件

回想一下,现在程序进行到什么状态? 看上图的subReactor每一个蓝色的箭头都是一个客户端的channel, 问题是netty还处理不了这些channel上的会发生的感兴趣的事件,因为第一步我们只是把jdk原生的chanel和原生的selector之间进行了关联, 而netty对他们的封装类还没有关联,于是下一步就通过传播active的行为去二次注册关联感兴趣的事件

关于pipeline中的事件传递太多内容了,在下篇博客中写,连载

现在直接给结果,

传递到header的read()源码如下

  @Override
public void read(ChannelHandlerContext ctx) {
// todo 如果是服务端: NioMessageUnsafe
// todo 如果是客户端: NioSocketChannelUnsafe
unsafe.beginRead();
}

接着跟进

@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
//todo 如果是服务端: 这里的SelectionKey就是我们在把NioServerSocketChannel 注册进BoosGroup中的eventLoop时,返回的selectionKey , 就在上面
//todo 如果是客户端: 这里的SelectionKey就是我们在把NioSocketChannel 注册进BoosGroup中的eventLoop时,返回的selectionKey , 就在上面
final SelectionKey selectionKey = this.selectionKey;
// todo 这SelectionKey 就是我们把 NioServerSocketChannel中的ServerSocketChannel注册进BossGroup时, 附加的第三个参数 0
if (!selectionKey.isValid()) {
return;
} readPending = true;
// todo 获取这个Selection 的感兴趣的事件,实际就是当时注册时的第二个参数 0
final int interestOps = selectionKey.interestOps();
// todo 如果是服务端, readInterestOp是创建服务端channel时设置的 op_accept
// todo 如果是客户端的新连接,readInterestOp是创建客户端channel时设置的 op_read
if ((interestOps & readInterestOp) == 0) {
// todo interestOps | readInterestOp两者进行或运算,原来是0事件 , 现在又增加了一个事件, accept事件或者是read
// todo 进而 从新注册到SelectionKey上面去。。。 0P_Accept 或者 OP_Read
selectionKey.interestOps(interestOps | readInterestOp);
}
}

ok, 到这里netty的新链接接入就完成了....

连载下一篇, pipeline中的事件传播

Netty-新连接接入源码解读的更多相关文章

  1. Netty 学习(八):新连接接入源码说明

    Netty 学习(八):新连接接入源码说明 作者: Grey 原文地址: 博客园:Netty 学习(八):新连接接入源码说明 CSDN:Netty 学习(八):新连接接入源码说明 新连接的接入分为3个 ...

  2. Netty是如何处理新连接接入事件的?

    更多技术分享可关注我 前言 前面的分析从Netty服务端启动过程入手,一路走到了Netty的心脏——NioEventLoop,又总结了Netty的异步API和设计原理,现在回到Netty服务端本身,看 ...

  3. HttpClient 4.3连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  4. HttpClient4.3 连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  5. Netty服务端NioEventLoop启动及新连接接入处理

    一 Netty服务端NioEventLoop的启动 Netty服务端创建.初始化完成后,再向Selector上注册时,会将服务端Channel与NioEventLoop绑定,绑定之后,一方面会将服务端 ...

  6. Netty异步Future源码解读

    本文地址: https://juejin.im/post/5df771ee6fb9a0161d743069 说在前面 本文的 Netty源码使用的是 4.1.31.Final 版本,不同版本会有一些差 ...

  7. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...

  8. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

  9. AFNetworking 3.0 源码解读 总结(干货)(上)

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...

随机推荐

  1. 零元学Expression Blend 4 - Chapter 45 ListBox里的物件不能换行吗?

    原文:零元学Expression Blend 4 - Chapter 45 ListBox里的物件不能换行吗? ListBox里的排列不是垂直就是水平,觉得这样的排列很枯燥乏味吗? 想要它变聪明吗? ...

  2. 【转】ORACLE AWR报告

    转自:http://blog.csdn.net/liqfyiyi/article/details/8236864 About Oracle AWR Oracle AWR is a powerful m ...

  3. UWP-消息提示(仿Android)

    原文:UWP-消息提示(仿Android) 在UWP中的消息提示框(一)中介绍了一些常见的需要用户主动去干涉的一些消息提示框,接下来打算聊聊不需要用户主动去干涉的一些消息提示框.效果就是像双击退出的那 ...

  4. nginx 简单应用

    从源代码编译 Nginx 把源码解压缩之后,在终端里运行如下命令: $ ./configure $ make $ sudo make install 默认情况下,Nginx 会被安装在 /usr/lo ...

  5. Channel 9视频整理【1】

    David Dong 微软mvp https://www.​facebook.com/​DotNet​Walker http://s​tudyhost.​blogspot.tw/ https://ch ...

  6. Understand the Qt containers(有对应表)

    Container classes are one of the cornerstones of object-oriented programming, invaluable tools that ...

  7. Redis 学习笔记(篇三):跳表

    跳表 跳表(skiplist)是一种有序的数据结构,是在有序链表的基础上发展起来的. 在 Redis 中跳表是有序集合(sort set)的底层实现之一. 说到 Redis 中的有序集合,是不是和 J ...

  8. kubernetes实战篇之为默认账户创建镜像拉取密钥

    系列目录 上一节我们分别使用纯文本账户密码和docker的config文件一创建一个kubernetes secret对象,并且把它添加到containers的imagePullSecrets字段用以 ...

  9. 分享Sql Server 2008 r2 数据备份,同步服务器数据(二.本地发布,订阅)

    上一篇文章中写到了数据库的本地备份,这一篇主要分享一下关于不同服务器的数据备份,主要是使用sql server中的本地发布,本地订阅功能,在数据库的读写分离中,也会经常性的用到这个功能. 复制-> ...

  10. 确认过眼神,看清HTTP协议

    导读:什么是 HTTP?它有什么属性?我们常用的是什么呢?快来阅读本文,将会为你一一道来. 什么是 HTTP 协议? 在了解HTTP之前,我们需要了解什么是网络通信模型(也就是我们常说的 OSI 模型 ...