web编程(5) Netty

1 概述

Netty是目前最流行的NIO框架之一,不管是功能、性能、健壮性,还是可定制、可扩展方面,表现都很卓越,尤其在高性能通讯方面,已逐渐成为业界主流,被广泛应用,例如阿里的分布式服务框架Dubbo,RocketMQ、Hadoop的Avro,以及spring 5.0 引入的全新技术框架WebFlux等,可以说,它基本上已是Java NIO 编程的首选框架。

通过个人实践、观察,netty之所以这么流行,除本身过硬的品质外,整个设计让人感触最深的是,它把复杂自己封装起来,把简单留给用户。

2 流程图

通过配置不同的启动参数,它可以支持Reactor单线程、多线程,以及主从多线程模型,以下为官方推荐的主从线程模型处理流程框图。

补充说明:

a. NioEventLoopGroup的线程池默认为CPU内核数的2倍。
b. 当一个TCP连接握手成功后,会在Work线程池,随机选择一个线程进行读写事件注册处理,也就是说SocketChannel会一直从属于一个I/O线程,不会出现多个线程处理一个SocketChannel的情况。
c. 每个SocketChannel连接,都会有一个的ChannelPipeline实例与其一一对应。
d. ChannelPipeline是ChannelHandler容器,它通过维护ChannelHandlerContext链表,来管理ChannelHandler。
f. 每个ChannelHandler,都会有一个的ChannelHandlerContext实例与其一一对应。   

3 核心类

主要分4个方面: I/O线程、Channel(Socket)、Handler、ByteBuf(内存)

3.1 I/O线程

NioEventLoopGroup类

服务端启动时,通常创建两个NioEventLoopGroup,它们实际是两独立的线程池,也就是常说的boss、work两个线程池,一个用于负责接收客户端的TCP连接,处理OP_ACCEPT事件,另一个负责处理I/O相关的读写操作,也就是处理OP_READ、OP_WRITE事件,以及执行各种task任务。

3.2 Channel(Socket)

Channel是Netty自己抽象的网络I/O处理接口,不同于JDK原生的Channel,更通俗说,就是非阻塞的Socket,主要有NioServerSocketChannel、NioSocketChannel实现类。

NioServerSocketChannel 用于服务端,监听网络端口,处理客户端连接请求。

NioSocketChannel 一旦服务端accept客户端请求,就会形成一个独立的通讯通道,由该实例来处理。后端所有的 handler 都是围绕着该通道,进行I/O 读写处理。

3.3 Handler

Handler是一组类,包含ChannelPipeline、ChannelHandler、ChannelHandlerContext等,其中ChannelHandler又分为ChannelInboundHandler、ChannelOutboundHandler两类。

ChannelPipeline是ChannelHandler的容器,管理ChannelHandler处理的前后关系。

ChannelInboundHandler 负责I/O读相关处理,对请求数据进行解码,提交给业务处理层。

ChannelOutboundHandler 负责I/O写相关处理,对响应数据按协议编码,最终响应给客户端。

ChannelHandlerContext 辅助类,跟ChannelHandler是一一对应的关系,Handler的处理结果,都是通过它进行协调处理。

3.4 ByteBuf(内存)

JDK 原生NIO的数据读写操作,都是通过缓存区ByteBuffer类处理,ByteBuf是Netty自己的缓存区实现,其中很多接口方法跟ByteBuffer类似,依据使用场景,分类如下:

UnpooledHeapByteBuf 基于堆内存进行分配的缓存区,每次I/O读写,都会创建新的实例,相比堆外内存的申请、释放,它的成本较低,因此,在满足性能的情况,它是推荐的处理缓存区方式。

PooledHeapByteBuf 带有池的堆内存,在某些方面可以节约内存的创建开销,但它内部的内存管理也更复杂。

UnPooledDirectByteBuf 基于直接内存,也就是非堆内存,它的内存分配、回收慢一些,但对I/O的读写少了一次内存复制,速度比堆内存快。

PooledDirectByteBuf 基于内存池实现,跟UnPooledDirectByteBuf功能一样,只是内存分配策略不同。

4 功能类

4.1 TCP粘包、拆包

TCP是个基于字节流的协议,在网络中按IP分组包进行数据传递,依据TCP发送缓存区的实际情况,进行包的划分,在业务层认为的的一个完整数据包,可能会分多个包传递,也可能多个小包被合并成一大的数据包传递,这就是所谓的TCP粘包、拆包问题。

