Netty服务端项目模块级代码

119 阅读8分钟

前言

Netty服务端项目模块级代码

这一篇主要是用于使用 Socket 通信的端对端的项目,本篇采取Java代码编写服务端,通过自定义编解码器实现数据约定及安全性校对,如有BUG请广大网友批评指针。

下面展示项目的包结构:

  1. 最外层的类为Netty初始化及基础的通道处理;
  2. 其中有三个大目录,business\codec\protocol,分别是业务包、编解码器、私定协议;
  3. request包及response包为请求消息体及响应消息体的封装;

将依赖引入后,将进行正式开始流程说明。

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.68.Final</version>
        </dependency>

本文是采取与SpringBoot的结合方式进行编码。

一、初始化启动

首先将服务端进行进程启动。

NettyServerInitialize.java

package org.springblade.netty;

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.CompletableFuture;

/**
 * @author 李家民
 */
@Component
public class NettyServerInitialize {

	private static Logger log = LoggerFactory.getLogger(NettyServerInitialize.class);
	/** 进程组 */
	private static EventLoopGroup bossGroup;
	private static EventLoopGroup workerGroup;
	private static ServerBootstrap serverBootstrap;
	/** 启动监听 */
	private static ChannelFuture channelFuture;
	/** 端口 */
	private static Integer port = 10222;

	static {
		// Server初始化
		bossGroup = new NioEventLoopGroup(1);
		workerGroup = new NioEventLoopGroup(2);
		serverBootstrap =
			new ServerBootstrap().group(bossGroup, workerGroup)
				.channel(NioServerSocketChannel.class)
				.option(ChannelOption.SO_BACKLOG, 128)
				.childOption(ChannelOption.SO_KEEPALIVE, true)
				.childHandler(new NettyServerChannel());
	}

	@PostConstruct
	public void starter() {
		CompletableFuture.runAsync(new Runnable() {
			@Override
			public void run() {
				try {
					channelFuture = serverBootstrap.bind(port)
						.addListener(new ChannelFutureListener() {
							@Override
							public void operationComplete(ChannelFuture future) throws Exception {
								if (future.isSuccess()) {
									System.out.println("监听端口 " + port + " 成功");
								} else {
									log.error("监听端口 " + port + " 失败");
								}
							}
						}).channel().closeFuture().sync();

				} catch (Exception e) {
					e.printStackTrace();
					bossGroup.shutdownGracefully();
					workerGroup.shutdownGracefully();
				}

			}
		});
	}
}

然后我们需要通道处理器进行流水线处理。

NettyServerChannel.java

package org.springblade.netty;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import org.springblade.netty.codec.BusinessMsgDecoder;
import org.springblade.netty.codec.BusinessMsgEncoder;

/**
 * 通道初始化器
 */
public class NettyServerChannel extends ChannelInitializer<SocketChannel> {
	public NettyServerChannel() {
		super();
	}

	/**
	 * 在将ChannelHandler添加到实际上下文并准备好处理事件后调用 如果覆盖此方法 请确保您调用 super
	 * @param ctx 通道处理器上下文
	 * @throws Exception
	 */
	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		super.handlerAdded(ctx);
	}

	/**
	 * Handle the {@link Throwable} by logging and closing the {@link Channel}. Sub-classes may override this.
	 * @param ctx
	 * @param cause
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		super.exceptionCaught(ctx, cause);
	}

	/**
	 * This method will be called once the {@link Channel} was registered. After the method returns this instance
	 * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
	 * @param ch the {@link Channel} which was registered.
	 * @throws Exception is thrown if an error occurs. In that case it will be handled by
	 *                   {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
	 *                   the {@link Channel}.
	 */
	@Override
	protected void initChannel(SocketChannel ch) {
		ChannelPipeline pipeline = ch.pipeline();
		// 编解码器 自定义
		pipeline.addLast(new BusinessMsgEncoder());
		pipeline.addLast(new BusinessMsgDecoder());
		pipeline.addLast(new NettyServerInboundHandler());
	}
}

二、消息入站

从上面的代码可以看到,我们在ChannelPipeline中加入了三个处理器,分别是

  1. 编码器;
  2. 解码器;
  3. 入站处理器;

