Netty的源码分析和业务场景
Netty 是一个高性能、异步事件驱动的网络应用框架,它基于 Java NIO 构建,广泛应用于互联网、大数据、游戏开发、通信行业等多个领域。以下是对 Netty 的源码分析、业务场景的详细介绍:
源码概述
- Netty 的核心组件:Netty 的架构设计围绕着事件驱动的核心思想,主要包括 Channel、EventLoopGroup、ChannelHandlerContext 和 ChannelPipeline 等关键概念。
 - Channel:是网络连接的抽象表示,每个 Channel 都有一个或多个 ChannelHandler 来处理网络事件,如连接建立、数据接收等。
 - EventLoopGroup:是一组 EventLoop 的集合,每个 EventLoop 负责处理一组 Channel 的 I/O 事件。当 Channel 的事件触发时,相应的 EventLoop 会调用 ChannelHandler 中的方法进行处理。
 - ChannelPipeline:是 ChannelHandler 的有序集合,用于处理进来的和出站的数据。通过在 Pipeline 中添加不同的 Handler,可以实现复杂的业务逻辑。
 - 源码中的关键流程:Netty 的源码分析需要关注的关键流程包括初始化、Channel 的注册、EventLoop 的工作流程、以及连接的建立和绑定过程。
 
Netty 提供了一个 Echo 示例,用于演示客户端和服务器端的基本通信流程。在这个示例中,客户端发送的消息被服务器端接收并原样返回,展示了 Netty 处理网络通信的基本方法。
下面 V 哥来详细介绍一下这几外关键核心组件。
1. Channel组件
Netty 的 Channel 组件是整个框架的核心之一,它代表了网络中的一个连接,可以是客户端的也可以是服务器端的。Channel 是一个低级别的接口,用于执行网络 I/O 操作。以下是对 Channel 组件的源码分析和解释:
Channel 接口定义
Channel 接口定义了一组操作网络连接的方法,例如绑定、连接、读取、写入和关闭。
public interface Channel extends AttributeMap {
    /**
     * Returns the {@link ChannelId} of this {@link Channel}.
     */
    ChannelId id();
    /**
     * Returns the parent {@link Channel} of this channel. {@code null} if this is the top-level channel.
     */
    Channel parent();
    /**
     * Returns the {@link ChannelConfig} of this channel.
     */
    ChannelConfig config();
    /**
     * Returns the local address of this channel.
     */
   SocketAddress localAddress();
    /**
     * Returns the remote address of this channel. {@code null} if the channel is not connected.
     */
    SocketAddress remoteAddress();
    /**
     * Returns {@code true} if this channel is open and may be used.
     */
    boolean isOpen();
    /**
     * Returns {@code true} if this channel is active and may be used for IO.
     */
    boolean isActive();
    /**
     * Returns the {@link ChannelPipeline}.
     */
    ChannelPipeline pipeline();
    /**
     * Returns the {@link ChannelFuture} which is fired once the channel is registered with its {@link EventLoop}.
     */
    ChannelFuture whenRegistered();
    /**
     * Returns the {@link ChannelFuture} which is fired once the channel is deregistered from its {@link EventLoop}.
     */
    ChannelFuture whenDeregistered();
    /**
     * Returns the {@link ChannelFuture} which is fired once the channel is closed.
     */
    ChannelFuture whenClosed();
    /**
     * Register this channel to the given {@link EventLoop}.
     */
    ChannelFuture register(EventLoop loop);
    /**
     * Bind and listen for incoming connections.
     */
    ChannelFuture bind(SocketAddress localAddress);
    /**
     * Connect to the given remote address.
     */
    ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress);
    /**
     * Disconnect if connected.
     */
    ChannelFuture disconnect();
    /**
     * Close this channel.
     */
    ChannelFuture close();
    /**
     * Deregister this channel from its {@link EventLoop}.
     */
    ChannelFuture deregister();
    /**
     * Write the specified message to this channel.
     */
    ChannelFuture write(Object msg);
    /**
     * Write the specified message to this channel and generate a {@link ChannelFuture} which is done
     * when the message is written.
     */
    ChannelFuture writeAndFlush(Object msg);
    /**
     * Flushes all pending messages.
     */
    ChannelFuture flush();
    // ... 更多方法定义
}
Channel 的关键方法
id(): 返回Channel的唯一标识符。parent(): 返回父Channel,如果是顶级Channel,则返回null。config(): 获取Channel的配置信息。localAddress()和remoteAddress(): 分别返回本地和远程地址。isOpen()和isActive(): 分别检查Channel是否打开和激活。pipeline(): 返回与Channel关联的ChannelPipeline,它是处理网络事件的处理器链。register(),bind(),connect(),disconnect(),close(),deregister(): 这些方法用于执行网络 I/O 操作。
Channel 的实现类
Netty 为不同类型的网络通信协议提供了多种 Channel 的实现,例如:
NioSocketChannel:用于 NIO 传输的 TCP 协议的Channel实现。NioServerSocketChannel:用于 NIO 传输的 TCP 服务器端Channel实现。OioSocketChannel和OioServerSocketChannel:类似 NIO,但是用于阻塞 I/O。
Channel 的生命周期
- 创建:
Channel通过其工厂方法创建,通常与特定的EventLoop关联。 - 注册:
Channel必须注册到EventLoop上,以便可以处理 I/O 事件。 - 绑定/连接:服务器端 
Channel绑定到特定地址并开始监听;客户端Channel连接到远程地址。 - 读取和写入:通过 
Channel读取和写入数据。 - 关闭:关闭 
Channel,释放相关资源。 
Channel 的事件处理
Channel 的事件处理是通过 ChannelPipeline 和 ChannelHandler 完成的。ChannelPipeline 是一个处理器链,负责处理所有的 I/O 事件和 I/O 操作。每个 Channel 都有一个与之关联的 ChannelPipeline,可以通过 Channel 的 pipeline() 方法访问。
异步处理
Channel 的操作(如绑定、连接、写入、关闭)都是异步的,返回一个 ChannelFuture 对象,允许开发者设置回调,当操作完成或失败时执行。
内存管理
Netty 的 Channel 实现还涉及内存管理,使用 ByteBuf 作为数据容器,它是一个可变的字节容器,提供了一系列的操作方法来读写网络数据。
小结
Channel 是 Netty 中的一个核心接口,它定义了网络通信的基本操作。Netty 提供了多种 Channel 的实现,以支持不同的 I/O 模型和协议。通过 Channel,Netty 实现了高性能、异步和事件驱动的网络通信。
2. EventLoopGroup组件
EventLoopGroup 是 Netty 中一个非常重要的组件,它负责管理一组 EventLoop,每个 EventLoop 可以处理多个 Channel 的 I/O 事件。以下是对 EventLoopGroup 组件的详细分析和解释:
EventLoopGroup 接口定义
EventLoopGroup 接口定义了一组管理 EventLoop 的方法,以下是一些关键方法:
public interface EventLoopGroup extends ExecutorService {
    /**
     * Returns the next {@link EventLoop} this group will use to handle an event.
     * This will either return an existing or a new instance depending on the implementation.
     */
    EventLoop next();
    /**
     * Shuts down all {@link EventLoop}s and releases all resources.
     */
    ChannelFuture shutdownGracefully();
    /**
     * Shuts down all {@link EventLoop}s and releases all resources.
     */
    ChannelFuture shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit);
    /**
     * Returns a copy of the list of all {@link EventLoop}s that are part of this group.
     */
    List<EventLoop> eventLoops();
}
EventLoopGroup 的关键方法
next(): 返回下一个EventLoop,用于处理事件。这可以是现有的EventLoop或者新创建的实例,具体取决于实现。shutdownGracefully(): 优雅地关闭所有EventLoop并释放所有资源。这个方法允许指定一个静默期和一个超时时间,以便在关闭之前等待所有任务完成。eventLoops(): 返回当前EventLoopGroup中所有EventLoop的列表。
EventLoopGroup 的实现类
Netty 提供了几种 EventLoopGroup 的实现,主要包括:
DefaultEventLoopGroup: 默认的EventLoopGroup实现,使用NioEventLoop作为其EventLoop实现。EpollEventLoopGroup: 特定于 Linux 的EventLoopGroup实现,使用EpollEventLoop作为其EventLoop实现,利用 Linux 的epoll机制提高性能。OioEventLoopGroup: 阻塞 I/O 模式下的EventLoopGroup实现,使用OioEventLoop作为其EventLoop实现。
EventLoopGroup 的工作原理
- 创建: 
EventLoopGroup通过其构造函数创建,可以指定线程数。 - 注册: 
Channel需要注册到EventLoop上,以便EventLoop可以处理其 I/O 事件。 - 事件循环: 每个 
EventLoop在其线程中运行一个事件循环,处理注册到它的Channel的 I/O 事件。 - 关闭: 
EventLoopGroup可以被关闭,释放所有资源。 
EventLoopGroup 的线程模型
- 单线程模型: 一个 
EventLoopGroup只包含一个EventLoop,适用于小容量应用。 - 多线程模型: 一个 
EventLoopGroup包含多个EventLoop,每个EventLoop在单独的线程中运行,适用于高并发应用。 
EventLoopGroup 的使用场景
- 服务器端: 在服务器端,通常使用两个 
EventLoopGroup。一个用于接受连接(bossGroup),一个用于处理连接(workerGroup)。bossGroup通常使用较少的线程,而workerGroup可以根据需要处理更多的并发连接。 - 客户端端: 在客户端,通常只需要一个 
EventLoopGroup,用于处理所有的连接。 
示例代码
以下是如何在 Netty 中使用 EventLoopGroup 的示例代码:
public class NettyServer {
    public static void main(String[] args) {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 用于接受连接
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理连接
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(new LoggingHandler());
                     p.addLast(new MyServerHandler());
                 }
             });
            ChannelFuture f = b.bind(8080).sync(); // 绑定端口并启动服务器
            System.out.println("Server started on port 8080");
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
在这个示例中,bossGroup 用于接受连接,workerGroup 用于处理连接。通过 ServerBootstrap 类配置服务器,并使用 ChannelInitializer 来设置 Channel 的处理器链。
总结
EventLoopGroup 是 Netty 中管理事件循环的核心组件,它通过 EventLoop 处理 I/O 事件,支持高并发和异步操作。通过合理配置 EventLoopGroup,可以显著提高网络应用的性能和可扩展性。
3. ChannelPipeline组件
ChannelPipeline 是 Netty 中的一个核心组件,它负责管理一组 ChannelHandler,并且定义了 I/O 事件和操作如何在这些处理器之间流动。以下是对 ChannelPipeline 组件的详细分析和解释:
ChannelPipeline 接口定义
ChannelPipeline 是一个接口,定义了操作 ChannelHandler 的方法:
public interface ChannelPipeline extends Iterable<ChannelHandler> {
    /**
     * Add the specified handler to the context of the current channel.
     */
    void addLast(EventExecutorGroup executor, String name, ChannelHandler handler);
    /**
     * Add the specified handlers to the context of the current channel.
     */
    void addLast(EventExecutorGroup executor, ChannelHandler... handlers);
    // ... 省略其他 addFirst, addBefore, addAfter, remove, replace 方法
    /**
     * Get the {@link ChannelHandler} by its name.
     */
    ChannelHandler get(String name);
    /**
     * Find the first {@link ChannelHandler} in the {@link ChannelPipeline} that matches the specified class.
     */
    ChannelHandler first();
    /**
     * Find the last {@link ChannelHandler} in the {@link ChannelPipeline} that matches the specified class.
     */
    ChannelHandler last();
    /**
     * Returns the context object of the specified handler.
     */
    ChannelHandlerContext context(ChannelHandler handler);
    // ... 省略 contextFor, remove, replace, fireChannelRegistered, fireChannelUnregistered 等方法
}
ChannelPipeline 的关键方法
addLast(String name, ChannelHandler handler): 在管道的末尾添加一个新的处理器,并为其指定一个名称。addFirst(String name, ChannelHandler handler): 在管道的开头添加一个新的处理器。addBefore(String baseName, String name, ChannelHandler handler): 在指定处理器前添加一个新的处理器。addAfter(String baseName, String name, ChannelHandler handler): 在指定处理器后添加一个新的处理器。get(String name): 根据名称获取ChannelHandler。first()和last(): 分别获取管道中的第一个和最后一个处理器。context(ChannelHandler handler): 获取指定处理器的上下文。
ChannelHandlerContext
ChannelHandlerContext 是 ChannelHandler 和 ChannelPipeline 之间的桥梁,提供了访问和管理 Channel、ChannelPipeline 和 ChannelFuture 的能力:
public interface ChannelHandlerContext extends AttributeMap, ResourceLeakHint {
    /**
     * Return the current channel to which this context is bound.
     */
    Channel channel();
    /**
     * Return the current pipeline to which this context is bound.
     */
    ChannelPipeline pipeline();
    /**
     * Return the name of the {@link ChannelHandler} which is represented by this context.
     */
    String name();
    /**
     * Return the {@link ChannelHandler} which is represented by this context.
     */
    ChannelHandler handler();
    // ... 省略其他方法
}
ChannelPipeline 的工作原理
ChannelPipeline 维护了一个双向链表的 ChannelHandler 集合。每个 Channel 实例都有一个与之关联的 ChannelPipeline。当 I/O 事件发生时,如数据被读取到 Channel,该事件会被传递到 ChannelPipeline,然后按照 ChannelHandler 在管道中的顺序进行处理。
处理器的执行顺序
- 入站事件:当数据被读取到 
Channel时,事件会从管道的尾部向头部传递,直到某个ChannelHandler处理该事件。 - 出站事件:当需要发送数据时,事件会从管道的头部向尾部传递,直到数据被写出。
 
