前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty中客户端如何连接服务端

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

作者头像
书唐瑞
发布2022-06-02 14:05:30
1.3K0
发布2022-06-02 14:05:30
举报
文章被收录于专栏:Netty历险记

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

代码语言:javascript
复制
<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.43.Final</version>
</dependency>

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

代码语言:javascript
复制

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.连接服务端

代码语言:javascript
复制
服务端主线流程: 1.创建Channel   2.初始化Channel   3.注册Channel    4.绑定端口
代码语言:javascript
复制
public ChannelFuture connect(SocketAddress remoteAddress) {
  return doResolveAndConnect(remoteAddress, config.localAddress());
}
代码语言:javascript
复制
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;
  }
}
代码语言:javascript
复制
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线程来完成.

连接服务端

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

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

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

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

代码语言:javascript
复制
// 代码位置
io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Netty历险记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档