更新netty版本websocket相关用protubuf替换josn实现方式,以及服务端由原来的spring Struts2 改为springboot

This commit is contained in:
枫树 2018-08-21 21:07:44 +08:00
parent d88f7c4b0c
commit c85b2ce239
180 changed files with 10558 additions and 4086 deletions

View File

@ -9,6 +9,6 @@
<classpathentry kind="lib" path="libs/netty-transport-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-common-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-handler-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="D:/devtools/android-sdk-windows/platforms/android-21/android.jar"/>
<classpathentry kind="lib" path="D:/dev/Android-SDK-Windows/platforms/android-25/android.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry combineaccessrules="false" kind="src" path="/cim-server-sdk"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -0,0 +1 @@
/.gradle/

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>cim-boot-server</name>
<comment>Project cim-boot-server created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
connection.project.dir=
eclipse.preferences.version=1

View File

@ -0,0 +1,63 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.4.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
//jar包包名和版本
jar {
baseName = 'lvxin-server'
version = '1.0.0'
}
repositories {
jcenter()
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
configurations {
//all*.exclude group: 'com.fasterxml.jackson.core' , module: 'jackson-databind'
all*.exclude group: 'com.fasterxml.jackson.datatype'
all*.exclude group: 'com.fasterxml.jackson.module'
all*.exclude group: 'org.yaml'
all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-jdbc'
all*.exclude group: 'org.apache.tomcat'
all*.exclude group: 'io.undertow', module: 'undertow-websockets-jsr'
all*.exclude group: 'org.jboss.spec.javax.websocket'
all*.exclude group: 'org.jboss.spec.javax.transaction'
all*.exclude group: 'org.glassfish'
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile 'org.springframework.boot:spring-boot-starter-web:2.0.4.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-undertow:2.0.4.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-freemarker:2.0.4.RELEASE'
compile 'com.google.protobuf:protobuf-java:3.6.0'
compile 'com.google.code.gson:gson:2.8.5'
compile 'commons-io:commons-io:2.6'
compile 'org.apache.commons:commons-lang3:3.5'
compile 'com.squareup.okhttp3:okhttp:3.10.0'
compile 'cn.teaey.apns4j:apns4j:1.1.4'
compile 'io.netty:netty-handler:4.1.28.Final'
compile 'io.netty:netty-buffer:4.1.28.Final'
compile 'io.netty:netty-codec:4.1.28.Final'
compile 'io.netty:netty-codec-http:4.1.28.Final'
compile 'io.netty:netty-common:4.1.28.Final'
compile 'io.netty:netty-transport:4.1.28.Final'
}

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip

172
cim_for_netty/cim-boot-server/gradlew vendored Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,10 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user guide at https://docs.gradle.org/4.5/userguide/multi_project_builds.html
*/
rootProject.name = 'cim-boot-server'

View File

@ -0,0 +1,64 @@
package com.farsunset.cim;
import java.io.IOException;
import java.util.HashMap;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.farsunset.cim.handler.BindHandler;
import com.farsunset.cim.handler.SessionClosedHandler;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
@Configuration
public class CIMConfig implements CIMRequestHandler {
@Value("${cim.server.port}")
private int port;
@Autowired
private BindHandler bindHandler;
@Autowired
private SessionClosedHandler closedHandler;
private HashMap<String,CIMRequestHandler> appHandlerMap = new HashMap<>();
@PostConstruct
private void initHandler() {
/*
* 账号绑定handler
*/
appHandlerMap.put("client_bind", bindHandler);
/*
* 连接关闭handler
*/
appHandlerMap.put("client_closed", closedHandler);
}
@Bean
public CIMNioSocketAcceptor getNioSocketAcceptor() throws IOException {
CIMNioSocketAcceptor nioSocketAcceptor = new CIMNioSocketAcceptor();
nioSocketAcceptor.setPort(port);
nioSocketAcceptor.setAppSentBodyHandler(this);
nioSocketAcceptor.bind();
return nioSocketAcceptor;
}
@Override
public void process(CIMSession session, SentBody body) {
CIMRequestHandler handler = appHandlerMap.get(body.getKey());
if(handler == null) {return ;}
handler.process(session, body);
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim;
import java.lang.reflect.Modifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
*
* 其中@SpringBootApplication申明让spring boot自动给程序进行必要的配置
* 等价于以默认属性使用@Configuration@EnableAutoConfiguration和@ComponentScan
*/
@SpringBootApplication
@EnableAutoConfiguration(exclude = { WebSocketMessagingAutoConfiguration.class,
WebSocketReactiveAutoConfiguration.class,
WebSocketServletAutoConfiguration.class,
WebServicesAutoConfiguration.class,
JmxAutoConfiguration.class,
DataSourceAutoConfiguration.class,
ValidationAutoConfiguration.class,
WebClientAutoConfiguration.class,
JacksonAutoConfiguration.class })
public class ServerLauncher {
public static void main(String[] args) {
SpringApplication.run(ServerLauncher.class, args);
}
@Bean
@ConditionalOnMissingBean
public GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.PROTECTED | Modifier.TRANSIENT | Modifier.FINAL | Modifier.STATIC)
.create();
converter.setGson(gson);
return converter;
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.admin.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class NavigationController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(ModelAndView model) {
model.setViewName("console/index");
return model;
}
@RequestMapping(value = "/webclient/main.action", method = RequestMethod.GET)
public ModelAndView webclient(ModelAndView model) {
model.setViewName("console/webclient/main");
return model;
}
}

View File

@ -19,34 +19,28 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.admin.action;
package com.farsunset.cim.admin.controller;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.apache.struts2.ServletActionContext;
import com.farsunset.cim.service.impl.CIMSessionServiceImpl;
@Controller
@RequestMapping("/console/session")
public class SessionController {
import com.farsunset.cim.push.SystemMessagePusher;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.sdk.server.session.DefaultSessionManager;
import com.farsunset.cim.util.ContextHolder;
import com.opensymphony.xwork2.ActionSupport;
public class SessionAction extends ActionSupport {
/**
*
*/
private static final long serialVersionUID = 1L;
public String list() {
ServletActionContext.getRequest().setAttribute("sessionList",
((DefaultSessionManager) ContextHolder.getBean("CIMSessionManager")).queryAll());
return "list";
@Autowired
private CIMSessionServiceImpl sessionManager;
@RequestMapping(value = "/list.action")
public String list(Model model) {
model.addAttribute("sessionList", sessionManager.queryAll());
return "console/session/manage";
}
public void offline() throws IOException {
/*public void offline() throws IOException {
String account = ServletActionContext.getRequest().getParameter("account");
Message msg = new Message();
@ -56,5 +50,5 @@ public class SessionAction extends ActionSupport {
// 向客户端 发送消息
ContextHolder.getBean(SystemMessagePusher.class).push(msg);
}
}*/
}

View File

@ -0,0 +1,78 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.api.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.farsunset.cim.api.controller.dto.MessageResult;
import com.farsunset.cim.push.DefaultMessagePusher;
import com.farsunset.cim.push.SystemMessagePusher;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.util.Constants;
import com.farsunset.cim.util.StringUtil;
@RestController
@RequestMapping("/api/message")
public class MessageController {
@Autowired
private SystemMessagePusher systemMessagePusher;
@Autowired
private DefaultMessagePusher defaultMessagePusher;
/**
* 此方法仅仅在集群时通过服务器调用
*
* @param message
* @return
*/
@RequestMapping(value = "/dispatch",method=RequestMethod.POST)
public MessageResult dispatchSend(Message message) {
return send(message);
}
@RequestMapping(value = "/send",method=RequestMethod.POST)
public MessageResult send(Message message) {
MessageResult result = new MessageResult();
message.setMid(StringUtil.getUUID());
if (Constants.MessageType.TYPE_2.equals(message.getAction())) {
systemMessagePusher.push(message);
} else {
defaultMessagePusher.push(message);
}
result.id = message.getMid();
result.timestamp = message.getTimestamp();
return result;
}
}

View File

@ -1,5 +1,5 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,22 +19,16 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.handler;
package com.farsunset.cim.api.controller.dto;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
import java.util.List;
/**
* 记录心跳实现
*
*/
public class HeartbeatHandler implements CIMRequestHandler {
import org.springframework.http.HttpStatus;
public ReplyBody process(CIMSession session, SentBody message) {
// 收到心跳响应设置心跳时间
session.setHeartbeat(System.currentTimeMillis());
return null;
}
public class BaseResult {
public int code = HttpStatus.OK.value();
public String message;
public Object data;
public List<?> dataList;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.api.controller.dto;
public class MessageResult extends BaseResult {
public long timestamp;
public String id;
}

View File

@ -21,32 +21,45 @@
*/
package com.farsunset.cim.handler;
import java.net.InetAddress;
import org.apache.log4j.Logger;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.farsunset.cim.push.SystemMessagePusher;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
import com.farsunset.cim.sdk.server.session.DefaultSessionManager;
import com.farsunset.cim.util.ContextHolder;
import com.farsunset.cim.service.impl.CIMSessionServiceImpl;
import com.farsunset.cim.util.StringUtil;
/**
* 账号绑定实现
*
*/
@Component
public class BindHandler implements CIMRequestHandler {
protected final Logger logger = Logger.getLogger(BindHandler.class);
protected final Logger logger = LoggerFactory.getLogger(BindHandler.class);
public ReplyBody process(CIMSession newSession, SentBody message) {
DefaultSessionManager sessionManager = ((DefaultSessionManager) ContextHolder.getBean("CIMSessionManager"));
@Autowired
private CIMSessionServiceImpl sessionManager;
@Value("${server.host}")
private String host;
public void process(CIMSession newSession, SentBody message) {
ReplyBody reply = new ReplyBody();
reply.setKey(message.getKey());
reply.setCode(CIMConstant.ReturnCode.CODE_200);
try {
@ -54,13 +67,14 @@ public class BindHandler implements CIMRequestHandler {
newSession.setGid(StringUtil.getUUID());
newSession.setAccount(account);
newSession.setDeviceId(message.get("deviceId"));
newSession.setHost(InetAddress.getLocalHost().getHostAddress());
newSession.setHost(host);
newSession.setChannel(message.get("channel"));
newSession.setDeviceModel(message.get("device"));
newSession.setClientVersion(message.get("version"));
newSession.setSystemVersion(message.get("osVersion"));
newSession.setBindTime(System.currentTimeMillis());
newSession.setPackageName(message.get("packageName"));
/**
* 由于客户端断线服务端可能会无法获知的情况客户端重连时需要关闭旧的连接
*/
@ -68,20 +82,13 @@ public class BindHandler implements CIMRequestHandler {
// 如果是账号已经在另一台终端登录则让另一个终端下线
if (newSession.fromOtherDevice(oldSession)) {
if (oldSession != null && fromOtherDevice(newSession,oldSession) && oldSession.isConnected()) {
sendForceOfflineMessage(oldSession, account, newSession.getDeviceModel());
}
// 如果是重复连接则直接返回
if (newSession.equals(oldSession)) {
// 第一次设置心跳时间为登录时间
newSession.setBindTime(System.currentTimeMillis());
oldSession.setStatus(CIMSession.STATUS_ENABLED);
sessionManager.update(oldSession);
reply.put("sid", oldSession.getGid());
return reply;
}
closeQuietly(oldSession);
// 第一次设置心跳时间为登录时间
newSession.setBindTime(System.currentTimeMillis());
@ -91,39 +98,32 @@ public class BindHandler implements CIMRequestHandler {
} catch (Exception e) {
reply.setCode(CIMConstant.ReturnCode.CODE_500);
e.printStackTrace();
logger.error(e.getMessage());
}
logger.debug("bind :account:" + message.get("account") + "-----------------------------" + reply.getCode());
reply.put("sid", newSession.getGid());
return reply;
newSession.write(reply);
}
private boolean fromOtherDevice(CIMSession oldSession ,CIMSession newSession) {
return !Objects.equals(oldSession.getDeviceId(), newSession.getDeviceId());
}
private void sendForceOfflineMessage(CIMSession oldSession, String account, String deviceModel) {
Message msg = new Message();
msg.setMid(String.valueOf(System.currentTimeMillis()));
msg.setAction(CIMConstant.MessageAction.ACTION_999);// 强行下线消息类型
msg.setReceiver(account);
msg.setSender("system");
msg.setContent(deviceModel);
closeQuietly(oldSession, msg);
ContextHolder.getBean(SystemMessagePusher.class).push(msg);
}
// 同一设备切换网络时关闭旧的连接
private void closeQuietly(CIMSession oldSession) {
if (oldSession != null && oldSession.isConnected()) {
oldSession.removeAttribute(CIMConstant.SESSION_KEY);
oldSession.closeNow();
}
msg.setMid(StringUtil.getUUID());
closeQuietly(oldSession,msg);
}
// 不同设备同一账号登录时关闭旧的连接
private void closeQuietly(CIMSession oldSession, Message msg) {
if (oldSession.isConnected()) {
private void closeQuietly(CIMSession oldSession,Message msg) {
if (oldSession.isConnected() && Objects.equals(host, oldSession.getHost())) {
oldSession.write(msg);
oldSession.removeAttribute(CIMConstant.SESSION_KEY);
oldSession.closeNow();

View File

@ -21,36 +21,41 @@
*/
package com.farsunset.cim.handler;
import org.apache.log4j.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
import com.farsunset.cim.sdk.server.session.DefaultSessionManager;
import com.farsunset.cim.util.ContextHolder;
import com.farsunset.cim.service.impl.CIMSessionServiceImpl;
/**
* 断开连接清除session
*
*/
@Component
public class SessionClosedHandler implements CIMRequestHandler {
protected final Logger logger = Logger.getLogger(SessionClosedHandler.class);
protected final Logger logger = LoggerFactory.getLogger(SessionClosedHandler.class);
public ReplyBody process(CIMSession ios, SentBody message) {
@Autowired
private CIMSessionServiceImpl sessionManager;
public void process(CIMSession ios, SentBody message) {
Object account = ios.getAttribute(CIMConstant.SESSION_KEY);
if (account == null) {
return null;
return;
}
DefaultSessionManager sessionManager = ((DefaultSessionManager) ContextHolder.getBean("CIMSessionManager"));
ios.removeAttribute(CIMConstant.SESSION_KEY);
sessionManager.remove(account.toString());
return null;
}
}

View File

@ -21,70 +21,70 @@
*/
package com.farsunset.cim.push;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.sdk.server.session.CIMSession;
import com.farsunset.cim.sdk.server.session.DefaultSessionManager;
import com.farsunset.cim.service.ApnsService;
import com.farsunset.cim.service.impl.MessageDispatcherImpl;
/**
* 消息发送实现类
*
*/
@Component
public class DefaultMessagePusher implements CIMMessagePusher {
@Value("${server.host}")
private String host;
@Autowired
private DefaultSessionManager sessionManager;
@Autowired
private MessageDispatcherImpl messageDispatcher;
public void setSessionManager(DefaultSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Autowired
private ApnsService apnsService;
/**
* 向用户发送消息
*
* @param msg
*/
public void push(Message msg) {
CIMSession session = sessionManager.get(msg.getReceiver());
public void push(Message message) {
CIMSession session = sessionManager.get(message.getReceiver());
/*
/**
* 服务器集群时可以在此 判断当前session是否连接于本台服务器如果是继续往下走如果不是将此消息发往当前session连接的服务器并
* return if(session!=null&&!session.isLocalhost()){//判断当前session是否连接于本台服务器如不是
* //发往目标服务器处理
* MessageDispatcher.execute(MessageUtil.transform(msg),session.getHost());
* return; }
*/
if (session != null && session.isConnected()) {
session.write(msg);
if (session.isConnected() && !Objects.equals(host, session.getHost())) {
messageDispatcher.forward(message, session.getHost());
return;
}
// 如果用户标示了APNS ON 说明这是ios设备需要使用apns发送
if (session != null && session.getApnsAble() == CIMSession.APNS_ON) {
apnsPush(1, msg.getContent(), session.getDeviceId());
/**
* 如果是在当前服务器则直接推送
*/
if (session.isConnected() && Objects.equals(host, session.getHost())) {
session.write(message);
return;
}
/**
* ios设备流程特别处理如果长链接断开了并且ApnsAble为打开状态的话优走apns
*/
if (Objects.equals(session.getChannel(), CIMSession.CHANNEL_IOS) && Objects.equals(session.getApnsAble(), CIMSession.APNS_ON)) {
apnsService.push(message, session.getDeviceId());
}
}
/**
* 引入javaapns相关jar
*
* @param badge
* @param content
* @param token
*/
private void apnsPush(int badge, String content, String token) {
/*
* String password = "password"; String keystore = "p12 文件 绝对路径"; boolean
* isDebug = true;
*
* PushNotificationPayload payload = PushNotificationPayload.complex(); try {
* payload.addAlert(content); payload.addBadge(1); payload.addSound("default");
* Push.payload(payload, keystore, password, isDebug, token);
*
* } catch (Exception e) { e.printStackTrace(); }
*/
}
}

View File

@ -21,8 +21,11 @@
*/
package com.farsunset.cim.push;
import org.springframework.stereotype.Component;
import com.farsunset.cim.sdk.server.model.Message;
@Component
public class SystemMessagePusher extends DefaultMessagePusher {
@Override

View File

@ -1,5 +1,5 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,27 +19,11 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.util;
package com.farsunset.cim.service;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import com.farsunset.cim.sdk.server.model.Message;
public class ContextHolder implements ApplicationContextAware {
public interface ApnsService {
private static ApplicationContext context;
public static Object getBean(String name) {
return context.getBean(name);
}
public static <T> T getBean(Class<T> c) {
return context.getBean(c);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
void push( Message message, String deviceToken);
}

View File

@ -0,0 +1,28 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service;
import com.farsunset.cim.sdk.server.model.Message;
public interface MessageDispatcher {
void forward(final Message msg, final String ip);
}

View File

@ -0,0 +1,31 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import org.springframework.stereotype.Service;
import com.farsunset.cim.sdk.server.session.DefaultSessionManager;
@Service
public class CIMSessionServiceImpl extends DefaultSessionManager {
}

View File

@ -0,0 +1,86 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import java.io.InputStream;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.service.ApnsService;
import com.farsunset.cim.util.ApnsPayloadCompat;
import cn.teaey.apns4j.Apns4j;
import cn.teaey.apns4j.network.ApnsChannel;
import cn.teaey.apns4j.network.ApnsChannelFactory;
import cn.teaey.apns4j.network.ApnsGateway;
@Service
public class JavaApnsServiceImpl implements ApnsService {
private final Logger logger = Logger.getLogger(JavaApnsServiceImpl.class.getName());
@Value("${apple.apns.p12.password}")
private String password;
@Value("${apple.apns.p12.file}")
private String p12Path;
@Value("${apple.apns.debug}")
private boolean isDebug;
@SuppressWarnings("deprecation")
@Override
public void push(Message message, String deviceToken) {
if(StringUtils.isBlank(deviceToken)) {
return ;
}
InputStream stream = getClass().getResourceAsStream(p12Path);
ApnsChannelFactory apnsChannelFactory = Apns4j.newChannelFactoryBuilder()
.keyStoreMeta(stream)
.keyStorePwd(password)
.apnsGateway(isDebug ? ApnsGateway.DEVELOPMENT : ApnsGateway.PRODUCTION)
.build();
ApnsChannel apnsChannel = apnsChannelFactory.newChannel();
try {
ApnsPayloadCompat apnsPayload = new ApnsPayloadCompat();
apnsPayload.setAction(message.getAction());
apnsPayload.setContent(message.getContent());
apnsPayload.setSender(message.getSender());
apnsPayload.setFormat(message.getFormat());
apnsPayload.setReceiver(message.getReceiver());
apnsChannel.send(deviceToken, apnsPayload);
logger.info(deviceToken +"\r\ndata:" +apnsPayload.toJsonString());
}catch(Exception e) {
e.printStackTrace();
logger.severe(e.getLocalizedMessage());
}finally {
apnsChannel.close();
IOUtils.closeQuietly(stream);
}
}
}

View File

@ -0,0 +1,83 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.service.MessageDispatcher;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@Component
public class MessageDispatcherImpl implements MessageDispatcher{
private static final Logger logger = Logger.getLogger(MessageDispatcherImpl.class.getName());
@Value("${sys.message.dispatch.url}")
private String dispatchUrl;
@Override
public void forward(final Message msg, final String ip) {
String apiUrl = String.format(dispatchUrl, ip);
try {
String response = httpPost(apiUrl, msg);
logger.info("消息转发目标服务器{" + ip + "},结果:" + response);
} catch (Exception e) {
e.printStackTrace();
logger.severe("消息转发目标服务器" + apiUrl + "message:" + e.getMessage());
}
}
@SuppressWarnings("deprecation")
private String httpPost(String url, Message msg) throws Exception {
OkHttpClient httpclient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).build();
FormBody.Builder build = new FormBody.Builder();
build.add("mid", String.valueOf(msg.getMid()));
build.add("action", msg.getAction());
build.add("title", msg.getTitle());
build.add("content", msg.getContent());
build.add("sender", msg.getSender());
build.add("receiver", msg.getReceiver());
build.add("format", msg.getFormat());
build.add("extra", msg.getExtra());
build.add("timestamp", String.valueOf(msg.getTimestamp()));
Request request = new Request.Builder().url(url).post(build.build()).build();
Response response = httpclient.newCall(request).execute();
String data = response.body().string();
IOUtils.closeQuietly(response);
return data;
}
}

View File

@ -1,65 +1,65 @@
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.session;
import java.util.List;
import com.farsunset.cim.sdk.server.session.CIMSession;
import com.farsunset.cim.sdk.server.session.SessionManager;
/**
* 集群 session管理实现示例 各位可以自行实现 AbstractSessionManager接口来实现自己的 session管理 服务器集群时
* 须要将CIMSession 信息存入数据库或者nosql 第三方存储空间中便于所有服务器都可以访问
*/
public class ClusterSessionManager implements SessionManager {
public CIMSession get(String account) {
// 这里查询数据库
/*
* CIMSession session = database.getSession(account);
* session.setIoSession(ContextHolder.getBean(CIMNioSocketAcceptor.class).
* getManagedSessions().get(session.getNid())); return session;
*/
return null;
}
@Override
public List<CIMSession> queryAll() {
return null;
}
@Override
public void remove(String account) {
}
@Override
public void update(CIMSession session) {
}
@Override
public void add(CIMSession arg0) {
}
}
/**
* Copyright 2013-2023 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.session;
import java.util.List;
import com.farsunset.cim.sdk.server.session.CIMSession;
import com.farsunset.cim.sdk.server.session.SessionManager;
/**
* 集群 session管理实现示例 各位可以自行实现 AbstractSessionManager接口来实现自己的 session管理 服务器集群时
* 须要将CIMSession 信息存入数据库或者nosql 第三方存储空间中便于所有服务器都可以访问
*/
public class ClusterSessionManager implements SessionManager {
public CIMSession get(String account) {
// 这里查询数据库
/*
* CIMSession session = database.getSession(account);
* session.setIoSession(ContextHolder.getBean(CIMNioSocketAcceptor.class).
* getManagedSessions().get(session.getNid())); return session;
*/
return null;
}
@Override
public List<CIMSession> queryAll() {
return null;
}
@Override
public void remove(String account) {
}
@Override
public void update(CIMSession session) {
}
@Override
public void add(CIMSession arg0) {
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright 2013-2033 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.util;
import cn.teaey.apns4j.protocol.ApnsPayload;
public class ApnsPayloadCompat extends ApnsPayload {
public static final String DATA = "{\"aps\": {\"message\": {\"action\":\"%s\",\"content\":\"%s\",\"sender\":\"%s\",\"receiver\":\"%s\",\"format\":\"%s\"},\"content-available\": 1}}";
private String action;
private String content;
private String sender;
private String format;
private String receiver;
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public String toJsonString() {
return String.format(DATA, action, content, sender,receiver,format);
}
}

View File

@ -63,7 +63,6 @@ public class StringUtil {
}
public static String getUUID() {
// TODO Auto-generated method stub
return UUID.randomUUID().toString().replaceAll("-", "");
}

View File

@ -0,0 +1,34 @@
server.port=8080
server.host=192.168.1.100
##################################################################
# Freemarker Config #
##################################################################
spring.freemarker.suffix=.html
spring.freemarker.charset=utf-8
spring.freemarker.content-type=text/html
spring.freemarker.cache=false
spring.freemarker.templateLoaderPath=classpath:/page/
spring.freemarker.settings.auto_import = /ftl/spring.ftl as spring
spring.messages.encoding=UTF-8
spring.messages.basename=i18n/messages
##################################################################
# CIM Config #
##################################################################
cim.server.port =23456
apple.apns.debug=false
apple.apns.p12.password= your p12 password
apple.apns.p12.file= /apns/lvxin.p12
##################################################################
# Undertow Config #
##################################################################
server.undertow.accesslog.enabled=false
server.undertow.worker-threads=16
server.undertow.io-threads=8
server.undertow.buffer-size=1024
server.undertow.direct-buffers=true
##################################################################
# System Config #
##################################################################
sys.message.dispatch.url = http://%1$s:${server.port}/api/message/dispatch

View File

@ -0,0 +1,67 @@
module.common.html.title = CIM\u7BA1\u7406\u7CFB\u7EDF
module.common.account = \u5e10\u53f7
module.common.password = \u5bc6\u7801
module.common.save = \u4fdd\u5b58
module.common.id = ID
module.common.type =\u7c7b\u578b
module.common.name =\u540d\u79f0
module.common.time =\u65f6\u95f4
module.common.operation =\u64cd\u4f5c
module.common.query =\u67e5\u8be2
module.common.look =\u67e5\u770b
module.common.preview =\u9884\u89c8
module.common.send =\u53d1\u9001
module.common.saveing = \u6b63\u5728\u4fdd\u5b58,\u8bf7\u7a0d\u5019......
module.common.add = \u6dfb\u52a0
module.common.update = \u4fee\u6539
module.common.delete = \u5220\u9664
module.common.save.success = \u4fdd\u5b58\u6210\u529f
module.common.delete.success = \u5220\u9664\u6210\u529f
module.common.delete.loading =\u6b63\u5728\u5220\u9664,\u8bf7\u7a0d\u5019......
module.common.loading = \u52a0\u8f7d\u4e2d,\u8bf7\u7a0d\u5019......
module.common.description = \u8bf4\u660e
module.common.state = \u72b6\u6001
module.common.content = \u5185\u5bb9
module.common.location = \u4f4d\u7f6e
module.common.longitude = \u7ecf\u5ea6
module.common.latitude = \u7eac\u5ea6
module.common.sort = \u6392\u5e8f
module.common.code = \u7f16\u53f7
module.common.import = \u5bfc\u5165
module.common.headlogo = \u5934\u50cf
module.common.homepage = \u4e3b\u9875
module.common.website = \u7f51\u5740
module.common.text = \u6587\u5b57
module.global.error.500.hint = \u670d\u52a1\u7a0b\u5e8f\u53d1\u751f\u5185\u90e8\u9519\u8bef
module.global.error.400.hint = \u8bf7\u6c42\u53c2\u6570\u7c7b\u578b\u4e0d\u6b63\u786e
module.global.error.404.hint = \u8bbf\u95ee\u7684\u8d44\u6e90\u4e0d\u5b58\u5728
module.global.pager.next = \u4e0b\u4e00\u9875
module.global.pager.previou = \u4e0a\u4e00\u9875
module.global.pager.description = \u5171{0}\u6761\u8bb0\u5f55,{1}\u9875
module.console.about = \u5173\u4e8e
module.console.about.version = v3.5.0
module.console.about.author = \u4f5c\u8005: \u8fdc\u65b9\u5915\u9633
module.console.about.author.wechat = \u5fae\u4fe1: 3979434
module.console.about.author.qq = Q Q: 3979434
module.console.about.copyright = \u57FA\u4E8EJava\u670D\u52A1\u7AEF\u7684\u5373\u65F6\u901A\u4FE1\u89E3\u51B3\u65B9\u6848,\u4E0Eandroid \u5BA2\u6237\u7AEF\u5B8C\u7F8E\u7ED3\u5408\uFF0C\u540C\u65F6\u652F\u6301\u5176\u4ED6\u8BED\u8A00\u7684\u79FB\u52A8\u5E94\u7528\uFF0C\u7269\u8054\u7F51\uFF0C\u667A\u80FD\u5BB6\u5C45\uFF0C\u684C\u9762\u5E94\u7528\uFF0CWEB\u5E94\u7528\u4EE5\u53CA\u540E\u53F0\u7CFB\u7EDF\u4E4B\u95F4\u7684\u5373\u65F6\u6D88\u4EA4\u4E92\uFF0C\u4E3A\u4F60\u89E3\u51B3\u4E86\u957F\u8FDE\u63A5\u5404\u79CD\u6D88\u606F\u4E8B\u4EF6\uFF0C\u65AD\u7EBF\u91CD\u8FDE\u7B49\u7E41\u7410\u7684\u5904\u7406\uFF0C\u4F7F\u7528\u65B9\u4FBF\u6613\u4E8E\u96C6\u6210\u3002<br/><a target="_blank" href="https://gitee.com/farsunset/cim">https://gitee.com/farsunset/cim</a>
module.console.menu.onlineuser = \u5728\u7ebf\u7528\u6237
module.console.cimsession.sending = \u6b63\u5728\u53d1\u9001\uff0c\u8bf7\u7a0d\u5019......
module.console.cimsession.send.success = \u53d1\u9001\u6210\u529f
module.console.cimsession.logo =\u5934\u50cf
module.console.cimsession.account =\u5e10\u53f7
module.console.cimsession.nid = \u8fde\u63a5ID
module.console.cimsession.channel = \u7ec8\u7aef
module.console.cimsession.app.version =\u5e94\u7528\u7248\u672c
module.console.cimsession.os.version =\u7cfb\u7edf\u7248\u672c
module.console.cimsession.deviceid =\u8bbe\u5907\u7f16\u53f7
module.console.cimsession.device.model =\u7ec8\u7aef\u578b\u53f7
module.console.cimsession.online.time =\u5728\u7ebf\u65f6\u957f(\u79d2)
module.console.cimsession.time.format ={0}\u79d2
module.console.cimsession.send.message =\u53d1\u9001\u6d88\u606f
module.console.cimsession.receiver = \u63a5\u6536\u5e10\u53f7
module.console.cimsession.message = \u6d88\u606f\u5185\u5bb9

View File

@ -0,0 +1,44 @@
<!-- header -->
<div id="_main_header_banner" class="header">
<h3 style="float:left;color:#ffffff;line-height: 60px;margin-left: 30px;font-family: 楷体;font-size: 32px">CIM体验页面</h3>
<div class="btn-group" style=" float: right;margin-top: 33px;margin-right:20px;">
<button class="btn btn-info" onclick="doShowDialog('aboutDialog')">
<span class="glyphicon glyphicon-info-sign"></span>&nbsp;<@spring.message "module.console.about"/>
</button>
</div>
<div class="header_liner"></div>
</div>
<div class="modal fade" id="aboutDialog" tabindex="-1" role="dialog">
<div class="modal-dialog" style="width: 420px;">
<div class="modal-content">
<div class="modal-header" >
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><@spring.message "module.common.html.title"/></h4>
</div>
<div class="modal-body">
<div style="text-align: center;border:none;height: 150px;">
<img src="/image/icon.png" width="100px" height="100px" style="box-shadow: 0px 0px 7px 2px #d1d3d6;border: 1px solid #dfdfe0;padding: 10px;border-radius: 100px;margin-top: 20px;"/>
<h4 style="margin-top: 15px;"><@spring.message "module.console.about.version"/></h4>
</div>
<ul class="list-group" style="margin-top: 20px;">
<li class="list-group-item" style="border-radius: 0px;">
<@spring.message "module.console.about.copyright"/>
</li>
<li class="list-group-item" style="border-radius: 0px;">
<@spring.message "module.console.about.author"/>
</li>
<li class="list-group-item" style="border-radius: 0px;">
<@spring.message "module.console.about.author.qq"/>
</li>
<li class="list-group-item" style="border-radius: 0px;">
<@spring.message "module.console.about.author.wechat"/>
</li>
</ul>
</div>
</div>
</div>
</div>
<div id="global_mask" style="display: none; position: absolute; top: 0px; left: 0px; z-index: 9999; background-color: rgb(20, 20, 20); opacity: 0.5; width: 100%; height: 100%; overflow: hidden; background-position: initial initial; background-repeat: initial initial;"></div>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><@spring.message "module.common.html.title"/></title>
<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/common.css" />
<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/js/common.js"></script>
</head>
<body class="web-app ui-selectable">
<#include "/console/header.html">
<#include "/console/nav.html">
<div id="mainWrapper" style="padding-top: 25px;" >
</div>
<script>
$('#indexMenu').addClass('current');
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<div id="_main_nav" class="ui-vnav">
<ul class="ui-nav-inner">
<li style="height: 50px;text-align: center;margin-top: 10px;">
<a type="button" target="_blank" href="/webclient/main.action" class="btn btn-danger" >
<span class="glyphicon glyphicon-globe"></span> WEB版本
</a>
</li>
<li style="border-bottom: 1px solid #D1D6DA;"></li>
<li class="ui-item" id="sessionMenu">
<a href="/console/session/list.action">
<img src="/image/icon/online.svg" style="margin-top:-2px;" width="24px" height="24px"/>
<span class="ui-text"><@spring.message "module.console.menu.onlineuser"/></span>
</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,129 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><@spring.message "module.common.html.title"/></title>
<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/common.css" />
<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/js/common.js"></script>
<script type="text/javascript" src="/js/jquery-ui.min.js"></script>
<script>
function showMessageDialog(account){
$('#messageDialog').modal('show');
$('#Saccount').val(account);
}
function doSendMessage(){
var message = $('#message').val();
var account = $('#Saccount').val();
if($.trim(message)=='')
{
return;
}
showProcess("<@spring.message 'module.console.cimsession.sending'/>");
$.post("/api/message/send", {content:message,action:2,sender:'system',receiver:account,format:'0'},
function(data){
hideProcess();
showSTip("<@spring.message 'module.console.cimsession.send.success'/>");
doHideDialog("messageDialog");
});
}
</script>
</head>
<body class="web-app ui-selectable">
<#include "/console/header.html">
<#include "/console/nav.html">
<div id="mainWrapper">
<div class="lay-main-toolbar"></div>
<div>
<form action="/console/session/list.action" method="post" id="searchForm" >
<input type="hidden" name="currentPage" id="currentPage" value="0"/>
<table style="width: 100%" class="utable">
<thead>
<tr class="tableHeader">
<th width="8%"><@spring.message 'module.console.cimsession.account'/></th>
<th width="6%"> <@spring.message 'module.console.cimsession.nid'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.channel'/></th>
<th width="14%"><@spring.message 'module.console.cimsession.deviceid'/></th>
<th width="10%"><@spring.message 'module.console.cimsession.device.model'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.app.version'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.os.version'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.online.time'/></th>
<th width="25%"><@spring.message 'module.common.location'/></th>
<th width="8%"><@spring.message "module.common.operation"/></th>
</tr>
</thead>
<tbody>
<#list sessionList as cimsession>
<tr style="height: 50px;">
<td>${cimsession.account! }</td>
<td>${cimsession.nid!}</td>
<td>${cimsession.channel! }</td>
<td>${cimsession.deviceId! }</td>
<td>${cimsession.deviceModel! }</td>
<td>${cimsession.clientVersion! }</td>
<td>${cimsession.systemVersion! }</td>
<td>
<@spring.messageArgs "module.console.cimsession.time.format",[((.now?long - cimsession.bindTime)/1000)?round?c] />
</td>
<td>${cimsession.location!}</td>
<td>
<div class="btn-group btn-group-xs">
<button type="button" class="btn btn-primary" style="padding: 5px;" onclick="showMessageDialog('${cimsession.account!}')">
<span class="glyphicon glyphicon-send" style="top:2px;"></span>
<@spring.message 'module.console.cimsession.send.message'/>
</button>
</div>
</td>
</tr>
</#list>
</tbody>
</table>
</form>
</div>
</div>
<div class="modal fade" id="messageDialog" tabindex="-1" role="dialog" >
<div class="modal-dialog" style="width: 420px;">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><@spring.message 'module.console.cimsession.send.message'/></h4>
</div>
<div class="modal-body">
<div class="form-groupBuy">
<label for="Amobile">
<@spring.message 'module.console.cimsession.receiver'/>
</label>
<input type="text" class="form-control" id="Saccount" name="account"
style="width: 100%; font-size: 20px; font-weight: bold;height:40px;"
disabled="disabled" />
</div>
<div class="form-groupBuy" style="margin-top: 20px;">
<label for="exampleInputFile">
<@spring.message 'module.console.cimsession.message'/>
</label>
<textarea rows="10" style="width: 100%; height: 200px;" id="message" name="message" class="form-control"></textarea>
</div>
</div>
<div class="modal-footer" style="padding: 5px 10px; text-align: center;">
<button type="button" class="btn btn-success btn-lg" style="width: 200px;" onclick="doSendMessage()">
<span class="glyphicon glyphicon-send" style="top:2px;"></span><@spring.message 'module.common.send'/>
</button>
</div>
</div>
</div>
</div>
<script>
$('#sessionMenu').addClass('current');
</script>
</body>
</html>

View File

@ -1,16 +1,7 @@
<%@ page language="java" pageEncoding="utf-8"%>
<%
String loginBasePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath();
%>
<script type="text/javascript">
function doLogin()
{
ACCOUNT = $('#account').val();
if($.trim(ACCOUNT)=='' )
{
if($.trim($('#account').val()) =='' ){
return;
}
showProcess('正在接入请稍后......');
@ -20,17 +11,17 @@
</script>
<div class="modal fade" id="LoginDialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog" style="width: 300px;margin: 30px auto;">
<div class="modal-dialog" style="width: 400px;margin: 64px auto;">
<div class="modal-content" >
<div class="modal-body" style="padding:0px;" >
<div style="height:150px;text-align: center; background: #5FA0D3; color: #ffffff; border: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px;">
<img src="<%=loginBasePath %>/resource/img/icon.png" style="height: 60px;width: 60px;margin-top:30px;"/>
<div style="margin-top: 20px; color: #ffffff;font-size: 16px;">使用用户帐号登录</div>
<img src="/image/icon.png" style="height: 60px;width: 60px;margin-top:30px;"/>
<div style="margin-top: 20px; color: #ffffff;font-size: 16px;">请输入一个帐号用于登录,随后接收推送消息</div>
</div>
<div class="input-group" style="margin-top: 20px;margin-left:10px;margin-right:10px;margin-bottom:20px;">
<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
<span class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></span>
<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号"
<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号(数字或者英文字母)"
style="display: inline; width: 100%; height: 50px;" />
</div>

View File

@ -0,0 +1,103 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="utf-8"/>
<title>CIM Webclient </title>
<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
<link charset="utf-8" rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
<link charset="utf-8" rel="stylesheet" href="/css/common.css" />
<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/js/common.js"></script>
<script type="text/javascript" src="/js/cim/message.js"></script>
<script type="text/javascript" src="/js/cim/replybody.js"></script>
<script type="text/javascript" src="/js/cim/sentbody.js"></script>
<script type="text/javascript" src="/js/cim/cim.web.sdk.js"></script>
<script type="text/javascript">
/** 当socket连接成功回调 **/
function onConnectionSuccessed(){
CIMWebBridge.bindAccount($('#account').val());
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(reply)
{
console.log(reply);
if(reply.key=='client_bind' && reply.code==200)
{
hideProcess();
$('#LoginDialog').fadeOut();
$('#MessageDialog').fadeIn();
$('#MessageDialog').addClass("in");
$("#current_account").text($('#account').val());
}
}
/** 当收到消息时候回调 **/
function onMessageReceived(message)
{
console.log(message);
if(message.action == ACTION_999){
$('#MessageDialog').fadeOut();
$('#LoginDialog').fadeIn();
$('#LoginDialog').addClass("in");
return ;
}
showNotification(message.content);
$("#messageList").append("<h6 style='text-align: center;'>时间:"+new Date(message.timestamp).toLocaleString()+"</h6>");
$("#messageList").append("<div class='alert alert-info' >"+message.content+"</div>");
}
$(document).ready(function(){
$('#LoginDialog').fadeIn();
$('#LoginDialog').addClass("in");
initNotification();
});
function initNotification(){
//判断浏览器是否支持桌面通知
if (window.Notification) {
var notification = window.Notification;
if (notification.permission == "default") {
notification.requestPermission(function(permission) {
});
}
}
}
function showNotification(msg){
var notify = new Notification("系统消息", {
body: msg,
icon: '/image/icon.png',
tag: 1
});
notify.onshow = function() {
setTimeout(function() {
notify.close();
}, 3000);
}
}
</script>
</head>
<body style="width: 600px;">
<#include "loginDialog.html">
<#include "messageDialog.html">
</body>
</html>

View File

@ -1,16 +1,12 @@
<%@ page language="java" pageEncoding="utf-8"%>
<div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog" >
<div class="modal-dialog" style="width: 500px;margin: 30px auto;">
<div class="modal-dialog" style="width: 600px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-header" style="text-align: center;">
<h4>消息列表</h4>
<span style="float: left;">请在管理页面推送一条消息</span>
<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account">10000</span></span>
<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account"></span></span>
</div>
<div class="modal-body" id="messageList" style="min-height: 600px;" >
</div>
</div>
</div>

View File

@ -0,0 +1,384 @@
<#ftl strip_whitespace=true>
<#--
* spring.ftl
*
* This file consists of a collection of FreeMarker macros aimed at easing
* some of the common requirements of web applications - in particular
* handling of forms.
*
* Spring's FreeMarker support will automatically make this file and therefore
* all macros within it available to any application using Spring's
* FreeMarkerConfigurer.
*
* To take advantage of these macros, the "exposeSpringMacroHelpers" property
* of the FreeMarker class needs to be set to "true". This will expose a
* RequestContext under the name "springMacroRequestContext", as needed by
* the macros in this library.
*
* @author Darren Davison
* @author Juergen Hoeller
* @since 1.1
-->
<#--
* message
*
* Macro to translate a message code into a message.
-->
<#macro message code>${springMacroRequestContext.getMessage(code)}</#macro>
<#--
* messageText
*
* Macro to translate a message code into a message,
* using the given default text if no message found.
-->
<#macro messageText code, text>${springMacroRequestContext.getMessage(code, text)}</#macro>
<#--
* messageArgs
*
* Macro to translate a message code with arguments into a message.
-->
<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)}</#macro>
<#--
* messageArgsText
*
* Macro to translate a message code with arguments into a message,
* using the given default text if no message found.
-->
<#macro messageArgsText code, args, text>${springMacroRequestContext.getMessage(code, args, text)}</#macro>
<#--
* theme
*
* Macro to translate a theme message code into a message.
-->
<#macro theme code>${springMacroRequestContext.getThemeMessage(code)}</#macro>
<#--
* themeText
*
* Macro to translate a theme message code into a message,
* using the given default text if no message found.
-->
<#macro themeText code, text>${springMacroRequestContext.getThemeMessage(code, text)}</#macro>
<#--
* themeArgs
*
* Macro to translate a theme message code with arguments into a message.
-->
<#macro themeArgs code, args>${springMacroRequestContext.getThemeMessage(code, args)}</#macro>
<#--
* themeArgsText
*
* Macro to translate a theme message code with arguments into a message,
* using the given default text if no message found.
-->
<#macro themeArgsText code, args, text>${springMacroRequestContext.getThemeMessage(code, args, text)}</#macro>
<#--
* url
*
* Takes a relative URL and makes it absolute from the server root by
* adding the context root for the web application.
-->
<#macro url relativeUrl extra...><#if extra?? && extra?size!=0>${springMacroRequestContext.getContextUrl(relativeUrl,extra)}<#else>${springMacroRequestContext.getContextUrl(relativeUrl)}</#if></#macro>
<#--
* bind
*
* Exposes a BindStatus object for the given bind path, which can be
* a bean (e.g. "person") to get global errors, or a bean property
* (e.g. "person.name") to get field errors. Can be called multiple times
* within a form to bind to multiple command objects and/or field names.
*
* This macro will participate in the default HTML escape setting for the given
* RequestContext. This can be customized by calling "setDefaultHtmlEscape"
* on the "springMacroRequestContext" context variable, or via the
* "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
* Also regards a "htmlEscape" variable in the namespace of this library.
*
* Producing no output, the following context variable will be available
* each time this macro is referenced (assuming you import this library in
* your templates with the namespace 'spring'):
*
* spring.status : a BindStatus instance holding the command object name,
* expression, value, and error messages and codes for the path supplied
*
* @param path : the path (string value) of the value required to bind to.
* Spring defaults to a command name of "command" but this can be overridden
* by user config.
-->
<#macro bind path>
<#if htmlEscape?exists>
<#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
<#else>
<#assign status = springMacroRequestContext.getBindStatus(path)>
</#if>
<#-- assign a temporary value, forcing a string representation for any
kind of variable. This temp value is only used in this macro lib -->
<#if status.value?exists && status.value?is_boolean>
<#assign stringStatusValue=status.value?string>
<#else>
<#assign stringStatusValue=status.value?default("")>
</#if>
</#macro>
<#--
* bindEscaped
*
* Similar to spring:bind, but takes an explicit HTML escape flag rather
* than relying on the default HTML escape setting.
-->
<#macro bindEscaped path, htmlEscape>
<#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
<#-- assign a temporary value, forcing a string representation for any
kind of variable. This temp value is only used in this macro lib -->
<#if status.value?exists && status.value?is_boolean>
<#assign stringStatusValue=status.value?string>
<#else>
<#assign stringStatusValue=status.value?default("")>
</#if>
</#macro>
<#--
* formInput
*
* Display a form input field of type 'text' and bind it to an attribute
* of a command or bean.
*
* @param path the name of the field to bind to
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formInput path attributes="" fieldType="text">
<@bind path/>
<input type="${fieldType}" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" value="<#if fieldType!="password">${stringStatusValue}</#if>" ${attributes}<@closeTag/>
</#macro>
<#--
* formPasswordInput
*
* Display a form input field of type 'password' and bind it to an attribute
* of a command or bean. No value will ever be displayed. This functionality
* can also be obtained by calling the formInput macro with a 'type' parameter
* of 'password'.
*
* @param path the name of the field to bind to
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formPasswordInput path attributes="">
<@formInput path, attributes, "password"/>
</#macro>
<#--
* formHiddenInput
*
* Generate a form input field of type 'hidden' and bind it to an attribute
* of a command or bean. This functionality can also be obtained by calling
* the formInput macro with a 'type' parameter of 'hidden'.
*
* @param path the name of the field to bind to
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formHiddenInput path attributes="">
<@formInput path, attributes, "hidden"/>
</#macro>
<#--
* formTextarea
*
* Display a text area and bind it to an attribute of a command or bean.
*
* @param path the name of the field to bind to
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formTextarea path attributes="">
<@bind path/>
<textarea id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
${stringStatusValue}</textarea>
</#macro>
<#--
* formSingleSelect
*
* Show a selectbox (dropdown) input element allowing a single value to be chosen
* from a list of options.
*
* @param path the name of the field to bind to
* @param options a map (value=label) of all the available options
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formSingleSelect path options attributes="">
<@bind path/>
<select id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
<#if options?is_hash>
<#list options?keys as value>
<option value="${value?html}"<@checkSelected value/>>${options[value]?html}</option>
</#list>
<#else>
<#list options as value>
<option value="${value?html}"<@checkSelected value/>>${value?html}</option>
</#list>
</#if>
</select>
</#macro>
<#--
* formMultiSelect
*
* Show a listbox of options allowing the user to make 0 or more choices from
* the list of options.
*
* @param path the name of the field to bind to
* @param options a map (value=label) of all the available options
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formMultiSelect path options attributes="">
<@bind path/>
<select multiple="multiple" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
<#list options?keys as value>
<#assign isSelected = contains(status.actualValue?default([""]), value)>
<option value="${value?html}"<#if isSelected> selected="selected"</#if>>${options[value]?html}</option>
</#list>
</select>
</#macro>
<#--
* formRadioButtons
*
* Show radio buttons.
*
* @param path the name of the field to bind to
* @param options a map (value=label) of all the available options
* @param separator the html tag or other character list that should be used to
* separate each option. Typically '&nbsp;' or '<br>'
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formRadioButtons path options separator attributes="">
<@bind path/>
<#list options?keys as value>
<#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
<input type="radio" id="${id}" name="${status.expression}" value="${value?html}"<#if stringStatusValue == value> checked="checked"</#if> ${attributes}<@closeTag/>
<label for="${id}">${options[value]?html}</label>${separator}
</#list>
</#macro>
<#--
* formCheckboxes
*
* Show checkboxes.
*
* @param path the name of the field to bind to
* @param options a map (value=label) of all the available options
* @param separator the html tag or other character list that should be used to
* separate each option. Typically '&nbsp;' or '<br>'
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formCheckboxes path options separator attributes="">
<@bind path/>
<#list options?keys as value>
<#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
<#assign isSelected = contains(status.actualValue?default([""]), value)>
<input type="checkbox" id="${id}" name="${status.expression}" value="${value?html}"<#if isSelected> checked="checked"</#if> ${attributes}<@closeTag/>
<label for="${id}">${options[value]?html}</label>${separator}
</#list>
<input type="hidden" name="_${status.expression}" value="on"/>
</#macro>
<#--
* formCheckbox
*
* Show a single checkbox.
*
* @param path the name of the field to bind to
* @param attributes any additional attributes for the element (such as class
* or CSS styles or size
-->
<#macro formCheckbox path attributes="">
<@bind path />
<#assign id="${status.expression?replace('[','')?replace(']','')}">
<#assign isSelected = status.value?? && status.value?string=="true">
<input type="hidden" name="_${status.expression}" value="on"/>
<input type="checkbox" id="${id}" name="${status.expression}"<#if isSelected> checked="checked"</#if> ${attributes}/>
</#macro>
<#--
* showErrors
*
* Show validation errors for the currently bound field, with
* optional style attributes.
*
* @param separator the html tag or other character list that should be used to
* separate each option. Typically '<br>'.
* @param classOrStyle either the name of a CSS class element (which is defined in
* the template or an external CSS file) or an inline style. If the value passed in here
* contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute
* will be used.
-->
<#macro showErrors separator classOrStyle="">
<#list status.errorMessages as error>
<#if classOrStyle == "">
<b>${error}</b>
<#else>
<#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style"></#if>
<span ${attr}="${classOrStyle}">${error}</span>
</#if>
<#if error_has_next>${separator}</#if>
</#list>
</#macro>
<#--
* checkSelected
*
* Check a value in a list to see if it is the currently selected value.
* If so, add the 'selected="selected"' text to the output.
* Handles values of numeric and string types.
* This function is used internally but can be accessed by user code if required.
*
* @param value the current value in a list iteration
-->
<#macro checkSelected value>
<#if stringStatusValue?is_number && stringStatusValue == value?number>selected="selected"</#if>
<#if stringStatusValue?is_string && stringStatusValue == value>selected="selected"</#if>
</#macro>
<#--
* contains
*
* Macro to return true if the list contains the scalar, false if not.
* Surprisingly not a FreeMarker builtin.
* This function is used internally but can be accessed by user code if required.
*
* @param list the list to search for the item
* @param item the item to search for in the list
* @return true if item is found in the list, false otherwise
-->
<#function contains list item>
<#list list as nextInList>
<#if nextInList == item><#return true></#if>
</#list>
<#return false>
</#function>
<#--
* closeTag
*
* Simple macro to close an HTML tag that has no body with '>' or '/>',
* depending on the value of a 'xhtmlCompliant' variable in the namespace
* of this library.
-->
<#macro closeTag>
<#if xhtmlCompliant?exists && xhtmlCompliant>/><#else>></#if>
</#macro>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
require('../../js/transition.js');
require('../../js/alert.js');
require('../../js/button.js');
require('../../js/carousel.js');
require('../../js/collapse.js');
require('../../js/dropdown.js');
require('../../js/modal.js');
require('../../js/tooltip.js');
require('../../js/popover.js');
require('../../js/scrollspy.js');
require('../../js/tab.js');
require('../../js/affix.js');

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1516610432242" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="36739" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M160 12.8h665.6c19.2 0 38.4 19.2 38.4 38.4v921.6c0 19.2-19.2 38.4-38.4 38.4H160c-19.2 0-38.4-19.2-38.4-38.4V51.2c0-25.6 19.2-38.4 38.4-38.4zM377.6 448c-6.4 12.8-19.2 12.8-32 6.4-6.4-6.4-6.4-25.6 0-32 19.2-25.6 38.4-44.8 64-57.6 25.6-12.8 51.2-19.2 83.2-19.2s57.6 6.4 83.2 19.2c25.6 12.8 51.2 32 64 57.6 12.8 6.4 12.8 25.6 0 32-12.8 6.4-25.6 6.4-32-6.4-12.8-19.2-32-32-51.2-44.8-19.2-12.8-38.4-12.8-64-12.8s-44.8 6.4-64 12.8c-19.2 12.8-32 25.6-51.2 44.8z m115.2 0c25.6 0 44.8 6.4 57.6 25.6 12.8 12.8 25.6 38.4 25.6 57.6 0 25.6-6.4 44.8-25.6 57.6-12.8 19.2-32 25.6-57.6 25.6-19.2 0-44.8-6.4-57.6-19.2-19.2-19.2-25.6-38.4-25.6-64s6.4-44.8 25.6-57.6c12.8-19.2 38.4-25.6 57.6-25.6z m32 57.6c-6.4-6.4-19.2-12.8-32-12.8s-19.2 6.4-25.6 12.8c-6.4 6.4-12.8 12.8-12.8 25.6s6.4 19.2 12.8 25.6c6.4 6.4 19.2 12.8 25.6 12.8 12.8 0 19.2-6.4 25.6-12.8 6.4-6.4 12.8-19.2 12.8-25.6 0-12.8 0-19.2-6.4-25.6z m-217.6-128c-6.4 6.4-25.6 12.8-32 0-12.8-6.4-12.8-19.2 0-32 25.6-32 64-57.6 102.4-76.8 76.8-38.4 166.4-38.4 243.2 0 38.4 19.2 70.4 44.8 102.4 76.8 6.4 6.4 6.4 25.6 0 32-19.2 12.8-32 6.4-38.4 0-25.6-25.6-51.2-51.2-83.2-64-32-12.8-64-25.6-102.4-25.6-38.4 0-70.4 6.4-102.4 25.6-38.4 12.8-64 38.4-89.6 64z m-51.2-256c1587.2 0-544 0 0 0z m108.8 748.8c-12.8 0-25.6-12.8-25.6-25.6s12.8-25.6 25.6-25.6h256c12.8 0 25.6 12.8 25.6 25.6s-12.8 25.6-25.6 25.6h-256z m422.4-780.8H198.4v844.8h588.8V89.6z" fill="#707070" p-id="36740"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 511.998 511.998" style="enable-background:new 0 0 511.998 511.998;" xml:space="preserve">
<g>
<path style="fill:#9BF57D;" d="M405.976,166.184c-3.258-0.555-6.474-0.644-9.639-0.467c-1.216,2.928-2.196,5.992-2.752,9.25
c-3.985,23.389,11.745,45.581,35.135,49.566c3.258,0.555,6.474,0.644,9.639,0.467c1.216-2.928,2.196-5.992,2.752-9.25
C445.096,192.359,429.366,170.169,405.976,166.184z"/>
<path style="fill:#9BF57D;" d="M499.501,182.118c3.258,0.555,6.322,1.536,9.25,2.752c0.178,3.166,0.088,6.381-0.467,9.639
c-3.985,23.389-26.176,39.12-49.566,35.135c-3.258-0.555-6.322-1.536-9.25-2.752c-0.178-3.166-0.088-6.381,0.467-9.639
C453.919,193.863,476.111,178.133,499.501,182.118z"/>
<path style="fill:#9BF57D;" d="M433.953,95.384c-2.479-0.827-4.981-1.283-7.476-1.526c-1.302,2.141-2.438,4.417-3.265,6.896
c-5.932,17.794,3.684,37.027,21.478,42.958c2.479,0.827,4.981,1.283,7.476,1.526c1.302-2.141,2.438-4.417,3.265-6.896
C461.364,120.549,451.747,101.316,433.953,95.384z"/>
<path style="fill:#9BF57D;" d="M505.102,119.101c2.479,0.827,4.755,1.962,6.896,3.265c-0.243,2.495-0.699,4.996-1.526,7.476
c-5.932,17.794-25.164,27.41-42.958,21.478c-2.479-0.827-4.755-1.962-6.896-3.265c0.243-2.495,0.699-4.996,1.526-7.476
C468.076,122.786,487.308,113.171,505.102,119.101z"/>
</g>
<path style="fill:#0096DC;" d="M396.392,258.169l0.062,38.105l38.306-12.963c4.92-1.665,5.052-8.575,0.199-10.427L396.392,258.169z"
/>
<path style="fill:#00C3FF;" d="M283.023,260.473c-23.844-61.418-74.093-137.198-169.947-168.553
c-5.247-1.716-10.733,1.759-11.692,7.195c-3.26,18.481-6.253,59.555,19.791,99.452l139.474,90.993L283.023,260.473z"/>
<path style="fill:#96E6FF;" d="M340.049,215.751c-19.154,0.569-36.128,9.747-47.223,23.792
c-9.504,12.03-20.355,22.916-33.075,31.046c-49.819,31.838-162.75,45.742-162.75,45.742c-26.048,3.453-54.285,5.819-84.328,6.553
c-9.327,0.228-15.347,9.996-11.488,18.49l30.47,67.032c3.192,7.024,11.725,9.791,18.414,5.95
c12.231-7.026,32.602-18.527,57.087-31.575c24.324,34.277,64.259,56.698,109.488,56.698c58.258,0,107.726-37.158,126.289-89.031
c2.057-5.748,7.147-9.721,13.097-11.087c28.525-6.548,49.625-32.518,48.525-63.28C403.345,242.237,373.896,214.744,340.049,215.751z
"/>
<path style="fill:#00D2FF;" d="M297.195,323.124c0,0-86.559-155.806-259.677-143.946c-4.858,0.332-8.194,5.039-7.081,9.779
c4.725,20.108,20.887,68.431,69.854,89.416l-18.651,12.434c-4.345,2.896-4.719,9.091-0.808,12.553
C104.834,324.596,185.537,382.675,297.195,323.124z"/>
<path style="fill:#0096DC;" d="M359.846,278.374c-4.947,0-8.95-4.007-8.95-8.95v-8.95c0-4.943,4.003-8.95,8.95-8.95
c4.947,0,8.95,4.007,8.95,8.95v8.95C368.796,274.366,364.793,278.374,359.846,278.374z"/>
<path style="fill:#7DC86E;" d="M440.397,314.174c-4.947,0-8.95-4.007-8.95-8.95c0-112.384,19.815-185.519,36.438-227.08
c1.836-4.589,7.071-6.809,11.633-4.986c4.589,1.84,6.827,7.045,4.991,11.637c-16.039,40.097-35.163,110.893-35.163,220.428
C449.347,310.167,445.344,314.174,440.397,314.174z"/>
<path style="fill:#00D2FF;" d="M13.047,367.47l18.607,40.935c3.192,7.024,11.724,9.791,18.414,5.948
c12.231-7.025,32.602-18.527,57.087-31.575l-10.313-15.157c-6.05-8.893-16.824-13.317-27.379-11.243L13.047,367.47z"/>
<path style="fill:#00C3FF;" d="M219.524,294.776c-32.788,1.918-90.925-2.55-119.233-16.402l-18.651,12.434
c-4.296,2.864-4.789,9.022-0.926,12.447c11.369,10.079,35.692,28.595,70.124,38.778c33.677-9.059,58.442-25.305,72.769-36.747
C228.116,301.685,225.284,294.439,219.524,294.776z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,211 @@
var CIM_URI="ws://127.0.0.1:23456";// 修改为服务器的真实IP
var CMD_HEARTBEAT_RESPONSE = new Uint8Array([67,82]);
var SDK_VERSION = "1.0.0";
var SDK_CHANNEL = "browser";
var APP_PACKAGE = "com.farsunset.cim";
const ACTION_999 = "999";//特殊的消息类型,代表被服务端强制下线
var DATA_HEADER_LENGTH = 3;
var C_H_RS = 0;
var S_H_RQ = 1;
var MESSAGE = 2;
var SENTBODY = 3;
var REPLYBODY = 4;
var socket;
var manualStop = false;
var CIMWebBridge = new Object();
CIMWebBridge.connection = function(){
manualStop = false;
window.localStorage.account = '';
socket = new WebSocket(CIM_URI);
socket.binaryType = 'arraybuffer';
socket.onopen = CIMWebBridge.innerOnConnectionSuccessed;
socket.onmessage = CIMWebBridge.innerOnMessageReceived;
socket.onclose = CIMWebBridge.innerOnConnectionClosed;
};
CIMWebBridge.bindAccount = function(account){
window.localStorage.account = account;
var deviceId = window.localStorage.deviceIddeviceId;
if(deviceId == '' || deviceId == undefined){
deviceId = generateUUID();
window.localStorage.deviceId = deviceId;
}
var browser = getBrowser();
var body = new proto.com.farsunset.cim.sdk.web.model.SentBody();
body.setKey("client_bind");
body.getDataMap().set("account",account);
body.getDataMap().set("channel",SDK_CHANNEL);
body.getDataMap().set("version",SDK_VERSION);
body.getDataMap().set("osVersion", browser.version);
body.getDataMap().set("packageName", APP_PACKAGE);
body.getDataMap().set("deviceId", deviceId);
body.getDataMap().set("device", browser.name);
CIMWebBridge.sendRequest(body);
};
CIMWebBridge.stop = function(){
manualStop = true;
socket.close();
};
CIMWebBridge.resume = function(){
manualStop = false;
CIMWebBridge.connection();
};
CIMWebBridge.innerOnConnectionSuccessed = function(){
var account = window.localStorage.account;
if(account == '' || account == undefined){
onConnectionSuccessed();
}else{
CIMWebBridge.bindAccount(account);
}
};
CIMWebBridge.innerOnMessageReceived = function(e){
var data = new Uint8Array(e.data);
var type = data[0];
/**
* 收到服务端发来的心跳请求立即回复响应否则服务端会在10秒后断开连接
*/
if(type == S_H_RQ){
CIMWebBridge.sendHeartbeatResponse();
return;
}
if(type == MESSAGE){
var length = getContentLength(data[1],data[2]);
var data = proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinary(data.subarray(DATA_HEADER_LENGTH,DATA_HEADER_LENGTH+length));
onInterceptMessageReceived(data.toObject(false));
return;
}
if(type == REPLYBODY){
var length = getContentLength(data[1],data[2]);
var data = proto.com.farsunset.cim.sdk.web.model.ReplyBody.deserializeBinary(data.subarray(DATA_HEADER_LENGTH,DATA_HEADER_LENGTH+length));
/**
* 将proto对象转换成json对象去除无用信息
*/
var reply = {};
reply.code = data.getCode();
reply.key = data.getKey();
reply.message = data.getMessage();
reply.timestamp = data.getTimestamp();
reply.data = {};
/**
* 注意遍历map这里的参数 value在前key在后
*/
data.getDataMap().forEach(function (v,k){
reply.data[k] = v;
});
onReplyReceived(reply);
}
};
CIMWebBridge.innerOnConnectionClosed = function(e){
if(!manualStop){
var time = Math.floor(Math.random()*(30-15+1)+15);
setTimeout(function(){
CIMWebBridge.connection();
},time);
}
};
CIMWebBridge.sendRequest = function(body){
var data = body.serializeBinary();
var header = buildHeader(SENTBODY,data.length);
var protubuf = new Uint8Array(data.length + header.length);
protubuf.set(header,0);
protubuf.set(data,header.length);
socket.send(protubuf);
};
CIMWebBridge.sendHeartbeatResponse = function(){
var data = CMD_HEARTBEAT_RESPONSE;
var header = buildHeader(C_H_RS,data.length);
var protubuf = new Uint8Array(data.length + header.length);
protubuf.set(header,0);
protubuf.set(data,header.length);
socket.send(protubuf);
};
function getContentLength( lv, hv) {
var l = (lv & 0xff);
var h = (hv & 0xff);
return (l | (h <<= 8));
}
function buildHeader(type,length){
var header = new Uint8Array(DATA_HEADER_LENGTH);
header[0] = type;
header[1] = (length & 0xff);
header[2] = ((length >> 8) & 0xff);
return header;
}
function onInterceptMessageReceived(message){
//被强制下线之后,不再继续连接服务端
if(message.action == ACTION_999){
manualStop = true;
}
//收到消息后,将消息发送给页面
if(onMessageReceived instanceof Function){
onMessageReceived(message);
}
}
function getBrowser() {
var explorer = window.navigator.userAgent.toLowerCase() ;
// ie
if (explorer.indexOf("msie") >= 0) {
var ver=explorer.match(/msie ([\d.]+)/)[1];
return {name:"IE",version:ver};
}
// firefox
else if (explorer.indexOf("firefox") >= 0) {
var ver=explorer.match(/firefox\/([\d.]+)/)[1];
return {name:"Firefox",version:ver};
}
// Chrome
else if(explorer.indexOf("chrome") >= 0){
var ver=explorer.match(/chrome\/([\d.]+)/)[1];
return {name:"Chrome",version:ver};
}
// Opera
else if(explorer.indexOf("opera") >= 0){
var ver=explorer.match(/opera.([\d.]+)/)[1];
return {name:"Opera",version:ver};
}
// Safari
else if(explorer.indexOf("Safari") >= 0){
var ver=explorer.match(/version\/([\d.]+)/)[1];
return {name:"Safari",version:ver};
}
return {name:"Other",version:"1.0.0"};
}
function generateUUID() {
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x3|0x8)).toString(16);
});
return uuid.replace(/-/g,'');
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>

View File

@ -5,7 +5,7 @@
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="D:\devtools\dev\gradle-3.4" />
<option name="gradleHome" value="C:/Program Files/gradle-4.5" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -1,18 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<list size="5">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
@ -27,17 +25,7 @@
</value>
</option>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -24,7 +24,7 @@ package com.farsunset.ichat.example.app;
public interface Constant {
//服务端IP地址
public static final String CIM_SERVER_HOST = "172.168.11.18";
public static final String CIM_SERVER_HOST = "192.168.1.103";
//注意这里的端口不是tomcat的端口CIM端口在服务端spring-cim.xml中配置的没改动就使用默认的23456

Binary file not shown.

View File

@ -1,26 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="lib" path="libs/log4j-1.2.17.jar"/>
<classpathentry kind="lib" path="libs/protobuf-java-3.2.0.jar"/>
<classpathentry kind="lib" path="libs/netty-buffer-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-common-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-handler-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-transport-4.1.9.Final.jar"/>
<classpathentry kind="lib" path="libs/fastjson-1.2.39.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-http-4.1.16.Final-sources.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-http-4.1.16.Final.jar" sourcepath="libs/netty-codec-http-4.1.16.Final-sources.jar"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="con" path="org.eclipse.jst.server.core.container/org.eclipse.jst.server.tomcat.runtimeTarget/Apache Tomcat v7.0">
<attributes>
<attribute name="owner.project.facets" value="jst.utility"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.8.0_131">
<classpathentry kind="lib" path="libs/slf4j-api-1.7.25.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes>
<attribute name="owner.project.facets" value="java"/>
</attributes>
</classpathentry>
<classpathentry kind="lib" path="libs/netty-buffer-4.1.28.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-4.1.28.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-codec-http-4.1.28.Final.jar" sourcepath="/cim-boot-server"/>
<classpathentry kind="lib" path="libs/netty-common-4.1.28.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-handler-4.1.28.Final.jar"/>
<classpathentry kind="lib" path="libs/netty-transport-4.1.28.Final.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -22,8 +22,6 @@
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="cim-server-sdk">
<wb-resource deploy-path="/" source-path="/src"/>
</wb-module>
</project-modules>

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<runtime name="Apache Tomcat v7.0"/>
<fixed facet="java"/>
<fixed facet="jst.utility"/>
<installed facet="java" version="1.6"/>
<installed facet="jst.utility" version="1.0"/>
</faceted-project>

Binary file not shown.

View File

@ -22,13 +22,20 @@
package com.farsunset.cim.sdk.server.filter;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.filter.decoder.AppMessageDecoder;
import com.farsunset.cim.sdk.server.filter.decoder.WebMessageDecoder;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.AttributeKey;
/**
* 服务端接收消息路由解码通过消息类型分发到不同的真正解码器
@ -38,6 +45,11 @@ public class ServerMessageDecoder extends ByteToMessageDecoder {
private WebMessageDecoder webMessageDecoder;
private AppMessageDecoder appMessageDecoder;
public static final Pattern SEC_KEY_PATTERN = Pattern.compile("^(Sec-WebSocket-Key:).+",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
public static final Pattern UPGRADE_PATTERN = Pattern.compile("^(Upgrade:).+",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
public ServerMessageDecoder() {
webMessageDecoder = new WebMessageDecoder();
appMessageDecoder = new AppMessageDecoder();
@ -46,19 +58,81 @@ public class ServerMessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext arg0, ByteBuf buffer, List<Object> queue) throws Exception {
buffer.markReaderIndex();
byte conetnType = buffer.readByte();
buffer.resetReaderIndex();
/**
* 原生SDK只会发送2种类型消息 1个心跳类型 另一个是sendbody报文的第一个字节为消息类型,否则才是websocket的消息
*/
if (conetnType == CIMConstant.ProtobufType.C_H_RS || conetnType == CIMConstant.ProtobufType.SENTBODY) {
appMessageDecoder.decode(arg0, buffer, queue);
} else {
Object protocol = arg0.channel().attr(AttributeKey.valueOf(CIMSession.PROTOCOL)).get();
if (Objects.equals(CIMSession.WEBSOCKET, protocol)) {
webMessageDecoder.decode(arg0, buffer, queue);
return;
}
if (Objects.equals(CIMSession.NATIVEAPP, protocol)) {
appMessageDecoder.decode(arg0, buffer, queue);
return;
}
boolean handshake = tryWebsocketHandleHandshake(arg0, buffer, queue);
if (!handshake) {
appMessageDecoder.decode(arg0, buffer, queue);
}
}
private boolean tryWebsocketHandleHandshake(ChannelHandlerContext arg0, ByteBuf iobuffer, List<Object> queue) {
iobuffer.markReaderIndex();
byte[] data = new byte[iobuffer.readableBytes()];
iobuffer.readBytes(data);
String request = new String(data);
String secKey = getSecWebSocketKey(request);
boolean handShake = secKey != null && Objects.equals(getUpgradeProtocol(request), CIMSession.WEBSOCKET);
if (handShake) {
/**
* 重要握手响应之后删除标志HANDSHAKE_FRAME,并标记当前session的协议为websocket
*/
arg0.channel().attr(AttributeKey.valueOf(CIMSession.PROTOCOL)).set(CIMSession.WEBSOCKET);
SentBody body = new SentBody();
body.setKey(CIMNioSocketAcceptor.WEBSOCKET_HANDLER_KEY);
body.setTimestamp(System.currentTimeMillis());
body.put("key", secKey);
queue.add(body);
} else {
iobuffer.resetReaderIndex();
}
return handShake;
}
/**
* 通过正则获取websocket握手消息中的Sec-WebSocket-Key
*
* @param message
* @return
*/
private String getSecWebSocketKey(String message) {
Matcher m = SEC_KEY_PATTERN.matcher(message);
if (m.find()) {
return m.group().split(":")[1].trim();
}
return null;
}
/**
* 通过正则获取websocket握手消息中的 Upgrade 预期为websocket
*
* @param message
* @return
*/
private String getUpgradeProtocol(String message) {
Matcher m = UPGRADE_PATTERN.matcher(message);
if (m.find()) {
return m.group().split(":")[1].trim();
}
return null;
}
}

View File

@ -21,10 +21,10 @@
*/
package com.farsunset.cim.sdk.server.filter;
import org.apache.log4j.Logger;
import java.util.Objects;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.model.HandshakerResponse;
import com.farsunset.cim.sdk.server.model.feature.EncodeFormatable;
import com.farsunset.cim.sdk.server.session.CIMSession;
@ -38,62 +38,51 @@ import io.netty.util.AttributeKey;
*/
public class ServerMessageEncoder extends MessageToByteEncoder<Object> {
protected final Logger logger = Logger.getLogger(ServerMessageEncoder.class.getSimpleName());
@Override
protected void encode(ChannelHandlerContext ctx, Object object, ByteBuf out) throws Exception {
Object channel = ctx.channel().attr(AttributeKey.valueOf("channel")).get();
protected void encode(final ChannelHandlerContext ctx, final Object object, ByteBuf out) throws Exception {
Object protocol = ctx.channel().attr(AttributeKey.valueOf(CIMSession.PROTOCOL)).get();
/**
* websocket的握手响应
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof WebsocketResponse) {
WebsocketResponse data = (WebsocketResponse) object;
byte[] byteArray = data.getBytes();
out.writeBytes(byteArray);
logger.info(data.toString());
if (Objects.equals(CIMSession.WEBSOCKET, protocol) && object instanceof HandshakerResponse) {
out.writeBytes(object.toString().getBytes());
return;
}
/**
* websocket的数据传输使用JSON编码数据格式因为Protobuf还没有支持js
* websocket的业务数据
*/
if (CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
if (Objects.equals(CIMSession.WEBSOCKET, protocol) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = encodeDataFrame(data.getJSONBody());
/**
* 由于websocket没有黏包和断包的问题所以不必知道消息体的大小
*/
out.writeBytes(byteArray);
logger.info(data.toString());
byte[] body = data.getProtobufBody();
byte[] header = createHeader(data.getDataType(), body.length);
byte[] protobuf = new byte[body.length + CIMConstant.DATA_HEADER_LENGTH];
System.arraycopy(header, 0, protobuf, 0, header.length);
System.arraycopy(body,0, protobuf, header.length, body.length);
byte[] binaryFrame = encodeDataFrame(protobuf);
out.writeBytes(binaryFrame);
return;
}
/**
* 非websocket的数据传输使用Protobuf编码数据格式
*/
if (!CIMSession.CHANNEL_BROWSER.equals(channel) && object instanceof EncodeFormatable) {
if (Objects.equals(CIMSession.NATIVEAPP, protocol) && object instanceof EncodeFormatable) {
EncodeFormatable data = (EncodeFormatable) object;
byte[] byteArray = data.getProtobufBody();
out.writeBytes(createHeader(data.getDataType(), byteArray.length));
out.writeBytes(byteArray);
logger.info(data.toString());
}
}
byte[] body = data.getProtobufBody();
byte[] header = createHeader(data.getDataType(), body.length);
out.writeBytes(header);
out.writeBytes(body);
}
/**
* 消息体最大为65535
*
* @param type
* @param length
* @return
*/
private byte[] createHeader(byte type, int length) {
byte[] header = new byte[CIMConstant.DATA_HEADER_LENGTH];
header[0] = type;
header[1] = (byte) (length & 0xff);
header[2] = (byte) ((length >> 8) & 0xff);
return header;
}
/**
@ -120,7 +109,7 @@ public class ServerMessageEncoder extends MessageToByteEncoder<Object> {
// 开始计算ws-frame
// frame-fin + frame-rsv1 + frame-rsv2 + frame-rsv3 + frame-opcode
result[0] = (byte) 0x81; // 129
result[0] = (byte) 0x82; // 0x82 二进制帧 0x80 文本帧
// frame-masked+frame-payload-length
// 从第9个字节开始是 1111101=125,掩码是第3-第6个数据
@ -143,4 +132,11 @@ public class ServerMessageEncoder extends MessageToByteEncoder<Object> {
return result;
}
private byte[] createHeader(byte type, int length) {
byte[] header = new byte[CIMConstant.DATA_HEADER_LENGTH];
header[0] = type;
header[1] = (byte) (length & 0xff);
header[2] = (byte) ((length >> 8) & 0xff);
return header;
}
}

View File

@ -23,26 +23,24 @@ package com.farsunset.cim.sdk.server.filter.decoder;
import java.util.List;
import org.apache.log4j.Logger;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
import com.farsunset.cim.sdk.server.session.CIMSession;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.AttributeKey;
/**
* 服务端接收来自应用的消息解码
*/
public class AppMessageDecoder extends ByteToMessageDecoder {
protected final Logger logger = Logger.getLogger(AppMessageDecoder.class);
@Override
public void decode(ChannelHandlerContext arg0, ByteBuf buffer, List<Object> queue) throws Exception {
/**
* 消息头3位
*/
@ -60,25 +58,24 @@ public class AppMessageDecoder extends ByteToMessageDecoder {
int conetnLength = getContentLength(lv, hv);
// 如果消息体没有接收完整则重置读取等待下一次重新读取
if (conetnLength > buffer.readableBytes()) {
buffer.resetReaderIndex();
return;
if (conetnLength <= buffer.readableBytes()) {
byte[] dataBytes = new byte[conetnLength];
buffer.readBytes(dataBytes);
Object message = mappingMessageObject(dataBytes, conetnType);
if (message != null) {
arg0.channel().attr(AttributeKey.valueOf(CIMSession.PROTOCOL)).set(CIMSession.NATIVEAPP);
queue.add(message);
return;
}
}
byte[] dataBytes = new byte[conetnLength];
buffer.readBytes(dataBytes);
Object message = mappingMessageObject(dataBytes, conetnType);
if (message != null) {
queue.add(message);
}
buffer.resetReaderIndex();
}
public Object mappingMessageObject(byte[] data, byte type) throws Exception {
if (CIMConstant.ProtobufType.C_H_RS == type) {
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
SentBody body = new SentBody();
body.setKey(CIMConstant.CLIENT_HEARTBEAT);
body.setTimestamp(System.currentTimeMillis());
@ -91,7 +88,6 @@ public class AppMessageDecoder extends ByteToMessageDecoder {
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
logger.info(body.toString());
return body;
}

View File

@ -21,161 +21,139 @@
*/
package com.farsunset.cim.sdk.server.filter.decoder;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import com.alibaba.fastjson.JSON;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.model.HeartbeatResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.proto.SentBodyProto;
import com.google.protobuf.InvalidProtocolBufferException;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 服务端接收来自websocket消息解码
* 服务端接收来自websocket消息解码,netty自带的websocket协议解码器和普通二进制流解码器无法共存所以只能重新写一遍websocket消息编解码咯
*/
public class WebMessageDecoder extends ByteToMessageDecoder {
public static final byte MASK = 0x1;// 1000 0000
public static final byte HAS_EXTEND_DATA = 126;
public static final byte HAS_EXTEND_DATA_CONTINUE = 127;
public static final byte PAYLOADLEN = 0x7F;// 0111 1111
public static final Pattern SEC_KEY_PATTERN = Pattern.compile("^(Sec-WebSocket-Key:).+",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
protected final Logger logger = Logger.getLogger(WebMessageDecoder.class);
public static final byte TAG_MASK = 0x0F;// 0000 1111 > 15
private static final byte OPCODE_BINARY = 0x2;
private static final byte OPCODE_CLOSE = 0x8;
@Override
public void decode(ChannelHandlerContext arg0, ByteBuf iobuffer, List<Object> queue) throws Exception {
/**
* 消息头2位
*/
if (iobuffer.readableBytes() < CIMConstant.WS_DATA_HEADER_LENGTH) {
return;
}
iobuffer.markReaderIndex();
byte head = iobuffer.readByte();// 第二个字节
byte datalength = (byte) (head & PAYLOADLEN);// 得到第二个字节后七位的值
int length = 0;
if (datalength < HAS_EXTEND_DATA) {// 第一种是消息内容少于126存储消息长度
length = datalength;
} else if (datalength == HAS_EXTEND_DATA) {// 第二种是消息长度大于等于126且少于UINT16的情况此值为126
if (iobuffer.readableBytes() < 2) {
iobuffer.resetReaderIndex();
return;
}
byte[] extended = new byte[2];
iobuffer.readBytes(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {// 第三种是消息长度大于UINT16的情况下此值为127
if (iobuffer.readableBytes() < 4) {
iobuffer.resetReaderIndex();
return;
}
byte[] extended = new byte[4];
iobuffer.readBytes(extended);
int shift = 0;
length = 0;
for (int i = extended.length - 1; i >= 0; i--) {
length = length + ((extended[i] & 0xFF) << shift);
shift += 8;
}
}
int ismask = head >> 7 & MASK;// 得到第二个字节第一位的值
if ((ismask == 1 && iobuffer.readableBytes() < 4 + length)
|| (ismask == 0 && iobuffer.readableBytes() < length)) {// 有掩码
/**
* 接下来判断fin标志位是否是1 如果是0 则等待消息接收完成
*/
byte tag = iobuffer.readByte();
int frameFin = tag > 0 ? 0 : 1; // 有符号byte 第一位为1则为负数 第一位为0则为正数以此 判断fin字段是 0 还是 1
if (frameFin == 0) {
iobuffer.resetReaderIndex();
return;
}
iobuffer.resetReaderIndex();
/**
* 获取帧类型因为使用了protobuf所以只支持二进制帧 OPCODE_BINARY以及客户端关闭连接帧通知 OPCODE_CLOSE
*/
int frameOqcode = tag & TAG_MASK;
decodeDataBody(iobuffer, queue);
}
if (OPCODE_BINARY == frameOqcode) {
public void decodeDataBody(ByteBuf iobuffer, List<Object> queue) {
iobuffer.readByte();
byte head = iobuffer.readByte();
byte datalength = (byte) (head & PAYLOADLEN);
if (datalength < HAS_EXTEND_DATA) {
} else if (datalength == HAS_EXTEND_DATA) {
iobuffer.readBytes(new byte[2]);
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {
iobuffer.readBytes(new byte[4]);
}
byte head = iobuffer.readByte();
byte datalength = (byte) (head & PAYLOADLEN);
int realLength = 0;
int ismask = head >> 7 & MASK;
byte[] data = null;
if (ismask == 1) {// 有掩码
// 获取掩码
byte[] mask = new byte[4];
iobuffer.readBytes(mask);
data = new byte[iobuffer.readableBytes()];
iobuffer.readBytes(data);
for (int i = 0; i < data.length; i++) {
// 数据进行异或运算
data[i] = (byte) (data[i] ^ mask[i % 4]);
/**
* Payload len7位或者7+16位或者7+64位表示数据帧中数据大小这里有好几种情况 如果值为0-125那么该值就是payload
* data的真实长度 如果值为126那么该7位后面紧跟着的2个字节就是payload data的真实长度
* 如果值为127那么该7位后面紧跟着的8个字节就是payload data的真实长度
*/
if (datalength == HAS_EXTEND_DATA) {
realLength = iobuffer.readShort();
} else if (datalength == HAS_EXTEND_DATA_CONTINUE) {
realLength = (int) iobuffer.readLong();
} else {
realLength = datalength;
}
handleSentBodyAndHeartPing(data, queue);
boolean masked = (head >> 7 & MASK) == 1;
if (masked) {// 有掩码
// 获取掩码
byte[] mask = new byte[4];
iobuffer.readBytes(mask);
byte[] data = new byte[realLength];
iobuffer.readBytes(data);
for (int i = 0; i < realLength; i++) {
// 数据进行异或运算
data[i] = (byte) (data[i] ^ mask[i % 4]);
}
handleMessage(data, queue);
}
} else if (OPCODE_CLOSE == frameOqcode) {
handleClose(arg0);
} else {
data = new byte[iobuffer.readableBytes()];
iobuffer.readBytes(data);
handleWebsocketHandshake(data, queue);
// 忽略其他类型的消息
iobuffer.readBytes(new byte[iobuffer.readableBytes()]);
}
}
private void handleWebsocketHandshake(byte[] data, List<Object> queue) {
String message = null;
try {
message = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
SentBody body = new SentBody();
body.setKey(CIMNioSocketAcceptor.WEBSOCKET_HANDLER_KEY);
Matcher m = SEC_KEY_PATTERN.matcher(message);
if (m.find()) {
String foundstring = m.group();
body.put("key", foundstring.split(":")[1].trim());
}
queue.add(body);
private void handleClose(ChannelHandlerContext arg0) {
arg0.channel().close();
}
public void handleSentBodyAndHeartPing(byte[] data, List<Object> queue) {
String message = null;
try {
message = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
public void handleMessage(byte[] data, List<Object> queue) throws InvalidProtocolBufferException {
byte type = data[0];
/**
* 只处理心跳响应以及sentbody消息
*/
if (HeartbeatResponse.CMD_HEARTBEAT_RESPONSE.equals(message)) {
if (type == CIMConstant.ProtobufType.C_H_RS) {
HeartbeatResponse response = HeartbeatResponse.getInstance();
logger.info(response.toString());
queue.add(response);
} else if (data.length > 2) {
SentBody body = JSON.parseObject(message, SentBody.class);
logger.info(body.toString());
}
if (type == CIMConstant.ProtobufType.SENTBODY) {
int length = getContentLength(data[1], data[2]);
byte[] protobuf = new byte[length];
System.arraycopy(data, CIMConstant.DATA_HEADER_LENGTH, protobuf, 0, length);
SentBodyProto.Model bodyProto = SentBodyProto.Model.parseFrom(protobuf);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
queue.add(body);
}
}
/**
* 解析消息体长度
*
* @param type
* @param length
* @return
*/
private int getContentLength(byte lv, byte hv) {
int l = (lv & 0xff);
int h = (hv & 0xff);
return (l | (h <<= 8));
}
}

View File

@ -24,25 +24,24 @@ package com.farsunset.cim.sdk.server.handler;
import java.io.IOException;
import java.util.HashMap;
import org.apache.log4j.Logger;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.filter.ServerMessageDecoder;
import com.farsunset.cim.sdk.server.filter.ServerMessageEncoder;
import com.farsunset.cim.sdk.server.model.HeartbeatRequest;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
@ -50,10 +49,19 @@ import io.netty.util.AttributeKey;
@Sharable
public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody> {
/**
* websocket特有的握手处理handler
*/
public final static String WEBSOCKET_HANDLER_KEY = "client_websocket_handshake";
private final static String CIMSESSION_CLOSED_HANDLER_KEY = "client_cimsession_closed";
private Logger logger = Logger.getLogger(CIMNioSocketAcceptor.class);
private HashMap<String, CIMRequestHandler> handlers = new HashMap<String, CIMRequestHandler>();
/**
* 连接关闭处理handler
*/
public final static String CIMSESSION_CLOSED_HANDLER_KEY = "client_closed";
private HashMap<String, CIMRequestHandler> innerHandlerMap = new HashMap<String, CIMRequestHandler>();
private CIMRequestHandler outerRequestHandler;
private int port;
// 连接空闲时间
@ -69,16 +77,20 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>
/**
* 预制websocket握手请求的处理
*/
handlers.put(WEBSOCKET_HANDLER_KEY, new WebsocketHandler());
innerHandlerMap.put(WEBSOCKET_HANDLER_KEY, new WebsocketHandler());
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup());
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ServerMessageDecoder());
ch.pipeline().addLast(new ServerMessageEncoder());
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new IdleStateHandler(READ_IDLE_TIME, WRITE_IDLE_TIME, 0));
ch.pipeline().addLast(CIMNioSocketAcceptor.this);
}
@ -87,46 +99,43 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>
bootstrap.bind(port);
}
public void channelRegistered(ChannelHandlerContext ctx) {
logger.info("sessionCreated()... from " + ctx.channel().remoteAddress() + " nid:"
+ ctx.channel().id().asShortText());
/**
* 设置应用层的sentbody处理handler
*
* @param outerRequestHandler
*/
public void setAppSentBodyHandler(CIMRequestHandler outerRequestHandler) {
this.outerRequestHandler = outerRequestHandler;
}
protected void channelRead0(ChannelHandlerContext ctx, SentBody body) throws Exception {
CIMSession cimSession = new CIMSession(ctx.channel());
CIMSession session = new CIMSession(ctx.channel());
CIMRequestHandler handler = handlers.get(body.getKey());
if (handler == null) {
ReplyBody reply = new ReplyBody();
reply.setKey(body.getKey());
reply.setCode(CIMConstant.ReturnCode.CODE_404);
reply.setMessage("KEY:" + body.getKey() + " not defined on server");
cimSession.write(reply);
} else {
ReplyBody reply = handler.process(cimSession, body);
if (reply != null) {
reply.setKey(body.getKey());
cimSession.write(reply);
}
CIMRequestHandler handler = innerHandlerMap.get(body.getKey());
/**
* 如果有内置的特殊handler需要处理则使用内置的
*/
if (handler != null) {
handler.process(session, body);
return;
}
/**
* 有业务层去处理其他的sentbody
*/
outerRequestHandler.process(session, body);
}
/**
*/
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
CIMSession cimSession = new CIMSession(ctx.channel());
CIMSession session = new CIMSession(ctx.channel());
SentBody body = new SentBody();
body.setKey(CIMSESSION_CLOSED_HANDLER_KEY);
outerRequestHandler.process(session, body);
logger.warn("sessionClosed()... from " + ctx.channel().remoteAddress() + " nid:" + cimSession.getNid()
+ ",isConnected:" + ctx.channel().isActive());
CIMRequestHandler handler = handlers.get(CIMSESSION_CLOSED_HANDLER_KEY);
if (handler != null) {
handler.process(cimSession, null);
}
}
/**
@ -135,16 +144,11 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>
if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state().equals(IdleState.WRITER_IDLE)) {
ctx.channel().attr(AttributeKey.valueOf(CIMConstant.HEARTBEAT_KEY)).set(System.currentTimeMillis());
ctx.channel().writeAndFlush(HeartbeatRequest.getInstance());
logger.debug(IdleState.WRITER_IDLE + "... from " + ctx.channel().remoteAddress() + " nid:"
+ ctx.channel().id().asShortText());
}
// 如果心跳请求发出30秒内没收到响应则关闭连接
if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state().equals(IdleState.READER_IDLE)) {
logger.debug(IdleState.READER_IDLE + "... from " + ctx.channel().remoteAddress() + " nid:"
+ ctx.channel().id().asShortText());
Long lastTime = (Long) ctx.channel().attr(AttributeKey.valueOf(CIMConstant.HEARTBEAT_KEY)).get();
if (lastTime != null && System.currentTimeMillis() - lastTime >= PING_TIME_OUT) {
ctx.channel().close();
@ -154,21 +158,8 @@ public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody>
}
}
/**
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("exceptionCaught()... from " + ctx.channel().remoteAddress() + " isConnected:"
+ ctx.channel().isActive() + " nid:" + ctx.channel().id().asShortText(), cause);
ctx.channel().close();
}
public void setPort(int port) {
this.port = port;
}
public void setHandlers(HashMap<String, CIMRequestHandler> handlers) {
this.handlers = handlers;
}
}

View File

@ -25,11 +25,10 @@ package com.farsunset.cim.sdk.server.handler;
* 请求处理接口,所有的请求实现必须实现此接口
* @author 3979434@qq.com
*/
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.session.CIMSession;
public interface CIMRequestHandler {
ReplyBody process(CIMSession session, SentBody message);
void process(CIMSession session, SentBody message);
}

View File

@ -24,9 +24,8 @@ package com.farsunset.cim.sdk.server.handler;
import java.security.MessageDigest;
import java.util.Base64;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.HandshakerResponse;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.sdk.server.model.WebsocketResponse;
import com.farsunset.cim.sdk.server.session.CIMSession;
/**
@ -39,7 +38,7 @@ public class WebsocketHandler implements CIMRequestHandler {
private final static String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public ReplyBody process(CIMSession session, SentBody body) {
public void process(CIMSession session, SentBody body) {
session.setChannel(CIMSession.CHANNEL_BROWSER);
String secKey = body.get("key") + GUID;
try {
@ -50,7 +49,6 @@ public class WebsocketHandler implements CIMRequestHandler {
} catch (Exception e) {
e.printStackTrace();
}
session.write(new WebsocketResponse(secKey));
return null;
session.write(new HandshakerResponse(secKey));
}
}

Some files were not shown because too many files have changed in this diff Show More