connect和bind过程详解(二)

Netty 源码解析(九): connect 过程和 bind 过程分析 原创申明:本文由公众号【猿灯塔】原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇。 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty 源码解析(三): Netty的 Future 和 Promise Netty 源码解析(四): Netty 的 ChannelPipeline Netty 源码解析(五): Netty 的线程池分析 Netty 源码解析(六): Channel 的 r 阅读详情

connect和bind过程详解(一)
Connect与Bind对比总结

Netty Bind 过程详解(服务端启动)

一、Bind 完整调用链路

从服务端代码 serverBootstrap.bind(port) 开始,整个调用链路如下:

ServerBootstrap.bind(port)
  ↓
ServerBootstrap.bind(SocketAddress)
  ↓
AbstractBootstrap.doBind()  // 先 register,再 bind
  ↓
AbstractBootstrap.initAndRegister()  // 创建 Channel 并注册
  ↓
AbstractBootstrap.doBind0()  // 提交 bind 任务到 EventLoop
  ↓
AbstractChannel.bind()  // 交给 pipeline
  ↓
DefaultChannelPipeline.bind()  // 从 tail 开始
  ↓
TailContext.bind()
  ↓
AbstractChannelHandlerContext.findContextOutbound()  // 找下一个 Outbound handler
  ↓
AbstractChannelHandlerContext.invokeBind()  // 调用 handler 的 bind 方法
  ↓
HeadContext.bind()  // 最终到达 head
  ↓
AbstractChannel.AbstractUnsafe.bind()  // 调用 unsafe
  ↓
NioServerSocketChannel.doBind()  // 执行真正的绑定
  ↓
ServerSocketChannel.bind()  // JDK 底层绑定端口

二、详细代码流程解析

2.1 入口:ServerBootstrap.bind()

用户代码:

// 服务端启动代码
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 .option(ChannelOption.SO_BACKLOG, 128)
 .childHandler(new MyChannelInitializer());

// 【入口】绑定端口
ChannelFuture f = b.bind(7397).sync();

第一步:端口转换

// AbstractBootstrap
public ChannelFuture bind(int inetPort) {
    return bind(new InetSocketAddress(inetPort));
}

第二步:地址校验

// AbstractBootstrap
public ChannelFuture bind(SocketAddress localAddress) {
    validate();  // 校验配置
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    return doBind(localAddress);
}

2.2 核心方法:doBind()

这个方法做两件事:

  1. initAndRegister():创建 ServerSocketChannel 并注册到 Selector
  2. doBind0():绑定端口
// AbstractBootstrap
private ChannelFuture doBind(final SocketAddress localAddress) {
    // 【重要】先完成 Channel 的创建和注册
    // 这一步会:创建 NioServerSocketChannel、初始化 Pipeline、注册到 Selector
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        // 情况1:register 已经完成(同步完成)
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // 情况2:register 还未完成(异步完成)
        // 添加监听器,等 register 完成后再执行 bind
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    promise.setFailure(cause);
                } else {
                    promise.registered();
                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

关键点:

  • bind 必须在 register 之后执行
  • register 可能同步完成,也可能异步完成
  • 如果异步,通过监听器等待 register 完成

2.3 提交任务:doBind0()

这个方法将 bind 操作提交到 EventLoop 的任务队列中执行。

// AbstractBootstrap
private static void doBind0(
    final ChannelFuture regFuture,
    final Channel channel,
    final SocketAddress localAddress,
    final ChannelPromise promise) {

    // 【关键】将 bind 操作提交到 EventLoop 的任务队列
    // 这样可以确保 bind 在 EventLoop 线程中执行
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                // 调用 Channel 的 bind 方法
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

关键点:

  • bind 操作必须在 EventLoop 线程中执行
  • 通过 eventLoop().execute() 提交任务
  • 添加失败监听器,自动清理资源

2.4 交给 Pipeline:AbstractChannel.bind()

// AbstractChannel
@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // 将 bind 操作交给 pipeline 处理
    return pipeline.bind(localAddress, promise);
}

作用:将操作委托给 pipeline,利用责任链模式处理。


2.5 从 Tail 开始:DefaultChannelPipeline.bind()

// DefaultChannelPipeline
@Override
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // bind 是 Outbound 事件,从 tail 开始传播
    return tail.bind(localAddress, promise);
}

关键点:

  • Outbound 事件从 tail 开始,向 head 方向传播
  • 这样用户自定义的 OutboundHandler 可以先处理

2.6 查找下一个 Handler:AbstractChannelHandlerContext.bind()