我们按照数据的流入到转出纵观看向这幅代码,数据进站前,流入解码器。

BusinessMsgDecoder.java

package org.springblade.netty.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springblade.netty.protocol.MsgProtocol;
import org.springblade.netty.protocol.request.ServerRequest;
import org.springblade.netty.protocol.response.SendResponse;

import java.util.Arrays;
import java.util.List;

/**
 * 自定义业务解码器
 * @author lijiamin
 */
public class BusinessMsgDecoder extends ByteToMessageDecoder {

	private static final Logger logger = LoggerFactory.getLogger(BusinessMsgDecoder.class);

	/**
	 * 消息解码量化交易
	 * flag(1 byte)+length(4 byte,后边内容的长度)+protocol code(4 byte)+content
	 * length的长度包括  :消息号 + 内容
	 * @param channelHandlerContext
	 * @param byteBuf
	 * @param list
	 * @throws Exception
	 */
	@Override
	protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
		// 数据包完整性校对
		Integer dateLength = MsgProtocol.flagSize + MsgProtocol.lengthSize + MsgProtocol.msgCodeSize;
		int readableBytes = byteBuf.readableBytes();
		if (byteBuf.readableBytes() < dateLength) {
			// 数据包长度不足
			logger.info("decode -- 数据包长度不足");
			return;
		}
		// 返回表示 ByteBuf 当前可读取的字节数
		byte[] req = new byte[readableBytes];
		// 把 ByteBuf 里面的数据全部读取到字节数组中
		byteBuf.readBytes(req);
		if (req[0] != 1) {
			logger.info("flag 错误");
			return;
		}
		// 参数体长度
		byte codeLength = req[4];
		if (codeLength != (req.length - 4 - 1)) {
			logger.info("数据包尚不完整");
			return;
		}
		// 协议码
		byte codeByte = req[8];
		if (readableBytes == 9) {
			// 无参传递
			list.add(new ServerRequest((int) codeByte, ""));
		} else {
			// 有参传递
			byte[] detailBytes = Arrays.copyOfRange(req, 11, req.length);
			list.add(new ServerRequest((int) codeByte, new String(detailBytes)));
		}
	}
}

当我们根据私定的协议对数据进行处理完成后,放入下一站处理器。

关于约定的协议格式后续会进行说明

NettyServerInboundHandler.java

package org.springblade.netty;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.undertow.util.CopyOnWriteMap;
import org.springblade.netty.business.dispatcher.MsgDispatcher;
import org.springblade.netty.protocol.GameSession;
import org.springblade.netty.protocol.request.ServerRequest;
import org.springblade.netty.protocol.response.ResponseMsg;

import java.net.SocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

/**
 * 处理器
 * @author 李家民
 */
public class NettyServerInboundHandler extends ChannelInboundHandlerAdapter {

	/** 通道储存 */
	private static CopyOnWriteMap<String, Channel> mapChannel = new CopyOnWriteMap<String, Channel>();

	/** 消息分发处理器 */
	private static MsgDispatcher msgDispatcher = new MsgDispatcher();

	/**
	 * 消息广播推送
	 * @param msg 消息体
	 */
	public static void sendBroadcast(ResponseMsg msg) {
		Set<String> setAddress = new HashSet<>();
		// 广播
		mapChannel.forEach(new BiConsumer<String, Channel>() {
			@Override
			public void accept(String address, Channel channel) {
				if (channel.isActive()) {
					channel.writeAndFlush(msg);
				} else {
					setAddress.add(address);
				}
			}
		});
		// 将失活连接铲除
		for (String address : setAddress) {
			mapChannel.remove(address);
		}
	}

