1.Nettyの紹介
Netty は、NIO ベースのクライアントおよびサーバー側プログラミング フレームワークです.Netty を使用すると、特定のプロトコルを実装するクライアントまたはサーバー アプリケーションなどのネットワーク アプリケーションを迅速かつ簡単に開発できます。Netty は、TCP および UDP に基づくソケット サービスの開発など、ネットワーク アプリケーションのプログラミングおよび開発プロセスを簡素化および合理化します。
この記事では完全なサンプル コードを使用して、dubbo と同様のリモート ネットワーク通信を実現する netty について詳しく説明します。
実装手順:
- インターフェイスと実装クラスを作成する
- クライアント コードを作成する
- ダイナミック プロキシ モードを介して netty リモート インターフェース呼び出しをカプセル化する
- 非同期スレッド待ち・通知により、非同期→同期を実現
- サーバーコードを作成する
- カスタムコーデック
- テストクライアント送信リクエストコードを書く
2. 完全なコード実装
プロジェクトの依存関係の紹介
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
1. インターフェイスと実装クラスを作成する
簡単なインターフェースと実装クラスを定義します。アノテーションを使用してインターフェイスとサービスの実装を定義し、後続のコードでアノテーションを解析します。
- @ServiceEntry はインターフェイス serviceId を定義します
- @MyService はサービス実装クラスを定義します
public interface IHelloService {
@ServiceEntry(serviceId = "001", name = "hello")
String hello(String msg);
}
@MyService
public class HelloServiceImpl implements IHelloService {
@Override
public String hello(String msg) {
return "re:这里是服务端,已收到客户端消息:" + msg.hashCode();
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ServiceEntry {
/**
* 服务Id
*/
String serviceId();
/**
* 服务名称
*/
String name() default "";
}
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService {
String value() default "";
}
2. クライアント コードの実装、動的プロキシ、非同期から同期へ
1) クライアント Handler である MyClientHandler を作成し、ChannelInboundHandlerAdapter を継承し、Callable インターフェイスを実装します。
- クライアントがリクエストを送信すると、call メソッドが呼び出されます。ここで、非同期から同期へ
- リクエストコンテキストをマップに入れ、スレッドを待ち、サーバーからのリターンを受け取ったら非同期でスレッドに実行を通知し、結果データを返す
- サーバーからのリターンを受信したら、リターン結果データを設定し、スレッドに実行を通知する
public class MyClientHandler extends ChannelInboundHandlerAdapter implements Callable<String> {
private ChannelHandlerContext ctx;
private ConcurrentHashMap<String, SyncSendContext> syncSendContextMap = new ConcurrentHashMap<>();
private Object[] param;
private String serviceId;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端和服务端链接成功");
this.ctx = ctx;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端收到服务端回复: " + msg);
ResponseData data = (ResponseData) msg;
String id = data.getId();
// 收到服务端返回时,设置返回结果数据,并通知线程执行
SyncSendContext context = syncSendContextMap.get(id);
context.setResp(data);
synchronized (context) {
context.notify();
}
}
@Override
public String call() throws Exception {
System.out.println("客户端向服务端发送消息: " + param[0].toString());
String id = UUID.randomUUID().toString();
RequestData data = new RequestData();
data.setId(id);
//强制设置参数1
data.setData(param[0].toString());
data.setServiceId(serviceId);
SyncSendContext context = new SyncSendContext();
context.setRequest(data);
// 将请求context放入map,并等待线程,在收到服务端返回时,异步通知线程执行,返回结果数据
syncSendContextMap.put(id, context);
synchronized (context) {
ctx.writeAndFlush(data);
context.wait();
return (String) context.getResp().getData();
}
}
public void setParam(Object[] param) {
this.param = param;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
}
2) クライアント コード MyClient を作成する
- 動的プロキシを介してリモート サービス リクエストをラップする
- サーバー リンクを初期化し、二重チェック ロックによって clientHandler がシングルトン実装であることを確認します。
- リクエストを送信するとき、clientHandler はスレッド プールを介して非同期に送信されます。
public class MyClient {
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private MyClientHandler clientHandler;
// 通过动态代理,包装远程服务请求
public <T> T getServie(final Class<T> service) {
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{
service},
(proxy, method, args) -> {
if (clientHandler == null) {
init("127.0.0.1", 7000);
}
ServiceEntry annotation = method.getAnnotation(ServiceEntry.class);
if (annotation == null) {
return null;
}
clientHandler.setParam(args);
clientHandler.setServiceId(annotation.serviceId());
return executor.submit(clientHandler).get();
});
}
// 初始化服务端链接,通过双检锁确保clientHandler是单例实现
private synchronized void init(String hostname, int port) {
if (clientHandler != null) {
return;
}
clientHandler = new MyClientHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap().group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new ResponseMessageCodec());
pipeline.addLast(new RequestMessageCodec());
pipeline.addLast(clientHandler);
}
});
bootstrap.connect(hostname, port).sync();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. サーバー コードの実装
1) サービス エンジニアリング クラス ServiceFacatory を作成し、アノテーションを解析してサービス インターフェイスと実装クラスを保存し、呼び出し時に Map から直接取得します。
public class ServiceFacatory {
private static final Map<String, Method> methodMap = new HashMap<>();
private static final Map<String, Object> serviceMap = new HashMap<>();
public static void init() throws Exception {
// 要扫描的包
String packages = "com.hj.netty.dubbo.api";
Set<MethodInfo> methods = PackageUtils.findClassAnnotationMethods(packages, ServiceEntry.class);
for (MethodInfo info : methods) {
ServiceEntry serviceEntry = (ServiceEntry) info.getAnnotation();
methodMap.put(serviceEntry.serviceId(), info.getMethod());
String serviceName = info.getMethod().getDeclaringClass().getName();
if (!serviceMap.containsKey(serviceName)) {
Object instance = info.getMethod().getDeclaringClass().newInstance();
serviceMap.put(serviceName, instance);
}
}
}
public static Object invoke(String serviceId, Object args) throws Exception {
Method method = methodMap.get(serviceId);
String serviceName = method.getDeclaringClass().getName();
Object instance = serviceMap.get(serviceName);
Object result = method.invoke(instance, args);
return result;
}
}
@Data
@AllArgsConstructor
public class MethodInfo {
private Annotation annotation;
private Method method;
}
2) パッケージ解析ツール クラス。指定されたディレクトリ内のすべてのサービス クラスを解析します。
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.SystemPropertyUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.*;
public class PackageUtils {
private final static Logger log = LoggerFactory.getLogger(PackageUtils.class);
//扫描 scanPackages 下的文件的匹配符
protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
/**
* 结合spring的类扫描方式
* 根据需要扫描的包路径及相应的注解,获取最终测method集合
* 仅返回public方法,如果方法是非public类型的,不会被返回
* 可以扫描工程下的class文件及jar中的class文件
*
* @param scanPackages
* @param annotation
* @return
*/
public static Set<MethodInfo> findClassAnnotationMethods(String scanPackages, Class<? extends Annotation> annotation) {
//获取所有的类
Set<String> clazzSet = findPackageClass(scanPackages);
Set<MethodInfo> methods = new HashSet<>();
//遍历类,查询相应的annotation方法
for (String clazz : clazzSet) {
try {
Set<MethodInfo> ms = findAnnotationMethods(clazz, annotation);
methods.addAll(ms);
} catch (ClassNotFoundException ignore) {
}
}
return methods;
}
public static Set<MethodInfo> findAnnotationMethods(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
Set<MethodInfo> methodSet = new HashSet<>();
Class<?> clz = Class.forName(fullClassName);
// 存储接口中定义的方法
Map<String, Method> mapMethodInf = new HashMap<>();
for (int i = 0; i < clz.getInterfaces().length; i++) {
Class<?> inf = clz.getInterfaces()[i];
Method[] methods = inf.getDeclaredMethods();
for (Method method : methods) {
String key = getMethodKey(method);
mapMethodInf.put(key, method);
}
}
Method[] methods = clz.getDeclaredMethods();
for (Method method : methods) {
if (method.getModifiers() != Modifier.PUBLIC) {
continue;
}
Annotation annotation = method.getAnnotation(anno);
if (annotation != null) {
methodSet.add(new MethodInfo(annotation,method));
} else {
// 从接口中读取对应的方法
String key = getMethodKey(method);
Method methodInf = mapMethodInf.get(key);
annotation = methodInf.getAnnotation(anno);
if (annotation != null) {
methodSet.add(new MethodInfo(annotation,method));
}
}
}
return methodSet;
}
/**
* 根据扫描包的,查询下面的所有类
*
* @param scanPackages 扫描的package路径
* @return
*/
private static Set<String> findPackageClass(String scanPackages) {
if (StringUtils.isBlank(scanPackages)) {
return Collections.EMPTY_SET;
}
//验证及排重包路径,避免父子路径多次扫描
Set<String> packages = checkPackage(scanPackages);
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
Set<String> clazzSet = new HashSet<String>();
for (String basePackage : packages) {
if (StringUtils.isBlank(basePackage)) {
continue;
}
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
org.springframework.util.ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)) + "/" + DEFAULT_RESOURCE_PATTERN;
try {
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources) {
//检查resource,这里的resource都是class
String clazz = loadClassName(metadataReaderFactory, resource);
clazzSet.add(clazz);
}
} catch (Exception e) {
log.error("获取包下面的类信息失败,package:" + basePackage, e);
}
}
return clazzSet;
}
/**
* 排重、检测package父子关系,避免多次扫描
*
* @param scanPackages
* @return 返回检查后有效的路径集合
*/
private static Set<String> checkPackage(String scanPackages) {
if (StringUtils.isBlank(scanPackages)) {
return Collections.EMPTY_SET;
}
Set<String> packages = new HashSet<>();
//排重路径
Collections.addAll(packages, scanPackages.split(","));
String[] strings = packages.toArray(new String[packages.size()]);
for (String pInArr : strings) {
if (StringUtils.isBlank(pInArr) || pInArr.equals(".") || pInArr.startsWith(".")) {
continue;
}
if (pInArr.endsWith(".")) {
pInArr = pInArr.substring(0, pInArr.length() - 1);
}
Iterator<String> packageIte = packages.iterator();
boolean needAdd = true;
while (packageIte.hasNext()) {
String pack = packageIte.next();
if (pInArr.startsWith(pack + ".")) {
//如果待加入的路径是已经加入的pack的子集,不加入
needAdd = false;
} else if (pack.startsWith(pInArr + ".")) {
//如果待加入的路径是已经加入的pack的父集,删除已加入的pack
packageIte.remove();
}
}
if (needAdd) {
packages.add(pInArr);
}
}
return packages;
}
/**
* 加载资源,根据resource获取className
*
* @param metadataReaderFactory spring中用来读取resource为class的工具
* @param resource 这里的资源就是一个Class
*/
private static String loadClassName(MetadataReaderFactory metadataReaderFactory, Resource resource) {
try {
if (resource.isReadable()) {
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
if (metadataReader != null) {
return metadataReader.getClassMetadata().getClassName();
}
}
} catch (Exception e) {
log.error("根据resource获取类名称失败", e);
}
return null;
}
private static String getMethodKey(Method method) {
StringBuilder key = new StringBuilder(method.getName());
for (Parameter parameter : method.getParameters()) {
key.append(parameter.getType().getName())
.append(parameter.getName());
}
return key.toString();
}
}
3) サーバー側の Handler クラスを作成し、クライアント リクエストを受け取り、サービス実装クラスを呼び出してインターフェイスを実行します。
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端接入");
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到客户端消息:" + msg);
RequestData req = (RequestData) msg;
if (req != null) {
String args = req.getData();
String serviceId = req.getServiceId();
// 调用服务实现类
Object res = ServiceFacatory.invoke(serviceId, args);
ResponseData resp = new ResponseData();
resp.setData(res);
resp.setId(req.getId());
ctx.writeAndFlush(resp);
}
System.out.println("----------响应结束----------" + req.getData());
}
}
3) サーバー スタートアップ クラス MyServer、ServerApp を作成し、ポート モニタリングを開始し、コーデックとサーバー MyServerHandler を追加します。
public class MyServer {
public static void start(String hostname, int port) throws Exception {
ServiceFacatory.init();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap().group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new RequestMessageCodec());
pipeline.addLast(new ResponseMessageCodec());
pipeline.addLast(new MyServerHandler());
}
});
ChannelFuture future = bootstrap.bind(hostname, port).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
public class ServerApp {
public static void main(String[] args) throws Exception {
MyServer.start("127.0.0.1", 7000);
}
}
4.カスタムコーデック
1) String とリクエストパラメータオブジェクト RequestData の相互変換を実現するリクエストデータコーデック RequestMessageCodec を作成する
public class RequestMessageCodec extends MessageToMessageCodec<String, RequestData> {
@Override
protected void encode(ChannelHandlerContext ctx, RequestData msg, List<Object> out) throws Exception {
System.out.println("RequestMessageCodec.encode 被调用 " + msg);
String json = JSONObject.toJSONString(msg);
out.add(json);
}
@Override
protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
System.out.println("RequestMessageCodec.decode 被调用 " + msg);
RequestData po = JSONObject.parseObject(msg, RequestData.class);
out.add(po);
}
}
2) String と応答データオブジェクト ResponseData の相互変換を実現するサービス応答データコーデック ResponseMessageCodec を作成する
public class ResponseMessageCodec extends MessageToMessageCodec<String, ResponseData> {
@Override
protected void encode(ChannelHandlerContext ctx, ResponseData msg, List<Object> out) throws Exception {
System.out.println("ResponseMessageCodec.encode 被调用 " + msg);
String json = JSONObject.toJSONString(msg);
out.add(json);
}
@Override
protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
System.out.println("ResponseMessageCodec.decode 被调用 " + msg);
ResponseData po = JSONObject.parseObject(msg, ResponseData.class);
out.add(po);
}
}
3) リクエストを作成し、VO に応答する
@Data
public class RequestData {
private String id;
private String serviceId;
private String data;
}
@Data
public class ResponseData {
private String id;
private Object data;
}
@Data
public class SyncSendContext {
private ResponseData resp;
private RequestData request;
}
5.テストクライアント送信リクエストコードを書く
1) クライアント要求クラス ClientTest を作成して、データの送信をシミュレートします。
public class ClientTest {
public static void main(String[] args) throws Exception {
MyClient client = new MyClient();
IHelloService servie = client.getServie(IHelloService.class);
for (int i = 0; i < 5; i++) {
Thread.sleep(2 * 1000);
String res = servie.hello("你好 服务端 ~~ " + i);
System.out.println("service 得到服务端返回消息: " + res);
System.out.println("-----------------------------" + i +" 结束");
}
}
}
2) ServerApp を実行し、サーバーを起動し、ClientTest を実行して、データを送信するクライアントをシミュレートします。
クライアント ログ:
客户端和服务端链接成功
客户端向服务端发送消息: 你好 服务端 ~~ 0
RequestMessageCodec.encode 被调用 RequestData(id=d081f840-4367-42d3-909c-32f6a1654c60, serviceId=001, data=你好 服务端 ~~ 0)
ResponseMessageCodec.decode 被调用 {"data":"re:这里是服务端,已收到客户端消息:1845339960","id":"d081f840-4367-42d3-909c-32f6a1654c60"}
客户端收到服务端回复: ResponseData(id=d081f840-4367-42d3-909c-32f6a1654c60, data=re:这里是服务端,已收到客户端消息:1845339960)
service 得到服务端返回消息: re:这里是服务端,已收到客户端消息:1845339960
-----------------------------0 结束
客户端向服务端发送消息: 你好 服务端 ~~ 1
RequestMessageCodec.encode 被调用 RequestData(id=d49105b0-2624-43c2-bb19-c826987133f1, serviceId=001, data=你好 服务端 ~~ 1)
ResponseMessageCodec.decode 被调用 {"data":"re:这里是服务端,已收到客户端消息:1845339961","id":"d49105b0-2624-43c2-bb19-c826987133f1"}
客户端收到服务端回复: ResponseData(id=d49105b0-2624-43c2-bb19-c826987133f1, data=re:这里是服务端,已收到客户端消息:1845339961)
service 得到服务端返回消息: re:这里是服务端,已收到客户端消息:1845339961
-----------------------------1 结束
客户端向服务端发送消息: 你好 服务端 ~~ 2
RequestMessageCodec.encode 被调用 RequestData(id=13f82f4a-0a2f-41cc-8420-38ab20fab2d2, serviceId=001, data=你好 服务端 ~~ 2)
ResponseMessageCodec.decode 被调用 {"data":"re:这里是服务端,已收到客户端消息:1845339962","id":"13f82f4a-0a2f-41cc-8420-38ab20fab2d2"}
客户端收到服务端回复: ResponseData(id=13f82f4a-0a2f-41cc-8420-38ab20fab2d2, data=re:这里是服务端,已收到客户端消息:1845339962)
service 得到服务端返回消息: re:这里是服务端,已收到客户端消息:1845339962
-----------------------------2 结束
客户端向服务端发送消息: 你好 服务端 ~~ 3
RequestMessageCodec.encode 被调用 RequestData(id=f4576cbd-8ee5-438c-ae6d-810b836c177a, serviceId=001, data=你好 服务端 ~~ 3)
ResponseMessageCodec.decode 被调用 {"data":"re:这里是服务端,已收到客户端消息:1845339963","id":"f4576cbd-8ee5-438c-ae6d-810b836c177a"}
客户端收到服务端回复: ResponseData(id=f4576cbd-8ee5-438c-ae6d-810b836c177a, data=re:这里是服务端,已收到客户端消息:1845339963)
service 得到服务端返回消息: re:这里是服务端,已收到客户端消息:1845339963
-----------------------------3 结束
客户端向服务端发送消息: 你好 服务端 ~~ 4
RequestMessageCodec.encode 被调用 RequestData(id=68e67b0f-0c35-4ead-915e-e1890a0c0b53, serviceId=001, data=你好 服务端 ~~ 4)
ResponseMessageCodec.decode 被调用 {"data":"re:这里是服务端,已收到客户端消息:1845339964","id":"68e67b0f-0c35-4ead-915e-e1890a0c0b53"}
客户端收到服务端回复: ResponseData(id=68e67b0f-0c35-4ead-915e-e1890a0c0b53, data=re:这里是服务端,已收到客户端消息:1845339964)
service 得到服务端返回消息: re:这里是服务端,已收到客户端消息:1845339964
-----------------------------4 结束
サーバーログ:
RequestMessageCodec.decode 被调用 {"data":"你好 服务端 ~~ 0","id":"f876eccf-a034-467a-8b5a-4c6dba80cee2","serviceId":"001"}
收到客户端消息:RequestData(id=f876eccf-a034-467a-8b5a-4c6dba80cee2, serviceId=001, data=你好 服务端 ~~ 0)
ResponseMessageCodec.encode 被调用 ResponseData(id=f876eccf-a034-467a-8b5a-4c6dba80cee2, data=re:这里是服务端,已收到客户端消息:1845339960)
----------响应结束----------你好 服务端 ~~ 0
RequestMessageCodec.decode 被调用 {"data":"你好 服务端 ~~ 1","id":"bcceaa9b-09be-4dcc-9135-ac14caa365d1","serviceId":"001"}
收到客户端消息:RequestData(id=bcceaa9b-09be-4dcc-9135-ac14caa365d1, serviceId=001, data=你好 服务端 ~~ 1)
ResponseMessageCodec.encode 被调用 ResponseData(id=bcceaa9b-09be-4dcc-9135-ac14caa365d1, data=re:这里是服务端,已收到客户端消息:1845339961)
----------响应结束----------你好 服务端 ~~ 1
RequestMessageCodec.decode 被调用 {"data":"你好 服务端 ~~ 2","id":"ab0181b1-b3fe-42b7-ae17-d2a533c56098","serviceId":"001"}
收到客户端消息:RequestData(id=ab0181b1-b3fe-42b7-ae17-d2a533c56098, serviceId=001, data=你好 服务端 ~~ 2)
ResponseMessageCodec.encode 被调用 ResponseData(id=ab0181b1-b3fe-42b7-ae17-d2a533c56098, data=re:这里是服务端,已收到客户端消息:1845339962)
----------响应结束----------你好 服务端 ~~ 2
RequestMessageCodec.decode 被调用 {"data":"你好 服务端 ~~ 3","id":"6a4e6061-9ebe-4250-b939-2e5f314096fc","serviceId":"001"}
收到客户端消息:RequestData(id=6a4e6061-9ebe-4250-b939-2e5f314096fc, serviceId=001, data=你好 服务端 ~~ 3)
ResponseMessageCodec.encode 被调用 ResponseData(id=6a4e6061-9ebe-4250-b939-2e5f314096fc, data=re:这里是服务端,已收到客户端消息:1845339963)
----------响应结束----------你好 服务端 ~~ 3
RequestMessageCodec.decode 被调用 {"data":"你好 服务端 ~~ 4","id":"69c726e6-a3f1-487a-8455-ada02b4e97ed","serviceId":"001"}
收到客户端消息:RequestData(id=69c726e6-a3f1-487a-8455-ada02b4e97ed, serviceId=001, data=你好 服务端 ~~ 4)
ResponseMessageCodec.encode 被调用 ResponseData(id=69c726e6-a3f1-487a-8455-ada02b4e97ed, data=re:这里是服务端,已收到客户端消息:1845339964)
----------响应结束----------你好 服务端 ~~ 4
コードアドレス
https://gitee.com/personal_practice/netty-demo