基于netty http协议栈的轻量级流程控制组件的实现
今儿个是冬至,所谓“冬大过年”,公司也应景五点钟就放大伙儿回家吃饺子喝羊肉汤了,而我本着极高的职业素养依然坚持留在公司(实则因为没饺子吃没羊肉汤喝,只能呆公司吃食堂……)。趁着这一个多小时的时间,想跟大家介绍下前段时间整的一个基于netty http协议栈的轻量级流程控制组件 nettice(点此查看代码),目前已经实现了一些功能并将持续完善,希望能为大伙儿切实解决一点开发问题(或者至少提供一些思路)。
什么是流程控制组件?
服务的流程,简单来说就是在一次交互过程中,对 client 端而言,是从请求的组装、发送,再到响应的接收、解析和业务处理的一个顺序流;对 server 端而言,是从请求的接收、解析和业务处理,再到响应的组装、发送的一个顺序流。而本文所说的流程控制组件,指的是在使用 netty http 协议栈开发 http server 的过程中,保证流程按照该顺序流执行,同时抽象出通用的非业务逻辑并对上层透明,使开发人员只需关注业务逻辑的底层实现。
为什么需要这么一个组件?
一个 http server 往往需要处理多种业务逻辑,每一个业务逻辑都对应着一个请求消息和一个响应消息,服务端需要把这些不同的消息自动分发到对应的业务逻辑中处理。
然而使用 netty http 协议栈开发过 http server 的童鞋都应该有所了解,netty 并没有提供消息分发组件。
这种情况下只能通过请求消息中的某个特殊标识(如某个字段值)来区分业务,使用 switch case 来处理。但这种方式下,随着业务逻辑的增多,switch case 代码块将越来越长,大大影响代码可读性;并且每次新增、删除业务逻辑时,都需要修改这段逻辑代码,后期维护也越来越麻烦。
此外,使用 netty http 协议栈时,并没有提供客户端 parameter 到服务端业务 method 入参的直接解析和映射。
这句话是什么意思呢?举个栗子,你在客户端使用 httpclient 给 netty http 服务端发送了一个消息,传递参数为“project=nettice&author=cyfonly”,而服务端有个业务方法 public void bizHandle(String project, String author),那么在调用 bizHandle 这个方法前,你肯定得先手动写代码解析客户端的请求参数解析出 project 和 author 两个 key 对应的 value。
那么问题来了,当业务逻辑越来越多,针对每个业务逻辑的请求,你都不得不单独写一段参数解析的代码。这是多么X疼的一件事情啊,而且后面还有一大堆业务逻辑代码要写呢!
有没有办法可以避免通过写 switch case 代码段来分发请求,并且使用统一方法来解析所有的请求参数呢?
当然有,nettice 就是为解决这个而诞生的啦~~
nettice 到底能做些什么呢?
特性
- 接收装配请求数据、流程控制和渲染数据
- URI 到方法直接映射,以及命名空间
功能
- 对 HttpRequest 的流程控制
- 像普通方法一样处理 http 请求
- 对请求的数据自动装配,支持基本类型、List、Array 和 Map
- 提供 Render 方法渲染并写回响应,支持多种 Content-Type
- 支持可配置的命名空间
nettice 是如何设计并实现的呢?
消息分发的整体设计如下(一图胜千言):
Action请求处理如下(一图胜千言+1):
如何使用 nettice?
nettice 引入项目
nettice 作为一个组件使用起来时很简单,此处使用具体的栗子来说明(demo代码请点此查看)。
首先是引入 nettice-core.jar,或者直接使用 nettice-core 源码作为 maven 项目的 module(目前没有上传到 maven 仓库,暂时没法通过 pom 依赖来引入)。然后定义 nettice 组件的必要配置 nettice.xml:
1 2 3 4 5 6 7 8 9 10 11 | <?xml version= "1.0" encoding= "UTF-8" ?> <router> <action-package> <package>com.server.action</package> </action-package> <namespaces> <!--按包分配命名空间,多个匹配项时,采用目录级别最多的--> < namespace name= "/nettp/" packages= "com.server.action.*" ></ namespace > < namespace name= "/nettp/sub/" packages= "com.server.action.sub" ></ namespace > </namespaces> </router> |
最后在服务端中添加消息分发handler:
1 | .addLast( "dispatcher" , new ActionDispatcher()) |
好了,现在就可以使用 nettice 的功能啦!
特别注意,业务处理类需继承 BaseAction 才能被 nettice 组件识别!
URI 映射和命名空间
使用方法名作为 URI 映射关键字,如果项目中存在同样名字的方法会产生冲突,开发者可以使用 @Namespaces 注解或者在 nettice.xml 配置中添加 namespaces 来修改 URI 映射,以规避此问题。
例如 com.server.action.DemoAction 提供了 returnTextUseNamespace() 方法,com.server.action.sub.SubDemoAction 也提供了 returnTextUseNamespace() 方法,但两个方法实现不同功能。nettice 组件默认使用方法名进行 URI 映射,那么上述两个 returnTextUseNamespace() 方法会产生冲突,开发者可以使用 @Namespace 注解修改 URI 映射:
1 2 3 4 5 6 7 8 | package com.server.action; public class DemoAction extends BaseAction{ @Namespace( "/nettp/demo/" ) public Render returnTextUseNamespace(@Read(key= "id" ) Integer id, @Read(key= "project" ) String project){ //do something return new Render(RenderType.TEXT, "returnTextUseNamespace in [DemoAction]" ); } } |
1 2 3 4 5 6 7 8 | package com.server.action.sub; public class SubDemoAction extends BaseAction{ @Namespace( "/nettp/subdemo/" ) public Render returnTextUseNamespace(@Read(key= "ids" ) Integer[] ids, @Read(key= "names" ) List<String> names){ //do something return new Render(RenderType.TEXT, "returnTextUseNamespace in [SubDemoAction]" ); } } |
也可以在 nettice.xml 中设置:
1 2 3 4 | <namespaces> < namespace name= "/nettp/demo/" packages= "com.server.action.*" ></ namespace > < namespace name= "/nettp/subdemo/" packages= "com.server.action.sub" ></ namespace > </namespaces> |
接收装配请求数据
使用 @Read 注解可以自动装配请求数组,支持不同的类型(基本类型、List、Array 和 Map),可以设置默认值(目前仅支持基本类型设置 defaultValue)。
基本数据类型解析
这个例子演示了从 HttpRequest 中获取基本类型的方法,如果没有值会自动设置默认值。
客户端请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private static void sendGetPriType() throws Exception{ String path = "http://127.0.0.1:8080/nettp/primTypeTest.action?" ; String getUrl = path + "id=10001&project=nettice&author=cyfonly" ; java.net.URL url = new java.net.URL(getUrl); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod( "GET" ); conn.setDoOutput( true ); conn.connect(); if (conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader( new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8" )); String msg = in .readLine(); System. out .println( "msg: " + msg); in .close(); } conn.disconnect(); } |
服务端 method:
1 2 3 4 | public Render primTypeTest(@Read(key= "id" , defaultValue= "1" ) Integer id, @Read(key= "project" ) String project, @Read(key= "author" ) String author){ System. out .println( "Receive parameters: id=" + id + ",project=" + project + ",author=" + author); return new Render(RenderType.TEXT, "Received your primTypeTest request.[from primTypeTest]" ); } |
输出结果:
1 | Receive parameters: id=10001,project=nettice,author=cyfonly |
List/Array 类型解析
这个例子演示了从 HttpRequest 中获取 List/Array 类型的方法。
客户端请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | private static void sendPostJsonArrayAndList() throws Exception{ String path = "http://127.0.0.1:8080/nettp/sub/arrayListTypeTest.action" ; JSONObject obj = new JSONObject(); int [] ids = {1,2,3}; List<String> names = new ArrayList<String>(); names.add( "aaaa" ); names.add( "bbbb" ); obj.put( "ids" , ids); obj.put( "names" , names); String jsonStr = obj.toJSONString(); byte [] data = jsonStr.getBytes(); java.net.URL url = new java.net.URL(path); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod( "POST" ); conn.setDoOutput( true ); conn.setRequestProperty( "Content-Type" , "application/json;charset=UTF-8" ); conn.setRequestProperty( "Content-Length" , String.valueOf(data.length)); OutputStream outStream = conn.getOutputStream(); outStream.write(data); outStream.flush(); outStream.close(); if (conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader( new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8" )); String msg = in .readLine(); System. out .println( "msg: " + msg); in .close(); } conn.disconnect(); } |
服务端 method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public Render arrayListTypeTest(@Read(key= "ids" ) Integer[] ids, @Read(key= "names" ) List<String> names){ System. out .println( "server output ids:" ); for ( int i=0; i<ids.length; i++){ System. out .println(ids[i]); } System. out .println( "server output names:" ); for (String item : names){ System. out .println(item); } JSONObject obj = new JSONObject(); obj.put( "code" , 0); obj.put( "msg" , "Received your Array/List request.[from arrayListTypeTest()]" );<br> return new Render(RenderType.JSON, obj.toJSONString()); } |
输出结果:
1 2 3 4 5 6 7 | server output ids: 1 2 3 server output names: aaaa bbbb |
Map 类型解析
这个例子演示了从 HttpRequest 中获取 Map 类型的方法。
客户端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private static void sendPostJsonMap() throws Exception{ String path = "http://127.0.0.1:8080/nettp/sub/mapTypeTest.action" ; JSONObject obj = new JSONObject(); Map<String, String> srcmap = new HashMap<String, String>(); srcmap.put( "project" , "nettice" ); srcmap.put( "author" , "cyfonly" ); obj.put( "srcmap" , srcmap); String jsonStr = obj.toJSONString(); byte [] data = jsonStr.getBytes(); java.net.URL url = new java.net.URL(path); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestMethod( "POST" ); conn.setDoOutput( true ); conn.setRequestProperty( "Content-Type" , "application/json;charset=UTF-8" ); conn.setRequestProperty( "Content-Length" , String.valueOf(data.length)); OutputStream outStream = conn.getOutputStream(); outStream.write(data); outStream.flush(); outStream.close(); if (conn.getResponseCode() == 200){ BufferedReader in = new BufferedReader( new InputStreamReader((InputStream) conn.getInputStream(), "UTF-8" )); String msg = in .readLine(); System. out .println( "msg: " + msg); in .close(); } conn.disconnect(); } |
服务端 method:
1 2 3 4 5 6 7 8 9 10 11 | public Render mapTypeTest(@Read(key= "srcmap" ) Map<String,String> srcmap){ System. out .println( "server output srcmap:" ); for (String key : srcmap.keySet()){ System. out .println(key + "=" + srcmap. get (key)); } JSONObject obj = new JSONObject(); obj.put( "code" , 0); obj.put( "msg" , "Received your Map request.[from mapTypeTest]" ); return new Render(RenderType.JSON, obj.toJSONString()); } |
输出结果:
1 2 3 | server output srcmap: author=cyfonly project=nettice |
注意,使用 Map 时限定了只能存在一个 Map 参数。
渲染数据
处理方法可以通过返回 Render 对象向客户端返回特定格式的数据,一个 Render 对象由枚举类型 RenderType 和 data 两部分组成。nettice 组件会通过 RenderType 来为 Response 设置合适的 Content-Type,开发者也可以扩展 Render 以及相关类来实现更多的类型支持。
例如这是一个返回 JSON 对象的例子,客户端将收到一个 Json 对象:
1 2 3 4 5 6 7 | public Render postPriMap(){ JSONObject obj = new JSONObject(); obj.put( "code" , 0); obj.put( "msg" , "had received your request." ); return new Render(RenderType.JSON, obj.toJSONString()); } |
接下来还会完善哪些?
正如开头说的那样,目前 nettice 实现了部分功能,在性能上也暂时没有太多的时间做优化,所以后续肯定会继续完善。目前有计划做的事情如下:
- java bean 支持
- 参数解析流程优化
- 性能优化
但就目前而言,nettice 确实解决了使用 netty http 协议栈开发 http server 的一些痛点。
好了,晚餐时间到,暂时先介绍这么多。如有未介绍到或者介绍不够详细的,将会完善本文,请持续关注~~
希望有兴趣的童鞋可以仔细研读代码,若有更好的想法欢迎通过评论或者加本人QQ(869827095)私下交流,或者和本人一起编码实现,都是非常欢迎的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?