	/**
	 * 通道关闭
	 * @param ctx
	 * @throws Exception
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		mapChannel.remove(ctx.channel().remoteAddress().toString());
		super.channelInactive(ctx);
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		SocketAddress remoteAddress = ctx.channel().remoteAddress();
		mapChannel.put(remoteAddress.toString(), ctx.channel());
		super.channelActive(ctx);
	}

	/**
	 * 消息读取
	 * @param ctx
	 * @param msg
	 * @throws Exception
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ServerRequest serverRequest = (ServerRequest) msg;
		msgDispatcher.dispatchMsg(new GameSession(ctx), serverRequest);
	}

	/**
	 * 数据读取完成后的操作
	 * 1. writeAndFlush代表写入+刷新
	 * 2. 还需要对返回的数据进行编码
	 * @param ctx
	 * @throws Exception
	 */
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		super.channelReadComplete(ctx);
	}

	/**
	 * 发生异常后, 一般是需要关闭通道
	 * @param ctx
	 * @param cause
	 * @throws Exception
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		ctx.close();
		cause.printStackTrace();
		super.exceptionCaught(ctx, cause);
	}
}

三、关于双方私定的协议

interface MsgProtocol

package org.springblade.netty.protocol;

/**
 * 消息协议相关常量
 * @author xxx
 */
public interface MsgProtocol {
	/** 默认flag值 */
	byte defaultFlag = 1;
	/** 最大长度 */
	int maxPackLength = 1024 * 5;
	/** 标识数占得 byte数 */
	int flagSize = 1;
	/** 协议中 长度部分  占用的byte数,其值表示( 协议号+内容) 的长度 */
	int lengthSize = 4;
	/** 消息号占用的byte数 */
	int msgCodeSize = 4;
}
  • flag(1 byte)+length(4 byte,后边内容的长度)+protocol code(4 byte)+content
  • length的长度包括 :消息号 + 内容

另外对ChannelHandlerContext也有一层自己的包装GameSession

package org.springblade.netty.protocol;

import io.netty.channel.ChannelHandlerContext;
import lombok.Data;
import org.springblade.netty.protocol.response.ResponseMsg;

/**
 * 消息通道的包装
 * @author 李家民
 */
public class GameSession {

	/** 通道 */
	private ChannelHandlerContext channelHandlerContext;

	/** 在线状态 */
	public Boolean isOnline = false;

	/**
	 * 构造方法
	 * @param channelHandlerContext 通道
	 */
	public GameSession(ChannelHandlerContext channelHandlerContext) {
		this.channelHandlerContext = channelHandlerContext;
	}

	/**
	 * 消息发送
	 * @param msg 消息体
	 */
	public void sendMessage(ResponseMsg msg){
		// ...校验是否登录
		// ...
		channelHandlerContext.writeAndFlush(msg);
	}
}

另:请求体与响应体的包装

  1. ServerRequest
  2. ResponseMsg
  3. ServerResponse
  4. SendResponse
  5. MsgBodyWrap

请求体

/**
 * 请求消息体
 * @author 李家民
 */
@Data
@AllArgsConstructor
public class ServerRequest {
	private Integer code;
	private String data;
}

响应体

ResponseMsg

package org.springblade.netty.protocol.response;


import io.netty.buffer.ByteBuf;

/**
 * 此接口声明了响应消息编码函数,服务端生产的每个响应消息都必须实现此接口。<br>
 * 这里声明的函数在protocolEncoder中会被自动调用,编码成byte stream发送到客户端。
 *
 * @author xxx
 *
 */
public interface ResponseMsg {

	/**
	 * 设置消息号
	 * @param code
	 */
	public void setMsgCode(int code);

	/**
	 * 返回消息的整体封包
	 * @return
	 */
	public ByteBuf entireMsg(ByteBuf out);

	/**
	 * 释放资源(数据流、对象引用)
	 */
	public void release();
}

public class ServerResponse implements ResponseMsg

package org.springblade.netty.protocol.response;


import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import org.springblade.netty.protocol.MsgProtocol;

/**
 * 服务端发给客户端的消息。 所有返回给客户端的消息都最好继承于它.<br>
 * 这里封装了基本的输出字节操作。
 * @author dyz
 */
public class ServerResponse implements ResponseMsg {
	protected MsgBodyWrap output = MsgBodyWrap.newInstance4Out();
	private int msgCode;
	private int status;

	/** 必须调用此方法设置消息号 */
	public ServerResponse(int status, int msgCode) {
		setStatus(status);
		setMsgCode(msgCode);
	}

	public void setStatus(int status) {
		this.status = status;
	}

	public void setMsgCode(int code) {
		msgCode = code;
	}