// AbstractChannelHandlerContext
@Override
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    if (isNotValidPromise(promise, false)) {
        return promise;
    }

    // 【关键】从当前节点开始,向前查找下一个 Outbound 类型的 handler
    final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
    
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeBind(localAddress, promise);
    } else {
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeBind(localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

2.7 调用 Handler:AbstractChannelHandlerContext.invokeBind()

// AbstractChannelHandlerContext
private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
    if (invokeHandler()) {
        try {
            // 调用当前 handler 的 bind 方法
            ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } else {
        bind(localAddress, promise);
    }
}

2.8 到达 Head:HeadContext.bind()

经过 pipeline 的传播,最终会到达 head。

// HeadContext
@Override
public void bind(
    ChannelHandlerContext ctx,
    SocketAddress localAddress,
    ChannelPromise promise) {
    
    // head 负责调用 unsafe 执行真正的底层操作
    unsafe.bind(localAddress, promise);
}

2.9 Unsafe 处理:AbstractUnsafe.bind()

这是 bind 的核心逻辑所在。

// AbstractChannel.AbstractUnsafe
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    boolean wasActive = isActive();
    try {
        // 【核心】执行真正的 bind 操作
        doBind(localAddress);
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }

    // bind 成功后,如果 channel 变为 active 状态
    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                // 触发 channelActive 事件
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}

关键点:

  1. 执行 doBind():调用底层的绑定方法
  2. 触发 channelActive:绑定成功后,触发 channelActive 事件
  3. 完成 Promise:设置 Promise 为成功状态

2.10 底层绑定:NioServerSocketChannel.doBind()

这是真正执行 JDK 底层绑定的地方。

// NioServerSocketChannel
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
        // JDK 7+ 的绑定方式
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        // JDK 6 的绑定方式
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}

关键点:

  1. 调用 JDK 底层方法:ServerSocketChannel.bind(localAddress, backlog)
  2. backlog 参数:TCP 连接队列的大小(通过 SO_BACKLOG 选项设置)
  3. 同步操作:bind 是同步的,会立即完成

2.11 JDK 底层调用:ServerSocketChannel.bind()

// JDK ServerSocketChannel
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
    synchronized (lock) {
        // 检查状态
        if (!isOpen())
            throw new ClosedChannelException();
        if (isBound())
            throw new AlreadyBoundException();
        
        // 绑定端口
        InetSocketAddress isa = (local == null) ? 
            new InetSocketAddress(0) : Net.checkAddress(local);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null)
            sm.checkListen(isa.getPort());
        
        // 调用底层 native 方法绑定
        NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort());
        Net.bind(fd, isa.getAddress(), isa.getPort());
        Net.listen(fd, backlog < 1 ? 50 : backlog);
        
        synchronized (stateLock) {
            localAddress = Net.localAddress(fd);
        }
    }
    return this;
}

作用:

  • 调用操作系统的 bind 和 listen 系统调用
  • 绑定端口并开始监听连接请求

三、Bind 完成后的处理

3.1 触发 channelActive 事件

// AbstractUnsafe.bind() 中
if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
        @Override
        public void run() {
            pipeline.fireChannelActive();
        }
    });
}

3.2 开始监听 OP_ACCEPT

// DefaultChannelPipeline.HeadContext.channelActive()
@Override
public void channelActive(ChannelHandlerContext ctx) {
    // 向后传播 channelActive 事件
    ctx.fireChannelActive();
    
    // 开始读取(对于 ServerSocketChannel 就是监听 OP_ACCEPT)
    readIfIsAutoRead();
}

3.3 注册 OP_ACCEPT 事件

// AbstractNioChannel.doBeginRead()
@Override
protected void doBeginRead() throws Exception {
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    final int interestOps = selectionKey.interestOps();
    // 对于 NioServerSocketChannel,readInterestOp 是 OP_ACCEPT
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

3.4 NioEventLoop 监听 OP_ACCEPT

// NioEventLoop.run() 方法会不断循环
protected void run() {
    for (;;) {
        // 1. select 操作,等待事件就绪
        select(wakenUp.getAndSet(false));
        
        // 2. 处理就绪的事件(包括 OP_ACCEPT)
        processSelectedKeys();
        
        // 3. 处理任务队列
        runAllTasks();
    }
}

3.5 处理 OP_ACCEPT 事件

// NioEventLoop.processSelectedKey()
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
    // 调用 unsafe.read() 接受新连接
    unsafe.read();
}

3.6 接受新连接:NioMessageUnsafe.read()

