在网上查了很多netty server端实现的例子,感觉还是有很多坑,这里记录一下自己的实现,也把自己踩的坑记录一下,利人利己。
首先创建一个server类,用来初始化netty的server端
public class NettyHttpServer {private Logger logger = LoggerFactory.getLogger(NettyHttpServer.class);private int inetPort;public NettyHttpServer(final int inetPort) {this.inetPort = inetPort;}public int getInetPort() {return inetPort;}public void init() throws Exception {//根据自己机器的核心数设置parent和child数量,也可以不设定,netty会读取机器的核心数信息自行配置EventLoopGroup parentGroup = new NioEventLoopGroup(8);EventLoopGroup childGroup = new NioEventLoopGroup(16);try {ServerBootstrap server = new ServerBootstrap();// 1. 绑定两个线程组分别用来处理客户端通道的accept和读写时间server.group(parentGroup, childGroup)// 2. 绑定服务端通道NioServerSocketChannel.channel(NioServerSocketChannel.class)//这句会在日志中打印netty日志,需要的可以加上//.handler(new LoggingHandler(LogLevel.INFO))// 3. 给读写事件的线程通道绑定handler去真正处理读写// ChannelInitializer初始化通道SocketChannel.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline p = socketChannel.pipeline();//这里要注意解码器和转码器的顺序,server端用下面的顺序,但是cilent端要反过来//请求解码器p.addLast("http-decoder", new HttpRequestDecoder());// 响应转码器p.addLast("http-encoder", new HttpResponseEncoder());// 将HTTP消息的多个部分合成一条完整的HTTP消息,对于报文较长的请求需要设置,短请求可以不设置p.addLast("http-aggregator", new HttpObjectAggregator(1024*1024));// 自定义处理handler,这个handler中写自己的业务处理代码p.addLast("http-server", new NettyHttpServerHandler());}});//开启http的长连接,这个根据自己的请求场景来设置server.option(ChannelOption.SO_KEEPALIVE, true)//开启tcp立即响应,这样会影响整体性能,但是能保证单个响应不会延迟太高.option(ChannelOption.TCP_NODELAY, true)//开启请求缓存队列.option(ChannelOption.SO_BACKLOG, 1024 * 1024)//开启地址重用.option(ChannelOption.SO_REUSEADDR, true);// 4. 监听端口(服务器host和port端口),同步返回logger.info("netty started");Channel ch = server.bind(this.inetPort).sync().channel();logger.info("netty started success");// 当通道关闭时继续向后执行,这是一个阻塞方法ch.closeFuture().sync();} finally {childGroup.shutdownGracefully();parentGroup.shutdownGracefully();logger.info("netty shutdown");}}
}
然后实现handler,完成自己的业务逻辑
//这边有多种类可以用来继承,server端一般使用ChannelInboundHandlerAdapter或者//SimpleChannelInboundHandler,不同之处在于后者会自动关闭conetxt,可能会导致通道在响应发送之前关//闭,所以我这边用ChannelInboundHandlerAdapter,需要手动关闭context
//client端一般使用ChannelOutboundHandlerAdapter或者SimpleChannelOutboundHandler
public class NettyHttpServerHandler extends ChannelInboundHandlerAdapter {private static Logger logger = LoggerFactory.getLogger(NettyHttpServerHandler.class);@Overridepublic void channelReadComplete(ChannelHandlerContext ctx){ctx.flush();}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{//这个方法中读取msg中的信息,进行自定义的业务逻辑处理BusinessHandler.processRequest(ctx,msg);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception{logger.error("handler error:" + cause);ctx.close();}}
需要注意的是channelRead方法中的两个参数,第一个参数看名字就明白是上下文信息,第二个参数msg,如果使用SimpleChannelInboundHandler,可以指定msg的类型,例如
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {}
那么就能在override的channelRead0()方法中指定msg的类型为FullHttpRequest,方便解析。
当然,这边的msg是可以有多种类型的,我这边使用的是FullHttpRequest,包含HttpRequest和FullHttpMessage两个类,http中的参数和内容等所有信息都在这两个类中。
然后实现BusinseeHandler类
public class BusinessHandler {public static void processRequest(ChannelHandlerContext ctx, Object msg){FullHttpRequest fullHttpRequest = (FullHttpRequest)msg;//请求内容在content中,通过ByteBuf可以转换成bytesByteBuf bb = fullHttpRequest.content();//获取urifullHttpRequest.uri();//获取paramteerString sNow = getParameter(fullHttpRequest, "now");/**真·业务逻辑**///判断是否是长连接boolean close = fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.CLOSE, true)|| fullHttpRequest.protocolVersion().equals(HttpVersion.HTTP_1_0)&& !fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.KEEP_ALIVE, true);// 返回FullHttpResponse response = processor.doResponse();if(close){//使用netstat查看你的服务器连接情况,如果存在大量time-wait状态的连接,就可能是这个地方的问题channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);} else {channelHandlerContext.writeAndFlush(response);}//手动释放,防止内存泄露fullHttpRequest.release();}public String getParameter(FullHttpRequest fullHttpRequest, String par) throws IOException{HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(fullHttpRequest);decoder.offer(fullHttpRequest);for (InterfaceHttpData parm : decoder.getBodyHttpDatas()) {Attribute data = (Attribute) parm;if(par.equals(data.getName())){return data.getValue();}}return null;}}
如果自身的业务逻辑耗时较长,也可以使用线程池进行处理,这样能提升整体性能,如果追求单个响应的速度,就不要用线程池。
大致贴一下doResponse方法
public FullHttpResponse doResponse(){FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(200));//响应内容,内容的格式需要与下面设置的content-Type对应String resp = "response content";ByteBuf buffer = Unpooled.copiedBuffer(new StringBuilder(resp), CharsetUtil.UTF_8);response.content().writeBytes(buffer);//必须手动release,防止内存泄漏buffer.release();response.headers().set("Content-Type", "application/json;charset=utf-8");//在返回内容不为空的情况下,Content-Length必须手动设置,否者通道会阻塞response.headers().setInt("Content-Length", response.content().readableBytes());return response;
}
这样netty server端就创建完成了