美文网首页
Netty中客户端如何连接服务端

Netty中客户端如何连接服务端

作者: 赵信信官属 | 来源:发表于2020-12-24 00:57 被阅读0次

本篇文章讲解使用的Netty版本

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.43.Final</version>
</dependency>

使用Netty构建一个客户端,那么它是如何连接服务端的呢?以下是客户端代码


import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

public class Client {

    public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        EventLoopGroup businessGroup = new NioEventLoopGroup(8);
        Bootstrap bootstrap = new Bootstrap();

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        ChannelPipeline channelPipeline = ch.pipeline();
                        channelPipeline.addLast(new StringDecoder());
                        channelPipeline.addLast(new StringEncoder());
                        channelPipeline.addLast(businessGroup, new ClientHandler());
                    }
                });

        bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.option(ChannelOption.TCP_NODELAY, true);

        // 连接服务端
        bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080));

    }
}

分析就从最后一行connect方法开始. 分析过程中会突出主线,忽略次要的内容.​

首先明确客户端主线流程
1.创建Channel
2.初始化Channel
3.注册Channel
4.连接服务端

服务端主线流程: 1.创建Channel 2.初始化Channel 3.注册Channel 4.绑定端口

public ChannelFuture connect(SocketAddress remoteAddress) {
    return doResolveAndConnect(remoteAddress, config.localAddress());
}
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    // 创建 初始化 注册
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();

    if (regFuture.isDone()) {
        if (!regFuture.isSuccess()) {
            return regFuture;
        }
        return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
    } else {
        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();
                    // 连接
                    doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                }
            }
        });
        return promise;
    }
}
final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 创建
        channel = channelFactory.newChannel();
        // 初始化
        init(channel);
    } catch (Throwable t) {
        if (channel != null) {
            channel.unsafe().closeForcibly();
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
    }

    // 注册
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }

    return regFuture;
}

创建NioSocketChannel. 在创建Channel的同时还会创建与之关联的Unsafe, DefaultChannelPipeline, NioSocketChannelConfig.

初始化Channel. 主要是设置Channel的Options和Attributes.

注册Channel. Netty的NioSocketChannel会注册到Netty的NioEventLoop上. 具体底层的JDKchannel注册到Selector是以任务的形式提交到NioEventLoop.

需要注意的是,从创建NioEventChannel->初始化Channel->注册Channel 一直都是同一个线程(记作线程A)在执行.因为执行注册和连接的操作必须由NioEventLoop对应的IO线程才能执行.因此线程A只能将注册和连接操作以任务的形式提交到NioEventLoop.

真正的注册操作是由IO线程来完成.

图片.png

连接服务端


图片.png

因为连接服务端要进行三次握手,是一个耗时操作.连接操作返回的是一个false.因此需要向Channel设置一个感兴趣的CONNECT连接事件. 当三次握手完成, 客户端感知到了连接已经成功建立.(NioEventLoop对应的IO线程会轮询IO事件,包括CONNECT连接完成事件)

图片.png

客户端连接服务端大体流程就是上面描述的情况.

也就是说,客户端已经有一个通道可以和服务端进行通信了.彼此可以互相发送数据了.

看过之前服务端文章的同学应该知道, 服务端监听到由客户端连接的时候,会接收连接,封装JDKchannel并创建一个Netty的NioSocketChannel. 然后会将这个Channel注册到一个NioEventLoop上. 之后服务端的这个NioEventLoop对应的IO线程会读写这个Channel上的数据. 下面我们做个实验, 客户端在成功连接服务端之后, 这个时候客户端是可以向服务端写数据了的,毕竟三次握手完成,连接成功建立. 但是呢, 在服务端, 将Channel注册到NioEventLoop的时候, 通过Debug的方式,让它'暂停'下来,我们观察下现象

图片.png
// 代码位置
io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead

客户端向服务端发送数据,然后观察服务端的网络情况


图片.png

会发现服务端有9个字符没有读取.当然这个数字9并不是重点,因为客户端就发送了9个字符,主要是服务端有字符没有读取. 就是因为客户端向通道中写了数据, 但是此时由于服务端的Channel还没有注册到NioEventLoop上,因此服务端的IO线程无法轮询到这个Channel,自然也就不会读取到Channel中的数据.

这里只是一个模拟实验, 实际场景中, 如果读写很慢, 可能就会出现Recv-Q和Send-Q上显示的数字都是大于0的,这个时候就要检查网络和程序情况了.

相关文章

网友评论

      本文标题:Netty中客户端如何连接服务端

      本文链接:https://www.haomeiwen.com/subject/qbxpnktx.html