// AbstractNioMessageChannel.NioMessageUnsafe
@Override
public void read() {
    assert eventLoop().inEventLoop();
    final ChannelConfig config = config();
    final ChannelPipeline pipeline = pipeline();
    
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.reset(config);

    boolean closed = false;
    Throwable exception = null;
    try {
        try {
            do {
                // 【核心】接受新连接,创建 NioSocketChannel
                int localRead = doReadMessages(readBuf);
                if (localRead == 0) {
                    break;
                }
                if (localRead < 0) {
                    closed = true;
                    break;
                }

                allocHandle.incMessagesRead(localRead);
            } while (allocHandle.continueReading());
        } catch (Throwable t) {
            exception = t;
        }

        int size = readBuf.size();
        for (int i = 0; i < size; i ++) {
            readPending = false;
            // 触发 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 {
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

四、完整流程图

用户代码: serverBootstrap.bind(7397)
         ↓
ServerBootstrap.bind(int)
         ↓
AbstractBootstrap.bind(SocketAddress)
         ↓
AbstractBootstrap.doBind()
         ├─→ initAndRegister() [创建 NioServerSocketChannel、注册到 Selector]
         └─→ doBind0()
                  ↓
         AbstractBootstrap.doBind0() [提交任务到 EventLoop]
                  ↓
         AbstractChannel.bind()
                  ↓
         DefaultChannelPipeline.bind() [从 tail 开始]
                  ↓
         TailContext.bind()
                  ↓
         findContextOutbound() [查找下一个 Outbound handler]
                  ↓
         自定义 OutboundHandler.bind() [如果有]
                  ↓
         HeadContext.bind()
                  ↓
         AbstractUnsafe.bind()
                  ↓
         NioServerSocketChannel.doBind()
                  ↓
         ServerSocketChannel.bind() [JDK 底层绑定端口]
                  ↓
         绑定成功
                  ↓
         触发 channelActive 事件
                  ↓
         注册 OP_ACCEPT 事件
                  ↓
         NioEventLoop 监听 OP_ACCEPT
                  ↓
         收到客户端连接请求
                  ↓
         doReadMessages() [接受连接,创建 NioSocketChannel]
                  ↓
         触发 channelRead 事件 [传递新连接]
                  ↓
         ServerBootstrapAcceptor 处理新连接
                  ↓
         将新连接注册到 workerGroup

五、关键点总结

5.1 Bind 的前置条件

  1. 必须先 register:ServerSocketChannel 必须先注册到 Selector,才能进行 bind
  2. 在 EventLoop 线程中执行:bind 操作必须在 EventLoop 线程中执行

5.2 Bind 的特点

对比项Bind(服务端)Connect(客户端)
Channel 类型NioServerSocketChannelNioSocketChannel
JDK 底层操作ServerSocketChannel.bind()SocketChannel.connect()
是否异步否(同步完成)是(可能异步)
监听的事件OP_ACCEPTOP_CONNECT → OP_READ
完成后的操作开始接受连接finishConnect()

5.3 Pipeline 的传播方向

  • Outbound 事件(bind、connect、write):tail → head
  • Inbound 事件(channelActive、channelRead):head → tail

5.4 Bind 后的事件流

  1. bind 完成 → 触发 channelActive 事件
  2. channelActive → 调用 readIfIsAutoRead()
  3. readIfIsAutoRead() → 注册 OP_ACCEPT 事件
  4. OP_ACCEPT 就绪 → 调用 unsafe.read()
  5. unsafe.read() → 调用 doReadMessages() 接受新连接
  6. doReadMessages() → 创建 NioSocketChannel
  7. 触发 channelRead → 传递新连接给 ServerBootstrapAcceptor
  8. ServerBootstrapAcceptor → 将新连接注册到 workerGroup

六、Bind 与 Connect 的对比

6.1 相同点

  1. 都必须先 register:Channel 必须先注册到 Selector
  2. 都在 EventLoop 线程中执行:保证线程安全
  3. 都通过 Pipeline 传播:从 tail 到 head
  4. 都是 Outbound 事件:用户主动发起的操作

6.2 不同点

  1. 同步 vs 异步:

    • bind 是同步的,立即完成
    • connect 可能是异步的,需要等待 OP_CONNECT 事件
  2. 监听的事件:

    • bind 后监听 OP_ACCEPT(接受新连接)
    • connect 后监听 OP_READ(读取数据)
  3. Channel 类型:

    • bind 使用 NioServerSocketChannel
    • connect 使用 NioSocketChannel
  4. 后续处理:

    • bind 后接受新连接,创建 NioSocketChannel
    • connect 后开始读写数据

七、ServerBootstrapAcceptor 详解

7.1 什么是 ServerBootstrapAcceptor?

ServerBootstrapAcceptor 是一个特殊的 ChannelInboundHandler,它的作用是:

  1. 接收 boss 线程接受的新连接(NioSocketChannel)
  2. 为新连接配置 childHandler 和 childOptions
  3. 将新连接注册到 workerGroup

7.2 ServerBootstrapAcceptor 的添加时机

在 ServerBootstrap.init(channel) 方法中添加:

// ServerBootstrap.init()
@Override
void init(Channel channel) throws Exception {
    // ...省略其他代码
    
    ChannelPipeline p = channel.pipeline();
    
    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
    
    // ...省略代码
    
    // 【关键】添加 ServerBootstrapAcceptor
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    pipeline.addLast(new ServerBootstrapAcceptor(
                        ch, currentChildGroup, currentChildHandler, 
                        currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
}

7.3 ServerBootstrapAcceptor 的工作流程

// ServerBootstrap.ServerBootstrapAcceptor
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
    
    private final EventLoopGroup childGroup;
    private final ChannelHandler childHandler;
    private final Entry<ChannelOption<?>, Object>[] childOptions;
    private final Entry<AttributeKey<?>, Object>[] childAttrs;

    ServerBootstrapAcceptor(
        final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler,
        Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
        this.childGroup = childGroup;
        this.childHandler = childHandler;
        this.childOptions = childOptions;
        this.childAttrs = childAttrs;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // msg 就是新接受的 NioSocketChannel
        final Channel child = (Channel) msg;

        // 1. 为新连接添加 childHandler
        child.pipeline().addLast(childHandler);

        // 2. 设置新连接的 options
        setChannelOptions(child, childOptions, logger);

        // 3. 设置新连接的 attributes
        for (Entry<AttributeKey<?>, Object> e: childAttrs) {
            child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }

        try {
            // 4. 【关键】将新连接注册到 workerGroup
            childGroup.register(child).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        forceClose(child, future.cause());
                    }
                }
            });
        } catch (Throwable t) {
            forceClose(child, t);
        }
    }
}

