├── .gitignore ├── HELP.md ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── oujiong │ └── exchange │ └── client │ ├── Application.java │ ├── common │ └── AbstractWebSocketClient.java │ ├── enums │ └── ExchangeMarketEnum.java │ ├── huobi │ ├── HuoBiProWebSocketClient.java │ ├── HuoBiProWebSocketClientHandler.java │ ├── Topic.java │ └── service │ │ ├── HuoBiProWebSocketService.java │ │ ├── HuobiProMainService.java │ │ └── impl │ │ ├── HuoBiProWebSocketServiceImpl.java │ │ └── HuobiProMainServiceImpl.java │ ├── scheduler │ └── HuobiTrigger.java │ └── utils │ ├── GZipUtils.java │ └── MonitorTask.java └── resources └── application.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /**/.DS_Store 2 | /logs/ 3 | /dubbo/ 4 | /**/*.iml 5 | /**/test/**/*.java 6 | 7 | target/ 8 | data/ 9 | !.mvn/wrapper/maven-wrapper.jar 10 | 11 | ### STS ### 12 | .apt_generated 13 | .classpath 14 | .factorypath 15 | .project 16 | .settings 17 | .springBeans 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | 25 | ### NetBeans ### 26 | nbproject/private/ 27 | build/ 28 | nbbuild/ 29 | dist/ 30 | nbdist/ 31 | 32 | 33 | .nb-gradle/ 34 | .settings 35 | .classpath 36 | .project 37 | .springBeans 38 | mvnw 39 | mvnw.cmd 40 | .mvn 41 | /bin/ 42 | 43 | 44 | .flattened-pom.xml -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 有关该项目中的代码部分简单解释,可以查看博客。 3 | 4 | 1、[Netty+WebSocket 获取火币交易所时时数据项目](https://www.cnblogs.com/qdhxhz/p/11280533.html) 5 | 6 | ### 简介 7 | 8 | 本项目使用SpringBoot+Netty来开发WebSocket服务器,与火币交易所Websocket建立连接,时时获取火币网交易所推送过来的交易对最新数据。 9 | 10 | 项目本身也是我在之前公司为了获取各大交易所数据所开发的项目,现在只是重新整理了下代码,现在它更像一个脚手架项目,可以在此基础上很方便的添加其它交易所。 11 | 12 | 13 | ## 一、项目概述 14 | 15 | #### 1、技术架构 16 | 17 | ``` 18 | SpringBoot2.1.5 + Netty4.1.25 + Maven3.5.4 + lombok(插件) 19 | ``` 20 | 21 | #### 2、项目整体结构 22 | 23 | ```makefile 24 | spring-boot-netty-websocket-huobi # 项目名称 25 | | 26 | ---src 27 | | 28 | ---com.jincou.exchange.client 29 | |# 连接交易所websocket的公共类 30 | ---common 31 | |# 枚举类 32 | ---enums 33 | |# 获取火币网交易所数据具体实现类 34 | ---huobi 35 | |# 定时任务 36 | ---scheduler 37 | |# 工具类 38 | ---util 39 | ---resources 40 | | # 配置类 41 | ---application.yml 42 | ``` 43 | 44 | `说明` 45 | 46 | 从这个项目的架构来看,当你需要添加获取一个新的交易所数据的时候,也很方便,因为这里已经把公共部分抽离出来了。 47 | 48 | #### 3、项目测试 49 | 50 | 直接启动Springboot启动类Application.java,就可以获取火币网推送过来交易对的数据了。 51 | 52 | 如图 53 | 54 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190731184311639-1781494089.gif) 55 | 56 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190731185211763-899166739.png) 57 | 58 | 59 | ## 二、需要注意的点 60 | 61 | #### 1、服务器问题 62 | 63 | 一般交易所的服务器都在国外,所以我们本地是无法建立Websocket连接的,除非本地翻墙。 64 | 65 | 同样项目也不能部署到阿里云等国内服务器,你只能选择香港或者国外服务器部署项目。 66 | 67 | 这里是火币网专门为我们提供的国内测试地址,所以本地可以获取数据。 68 | 69 | #### 2、获取交易所最新交易对数据问题 70 | 71 | 我们在向交易所Websocket订阅交易对的时候,首先就是要知道该交易所有哪些交易对,这份数据是需要我们单独去获取的,而且不是一次获取就好了。 72 | 73 | 因为该交易所可能新增或者删除交易对。所以需要我们通过定时任务去获取更新最新的交易对数据。 74 | 75 | 我这边只是模拟了一个交易对`btcusdt`,并没有提供获取最新交易对数据的服务。 76 | 77 | #### 3、数据存储问题 78 | 79 | 这也是最值得思考的一个问题,数据我们是获取了,但如果保存! 80 | 81 | 正常合理的开发应该获取数据是一个微服务,处理获取的数据是一个微服务。那么只需要获取数据后去调处理数据微服务就可以保存数据了。 82 | 83 | 但在这里,如果只是这样是行不通的。 84 | 85 | 因为**火币网向我们推送的消息的速度会比我们调其它服务保存的数据要快,这就会存在数据丢失的情况发生**。 86 | 87 | 这里仅仅是输出一个`btcusdt`交易对,并且只是订阅一个`k线`主题,而实际上交易所会有上百个交易对和几种订阅主题, 88 | 89 | 这样的消息推送速度是上面的几百倍。所以你会发现如果你不做任何改动,对于一些大的交易所而言,你的数据是来不及存储的。 90 | 91 |
92 | 93 | `补充` 94 | 95 | 这边之前也写过有关 Netty 和 Websocket 相关的博客文章,可以做个参考 96 | 97 | [1、Netty专题(共9篇)](https://www.cnblogs.com/qdhxhz/category/1343708.html) 98 | 99 | [2、Websocket专题(共5篇)](https://www.cnblogs.com/qdhxhz/category/1166311.html) 100 | 101 | 102 | ![acda64387e0896604b5932dc433c8b77](https://user-images.githubusercontent.com/37285812/142141781-77ea9968-d8ea-4bdd-87a8-1e88aae39b9a.gif) 103 | 104 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.5.RELEASE 9 | 10 | 11 | com.jincou 12 | exchange.spider.client 13 | 0.0.1-SNAPSHOT 14 | exchange.spider.client 15 | spring-boot-netty-websocket-huobi 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | io.netty 32 | netty-all 33 | 4.1.25.Final 34 | 35 | 36 | com.alibaba 37 | fastjson 38 | 1.2.31 39 | 40 | 41 | org.apache.commons 42 | commons-lang3 43 | 3.5 44 | 45 | 46 | commons-collections 47 | commons-collections 48 | 3.2.2 49 | 50 | 51 | org.projectlombok 52 | lombok 53 | true 54 | 55 | 56 | com.google.guava 57 | guava 58 | 20.0 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/Application.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @Description: 启动类 8 | * 9 | * @author xub 10 | * @date 2019/7/27 下午7:07 11 | */ 12 | @SpringBootApplication 13 | public class Application { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/common/AbstractWebSocketClient.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.common; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.oujiong.exchange.client.utils.MonitorTask; 5 | import io.netty.bootstrap.Bootstrap; 6 | import io.netty.channel.*; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.SocketChannel; 9 | import io.netty.channel.socket.nio.NioSocketChannel; 10 | import io.netty.handler.codec.http.HttpClientCodec; 11 | import io.netty.handler.codec.http.HttpObjectAggregator; 12 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 13 | import io.netty.handler.ssl.SslContext; 14 | import io.netty.handler.ssl.SslContextBuilder; 15 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.net.URI; 19 | import java.nio.channels.UnsupportedAddressTypeException; 20 | import java.util.Set; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.ScheduledExecutorService; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * @Description: 抽离出 父类 27 | * 以后主要新接入一家交易所,都需要继承该父类 28 | * 29 | * @author xub 30 | * @date 2019/7/30 下午5:30 31 | */ 32 | @Slf4j 33 | public abstract class AbstractWebSocketClient { 34 | 35 | protected Channel channel; 36 | /** 37 | * 所有交易所的交易对 38 | */ 39 | protected Set subChannel = Sets.newHashSet(); 40 | protected EventLoopGroup group; 41 | protected MonitorTask monitorTask; 42 | private ScheduledExecutorService scheduledExecutorService; 43 | 44 | /** 45 | * 启动订阅netty 46 | */ 47 | public void start() { 48 | //建立连接 49 | this.connect(); 50 | //这里的线程组用来查看本地webocket有没有与交易所的websocket断开 51 | monitorTask = new MonitorTask(this); 52 | scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 53 | scheduledExecutorService.scheduleWithFixedDelay(monitorTask, 0,5000, TimeUnit.MILLISECONDS); 54 | } 55 | 56 | public abstract void connect(); 57 | 58 | /** 59 | * 连接WebSocket, 60 | * 61 | * @param uri url构造出URI 62 | * @param handler 处理消息 63 | */ 64 | protected void connectWebSocket(final URI uri, SimpleChannelInboundHandler handler) { 65 | try { 66 | String scheme = uri.getScheme() == null ? "http" : uri.getScheme(); 67 | final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); 68 | final int port; 69 | 70 | if (uri.getPort() == -1) { 71 | if ("http".equalsIgnoreCase(scheme) || "ws".equalsIgnoreCase(scheme)) { 72 | port = 80; 73 | } else if ("wss".equalsIgnoreCase(scheme)) { 74 | port = 443; 75 | } else { 76 | port = -1; 77 | } 78 | } else { 79 | port = uri.getPort(); 80 | } 81 | 82 | if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) { 83 | System.out.println("Only WS(S) is supported"); 84 | throw new UnsupportedAddressTypeException(); 85 | } 86 | final boolean ssl = "wss".equalsIgnoreCase(scheme); 87 | final SslContext sslCtx; 88 | if (ssl) { 89 | sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); 90 | } else { 91 | sslCtx = null; 92 | } 93 | 94 | group = new NioEventLoopGroup(2); 95 | //构建客户端Bootstrap 96 | Bootstrap bootstrap = new Bootstrap(); 97 | bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer() { 98 | @Override 99 | protected void initChannel(SocketChannel ch) throws Exception { 100 | ChannelPipeline pipeline = ch.pipeline(); 101 | if (sslCtx != null) { 102 | pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port)); 103 | } 104 | //pipeline可以同时放入多个handler,最后一个为自定义hanler 105 | pipeline.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192), handler); 106 | } 107 | }); 108 | channel = bootstrap.connect(host, port).sync().channel(); 109 | } catch (Exception e) { 110 | log.error(" webSocketClient start error.", e); 111 | if (group != null) { 112 | group.shutdownGracefully(); 113 | } 114 | } 115 | } 116 | 117 | public boolean isAlive() { 118 | return this.channel != null && this.channel.isActive(); 119 | } 120 | 121 | public void sendMessage(String msg) { 122 | if (!isAlive()) { 123 | log.warn("webSocket is not alive addchannel error."); 124 | return; 125 | } 126 | log.info("send:" + msg); 127 | this.channel.writeAndFlush(new TextWebSocketFrame(msg)); 128 | } 129 | 130 | /** 131 | * 断开与交易所的连接 也需要关闭线程组 不然线程组还会重新去连接 132 | */ 133 | public void close() { 134 | monitorTask = null; 135 | scheduledExecutorService.shutdown(); 136 | 137 | } 138 | 139 | public abstract void sendPing(); 140 | 141 | public abstract void addChannel(String channel); 142 | 143 | public abstract void removeChannel(String channel); 144 | 145 | public abstract void reConnect(); 146 | 147 | public abstract void onReceive(String msg); 148 | } -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/enums/ExchangeMarketEnum.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.enums; 2 | 3 | /** 4 | * @Description: 获取各大交易所的最新交易对消息 5 | * 6 | * @author xub 7 | * @date 2019/7/30 下午4:37 8 | */ 9 | public enum ExchangeMarketEnum { 10 | 11 | /** 12 | * https://www.zb.cn/i/developer 13 | */ 14 | ZB(1), 15 | 16 | /** 17 | * okex http://www.okex.com 18 | */ 19 | OKEX(2), 20 | 21 | /** 22 | * 币安 https://www.binance.com/ 23 | */ 24 | BINANCE(3), 25 | 26 | /** 27 | * 火币网 https://www.huobi.com/zh-cn/ 28 | */ 29 | HUOBI_PRO(4), 30 | 31 | /** 32 | * bitfinex https://www.bitfinex.com/ 33 | */ 34 | BITFINEX(6), 35 | 36 | /** 37 | * https://api.hitbtc.com/ 38 | */ 39 | HITBTC(10), 40 | 41 | /** 42 | * https://www.upbit.com/ 43 | */ 44 | UPBIT(12), 45 | 46 | /** 47 | * https://apidoc.bit-z.pro/cn/ 48 | */ 49 | BITZ(17), 50 | 51 | /** 52 | * https://support.bittrex.com/hc/en-us/articles/115003723911 53 | */ 54 | BITTREX(38), 55 | 56 | /** 57 | * https://www.kucoin.com/#/ 58 | */ 59 | KUCOIN(73), 60 | 61 | /** 62 | * https://support.bittrex.com/hc/en-us/articles/115003723911 63 | */ 64 | EXX(92), 65 | 66 | /** 67 | * https://cn.bitforex.com/ 68 | */ 69 | BITFOREX(256), 70 | 71 | /** 72 | * https://www.58coin.com/ 73 | */ 74 | CIOIN58(257), 75 | 76 | /** 77 | *https://pro.coinbase.com/ 78 | */ 79 | COINBASE(260), 80 | 81 | /** 82 | * https://www.fcoin.com/ 83 | */ 84 | FCOIN(46), 85 | 86 | /** 87 | * https://gateio.io/ 88 | */ 89 | GATE(53), 90 | 91 | /** 92 | * https://www.bibox.com/ 93 | */ 94 | BIBOX(16), 95 | 96 | /** 97 | * https://www.bithumb.com/ 98 | */ 99 | BITHUMB(14); 100 | 101 | private final int code; 102 | 103 | ExchangeMarketEnum(int code) { 104 | this.code = code; 105 | } 106 | 107 | public int getCode() { 108 | return code; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/HuoBiProWebSocketClient.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.oujiong.exchange.client.common.AbstractWebSocketClient; 5 | import com.oujiong.exchange.client.huobi.service.HuoBiProWebSocketService; 6 | 7 | 8 | import io.netty.handler.codec.http.DefaultHttpHeaders; 9 | import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory; 10 | import io.netty.handler.codec.http.websocketx.WebSocketVersion; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import java.net.URI; 15 | import java.util.UUID; 16 | 17 | /** 18 | * @Description: 火币网websckoet类 19 | * 20 | * @author xub 21 | * @date 2019/7/30 下午7:07 22 | */ 23 | @Slf4j 24 | public class HuoBiProWebSocketClient extends AbstractWebSocketClient { 25 | 26 | private HuoBiProWebSocketService service; 27 | 28 | /** 29 | * 订阅的client Id 30 | */ 31 | private String subId = StringUtils.EMPTY; 32 | 33 | public HuoBiProWebSocketClient(HuoBiProWebSocketService service) { 34 | this.service = service; 35 | } 36 | 37 | @Override 38 | public void connect() { 39 | try { 40 | subId = UUID.randomUUID().toString(); 41 | //TODO 注意这里的订阅地址来自火币网 如果连不上可以试下其它地址。当然也可能需要外网才能访问 火币官方API https://www.huobi.com/zh-cn/ 42 | String url = "wss://api.huobi.pro/ws"; 43 | final URI uri = URI.create(url); 44 | 45 | //自定义handle加入Pipeline管道中 46 | HuoBiProWebSocketClientHandler handler = new HuoBiProWebSocketClientHandler(WebSocketClientHandshakerFactory 47 | .newHandshaker(uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()), this); 48 | connectWebSocket(uri, handler); 49 | if (isAlive()) { 50 | handler.handshakeFuture().sync(); 51 | } 52 | } catch (Exception e) { 53 | log.error("WebSocketClient start error", e); 54 | if (group != null) { 55 | group.shutdownGracefully(); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 发送ping消息 62 | */ 63 | @Override 64 | public void sendPing() { 65 | JSONObject jsonObject = new JSONObject(); 66 | jsonObject.put("ping", System.currentTimeMillis()); 67 | this.sendMessage(jsonObject.toString()); 68 | } 69 | 70 | /** 71 | * 发送pong消息 72 | * 73 | * @param pong 74 | */ 75 | public void sendPong(long pong) { 76 | JSONObject jsonObject = new JSONObject(); 77 | jsonObject.put("pong", pong); 78 | this.sendMessage(jsonObject.toString()); 79 | } 80 | 81 | /** 82 | * 订阅主题 83 | */ 84 | public void addSub(String channel) { 85 | if (!isAlive()) { 86 | return; 87 | } 88 | JSONObject jsonObject = new JSONObject(); 89 | jsonObject.put("sub", channel); 90 | jsonObject.put("id", subId); 91 | String msg = jsonObject.toString(); 92 | this.sendMessage(msg); 93 | this.addChannel(channel); 94 | } 95 | 96 | /** 97 | * 添加交易对 集合 98 | */ 99 | @Override 100 | public void addChannel(String msg) { 101 | if (channel == null) { 102 | return; 103 | } 104 | subChannel.add(msg); 105 | } 106 | 107 | /** 108 | * 移除交易对 集合 109 | */ 110 | @Override 111 | public void removeChannel(String channel) { 112 | if (channel == null) { 113 | return; 114 | } 115 | subChannel.remove(channel); 116 | } 117 | 118 | /** 119 | * 重新建立连接 120 | */ 121 | @Override 122 | public void reConnect() { 123 | if (group != null) { 124 | this.group.shutdownGracefully(); 125 | } 126 | this.group = null; 127 | this.connect(); 128 | if (isAlive()) { 129 | // 重新订阅历史记录 130 | for (String channel : subChannel) { 131 | this.addSub(channel); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * 为什么要发送ping pong这个根据各个交易所的websocket API文档而定 138 | * 因为火币是需要隔一短时间发送pong消息,如果长时间不发,火币网会认为与我们已经断开了,就自动断开与我们的连接 139 | */ 140 | @Override 141 | public void onReceive(String msg) { 142 | // ping 消息 143 | monitorTask.updateTime(); 144 | if (msg.contains("ping")) { 145 | this.sendMessage(msg.replace("ping", "pong")); 146 | return; 147 | } 148 | if (msg.contains("pong")) { 149 | this.sendPing(); 150 | return; 151 | } 152 | // 处理订阅成功之后,返回的业务数据 153 | service.onReceive(msg); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/HuoBiProWebSocketClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi; 2 | 3 | 4 | import com.oujiong.exchange.client.utils.GZipUtils; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.*; 7 | import io.netty.handler.codec.http.FullHttpResponse; 8 | import io.netty.handler.codec.http.websocketx.*; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.nio.charset.StandardCharsets; 12 | 13 | /** 14 | * @Description: 火币网WebSocket 消息处理类 15 | * 自定义入站的handler 这个也是核心类 16 | * 17 | * @author xub 18 | * @date 2019/7/30 下午7:07 19 | */ 20 | @Slf4j 21 | public class HuoBiProWebSocketClientHandler extends SimpleChannelInboundHandler { 22 | 23 | private WebSocketClientHandshaker handshaker; 24 | private ChannelPromise handshakeFuture; 25 | private HuoBiProWebSocketClient client; 26 | 27 | /** 28 | * 该handel获取消息的方法 29 | */ 30 | @Override 31 | protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { 32 | Channel channel = ctx.channel(); 33 | if (!handshaker.isHandshakeComplete()) { 34 | handshaker.finishHandshake(channel, (FullHttpResponse) msg); 35 | handshakeFuture.setSuccess(); 36 | return; 37 | } 38 | 39 | WebSocketFrame frame = (WebSocketFrame) msg; 40 | if (frame instanceof BinaryWebSocketFrame) { 41 | //火币网的数据是压缩过的,所以需要我们进行解压 42 | BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame; 43 | client.onReceive(decodeByteBuf(binaryFrame.content())); 44 | } else if (frame instanceof TextWebSocketFrame) { 45 | TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) frame; 46 | client.onReceive(textWebSocketFrame.text()); 47 | } else if (frame instanceof PongWebSocketFrame) { 48 | log.info("websocket client recived pong!"); 49 | } else if (frame instanceof CloseWebSocketFrame) { 50 | log.info("WebSocket Client Received Closing."); 51 | channel.close(); 52 | } 53 | } 54 | 55 | public HuoBiProWebSocketClientHandler(WebSocketClientHandshaker handshaker, HuoBiProWebSocketClient client) { 56 | this.handshaker = handshaker; 57 | this.client = client; 58 | } 59 | 60 | public ChannelFuture handshakeFuture() { 61 | return handshakeFuture; 62 | } 63 | 64 | @Override 65 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 66 | handshakeFuture = ctx.newPromise(); 67 | } 68 | 69 | @Override 70 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 71 | handshaker.handshake(ctx.channel()); 72 | } 73 | 74 | @Override 75 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 76 | log.warn("websocket client disconnected."); 77 | client.start(); 78 | } 79 | @Override 80 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 81 | cause.printStackTrace(); 82 | if (!handshakeFuture.isDone()) { 83 | handshakeFuture.setFailure(cause); 84 | } 85 | ctx.close(); 86 | } 87 | 88 | /** 89 | * 解压数据 90 | */ 91 | private String decodeByteBuf(ByteBuf buf) throws Exception { 92 | byte[] temp = new byte[buf.readableBytes()]; 93 | buf.readBytes(temp); 94 | // gzip 解压 95 | temp = GZipUtils.decompress(temp); 96 | return new String(temp, StandardCharsets.UTF_8); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/Topic.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi; 2 | 3 | /** 4 | * @Description: 订阅主题, 火币网 5 | * 一般需要订阅以下五种数据类型 6 | * 7 | * @author xub 8 | * @date 2019/7/29 下午8:53 9 | */ 10 | public final class Topic { 11 | 12 | /** 13 | * K线订阅 14 | */ 15 | public static String KLINE_SUB = "market.%s.kline.%s"; 16 | 17 | /** 18 | * 交易深度 19 | */ 20 | public static String MARKET_DEPTH_SUB = "market.%s.depth.step0"; 21 | 22 | /** 23 | * 交易行情 24 | */ 25 | public static String MARKET_TRADE_SUB = "market.%s.trade.detail"; 26 | 27 | /** 28 | * 行情 29 | */ 30 | public static String MARKET_DETAIL_SUB = "market.%s.detail"; 31 | 32 | /** 33 | * K线交易周期 34 | */ 35 | public static String[] PERIOD = {"1min" /*, "5min", "15min", "30min", "60min", "1day", "1mon", "1week", "1year"*/ }; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/service/HuoBiProWebSocketService.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi.service; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @Description: 其它服务拉取交易对消息 和调其它服务保存消息 相关接口 7 | * 8 | * @author xub 9 | * @date 2019/7/29 下午8:55 10 | */ 11 | public interface HuoBiProWebSocketService { 12 | 13 | /** 14 | * 获取订阅的消息进行消费 15 | * 16 | * @param msg 消息内容 17 | */ 18 | void onReceive(String msg); 19 | 20 | /** 21 | * 获取交易所, 交易对缓存 22 | * 23 | * @return 返回 已缓存的交易对 24 | */ 25 | List getChannelCache(); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/service/HuobiProMainService.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi.service; 2 | 3 | 4 | /** 5 | * @Description: 订阅接口 6 | * 7 | * @author xub 8 | * @date 2019/7/28 下午7:06 9 | */ 10 | public interface HuobiProMainService { 11 | 12 | /** 13 | * 首次订阅数据 14 | */ 15 | void start(); 16 | 17 | /** 18 | * 刷新数据 19 | */ 20 | void refreshSubData(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/service/impl/HuoBiProWebSocketServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi.service.impl; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.oujiong.exchange.client.huobi.service.HuoBiProWebSocketService; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | 11 | 12 | /** 13 | * @author xub 14 | * @Description: 其它服务拉取交易对消息 和调其它服务保存消息 15 | * @date 2019/7/30 下午6:58 16 | */ 17 | @Service 18 | @Slf4j 19 | public class HuoBiProWebSocketServiceImpl implements HuoBiProWebSocketService { 20 | 21 | @Override 22 | public void onReceive(String msg) { 23 | // 直接发送消息给中转服务, 中转服务来处理信息 24 | if (StringUtils.isBlank(msg)) { 25 | log.error("====onReceive-huobi==msg is null"); 26 | return; 27 | } 28 | log.info("火币网数据:{}", msg); 29 | } 30 | 31 | @Override 32 | public synchronized List getChannelCache() { 33 | // 假设这里是从远处拉取数据 34 | List list = Lists.newArrayList("btcusdt"); 35 | return list; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/huobi/service/impl/HuobiProMainServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.huobi.service.impl; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.oujiong.exchange.client.huobi.HuoBiProWebSocketClient; 5 | import com.oujiong.exchange.client.huobi.Topic; 6 | import com.oujiong.exchange.client.huobi.service.HuoBiProWebSocketService; 7 | import com.oujiong.exchange.client.huobi.service.HuobiProMainService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.collections.CollectionUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author xub 17 | * @Description: 火币网 主服务 18 | * @date 2019/7/28 下午6:24 19 | */ 20 | @Slf4j 21 | @Component 22 | public class HuobiProMainServiceImpl implements HuobiProMainService { 23 | 24 | @Autowired 25 | private HuoBiProWebSocketService huoBiProWebSocketService; 26 | 27 | private HuoBiProWebSocketClient klineClient; 28 | 29 | private static List channelCache = Lists.newLinkedList(); 30 | 31 | @Override 32 | public void start() { 33 | // 拉取最新的订阅交易对 34 | List channelList = huoBiProWebSocketService.getChannelCache(); 35 | if (CollectionUtils.isEmpty(channelList)) { 36 | return; 37 | } 38 | channelCache = channelList; 39 | // 订阅kline 40 | firstSub(channelList, Topic.KLINE_SUB); 41 | } 42 | 43 | /** 44 | * 首次订阅交易对数据 45 | * 46 | * @param channelList 交易对列表 47 | * @param topicFormat 交易对订阅主题格式 48 | */ 49 | private void firstSub(List channelList, String topicFormat) { 50 | //封装huoBiProWebSocketService对象 51 | klineClient = new HuoBiProWebSocketClient(huoBiProWebSocketService); 52 | //启动连接火币网websocket 53 | klineClient.start(); 54 | for (String channel : channelList) { 55 | //订阅具体交易对 56 | klineClient.addSub(formatChannel(topicFormat, channel)); 57 | } 58 | } 59 | 60 | /** 61 | * 刷新数据 62 | */ 63 | @Override 64 | public void refreshSubData() { 65 | // 拉取最新的订阅交易对 66 | List channelList = huoBiProWebSocketService.getChannelCache(); 67 | if (CollectionUtils.isEmpty(channelList)) { 68 | return; 69 | } 70 | reSub(channelList, Topic.KLINE_SUB); 71 | } 72 | 73 | private void reSub(List channelList, String topicFormat) { 74 | for (String sub : channelList) { 75 | //如果不存在说明该交易所新增加了交易对 需要订阅该交易对 76 | if (!channelCache.contains(sub)) { 77 | klineClient.addSub(formatChannel(topicFormat, sub)); 78 | } 79 | } 80 | channelCache = channelList; 81 | //交易所删除交易对 删除的这边就不做处理了 82 | } 83 | 84 | /** 85 | * 拼接订阅主题 86 | */ 87 | private String formatChannel(String topic, String channel) { 88 | if (topic.equalsIgnoreCase(Topic.KLINE_SUB)) { 89 | return String.format(topic, channel, Topic.PERIOD[0]); 90 | } 91 | return String.format(topic, channel); 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/scheduler/HuobiTrigger.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.scheduler; 2 | 3 | import com.oujiong.exchange.client.huobi.service.HuobiProMainService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | 11 | /** 12 | * @author xub 13 | * @Description: 火币网定时任务获取时时虚拟币数据 14 | * @date 2019/7/27 下午9:50 15 | */ 16 | @Component 17 | @Slf4j 18 | public class HuobiTrigger { 19 | 20 | @Autowired 21 | private HuobiProMainService huobiProMainService; 22 | 23 | /** 24 | * 首次启动并订阅火币websocket数据 25 | */ 26 | @PostConstruct 27 | public void firstSub() { 28 | try { 29 | huobiProMainService.start(); 30 | } catch (Exception e) { 31 | log.error("huobi 首次启动订阅异常", e); 32 | } 33 | } 34 | 35 | /** 36 | * 上面首次启动就已经可以本地websokcet就已经和火币的websokcet连接成功,时时获取数据了。 37 | *

38 | * 但是因为火币网的交易对可能会新增和减少,所以这里通过定时任务获取最新交易对数据,并进行订阅 39 | */ 40 | @Scheduled(cron = "0 0 */1 * * *") 41 | public void doSubHuoBiPro() { 42 | try { 43 | huobiProMainService.refreshSubData(); 44 | } catch (Exception e) { 45 | log.error("刷新火币网交易对缓存, 并尝试重新订阅数据, error.", e); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/utils/GZipUtils.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.utils; 2 | 3 | import java.io.*; 4 | import java.util.zip.GZIPInputStream; 5 | 6 | /** 7 | * @Description: GZip 压缩辅助类(火币网数据需要解压) 8 | * 9 | * @author xub 10 | * @date 2019/7/30 下午7:07 11 | */ 12 | public class GZipUtils { 13 | 14 | public static final int BUFFER = 1024; 15 | 16 | /** 17 | * 数据解压缩 18 | * 19 | * @param data 20 | */ 21 | public static byte[] decompress(byte[] data) throws Exception { 22 | ByteArrayInputStream bais = new ByteArrayInputStream(data); 23 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 24 | 25 | // 解压缩 26 | decompress(bais, baos); 27 | data = baos.toByteArray(); 28 | baos.flush(); 29 | baos.close(); 30 | bais.close(); 31 | return data; 32 | } 33 | 34 | 35 | /** 36 | * 数据解压缩 37 | * 38 | * @param is 39 | * @param os 40 | */ 41 | public static void decompress(InputStream is, OutputStream os) throws Exception { 42 | GZIPInputStream gis = new GZIPInputStream(is); 43 | int count; 44 | byte data[] = new byte[BUFFER]; 45 | while ((count = gis.read(data, 0, BUFFER)) != -1) { 46 | os.write(data, 0, count); 47 | } 48 | gis.close(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/oujiong/exchange/client/utils/MonitorTask.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.exchange.client.utils; 2 | 3 | import com.oujiong.exchange.client.common.AbstractWebSocketClient; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | 7 | /** 8 | * @Description: 防止 因为某种原因与火币网的websokcet连接被断开 所以需要有线程不定时查看查看, 9 | *

10 | * 当5秒中没有获取数据火币网的消息,就认为与火币websocket失去连接 ,需要重新连接 11 | * @author xub 12 | * @date 2019/7/30 下午7:31 13 | */ 14 | @Slf4j 15 | public class MonitorTask implements Runnable { 16 | 17 | private long startTime = System.currentTimeMillis(); 18 | 19 | private long checkTime = 5000L; 20 | 21 | private AbstractWebSocketClient client; 22 | 23 | public MonitorTask(AbstractWebSocketClient client) { 24 | this.client = client; 25 | } 26 | 27 | /** 28 | * 每次获取消息都会更新startTime 29 | */ 30 | public void updateTime() { 31 | this.startTime = System.currentTimeMillis(); 32 | } 33 | 34 | @Override 35 | public void run() { 36 | if (System.currentTimeMillis() - startTime > checkTime) { 37 | client.reConnect(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | --------------------------------------------------------------------------------