业界主流解决方案,大致可归纳为3种,Netty都已封装实现,如下:

(1) DelimiterBasedFrameDecoder、LineBasedFrameDecoder 基于分割符的数据帧格式,数据包间以约定的分割负划分,例如"$-$"、“@%@”等,其中LineBasedFrameDecoder 是以"\r\n"固定封装好的解码类。

(2) FixedLengthFrameDecoder 是固定长数据帧格式解码类,例如100字节,如果不足,用空位补齐。

(3) LengthFieldBasedFrameDecoder自定义长度帧解码器,将数据包分数据头、数据体两部分,

数据头包含数据长度等信息,类似于http-post协议中的header部分的  Content-Length。该种方案更灵活,通常自定义协议采用该种方式。

4.2 编解码

netty内置了丰富的编解码器,例如http、websocket、Protobuf、Marshaller等,在"6 案例"部分,有演示的小例子。

5 配置

由ServerBootstrap服务引导类处理,主要涉及线程池、TCP参数、ChannelHandler,以下只介绍前两项。

5.1 线程池

		// 配置服务端的NIO线程组
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();

		// 服务引导类
		ServerBootstrap b = new ServerBootstrap();
		
		// 单线程池
		b.group(bossGroup);
		// 主从线程池(推荐)
		b.group(bossGroup, workerGroup);

5.2 TCP参数

作为服务端,主要是BACKLOG参数的设置,其他还包括读写缓存区、NODELAY、超时等设置,ChannelOption类已内置定义这些参数(以下仅部分举例):

    public static final ChannelOption<Integer> SO_SNDBUF = valueOf("SO_SNDBUF");
    public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");

    public static final ChannelOption<Integer> SO_BACKLOG = valueOf("SO_BACKLOG");
    public static final ChannelOption<Integer> SO_TIMEOUT = valueOf("SO_TIMEOUT");

    public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");

通常只配置BACKLOG、NODELAY就可以,如下:

			ServerBootstrap b = new ServerBootstrap();
			
			b.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 100)
			.option(ChannelOption.TCP_NODELAY, false)

BACKLOG 是内核中socket排队的最大连接数,依据TCP的3次握手过程,分为两队列未完成队列(收到客户端SYN时)、已完成队列(收到客户端SYN-ACK时,成功握手),BACKLOG为两个队列之和最大值。

NODELAY 是否延时,通常选false,由Socket依据发送缓存区实际情况,进行分组发送数据。

5.3 依赖

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

6 案例

6.1 简单例子(客户、服务端)

下面是一个包含Netty客户端、服务端的小例子,通讯协议采用最简单的回车换行格式,可以看到例子采用LineBasedFrameDecoder来解决tcp粘包、拆包问题。

netty服务端:启动类

public class DemoServer {

	public static void main(String[] args) throws Exception {
		
		new DemoServer().bind(8080);
	}
	
	public void bind(int port) throws Exception {

		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			
			b.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 100)
			
			.childHandler(new ChildChannelHandler());
			
			// 同步等待成功
			ChannelFuture f = b.bind(port).sync();			
			System.out.println("DemoServer start success, port: " + port);

			// 监听端口关闭
			f.channel().closeFuture().sync();
		} finally {
			// 释放线程池资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
		@Override
		protected void initChannel(SocketChannel arg0) throws Exception {
			arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
			arg0.pipeline().addLast(new StringDecoder());
			arg0.pipeline().addLast(new DemoServerHandler());
			
			System.out.println("initChannel:pipeline");
		}
	}	
}

netty服务端:Handler类

public class DemoServerHandler extends ChannelInboundHandlerAdapter {
	
	private static final Logger logger = Logger.getLogger(DemoServerHandler.class.getName());
	
	private static final AtomicInteger counter = new AtomicInteger();
		
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		String reqMsg = (String) msg;
		logger.info("netty-server request : " + reqMsg);
		
		String resMsg = reqMsg + " pong " + counter.addAndGet(1) + System.getProperty("line.separator");
		ByteBuf resp = Unpooled.copiedBuffer(resMsg.getBytes());
		ctx.writeAndFlush(resp);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		logger.warning("netty-server exceptionCaught : " + cause.getMessage());
		ctx.close();
	}	

}

netty客户端:启动类

public class DemoClient {

