手写rpc必须序列化对象且类定义严格一致,代理需同步调用,服务端应长连接复用并处理类加载器问题,还需传递调用上下文。

Socket 传对象前必须序列化,否则直接报 java.io.StreamCorruptedException
Java 的 Socket 只认字节流,不认对象。你往 ObjectOutputStream 里 write 一个 User 实例,对方用 ObjectInputStream 读不到——除非两边类定义完全一致,且该类实现了 Serializable,连 serialVersionUID 都得对得上。
常见错误现象:StreamCorruptedException: invalid type code: 00 或直接阻塞在 readObject();根本不是网络问题,是序列化链路断了。
- 服务端和客户端的接口、DTO 类必须**完全同包、同名、同字段、同
serialVersionUID**(建议显式声明,比如private static final long serialVersionUID = 1L;) - 别用 Lombok 的
@Data自动生成serialVersionUID—— 它不保证跨编译一致,手动写最稳 - 生产环境慎用原生序列化:性能低、有反序列化漏洞风险(如
CommonsCollections链),测试阶段可用,但心里得清楚这只是临时跳板
动态代理生成远程接口实现时,InvocationHandler 里必须同步发请求并等响应
你用 Proxy.newProxyInstance() 生成一个 UserService 代理,调用 getUser(123) 时,实际执行的是 invoke() 方法。这里不能只发请求就返回,否则调用方拿到的是 null 或未完成的 Future。
使用场景:客户端拿到代理对象后,期望行为和本地调用一致 —— 阻塞直到结果回来。
立即学习“Java免费学习笔记(深入)”;
-
invoke()内部必须用Socket发送方法名、参数类型、参数值(序列化后),然后ObjectInputStream.readObject()同步等待服务端返回 - 别忘了处理异常:服务端抛异常时,也要把异常对象序列化传回,客户端在
invoke()中重新 throw,否则调用方感知不到业务错误 - 如果用 NIO 或 Netty 后期替换,这里就是最大改造点 —— 同步模型要切到回调或 CompletableFuture,但接口语义不能变
服务端用 ServerSocket.accept() 处理连接,但一个连接不能只服务一次
新手常写成“accept → 读一次 → 关闭 socket”,结果并发调用直接失败。RPC 是长连接复用场景,不是 HTTP 那种一来一走。
性能影响明显:频繁建连/断连开销远大于内存里多维持几个 socket 流。
- 每个
Socket接收后,应启动一个独立线程(或丢进线程池),在其内部循环readObject()—— 直到流关闭或超时 - 协议层面需要简单分帧:比如先读 4 字节表示后续字节数,再读具体内容,否则多个请求粘包,
ObjectInputStream会卡死或错位 - 别让一个 socket 线程卡在某个慢方法里,超时控制得做在读取和处理两个环节:socket 读超时(
setSoTimeout)、业务执行超时(单独计时器或 future.get(timeout))
接口与实现分离时,ClassLoader 加载不到实现类会导致 ClassNotFoundException
服务端收到方法名和参数,靠反射调用本地实现类。但如果实现类不在当前类路径,或被不同 ClassLoader 加载过,Class.forName() 就会炸。
这是本地跑通、打包后运行就挂的典型坑。
- 服务端注册实现类时,别只存 class 名字符串,最好缓存
Class对象本身,并确保它由当前线程上下文类加载器(Thread.currentThread().getContextClassLoader())加载 - 避免在
static块里提前加载实现类 —— 此时上下文类加载器可能是 Bootstrap 或 Extension,不是你的应用类加载器 - 调试技巧:打印
clazz.getClassLoader()和Thread.currentThread().getContextClassLoader()是否一致,不一致就用后者去loadClass
手写 RPC 最容易被忽略的,其实是调用上下文传递 —— 比如 traceId、用户身份、超时时间,这些没法塞进接口方法签名,又不能硬编码在代理逻辑里。往后加 Filter 链或 Invocation 对象包装时,这儿就是第一个撕裂口。










