简介面向Java与Android开发者的SignalR实时通信客户端库源码包用于在非浏览器环境中接入ASP.NET SignalR服务实现服务器向客户端实时推送。ASP.NET SignalR是一套成熟的实时Web通信库而这份Java客户端扩展了它的使用边界让桌面Java应用和Android应用也能获得服务器消息主动送达的能力。资源为zip压缩包共141个文件以Java源代码为主体另有Gradle构建脚本、XML配置文件、ProGuard混淆规则、Git忽略文件及PNG图标等压缩包仅214KB结构紧凑便于直接查看核心实现。目前已有459人学习下载。对于需要在普通Java桌面应用或Android应用中集成实时功能的团队可直接使用或参考其中的连接管理、事件分发与消息处理逻辑省去自行维护WebSocket连接、心跳和重连等底层工作。包含的build.gradle等构建配置齐全可导入Android Studio编译运行也可抽取源码模块嵌入既有工程。适合具备一定Android/Java基础、希望快速为应用补充推送通知、在线协作或实时数据展示能力的开发者。1. 一个 Java 后端连上 SignalR 的完整落地过程不只是把 URL 换成 https接手过一个用 .NET 写的实时推送服务服务端用的是 SignalR Hub前端网页和 .NET 客户端都连得好好的轮到我这边的 Java 服务要接入时翻了半天官方文档才发现 SignalR Java 客户端是真实存在的而且已经比较成熟。不是调用 REST 接口轮询而是通过 WebSocket 或长轮询维持一条长连接服务端可以主动往 Java 这边推消息延迟在毫秒级。做订单状态同步、工单提醒、在线用户列表这类场景非常合适。这篇就把我接入过程中的核心步骤和踩过的坑写清楚尤其是从 http 切到 https 后遇到的那一串证书和握手的问题。2. 传输机制与选型为什么 Java 客户端能连上 .NET 的 Hub2.1 三种传输方式WebSocket、Server-Sent Events、长轮询SignalR 的 Java 客户端不是自己去实现一遍 .NET 端的 Hub 协议而是走标准的 SignalR 协议基于 JSON 或 MessagePack 序列化。连接建立后客户端和服务端之间的交互分为两个阶段先是 negotiate协商阶段客户端发一个 HTTP 请求到/negotiate端点服务端返回该连接支持的传输方式和连接 ID然后才是真正的消息传输阶段。Java 客户端库支持三种传输方式传输方式底层实现适用场景注意点WebSocket长连接双向全双工生产环境首选延迟最低服务端和网络代理必须支持 WebSocketServer-Sent EventsHTTP 长连接服务端单向推送只需要服务端推、客户端不需要频繁发客户端库对 SSE 支持在部分代理环境下有兼容问题Long Polling轮询 挂起网络环境限制 WebSocket 和 SSE 时兜底延迟最高连接数多时服务端压力大默认情况下客户端会按服务端返回的传输列表顺序选择可用的方式。TransportEnum是客户端库里的枚举类型可以在构建连接时手动指定。我一般会显式指定 WebSocket因为 SSE 和 Long Polling 在跨网络比如经过某些网关时问题更多。2.2 版本选择与 Maven 依赖5.x 是分水岭SignalR Java 客户端从 3.0 开始随 .NET Core 版本同步发版。当前主流的 5.x 版本包名是com.microsoft.signalrGAV 坐标如下dependency groupIdcom.microsoft.signalr/groupId artifactIdsignalr/artifactId version5.0.0/version /dependency注意5.0.0和3.1.0版本的 API 基本一致但 5.x 修复了若干重连时的状态清理问题。如果你现在还在用 3.x 的旧包建议切到 5.x 再联调。从 6.0 开始客户端库的包结构有变化但 Java 侧依旧沿用com.microsoft.signalr.*的命名空间。我自己用的 5.0.0 在 JDK 8 和 JDK 11 下都验证过没有遇到兼容性问题。2.3 服务端点与协议版本先抓包确认再写代码接入前先用浏览器开发者工具或 Postman 观察一下服务端/negotiate返回的 JSON 结构这是最快摸清服务端配置的方式。正常返回大概是{ connectionId: xxx-xxx-xxx, availableTransports: [ { transport: WebSockets, transferFormats: [Text, Binary] }, { transport: ServerSentEvents, transferFormats: [Text] }, { transport: LongPolling, transferFormats: [Text] } ] }如果服务端返回的协议版本不兼容比如服务端配置了MaximumReceiveMessageSize低于客户端要发送的消息体大小握手阶段会直接报错。抓包看这一层比盲调代码快得多。3. 接入步骤从依赖引入到消息收发全流程3.1 构建连接核心参数与含义先写一个最简的客户端连接类把连接构建、事件注册、启动串起来import com.microsoft.signalr.*; import java.util.concurrent.TimeUnit; public class SignalRClient { private HubConnection hubConnection; public void connect(String serverUrl, String hubName, String accessToken) { // 构建连接对象 HubConnectionBuilder builder HubConnectionBuilder .create(serverUrl / hubName) .withTransport(TransportEnum.WEBSOCKETS) // 强制走 WebSocket .withAccessTokenProvider(single - accessToken) // 动态提供 token .withHandshakeResponseTimeout(10, TimeUnit.SECONDS) .withServerTimeout(30, TimeUnit.SECONDS) .withClientTimeout(30, TimeUnit.SECONDS) .withSkipNegotiate(false); // 保留 negotiate 阶段 hubConnection builder.build(); // 注册连接生命周期回调 hubConnection.onClosed(error - { System.out.println(连接关闭: (error null ? 正常关闭 : error.getMessage())); }); hubConnection.start().block(); // 阻塞等待连接建立 System.out.println(连接建立成功); } }上面代码里有几个参数需要细说withTransport(TransportEnum.WEBSOCKETS)强制只走 WebSocket。如果不调用这个方法客户端会按服务端返回的传输列表逐个尝试存在一定的不可控性。withAccessTokenProvider传入的是一个SingleString类型的供应商函数连接和重连时都会回调。适合 token 有失效时间、需要动态刷新的场景。withHandshakeResponseTimeout握手超时时间默认 10 秒。如果你的服务端在握手前有较重的鉴权逻辑比如查库校验 token适当调大到 15 秒或 20 秒否则容易误报超时。withServerTimeout和withClientTimeout这是双向的心跳超时控制。服务端每 15 秒发一次心跳客户端如果在withServerTimeout内没收到任何消息就认为连接死了同理客户端也要在withClientTimeout内发心跳。这两个值不建议设得太小否则网络抖动会频繁触发重连。3.2 注册方法映射接收服务端推送与调用 Hub 方法SignalR 是 RPC 式的通信模型。Java 客户端可以通过hubConnection.on(...)注册方法来接收服务端的推送这里方法是通过字符串名称匹配的对应 Hub 里定义的.SendAsync(方法名, args)或.Clients.All.SendAsync(方法名, args)。// 接收服务端推送的更新 hubConnection.on(OrderStatusChanged, (orderId, newStatus) - { System.out.println(订单 orderId 状态变化为: newStatus); }, String.class, String.class); // 调用 Hub 上的业务方法 hubConnection.invoke(JoinGroup, group-1024) .doOnSuccess(v - System.out.println(加入分组成功)) .doOnError(e - System.err.println(加入分组失败: e.getMessage())) .subscribe();on方法的最后一个参数列表是可变参数每个Class?对应 Hub 方法中的一个参数类型。类型必须与服务端 C# 方法签名匹配否则反序列化会抛异常。常见的匹配规则是C#string↔ JavaString.classC#int/long↔ JavaInteger.class/Long.classC#bool↔ JavaBoolean.classC# 自定义类 ↔ Java 自定义 POJO要求 JSON 字段名能映射invoke返回CompletableFuture可以继续链式调doOnSuccess和doOnError。注意这里抛出的异常不一定是SignalRException也可能是 JSON 反序列化异常JsonSyntaxException所以异常处理要写得宽一点不要只捕获单一类型。3.3 消息帧与序列化为什么服务端发的对象经常解析失败SignalR 默认用 JSON 做序列化Java 客户端底层用的是Jackson。服务端默认序列化时遵循 C# 的命名规则属性是PascalCase首字母大写比如OrderId、OrderStatus。而 Java 侧的习惯是camelCase。两边一碰最常见的问题就是字段对不上。// 服务端推送的对象可能是这样: // { OrderId: A1001, OrderStatus: SHIPPED } // Java 侧接收对象定义: public class OrderMessage { JsonProperty(OrderId) private String orderId; JsonProperty(OrderStatus) private String orderStatus; // getter / setter 省略 }如果你不想在字段上加注解可以改服务端的AddJsonProtocol配置让它输出 camelCase 格式。但生产环境里服务端往往不是你说了算所以 Java 侧用JsonProperty做映射是最稳妥的做法。提示withSkipNegotiate(false)表示保留 negotiate 阶段这是默认值。但如果你的服务端直接走 WebSocket、不允许单独发 negotiate 请求比如某些定制网关可以尝试withSkipNegotiate(true)让连接直接走 WebSocket 并跳过 /negotiate。这个参数在标准 .NET SignalR 服务端上需要额外配置才能生效默认情况下不要改。4. HTTPS 接入专题证书校验、hostname 校验与两个典型翻车现场4.1 https 连接与证书信任Java 客户端的默认行为把serverUrl从http://改成https://不是改个字符串就完了。Java 的HttpClient和 WebSocket 客户端在连接时默认会做两件事校验服务端证书链是否受信任PKIX校验校验服务端证书的域名是否和连接 URL 的 host 一致hostname校验如果你的服务端用的是公司内部 CA 签发的证书Java 默认的cacerts信任库里面没有这条信任链连接会直接抛SSLHandshakeException: PKIX path building failed。如果你的服务端用的是公网证书比如某云厂商的免费证书但域名是 IP 而不是域名则会报No subject alternative names present。这两个问题都是真实项目中高频遇到的。下面给出对应的解决方式。4.2 信任自签名证书用 JKS 加载或全局 TrustManager如果服务端用的是自签名证书或内网 CA 证书最稳妥的做法是把证书导入到 JKS 信任库。import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import java.io.FileInputStream; import java.security.KeyStore; public SSLContext buildSslContext() throws Exception { KeyStore trustStore KeyStore.getInstance(JKS); try (FileInputStream fis new FileInputStream(/path/to/truststore.jks)) { trustStore.load(fis, changeit.toCharArray()); } TrustManagerFactory tmf TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); SSLContext sslContext SSLContext.getInstance(TLS); sslContext.init(null, tmf.getTrustManagers(), null); return sslContext; }这里的关键点是KeyStore.getInstance(JKS)也可以用PKCS12看你的信任库文件实际格式。生成信任库的命令是keytool -import -alias myServer -keystore truststore.jks -file server.crt加载完SSLContext之后还要把它设置给 SignalR 客户端但 SignalR Java 客户端的HubConnectionBuilder里没有直接暴露设置SSLContext的方法。常见做法是自定义OkHttpClient或底层的HttpClient传给HubConnection。SignalR 5.x Java 客户端的底层 HTTP 客户端和 WebSocket 实现是分离的HTTP 层走 JDK 的 HttpClient或 OkHttp取决于你引入的依赖WebSocket 层走 OkHttp。实测中只设置TrustManager还不够还要把 OkHttp 的自定义SslSocketFactory设置进去。以 OkHttp 为例完整的做法是在HubConnectionBuilder之外构建一个带自定义 SocketFactory 的 OkHttpClient然后通过withHttpClient传入。4.3 关闭 hostname 校验的利弊与正确姿势如果不方便导入证书还有一种临时做法是「全部信任 跳过 hostname 校验」。这个方案在本地联调时效率最高但不建议直接搬到生产环境。import javax.net.ssl.*; import java.security.cert.X509Certificate; TrustManager[] trustAllCerts new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] chain, String authType) {} public void checkServerTrusted(X509Certificate[] chain, String authType) {} } };这是一个标准的「信任所有证书」的写法问题在于把checkServerTrusted和checkClientTrusted直接置空意味着任何证书都放行。如果代码被提交到生产环境且没有加环境开关等于整个通信链路裸奔。所以我的习惯是本地联调写在Profile(dev)这类环境配置内测试环境走自建 CA 或临时导入证书生产环境必须走标准证书链路任何跳过校验的代码都不允许进主分支注意跳过 hostname 校验和跳过证书校验是两码事。有些场景是证书合法但域名对不上比如用 IP 访问这种情况只需要自定义HostnameVerifier证书校验可以保留。不要一上来就把两者全部关掉。5. 同避坑与排查我在接入 HTTPS 时踩过的四个真实问题5.1 连接超时握手明明成功却一直进不了 onClosed现象hubConnection.start()没有抛异常但业务消息一直收不到onClosed也没有触发。原因服务端配置的MaximumReceiveMessageSize小于客户端要接收的消息体大小。握手阶段不会暴露这个问题但消息传输阶段会静默失败。这种问题在低频小消息场景下几乎不出现一旦有大数据推送比如批量列表就立刻复现。解决在代码里先调hubConnection.invoke(Ping)或任意最小方法确认服务端能正常返回如果有批量推送的场景先在测试环境把服务端MaximumReceiveMessageSize调大再压测验证。客户端侧没有这个限制纯服务端配置。5.2 WebSocket 握手 400代理上没开 Upgrade 头现象本地直连服务端一切正常发到测试环境前面有 Nginx 或网关代理就报WebSocket connection failed错误码 400。原因从 http 切到 https 后如果代理层只配了普通 TCP 转发没有配置 WebSocket 协议升级HTTP Upgrade 头会被丢弃服务端只能按普通 HTTP 请求处理。解决代理层需要显式开启 WebSocket 支持。Nginx 配置里要加proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;。另外如果你强制走 WebSocket 但服务端代理只开了 SSE也会在握手阶段失败。排查方式是抓包看/negotiate响应里的availableTransports是否包含WebSockets没有就无法走通。5.3 https 下收到证书校验失败但浏览器访问没问题现象用浏览器打开https://服务端地址显示证书有效但 Java 客户端连不上报PKIX path building failed。原因浏览器用的是操作系统信任库Java 用的是 JDK 自带的cacerts。即使操作系统信任了某张证书JDK 未必信任。特别是公司自建 CA 签发的情况JVM 根证书列表里根本没有这条信任链。解决把服务端的 CA 证书导入到 JDK 的cacerts或者用独立的 truststore 文件加载。命令如下keytool -import -alias internalCa -keystore $JAVA_HOME/lib/security/cacerts -file internal-ca.crt提示修改cacerts是全局生效的会影响这台机器上所有 Java 应用。如果只影响单个应用用独立 truststore 更干净别动不动就改 JDK 全局文件。5.4 MessagePack 协议下 Java 客户端收到乱码现象服务端配了 MessagePack 协议Java 端收到数据不是 JSON直接显示乱码或解析失败。原因Java 客户端 5.x 默认只支持 JSON。要支持 MessagePack需要额外引入序列化包。解决先确认服务端是不是真的开了 MessagePackAddMessagePackProtocol如果是Java 端引入兼容的 MessagePack 序列化依赖或者在服务端把协议改回 JSON。注意这个是两边协议栈问题不是 https 导致的但在 https 环境下更难排查因为证书报错会先于协议报错出现。6. 验证与压测连接稳定性检查和重连策略验证这部分是我实际项目里最后做的事也是区分「能连上」和「能上线」的关键。6.1 连接稳定性验证脚本先写一个简单的连接监控类记录连接状态变化和最近心跳时间便于判断是网络抖动还是服务端主动断开public class ConnectionMonitor { private Instant lastMessageTime Instant.now(); private int reconnectCount 0; public void attach(HubConnection connection) { connection.onClosed(e - { System.out.println(连接断开准备重连原因: e); reconnectCount; }); // 可以在每个业务回调里更新 lastMessageTime // 如果 2 个 serverTimeout 内没有消息就主动重连 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { long idleSec Duration.between(lastMessageTime, Instant.now()).getSeconds(); if (idleSec 60) { System.err.println(心跳超时尝试重连); connection.stop().block(); connection.start().block(); } }, 0, 30, TimeUnit.SECONDS); } }onClosed里只记录重连次数不直接启动重连避免和 SignalR 内部自动重连逻辑冲突。如果你用的是 5.0.0 版本HubConnection内部并没有像 .NET 端那样内置自动重连那是 6.0 之后才有的功能所以生产环境必须在业务层自己实现重连逻辑。上面的写法是一种偏保守的策略——先停掉连接再重新 start每次重连前都打印日志。6.2 压测时看什么指标实时推送服务的压测不能只看 TPS更要看连接保持率。我的验证维度指标通过标准并发连接数500 个连接保持 30 分钟不掉线消息延迟99 线低于 1 秒同一机房内断线恢复手动杀掉服务端进程后300 秒内客户端全部恢复内存占用500 连接 每秒 10 条广播JVM 堆不持续增长如果压测中发现偶发断连先看服务端的日志是不是因为KeepAlive超时主动断了连接。SignalR 服务端有个KeepAliveInterval默认 15 秒如果客户端在ServerTimeout内没收到心跳会自动断开。对应的客户端的ServerTimeout默认 30 秒建议客户端设置不小于服务端KeepAliveInterval的两倍。6.3 https 证书切换后一定要跑一遍全链路回归证书从 http 切到 https 后不只是连接层变化。我遇到过一个问题服务端在 https 环境下negotiate响应里的 URL 会跟随请求协议动态生成但某些代理会把内网 http 地址返回给客户端导致客户端拿到一个http://的 WebSocket 地址最终混合内容混合协议被浏览器或 Java 客户端拒绝。这个问题的排查方法很直接抓包看/negotiate响应里的url字段是不是https://如果是http://那基本可以断定代理配置有问题。后来我在做自动化回归时专门加了一条检查规则任何包含ws://或http://内网地址的 negotiate 响应直接判失败只有wss://和https://才通过。从那以后每次环境切换dev → test → prod我都强制走一遍连接监控脚本连续观察 10 分钟心跳日志确认negotiate返回的 URL 协议与实际环境一致再放行。这条习惯帮我挡掉了至少三次代理配置问题引发的线上事故。希望帮到你。本文还有配套的精品资源点击获取