	public synchronized ByteBuf entireMsg(ByteBuf out) {
		byte[] body = output.toByteArray();
		// 标志 byte 长度short
		int length = MsgProtocol.flagSize+MsgProtocol.lengthSize+MsgProtocol.msgCodeSize+ body.length+4;
		// 处理
		out.writeByte(MsgProtocol.defaultFlag);
		out.writeInt(length);
		out.writeInt(msgCode);
		out.writeInt(status);
		out.writeBytes(body);
		return out;
	}

	/**
	 * 释放资源(数据流、对象引用)
	 */
	public synchronized void release() {
		if (output != null) {
			output.close();
			output = null;
		}
		output = null;
	}
}

public class ServerResponse implements ResponseMsg

package org.springblade.netty.protocol.response;


import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import org.springblade.netty.protocol.MsgProtocol;

/**
 * 服务端发给客户端的消息。 所有返回给客户端的消息都最好继承于它.<br>
 * 这里封装了基本的输出字节操作。
 * @author xxx
 */
public class ServerResponse implements ResponseMsg {
	protected MsgBodyWrap output = MsgBodyWrap.newInstance4Out();
	private int msgCode;
	private int status;

	/** 必须调用此方法设置消息号 */
	public ServerResponse(int status, int msgCode) {
		setStatus(status);
		setMsgCode(msgCode);
	}

	public void setStatus(int status) {
		this.status = status;
	}

	public void setMsgCode(int code) {
		msgCode = code;
	}

	public synchronized ByteBuf entireMsg(ByteBuf out) {
		byte[] body = output.toByteArray();
		// 标志 byte 长度short
		int length = MsgProtocol.flagSize+MsgProtocol.lengthSize+MsgProtocol.msgCodeSize+ body.length+4;
		// 处理
		out.writeByte(MsgProtocol.defaultFlag);
		out.writeInt(length);
		out.writeInt(msgCode);
		out.writeInt(status);
		out.writeBytes(body);
		return out;
	}

	/**
	 * 释放资源(数据流、对象引用)
	 */
	public synchronized void release() {
		if (output != null) {
			output.close();
			output = null;
		}
		output = null;
	}
}

public class SendResponse extends ServerResponse

package org.springblade.netty.protocol.response;

import com.alibaba.fastjson.JSONObject;

import java.io.IOException;

/**
 * 消息返回模板
 * @author 李家民
 */
public class SendResponse extends ServerResponse {

	/**
	 * 消息返回模板
	 * @param status  消息状态 例如:200 成功
	 * @param data    实体数据
	 * @param msgCode 返回码
	 * @param detail  对这条数据的描述信息
	 */
	public SendResponse(int status, Object data, int msgCode, String detail) {
		super(status, msgCode);
		try {
			if (status > 0) {
				output.writeUTF(JSONObject.toJSONString(data));
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			output.close();
		}
	}
}

最后就是这个消息体的封装

package org.springblade.netty.protocol.response;


import java.io.*;

/**
 * 消息体封装 用于封装从客户端读取到的数据 或者发送到客户端的数据
 * @author xxx
 */
public class MsgBodyWrap {
	private ByteArrayInputStream in;
	private ByteArrayOutputStream out;

	private DataInputStream dataIn;
	private DataOutputStream dataOut;

	public static MsgBodyWrap newInstance4In(byte[] buffer) {
		return new MsgBodyWrap(buffer);

	}

	public static MsgBodyWrap newInstance4Out() {
		return new MsgBodyWrap();
	}

	private MsgBodyWrap(byte[] buffer) {
		in = new ByteArrayInputStream(buffer);
		dataIn = new DataInputStream(in);
	}

	private MsgBodyWrap() {
		out = new ByteArrayOutputStream();
		dataOut = new DataOutputStream(out);
	}

	public byte readByte() throws IOException {
		return dataIn.readByte();
	}

	public int read() throws IOException {
		return dataIn.read();
	}

	public short readShort() throws IOException {
		return dataIn.readShort();
	}

	public int readInt() throws IOException {
		return dataIn.readInt();
	}

	public long readLong() throws IOException {
		return dataIn.readLong();
	}

	public float readFloat() throws IOException {
		return dataIn.readFloat();
	}