八、完整的服务端启动流程

1. 创建 bossGroup 和 workerGroup
         ↓
2. 创建 ServerBootstrap 并配置
         ↓
3. 调用 bind(port)
         ↓
4. initAndRegister()
   ├─ 创建 NioServerSocketChannel
   ├─ 初始化 Pipeline(添加 ServerBootstrapAcceptor)
   └─ 注册到 bossGroup 的某个 EventLoop
         ↓
5. doBind0()
   └─ 绑定端口
         ↓
6. 触发 channelActive
   └─ 注册 OP_ACCEPT 事件
         ↓
7. bossGroup 的 EventLoop 监听 OP_ACCEPT
         ↓
8. 客户端发起连接
         ↓
9. OP_ACCEPT 事件就绪
         ↓
10. doReadMessages()
    └─ 接受连接,创建 NioSocketChannel
         ↓
11. 触发 channelRead 事件
    └─ 传递新连接给 ServerBootstrapAcceptor
         ↓
12. ServerBootstrapAcceptor 处理
    ├─ 为新连接添加 childHandler
    ├─ 设置 childOptions 和 childAttrs
    └─ 将新连接注册到 workerGroup
         ↓
13. workerGroup 的 EventLoop 处理新连接的 IO

九、常见问题

Q1:为什么 bind 是同步的,而 connect 可能是异步的?

A:

  • Bind:只是在本地绑定端口,不涉及网络通信,操作系统可以立即完成
  • Connect:需要进行 TCP 三次握手,涉及网络传输,需要时间

Q2:SO_BACKLOG 参数的作用是什么?

A:

  • SO_BACKLOG 指定了 TCP 连接队列的大小
  • 当服务端来不及处理新连接时,新连接会暂存在这个队列中
  • 如果队列满了,新的连接请求会被拒绝

示例:

serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);

Q3:bossGroup 和 workerGroup 的区别是什么?

A:

  • bossGroup:负责接受新连接(处理 OP_ACCEPT 事件)
  • workerGroup:负责处理已建立连接的 IO 操作(处理 OP_READ、OP_WRITE 事件)

类比:

  • bossGroup 像餐厅的迎宾员,负责迎接客人
  • workerGroup 像餐厅的服务员,负责为客人提供服务

Q4:为什么 bossGroup 通常只需要 1 个线程?

A:

  • 一个服务端通常只监听一个端口
  • 接受连接的操作很快,一个线程足够
  • 真正耗时的 IO 操作由 workerGroup 处理

示例:

// bossGroup 只需要 1 个线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// workerGroup 根据 CPU 核心数设置
EventLoopGroup workerGroup = new NioEventLoopGroup();

十、总结

Bind 过程的核心要点:

  1. 调用链路清晰:从 ServerBootstrap 到 Pipeline 到 Unsafe 到 JDK
  2. 同步操作:bind 是同步的,会立即完成
  3. Pipeline 传播:所有操作都通过 Pipeline 传播(tail → head)
  4. 触发 channelActive:bind 成功后触发 channelActive,开始监听 OP_ACCEPT
  5. 接受新连接:通过 ServerBootstrapAcceptor 将新连接注册到 workerGroup
  6. 主从 Reactor 模式:bossGroup 负责接受连接,workerGroup 负责处理 IO

Netty Connect 与 Bind 对比总结

一、快速对比表

对比项Connect(客户端)Bind(服务端)
使用场景客户端连接服务端服务端绑定端口
Channel 类型NioSocketChannelNioServerSocketChannel
JDK 底层操作SocketChannel.connect()ServerSocketChannel.bind()
是否异步是(可能异步)否(同步完成)
返回值含义true=立即成功, false=正在进行无返回值(同步)
监听的事件OP_CONNECT → OP_READOP_ACCEPT
完成后的操作finishConnect() → 读写数据接受新连接
EventLoopGroup单个 workerGroupbossGroup + workerGroup
Pipeline 传播tail → head(Outbound)tail → head(Outbound)
超时处理有(可配置连接超时)无(立即完成)

