Nettyに基づくサーバーとクライアントの通信の実装

個人ブログ

http://www.milovetingting.cn

Nettyに基づくサーバーとクライアント間の通信の実現

序文

この記事では、Nettyに基づくサーバーとクライアントの通信の簡単な使用方法を紹介し、これに基づいてサーバーとクライアントのコマンド通信の簡単なデモを実装します。

Nettyとは

Nettyは、プロトコルサーバーやクライアントなどのネットワークアプリケーションをすばやく簡単に開発できるNIOクライアントサーバーフレームワークです。TCPおよびUDPソケットサーバーの開発など、ネットワークプログラミングを大幅に簡素化します。非同期のイベント駆動型ネットワークアプリケーションフレームワークとツールを提供し、メンテナンス可能な高性能でスケーラビリティの高いプロトコルサーバーとクライアントを迅速に開発します。

上記のコンテンツはhttps://netty.io/wiki/user-guide-for-4.x.htmlからの抜粋です

Nettyには次の特性があります。

  • さまざまな伝送タイプの統一および非ブロッキングソケット
  • 高スループット、低レイテンシ
  • リソース消費を削減
  • 不要なメモリのコピーを減らす
  • 完全なSSL / TLSおよびStartTLSサポート

上記のコンテンツはhttps://netty.io/からの抜粋です

はじめに

Nettyの使用法については、Nettyの公式ドキュメントを参照してください。ここでは、サーバーとクライアントでのNettyの使用法を示す例として4.xを取り上げます。ドキュメントアドレス:https : //netty.io/wiki/user-guide-for-4.x.html

ここでは開発にEclipseを使用しており、サーバーとクライアントの両方が1つのプロジェクトに配置されています。

新しいJavaプロジェクト

サーバー

まず、netty jarパッケージをインポートする必要があります。ここではNetty-all-4.1.48.Final.jarが使用されています。

NettyServer

新しいNettyServerクラス

public class NettyServer {

	private int mPort;

	public NettyServer(int port) {
		this.mPort = port;
	}

	public void run() {
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
					// 指定连接队列大小
					.option(ChannelOption.SO_BACKLOG, 128)
					//KeepAlive
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					//Handler
					.childHandler(new ChannelInitializer<SocketChannel>() {

						@Override
						protected void initChannel(SocketChannel channel) throws Exception {
							channel.pipeline().addLast(new NettyServerHandler());
						}
					});
			ChannelFuture f = b.bind(mPort).sync();
			if (f.isSuccess()) {
				LogUtil.log("Server,启动Netty服务端成功,端口号:" + mPort);
			}
			// f.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// workerGroup.shutdownGracefully();
			// bossGroup.shutdownGracefully();
		}
	}

}

NettyServerHandler

初期化中に、チャネル関連のサービスを処理するためにハンドルを指定する必要があります。

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelActive");
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		LogUtil.log("Server,接收到客户端发来的消息:" + msg);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		LogUtil.log("Server,exceptionCaught");
		cause.printStackTrace();
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelInactive");
	}

}

以上の手順でサーバーの基本設定が完了しました。

クライアント

初期化中のクライアントとサーバーはほぼ同じですが、サーバーよりも単純です。

NettyClient

public class NettyClient {

	private String mHost;

	private int mPort;

	private NettyClientHandler mClientHandler;

	private ChannelFuture mChannelFuture;

	public NettyClient(String host, int port) {
		this.mHost = host;
		this.mPort = port;
	}

	public void connect() {
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			mClientHandler = new NettyClientHandler();
			b.group(workerGroup).channel(NioSocketChannel.class)
					// KeepAlive
					.option(ChannelOption.SO_KEEPALIVE, true)
					// Handler
					.handler(new ChannelInitializer<SocketChannel>() {

						@Override
						protected void initChannel(SocketChannel channel) throws Exception {
							channel.pipeline().addLast(mClientHandler);
						}
					});
			mChannelFuture = b.connect(mHost, mPort).sync();
			if (mChannelFuture.isSuccess()) {
				LogUtil.log("Client,连接服务端成功");
			}
			mChannelFuture.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			workerGroup.shutdownGracefully();
		}
	}
}