	public static void main(String[] args) throws Exception {
		new DemoClient().connect(8080, "127.0.0.1");
	}
	
	public void connect(int port, String host) throws Exception {

		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group)
			.channel(NioSocketChannel.class)
			.option(ChannelOption.TCP_NODELAY, false)
			.handler(new ChildChannelHandler());

			// 同步等待成功
			ChannelFuture f = b.connect(host, port).sync();
			System.out.println(String.format("DemoClient start success, host: %s, port: %s", host, port));

			// 链路关闭
			f.channel().closeFuture().sync();
		} finally {
			// 释放线程池资源
			group.shutdownGracefully();
		}
	}
	
	private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
		@Override
		protected void initChannel(SocketChannel arg0) throws Exception {
			arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
			arg0.pipeline().addLast(new StringDecoder());
			arg0.pipeline().addLast(new DemoClientHandler());
			
			System.out.println("initChannel:pipeline");
		}
	}
}

netty客户端:Handler类

public class DemoClientHandler extends ChannelInboundHandlerAdapter {
	
	private static final Logger logger = Logger.getLogger(DemoClientHandler.class.getName());
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) {
		
		byte[] msg = ("hello-ping" + System.getProperty("line.separator")).getBytes();
		
		ByteBuf buf = Unpooled.buffer(msg.length);;
		buf.writeBytes(msg);
		ctx.writeAndFlush(buf);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		String body = (String) msg;
		logger.info("netty-client response : " + body);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		
		logger.warning("netty-client exceptionCaught : " + cause.getMessage());
		ctx.close();
	}

}

6.2 http服务

一个http服务小例子,测试链接:http://localhost:8080/demo/hello

netty服务端:启动类

public class HttpServer {

	public static void main(String[] args) throws Exception {
		
		new HttpServer().bind(8080);
	}
	
	public void bind(int port) throws Exception {

		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			
			b.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 100)
			
			.childHandler(new ChildChannelHandler());
			
			// 同步等待成功
			ChannelFuture f = b.bind(port).sync();			
			System.out.println("HttpServer start success, port: " + port);

			// 监听端口关闭
			f.channel().closeFuture().sync();
		} finally {
			// 释放线程池资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
		@Override
		protected void initChannel(SocketChannel sc) throws Exception {
			sc.pipeline().addLast("http-decoder", new HttpRequestDecoder());
			// 目的是将多个消息转换为单一的request或者response对象
			sc.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
			sc.pipeline().addLast("http-encoder", new HttpResponseEncoder());
			// 目的是支持异步大文件传输()
			sc.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
			sc.pipeline().addLast("hello-handler", new HelloChannelHandler());			
			
			System.out.println("initChannel:pipeline");
		}
	}	
}

netty服务端:Handler类

public class HelloChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
	
	private final String GET_URL_HELLO = "/demo/hello"; 

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
		
		if (!request.getDecoderResult().isSuccess()) {
			sendError(ctx, BAD_REQUEST);
			return;
		}
		if (request.getMethod() != GET) {
			sendError(ctx, METHOD_NOT_ALLOWED);
			return;
		}
		final String uri = request.getUri();
		if (!GET_URL_HELLO.equals(uri)) {
			sendError(ctx, FORBIDDEN);
			return;			
		}
		
		sendHello(ctx, "hello, baby!");		
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		if (ctx.channel().isActive()) {
			sendError(ctx, INTERNAL_SERVER_ERROR);
		}
	}	
	
	private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
		FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status,
				Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
		response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
	
	private static void sendHello(ChannelHandlerContext ctx, String msg) {
		FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
		response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
				
		ByteBuf buffer = Unpooled.copiedBuffer(msg + "\r\n", CharsetUtil.UTF_8);
		response.content().writeBytes(buffer);
		buffer.release();
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}	

}

6.3 模拟MVC(Protobuf)

实施思路,跟spring mvc 的DispatcherServlet是一样的,不同之处就是,mvc 通过请求url与Controller的@RequestMapping方法对应,而Protobuf协议,通过数据包 reqCode 与Controller的自定义注解@PbRequestMapping方法对应,启动时HandlerMethodMapping扫描所有HandlerMethod实例方法,维护reqCode与HandlerMethod匹配关系, 以下是源码:

6.3.1 pom依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mutouren</groupId>
  <artifactId>mtr-demo-base-blog-netty-mvc</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>mtr-demo-base-blog-netty-mvc</name>
  
	<properties>
		<jdk.version>1.8</jdk.version>
	</properties>

	<dependencies>
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>		
	
 		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>
				
		<dependency>
		    <groupId>io.netty</groupId>
		    <artifactId>netty-all</artifactId>
		    <version>4.0.56.Final</version>
		</dependency>		
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>2.6.1</version>
        </dependency>			

	</dependencies>
	
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>2.3.2.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>	

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.5.1</version>
				<configuration>
					<source>${jdk.version}</source>
					<target>${jdk.version}</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

6.3.2 Protobuf文件

Request.proto

option java_package = "mtr.demo.netty.model";
option java_outer_classname = "RequestProto";

message Request {

 	required int32 pd_type = 1; 			// Message类型
 	required int32 pd_req_code = 2; 		// 请求编号

	required int64 pd_timestamp = 3;		// 时间戳
 	required bytes pd_data = 4; 			// 内容
}

Response.proto

option java_package = "mtr.demo.netty.model";
option java_outer_classname = "ResponseProto";

message Response {

 	required int32 pd_type = 1; 			// Message类型
 	required int32 pd_res_code = 2; 		// 响应编号
 	
 	required int32 pd_state = 3; 			// 响应状态
 	optional string pd_message = 4; 		// 响应信息

	optional int64 pd_timestamp = 5;		// 时间戳
 	optional bytes pd_data = 6; 			// 内容
}

ReqMessgeA.proto

option java_package = "mtr.demo.netty.model";
option java_outer_classname = "ReqMessageAProto";

message ReqMessgeA {

 	required int32 pd_id = 1; 				// 消息id
 	optional string pd_desc = 2; 			// 描述
}

ReqMessgeB.proto

option java_package = "mtr.demo.netty.model";
option java_outer_classname = "ReqMessageBProto";

message ReqMessgeB {

 	required int32 pd_id = 1; 				// 消息id
	required string pd_name = 2; 			// 名称
 	optional string pd_desc = 3; 			// 描述
}

ResMessgeAB.proto

option java_package = "mtr.demo.netty.model";
option java_outer_classname = "ResMessageABProto";

message ResMessgeAB {

 	required int32 pd_id = 1; 				// 消息id
	required string pd_content = 2; 		// 内容
 	optional string pd_desc = 3; 			// 描述
}

6.3.3 服务端

(1)启动类
@SpringBootApplication
public class Application {

	public static void main(String[] args) throws Exception {
		
        SpringApplication springApplication = new SpringApplication(Application.class);
        springApplication.setWebApplicationType(WebApplicationType.NONE);
        ApplicationContext ctx = springApplication.run(args);

        NettyServer.bind(ctx, 8080);
	}
}
public class NettyServer {
	
	private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);

	public static void bind(ApplicationContext context, int port) throws Exception {

		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			
			b.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 100)
			
			.childHandler(new ChildChannelHandler(context));
			
			// 同步等待成功
			ChannelFuture f = b.bind(port).sync();			
			logger.info("NettyServer start success, port: {}", port);

			// 监听端口关闭
			f.channel().closeFuture().sync();
		} finally {
			// 释放线程池资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	private static class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
		
		private final ApplicationContext context;
		
		public ChildChannelHandler(ApplicationContext context) {
			this.context = context;
		}
		
		@Override
		protected void initChannel(SocketChannel ch) throws Exception {
			ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());							
			ch.pipeline().addLast(new ProtobufDecoder(RequestProto.Request.getDefaultInstance()));
			ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
			ch.pipeline().addLast(new ProtobufEncoder());						
			ch.pipeline().addLast(new DispatcherChannelHandler(context));
			
			System.out.println("initChannel:pipeline");
		}
	}
}
(2)Controller类
@Controller
public class DemoController {
	
	private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
	
	@PbRequestMapping(reqCode=Constant.REQ_MESSAGE_A)
	public Response processA(Request request, ReqMessgeA reqMessgeA) {
		
		logger.info("processA: {}", reqMessgeA);		
		
		ResMessageABProto.ResMessgeAB.Builder builder = ResMessageABProto.ResMessgeAB.newBuilder();
		builder.setPdId(1);
		builder.setPdContent("aa");
		builder.setPdDesc("111");
		
		return ResponseHelper.buildResponse(Constant.RES_STATE_SUCCESS, builder.build().toByteArray(), request);
	}
	