源码分析
ChannelPipeline 的实现类 DefaultChannelPipeline 内部使用了一个 ChannelHandler 的双向链表来维护处理器的顺序:
private final AbstractChannelHandlerContext head;
private final AbstractChannelHandlerContext tail;
private final List<ChannelHandler> handlers = new ArrayList<ChannelHandler>();
head和tail是链表的头尾节点。handlers是存储所有处理器的列表。
添加处理器时,DefaultChannelPipeline 会更新链表和列表:
public void addLast(EventExecutorGroup executor, String name, ChannelHandler handler) {
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    if (name == null) {
        throw new NullPointerException("name");
    }
    AbstractChannelHandlerContext newCtx = new TailContext(this, executor, name, handler);
    synchronized (this) {
        if (tail == null) {
            head = tail = newCtx;
        } else {
            tail.next = newCtx;
            newCtx.prev = tail;
            tail = newCtx;
        }
        handlers.add(newCtx);
    }
}
小结
ChannelPipeline 是 Netty 中处理网络事件和请求的管道,它通过维护一个 ChannelHandler 的链表来管理事件的流动。通过 ChannelHandlerContext,ChannelHandler 能够访问和修改 Channel 和 ChannelPipeline 的状态。这种设计使得事件处理流程高度可定制和灵活,是 Netty 高性能和易于使用的关键因素之一。
4. 源码中的关键流程
在 Netty 的 ChannelPipeline 的源码中,关键流程涉及处理器的添加、事件的触发、以及事件在处理器之间的流动。以下是一些关键流程的分析:
1. 处理器的添加
当创建 ChannelPipeline 并准备添加 ChannelHandler 时,需要确定处理器的顺序和位置。Netty 允许开发者在管道的开始、结束或指定位置插入处理器。
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("myHandler", new MyChannelHandler());
在 DefaultChannelPipeline 类中,处理器被添加到一个双向链表中,每个处理器节点(AbstractChannelHandlerContext)保存了指向前一个和后一个处理器的引用。
2. 事件循环和触发
每个 Channel 都与一个 EventLoop 关联,EventLoop 负责处理所有注册到它上面的 Channel 的事件。当 EventLoop 运行时,它会不断地循环,等待并处理 I/O 事件。
// EventLoop 的事件循环
public void run() {
    for (;;) {
        // ...
        processSelectedKeys();
        // ...
    }
}
3. 事件的捕获和传递
当 EventLoop 检测到一个 I/O 事件(如数据到达)时,它会触发相应的操作。对于 ChannelPipeline 来说,这意味着需要调用适当的 ChannelHandler 方法。
// 伪代码,展示了事件如何被传递到 ChannelHandler
if (channelRead) {
    pipeline.fireChannelRead(msg);
}
4. 入站和出站事件的处理
- 入站事件(如数据被读取)通常从 
ChannelPipeline的尾部开始传递,沿着管道向前,直到某个处理器处理了该事件。 - 出站事件(如写数据)则从 
ChannelPipeline的头部开始传递,沿着管道向后,直到数据被写出。 
// 入站事件处理
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 处理消息或传递给下一个处理器
    ctx.fireChannelRead(msg);
}
// 出站事件处理
public void write(ChannelHandlerContext ctx, Object msg) {
    // 写消息或传递给下一个处理器
    ctx.write(msg);
}
5. 处理器链的遍历
ChannelPipeline 需要能够遍历处理器链,以便按顺序触发事件。这通常通过从 ChannelHandlerContext 获取下一个或前一个处理器来实现。
// 伪代码,展示了如何获取下一个处理器并调用它
ChannelHandlerContext nextCtx = ctx.next();
if (nextCtx != null) {
    nextCtx.invokeChannelRead(msg);
}
6. 动态修改处理器链
在事件处理过程中,可能需要动态地修改处理器链,如添加新的处理器或移除当前处理器。
pipeline.addLast("newHandler", new AnotherChannelHandler());
pipeline.remove(ctx.handler());
7. 资源管理和清理
当 Channel 关闭时,ChannelPipeline 需要确保所有的 ChannelHandler 都能够执行它们的清理逻辑,释放资源。
public void channelInactive(ChannelHandlerContext ctx) {
    // 清理逻辑
}
8. 异常处理
在事件处理过程中,如果抛出异常,ChannelPipeline 需要能够捕获并适当地处理这些异常,避免影响整个管道的运行。
try {
    // 可能抛出异常的操作
} catch (Exception e) {
    ctx.fireExceptionCaught(e);
}
小结
ChannelPipeline 的源码中包含了多个关键流程,确保了事件能够按顺序在处理器之间传递,同时提供了动态修改处理器链和异常处理的能力。这些流程共同构成了 Netty 中事件驱动的网络编程模型的基础。
业务场景
- 微服务架构:Netty 可以作为 RPC 框架的基础,实现服务间的高效通信。
 - 游戏服务器:由于游戏行业对延迟和并发要求极高,Netty 的异步非阻塞特性非常适合构建高并发的游戏服务器。
 - 实时通信系统:Netty 可用于构建如即时消息、视频会议等需要低延迟数据传输的实时通信系统。
 - 物联网平台:Netty 可以作为设备与云平台之间的通信桥梁,处理大规模的设备连接和数据流。
 - 互联网行业:在分布式系统中,Netty 常作为基础通信组件被 RPC 框架使用,例如阿里的分布式服务框架 Dubbo 使用 Netty 作为其通信组件。
 - 大数据领域:Netty 也被用于大数据技术的网络通信部分,例如 Hadoop 的高性能通信组件 Avro 的 RPC 框架就采用了 Netty。
 