NettyClientHandler

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Client,channelActive");
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		LogUtil.log("Client,接收到服务端发来的消息:" + msg);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		LogUtil.log("Client,exceptionCaught");
		cause.printStackTrace();
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Client,channelInactive");
	}

}

この時点で、クライアントの基本設定は完了しています。

サーバーに接続する

新しいMainクラスを作成して、サーバーとクライアントが正常に接続できるかどうかをテストします。

public class Main {

	public static void main(String[] args) {
		try {
			String host = "127.0.0.1";
			int port = 12345;
			NettyServer server = new NettyServer(port);
			server.run();
			Thread.sleep(1000);
			NettyClient client = new NettyClient(host, port);
			client.connect();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

mainメソッドを実行すると、出力ログは次のようになります。

2020-4-13 0:11:02--Server,启动Netty服务端成功,端口号:12345
2020-4-13 0:11:03--Client,channelActive
2020-4-13 0:11:03--Client,连接服务端成功
2020-4-13 0:11:03--Server,channelActive

クライアントがサーバーに正常に接続し、サーバーとクライアントに設定されたハンドラーのchannelActiveメソッドがコールバックされることがわかります。

サーバーとクライアントの通信

サーバーとクライアント間の接続が成功した後、私たちは多くの場合、2つのパーティ間で通信する必要があります。ここでは、接続が成功した後、サーバーがウェルカムメッセージ「Hello、client」をクライアントに送信し、サーバーからメッセージを受信した後、クライアントがメッセージ「Hello、server」でサーバーに応答することを前提としています。 。特定の機能を実装してみましょう。

サーバーのNettyServerHandlerでchannelActiveメソッドとchannelReadメソッドを変更し、channelActiveメソッドでクライアントにメッセージを送信し、channelReadメソッドでクライアントが送信したメッセージを解析します。

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelActive");
		ByteBuf byteBuf = Unpooled.copiedBuffer("你好,客户端", Charset.forName("utf-8"));
		ctx.writeAndFlush(byteBuf);
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf) msg;
		byte[] buffer = new byte[buf.readableBytes()];
		buf.readBytes(buffer);
		String message = new String(buffer, "utf-8");
		LogUtil.log("Server,接收到客户端发来的消息:" + message);
	}

}

クライアントのNettyClientHandlerでchannelReadメソッドを変更し、サーバーからメッセージを受信したときにサーバーに返信する

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf) msg;
		byte[] buffer = new byte[buf.readableBytes()];
		buf.readBytes(buffer);
		String message = new String(buffer,"utf-8");
		LogUtil.log("Client,接收到服务端发来的消息:" + message);
		
		ByteBuf byteBuf = Unpooled.copiedBuffer("你好,服务端", Charset.forName("utf-8"));
		ctx.writeAndFlush(byteBuf);
	}

}

実行後の出力ログは次のとおりです。

2020-4-13 0:29:16--Server,启动Netty服务端成功,端口号:12345
2020-4-13 0:29:17--Client,channelActive
2020-4-13 0:29:17--Client,连接服务端成功
2020-4-13 0:29:17--Server,channelActive
2020-4-13 0:29:17--Client,接收到服务端发来的消息:你好,客户端
2020-4-13 0:29:17--Server,接收到客户端发来的消息:你好,服务端

サーバーとクライアントが正常に通信できることがわかります。

貼り付けと開梱

実際の使用シナリオでは、短時間で大量のデータを送信するという問題が発生する可能性があります。このシナリオをシミュレートします。クライアントがサーバーに接続した後、サーバーはクライアントに100メッセージを送信しますが、分析のために、サーバーメッセージを受信した後、クライアントは応答しません。