	@PbRequestMapping(reqCode=Constant.REQ_MESSAGE_B)
	public Response processB(Request request, ReqMessgeB reqMessgeB, @UserId Integer userId) {
		
		logger.info("processB: {}, userId:{}", reqMessgeB, userId);
		
		ResMessageABProto.ResMessgeAB.Builder builder = ResMessageABProto.ResMessgeAB.newBuilder();
		builder.setPdId(2);
		builder.setPdContent("bb");
		builder.setPdDesc("222");
		
		return ResponseHelper.buildResponse(Constant.RES_STATE_SUCCESS, builder.build().toByteArray(), request);
	}
}
(3)Dispatcher相关

DispatcherChannelHandler类

public class DispatcherChannelHandler extends ChannelInboundHandlerAdapter {
	
	private static final Logger logger = LoggerFactory.getLogger(DispatcherChannelHandler.class);
	
	private final HandlerMethodMapping handlerMethodMapping;
	
	public DispatcherChannelHandler(ApplicationContext context) {
		this.handlerMethodMapping = context.getBean(HandlerMethodMapping.class);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		RequestProto.Request req = (RequestProto.Request) msg;

		switch (req.getPdType()) {
		case Constant.REQ_TYPE_BIZ:
			this.doBizRequest(req, ctx);
			break;
		case Constant.REQ_TYPE_CHAT:	
			this.doChat(req, ctx);
			break;
		default:
			// 不能识别
			ResponseHelper.doErrorResponse(req, ctx, "pdType is not valid");
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		logger.warn("ctx close", cause);
		ctx.close();
	}
	
	private void doBizRequest(Request req, ChannelHandlerContext ctx) {
		
		try {
			// 参数
			byte[] pdData = req.getPdData().toByteArray();
			
			// 匹配HandlerMethod
			HandlerMethod handlerMethod = handlerMethodMapping.getHandler(req.getPdReqCode());
			
			// 处理
			HandlerMethodAdapter.process(req, ctx, handlerMethod, pdData);		
		} catch (Throwable t) {
			logger.error("doBizRequest error", t);
			ResponseHelper.doErrorResponse(req, ctx, t.getMessage());
		}
	}
	
	private void doChat(Request req, ChannelHandlerContext ctx) {
		// 略
		ResponseHelper.doErrorResponse(req, ctx, "暂不支持");
	}
}

HandlerMethod类

public class HandlerMethod {
	
	private final Object handler;

	private final Class<?> handlerType;

	private final Method method;
	
	private final Class<?>[] parameterTypes;
	
	public HandlerMethod(Object handler, Class<?> handlerType, Method method, Class<?>[] parameterTypes) {
		this.handler = handler;
		this.handlerType = handlerType;
		this.method = method;
		this.parameterTypes = parameterTypes;
	}

	public Object getHandler() {
		return handler;
	}

	public Class<?> getHandlerType() {
		return handlerType;
	}

	public Method getMethod() {
		return method;
	}

	public Class<?>[] getParameterTypes() {
		return parameterTypes;
	}
}

HandlerMethodMapping类

@Component
public class HandlerMethodMapping extends ApplicationObjectSupport implements InitializingBean {
	
	private final Map<Integer, HandlerMethod> mapHandlerMethod = new ConcurrentHashMap<>();
	
	public HandlerMethod getHandler(int reqCode) {
		return this.mapHandlerMethod.get(reqCode);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		initHandlerMethods();		
	}
	
	private void initHandlerMethods() {

		String[] beanNames = getApplicationContext().getBeanNamesForType(Object.class);

		for (String beanName : beanNames) {
			Class<?> beanType = null;
			try {
				beanType = getApplicationContext().getType(beanName);
			}
			catch (Throwable ex) {
				// An unresolvable bean type, probably from a lazy bean - let's ignore it.
				logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
			}
			if (beanType != null && isHandler(beanType)) {
				detectHandlerMethods(beanName);
			}	
		}
		
		logger.info("initHandlerMethods finish, size: " + mapHandlerMethod.size());
	}
	
	private boolean isHandler(Class<?> beanType) {
		return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
	}
	