二、调用链路对比

2.1 Connect 调用链路

Bootstrap.connect(host, port)
  ↓
doResolveAndConnect()
  ├─→ initAndRegister() [创建 NioSocketChannel]
  └─→ doResolveAndConnect0() [DNS 解析]
       ↓
  doConnect() [提交任务]
       ↓
  pipeline.connect() [tail → head]
       ↓
  unsafe.connect()
       ↓
  NioSocketChannel.doConnect()
       ↓
  SocketChannel.connect() [JDK]
       ↓
  ┌────────┴────────┐
  ↓                 ↓
立即成功      正在进行中
  ↓                 ↓
完成Promise    注册 OP_CONNECT
  ↓                 ↓
channelActive   等待事件就绪
  ↓                 ↓
注册 OP_READ    finishConnect()
                    ↓
               完成Promise
                    ↓
               channelActive
                    ↓
               注册 OP_READ

2.2 Bind 调用链路

ServerBootstrap.bind(port)
  ↓
doBind()
  ├─→ initAndRegister() [创建 NioServerSocketChannel]
  └─→ doBind0() [提交任务]
       ↓
  pipeline.bind() [tail → head]
       ↓
  unsafe.bind()
       ↓
  NioServerSocketChannel.doBind()
       ↓
  ServerSocketChannel.bind() [JDK]
       ↓
  绑定成功(同步)
       ↓
  完成Promise
       ↓
  channelActive
       ↓
  注册 OP_ACCEPT
       ↓
  等待客户端连接
       ↓
  OP_ACCEPT 就绪
       ↓
  doReadMessages() [接受连接]
       ↓
  创建 NioSocketChannel
       ↓
  channelRead 事件
       ↓
  ServerBootstrapAcceptor
       ↓
  注册到 workerGroup

三、相同点

3.1 都必须先 Register

无论是 connect 还是 bind,都必须先完成 register 操作:

// 都会先调用 initAndRegister()
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();

// 等待 register 完成后,再执行 connect 或 bind
if (regFuture.isDone()) {
    // register 已完成
    doXxx(channel, ...);
} else {
    // register 还未完成,添加监听器
    regFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) {
            doXxx(channel, ...);
        }
    });
}

3.2 都在 EventLoop 线程中执行

// Connect
channel.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        channel.connect(remoteAddress, connectPromise);
    }
});

// Bind
channel.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        channel.bind(localAddress, promise);
    }
});

3.3 都通过 Pipeline 传播(Outbound 事件)

// 都是从 tail 开始,向 head 传播
pipeline.connect(remoteAddress, promise);  // Connect
pipeline.bind(localAddress, promise);      // Bind

// 传播路径:tail → 自定义 OutboundHandler → head

3.4 都由 Head 调用 Unsafe

// HeadContext
@Override
public void connect(...) {
    unsafe.connect(remoteAddress, localAddress, promise);
}

@Override
public void bind(...) {
    unsafe.bind(localAddress, promise);
}

3.5 都会触发 channelActive 事件

// 完成后都会触发 channelActive
if (!wasActive && isActive()) {
    pipeline.fireChannelActive();
}

四、不同点详解

4.1 同步 vs 异步

Connect(异步):

// 调用 JDK 的 connect,可能返回 false
boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);

if (!connected) {
    // 连接正在进行中,注册 OP_CONNECT 事件
    selectionKey().interestOps(SelectionKey.OP_CONNECT);
    // 等待 NioEventLoop 监听到 OP_CONNECT 事件
    // 然后调用 finishConnect() 完成连接
}

Bind(同步):

// 调用 JDK 的 bind,立即完成
javaChannel().bind(localAddress, config.getBacklog());
// 没有返回值,绑定立即完成

4.2 监听的事件不同

Connect 后监听 OP_READ:

// 连接完成后,开始监听 OP_READ,准备读取数据
selectionKey().interestOps(SelectionKey.OP_READ);

Bind 后监听 OP_ACCEPT:

// 绑定完成后,开始监听 OP_ACCEPT,准备接受新连接
selectionKey().interestOps(SelectionKey.OP_ACCEPT);

4.3 后续处理不同

Connect 后的处理:

// 1. 连接完成
finishConnect();

// 2. 触发 channelActive
pipeline.fireChannelActive();

// 3. 开始读写数据
// 用户的 handler 可以开始发送和接收数据

Bind 后的处理:

// 1. 绑定完成
doBind(localAddress);

// 2. 触发 channelActive
pipeline.fireChannelActive();

// 3. 开始接受新连接
// 当有客户端连接时:
doReadMessages(readBuf);  // 接受连接,创建 NioSocketChannel
pipeline.fireChannelRead(child);  // 传递给 ServerBootstrapAcceptor
childGroup.register(child);  // 注册到 workerGroup

