├── screenshot.png ├── dist └── .gitignore ├── java ├── src │ └── main │ │ ├── webapp │ │ ├── bundle.min.js.gz │ │ ├── bundle.min.js.map │ │ ├── index.html │ │ └── WEB-INF │ │ │ └── web.xml │ │ ├── resources │ │ ├── logback.xml │ │ └── springMVC.xml │ │ └── java │ │ └── me │ │ └── jxy │ │ └── websocket │ │ ├── controller │ │ ├── StatusController.java │ │ └── MessageController.java │ │ ├── handler │ │ ├── HandlerB.java │ │ ├── HandlerA.java │ │ └── HandshakeInterceptor.java │ │ ├── listener │ │ └── SessionConnectEventListener.java │ │ ├── channel │ │ └── ChannelInterceptor.java │ │ └── netty │ │ └── Demo.java ├── .gitignore ├── README.md └── pom.xml ├── .gitignore ├── src ├── index.js └── components │ └── WebSocketDebugTool │ ├── index.less │ └── index.js ├── .editorconfig ├── .stylelintrc ├── .lesshintrc ├── .eslintrc ├── index.html.template ├── README.md ├── package.json ├── webpack.config.js ├── webpack.config.prod.js └── LICENSE /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiangxy/websocket-debug-tool/HEAD/screenshot.png -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | # 这个文件其实只是个占位符, 目的是为了忽略本目录下的所有文件, 但不要忽略目录本身 2 | 3 | * 4 | */ 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /java/src/main/webapp/bundle.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiangxy/websocket-debug-tool/HEAD/java/src/main/webapp/bundle.min.js.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea 3 | *.iml 4 | *.iws 5 | 6 | # Mac 7 | .DS_Store 8 | 9 | # node 10 | node_modules 11 | npm-debug.log 12 | 13 | # other 14 | # dist/ 15 | # !dist/*.html 16 | -------------------------------------------------------------------------------- /java/src/main/webapp/bundle.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bundle.min.js","sources":["webpack:///bundle.min.js"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sourceRoot":""} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import WebSocketDebugTool from './components/WebSocketDebugTool'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "string-quotes": "single", 5 | "selector-pseudo-class-no-unknown": [ 6 | true, 7 | { 8 | "ignorePseudoClasses": [ 9 | "global", 10 | "local" 11 | ] 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | .localized 14 | 15 | # Maven 16 | target/ 17 | 18 | # other 19 | log/ 20 | .svn/ 21 | *.imp 22 | *.crc 23 | *.ipr 24 | dependency-reduced-pom.xml 25 | -------------------------------------------------------------------------------- /java/src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | WebSocket Debug Tool
-------------------------------------------------------------------------------- /.lesshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "propertyOrdering": false, 3 | "hexLength": "short", 4 | "stringQuotes": false, 5 | "decimalZero": false, 6 | "importantRule": false, 7 | "zeroUnit": "no_unit", 8 | "qualifyingElement": false, 9 | "duplicateProperty": false, 10 | "importPath": false, 11 | "finalNewline": false, 12 | "spaceAroundComma": false, 13 | 14 | "fileExtensions": [".less", ".css"], 15 | "excludedFiles": [ 16 | "test.less" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "spaced-comment": [0], 6 | "no-unused-vars": [0], 7 | "no-empty": [0], 8 | "react/wrap-multilines": [0], 9 | "react/no-multi-comp": [0], 10 | "no-constant-condition": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "arrow-body-style": [0], 14 | "react/prefer-stateless-function": [0], 15 | "semi": [0] 16 | }, 17 | "ecmaFeatures": { 18 | "experimentalObjectRestSpread": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /java/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.out 5 | 6 | %d %p [%t] %c{2} - %m%n 7 | UTF-8 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /index.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 10 | <% if (htmlWebpackPlugin.options.favIcon && htmlWebpackPlugin.options.favIcon.length > 0) { %> 11 | 12 | <% } %> 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | <% if (htmlWebpackPlugin.options.devMode) { %> 21 | 22 | <% } %> 23 | 24 | 25 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/controller/StatusController.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | 9 | /** 10 | * 测试用 11 | * 12 | * @version 1.0 13 | * @author jiangxy 14 | */ 15 | @Controller 16 | public class StatusController { 17 | 18 | private static Logger logger = LoggerFactory.getLogger(StatusController.class); 19 | 20 | /** 21 | * 检查API是否正常 22 | * 23 | * @return 24 | */ 25 | @RequestMapping("/status") 26 | @ResponseBody 27 | public String status() { 28 | logger.info("check api status"); 29 | return "OK"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/handler/HandlerB.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.handler; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.socket.TextMessage; 6 | import org.springframework.web.socket.WebSocketSession; 7 | import org.springframework.web.socket.handler.TextWebSocketHandler; 8 | 9 | /** 10 | * 测试用的handler 11 | * 12 | * @version 1.0 13 | * @author jiangxy 14 | */ 15 | public class HandlerB extends TextWebSocketHandler { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(HandlerB.class); 18 | 19 | @Override 20 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 21 | logger.info("HandlerB received message: {}", message.getPayload()); 22 | TextMessage resMsg = new TextMessage("[HandlerB] I received: " + message.getPayload()); 23 | session.sendMessage(resMsg); 24 | } 25 | } -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/handler/HandlerA.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.handler; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.socket.TextMessage; 6 | import org.springframework.web.socket.WebSocketSession; 7 | import org.springframework.web.socket.handler.TextWebSocketHandler; 8 | 9 | /** 10 | * 测试用的handler 11 | * 12 | * @version 1.0 13 | * @author jiangxy 14 | */ 15 | public class HandlerA extends TextWebSocketHandler { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(HandlerA.class); 18 | 19 | @Override 20 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 21 | // 握手过程中很重要的一个事就是将http session复制过来 22 | // 注意无论sockjs还是raw websocket,url中都是可以带参数的,spring似乎会对token参数做一些特殊处理 23 | logger.info("HandlerA received message: {}, url = {}, attrMap = {}", message.getPayload(), session.getUri().toString(), session.getAttributes()); 24 | TextMessage resMsg = new TextMessage("[HandlerA] I received: " + message.getPayload()); 25 | session.sendMessage(resMsg); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/listener/SessionConnectEventListener.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.listener; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.messaging.Message; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.socket.messaging.SessionConnectedEvent; 9 | 10 | /** 11 | * 监听websocket的连接事件 12 | * 13 | * @version 1.0 14 | * @author jiangxy 15 | */ 16 | @Component 17 | public class SessionConnectEventListener implements ApplicationListener { 18 | 19 | // 有哪些事件可以监听: 20 | // https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events 21 | 22 | private static Logger logger = LoggerFactory.getLogger(SessionConnectEventListener.class); 23 | 24 | @Override 25 | public void onApplicationEvent(SessionConnectedEvent event) { 26 | Message message = event.getMessage(); 27 | logger.info("receive SessionConnectedEvent and headers = " + message.getHeaders()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /java/README.md: -------------------------------------------------------------------------------- 1 | # WebSocket server for test 2 | 3 | ## Spring-based 4 | 5 | Based on Spring 4.2.6 6 | 7 | ### Quick Start 8 | 9 | 1. you need maven 3. 10 | 2. `git clone` and `mvn jetty:run` to start the embedded Jetty server. 11 | 3. open `http://localhost:8080/index.html` and enjoy. 12 | 13 | ### WebSocket URI 14 | 15 | Once the Jetty server started, there are some URI available for WebSocket transport: 16 | 17 | 1. `ws://localhost:8080/handlerA` and `ws://localhost:8080/handlerB` for raw WebSocket. 18 | 2. `http://localhost:8080/sockjs/handlerA` and `http://localhost:8080/sockjs/handlerB` for SockJS. 19 | 3. `http://localhost:8080/stomp` for SockJS + STOMP. 20 | 21 | For more detail, view [SpringMVC.xml](src/main/resources/springMVC.xml). 22 | 23 | ## Netty-based 24 | 25 | Based on Netty 4.1.9.Final 26 | 27 | ### Quick Start 28 | 29 | 1. you need maven 3. 30 | 2. `git clone` and `mvn jetty:run` 31 | 3. open a new terminal window and `mvn exec:java -Dexec.mainClass=me.jxy.websocket.netty.Demo` 32 | 4. open `http://localhost:8080/index.html` 33 | 34 | ### WebSocket URI 35 | 36 | This server is very simple, just a demo. Only one URI `ws://localhost:12345/websocket` is available for raw WebSocket. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Debug Tool 2 | 3 | A tool to debug websocket/sockjs/stomp. 4 | 5 | ![](screenshot.png) 6 | 7 | ## Features 8 | 9 | * support WebSocket/[SockJS](https://github.com/sockjs/sockjs-client), with or without [STOMP](https://github.com/jmesnil/stomp-websocket) 10 | * React + ES6 + webpack + [Ant Design](https://github.com/ant-design/ant-design) 11 | 12 | ## Quick Start 13 | 14 | Open [http://jiangxy.github.io/websocket-debug-tool](http://jiangxy.github.io/websocket-debug-tool) and use directly. Pay attention to Cross-Domain Policy. 15 | 16 | If you wanna modify the source code: 17 | 18 | 1. node 5.3+, npm 3.3+ 19 | 2. `git clone` and `npm install` 20 | 3. `npm run dev` to start webpack-dev-server (with react-hot-loader) 21 | 4. open `http://localhost:4040` 22 | 23 | ## Spring-based & Netty-based server 24 | 25 | There is a spring-based WebSocket server in the [java](java) folder for test purpose. View [Doc](java/README.md). 26 | 27 | There is also a netty-based example server. 28 | 29 | ## Browsers support 30 | 31 | | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | 32 | | --------- | 33 | | last 2 versions 34 | -------------------------------------------------------------------------------- /java/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | websocket-demo 5 | 6 | 7 | springMVC 8 | org.springframework.web.servlet.DispatcherServlet 9 | 10 | contextConfigLocation 11 | classpath:springMVC.xml 12 | 13 | 1 14 | 15 | 16 | springMVC 17 | / 18 | 19 | 20 | 21 | encoding-filter 22 | org.springframework.web.filter.CharacterEncodingFilter 23 | 24 | encoding 25 | UTF-8 26 | 27 | 28 | forceEncoding 29 | true 30 | 31 | 32 | 33 | encoding-filter 34 | /* 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/channel/ChannelInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.channel; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.messaging.Message; 6 | import org.springframework.messaging.MessageChannel; 7 | import org.springframework.messaging.simp.stomp.StompCommand; 8 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 9 | import org.springframework.messaging.support.ChannelInterceptorAdapter; 10 | import org.springframework.messaging.support.GenericMessage; 11 | 12 | /** 13 | * channel interceptor 14 | * 15 | * @version 1.0 16 | * @author jiangxy 17 | */ 18 | public class ChannelInterceptor extends ChannelInterceptorAdapter { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(ChannelInterceptor.class); 21 | 22 | @Override 23 | public Message preSend(Message message, MessageChannel channel) { 24 | StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); 25 | StompCommand command = accessor.getCommand(); 26 | logger.info("receive message " + command.name()); 27 | 28 | // Interceptor可以做很多事,可以修改原来的消息,也可以通过MessageChannel发送更多消息 29 | // 常见的用法是权限认证 30 | 31 | Message newMessage = new GenericMessage("yoyoyo".getBytes(), message.getHeaders()); 32 | return newMessage; 33 | } 34 | } -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/handler/HandshakeInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.handler; 2 | 3 | import java.util.Map; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.server.ServerHttpRequest; 8 | import org.springframework.http.server.ServerHttpResponse; 9 | import org.springframework.web.socket.WebSocketHandler; 10 | import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; 11 | 12 | /** 13 | * 测试握手过程 14 | * 15 | * @version 1.0 16 | * @author jiangxy 17 | */ 18 | public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor { 19 | 20 | private static Logger logger = LoggerFactory.getLogger(HandshakeInterceptor.class); 21 | 22 | @Override 23 | public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) 24 | throws Exception { 25 | logger.info("before websocket handshake, request method={}, url={}, headers={}, attributes={}", request.getMethod(), request.getURI(), 26 | request.getHeaders(), attributes); 27 | // 这个方法如果返回false,说明握手失败 28 | return super.beforeHandshake(request, response, wsHandler, attributes); 29 | } 30 | 31 | @Override 32 | public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { 33 | logger.info("after handshake"); 34 | super.afterHandshake(request, response, wsHandler, ex); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-debug-tool", 3 | "version": "1.0.0", 4 | "description": "A tool to debug websocket/sockjs/stomp", 5 | "author": "jiangxy ", 6 | "license": "Apache-2.0", 7 | "engines": { 8 | "node": ">=5.0 <7", 9 | "npm": ">=3.3 <4" 10 | }, 11 | "keywords": [ 12 | "ant", 13 | "react", 14 | "websocket", 15 | "sockjs", 16 | "stomp", 17 | "frontend" 18 | ], 19 | "devDependencies": { 20 | "antd": "2.7.1", 21 | "babel-core": "^6.21.0", 22 | "babel-eslint": "6.0.0", 23 | "babel-loader": "^6.2.10", 24 | "babel-plugin-import": "1.1.0", 25 | "babel-polyfill": "^6.20.0", 26 | "babel-preset-latest": "^6.16.0", 27 | "babel-preset-react": "^6.16.0", 28 | "babel-preset-stage-0": "^6.16.0", 29 | "compression-webpack-plugin": "0.3.2", 30 | "cross-env": "3.1.4", 31 | "css-loader": "0.23.1", 32 | "eslint": "2.7.0", 33 | "eslint-config-airbnb": "6.x", 34 | "eslint-plugin-react": "4.x", 35 | "extract-text-webpack-plugin": "1.0.1", 36 | "file-loader": "0.9.0", 37 | "html-webpack-plugin": "2.28.0", 38 | "less": "2.7.1", 39 | "less-loader": "2.2.3", 40 | "lesshint": "2.0.2", 41 | "react": "15.3.0", 42 | "react-dom": "15.3.0", 43 | "react-hot-loader": "1.3.0", 44 | "rimraf": "2.5.4", 45 | "sockjs-client": "1.1.2", 46 | "stompjs": "2.3.3", 47 | "strip-loader": "0.1.2", 48 | "style-loader": "0.13.1", 49 | "stylelint": "6.6.0", 50 | "stylelint-config-standard": "9.0.0", 51 | "url-loader": "0.5.7", 52 | "webpack": "1.14.0", 53 | "webpack-dev-server": "1.16.2" 54 | }, 55 | "scripts": { 56 | "prod": "cross-env NODE_ENV=production webpack --progress --colors --config webpack.config.prod.js -p --display-error-details", 57 | "clean": "rimraf dist/*bundle* dist/*vendor* dist/*index*", 58 | "dev": "webpack-dev-server --devtool eval --progress --colors --content-base dist --hot --inline --display-error-details --port 4040", 59 | "eslint": "eslint --ext .js,.jsx src", 60 | "stylelint": "stylelint \"src/**/*.css\"", 61 | "lesshint": "lesshint src/" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/WebSocketDebugTool/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | height: 720px; 3 | } 4 | 5 | .base { 6 | width: 50%; 7 | margin: 0 auto; 8 | padding: 8px; 9 | } 10 | 11 | .common { 12 | .base; 13 | border-bottom: 3px solid #4588ba; 14 | margin-bottom: 12px; 15 | height: 12%; 16 | } 17 | 18 | .stomp { 19 | .base; 20 | border-bottom: 3px solid #4588ba; 21 | margin-bottom: 10px; 22 | height: 28%; 23 | } 24 | 25 | .stomp pre { 26 | text-align: center; 27 | } 28 | 29 | .message { 30 | .base; 31 | height: 5%; 32 | } 33 | 34 | .output { 35 | height: 45%; 36 | background: #FFFFFF; 37 | color: #fff; 38 | font-family: "Proxima Nova", "Helvetica Neue", helvetica, arial, sans-serif; 39 | font-weight: 300; 40 | font-size: 14px; 41 | line-height: 1.5; 42 | padding-top: 2rem; 43 | } 44 | 45 | .output .window { 46 | height: 100%; 47 | width: 80%; 48 | margin: 0 auto 2rem; 49 | box-shadow: 0 0.25rem 0.5rem #12181e; 50 | border-radius: 0 0 0.1rem 0.1rem; 51 | } 52 | 53 | .output .body { 54 | width: 100%; 55 | height: 100%; 56 | background: #232323; 57 | padding: 18px; 58 | border-radius: 0.1rem; 59 | overflow: auto; 60 | } 61 | 62 | .output .body pre { 63 | white-space: pre-wrap; 64 | word-wrap: break-word; 65 | margin: 0; 66 | } 67 | 68 | .output .body .pulse { 69 | -webkit-animation: pulse 1s ease-in-out infinite; 70 | -moz-animation: pulse 1s ease-in-out infinite; 71 | animation: pulse 1s ease-in-out infinite; 72 | } 73 | 74 | @-webkit-keyframes pulse { 75 | 0% { 76 | opacity: 0; 77 | } 78 | 50% { 79 | opacity: 1; 80 | } 81 | 100% { 82 | opacity: 0; 83 | } 84 | } 85 | 86 | @-moz-keyframes pulse { 87 | 0% { 88 | opacity: 0; 89 | } 90 | 50% { 91 | opacity: 1; 92 | } 93 | 100% { 94 | opacity: 0; 95 | } 96 | } 97 | 98 | @keyframes pulse { 99 | 0% { 100 | opacity: 0; 101 | } 102 | 50% { 103 | opacity: 1; 104 | } 105 | 100% { 106 | opacity: 0; 107 | } 108 | } 109 | 110 | .output .command { 111 | color: #32c146; 112 | } 113 | 114 | .output .comment { 115 | opacity: 0.5; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | // 将babel-loader的配置独立出来, 因为webpack的限制: http://stackoverflow.com/questions/33117136/how-to-add-a-query-to-a-webpack-loader-with-multiple-loaders 5 | const babelLoaderConfig = { 6 | presets: ['latest', 'stage-0', 'react'], // 开启ES6、部分ES7、react特性, preset相当于预置的插件集合 7 | plugins: [['import', {libraryName: 'antd', style: true}]], // antd模块化加载, https://github.com/ant-design/babel-plugin-import 8 | cacheDirectory: true, 9 | }; 10 | 11 | module.exports = { 12 | devtool: 'eval-source-map', 13 | 14 | entry: [ 15 | 'webpack-dev-server/client?http://0.0.0.0:4040', // WebpackDevServer host and port 16 | 'webpack/hot/only-dev-server', // "only" prevents reload on syntax errors 17 | 'babel-polyfill', // 可以使用完整的ES6特性, 大概增加100KB 18 | './src/index.js', // 编译的入口 19 | ], 20 | 21 | output: { // 输出的目录和文件名 22 | path: __dirname + '/dist', 23 | filename: 'bundle.js', 24 | }, 25 | 26 | resolve: { 27 | modulesDirectories: ['node_modules', './src'], // import时到哪些地方去寻找模块 28 | extensions: ['', '.js', '.jsx'], // require的时候可以直接使用require('file'),不用require('file.js') 29 | alias: { 30 | antdcss: 'antd/dist/antd.min.css', // import时的别名 31 | }, 32 | }, 33 | 34 | module: { 35 | loaders: [ // 定义各种loader 36 | { 37 | test: /\.jsx?$/, 38 | loaders: ['react-hot', 'babel-loader?' + JSON.stringify(babelLoaderConfig)], // react-hot-loader可以不用刷新页面, 如果用普通的dev-server的话会自动刷新页面 39 | exclude: /node_modules/, 40 | }, { 41 | test: /\.css$/, 42 | loader: 'style!css', 43 | }, { 44 | test: /\.less$/, 45 | loader: 'style!css!' + `less?{"sourceMap":true}`, // 用!去链式调用loader 46 | }, { 47 | test: /\.(png|jpg|svg)$/, 48 | loader: 'url?limit=25000', // 图片小于一定值的话转成base64 49 | }, 50 | ], 51 | }, 52 | 53 | plugins: [ 54 | new webpack.BannerPlugin('This file is created by jxy'), // 生成文件时加上注释 55 | new webpack.DefinePlugin({ 56 | 'process.env.NODE_ENV': JSON.stringify('development'), 57 | __DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'production' ? 'false' : 'true')), // magic globals, 用于打印一些调试的日志, webpack -p时会删除 58 | }), 59 | // 生成html文件 60 | new HtmlWebpackPlugin({ 61 | template: 'index.html.template', 62 | title: 'WebSocket Debug Tool', 63 | 64 | // HtmlWebpackPlugin自己有一个favicon属性, 但用起来有点问题, 所以自己重新搞个favIcon属性 65 | favIcon: 'http://jxy.me/favicon.ico', 66 | // 这个属性也是我自己定义的, dev模式下要加载一些额外的js 67 | devMode: true, 68 | }), 69 | ], 70 | }; 71 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | const CompressionPlugin = require("compression-webpack-plugin"); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const babelLoaderConfig = { 7 | presets: ['latest', 'stage-0', 'react'], 8 | plugins: [['import', {libraryName: 'antd', style: true}]], 9 | cacheDirectory: true, 10 | }; 11 | 12 | module.exports = { 13 | devtool: 'cheap-module-source-map', 14 | 15 | entry: [ 16 | 'babel-polyfill', 17 | './src/index.js', 18 | ], 19 | 20 | output: { 21 | path: __dirname + '/dist', 22 | filename: 'bundle.min.js', 23 | }, 24 | 25 | resolve: { 26 | modulesDirectories: ['node_modules', './src'], 27 | extensions: ['', '.js', '.jsx'], 28 | }, 29 | 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.jsx?$/, 34 | // 删除一些debug语句 35 | loaders: ['babel-loader?' + JSON.stringify(babelLoaderConfig), 'strip-loader?strip[]=logger.info,strip[]=logger.debug,strip[]=console.log,strip[]=console.debug'], 36 | exclude: /node_modules/, 37 | }, { 38 | test: /\.css$/, 39 | loader: 'style!css', 40 | }, { 41 | test: /\.less$/, 42 | loader: 'style!css!' + `less?{"sourceMap":true}`, 43 | }, { 44 | test: /\.(png|jpg|svg)$/, 45 | loader: 'url?limit=25000', 46 | }, 47 | ], 48 | }, 49 | 50 | // 减小bundle size是个很大的学问... 51 | // https://chrisbateman.github.io/webpack-visualizer/ 52 | // http://stackoverflow.com/questions/34239731/how-to-minimize-the-size-of-webpacks-bundle 53 | // https://webpack.github.io/docs/code-splitting.html 54 | // http://survivejs.com/webpack/building-with-webpack/splitting-bundles/ 55 | 56 | plugins: [ 57 | // 代码压缩 58 | new webpack.optimize.UglifyJsPlugin({ 59 | sourceMap: true, 60 | minimize: true, 61 | compress: {warnings: false}, 62 | output: {comments: false}, 63 | }), 64 | 65 | new HtmlWebpackPlugin({ 66 | template: 'index.html.template', 67 | title: 'WebSocket Debug Tool', 68 | favIcon: 'http://jxy.me/favicon.ico', 69 | hash: true, // 引入js/css的时候加个hash, 防止cdn的缓存问题 70 | minify: {removeComments: true, collapseWhitespace: true}, 71 | }), 72 | 73 | new webpack.optimize.DedupePlugin(), 74 | // 比对id的使用频率和分布来得出最短的id分配给使用频率高的模块 75 | new webpack.optimize.OccurenceOrderPlugin(), 76 | new webpack.optimize.AggressiveMergingPlugin(), 77 | // 允许错误不打断程序 78 | new webpack.NoErrorsPlugin(), 79 | 80 | // 压缩成gzip格式 81 | new CompressionPlugin({ 82 | asset: "[path].gz[query]", 83 | algorithm: "gzip", 84 | test: /\.js$|\.css$|\.html$/, 85 | threshold: 10240, 86 | minRatio: 0, 87 | }), 88 | 89 | new webpack.DefinePlugin({ 90 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 91 | __DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'production' ? 'false' : 'true')), 92 | }), 93 | ], 94 | }; 95 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.controller; 2 | 3 | import javax.annotation.Resource; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.messaging.MessageHeaders; 9 | import org.springframework.messaging.handler.annotation.DestinationVariable; 10 | import org.springframework.messaging.handler.annotation.MessageMapping; 11 | import org.springframework.messaging.handler.annotation.Payload; 12 | import org.springframework.messaging.handler.annotation.SendTo; 13 | import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 14 | import org.springframework.messaging.simp.SimpMessagingTemplate; 15 | import org.springframework.messaging.simp.annotation.SubscribeMapping; 16 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 17 | import org.springframework.messaging.support.MessageHeaderAccessor; 18 | import org.springframework.stereotype.Controller; 19 | 20 | /** 21 | * 消息处理controller 22 | * 23 | * @version 1.0 24 | * @author jiangxy 25 | */ 26 | @Controller 27 | public class MessageController { 28 | 29 | // spring的websocket处理遵循和springMVC类似的模式,将消息处理逻辑映射到controller对应的方法上 30 | // 首先必须理解spring中的消息流:https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow 31 | 32 | // 关于各种注解和注入参数的说明: 33 | // https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-handle-annotations 34 | 35 | private static final Logger logger = LoggerFactory.getLogger(MessageController.class); 36 | 37 | @Resource 38 | private SimpMessagingTemplate template; // 这个对象用来直接向broker发送消息 39 | 40 | /** 41 | * 发送到/app/handlerA的消息会被这个方法处理 42 | * 这个方法可以注入很多参数,参考官方文档 43 | * 如果这个方法有返回值,就会包装成Message对象发送到/topic/handlerA(这里topic前缀是写死的,跟配置无关) 44 | * 45 | * @return 46 | */ 47 | @MessageMapping("/handlerA") 48 | public String handlerA(Message message, @Payload String payload) { 49 | logger.info("handlerA receive message: payload={}, headers={}", payload, message.getHeaders()); 50 | 51 | // SimpMessagingTemplate只能向broker发送消息,所以下面这行代码其实是无效的,不会触发下面的handlerB方法 52 | template.convertAndSend("/app/handlerB", "message from handerA"); 53 | 54 | // 这行代码才是有效的,会将消息向/topic/handlerB广播 55 | template.convertAndSend("/topic/handlerB", "message from handerA"); 56 | return "OK"; 57 | } 58 | 59 | /** 60 | * 发送到/app/handlerB的消息会被这个方法处理 61 | */ 62 | @MessageMapping("/handlerB") 63 | public void handlerB(Message message, @Payload String payload) { 64 | logger.info("handlerB receive message: payload={}, headers={}", payload, message.getHeaders()); 65 | } 66 | 67 | /** 68 | * 可以用SendTo注解设置返回的消息发到哪里 69 | */ 70 | @MessageMapping("/handlerA/{id}") // 路径参数 71 | @SendTo("/queue/test") 72 | public String handlerA2(@DestinationVariable Integer id, // 路径参数 73 | MessageHeaders messageHeaders, MessageHeaderAccessor messageHeaderAccessor, SimpMessageHeaderAccessor simpMessageHeaderAccessor, 74 | StompHeaderAccessor stompHeaderAccessor) { 75 | logger.info("test SendTo, session = {}", simpMessageHeaderAccessor.getSessionId()); 76 | // 可注入的参数非常多 77 | // MessageHeaders是一个map,最原始的header 78 | // MessageHeaderAccessor对MessageHeaders包装了下,新增了一些get/set方法 79 | // SimpMessageHeaderAccessor似乎是用于访问spring in-memory broker自己加的一些header,这些header都是simp前缀的 80 | // StompHeaderAccessor用于访问STOMP协议的一些header,在MessageHeaders中这被称为native header 81 | 82 | return "OK " + id; 83 | } 84 | 85 | /** 86 | * SubscribeMapping注解比较特殊,如果客户端订阅/app/fake,就会触发这个方法 87 | * 这个方法如果有返回值,会直接返回给客户端,不会发送到broker,可用于实现request-reply模式 88 | * 如果没有返回值,就什么都不会发生 89 | * 90 | * 注意只能用于/app路径下,订阅/topic/fake不会触发这个方法 91 | */ 92 | @SubscribeMapping("/fake") 93 | public String subscribeMapper() { 94 | logger.info("test SubscribeMapping"); 95 | return "fake topic"; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /java/src/main/resources/springMVC.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | WriteMapNullValue 22 | DisableCircularReferenceDetect 23 | WriteDateUseDateFormat 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /java/src/main/java/me/jxy/websocket/netty/Demo.java: -------------------------------------------------------------------------------- 1 | package me.jxy.websocket.netty; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.ChannelFuture; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInboundHandlerAdapter; 8 | import io.netty.channel.ChannelInitializer; 9 | import io.netty.channel.ChannelOption; 10 | import io.netty.channel.ChannelPipeline; 11 | import io.netty.channel.EventLoopGroup; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.SocketChannel; 14 | import io.netty.channel.socket.nio.NioServerSocketChannel; 15 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 16 | import io.netty.handler.codec.http.FullHttpRequest; 17 | import io.netty.handler.codec.http.FullHttpResponse; 18 | import io.netty.handler.codec.http.HttpObjectAggregator; 19 | import io.netty.handler.codec.http.HttpRequestDecoder; 20 | import io.netty.handler.codec.http.HttpResponseEncoder; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | import io.netty.handler.codec.http.HttpVersion; 23 | import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; 24 | import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; 25 | import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; 26 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 27 | import io.netty.handler.codec.http.websocketx.WebSocketFrame; 28 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; 29 | import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; 30 | import io.netty.handler.stream.ChunkedWriteHandler; 31 | 32 | /** 33 | * 一个简单的websocket服务端 34 | * 35 | * @version 1.0 36 | * @author jiangxy 37 | */ 38 | public class Demo { 39 | 40 | // netty官方的例子:http://netty.io/4.1/xref/io/netty/example/http/websocketx/server/package-summary.html 41 | // 更完善,但是也更复杂 42 | 43 | public static void main(String[] args) { 44 | 45 | EventLoopGroup boss = new NioEventLoopGroup(); 46 | EventLoopGroup worker = new NioEventLoopGroup(); 47 | 48 | try { 49 | ServerBootstrap server = new ServerBootstrap(); 50 | server.group(boss, worker).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler()); 51 | 52 | ChannelFuture f = server.bind(12345).sync(); 53 | f.channel().closeFuture().sync(); 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | } finally { 57 | boss.shutdownGracefully(); 58 | worker.shutdownGracefully(); 59 | } 60 | } 61 | 62 | private static class ChildChannelHandler extends ChannelInitializer { 63 | 64 | @Override 65 | protected void initChannel(SocketChannel ch) throws Exception { 66 | ChannelPipeline pipeline = ch.pipeline(); 67 | 68 | // 前面这一堆handler和普通的http处理是一样的,都是netty直接提供的handler 69 | pipeline.addLast("http-decoder", new HttpRequestDecoder()); 70 | pipeline.addLast("http-aggr", new HttpObjectAggregator(65535)); 71 | pipeline.addLast("http-encoder", new HttpResponseEncoder()); 72 | pipeline.addLast("http-chunked", new ChunkedWriteHandler()); 73 | 74 | // 这里才是真正的处理逻辑 75 | pipeline.addLast("handler", new WebsocketHandler()); 76 | } 77 | 78 | } 79 | 80 | private static class WebsocketHandler extends ChannelInboundHandlerAdapter { 81 | 82 | private WebSocketServerHandshaker handshaker; 83 | 84 | @Override 85 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 86 | // 判断是否是握手消息,握手是基于http的 87 | if (msg instanceof FullHttpRequest) { 88 | handleHttp(ctx, (FullHttpRequest) msg); 89 | } else if (msg instanceof WebSocketFrame) { 90 | handleWebSocket(ctx, (WebSocketFrame) msg); 91 | } else { 92 | sendError(ctx); 93 | } 94 | } 95 | 96 | private void sendError(ChannelHandlerContext ctx) { 97 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); 98 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 99 | } 100 | 101 | private void handleHttp(ChannelHandlerContext ctx, FullHttpRequest msg) { 102 | if (!msg.decoderResult().isSuccess() || !"websocket".equals(msg.headers().get("Upgrade"))) { 103 | sendError(ctx); 104 | return; 105 | } 106 | 107 | // http握手 108 | WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:12345/websocket", null, false); 109 | handshaker = wsFactory.newHandshaker(msg); 110 | if (handshaker == null) { 111 | WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); 112 | } else { 113 | handshaker.handshake(ctx.channel(), msg); 114 | } 115 | } 116 | 117 | // 真正去处理websocket消息 118 | private void handleWebSocket(ChannelHandlerContext ctx, WebSocketFrame msg) { 119 | if (msg instanceof CloseWebSocketFrame) { 120 | handshaker.close(ctx.channel(), (CloseWebSocketFrame) msg.retain()); 121 | return; 122 | } 123 | 124 | if (msg instanceof PingWebSocketFrame) { 125 | ctx.channel().write(new PongWebSocketFrame(msg.content().retain())); 126 | return; 127 | } 128 | 129 | if (!(msg instanceof TextWebSocketFrame)) { 130 | ctx.channel().write(new TextWebSocketFrame("only support text message")); 131 | return; 132 | } 133 | 134 | TextWebSocketFrame frame = (TextWebSocketFrame) msg; 135 | String request = frame.text(); 136 | System.out.println("server receive " + request); 137 | 138 | // 类似EchoServer,注意flush 139 | ctx.channel().writeAndFlush(new TextWebSocketFrame("server recevie " + request)); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | me.jxy 7 | websocket-demo 8 | war 9 | 1.0.0-SNAPSHOT 10 | 11 | websocket-demo 12 | 测试websocket用的demo 13 | 14 | 15 | 16 | jiangxy 17 | jiangxy 18 | foolbeargm@gmail.com 19 | +8 20 | 21 | 22 | 23 | 24 | UTF-8 25 | UTF-8 26 | 1.7 27 | 1.7 28 | 29 | 30 | 15.0 31 | 4.2.6.RELEASE 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.springframework 39 | spring-context 40 | ${spring.version} 41 | 42 | 43 | org.springframework 44 | spring-webmvc 45 | ${spring.version} 46 | 47 | 48 | org.springframework 49 | spring-orm 50 | ${spring.version} 51 | 52 | 53 | org.springframework 54 | spring-aop 55 | ${spring.version} 56 | 57 | 58 | org.aspectj 59 | aspectjrt 60 | 1.6.11 61 | 62 | 63 | org.aspectj 64 | aspectjweaver 65 | 1.6.11 66 | 67 | 68 | org.springframework 69 | spring-messaging 70 | ${spring.version} 71 | 72 | 73 | org.springframework 74 | spring-websocket 75 | ${spring.version} 76 | 77 | 78 | 79 | 80 | ch.qos.logback 81 | logback-classic 82 | 1.1.3 83 | 84 | 85 | org.slf4j 86 | slf4j-api 87 | 1.7.12 88 | 89 | 90 | org.slf4j 91 | jcl-over-slf4j 92 | 1.7.12 93 | 94 | 95 | 96 | javax.servlet 97 | javax.servlet-api 98 | 3.1.0 99 | provided 100 | 101 | 102 | javax.websocket 103 | javax.websocket-api 104 | 1.1 105 | provided 106 | 107 | 108 | 109 | com.google.guava 110 | guava 111 | ${guava.version} 112 | 113 | 114 | 115 | com.alibaba 116 | fastjson 117 | 1.2.6 118 | 119 | 120 | 121 | joda-time 122 | joda-time 123 | 2.6 124 | 125 | 126 | 127 | 128 | commons-lang 129 | commons-lang 130 | 2.6 131 | 132 | 133 | commons-codec 134 | commons-codec 135 | 1.10 136 | 137 | 138 | commons-fileupload 139 | commons-fileupload 140 | 1.2.2 141 | 142 | 143 | 144 | 145 | com.fasterxml.jackson.core 146 | jackson-databind 147 | 2.4.1 148 | 149 | 150 | com.fasterxml.jackson.core 151 | jackson-core 152 | 2.4.1 153 | 154 | 155 | 156 | io.netty 157 | netty-all 158 | 4.1.9.Final 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | junit 168 | junit 169 | 4.8.2 170 | test 171 | 172 | 173 | org.mockito 174 | mockito-all 175 | 1.9.5 176 | test 177 | 178 | 179 | org.springframework 180 | spring-test 181 | ${spring.version} 182 | test 183 | 184 | 185 | 186 | 187 | org.projectlombok 188 | lombok 189 | 1.14.4 190 | provided 191 | 192 | 193 | 194 | 195 | org.springframework 196 | spring-context 197 | 198 | 199 | org.springframework 200 | spring-webmvc 201 | 202 | 203 | org.springframework 204 | spring-orm 205 | 206 | 207 | org.springframework 208 | spring-aop 209 | 210 | 211 | org.aspectj 212 | aspectjrt 213 | 214 | 215 | org.aspectj 216 | aspectjweaver 217 | 218 | 219 | org.springframework 220 | spring-messaging 221 | 222 | 223 | org.springframework 224 | spring-websocket 225 | 226 | 227 | 228 | 229 | ch.qos.logback 230 | logback-classic 231 | 232 | 233 | org.slf4j 234 | slf4j-api 235 | 236 | 237 | org.slf4j 238 | jcl-over-slf4j 239 | 240 | 241 | 242 | javax.servlet 243 | javax.servlet-api 244 | 245 | 246 | javax.websocket 247 | javax.websocket-api 248 | 249 | 250 | 251 | com.google.guava 252 | guava 253 | 254 | 255 | 256 | com.alibaba 257 | fastjson 258 | 259 | 260 | 261 | com.fasterxml.jackson.core 262 | jackson-databind 263 | 264 | 265 | com.fasterxml.jackson.core 266 | jackson-core 267 | 268 | 269 | 270 | io.netty 271 | netty-all 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | org.apache.maven.plugins 280 | maven-compiler-plugin 281 | 2.3.2 282 | 283 | 1.7 284 | 1.7 285 | UTF-8 286 | true 287 | true 288 | 289 | 290 | 291 | 292 | org.apache.maven.plugins 293 | maven-resources-plugin 294 | 2.5 295 | 296 | UTF-8 297 | 298 | 299 | 300 | 301 | org.apache.maven.plugins 302 | maven-source-plugin 303 | 2.1.2 304 | 305 | 306 | attach-sources 307 | compile 308 | 309 | jar 310 | 311 | 312 | 313 | 314 | 315 | 316 | org.eclipse.jetty 317 | jetty-maven-plugin 318 | 9.2.8.v20150217 319 | 320 | 321 | manual 322 | false 323 | 324 | / 325 | 326 | 328 | 329 | 330 | 331 | 332 | org.codehaus.mojo 333 | exec-maven-plugin 334 | 1.6.0 335 | 336 | 337 | 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/components/WebSocketDebugTool/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Form, Icon, Input, Button, Checkbox} from 'antd'; 3 | import SockJS from 'sockjs-client'; 4 | import StompJS from 'stompjs/lib/stomp.js'; 5 | import './index.less'; 6 | 7 | const FormItem = Form.Item; 8 | const CheckboxGroup = Checkbox.Group; 9 | const Stomp = StompJS.Stomp; 10 | 11 | /** 12 | * 用于调试websocket的一个小工具, 支持sockjs和stomp 13 | */ 14 | class WebSocketDebugTool extends React.PureComponent { 15 | 16 | state = { 17 | // 全局状态 18 | connected: false, // 是否已经建立连接 19 | sockjs: false, // 是否启用sockjs 20 | stomp: false, // 是否启用stomp 21 | 22 | // controlled components相关状态 23 | url: '', 24 | stompConnectHeader: '', 25 | stompSubscribeDestination: '', 26 | stompSendHeader: '', 27 | stompSendDestination: '', 28 | messageContent: '', 29 | 30 | // 在console中显示的信息 31 | message: [], 32 | }; 33 | 34 | /** 35 | * 连接服务端 36 | */ 37 | connect = () => { 38 | const that = this; 39 | try { 40 | let client; 41 | // 对于STOMP和非STOMP要分别处理 42 | // sockjs和raw websocket的API是一样的 43 | if (this.state.stomp) { 44 | if (this.state.sockjs) { 45 | client = Stomp.over(new SockJS(this.state.url)); 46 | } else { 47 | client = Stomp.over(new WebSocket(this.state.url)); 48 | } 49 | 50 | let connectHeader = {}; 51 | // header必须是个正确的json 52 | if (this.state.stompConnectHeader.length !== 0) { 53 | try { 54 | connectHeader = JSON.parse(this.state.stompConnectHeader) 55 | } catch (e) { 56 | console.error('parse STOMP connect header error %o', e); 57 | this.error(`STOMP connect header format error: ${this.state.stompConnectHeader}`); 58 | return; 59 | } 60 | } 61 | 62 | client.connect(connectHeader, () => { 63 | that.setState({connected: true}); 64 | that.info(`Connect STOMP server success, url = ${that.state.url}, connectHeader = ${that.state.stompConnectHeader}`) 65 | }); 66 | 67 | } else { 68 | if (this.state.sockjs) { 69 | client = new SockJS(this.state.url); 70 | } else { 71 | client = new WebSocket(this.state.url); 72 | } 73 | 74 | client.binaryType = 'arraybuffer'; 75 | client.onopen = e => { 76 | console.debug('Connect success %o', e); 77 | that.info(`Connect success, url = ${that.state.url}`); 78 | that.setState({connected: true}); 79 | }; 80 | client.onmessage = e => { 81 | console.debug('Received message %o', e); 82 | that.info(`Received message: ${e.data}`); 83 | }; 84 | client.onerror = e => { 85 | console.error('Connect error %o', e); 86 | that.error(`Connect error, message = ${e.data}, view chrome console for detail`); 87 | that.setState({connected: false}); 88 | }; 89 | } 90 | 91 | this.client = client; 92 | 93 | } catch (e) { 94 | console.error('Connect error %o', e); 95 | that.error(`Connect error, message = ${e.message}, view chrome console for detail`); 96 | that.setState({connected: false}); 97 | return; 98 | } 99 | }; 100 | 101 | /** 102 | * 关闭连接 103 | */ 104 | disconnect = () => { 105 | if (!this.state.connected) { 106 | this.error(`Not Connected Yet`); 107 | return; 108 | } 109 | 110 | try { 111 | if (this.state.stomp) { 112 | this.client.disconnect(); 113 | } else { 114 | this.client.close(); 115 | } 116 | this.info('Close Connection Success'); 117 | this.setState({connected: false}); 118 | } catch (e) { 119 | console.log('disconnect fail %o', e); 120 | this.error(`disconnect fail, message = ${e.message}, view chrome console for detail`); 121 | } 122 | }; 123 | 124 | /** 125 | * 发送消息 126 | */ 127 | send = () => { 128 | try { 129 | if (this.state.stomp) { 130 | let headerJSON = {}; 131 | // 如果是stomp协议, 必须校验一些条件 132 | 133 | // 必须有destination 134 | if (this.state.stompSendDestination.length === 0) { 135 | this.error(`STOMP send destination can not be empty.`); 136 | return; 137 | } 138 | // header必须是个正确的json 139 | if (this.state.stompSendHeader.length !== 0) { 140 | try { 141 | headerJSON = JSON.parse(this.state.stompSendHeader) 142 | } catch (e) { 143 | console.error('parse STOMP send header error %o', e); 144 | this.error(`STOMP send header format error: ${this.state.stompSendHeader}`); 145 | return; 146 | } 147 | } 148 | 149 | this.client.send(this.state.stompSendDestination, headerJSON, this.state.messageContent); 150 | this.info(`send STOMP message, destination = ${this.state.stompSendDestination}, content = ${this.state.messageContent}, header = ${this.state.stompSendHeader}`); 151 | } else { 152 | this.client.send(this.state.messageContent); 153 | this.info(`send message, content = ${this.state.messageContent}`); 154 | } 155 | } catch (e) { 156 | console.log('send message fail %o', e); 157 | this.error(`send message fail, message = ${e.message}, view chrome console for detail`); 158 | } 159 | }; 160 | 161 | /** 162 | * stomp subscribe 163 | */ 164 | subscribe = () => { 165 | if (this.state.stompSubscribeDestination.length === 0) { 166 | this.error(`STOMP subscribe destination can not be empty.`); 167 | return; 168 | } 169 | 170 | if (!this.state.stomp) { 171 | this.error(`Not in STOMP mode`); 172 | return; 173 | } 174 | 175 | if (!this.state.connected) { 176 | this.error(`Not Connected yet`); 177 | return; 178 | } 179 | 180 | try { 181 | this.client.subscribe(this.state.stompSubscribeDestination, this.getSubscribeCallback(this.state.stompSubscribeDestination)); 182 | this.info(`subscribe destination ${this.state.stompSubscribeDestination} success`); 183 | } catch (e) { 184 | console.error('subscribe fail: %o', e); 185 | this.error(`subscribe destination ${this.state.stompSubscribeDestination} fail, message = ${e.message}`); 186 | } 187 | }; 188 | 189 | getSubscribeCallback(destination) { 190 | return content => { 191 | this.info(`Receive subscribed message from destination ${destination}, content = ${content}`) 192 | }; 193 | } 194 | 195 | 196 | // controlled component相关handle方法 197 | // 挺蛋疼的... 198 | 199 | 200 | handleUrlChange = e => { 201 | this.setState({url: e.target.value}); 202 | }; 203 | 204 | handleConnectTypeChange = value => { 205 | let sockjs = false; 206 | let stomp = false; 207 | for (const tmp of value) { 208 | if (tmp === 'SockJS') { 209 | sockjs = true; 210 | } else if (tmp === 'STOMP') { 211 | stomp = true; 212 | } 213 | } 214 | this.setState({sockjs, stomp}); 215 | }; 216 | 217 | handleStompConnectHeaderChange = e => { 218 | this.setState({stompConnectHeader: e.target.value}); 219 | }; 220 | 221 | handleStompSubscribeDestinationChange = e => { 222 | this.setState({stompSubscribeDestination: e.target.value}); 223 | }; 224 | 225 | handleStompSendHeaderChange = e => { 226 | this.setState({stompSendHeader: e.target.value}); 227 | }; 228 | 229 | handleStompSendDestinationChange = e => { 230 | this.setState({stompSendDestination: e.target.value}); 231 | }; 232 | 233 | handleMessageContentChange = e => { 234 | this.setState({messageContent: e.target.value}); 235 | }; 236 | 237 | /** 238 | * 输出info级别信息 239 | * 240 | * @param message 241 | */ 242 | info = (message) => { 243 | this.log(`_INFO_:${message}`); 244 | }; 245 | 246 | /** 247 | * 输出error级别信息 248 | * 249 | * @param message 250 | */ 251 | error = (message) => { 252 | this.log(`_ERROR_:${message}`); 253 | }; 254 | 255 | /** 256 | * 在console中新增一行输出 257 | * 258 | * @param message 259 | */ 260 | log = (message) => { 261 | const length = this.state.message.length; 262 | const newMessage = this.state.message.slice(0, length); 263 | newMessage.push(message); 264 | this.setState({message: newMessage}, this.scrollToBottom); 265 | }; 266 | 267 | /** 268 | * 将console div滚动到底部 269 | */ 270 | scrollToBottom() { 271 | const scrollHeight = this.consoleOutput.scrollHeight; 272 | const height = this.consoleOutput.clientHeight; 273 | const maxScrollTop = scrollHeight - height; 274 | this.consoleOutput.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0; 275 | } 276 | 277 | /** 278 | * 清除console div中的内容 279 | */ 280 | clearOutput = () => { 281 | this.setState({message: []}); 282 | }; 283 | 284 | 285 | render() { 286 | 287 | const connectTypeArray = []; 288 | if (this.state.sockjs) { 289 | connectTypeArray.push('SockJS'); 290 | } 291 | if (this.state.stomp) { 292 | connectTypeArray.push('STOMP'); 293 | } 294 | 295 | // 我的布局真是一团糟... 296 | return
297 | 298 | {/*fork me on github*/} 299 | 300 | Fork me on GitHub 304 | 305 | 306 | {/*common input*/} 307 |
308 |
309 | 310 | } 311 | placeholder="URL to connect, 'ws://' for raw WebSocket or 'http://' for SockJS" 312 | style={{ width:'400px' }} value={this.state.url} onChange={this.handleUrlChange} 313 | disabled={this.state.connected}/> 314 | 315 | 316 | 318 | 319 |
320 |
321 | 322 | 325 | 326 |
327 |
328 | 329 | {/*STOMP相关输入项, 注意disabled条件, 必须上面勾选STOMP后才可用*/} 330 |
331 |
available if Connect Type = STOMP
332 |
333 | 334 | 337 | 338 |
339 |
340 | 341 | 344 | 345 | 346 | 347 | 348 |
349 |
350 | 351 | 354 | 355 |
356 |
357 | 358 | 361 | 362 |
363 |
364 | 365 | {/*要发送的消息*/} 366 |
367 |
368 | 369 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 |
380 |
381 | 382 | {/*console output*/} 383 |
384 |
385 |
{this.consoleOutput = div;}}> 386 |
387 |               
# console output
388 | 389 | {this.state.message.length == 0 &&
$ _
} 390 | 391 | {/*要显示的信息*/} 392 | {this.state.message.map((item, index) =>
393 | $  394 | {item} 395 |
)} 396 |
397 |
398 |
399 |
400 | 401 |
402 | }; 403 | 404 | } 405 | 406 | export default WebSocketDebugTool; 407 | --------------------------------------------------------------------------------