サーバーでNettyServerHandlerのchannelActiveメソッドを変更します

@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelActive");
		for (int i = 0; i < 100; i++) {
			ByteBuf byteBuf = Unpooled.copiedBuffer("你好,客户端", Charset.forName("utf-8"));
			ctx.writeAndFlush(byteBuf);
		}
	}

クライアントでNettyClientHandlerのchannelReadメソッドを変更します

@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf) msg;
		byte[] buffer = new byte[buf.readableBytes()];
		buf.readBytes(buffer);
		String message = new String(buffer, "utf-8");
		LogUtil.log("Client,接收到服务端发来的消息:" + message);

        //ByteBuf byteBuf = Unpooled.copiedBuffer("你好,服务端", Charset.forName("utf-8"));
        //ctx.writeAndFlush(byteBuf);
	}

実行後、出力結果の一部は次のようになります。

2020-4-13 0:35:28--Server,启动Netty服务端成功,端口号:12345
2020-4-13 0:35:29--Client,channelActive
2020-4-13 0:35:29--Client,连接服务端成功
2020-4-13 0:35:29--Server,channelActive
2020-4-13 0:35:29--Client,接收到服务端发来的消息:你好,客户端
2020-4-13 0:35:29--Client,接收到服务端发来的消息:你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端你好,客户端
2020-4-13 0:35:29--Client,接收到服务端发来的消息:你好,客户端

ご覧のように、複数のメッセージが「くっついて」います。

付着と開梱とは

TCPは「ストリーミング」プロトコルであり、いわゆるストリームは境界のない一連のデータです。TCPの最下層は、上層のビジネスデータの特定の意味を理解していません。TCPバッファの実際の状況に従ってパケットを分割するため、ビジネスでは、TCPによって完全なパケットが複数のパケットに分割されて送信されると考えられます複数の小さなパケットを大きなデータパケットにカプセル化して送信することは可能で、これはいわゆるTCPのスティッキングとアンパッキングの問題です。

上記の内容は、TCPのスティッキング/アンパックおよびNettyソリューションからの抜粋です。

解決策

Nettyがない場合、ユーザーがアンパックする必要がある場合、基本的な原則は、TCPバッファーからデータを継続的に読み取ることです。読み取り後は、毎回、それが完全なデータパケットであるかどうかを判断する必要があります。現在のデータが十分でない場合完全なビジネスデータパケットにスプライスされ、データを保持します。完全なデータパケットを取得するまで、TCPバッファーから読み取りを続けます。現在読み取られたデータと既に読み取られたデータがデータパケットにスプライスされるのに十分である場合、読み取られたデータは、今回読み取られたデータにスプライスされて完全なビジネスデータパッケージを形成し、冗長なビジネスロジックに渡されます。データは引き続き保持されるため、次回に読み取ったデータとつなぎ合わせることができます。

上記の内容はNettyの完全な理解から抜粋したものであり、この記事で十分です

Nettyを使用すると、この問題の解決策ははるかに簡単になります。Nettyは4つのアンパッカーを提供しています。

  • FixedLengthFrameDecoder:固定長アンパッカー、Nettyは固定長パケットを次のchannelHandlerに送信します
  • LineBasedFrameDecoder:行アンパッカー、各パケットは改行区切り文字で送信されます
  • DelimiterBasedFrameDecoder:デリミタアンパッカー、デリミタをカスタマイズできます。ラインアンパッカーはデリミタアンパッカーの特殊なケースです。
  • LengthFieldBasedFrameDecoder:長さフィールドに基づくアンパッカー。カスタムプロトコルに長さフィールドのフィールドが含まれている場合、このアンパッカーを使用できます。

ここでは、セパレーターのアンパッカーを選択します

最初にセパレータを定義します

public class Config {
	public static final String DATA_PACK_SEPARATOR = "#$&*";
}

サーバー側のchannelHandler構成では、それを増やす必要があります

