Netty客户端
- 建立一次通信,必然有着两个对象。发送者和接收者
- 如我们使用微信、qq进行聊天的时候,就是一个通信的过程
- 当我们使用java NIO实现简单的通信功能时,也必然存在着发送端(Client)和接收端(Server)
- 接下来通过java NIO的实现,去逐步深入理解学习Netty的实现原理
Java BIO简单通信功能
- 服务端
public class Server{
// 首先定义一个 ServerSocket 对象
private ServerSocket server;
// 在构造方法中进行初始化
public Server(int port) {
// 传入需要监听的端口号
server = new ServerSocket(port);
}
public void listen() {
// 轮询
while(true) {
// server.accept() 是一个阻塞(同步方法),在没有请求接收的时候—阻塞
Socket socket = server.accept();
// 拿到请求之后,意味着建立了一个Sokdcet连接
// 拿到 client 发送的数据
InputStream is = socket.getInputStream();
byte[] buff = new byte[10];
// 把数据写入到buff数组中去
int length = is.read(buff);
if(length > 0) {
// 转化为 String 字符串
String msg = new String(buff, 0, length);
}
// 关闭连接
socket.close();
}
}
}
- 客户端
public class Client {
public static void main(String[] args) throws IOException {
// localhost:和谁进行通信 port:服务器端口
Socket client = new Socket("localhost", 8080);
// 客户端 输出 数据:创建输出流对象
OutputStream os = client.getOutputStream();
// 生成一个随机字符串
String uId = UUID.randomUUID().toString();
System.out.println("客户端准备开始发送数据:"+uId);
System.out.println("发送数据字节长度:"+uId.getBytes().length);
os.write(uId.getBytes());
os.close();
client.close();
}
}
- 通过简单的BIO实现,脑海中定下一个基调
- 客户端发送,服务端接收,通过IP和端口地址进行关联
- 不管Netty如何复杂,其思想是不变的
Netty是如何发起一次连接的
- 首先提出问题,假如使用Netty去发出一个请求
- 必然会和java的实现类似通过IP和端口进行连接,这点是躲不开的。
BootStrap来了
- 找到Netty中的BootStrap.java,偷来官方注释瞧一瞧
- 比较清晰明了,传达了两点
- 客户端通过Bootstrap使用Channel将会很简单
- 我们使用UDP协议的时候,可以使用bind()方法来进行绑定。使用TCP的时候,使用connect()方法进行绑定
详细注释版本代码
- 跟进connect方法,最终调用了Bootstrap的doResolveAndConnect方法
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;
}
}
- 这里面有几个关键点需要我们注意
- initAndRegister方法具体做了什么事情
- 客户端时如何建立连接的
深入理解initAndRegister方法
- 调用到AbstractBootstrap的 initAndRegister 方法
NioSocketChannel初始化
- 这里走到 ReflectiveChannelFactory 通过反射的方式创建 Channel 对象
- 创建Channel对象的类型 由 Bootstrap 的 channel() 方法指定,这里实际上调用到了 NioSocketChannel 的构造方法
- 通过 NioSocketChannel 方法的构造器,深入了解
- newSocket(privider) 方法通过 SelectorProvider 对象新建一个 SocketChannel 对象
- 通过层层调用到 AbstractNioChannel 的构造方法
- 这里parent属性传入为null
- 通过 ch 设置 SocketChannel 非阻塞
- 通过 readInterestOp 设置 SelectionKey.OP_READ 为初始状态
- 接着调用父类 AbstractChannel 的构造方法
- 这里把 Channel 对象设置为 null
- 为已经创建的 Channel 对象设置一个 唯一 id
- 对 Unsafe 对象进行初始化——其中封装了Java底层Socket的操作
- 对 Pipeline 对象进行初始化
- 至此 我们最开始设置的 NioSocketChannel 对象就初始化完成了
- 接下来是 初始化完成之后的 init(Channel) 方法
NioSocketChannel注册
- 完成初始化之后调用 init(channel) 方法
- 这里体现了 Pipeline对象 和 Channel 对象的关联
void init(Channel channel) throws Exception {
// 这里通过 Channel 对象获取已经初始化完成的 Pipeline 对象
ChannelPipeline p = channel.pipeline();
// 把 handler 添加到 Pipeline 对象中
p.addLast(config.handler());
// 这里获取一些底层配置,如 ChannelOption.SO_KEEPALIVE 表示是否开启TCP底层心跳机制,true为开启
final Map<ChannelOption<?>, Object> options = options0();
// 为每一个 channel 进行配置
synchronized (options) {
for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
try {
if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + channel, t);
}
}
}
// 提供自定义属性功能 存储在 attrs 中,通过 channel.attr(key)方法可以获取相对应属性
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
- 接下来会执行以下代码
ChannelFuture regFuture = config().group().register(channel);
- 调用链时序图
- 最终调用到 AbstractNioChannel 对象的 doRegister 方法
- 到这里完成了注册关联
- channel的注册过程可以总结为:和对应的EventLoop对象的Selector相关联
- 初始化注册完成之后,接下来就是客户端连接操作了
客户端连接实现
- 首先通过时序图观察一下客户端连接的调用过程,通过调用过程寻找关键代码
- 最终调用到 NioSocketChannel 方法完成连接
- javaChannel()方法获取java NIO SOcketChannel 完成连接