文章目录
2021SC@SDUSC
本章将讲述基于Netty实现ServerCnxn服务器连接。使用Netty框架来高效处理服务器与客户端之间的通信。
源码分析
(1)属性,与NIO类似。NettyServerCnxn维护了服务器与客户端之间的通道缓冲、缓冲区以及会话等属性。
//获取基于Netty框架服务连接的日志
private static final Logger LOG = LoggerFactory.getLogger(NettyServerCnxn.class);
//连接通道,客户端与服务器连接通道
private final Channel channel;
//通道缓存
private CompositeByteBuf queuedBuffer;
private final AtomicBoolean throttled = new AtomicBoolean(false);
//字节缓冲区
private ByteBuffer bb;
//设置缓冲区为4个字节
private final ByteBuffer bbLen = ByteBuffer.allocate(4);
//客户端与服务器之间会话的ID
private long sessionId;
//客户端与服务器之间会话超时时间,当会话超时时,会自动释放资源结束会话
private int sessionTimeout;
private Certificate[] clientChain;
//关闭连接通道
private volatile boolean closingChannel;
//NettyServerCnxn工厂
private final NettyServerCnxnFactory factory;
//初始化标志
private boolean initialized;
public int readIssuedAfterReadComplete;
//握手状态,即连接状态默认为未连接
private volatile HandshakeState handshakeState = HandshakeState.NONE;
//连接状态分为未连接、开始连接、完成连接
public enum HandshakeState {
NONE,
STARTED,
FINISHED
}
(2)构造函数,与NIOServerCnxn区别在于Netty的构造函数需要用户进行登录验证。其余部位对NIOServerCnxn的部分重要属性进行赋值。
//参数需求存在连接通道,服务器以及已经初始化的NettyServerCnxn工厂
NettyServerCnxn(Channel channel, ZooKeeperServer zks, NettyServerCnxnFactory factory) {
super(zks);
this.channel = channel;
this.closingChannel = false;
this.factory = factory;
//用户登录
if (this.factory.login != null) {
this.zooKeeperSaslServer = new ZooKeeperSaslServer(factory.login);
}
//获取IP地址
InetAddress addr = ((InetSocketAddress) channel.remoteAddress()).getAddress();
//认证信息中添加IP地址
addAuthInfo(new Id("ip", addr.getHostAddress()));
}
(3)核心函数(一)——close(),关闭服务器与客户端之间的连接通道。
//关闭连接通道
public void close() {
//关闭连接通道标志为true
closingChannel = true;
//关闭会话的ID
LOG.debug("close called for session id: 0x{}", Long.toHexString(sessionId));
setStale();
// 关闭时始终取消注册连接,以防止在某些竞争条件下连接bean泄漏。
factory.unregisterConnection(this);
// 如果这连接通道不在cnxns中,那么它已经关闭了
if (!factory.cnxns.remove(this)) {
LOG.debug("cnxns size:{}", factory.cnxns.size());
if (channel.isOpen()) {
channel.close();
}
return;
}
LOG.debug("close in progress for session id: 0x{}", Long.toHexString(sessionId));
factory.removeCnxnFromSessionMap(this);
factory.removeCnxnFromIpMap(this, ((InetSocketAddress) channel.remoteAddress()).getAddress());
if (zkServer != null) {
zkServer.removeCnxn(this);
}
//如果连接通道没有关闭
if (channel.isOpen()) {
/**由于不检查通过对channel complete的写调用创建的未来,因此需要确保
* 在关闭通道之前,所有写入都已完成,否则我们将面临数据丢失的风险
*/
channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
future.channel().close().addListener(f -> releaseQueuedBuffer());
}
});
} else {
ServerMetrics.getMetrics().CONNECTION_DROP_COUNT.add(1);
channel.eventLoop().execute(this::releaseQueuedBuffer);
}
}
核心函数(二)——process(),处理被NettyServerCnxn监听的事件。首先创建ReplyHeader,然后再调用sendResponse来发送响应,最后调用close函数进行后续关闭处理。
public void process(WatchedEvent event) {
// 创建响应头
ReplyHeader h = new ReplyHeader(ClientCnxn.NOTIFICATION_XID, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(
LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x" + Long.toHexString(this.sessionId) + " through " + this);
}
//将WatchedEvent转换为可以通过线路发送的类型
WatcherEvent e = event.getWrapper();
try {
// 发送响应
sendResponse(h, e, "notification");
} catch (IOException e1) {
LOG.debug("Problem sending to {}", getRemoteSocketAddress(), e1);
close();
}
}
核心函数(三)——sendResponse(),给客户端发送响应。
public void sendResponse(ReplyHeader h, Record r, String tag,
String cacheKey, Stat stat, int opCode) throws IOException {
// cacheKey和stat用于缓存,而不是在这里实现。实现示例可以在NIOServerCnxn中找到。
if (closingChannel || !channel.isOpen()) {
return;
}
sendBuffer(serialize(h, r, tag, cacheKey, stat, opCode));
decrOutstandingAndCheckThrottle(h);
}
核心函数(四)——receiveMessage()。该函数用于接收ChannelBuffer中的数据,函数在while循环体中,当writerIndex大于readerIndex(表示ChannelBuffer中还有可读内容)且throttled为false时执行while循环体,该函数大致可以分为两部分:
首先是当bb不为空时,表示已经准备好读取ChannelBuffer中的内容。其中主要的部分是判断bb的剩余空间是否大于message中的内容,简单而言,就是判断bb是否还有足够空间存储message内容,然后设置bb的limit,之后将message内容读入bb缓冲中,之后再次确定时候已经读完message内容,统计接收信息,再根据是否已经初始化来处理包或者是连接请求,其中的请求内容都存储在bb中。
当bb为空时,表示还没有给bb分配足够的内存空间来读取message,首先还是将message内容(后续内容的长度)读入bbLen中,然后再确定读入的内容代表后续真正内容的长度len,然后再根据len来为bb分配存储空间,方便后续读取真正的内容。
private void receiveMessage(ByteBuf message) {
//检查消息是否在实践队列中
checkIsInEventLoop("receiveMessage");
try {
// 当writerIndex > readerIndex,并且不节流时,满足条件
while (message.isReadable() && !throttled.get()) {
// bb不为空
if (bb != null) {
if (LOG.isTraceEnabled()) {
LOG.trace("message readable {} bb len {} {}", message.readableBytes(), bb.remaining(), bb);
ByteBuffer dat = bb.duplicate();
dat.flip();
LOG.trace("0x{} bb {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
}
// bb剩余空间大于message中可读字节大小
if (bb.remaining() > message.readableBytes()) {
// 确定新的limit
int newLimit = bb.position() + message.readableBytes();
bb.limit(newLimit);
}
// 将message写入bb中
message.readBytes(bb);
// 重置bb的limit
bb.limit(bb.capacity());
if (LOG.isTraceEnabled()) {
LOG.trace("after readBytes message readable {} bb len {} {}", message.readableBytes(), bb.remaining(), bb);
ByteBuffer dat = bb.duplicate();
dat.flip();
LOG.trace("after readbytes 0x{} bb {}",
Long.toHexString(sessionId),
ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
}
// 已经读完message,表示内容已经全部接收
if (bb.remaining() == 0) {
// 翻转,可读
bb.flip();
// 统计接收信息
packetReceived(4 + bb.remaining());
ZooKeeperServer zks = this.zkServer;
// Zookeeper服务器为空
if (zks == null || !zks.isRunning()) {
throw new IOException("ZK down");
}
if (initialized) {// 未被初始化
// TODO: if zks.processPacket() is changed to take a ByteBuffer[],
// we could implement zero-copy queueing.
// 处理bb中包含的包信息
zks.processPacket(this, bb);
} else {// 已经初始化
LOG.debug("got conn req request from {}", getRemoteSocketAddress());
// 处理连接请求
zks.processConnectRequest(this, bb);
initialized = true;
}
bb = null;
}
} else {// bb为null
if (LOG.isTraceEnabled()) {
LOG.trace("message readable {} bblenrem {}", message.readableBytes(), bbLen.remaining());
// 复制bbLen缓冲
ByteBuffer dat = bbLen.duplicate();
// 翻转
dat.flip();
LOG.trace("0x{} bbLen {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
}
// bb剩余空间大于message中可读字节大小
if (message.readableBytes() < bbLen.remaining()) {
// 重设bbLen的limit
bbLen.limit(bbLen.position() + message.readableBytes());
}
// 将message内容写入bbLen中
message.readBytes(bbLen);
// 重置bbLen的limit
bbLen.limit(bbLen.capacity());
// 已经读完message,表示内容已经全部接收
if (bbLen.remaining() == 0) {
// 翻转
bbLen.flip();
if (LOG.isTraceEnabled()) {
LOG.trace("0x{} bbLen {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(bbLen)));
}
// 读取position后四个字节
int len = bbLen.getInt();
if (LOG.isTraceEnabled()) {
LOG.trace("0x{} bbLen len is {}", Long.toHexString(sessionId), len);
}
// 清除缓存
bbLen.clear();
if (!initialized) {// 未被初始化
if (checkFourLetterWord(channel, message, len)) {// 是否是四个字母的命令
return;
}
}
if (len < 0 || len > BinaryInputArchive.maxBuffer) {
throw new IOException("Len error " + len);
}
ZooKeeperServer zks = this.zkServer;
if (zks == null || !zks.isRunning()) {
throw new IOException("ZK down");
}
// 根据len重新分配缓冲,以便接收内容
zks.checkRequestSizeWhenReceivingMessage(len);
bb = ByteBuffer.allocate(len);
}
}
}
} catch (IOException e) {
LOG.warn("Closing connection to {}", getRemoteSocketAddress(), e);
close(DisconnectReason.IO_EXCEPTION);
} catch (ClientCnxnLimitException e) {
// Common case exception, print at debug level
ServerMetrics.getMetrics().CONNECTION_REJECTED.add(1);
LOG.debug("Closing connection to {}", getRemoteSocketAddress(), e);
close(DisconnectReason.CLIENT_RATE_LIMIT);
}
}
总结
ZooKeeper中网络通信不仅有基于NIO完成服务端与客户端之间的通信,也有基于Netty完成服务端与客户端之间的通信。本章讲述了基于Netty的通信,其通信效率相对于ZooKeeper默认的NIO的通信来说是高效的。