@Override
protected void initChannel(SocketChannel channel) throws Exception {
    //这个配置需要在添加Handler前设置
	channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,Unpooled.copiedBuffer(Config.DATA_PACK_SEPARATOR.getBytes())));
	channel.pipeline().addLast(new NettyServerHandler());
	}

クライアントのchannelHandlerの構成では、それも追加する必要があります

@Override
protected void initChannel(SocketChannel channel) throws Exception {
    //这个配置需要在添加Handler前设置
	channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,Unpooled.copiedBuffer(Config.DATA_PACK_SEPARATOR.getBytes())));
	channel.pipeline().addLast(new NettyServerHandler());
	}

データを送信するときは、データの最後にセパレータを追加します。

@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelActive");
		for (int i = 0; i < 100; i++) {
			ByteBuf byteBuf = Unpooled.copiedBuffer("你好,客户端"+Config.DATA_PACK_SEPARATOR, Charset.forName("utf-8"));
			ctx.writeAndFlush(byteBuf);
		}
	}

実行後、「付着」と「解凍」の問題が解決されたことがわかります。

ハートビート

ネットワークアプリケーションでは、接続がまだ存在するかどうかを判断するために、通常、ハートビートパケットを送信することで検出されます。Nettyでは、ハートビートパケットを構成する手順は次のとおりです。

クライアントのchannelHandlerの構成では、それを増やす必要があります

@Override
protected void initChannel(SocketChannel channel) throws Exception {
			channel.pipeline().addLast(new IdleStateHandler(5, 5, 10));
            //...
						}

NettyClientHandlerで、userEventTriggeredメソッドをオーバーライドします

@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		IdleStateEvent event = (IdleStateEvent) evt;
		LogUtil.log("Client,Idle:" + event.state());
		switch (event.state()) {
		case READER_IDLE:

			break;
		case WRITER_IDLE:
			ByteBuf byteBuf = Unpooled.copiedBuffer("心跳^v^v", Charset.forName("utf-8"));
			break;
		case ALL_IDLE:
			break;
		default:
			super.userEventTriggered(ctx, evt);
			break;
		}
	}

書き込みアイドルが構成された時間に達すると、ハートビートメッセージをサーバーに送信します

実行後のログ出力は次のとおりです。

2020-4-13 1:22:50--Server,启动Netty服务端成功,端口号:12345
2020-4-13 1:22:51--Client,channelActive
2020-4-13 1:22:51--Client,连接服务端成功
2020-4-13 1:22:51--Server,channelActive
2020-4-13 1:22:51--Client,接收到服务端发来的消息:你好,客户端
2020-4-13 1:22:56--Client,Idle:WRITER_IDLE
2020-4-13 1:22:56--Server,接收到客户端发来的消息:心跳^v^
2020-4-13 1:22:56--Client,Idle:READER_IDLE
2020-4-13 1:23:01--Client,Idle:WRITER_IDLE
2020-4-13 1:23:01--Server,接收到客户端发来的消息:心跳^v^
2020-4-13 1:23:01--Client,Idle:READER_IDLE

設定した時間通りにハートビートパケットが正常に出力されていることがわかります。

エンコーダーとデコーダーを構成する

上記のデータを送信するときは、ByteBufを介して文字列を変換する必要があり、エンコーディングとデコーダを構成することで、文字列を直接送信できます。構成は次のとおりです。

サーバーとクライアントのそれぞれのchannelHandlerに次の構成を追加します。

@Override
protected void initChannel(SocketChannel channel) throws Exception {
	//...
	//这个配置需要在添加Handler前设置
	channel.pipeline().addLast("encoder", new StringEncoder());
	channel.pipeline().addLast("decoder", new StringDecoder());
    //...
}

メッセージを送信するときは、直接送信できますctx.writeAndFlush("心跳^v^" + Config.DATA_PACK_SEPARATOR)

ソースコード