	public double readDouble() throws IOException {
		return dataIn.readDouble();
	}

	public String readUTF() throws IOException {
		return dataIn.readUTF();
	}

	public void writeByte(int value) throws IOException {
		dataOut.writeByte(value);
	}

	public void writeBoolean(boolean value) throws IOException {
		dataOut.writeBoolean(value);
	}

	public void writeBytes(byte[] value) throws IOException {
		dataOut.write(value);
	}

	public void writeShort(int value) throws IOException {
		dataOut.writeShort(value);
	}

	public void writeInt(int value) throws IOException {
		dataOut.writeInt(value);
	}

	public void writeLong(long value) throws IOException {
		dataOut.writeLong(value);
	}

	public void writeFloat(float value) throws IOException {
		dataOut.writeFloat(value);
	}

	public void writeDouble(double value) throws IOException {
		dataOut.writeDouble(value);
	}

	public void writeUTF(String value) throws IOException {
		int bufferSize = 2147483647;
		int i = 0;
		int sum = 0;
		while (i < value.length()) {
			int endIdx = java.lang.Math.min(value.length(), i + bufferSize);
			String jsosPart = value.substring(i, endIdx);
			dataOut.writeUTF(jsosPart);
			sum += jsosPart.length();
			i += bufferSize;
		}
		assert sum == value.length();
	}

	public byte[] toByteArray() {
		return this.out.toByteArray();
	}