最后
通过深入分析 Netty 的源码和理解其在不同业务场景下的应用,开发者可以更好地利用这一强大的网络编程框架,构建高效、稳定且可扩展的网络应用。
Netty的源码分析和业务场景的更多相关文章
- netty : NioEventLoopGroup 源码分析
		
NioEventLoopGroup 源码分析 1. 在阅读源码时做了一定的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限.为了方便 IDE 查看.跟踪.调试 代码,所以在 github ...
 - 【源码分析】cocostudio场景编辑器的触发器逻辑
		
去看场景编辑器的差不多都可以看到有模拟器的设置(菜单栏的设置).默认是选择cocostudio安装路径中的Simulator.exe这个模拟器,看官网介绍是自己可以选择模拟器,而且公开源代码可以按需设 ...
 - Netty ByteBuf源码分析
		
Netty的ByteBuf是JDK中ByteBuffer的升级版,提供了NIO buffer和byte数组的抽象视图. ByteBuf的主要类集成关系: (图片来自Netty权威指南,图中有一个画错的 ...
 - 【Netty】源码分析目录
		
前言 为方便系统的学习Netty,特整理文章目录如下. [Netty]第一个Netty应用 [Netty]Netty核心组件介绍 [Netty]Netty传输 [Netty]Netty之ByteBuf ...
 - netty UnpooledHeapByteBuf 源码分析
		
UnpooledHeapByteBuf 是基于堆内存进行内存分配的字节缓冲区,没有基于对象池技术实现,这意味着每次I/O的读写都会创建一个新的UnpooledHeapByteBuf,频繁进行大块内存的 ...
 - ThreadLocal部分源码分析和应用场景
		
结构演进 早起JDK版本中,ThreadLocal内部结构是一个Map,线程为key,线程在"线程本地变量"中绑定的值为Value.每一个ThreadLocal实例拥有一个Map实 ...
 - lesson2:java阻塞队列的demo及源码分析
		
本文向大家展示了java阻塞队列的使用场景.源码分析及特定场景下的使用方式.java的阻塞队列是jdk1.5之后在并发包中提供的一组队列,主要的使用场景是在需要使用生产者消费者模式时,用户不必再通过多 ...
 - NIO-EPollSelectorIpml源码分析
		
目录 NIO-EPollSelectorIpml源码分析 目录 前言 初始化EPollSelectorProvider 创建EPollSelectorImpl EPollSelectorImpl结构 ...
 - 【Netty源码分析】Reactor线程模型
		
1. 背景 1.1. Java线程模型的演进 1.1.1. 单线程 时间回到十几年前,那时主流的CPU都还是单核(除了商用高性能的小机),CPU的核心频率是机器最重要的指标之一. 在Java领域当时比 ...
 - Netty源码分析(前言, 概述及目录)
		
Netty源码分析(完整版) 前言 前段时间公司准备改造redis的客户端, 原生的客户端是阻塞式链接, 并且链接池初始化的链接数并不高, 高并发场景会有获取不到连接的尴尬, 所以考虑了用netty长 ...
 
随机推荐
- ChatTTS,语气韵律媲美真人的开源TTS模型,文字转语音界的新魁首,对标微软Azure-tts
			
前两天 2noise 团队开源了ChatTTS项目,并且释出了相关的音色模型权重,效果确实非常惊艳,让人一听难忘,即使摆在微软的商业级项目Azure-tts面前,也是毫不逊色的. ChatTTS是专门 ...
 - Vue3组件通信方式
			
Vue3组件通信方式 不管是vue2还是vue3,组件通信方式很重要,不管是项目还是面试都是经常用到的知识点. 比如:vue2组件通信方式 props:可以实现父子组件.子父组件.甚至兄弟组件通信 自 ...
 - 从数据库设计到性能调优,全面掌握openGemini应用开发最佳实践
			
本文分享自华为云社区<DTSE Tech Talk × openGemini :从数据库设计到性能调优,全面掌握openGemini应用开发最佳实践>,作者:华为云开源. 在本期<从 ...
 - Java中的空指针异常 java.lang.NullPointerException
			
空指针异常 属于运行错误,java.lang.NullPointerException 原因:当引用名称的值为null时,就不能方法某个对象中的属性或方法,如果非要访问则就出现空指针异常 解决办法:在 ...
 - echarts的示例跟做出来的不一样
			
先给大家看下我做出来的和echarts官网做出来的 代码什么的都是一模一样但是颜色不一样 它字的颜色和柱状图颜色还一样不知道是不是脑子有猫病~ 上面是我做的 下面是官网的 主要是代码都是一样 我又不 ...
 - docker registry 镜像源
			
修改文件 /etc/docker/daemon.json vi /etc/docker/daemon.json添加以下内容后,重启docker服务: { "registry-mirrors& ...
 - github fork后对上游仓库的做rebase
			
想对上游仓库做更新同步 先添加上游仓库 git remote add upstream https://github.com/原始作者/原始仓库.git 其中这里的upstream 是一个命名,和 o ...
 - 14-vertical-aligin
			
01 行盒的理解 作用: 将当前行里的所有内容包裹起来 <!DOCTYPE html> <html lang="en"> <head> < ...
 - 个人团队兼职开发app(社交,语聊1v1,视频直播)
			
如果您有意向创业,意向社交类产品,如语聊,及时通信,视频直播,1v1等,又苦无没有人力资源. 我们岁数都是30+,在互联网行业摸爬滚打十年有余. 后端,前端,客户端,运维,四个人. 我们共事很长一段时 ...
 - c/c++:带有返回类型的函数没有return语句会怎么样?
			
c/c++:带有返回类型的函数没有return语句会怎么样 背景 机器有时候启动的时候发现异常,跟踪了代码发现,有人在写一个int函数的时候,有一个分支没有return: 参考:https://www ...