	private void detectHandlerMethods(final String beanName) {
		Class<?> handlerType = getApplicationContext().getType(beanName);
		Object handler = getApplicationContext().getBean(beanName);

		Method[] Methods = handlerType.getMethods();
		for(Method m : Methods) {
			PbRequestMapping reqMapping = m.getAnnotation(PbRequestMapping.class);
            if (reqMapping == null) {
                continue;
            }
            
            int reqCode = reqMapping.reqCode();
            Class<?>[] parameterTypes = m.getParameterTypes();            
            HandlerMethod HandlerMethod = new HandlerMethod(handler, handlerType, m, parameterTypes);
            
            if (mapHandlerMethod.containsKey(reqCode)) {
            	throw new RuntimeException("load HandlerMethod exception, reqCode match more method: " + reqCode);
            }
            mapHandlerMethod.put(reqCode, HandlerMethod);
		}
	}	
}

HandlerMethodAdapter类

public class HandlerMethodAdapter {
	
	private static final Logger logger = LoggerFactory.getLogger(HandlerMethodAdapter.class);
	private static final ExecutorService executor = Executors.newFixedThreadPool(100, new DemoThreadFactory("mmm"));
	
	public static void process(Request req, ChannelHandlerContext ctx, HandlerMethod handlerMethod, byte[] pdData) throws Exception {
		
		Object handler = handlerMethod.getHandler();
		Method method = handlerMethod.getMethod();
		Class<?>[] paramTypes = handlerMethod.getParameterTypes();
		
		// 参数
		Object[] params = getParams(req, ctx, method, paramTypes, pdData);
		
		executor.execute(new Runnable() {

			@Override
			public void run() {				
				try {
					// 处理
					Object result = method.invoke(handler, params);

					// 回复		
					ctx.writeAndFlush((ResponseProto.Response)result);					
				} catch (Throwable t) {
					logger.error("handler process excetpion", t);
					ResponseHelper.doErrorResponse(req, ctx, "inner exception: " + t.getMessage());
				}				
			}			
		});
	}
	
	private static Object[] getParams(Request req, ChannelHandlerContext ctx, Method method, Class<?>[] paramTypes, byte[] pdData) throws Exception {
				
		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
		
		Object[] params = new Object[paramTypes.length];
        for (int i = 0; i < params.length; i++) {
        	Class<?> paramType = paramTypes[i];
        	Annotation[] annotations = parameterAnnotations[i];

        	if (ChannelHandlerContext.class.isAssignableFrom(paramType)) {
        		params[i] = ctx;
        	} else if (Request.class.isAssignableFrom(paramType)) {
        		params[i] = req;
        	} else if (GeneratedMessage.class.isAssignableFrom(paramType)) {
                if (pdData == null) {
                    params[i] = null;
                } else {
                    Method m = paramType.getMethod("parseFrom", ByteString.class);
                    Object o = m.invoke(paramType, ByteString.copyFrom(pdData));
                    params[i] = o;
                }
        	} else {
        		for (Annotation a : annotations) {
        			if (UserId.class.isAssignableFrom(a.getClass())) {
        				params[i] = getUserId();
        				break;
        			}
        		}
        	}
        }

		return params;
	}
	
	private static Integer getUserId() {
		// 略
		return 123;
	}
	
    private static final class DemoThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        public DemoThreadFactory(String alias) {
            group = new ThreadGroup(alias);            
            namePrefix = String.format("%s-pool-%d-thread-", alias, poolNumber.getAndIncrement());
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
            
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }	
}
(4)注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PbRequestMapping {
	
    int reqCode();
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId { }
(5)辅助类
public interface Constant {
	
	int REQ_TYPE_BIZ = 1;
	int REQ_TYPE_CHAT = 2;
	
    int REQ_MESSAGE_A = 1;
    int REQ_MESSAGE_B = 2;
    
    int RES_STATE_SUCCESS = 0;
    int RES_STATE_FAIL = 1;
}
public class ResponseHelper {
	
	public static Response buildResponse(int state, byte[] data, Request request) {
		return buildResponse(state, "", data, request);
	}
	
	public static Response buildResponse(int state, String msg, byte[] data, Request request) {
		ResponseProto.Response.Builder builder = ResponseProto.Response.newBuilder();
		builder.setPdType(request.getPdType());
		builder.setPdResCode(request.getPdReqCode());
		builder.setPdState(state);
		builder.setPdMessage(msg);
		
		builder.setPdTimestamp(System.currentTimeMillis());
		builder.setPdData(ByteString.copyFrom(data));
		return builder.build();
	}	
	
	public static void doErrorResponse(Request req, ChannelHandlerContext ctx, String msg) {		
		Response res = buildResponse(Constant.RES_STATE_FAIL, msg, null, req);
		
		ctx.writeAndFlush(res);
	}	
}

6.3.4 客户端(测试)

public class DemoClient {

	public static void main(String[] args) throws Exception {
		new DemoClient().connect(8080, "127.0.0.1");
	}
	
	public void connect(int port, String host) throws Exception {
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group)
			.channel(NioSocketChannel.class)
			.option(ChannelOption.TCP_NODELAY, false)
			.handler(new ChildChannelHandler());

			// 同步等待成功
			ChannelFuture f = b.connect(host, port).sync();
			System.out.println(String.format("DemoClient start success, host: %s, port: %s", host, port));

			// 链路关闭
			f.channel().closeFuture().sync();
		} finally {
			// 释放线程池资源
			group.shutdownGracefully();
		}
	}