4.4 EventLoopGroup 的使用

Connect(客户端):

// 只需要一个 EventLoopGroup
EventLoopGroup workerGroup = new NioEventLoopGroup();

Bootstrap b = new Bootstrap();
b.group(workerGroup)  // 只设置一个 group
 .channel(NioSocketChannel.class);

Bind(服务端):

// 需要两个 EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);      // 接受连接
EventLoopGroup workerGroup = new NioEventLoopGroup();     // 处理 IO

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)  // 设置两个 group
 .channel(NioServerSocketChannel.class);

五、代码示例对比

5.1 客户端 Connect 示例

public class NettyClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.AUTO_READ, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ch.pipeline().addLast(new MyClientHandler());
                 }
             });
            
            // 【Connect】连接服务端
            ChannelFuture f = b.connect("localhost", 8080).sync();
            System.out.println("客户端连接成功");
            
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

5.2 服务端 Bind 示例

public class NettyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 128)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ch.pipeline().addLast(new MyServerHandler());
                 }
             });
            
            // 【Bind】绑定端口
            ChannelFuture f = b.bind(8080).sync();
            System.out.println("服务端启动成功,监听端口:8080");
            
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

六、常见问题对比

Q1:为什么 Connect 可能是异步的,而 Bind 是同步的?

A:

  • Connect:需要进行 TCP 三次握手,涉及网络传输,需要时间

    客户端 ---SYN---> 服务端
    客户端 <--SYN+ACK--- 服务端
    客户端 ---ACK---> 服务端
    
  • Bind:只是在本地绑定端口,不涉及网络通信,操作系统可以立即完成

Q2:为什么服务端需要两个 EventLoopGroup?

A:

  • bossGroup:专门负责接受新连接(处理 OP_ACCEPT)

    • 通常只需要 1 个线程
    • 接受连接的操作很快
  • workerGroup:负责处理已建立连接的 IO 操作

    • 需要多个线程(根据 CPU 核心数)
    • IO 操作比较耗时

类比:

  • bossGroup = 餐厅的迎宾员(迎接客人)
  • workerGroup = 餐厅的服务员(为客人服务)

Q3:Connect 超时了怎么办?

A:

Bootstrap b = new Bootstrap();
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);  // 设置连接超时 5 秒

ChannelFuture future = b.connect("localhost", 8080);
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture f) {
        if (!f.isSuccess()) {
            System.out.println("连接失败:" + f.cause());
            // 可以实现重试逻辑
        }
    }
});

Q4:Bind 失败了怎么办?

A:

try {
    ChannelFuture f = serverBootstrap.bind(8080).sync();
} catch (Exception e) {
    // 可能的原因:
    // 1. 端口已被占用
    // 2. 没有权限绑定该端口(如 80 端口需要 root 权限)
    // 3. 地址不可用
    
    System.out.println("绑定失败:" + e.getMessage());
    
    // 可以尝试绑定其他端口
    serverBootstrap.bind(8081).sync();
}

七、性能优化建议

7.1 客户端优化

Bootstrap b = new Bootstrap();
b.group(workerGroup)
 .channel(NioSocketChannel.class)
 // 1. 启用 TCP_NODELAY,禁用 Nagle 算法,减少延迟
 .option(ChannelOption.TCP_NODELAY, true)
 // 2. 启用 SO_KEEPALIVE,保持长连接
 .option(ChannelOption.SO_KEEPALIVE, true)
 // 3. 设置连接超时
 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
 // 4. 设置发送缓冲区大小
 .option(ChannelOption.SO_SNDBUF, 1024 * 64)
 // 5. 设置接收缓冲区大小
 .option(ChannelOption.SO_RCVBUF, 1024 * 64);

7.2 服务端优化

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 // 1. 设置 TCP 连接队列大小
 .option(ChannelOption.SO_BACKLOG, 1024)
 // 2. 启用地址重用(服务重启时快速绑定端口)
 .option(ChannelOption.SO_REUSEADDR, true)
 // 3. 为子 Channel 启用 TCP_NODELAY
 .childOption(ChannelOption.TCP_NODELAY, true)
 // 4. 为子 Channel 启用 SO_KEEPALIVE
 .childOption(ChannelOption.SO_KEEPALIVE, true)
 // 5. 设置子 Channel 的缓冲区大小
 .childOption(ChannelOption.SO_SNDBUF, 1024 * 64)
 .childOption(ChannelOption.SO_RCVBUF, 1024 * 64);

八、总结

8.1 Connect 的核心要点

  1. 异步操作:可能需要等待 OP_CONNECT 事件
  2. 单 EventLoopGroup:只需要 workerGroup
  3. 连接完成后:开始读写数据
  4. 超时处理:可配置连接超时时间
  5. 重试机制:需要用户自己实现