この時点で、サーバーとクライアント間の通信の最も単純なデモが完了しています。ソースアドレス:https : //github.com/milovetingting/Samples/tree/master/NettyDemo

高度な使用

上記に基づいて、次の要件のいずれかを実装しましょう。

  • クライアントはサーバーにログインする必要があります

  • クライアントが正常にログインした後、サーバーはクライアントに指示メッセージを送信できます。メッセージを受信して​​メッセージを処理した後、クライアントはサーバーに報告する必要があります

パッケージ接続

プログラムの拡張を容易にするために、サーバーに接続されているクライアントの一部を抽出します。接続メソッドはインターフェースを介して定義され、接続の具体的な実装はサブクラスによって実装されます。

インターフェースを定義する

public interface IConnection {

	/**
	 * 连接服务器
	 * 
	 * @param host     服务器地址
	 * @param port     端口
	 * @param callback 连接回调
	 */
	public void connect(String host, int port, IConnectionCallback callback);

}

ここでは、接続のコールバックインターフェースも定義する必要があります。

public interface IConnectionCallback {

	/**
	 * 连接成功
	 */
	public void onConnected();

}

具体的な接続実装クラス

public class NettyConnection implements IConnection {

	private NettyClient mClient;

	@Override
	public void connect(String host, int port, IConnectionCallback callback) {
		if (mClient == null) {
			mClient = new NettyClient(host, port);
			mClient.setConnectionCallBack(callback);
			mClient.connect();
		}
	}

}

接続の管理を容易にするために、接続管理クラスを定義します

public class ConnectionManager implements IConnection {

	private static IConnection mConnection;

	private ConnectionManager() {

	}

	static class ConnectionManagerInner {
		private static ConnectionManager INSTANCE = new ConnectionManager();
	}

	public static ConnectionManager getInstance() {
		return ConnectionManagerInner.INSTANCE;
	}

	public static void initConnection(IConnection connection) {
		mConnection = connection;
	}

	private void checkInit() {
		if (mConnection == null) {
			throw new IllegalAccessError("please invoke initConnection first!");
		}
	}

	@Override
	public void connect(String host, int port, IConnectionCallback callback) {
		checkInit();
		mConnection.connect(host, port, callback);
	}

}

通話接続:

public class Main {