	/**
	 * 关闭缓冲读写数据流
	 */
	public void close() {
		if (in != null) {
			try {
				in.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (out != null) {
			try {
				out.flush();
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (dataIn != null) {
			try {
				dataIn.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (dataOut != null) {
			try {
				out.flush();
				dataOut.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

四、消息处理器 - 策略模式

通过上面说到,在入站消息完成解码工作后,开始对消息的消费,也就是我们的业务处理动作,此时我们需要区分消息,大量的if及switch显然并不优雅,所以这里是采取了策略模式。

MsgProcessor.java

package org.springblade.netty.business.dispatcher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springblade.netty.protocol.GameSession;
import org.springblade.netty.protocol.request.ServerRequest;

/**
 * 策略模式 - 消息处理
 */
public abstract class MsgProcessor {

	private static final Logger logger = LoggerFactory.getLogger(MsgProcessor.class);

	public void handle(GameSession gameSession, ServerRequest serverRequest) {
		try {
			process(gameSession, serverRequest);
		} catch (Exception e) {
			logger.error("消息处理出错 - msg code:" + serverRequest.getCode());
			e.printStackTrace();
		}
	}

	public abstract void process(GameSession gameSession, ServerRequest serverRequest) throws Exception;
}

接下来是注册器与分配器,根据消息code进行区分.

分配器

package org.springblade.netty.business.dispatcher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springblade.netty.business.handle.INotAuthProcessor;
import org.springblade.netty.protocol.GameSession;
import org.springblade.netty.protocol.request.ServerRequest;

import java.util.HashMap;
import java.util.Map;


/**
 * 消息分发器 根据消息号 找到相应的消息处理器
 * @author xxx
 */
public class MsgDispatcher {

	private static final Logger logger = LoggerFactory.getLogger(MsgDispatcher.class);

	private Map<Integer, MsgProcessor> processorsMap = new HashMap<Integer, MsgProcessor>();

	public MsgDispatcher() {
		for (MsgProcessorRegister register : MsgProcessorRegister.values()) {
			processorsMap.put(register.getMsgCode(), register.getProcessor());
		}
		logger.info("初始化 消息处理器成功 --- ");
	}

	/**
	 * 通过协议号得到MsgProcessor
	 * @param msgCode
	 * @return
	 */
	public MsgProcessor getMsgProcessor(int msgCode) {
		return processorsMap.get(msgCode);
	}

	/**
	 * 派发消息协议
	 * @param gameSession
	 * @param serverRequest
	 */
	public void dispatchMsg(GameSession gameSession, ServerRequest serverRequest) {
		MsgProcessor processor = getMsgProcessor(serverRequest.getCode());
		// 是否登录判断
		try {
			if (true || processor instanceof INotAuthProcessor) {
				processor.handle(gameSession, serverRequest);
			}
		} catch (NullPointerException e) {
			logger.error("存在未实现的接口:接口码 = " + serverRequest.getCode());
			e.printStackTrace();
		}

	}
}

注册器

package org.springblade.netty.business.dispatcher;

import lombok.Getter;
import org.springblade.netty.business.MsgCode;
import org.springblade.netty.business.handle.LoginMsgHandler;

/**
 * 消息处理器注册类
 * @author xxx
 */
@Getter
public enum MsgProcessorRegister {

	/** 登陆处理器 */
	login(MsgCode.CODE_1.getReq(), new LoginMsgHandler()),
	;

	private int msgCode;
	private MsgProcessor processor;

	/**
	 * 不允许外部创建
	 * @param msgCode
	 * @param processor
	 */
	private MsgProcessorRegister(int msgCode, MsgProcessor processor) {
		this.msgCode = msgCode;
		this.processor = processor;
	}
}

另:关于消息码

package org.springblade.netty.business;

import lombok.Getter;

/**
 * 消息码
 * @author 李家民
 */
@Getter
public enum MsgCode {

	////////////////////// 系统级请求响应从 1 开始 ////////////////////////////
	/** 登陆处理器 */
	CODE_1(1, 2),

	////////////////////// 业务级请求响应从 100 开始 //////////////////////////
	/** XXX */
	CODE_100(101, 102),

	///////////////////////////////// 广播消息从 300 开始 ////////////////////
	/** XXX */
	CODE_301(0, 301);


	/** 请求码 */
	private Integer req;
	/** 响应码 */
	private Integer resp;

	MsgCode(Integer req_code, Integer resp_code) {
		this.req = req_code;
		this.resp = resp_code;
	}
}

五、业务处理器

此时的业务处理器,需要实现上述的MsgProcessor,通过多态实现设计模式中的策略模式。

package org.springblade.netty.business.handle;

import org.springblade.core.tool.api.R;
import org.springblade.netty.business.MsgCode;
import org.springblade.netty.business.dispatcher.MsgProcessor;
import org.springblade.netty.protocol.GameSession;
import org.springblade.netty.protocol.request.ServerRequest;
import org.springblade.netty.protocol.response.SendResponse;

import java.util.HashMap;
import java.util.Map;

public class LoginMsgHandler extends MsgProcessor implements INotAuthProcessor {
	@Override
	public void process(GameSession gameSession, ServerRequest serverRequest) throws Exception {

		// ...业务处理
		Map<String, Object> map = new HashMap<>();

		map.put("realName", "牛逼");
		map.put("faceInformation", "");
		map.put("isLogin", true);
		map.put("msg", 1);//成功
		map.put("user", null);

		// ...
		R loginInfo = R.data(map);

		// 返回数据
		// ...0x000002
		gameSession.sendMessage(new SendResponse(200, loginInfo, MsgCode.CODE_1.getResp(), "测试我登录成功了"));

	}
}

然而我们也需要有无需校验的接口标识

package org.springblade.netty.business.handle;

/**
 * 标志接口 实现此处理器的processor不需要验证
 * @author xxx
 */
public interface INotAuthProcessor {
}

六、消息的出站

完成业务代码后,将消息发送到对端的客户端,此时需要编码器的处理。

package org.springblade.netty.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.springblade.netty.protocol.response.ResponseMsg;

/**
 * 自定义业务编码器
 * @author 李家民
 */
public class BusinessMsgEncoder extends MessageToByteEncoder {
	@Override
	protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
		// 引用计数处理 - 多出站处理器会导致资源过度释放
		out.retain();
		ResponseMsg valueMsg = (ResponseMsg) msg;
		ctx.writeAndFlush(valueMsg.entireMsg(out));
		// 资源释放
		valueMsg.release();
	}
}

七、Gitee地址

网页链接
netty-server: 服务模块,通用于常规TCP/WebSocket等,后续将逐渐补充 (gitee.com)

总结

我的工作中作为服务器开发经常要与各式各样的客户端进行对接,在本次的项目中,将apache mina框架完美平移到了Netty中,花了两天的时间,也算是收获颇多。