	private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
		@Override
		protected void initChannel(SocketChannel ch) throws Exception {
			ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
			ch.pipeline().addLast(new ProtobufDecoder(ResponseProto.Response.getDefaultInstance()));
			ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
			ch.pipeline().addLast(new ProtobufEncoder());
			ch.pipeline().addLast(new DemoClientHandler());
			
			System.out.println("initChannel:pipeline");
		}
	}
}
public class DemoClientHandler extends ChannelInboundHandlerAdapter {
	
	private static final Logger logger = LoggerFactory.getLogger(DemoClientHandler.class);
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) {		
//		ctx.writeAndFlush(this.getRequestForMessageA(100));
		ctx.writeAndFlush(this.getRequestForMessageB(200));
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ResponseProto.Response res = (ResponseProto.Response) msg;	
				
		if (res.getPdState() == 0) {
			ResMessageABProto.ResMessgeAB data = ResMessageABProto.ResMessgeAB.parseFrom(res.getPdData());
			logger.info("响应, state:{}, msg:{}, date:{}", res.getPdState(), res.getPdMessage(), data);
		} else {
			logger.info("响应, state:{}, msg:{}", res.getPdState(), res.getPdMessage());	
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {		
		logger.error("netty-client exceptionCaught", t);
		ctx.close();
	}
	
	private RequestProto.Request getRequestForMessageA(int id) {
		RequestProto.Request.Builder reqBuilder = RequestProto.Request.newBuilder();
		reqBuilder.setPdType(Constant.REQ_TYPE_BIZ);
		reqBuilder.setPdReqCode(Constant.REQ_MESSAGE_A);
		reqBuilder.setPdTimestamp(System.currentTimeMillis());
		
		ReqMessageAProto.ReqMessgeA.Builder databuilder = ReqMessageAProto.ReqMessgeA.newBuilder();
		databuilder.setPdId(id);
		databuilder.setPdDesc("hello A");
		
		reqBuilder.setPdData(ByteString.copyFrom(databuilder.build().toByteArray()));		
		return reqBuilder.build();
	}	
	
	private RequestProto.Request getRequestForMessageB(int id) {
		RequestProto.Request.Builder reqBuilder = RequestProto.Request.newBuilder();
		reqBuilder.setPdType(Constant.REQ_TYPE_BIZ);
		reqBuilder.setPdReqCode(Constant.REQ_MESSAGE_B);
		reqBuilder.setPdTimestamp(System.currentTimeMillis());
		
		ReqMessageBProto.ReqMessgeB.Builder dataBuilder = ReqMessageBProto.ReqMessgeB.newBuilder();
		dataBuilder.setPdId(id);
		dataBuilder.setPdName("mm");
		dataBuilder.setPdDesc("hello B");
		
		reqBuilder.setPdData(ByteString.copyFrom(dataBuilder.build().toByteArray()));
		return reqBuilder.build();
	}	
}

6.4 消息推送(Protobuf)

这里只提供一下思路,就不列源码了,只要在6.3 案例基础上,增加维护一个TCP长链接的 “会话字典”就可以了,ChannelInboundHandlerAdapter已提供链接开启、关闭的两个事件handlerAdded(ChannelHandlerContext ctx),handlerRemoved(ChannelHandlerContext ctx),恰好满足要求,再结合业务本身的登录、退出逻辑,就可以维护一个userId 与ChannelHandlerContext的字典。

当消息推送时,通过userId 找到对应的ChannelHandlerContext,如果链接有效,就可以进行消息推送。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值