	public static void main(String[] args) {
		try {
			String host = "127.0.0.1";
			int port = 12345;
			NettyServer server = new NettyServer(port);
			server.run();
			Thread.sleep(1000);
			ConnectionManager.initConnection(new NettyConnection());
			ConnectionManager.getInstance().connect(host, port, new IConnectionCallback() {

				@Override
				public void onConnected() {
					LogUtil.log("Main,onConnected"););
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

connectメソッドを呼び出す前に、initConnectionを呼び出して特定の接続クラスを指定する必要があります

メッセージBeanの定義

接続が成功すると、サーバーはウェルカムメッセージをクライアントに送信します。管理を容易にするために、メッセージBeanを定義します

public class Msg {

	/**
	 * 欢迎
	 */
	public static final int TYPE_WELCOME = 0;

	public int type;

	public String msg;

}

サーバーがウェルカムメッセージを送信する

サーバーがメッセージを送信する

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

	private ChannelHandlerContextWrapper mChannelHandlerContextWrapper;

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogUtil.log("Server,channelActive");
		mChannelHandlerContextWrapper = new ChannelHandlerContextWrapper(ctx);
		MsgUtil.sendWelcomeMsg(mChannelHandlerContextWrapper);
	}
}

ここでは、ChannelHandlerContextWrapperクラスを定義して、メッセージセパレータを均一に管理します。

public class ChannelHandlerContextWrapper {

	private ChannelHandlerContext mContext;

	public ChannelHandlerContextWrapper(ChannelHandlerContext context) {
		this.mContext = context;
	}

	/**
	 * 包装writeAndFlush方法
	 * 
	 * @param object
	 */
	public void writeAndFlush(Object object) {
		mContext.writeAndFlush(object + Config.DATA_PACK_SEPARATOR);
	}

}

さらに進んで、MsgUtilクラスを定義して、ウェルカムメッセージをカプセル化して送信します。

public class MsgUtil {

	/**
	 * 发送欢迎消息
	 * 
	 * @param wrapper
	 */
	public static void sendWelcomeMsg(ChannelHandlerContextWrapper wrapper) {
		Msg msg = new Msg();
		msg.type = Msg.TYPE_WELCOME;
		msg.msg = "你好,客户端";
		wrapper.writeAndFlush(Global.sGson.toJson(msg));
	}

}

クライアントメッセージの受信

クライアントの場合、メッセージの処理を容易にするために、メッセージを受信するメソッドを定義する必要があります。これは、IConnectionインターフェイスにregisterMsgCallbackメソッドを追加することで実現されます。

public interface IConnection {

	/**
	 * 连接服务器
	 * 
	 * @param host     服务器地址
	 * @param port     端口
	 * @param callback 连接回调
	 */
	public void connect(String host, int port, IConnectionCallback callback);

	/**
	 * 注册消息回调
	 * 
	 * @param callback
	 */
	public void registerMsgCallback(IMsgCallback callback);

}

ここでは、IMsgCallbackインターフェースも追加する必要があります。

public interface IMsgCallback {

	/**
	 * 接收到消息时的回调
	 * 
	 * @param msg
	 */
	public void onMsgReceived(Msg msg);

}

実装クラスに対応

public class NettyConnection implements IConnection {

	private NettyClient mClient;

	@Override
	public void connect(String host, int port, IConnectionCallback callback) {
		if (mClient == null) {
			mClient = new NettyClient(host, port);
			mClient.setConnectionCallBack(callback);
			mClient.connect();
		}
	}

	@Override
	public void registerMsgCallback(IMsgCallback callback) {
		if (mClient == null) {
			throw new IllegalAccessError("please invoke connect first!");
		}
		mClient.registerMsgCallback(callback);
	}

}

メッセージ配信

クライアント側では、メッセージの処理を容易にするために、メッセージタイプを分割します

メッセージBeanの変更

public class Msg {

	/**
	 * 欢迎
	 */
	public static final int TYPE_WELCOME = 0;

	/**
	 * 心跳
	 */
	public static final int TYPE_HEART_BEAT = 1;

	/**
	 * 登录
	 */
	public static final int TYPE_LOGIN = 2;

	public static final int TYPE_COMMAND_A = 3;

	public static final int TYPE_COMMAND_B = 4;

	public static final int TYPE_COMMAND_C = 5;

	public int type;

	public String msg;
}

メッセージがシリアルであると仮定すると、メッセージは1つずつ処理する必要があります。メッセージの管理を容易にするために、MsgQueueクラスを追加します

public class MsgQueue {

	private PriorityBlockingQueue<Msg> mQueue;

	private boolean using;

	private MsgQueue() {
		mQueue = new PriorityBlockingQueue<>(128, new Comparator<Msg>() {

			@Override
			public int compare(Msg msg1, Msg msg2) {
				int res = msg2.priority - msg1.priority;
				if (res == 0 && msg1.time != msg2.time) {
					return (int) (msg2.time - msg1.time);
				}
				return res;
			}
		});
	}

	public static MsgQueue getInstance() {
		return MsgQueueInner.INSTANCE;
	}

	private static class MsgQueueInner {
		private static final MsgQueue INSTANCE = new MsgQueue();
	}

	/**
	 * 将消息加入消息队列
	 * 
	 * @param msg
	 */
	public void enqueueMsg(Msg msg) {
		mQueue.add(msg);
	}

	/**
	 * 从消息队列获取消息
	 * 
	 * @return
	 */
	public synchronized Msg next() {
		if (using) {
			return null;
		}
		Msg msg = mQueue.poll();
		if (msg != null) {
			makeUse(true);
		}
		return msg;
	}

	/**
	 * 标记使用状态
	 * 
	 * @param use
	 */
	public synchronized void makeUse(boolean use) {
		using = use;
	}

	/**
	 * 是否能够使用
	 * 
	 * @return
	 */
	public synchronized boolean canUse() {
		return !using;
	}

}

メッセージ配布クラスMsgDispatcherを増やします

public class MsgDispatcher {

	private static Map<Integer, Class<? extends IMsgHandler>> mHandlerMap;

	static {
		mHandlerMap = new HashMap<>();
		mHandlerMap.put(Msg.TYPE_WELCOME, WelcomeMsgHandler.class);
		mHandlerMap.put(Msg.TYPE_HEART_BEAT, HeartBeatMsgHandler.class);
		mHandlerMap.put(Msg.TYPE_LOGIN, HeartBeatMsgHandler.class);
		mHandlerMap.put(Msg.TYPE_COMMAND_A, CommandAMsgHandler.class);
		mHandlerMap.put(Msg.TYPE_COMMAND_B, CommandBMsgHandler.class);
		mHandlerMap.put(Msg.TYPE_COMMAND_C, CommandCMsgHandler.class);
	}

	public static void dispatch() {
		if (MsgQueue.getInstance().canUse()) {
			Msg msg = MsgQueue.getInstance().next();
			if (msg == null) {
				return;
			}
			dispatch(msg);
		}
	}

	public static void dispatch(Msg msg) {
		try {
			IMsgHandler handler = (IMsgHandler) Class.forName(mHandlerMap.get(msg.type).getName()).newInstance();
			handler.handle(msg);
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

}

メッセージ処理

IMsgHandlerを定義し、ここで処理メソッドを定義します。特定の実装はサブクラスによって実現されます

public interface IMsgHandler {

	/**
	 * 处理消息
	 * 
	 * @param msg
	 */
	public void handle(Msg msg);

}

統合管理の場合、基本クラスBaseCommandHandlerを定義します

public abstract class BaseCommandHandler implements IMsgHandler {

	@Override
	public void handle(Msg msg) {
		execute(msg);
	}

	public final void execute(Msg msg) {
		LogUtil.log("Client,received command:" + msg);
		doHandle(msg);
		MsgQueue.getInstance().makeUse(false);
		LogUtil.log("Client,report command:" + msg);
		MsgDispatcher.dispatch();
	}

	public abstract void doHandle(Msg msg);

}

BaseCommandHandlerで、executeメソッドを定義し、それを順番に呼び出します。報告されたメッセージは正常に受信され、メッセージは処理され、報告されたメッセージは処理されました。ここでのメッセージレポートの部分は、代わりにログを出力するだけですが、実際のビジネスでは、サブクラスによって抽象メソッドを抽出して実装できます。

BaseCommandHandlerから継承されたサブクラスを定義する

public class LoginMsgHandler extends BaseCommandHandler {

	@Override
	public void doHandle(Msg msg) {
		LogUtil.log("Client,handle msg:" + msg);
	}

}

対応するハートビートタイプのメッセージ、ウェルカムタイプのメッセージなどは、対応する処理クラスを追加することで実装できますが、ここでは展開しません。

メッセージ受信時の処理

public class Main {

	public static void main(String[] args) {
		try {
			String host = "127.0.0.1";
			int port = 12345;
			NettyServer server = new NettyServer(port);
			server.run();
			Thread.sleep(1000);
			ConnectionManager.initConnection(new NettyConnection());
			ConnectionManager.getInstance().connect(host, port, new IConnectionCallback() {

				@Override
				public void onConnected() {
					LogUtil.log("Main,onConnected");

					ConnectionManager.getInstance().registerMsgCallback(new IMsgCallback() {

						@Override
						public void onMsgReceived(Msg msg) {
							MsgQueue.getInstance().enqueueMsg(msg);
							MsgDispatcher.dispatch();
						}
					});
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

クライアントログイン

メッセージBeanを変更して、ログイン要求と応答を増やします。

public class Msg {

	/**
	 * 欢迎
	 */
	public static final int TYPE_WELCOME = 0;

	/**
	 * 心跳
	 */
	public static final int TYPE_HEART_BEAT = 1;

	/**
	 * 登录
	 */
	public static final int TYPE_LOGIN = 2;

	public static final int TYPE_COMMAND_A = 3;

	public static final int TYPE_COMMAND_B = 4;

	public static final int TYPE_COMMAND_C = 5;

	public int type;

	public String msg;

	public int priority;

	public long time;

	/**
	 * 登录请求信息
	 * 
	 * @author Administrator
	 *
	 */
	public static class LoginRuquestInfo {
		/**
		 * 用户名
		 */
		public String user;

		/**
		 * 密码
		 */
		public String pwd;

		@Override
		public String toString() {
			return "LoginRuquestInfo [user=" + user + ", pwd=" + pwd + "]";
		}
	}

	/**
	 * 登录响应信息
	 * 
	 * @author Administrator
	 *
	 */
	public static class LoginResponseInfo {

		/**
		 * 登录成功
		 */
		public static final int CODE_SUCCESS = 0;

		/**
		 * 登录失败
		 */
		public static final int CODE_FAILED = 100;

		/**
		 * 响应码
		 */
		public int code;

		/**
		 * 响应数据
		 */
		public String data;

		public static class ResponseData {
			public String token;
		}

		@Override
		public String toString() {
			return "LoginResponseInfo [code=" + code + ", data=" + data + "]";
		}

	}
}

ログインリクエストを送信

public class Main {

	public static void main(String[] args) {
		try {
			String host = "127.0.0.1";
			int port = 12345;
			NettyServer server = new NettyServer(port);
			server.run();
			Thread.sleep(1000);
			ConnectionManager.initConnection(new NettyConnection());
			ConnectionManager.getInstance().connect(host, port, new IConnectionCallback() {

				@Override
				public void onConnected() {
					LogUtil.log("Main,onConnected");

					ConnectionManager.getInstance().registerMsgCallback(new IMsgCallback() {

						@Override
						public void onMsgReceived(Msg msg) {
							MsgQueue.getInstance().enqueueMsg(msg);
							MsgDispatcher.dispatch();
						}
					});

					Msg msg = new Msg();
					msg.type = Msg.TYPE_LOGIN;

					Msg.LoginRuquestInfo request = new LoginRuquestInfo();
					request.user = "wangyz";
					request.pwd = "wangyz";

					Gson gson = new Gson();
					msg.msg = gson.toJson(request);

					ConnectionManager.getInstance().sendMsg(msg);
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

ここでは、メッセージBeanをjson文字列に送信するためにGsonが導入されています。

サーバーに対応して、メッセージの解析を容易にするために、対応するメッセージ変更Beanも必要です。サーバーによるメッセージの特定の配布と処理はクライアントのそれと同様であり、ここでは拡張されません。

ソースコード

スペースの制限、デモでの指示の優先処理、シミュレートされたサーバーでの指示の発行などのため、これ以上の詳細はここでは紹介しません詳細については、ソースコードを参照してください:https : //github.com/milovetingting/Samples/tree/master/Netty

追記

この記事では、Nettyに基づくサーバーとクライアントの通信の基本的な使用法を紹介し、それに基づいてサーバー側の命令の処理とレポートを実装します。デモの通信のデータ形式はjsonを使用しており、最適化方法はprotobufで実装できます。ここでは、通信プロセスと単純なカプセル化のみが示されているため、protobufは使用されていません。デモは一般的なプロセスのみを実装します。テストされていないバグが存在する可能性があります。参考にしましょう。

終わり〜

おすすめ

転載: www.cnblogs.com/milovetingting/p/12689103.html