エンコーダの実装のタイミング
まず第一に、我々は通常、我々が呼ぶ、サーバを介してデータを送信するために、クライアントに、考えてctx.writeAndFlush(数据)
も対象となる場合があり、基本的なデータタイプかもしれ基準位置への道データを
第二に、エンコーダは、データの基礎となる私たちのメッセージが実際に書き込まれたByteBufferのJDKは、符号化プロセスを通過する必要がある前に、それがエンコードされた送信することを意味するものではありませんハンドラに属しているが、彼は役割をコーディング専門のハンドラに専用されていますクライアントが文字化けを受けることができない、出て行くが、エンコードされていません
その後、我々がいることを知っているctx.writeAndFlush(数据)
私たちは、ヘッダに追加した前に、それは実際に発信プロセッサ固有の動作であるため、それがパイプラインに渡す必要が運命に、最初から?テール・ノードからの転送を行い、普及している自定义的解码器
中で
WriteAndFlush()
ロジック
私たちは、ソース従うWriteAndFlush()
に対するWrite()
そのフラッシュフィールドが真であります
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
//todo 因为flush 为 true
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
だから、これは意志
- ハンドラを呼び出すことによって一つ
write()
- ハンドラを呼び出すことによって一つ
flush()
以下に、私がここに書いた両方の電波伝搬波書き込み、波フラッシュ、イベントの一般的な流れ、私たちが知っていることを意味し、知ることが重要である、イベントを広げる二つの波に分かれています
書きます
- DirctBufferに変換ByteBuf
- 書き込みキュー・エントリにカプセル化されたメッセージ(DirctBuffer)が挿入されています
- 書き込み状態を設定します。
流す
- リフレッシュフラグは、ステータスを書き込むように設定されています
- 可変バッファキュー、フィルタバッファ
- JDK根本的なAPIを呼び出し、ByteBufはネイティブJDKを書きます
ByteBuffer
単純なカスタムエンコーダ
/**
* @Author: Changwu
* @Date: 2019/7/21 20:49
*/
public class MyPersonEncoder extends MessageToByteEncoder<PersonProtocol> {
// todo write动作会传播到 MyPersonEncoder的write方法, 但是我们没有重写, 于是就执行 父类 MessageToByteEncoder的write, 我们进去看
@Override
protected void encode(ChannelHandlerContext ctx, PersonProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyPersonEncoder....");
// 消息头 长度
out.writeInt(msg.getLength());
// 消息体
out.writeBytes(msg.getContent());
}
}
継承することを選択しMessageToByteEncoder<T>
たバイトのメッセージにエンコーダから
フォローアップを続行
[OK]を、今、私たちは私たちのカスタムデコーダに来てMyPersonEncoder
、
しかし、私が普及されて表示されませんでしたwriteAndFlush()
、それは、私たち自身の継承されたデコーダは関係ありませんMessageToByteEncoder
、親クラスが実装writeAndFlush()
裏に書かれた構文解析したソースコードを次のように、ソースコードを
// todo 看他的write方法
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {// todo 1 判断当前是否可以处理这个对象
@SuppressWarnings("unchecked")
I cast = (I) msg;
// todo 2 内存分配
buf = allocateBuffer(ctx, cast, preferDirect);
try {
// todo 3 调用本类的encode(), 这个方法就是我们自己实现的方法
encode(ctx, cast, buf);
} finally {
// todo 4 释放
ReferenceCountUtil.release(cast);
}
if (buf.isReadable()) {
// todo 5. 往前传递
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
// todo 释放
buf.release();
}
}
- 私たちは、ByteBufにカプセル化されたメッセージmsgを送信します
- コード:実行
encode()
私たちのカスタムエンコーダによって実装抽象メソッドである方法、- 私たちの実装は、以下のデータに二度書かれていたBUF非常に簡単です
- メッセージのint型の長さ
- メッセージ本文
- 私たちの実装は、以下のデータに二度書かれていたBUF非常に簡単です
- MSGリリース
- 以降の転送を続行
write()
するイベントを - 最終的には、リリースを作成するには、まずByteBuf
概要
この時点までは、エンコーダの実装プロセスが完了している、我々はアーキテクチャとロジックデコーダが似ていることがわかります、私たちのためのテンプレートのデザインパターンと同様に、ちょうどフィル・インを作りました
実際には、トップへの最終段階释放第一步创建的ByteBuf
のメッセージはそれを行う方法を、基礎となるByteBufferのJDKに書かれている前?それが提供し続けるために一歩前進だ忘れてはいけないwrite()
イベントを、次に移動し、実際にありHeaderContext
、かつHeaderContext
直接的に関連しますクラスは、我々はすべて知っているように、それは、驚くべきことではない、安全でないクライアントやサーバーの基礎となる読み取りと書き込みの両方のデータチャネルでネッティー、危険なに依存しています
以下の分析では、始まった
WriteAndFlush()
二つの波のタスクレベルの詳細を
第1の波長イベント配信 write()
私たちは、HenderContextに従ってくださいwrite()
、そして依存でHenderContextはあるunsafe.wirte()
にそう直接AbstractChannel
、次のように安全でないソースコード:
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) { // todo 缓存 写进来的 buffer
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
// todo buffer Dirct化 , (我们查看 AbstractNioByteBuf的实现)
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
// todo 插入写队列 将 msg 插入到 outboundBuffer
// todo outboundBuffer 这个对象是 ChannelOutBoundBuf类型的,它的作用就是起到一个容器的作用
// todo 下面看, 是如何将 msg 添加进 ChannelOutBoundBuf中的
outboundBuffer.addMessage(msg, size, promise);
}
メッセージの位置パラメータ、デコーダ経由のスーパー、当社のカスタムパッケージであるByteBuf
メッセージのタイプ
この方法では、3つのことを行うために主です
- まず:
filterOutboundMessage(msg);
変換ByteBufDirctByteBuf
我々は彼の実現を確認するために入ったときは、アイデアはそのサブクラスがそれを書き換え、このメソッドをオーバーライド要求されます?ですAbstractNioByteChannel
。このクライアントキャンプのクラスであり、クラス、およびサーバ・AbstractNioMessageChannel
パー
ソースは以下のとおりです。
protected final Object filterOutboundMessage(Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (buf.isDirect()) {
return msg;
}
return newDirectBuffer(buf);
}
if (msg instanceof FileRegion) {
return msg;
}
throw new UnsupportedOperationException(
"unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
}
- 二つ目:変換
DirectBuffer
書き込みキューに挿入
書き込みキューは何ですか?役割は何ですか?
それは私たちがこのコンテナを必要とする理由、それ?サーバは、クライアント、メッセージがにカプセル化されるにメッセージを送信する必要があることを思い出して、実際にコンテナネッティーカスタム、片方向リンクリスト構造を使用しているByteBuf
が、その後、クライアントへの書き込み2つの方法があります
- 書きます()
- writeAndFlush()
このアプローチに差があるが、前者が書かれた、リフレッシュする内容ではなかった(ByteBufが書いた)ByteBuffer
、それをさらにJDKのネイティブを書くための方法はありません、キャッシュを更新するのではなく、ByteBuffer
一方で、writeAndFlush()
それはより便利ですMSG最初の書き込みはByteBuf
、その後、ソケットに直接奪うのセットを磨くため、ノックの後
しかし、クライアントが使用していない発生した場合writeAndFlush()
、および元の使用、その後、メッセージは咲くByteBuf
か?やる危険はそれがクライアントに書いて置くことができない、捨てないでください、ハンドラの最初の場所に配信されますか!
したがって、この問題を解決するためにキューを作成し、それを示していますデータ構造として、上から新しいコミュニケーションByteBuf
彼が使用されているノード、このリストを、区別するために、メンテナンスのために一つのノード(エントリ)にパッケージ化され、どのノードが使用されていない以下のように、彼は、ラベルされた3つのポインタで標識されます。
- エントリをオフにフラッシュさflushedEntry
- tailEntryのテール・ノード
- エントリを磨くないunflushedEntry
ここでは、それが新しいノードが書き込みキューに追加されたものになるかどうかを見て
addMessage(Object msg, int size, ChannelPromise promise)
書き込みキューを追加します。
public void addMessage(Object msg, int size, ChannelPromise promise) {
// todo 将上面的三者封装成实体
// todo 调用工厂方法, 创建 Entry , 在 当前的ChannelOutboundBuffer 中每一个单位都是一个 Entry, 用它进一步包装 msg
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
// todo 调整三个指针, 去上面查看这三个指针的定义
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
if (unflushedEntry == null) {
unflushedEntry = entry;
}
// increment pending bytes after adding message to the unflushed arrays.
// See https://github.com/netty/netty/issues/1619
// todo 跟进这个方法
incrementPendingOutboundBytes(entry.pendingSize, false);
}
実際には、単純なリスト操作は、テール挿入方法の挿入のために行われる最終位置に挿入された、リストの先頭がマークされ、彼のソースコードを参照してunflushedEntry
2つのノード間のエントリを、ノードがフラッシュによって表すことができます。
それぞれが新しいノードが呼び出しを追加した後incrementPendingOutboundBytes(entry.pendingSize, false)
の方法を、このメソッドの役割は状態を設定する方法、書き込み状態を設定するのですか?私たちは、そのソースコードを見て、あなたが見ることができる、それが累積記録されますByteBuf
一度しきい値を超えて、容量をイベントを書き込むことができないチャンネルが広がっていきます
- これは、
write()
3つ目
private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if (size == 0) {
return;
}
// todo TOTAL_PENDING_SIZE_UPDATER 当前缓存中 存在的代写的 字节
// todo 累加
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
// todo 判断 新的将被写的 buffer的容量不能超过 getWriteBufferHighWaterMark() 默认是 64*1024 64字节
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
// todo 超过64 字节,进入这个方法
setUnwritable(invokeLater);
}
}
要約:
これまでのところ、最初の波write()
イベントが完了している、我々が見ることができ、このイベントの機能は使用することですChannelOutBoundBuf
過去に、単一のイベントの伝播を書くためにByteBuf
フラッシュイベントの普及を待って、それらを維持するために、
第2の波長イベント配信 flush()
背中、私たちAbstractChannel
は次のよう展開状態では、彼の第二波のフラッシュイベントソースを表示するには:それは、次の3つのことを行うために主です
- リフレッシュフラグは、ステータスを書き込むように設定されている追加
- バッファキュートラバーサル、フィルタリングは、バッファをフラッシュすることができます
- JDK根本的なAPIを呼び出し、スピン書き込みました
// todo 最终传递到 这里
@Override
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
// todo 添加刷新标志, 设置写状态
outboundBuffer.addFlush();
// todo 遍历buffer队列, 过滤byteBuf
flush0();
}
リフレッシュフラグは、ステータスを書き込むように設定されている追加
追加リフレッシュがそれに署名されて何?実際には、リスト内のカーソル位置を変更することです、それは間の3つのポインタ完成することができentry
、一度フラッシュおよび非フラッシュノード以上の分割を
[OK]を、継続
状態を設定する方法で、以下の表情は、addflush()のソースコードは次のように:
* todo 给 ChannelOutboundBuffer 添加缓存, 这意味着, 原来添加进 ChannelOutboundBuffer 中的所有 Entry, 全部会被标记为 flushed 过
*/
public void addFlush() {
// todo 默认让 entry 指向了 unflushedEntry ==> 其实链表中的最左边的 未被使用过的 entry
// todo
Entry entry = unflushedEntry;
if (entry != null) {
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
do {
flushed ++;
if (!entry.promise.setUncancellable()) {
// Was cancelled so make sure we free up memory and notify about the freed bytes
int pending = entry.cancel();
// todo 跟进这个方法
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null);
// All flushed so reset unflushedEntry
unflushedEntry = null;
}
}
目標は、各ノードの状態を変更するためのポインタを移動することで、ポインタ?はいれflushedEntry
、それはノードを読み取るためにポイントは、その左側に、すなわち、面一になっている、を介して処理されます
次のコードは、開始位置が選択され、flushedEntryは== nullの場合、何の説明は、ノードを介してフラッシュされていなかった場合は、位置が左に配置されますので、始めに開始、
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
DO-whileループ、最後はいるが続くflushedEntry
尾に場所、各ノードを1つずつ、これらのノードがキャッシュに面一にしているので、我々はソースとして、書き込み時に失うする能力を蓄積する必要があります
private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
if (size == 0) {
return;
}
// todo 每次 减去 -size
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
// todo 默认 getWriteBufferLowWaterMark() -32kb
// todo newWriteBufferSize<32 就把不可写状态改为可写状态
if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
setWritable(invokeLater);
}
}
32キロバイト未満イベントチャネルを書き込み可能に広がっていく場合にも、原子クラスは、容量が減少した後、加えて、これを使用してください
バッファキュートラバーサル、フィルタリングbyteBuf
これは、ソケットにデータを書き込むための動作を実装してフラッシュのハイライトであり、
私たちは、そのソースコードをフォローアップdoWrite(ChannelOutboundBuffer in)
、この一種であるAbstractChannel
具体的なチャンネルの書き込みに抽象化するために設計されて、このような論理的なアプローチを書くように抽象メソッド、の、および実装依存、我々はその実装を作成するために、クライアントに提示したいと思います市はAbstractNioByteChannel
次のように、私たちはその実装を入力して、ソースコードがあります
boolean setOpWrite = false;
// todo 整体是无限循环, 过滤ByteBuf
for (;;) {
// todo 获取第一个 flushedEntity, 这个entity中 有我们需要的 byteBuf
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
}
if (msg instanceof ByteBuf) {
// todo 第三部分,jdk底层, 进行自旋的写
ByteBuf buf = (ByteBuf) msg;
int readableBytes = buf.readableBytes();
if (readableBytes == 0) {
// todo 当前的 ByteBuf 中,没有可写的, 直接remove掉
in.remove();
continue;
}
boolean done = false;
long flushedAmount = 0;
if (writeSpinCount == -1) {
// todo 获取自旋锁, netty使用它进行
writeSpinCount = config().getWriteSpinCount();
}
// todo 这个for循环是在自旋尝试往 jdk底层的 ByteBuf写入数据
for (int i = writeSpinCount - 1; i >= 0; i --) {
// todo 把 对应的 buf , 写到socket中
// todo localFlushedAmount就是 本次 往jdk底层的 ByteBuffer 中写入了多少字节
int localFlushedAmount = doWriteBytes(buf);
if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}
// todo 累加一共写了多少字节
flushedAmount += localFlushedAmount;
// todo 如果buf中的数据全部写完了, 设置完成的状态, 退出循环
if (!buf.isReadable()) {
done = true;
break;
}
}
in.progress(flushedAmount);
// todo 自旋结束,写完了 done = true
if (done) {
// todo 跟进去
in.remove();
} else {
// Break the loop and so incompleteWrite(...) is called.
break;
}
....
コードのこの作品は、次のようにその主なロジックがあり、非常に長いです。
無限ループすることで、あなたはすべてのノード上の保証を得ることができByteBuf
、ノードは、この機能を介して取得、Object msg = in.current();
それは私たちだけのラベルが付いたノードを削除しますと、私たちはさらに、その実装を見て
public Object current() {
Entry entry = flushedEntry;
if (entry == null) {
return null;
}
return entry.msg;
}
次に、関数を呼び出し、ByteBufferのJDK基本となる書き込みデータをしようとするJDKのスピンロックループ16を使用して、doWriteBytes(buf);
彼クラスの抽象メソッド、特定の実装があり、クライアントシャネルラッパークラスをNioSocketChannel
以下のように実装のソースコードは次のとおりです。
// todo
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
// todo 将字节数据, 写入到 java 原生的 channel中
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
これはreadBytes()
、我々は、フロントが置かれているので、まだ抽象メソッドでByteBuf
その実装クラスが変換されるように、DirctタイプになったPooledDirctByteBuf
最終的に親密なシーンを見て次のようにフォローアップします
// todo
@Override
public int readBytes(GatheringByteChannel out, int length) throws IOException {
checkReadableBytes(length);
//todo 关键的就是 getBytes() 跟进去
int readBytes = getBytes(readerIndex, out, length, true);
readerIndex += readBytes;
return readBytes;
}
跟进getBytes(){
index = idx(index);
// todo 将netty 的 ByteBuf 塞进 jdk的 ByteBuffer tmpBuf;
tmpBuf.clear().position(index).limit(index + length);
// todo 调用jdk的write()方法
return out.write(tmpBuf);
}
さらに、ノードは、ソースとしてだけでなく、リストの操作のために、()離れて削除を使用します
private void removeEntry(Entry e) {
if (-- flushed == 0) { // todo 如果是最后一个节点, 把所有的指针全部设为 null
// processed everything
flushedEntry = null;
if (e == tailEntry) {
tailEntry = null;
unflushedEntry = null;
}
} else { //todo 如果 不是最后一个节点, 把当前节点,移动到最后的 节点
flushedEntry = e.next;
}
}
概要
ここでは、第二波の伝播タスクが完了しています
書きます
- バッファはDirctBufferに変換します
- メッセージを書き込みキューのエントリに挿入され、
- 書き込み状態を設定します。
流す
- リフレッシュフラグは、ステータスを書き込むように設定されています
- 可変バッファキュー、フィルタバッファ
- JDK根本的なAPIを呼び出し、ByteBufはネイティブJDKを書きます
ByteBuffer