問題の背景
NIOはバッファ指向であり、ストリーム指向ではありません。それはバッファなので、固定サイズでなければならないことは誰でも知っています。その結果、通常2つの問題が発生します。
- メッセージスティッキパケット:バッファが十分に大きい場合、ネットワークが不安定になるさまざまな理由により、チャネルからバッファに読み込まれるメッセージが多くなる可能性があります。このとき、データパケット間の境界を明確に区別できない場合、スティッキパケットの問題が発生します。
- 不完全なメッセージ:メッセージが受信されない場合、バッファーがいっぱいになり、バッファーから取り出されたメッセージが不完全になる、つまりハーフパケット現象が発生します。
この問題を紹介する前に、私のコードの全体的なアーキテクチャについて言及してください。
コードについては、GitHubリポジトリを参照してください
https://github.com/CuriousLei/smyl-im
このプロジェクトでは、NIOコアライブラリの設計アイデアのフローチャートを以下に示します。
はじめに:
- サーバーは、接続されているクライアントごとにConnectorオブジェクトを確立して、IOサービスを提供します。
- ioArgsオブジェクトの内部インスタンスフィールドは、チャネルとの直接的なデータ相互作用のためのバッファとしてバッファバッファを参照します。
- 2つのスレッドプールは、それぞれioArgsを制御して読み取りおよび書き込み操作を行います。
- コネクターとioArgsの関係:(1)入力、スレッドプールは読み取りイベントを処理し、データはioArgsに書き込まれ、コネクターにコールバックします;(2)出力、コネクターはデータをioArgsに書き込み、ioArgsはスレッドプール処理のためにRunnableオブジェクトに渡されます。
- 2つのセレクタースレッドは、チャネルの読み取りイベントと書き込みイベントをそれぞれ監視します。イベントの準備ができると、スレッドプールの作業がトリガーされます。
アイデア
これが達成されると、スティッキーパックとハーフパックの問題が避けられなくなります。これら2つの問題を再現するのも簡単です。
- ioArgsでは、バッファーを少し小さく設定し、この長さよりも大きいデータの一部を送信すると、サーバーはそれを2つのメッセージとして読み取ります。つまり、メッセージは不完全です。
- スレッドコードで、待機するThread.sleep()遅延を追加すると、クライアントはいくつかのメッセージを継続的に送信し(全長はバッファーサイズよりも短くなります)、スティッキーパケット現象も再現できます。
この問題は基本的に、メッセージ本文とバッファデータ間の1対1の対応が原因で発生します。それで、それをどのように解決しますか?
固定ヘッドソリューション
これは、固定ヘッダースキームを使用して解決できます。ヘッダーは4バイトで設定され、int値が格納され、次のデータの長さが記録されます。これを使用して、メッセージ本文にマークを付けます。
- データを読み取るときは、ヘッダーの長さ情報に従って、ioArgsバッファー内のデータを順番に読み取り、長さの要件が満たされていない場合は、次のioArgsの読み取りを続行します。これは当然粘着性のパックおよび半分のパックを引き起こしません。
- データを出力するときも、同じメカニズムを使用してデータをカプセル化します。最初の4バイトのレコード長です。
私のエンジニアリングプロジェクトでは、クライアントとサーバーがnioコアパッケージ、つまりniohdlを共有しています。これにより、データの送受信の一貫した形式を確保できます。
設計
上記の前提を実現するには、メッセージボディとバッファー間の変換関係を処理するために、DispatcherのレイヤーをコネクタとioArgsの間に追加する必要があります(メッセージボディは名前です:パケット)。さまざまな入力と出力に応じて、ReceiveDispatcherおよびSendDispatcherと呼ばれます。つまり、それらを使用して、PacketとioArgs間の変換を操作します。
パケット
このメッセージ本文を定義します。継承関係を以下に示します。
パケットは基本クラスで、コードは次のとおりです。
package cn.buptleida.niohdl.core;
import java.io.Closeable;
import java.io.IOException;
/**
* 公共的数据封装
* 提供了类型以及基本的长度的定义
*/
public class Packet implements Closeable {
protected byte type;
protected int length;
public byte type(){
return type;
}
public int length(){
return length;
}
@Override
public void close() throws IOException {
}
}
SendPacketおよびReceivePacketは、それぞれメッセージ本文の送信およびメッセージ本文の受信を表します。StringReceivePacketとStringSendPacketは、文字列型のメッセージを表します。これは、文字列メッセージの送受信に限定されているためです。将来的には、ファイルなどを拡張する必要があるかもしれません。
コードには必ずバイト配列の操作が含まれるため、StringSendPacketを例にとると、文字列をバイト[]に変換するメソッドを提供する必要があります。コードは次のとおりです。
package cn.buptleida.niohdl.box;
import cn.buptleida.niohdl.core.SendPacket;
public class StringSendPacket extends SendPacket {
private final byte[] bytes;
public StringSendPacket(String msg) {
this.bytes = msg.getBytes();
this.length = bytes.length;//父类中的实例域
}
@Override
public byte[] bytes() {
return bytes;
}
}
SendDispatcher
SendDispatcherオブジェクトは、コネクタオブジェクトのインスタンスフィールドで参照されます。データを送信すると、データはカプセル化されて、SendDispatcherのメソッドによって処理されます。一般的な関係図は次のとおりです。
SendDispatcherでタスクキューQueueを設定する
このプロセスのブロック図は次のとおりです。
コードでは、SendDispatcherは実際にはインターフェイスです。このインターフェイスを実装するには、AsyncSendDispatcherを使用します。コードは次のとおりです。
package cn.buptleida.niohdl.impl.async;
import cn.buptleida.niohdl.core.*;
import cn.buptleida.utils.CloseUtil;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
public class AsyncSendDispatcher implements SendDispatcher {
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private Sender sender;
private Queue<SendPacket> queue = new ConcurrentLinkedDeque<>();
private AtomicBoolean isSending = new AtomicBoolean();
private ioArgs ioArgs = new ioArgs();
private SendPacket packetTemp;
//当前发送的packet大小以及进度
private int total;
private int position;
public AsyncSendDispatcher(Sender sender) {
this.sender = sender;
}
/**
* connector将数据封装进packet后,调用这个方法
* @param packet
*/
@Override
public void send(SendPacket packet) {
queue.offer(packet);//将数据放进队列中
if (isSending.compareAndSet(false, true)) {
sendNextPacket();
}
}
@Override
public void cancel(SendPacket packet) {
}
/**
* 从队列中取数据
* @return
*/
private SendPacket takePacket() {
SendPacket packet = queue.poll();
if (packet != null && packet.isCanceled()) {
//已经取消不用发送
return takePacket();
}
return packet;
}
private void sendNextPacket() {
SendPacket temp = packetTemp;
if (temp != null) {
CloseUtil.close(temp);
}
SendPacket packet = packetTemp = takePacket();
if (packet == null) {
//队列为空,取消发送状态
isSending.set(false);
return;
}
total = packet.length();
position = 0;
sendCurrentPacket();
}
private void sendCurrentPacket() {
ioArgs args = ioArgs;
args.startWriting();//将ioArgs缓冲区中的指针设置好
if (position >= total) {
sendNextPacket();
return;
} else if (position == 0) {
//首包,需要携带长度信息
args.writeLength(total);
}
byte[] bytes = packetTemp.bytes();
//把bytes的数据写入到IoArgs中
int count = args.readFrom(bytes, position);
position += count;
//完成封装
args.finishWriting();//flip()操作
//向通道注册OP_write,将Args附加到runnable中;selector线程监听到就绪即可触发线程池进行消息发送
try {
sender.sendAsync(args, ioArgsEventListener);
} catch (IOException e) {
closeAndNotify();
}
}
private void closeAndNotify() {
CloseUtil.close(this);
}
@Override
public void close(){
if (isClosed.compareAndSet(false, true)) {
isSending.set(false);
SendPacket packet = packetTemp;
if (packet != null) {
packetTemp = null;
CloseUtil.close(packet);
}
}
}
/**
* 接收回调,来自writeHandler输出线程
*/
private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() {
@Override
public void onStarted(ioArgs args) {
}
@Override
public void onCompleted(ioArgs args) {
//继续发送当前包packetTemp,因为可能一个包没发完
sendCurrentPacket();
}
};
}
ReceiveDispatcher
同様に、ReceiveDispatcherもインターフェイスであり、AsyncReceiveDispatcherを使用してコードに実装されます。AsyncReceiveDispatcherオブジェクトは、コネクタオブジェクトのインスタンスフィールドで参照されます。データを受信すると、受信したデータは、ReceiveDispatcherのメソッドによって解凍されます。一般的な関係図は次のとおりです。
各メッセージ本文のヘッダーには、メッセージの長さを表す4バイトのintフィールドがあり、この長さに従って読み取られます。1つのioArgsがこの長さを満たしていない場合、次のioArgsが読み取られて、データパケットの整合性が保証されます。このプロセスはブロック図を描画せず、怠惰なhhhhを盗みます。実際、以下のコードコメントは非常に明確で理解しやすいものです。
AsyncReceiveDispatcherのコードは次のとおりです。
package cn.buptleida.niohdl.impl.async;
import cn.buptleida.niohdl.box.StringReceivePacket;
import cn.buptleida.niohdl.core.ReceiveDispatcher;
import cn.buptleida.niohdl.core.ReceivePacket;
import cn.buptleida.niohdl.core.Receiver;
import cn.buptleida.niohdl.core.ioArgs;
import cn.buptleida.utils.CloseUtil;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
public class AsyncReceiveDispatcher implements ReceiveDispatcher {
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private final Receiver receiver;
private final ReceivePacketCallback callback;
private ioArgs args = new ioArgs();
private ReceivePacket packetTemp;
private byte[] buffer;
private int total;
private int position;
public AsyncReceiveDispatcher(Receiver receiver, ReceivePacketCallback callback) {
this.receiver = receiver;
this.receiver.setReceiveListener(ioArgsEventListener);
this.callback = callback;
}
/**
* connector中调用该方法进行
*/
@Override
public void start() {
registerReceive();
}
private void registerReceive() {
try {
receiver.receiveAsync(args);
} catch (IOException e) {
closeAndNotify();
}
}
private void closeAndNotify() {
CloseUtil.close(this);
}
@Override
public void stop() {
}
@Override
public void close() throws IOException {
if(isClosed.compareAndSet(false,true)){
ReceivePacket packet = packetTemp;
if(packet!=null){
packetTemp = null;
CloseUtil.close(packet);
}
}
}
/**
* 回调方法,从readHandler输入线程中回调
*/
private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() {
@Override
public void onStarted(ioArgs args) {
int receiveSize;
if (packetTemp == null) {
receiveSize = 4;
} else {
receiveSize = Math.min(total - position, args.capacity());
}
//设置接受数据大小
args.setLimit(receiveSize);
}
@Override
public void onCompleted(ioArgs args) {
assemblePacket(args);
//继续接受下一条数据,因为可能同一个消息可能分隔在两份IoArgs中
registerReceive();
}
};
/**
* 解析数据到packet
* @param args
*/
private void assemblePacket(ioArgs args) {
if (packetTemp == null) {
int length = args.readLength();
packetTemp = new StringReceivePacket(length);
buffer = new byte[length];
total = length;
position = 0;
}
//将args中的数据写进外面buffer中
int count = args.writeTo(buffer,0);
if(count>0){
//将数据存进StringReceivePacket的buffer当中
packetTemp.save(buffer,count);
position+=count;
if(position == total){
completePacket();
packetTemp = null;
}
}
}
private void completePacket() {
ReceivePacket packet = this.packetTemp;
CloseUtil.close(packet);
callback.onReceivePacketCompleted(packet);
}
}
まとめ
実際、スティッキーパックとハーフパックのソリューションには謎がありません。それらは単純に複雑です。このメソッドの中核は、メッセージパケットをカスタマイズし、パケット内のバイト配列とバッファ配列間のコピー変換を完了することです。もちろん、位置、制限、その他の指針は非常に重要です。
このブログをまとめると、これまでの作品を整理して記録することでもあります。私は、smyl-imプロジェクトを通して+練習を続けます。前の学習プロセスにはたくさんの知識があり、それらすべてが私のDaoyunノートにあるため、それらをブログとして要約する必要はないと感じています。このブログのコンテンツは、たまたま体系的なものであり、このプロジェクトの背景をもたらすだけであり、それに基づいて後続のブログを派生させ、拡張することができます。