8.2 Bind 的核心要点

  1. 同步操作:立即完成,无需等待
  2. 双 EventLoopGroup:bossGroup + workerGroup
  3. 绑定完成后:开始接受新连接
  4. 主从 Reactor:bossGroup 接受连接,workerGroup 处理 IO
  5. ServerBootstrapAcceptor:负责将新连接注册到 workerGroup

8.3 共同点

  1. 都必须先 register
  2. 都在 EventLoop 线程中执行
  3. 都通过 Pipeline 传播(Outbound 事件)
  4. 都由 Head 调用 Unsafe
  5. 都会触发 channelActive 事件
c#中connect函数_(十一)socket、connect、bind函数详解 一、socket函数1、头文件:#include /* See NOTES */#include 2、函数原型:int socket(int domain, int type, int protocol);socket函数类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。dimain:域,网络域,网络地址范围(... 阅读详情

相关推荐

专治疑难系列 - 解决打印机凭证冲突问题

一、问题描述 二、解决方法 三、问题已解决

Passerby_Wang的博客 2万+

udp通讯中的connect()和bind()函数

udp是一个基于无连接的通讯协议,通讯基本模型如下: 可以看出,不论是在客户端还是服务器,connect()似乎用不上,bind()在客户端也用不上,但是事实并非如此。1. udp客户端使用connect()函数udp客户端建立了socket后可以直接调用sendto()函数向服务器发送数据,但是需要在sendto()函数的参数中指定目的地址/端口,但是可以调用connect()函数先指明目的地

echo_bright_的博客 5万+

Matlab+Matpower实战:手把手教你生成IEEE14节点FDIA攻击数据集(附完整代码)

本文提供了一份基于Matlab与Matpower工具构建IEEE14节点FDIA攻击数据集的完整实战指南。通过动态负荷数据模拟、潮流计算、状态估计及隐蔽攻击向量注入,手把手教你生成可用于电力系统网络安全研究的仿真数据集,并附有核心代码与调试技巧。

bean的博客 399

connect和bind过程详解(一)

connect和bind过程详解(一)

java_rutu的博客 615

论文学习:Towards Automated Infographic Design:Deep Learning-based Auto-generation of Extensible Timeline

title: “Towards Automated Infographic Design:Deep Learning-based Auto-generation of Extensible Timeline” tags: [“论文评述”, “报告”] date: 2019-08-22 author: 叶慧 mathjax: true 论文:Towards Automated Infograph...

xiaoyehahaha的博客 731

SpotBugs实战:10大类Bug检测规则详解与代码优化实例(附避坑清单)

本文深入解析SpotBugs在Java开发中的实战应用,详细讲解10大类Bug检测规则与代码优化实例,包括NPE防御、多线程安全、性能优化等关键问题。通过具体代码示例和避坑清单,帮助开发者提升代码质量,有效集成到CI/CD流程中。

weixin_42665255的博客 197

设置connect超时时间_Netty 源码解析(九): connect 过程和 bind 过程分析

原创申明:本文由公众号【猿灯塔】原创,转载请说明出处标注今天是猿灯塔“365篇原创计划”第九篇。接下来的时间灯塔君持续更新Netty系列一共九篇Netty 源码解析(一): 开始Netty 源码解析(二): Netty 的 ChannelNetty 源码解析(三): Netty的 Future 和 PromiseNetty 源码解析(四): Netty 的 ChannelPipelineNetty...

weixin_39955953的博客 287

jieba分词

jieba

sikh_0529的博客 1万+

使用bind()和connect()函数

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。 bind() 函数 bind() 函数的原型为: int bind(int sock, struct sockaddr *addr, sockl

芝麻软件工作室的专栏 1万+

运维系列【仅供参考】:RustDesk部署疑难解答

RustDesk部署疑难解答

weixin_54626591的博客 283

LDRA Testbed(TBrun)软件单元测试_实例讲解(对多次调用的函数打桩)

在之前的文章《LDRA Testbed(TBrun)软件单元测试_操作指南》中,已经讲解了如何使用TBrun进行软件单元测试的基本操作步骤。本篇文章将结合实例进一步讲解被测函数中包含多次调用其他函数时,如何对其打桩来实现测试目的(详细的操作流程本文不再重述,若有不清楚的,可以结合之前的操作指南一起来看)。本文以一个被测函数作为示例,讲解了如何使用Testbed(TBrun)对涉及到多次调用其他函数的被测函数进行单元测试。本文的示例可能比较简单,但是思路、方法和流程都是通用的,希望对大家有所帮助。

文章主要涉及嵌入式软件开发、测试、项目管理等,欢迎交流,共同进步。 1466

socket、listen、connect、bind、accept函数详解

socket函数 为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议 #include<sys/socket.h> int socket(int family,int type,int protocol) 返回成功则返回非负文件描述符,若出错则为-1 其中family参数指明协议族,它是某些常量AF_INET:IPv4、AF_INET:Ipv6等...

Patrick的博客 2133

2023年下半年网络工程师案例分析真题答案解析

R1] ip route-static 0.0.0.0 0.0.0.0 GigabitEthemet2/0/1 (3) track (4) R1 to R2 //配置R1的缺省路由,联动BFD使得R1到2的流量优先走R1->S1->R2链路,当此链路发生故障时,流量切换到R1->R3->R2链路上。当同一VLAN内也需要网络隔离时,我们可采用交换机的(2)功能,若在配置了二层隔离后,部分主机有互通的需求,则可在需求主机的网关上配置(3)功能。不需要,MAC地址是数据链路层封装以太帧的地址标识。

