本篇文章讲解使用的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的,这个时候就要检查网络和程序情况了.
网友评论