鹿鸣天涯 752

Netty+源码解析(九)_+connect+过程和+bind+过程分析

Netty 源码分析系列: Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty 源码解析(三): Netty 的 Future 和 Promise Netty 源码解析(四): Netty 的 ChannelPipeline Netty 源码解析(五): Netty 的线程池分析 Netty 源码解析(六): Channel 的 register 操作 Netty 源码解析(七): NioEventLoop 工作流程 Netty 源码解析(八): 回

不朽丶 296

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

socket学习一、socket、bind/connect、listen函数详解

Socket本意是插座,在计算机通信领域中被翻译为“套接字”,是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。通过Socket,两台计算机可以通过网络进行信息的传递。

个人工作中学到的专业技术与职业发展知识笔记 3682

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

socket、connect、bind、listen函数

如下给出了在一对 TCP 客户和服务器进程之间发生的一些典型事件的时间表。

YangLei253的博客 2163

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

UDP协议为什么要使用connect和bind

转载自https://www.cnblogs.com/kex1n/p/8342416.html UDP中使用bind和connect的作用 1:UDP中可以使用connect系统调用 2:UDP中connect操作与TCP中connect操作有着本质区别。 TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&por...

jtabibito's blog 1157

socket编程 -- socket、bind、accept、connect函数

socketsocket函数原型#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>int socket(int domain, int type, int protocol);参数说明 domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 AF_INET6 与上面

未来之歌 1万+

connect和bind

考虑以下情形:我们使用UDP写一个echo程序,kehu

GetnextWindow的专栏 2249

Linux深入理解TCP协议(connect、bind、listen、accept)及其源码

文章目录TCP建立连接的三次握手过程探究使用Linux Socket api建立TCP连接的过程bind,listenconnect从源码角度分析TCP三次握手的过程对tcp_v4_connect的部分源码分析 主要探讨的问题就是connect及bind、listen、accept背后的三次握手相关问题 TCP建立连接的三次握手过程 首先还是老生常谈的三次握手问题,还是照惯例先来再温习一遍 第一次握手:客户端尝试连接服务器,向服务器发送syn(全称是同步序列编号)报文,syn=i,客户端进入SYN_SE

“逛丢一只鞋”的博客 5245

套接字API connect 和 bind 的联系和区别

connect 和 bind 的联系和区别

弘毅_hao的博客 850

udp编程 bind,connect函数的使用

问题 udp是一个基于无连接的通讯协议,基本的通讯模型如下: 如基本模型所示,udp编程中几乎不怎么使用到bind和 connect函数,这两个函数在udp编程中起怎么样的作用?实际上以上基本模式是通用模型,在一些情况下性能不是最好,在特殊情况下可以采用bind,connect函数来提高程序性能。 bind函数使用 bind函数作用为套接字绑定本地地址和端口号。 服务器调用bind函数的作...

qazw9600的专栏 2325

【socket编程-06】使用bind()和connect()函数

  声明:本博所转《socket编程》系列教程来自C语言中文网,本文地址使用bind()和connect()函数,本文仅供学习交流,更多内容请支持原创! socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。 #...

学问思辨行 2003

Linux网络编程——socket、bind、listen、accpet、connect、read和write

socket网络编程,总体步骤清晰。服务端所需要用到socket()、bind()、listen()、accept();客户端所需要用到socket()、connect();两者数据传输均需要read()、write()。 bind()中绑定IP地址和端口号,需要使用到内核指定结构体,需要理解接受;connect()和bind()使用方法相同。网络编程需要知道大端和小端字节序的转换,以及网络字节序和主机字节序的获取。

Jackice_Lee的博客 2341

客户端调用bind的作用及UDP客户端调用connect的问题

转载自:http://blog.chinaunix.net/uid-23193900-id-3199141.html 原文地址:客户端调用bind的作用及UDP客户端调用connect的问题 作者:bedreaming 在水木上看到一个关于在客户端调用bind的讨论, http://www.newsmth.net/bbstcon.php?board=NetPRG&

juan380649731的专栏 3657
上一篇: connect和bind过程详解(一)
下一篇: Netty源码分析---Connect与Bind对比总结
不早睡不改名@
博客等级 码龄5年 536粉丝 306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值