├── .gitignore ├── README.md ├── build.gradle ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── assemblies └── plugin.xml ├── bin ├── consumer.sh ├── rpcServer.sh └── tcpServer.sh ├── config ├── im.properties ├── logback.xml └── spring │ ├── router.xml │ ├── rpc.xml │ ├── spring.xml │ └── tcp.xml ├── docs ├── IM-architecture.jpg ├── Im-Communication.jpg ├── Im-PullMessage.jpg ├── Im-SyncUnread.jpg └── mysql.sql └── java └── com └── linbox └── im ├── exceptions ├── ConfigFailException.java ├── IMConsumerException.java ├── IMException.java └── ServiceNotFoundException.java ├── interfaces └── IIMService.java ├── message ├── AuthRequest.java ├── AuthResponse.java ├── ByteCreator.java ├── Message.java ├── MessageType.java ├── MessageWrapper.java ├── NewMessage.java ├── OfflineInfo.java ├── Ping.java ├── Pong.java ├── PullOldMsgRequest.java ├── PullOldMsgRequestType.java ├── PullOldMsgResponse.java ├── ReadAckRequest.java ├── ReadAckResponse.java ├── RequestResponseType.java ├── SendMsgRequest.java ├── SendMsgResponse.java ├── SyncUnreadRequest.java ├── SyncUnreadResponse.java ├── UnreadMsg.java └── system │ ├── JobNotificationMessage.java │ ├── SyncSystemUnreadRequest.java │ ├── SyncSystemUnreadResponse.java │ ├── SystemMessage.java │ ├── SystemMessageTypes.java │ ├── SystemUnreadAckRequest.java │ ├── SystemUnreadAckResponse.java │ └── content │ └── SystemUnreadContent.java ├── server ├── connector │ ├── rpc │ │ └── IMService.java │ └── tcp │ │ ├── ImTcpServer.java │ │ ├── constant │ │ └── HandlerName.java │ │ └── handler │ │ ├── AES.java │ │ ├── AuthHandler.java │ │ ├── IMChannelInitializer.java │ │ ├── IMIdleStateHandler.java │ │ ├── IMMessageDecoder.java │ │ ├── IMMessageEncoder.java │ │ └── IMMessageHandler.java ├── constant │ ├── MessageTopic.java │ ├── RedisKey.java │ └── SpecialUsers.java ├── monitor │ ├── IConnectorMonitor.java │ └── impl │ │ └── ConnectorMonitor.java ├── router │ ├── ImRouterServer.java │ └── handlers │ │ ├── Handler.java │ │ ├── PingHandler.java │ │ ├── PullOldMsgHandler.java │ │ ├── ReadAckHandler.java │ │ ├── SendMsgHandler.java │ │ ├── SyncSystemUnreadHandler.java │ │ ├── SyncUnreadHandler.java │ │ └── dispatcher │ │ ├── DispatchToGroupHandler.java │ │ ├── DispatchToSingleHandler.java │ │ ├── SendDispatchMessage.java │ │ └── SendDispatcher.java ├── service │ ├── IInboxService.java │ ├── IOutboxService.java │ ├── IPushService.java │ ├── StopcockService.java │ └── impl │ │ ├── InboxService.java │ │ ├── OutboxService.java │ │ └── PushService.java └── storage │ ├── UnreadLoopData.java │ ├── dao │ ├── IGroupDAO.java │ ├── IGroupMessageDAO.java │ ├── IMessageDAO.java │ ├── IServerDAO.java │ ├── ISessionMessageDAO.java │ ├── IUserDAO.java │ └── impl │ │ ├── GroupDAO.java │ │ ├── GroupMessageDao.java │ │ ├── MessageDAO.java │ │ ├── ServerDAO.java │ │ ├── SessionMessageDAO.java │ │ └── UserDAO.java │ └── entity │ ├── GroupEntity.java │ ├── GroupMessageEntity.java │ └── SessionMessageEntity.java └── utils ├── AESUtils.java ├── IMUtils.java └── IdGenerator.java /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven template 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | ### Java template 12 | *.class 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.ear 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 26 | 27 | *.iml 28 | 29 | ## Directory-based project format: 30 | .idea/ 31 | # if you remove the above rule, at least ignore the following: 32 | 33 | # User-specific stuff: 34 | # .idea/workspace.xml 35 | # .idea/tasks.xml 36 | # .idea/dictionaries 37 | 38 | # Sensitive or high-churn files: 39 | # .idea/dataSources.ids 40 | # .idea/dataSources.xml 41 | # .idea/sqlDataSources.xml 42 | # .idea/dynamic.xml 43 | # .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | # .idea/gradle.xml 47 | # .idea/libraries 48 | 49 | # Mongo Explorer plugin: 50 | # .idea/mongoSettings.xml 51 | 52 | ## File-based project format: 53 | *.ipr 54 | *.iws 55 | 56 | ## Plugin-specific files: 57 | 58 | # IntelliJ 59 | /out/ 60 | 61 | # mpeltonen/sbt-idea plugin 62 | .idea_modules/ 63 | 64 | # JIRA plugin 65 | atlassian-ide-plugin.xml 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | ### Gradle template 70 | .gradle 71 | build/ 72 | gradle 73 | 74 | # Ignore Gradle GUI config 75 | gradle-app.setting 76 | 77 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 78 | !gradle-wrapper.jar 79 | 80 | crashlytics.properties 81 | crashlytics-build.properties 82 | ### OSX template 83 | .DS_Store 84 | .AppleDouble 85 | .LSOverride 86 | 87 | # Icon must end with two \r 88 | Icon 89 | 90 | # Thumbnails 91 | ._* 92 | 93 | # Files that might appear in the root of a volume 94 | .DocumentRevisions-V100 95 | .fseventsd 96 | .Spotlight-V100 97 | .TemporaryItems 98 | .Trashes 99 | .VolumeIcon.icns 100 | 101 | # Directories potentially created on remote AFP share 102 | .AppleDB 103 | .AppleDesktop 104 | Network Trash Folder 105 | Temporary Items 106 | .apdisk 107 | 108 | # Created by .ignore support plugin (hsz.mobi) 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linbox_server 2 | 3 | LINBOX is a distributed IM system designed for www.medtree.cn. 4 | 5 | Download medtree app for iphone at [here](https://itunes.apple.com/cn/app/yi-shu/id933709180?mt=8) 6 | 7 | Download medtree app for android at [here](https://medtree.cn/release/android/4.1.1/medtree.apk) 8 | 9 | # Tech 10 | * Netty and tcp protocol 11 | * Kafka as center MQ 12 | * Dubbo as rpc framework 13 | * Zookeeper as config center 14 | 15 | # Usage 16 | Build the project: 17 | ``` 18 | gradle zip 19 | ``` 20 | 21 | distribution package is under 22 | ``` 23 | build/distribution/linbox_server-1.0.zip 24 | ``` 25 | 26 | Unzip it and start the server: 27 | ``` 28 | # start the tcp client: 29 | sh tcpServer.sh 30 | 31 | # start the router: 32 | sh consumer.sh 33 | 34 | # start the rpc client 35 | sh rpcServer.sh 36 | ``` 37 | 38 | # High availablity and scaliability 39 | Linbox is distributed by design. 40 | All of the components of linbox - tcp client, rpc client and router - are all distributed. 41 | 42 | Steps to prompt availability: 43 | 44 | * Use zookeeper cluster 45 | * Use Kafka cluster 46 | * Use tcp client cluster 47 | * [im.properties](src/main/config/im.properties) need tobe configured for server hosts and ports 48 | * Use rpc client cluster 49 | * [rpc.xml](src/main/config/spring/rpc.xml) need to be configured for pointed rpc port, or you can use dynamic ports generated by system. 50 | * Use router client cluster 51 | 52 | # Dependencies 53 | ## Java Version 54 | **JDK-1.8** is required to build and run it. 55 | 56 | ## Security Update For JRE 57 | **Be carefual:** as we use AES-256 encryption, you need to download packages to update your local jre environment. 58 | * Download package at [here](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) 59 | * Unzip and copy the jars into ```${java.home}/jre/lib/security/``` 60 | 61 | Click [this](http://stackoverflow.com/questions/6481627/java-security-illegal-key-size-or-default-parameters) for detail information for this problem. 62 | 63 | ## Mysql 64 | Mysql database is used as persistence storage for im messages. 65 | 66 | ### Mysql Versions 67 | There is no special requirement for mysql version. 68 | But if you want to use **emojis**, you need to use mysql version **5.6+**, and config the character set to *utf8mb4*. 69 | 70 | ### Predefined Tables 71 | You can find all mysql operations in package ```com.linbox.im.server.storage``` 72 | 73 | There are 4 tables predefined in programs, sql script could be find in [mysql.sql](src/main/docs/mysql.sql) 74 | 75 | ### Configs 76 | Config mysql connection in [im.properties](src/main/config/im.properties) 77 | 78 | ## Redis 79 | Config redis connection in [im.properties](src/main/config/im.properties) 80 | 81 | ## Zookeeper 82 | Zookeeper is used as config center of the project. It is also a dependency of *dubbo* and *Kafka*. 83 | Config Zookeeper in [rpc.xml](src/main/config/spring/rpc.xml) 84 | 85 | ## Kafka 86 | Kafka is used as Center MQ. 87 | Kafka is configd in [im.properties](src/main/config/im.properties) 88 | 89 | 90 | # Architecture 91 | ![architecture](src/main/docs/IM-architecture.jpg) 92 | 93 | 94 | # Communication Protocol 95 | Linbox use tcp protocal and JSON structure in communication. 96 | Data package structure is designed as below: 97 | 98 | |Version|Type|Length|Content| 99 | |-----|-----|----|------| 100 | |2 Byte|2 Byte|4 Byte|JSON String| 101 | 102 | 103 | |Name|Description| 104 | |:--|:--| 105 | |Version| A flag to show the protocol version of the package| 106 | |Type| Request / Response command type| 107 | |Length| The length of the content part| 108 | |Content| Encrypted Data content structured in json| 109 | 110 | 111 | # Main Communications 112 | ## Get history messages 113 | 114 | ![im-pulloldmessage](src/main/docs/Im-PullMessage.jpg) 115 | 116 | ## Get unread count and messages 117 | 118 | ![im-syncunread](src/main/docs/Im-SyncUnread.jpg) 119 | 120 | ## Send messages 121 | 122 | ![Im-Communication](src/main/docs/Im-Communication.jpg) 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | 4 | defaultTasks 'zip' 5 | 6 | group = 'com.linbox' 7 | version = '1.0' 8 | 9 | description = 'linbox server' 10 | 11 | sourceCompatibility = 1.8 12 | targetCompatibility = 1.8 13 | 14 | repositories { 15 | 16 | mavenLocal() 17 | 18 | maven { 19 | url 'http://repo.mobimind.net/nexus/content/groups/mobimind-all' 20 | } 21 | 22 | maven { 23 | url 'http://repo1.maven.org/maven2' 24 | artifactUrls 'http://repository.jboss.org/nexus/content/groups/public-jboss' 25 | } 26 | 27 | } 28 | configurations.all { 29 | // check for updates every build 30 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 31 | } 32 | 33 | dependencies { 34 | compile 'commons-lang:commons-lang:2.6' 35 | compile 'redis.clients:jedis:2.7.2' 36 | compile 'com.alibaba:fastjson:1.2.4' 37 | compile 'joda-time:joda-time:2.8.1' 38 | compile 'com.google.guava:guava:18.0' 39 | 40 | //slf4j and logback 41 | compile 'ch.qos.logback:logback-classic:1.1.3' 42 | 43 | // spring 44 | compile 'org.springframework:spring-core:4.2.2.RELEASE' 45 | compile 'org.springframework:spring-context:4.2.2.RELEASE' 46 | compile 'org.springframework:spring-expression:4.2.2.RELEASE' 47 | 48 | // mysql 49 | compile 'com.zaxxer:HikariCP:2.4.3' 50 | compile 'org.sql2o:sql2o:1.5.4' 51 | compile 'mysql:mysql-connector-java:5.1.38' 52 | 53 | //kafka 54 | compile 'org.apache.kafka:kafka-clients:0.9.0.0' 55 | 56 | //dubbo 57 | compile group: "com.alibaba", name: "dubbo", version: "2.5.4-SNAPSHOT", changing: true, { 58 | exclude module: 'spring' 59 | } 60 | compile 'com.github.sgroschupf:zkclient:0.1' 61 | 62 | //netty 63 | compile 'io.netty:netty-all:4.0.29.Final' 64 | 65 | 66 | testCompile 'junit:junit:3.8.1' 67 | } 68 | 69 | task copyConfig(type: Copy, dependsOn: 'build') { 70 | from 'src/main/config' 71 | from 'src/main/resource' 72 | 73 | into 'build/target/config' 74 | } 75 | 76 | task copyBin(type: Copy, dependsOn: 'copyConfig') { 77 | from 'src/main/bin' 78 | into 'build/target/bin' 79 | } 80 | 81 | task copyLibs(type: Copy, dependsOn: 'copyBin') { 82 | from configurations.runtime 83 | from 'build/libs' 84 | 85 | into 'build/target/lib' 86 | } 87 | 88 | task zip(type: Zip, dependsOn: 'copyLibs') { 89 | outputs.upToDateWhen { false } 90 | 91 | from 'build/target' 92 | into 'linbox_server' 93 | } 94 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'linbox_server' 2 | -------------------------------------------------------------------------------- /src/main/assemblies/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | im_server 4 | 5 | zip 6 | 7 | true 8 | 9 | 10 | /lib 11 | true 12 | true 13 | 14 | 15 | 16 | 17 | ${basedir}/src/main/bin 18 | /bin 19 | 0777 20 | 21 | 22 | ${basedir}/src/main/config/ 23 | /config 24 | 0777 25 | 26 | 27 | ${basedir}/src/main/resource/ 28 | /resource 29 | 0777 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/bin/consumer.sh: -------------------------------------------------------------------------------- 1 | #java -cp ".:../lib/*:../config" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006 -Xloggc:consumer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./consumer.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms1g -Xmx3g com.medtree.im.server.router.ImRouterService 2 | java -cp ".:../lib/*:../config" -Xloggc:consumer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./consumer.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms1g -Xmx3g com.linbox.im.server.router.ImRouterServer -------------------------------------------------------------------------------- /src/main/bin/rpcServer.sh: -------------------------------------------------------------------------------- 1 | #java -cp ".:../lib/*:../config:../cert" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5008 -Xloggc:tcpServer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./tcp_server.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms2g -Xmx2g -Ddubbo.application.logger=slf4j -Ddubbo.spring.config=classpath*:spring/*.xml -Dname=medtree-im-rpc-server com.alibaba.dubbo.container.Main spring logback 2 | java -cp ".:../lib/*:../config:../cert" -Xloggc:tcpServer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./tcp_server.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms2g -Xmx2g -Ddubbo.application.logger=slf4j -Ddubbo.spring.config=classpath*:spring/rpc.xml -Dname=medtree-Im-rpc-server com.alibaba.dubbo.container.Main spring logback -------------------------------------------------------------------------------- /src/main/bin/tcpServer.sh: -------------------------------------------------------------------------------- 1 | #java -cp ".:../lib/*:../config:" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xloggc:tcpServer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./tcp_server.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms6g -Xmx10g com.medtree.im.server.connector.tcp.ImTcpServer 2 | java -cp ".:../lib/*:../config:" -Xloggc:tcpServer.gc.log -verbose:gc -XX:-UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M -XX:-PrintGCDetails -XX:HeapDumpPath=./tcp_server.hprof -XX:-HeapDumpOnOutOfMemoryError -Xms6g -Xmx10g com.linbox.im.server.connector.tcp.ImTcpServer -------------------------------------------------------------------------------- /src/main/config/im.properties: -------------------------------------------------------------------------------- 1 | # server information 2 | im.ip=127.0.0.1 3 | im.port=9502 4 | 5 | #mysql configs 6 | mysql.url=jdbc:mysql://localhost/linbox 7 | mysql.user=root 8 | mysql.password=root 9 | mysql.cachePrepStmts=true 10 | mysql.prepStmtCacheSize=300 11 | mysql.prepStmtCacheSqlLimit=2048 12 | 13 | #Kafka Configs 14 | bootstrap.servers=localhost:9092 15 | acks=all 16 | retries=0 17 | batch.size=16384 18 | linger.ms=1 19 | buffer.memory=33554432 20 | key.serializer=org.apache.kafka.common.serialization.StringSerializer 21 | value.serializer=org.apache.kafka.common.serialization.StringSerializer 22 | group.id=im 23 | enable.auto.commit=true 24 | auto.commit.interval.ms=1000 25 | session.timeout.ms=30000 26 | key.deserializer=org.apache.kafka.common.serialization.StringDeserializer 27 | value.deserializer=org.apache.kafka.common.serialization.StringDeserializer 28 | 29 | #redis config 30 | redis.host=127.0.0.1 31 | redis.port=6379 32 | redis.maxActive=1000 33 | redis.maxIdle=20 34 | redis.maxWaitMilli=3000 35 | redis.timeout=6000 36 | 37 | #monitor config 38 | monitor.duration.second=10 -------------------------------------------------------------------------------- /src/main/config/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | true 11 | 12 | /tmp/logging/im-server/%d{yyyy-MM-dd}.%i.log 13 | 10 14 | 15 | 100MB 16 | 17 | 18 | 19 | %d{HH:mm:ss.SSS} %level [%thread] %logger.%class{0}#%method [%file:%line] %msg%n 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/config/spring/router.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/config/spring/rpc.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/config/spring/spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | classpath*:im.properties 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ${mysql.url} 29 | ${mysql.user} 30 | ${mysql.password} 31 | ${mysql.cachePrepStmts} 32 | ${mysql.prepStmtCacheSize} 33 | ${mysql.prepStmtCacheSqlLimit} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/config/spring/tcp.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/docs/IM-architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrsec/linbox_server/40ea82c52c7cd61ac4719e08b12602c6bee80245/src/main/docs/IM-architecture.jpg -------------------------------------------------------------------------------- /src/main/docs/Im-Communication.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrsec/linbox_server/40ea82c52c7cd61ac4719e08b12602c6bee80245/src/main/docs/Im-Communication.jpg -------------------------------------------------------------------------------- /src/main/docs/Im-PullMessage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrsec/linbox_server/40ea82c52c7cd61ac4719e08b12602c6bee80245/src/main/docs/Im-PullMessage.jpg -------------------------------------------------------------------------------- /src/main/docs/Im-SyncUnread.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrsec/linbox_server/40ea82c52c7cd61ac4719e08b12602c6bee80245/src/main/docs/Im-SyncUnread.jpg -------------------------------------------------------------------------------- /src/main/docs/mysql.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE IF EXISTS `im_session_message`; 3 | 4 | CREATE TABLE `im_session_message` ( 5 | `RId` BIGINT(20) NOT NULL COMMENT '消息发送方的 rid', 6 | `SessionId` VARCHAR(32) NOT NULL COMMENT '对话 session id', 7 | `FromUserID` BIGINT(20) NOT NULL, 8 | `ToUserID` BIGINT(20) NOT NULL, 9 | `MsgID` BIGINT(20) NOT NULL COMMENT '消息编号', 10 | `MimeType` VARCHAR(64) NOT NULL COMMENT '多媒体文件类型', 11 | `Content` VARCHAR(500) DEFAULT NULL COMMENT '文件内容', 12 | `SendTime` BIGINT(20) NOT NULL COMMENT '服务器端接收到消息的时间,消息发送时间', 13 | `Created` BIGINT(20) NOT NULL 14 | COMMENT '记录创建时间', 15 | UNIQUE KEY `session_msg` (`SessionId`, `MsgID`) 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息记录'; 17 | 18 | 19 | DROP TABLE IF EXISTS `im_group_message`; 20 | 21 | CREATE TABLE `im_group_message` ( 22 | `RId` BIGINT(20) NOT NULL COMMENT '消息发送方的 rid', 23 | `GroupId` VARCHAR(32) NOT NULL COMMENT '对话 session id', 24 | `FromUserID` BIGINT(20) NOT NULL, 25 | `MsgID` BIGINT(20) NOT NULL COMMENT '消息编号', 26 | `MimeType` VARCHAR(64) NOT NULL COMMENT '多媒体文件类型', 27 | `Content` VARCHAR(500) DEFAULT NULL COMMENT '文件内容', 28 | `SendTime` BIGINT(20) NOT NULL COMMENT '服务器端接收到消息的时间,消息发送时间', 29 | `Created` BIGINT(20) NOT NULL COMMENT '记录创建时间', 30 | UNIQUE KEY `group_msg` (`GroupId`, `MsgID`) 31 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息记录'; 32 | 33 | DROP TABLE IF EXISTS `group_members`; 34 | 35 | CREATE TABLE `group_members` ( 36 | `Id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id,用于表维护目的', 37 | `GroupId` bigint(20) unsigned NOT NULL COMMENT '群组id', 38 | `AccountId` bigint(20) unsigned NOT NULL COMMENT '系统用户 id', 39 | `Role` int(2) DEFAULT '0' COMMENT '用户在群组中的身份', 40 | `Visible` tinyint(1) DEFAULT '1' COMMENT '消息是否在用户列表可见', 41 | `Created` bigint(20) unsigned NOT NULL, 42 | `Updated` bigint(20) unsigned NOT NULL, 43 | PRIMARY KEY (`Id`), 44 | KEY `group_id` (`GroupId`), 45 | KEY `account_id` (`AccountId`) 46 | ) ENGINE=InnoDB AUTO_INCREMENT=555 DEFAULT CHARSET=utf8mb4; 47 | 48 | DROP TABLE IF EXISTS `profile`; 49 | 50 | CREATE TABLE `profile` ( 51 | `AccountID` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '账户ID', 52 | `MDTCode` bigint(20) NOT NULL, 53 | `ChatID` varchar(32) COLLATE utf8_bin DEFAULT NULL, 54 | `Birthday` date DEFAULT NULL COMMENT '生日', 55 | `Profession` int(2) NOT NULL DEFAULT '0' COMMENT '职业:\n0-未知\n10-医生\n20-护士\n30-医学生\n90-医护工作者', 56 | `Signature` varchar(200) CHARACTER SET utf8mb4 DEFAULT NULL, 57 | `Phone` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '电话', 58 | `OftenPlace` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '出没地点', 59 | `Interest` varchar(200) CHARACTER SET utf8mb4 DEFAULT NULL, 60 | `ActiveState` int(1) NOT NULL DEFAULT '0' COMMENT '账户激活状态:\n0-未激活;\n1-已激活;\n9-停用;', 61 | `RealName` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL, 62 | `NickName` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL, 63 | `Age` varchar(45) COLLATE utf8_bin DEFAULT NULL, 64 | `Gender` int(2) DEFAULT '0' COMMENT 'Secrecy(0,"保密"), /** * 男性 */ Male(1,"男"), /** * 女性 */ Female(2,"女");', 65 | `Constellation` varchar(45) COLLATE utf8_bin DEFAULT NULL, 66 | `LastActive` int(11) DEFAULT NULL, 67 | `Status` int(2) DEFAULT '0' COMMENT '状态\n0-离线\n1-在线\n2-手术中\n3-门诊中\n4-工作中\n5-休假中', 68 | `Avatar` varchar(50) COLLATE utf8_bin DEFAULT NULL, 69 | `OrgName` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '机构名称', 70 | `OrgID` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '机构ID', 71 | `DeptName` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '部门名称', 72 | `DeptID` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '部门ID', 73 | `Title` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '学历或职位名称', 74 | `TitleType` int(1) DEFAULT '0' COMMENT '学历或职位类型(不同经历枚举含义不同):\n0-未设置\n\n学校:\n1-大专\n2-本科\n3-硕士\n4-博士\n5-其他\n\n医院', 75 | `Created` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 76 | `Updated` timestamp NULL DEFAULT NULL, 77 | `IsCertificated` int(1) DEFAULT '0' COMMENT '是否认证', 78 | `CertificateInfo` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '认证信息,由审核人员添加', 79 | `FirstDeptName` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '一级部门名称', 80 | `CreatedNew` int(11) DEFAULT '0', 81 | `UpdatedNew` int(11) DEFAULT '0', 82 | `PinYin` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '真实姓名拼音字段', 83 | `Province` varchar(50) CHARACTER SET utf8mb4 DEFAULT '0', 84 | `City` varchar(50) CHARACTER SET utf8mb4 DEFAULT '0', 85 | `Achievement` varchar(250) CHARACTER SET utf8mb4 DEFAULT NULL, 86 | `Sideline` varchar(130) CHARACTER SET utf8mb4 DEFAULT NULL, 87 | `Zone` int(2) DEFAULT '2', 88 | PRIMARY KEY (`AccountID`), 89 | UNIQUE KEY `MDTCode_UNIQUE123` (`MDTCode`), 90 | UNIQUE KEY `AccountID_UNIQUE` (`AccountID`) 91 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=COMPACT COMMENT='用户信息表' -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/exceptions/ConfigFailException.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.exceptions; 2 | 3 | /** 4 | * Created by lrsec on 11/13/15. 5 | */ 6 | public class ConfigFailException extends RuntimeException { 7 | public ConfigFailException (String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/exceptions/IMConsumerException.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.exceptions; 2 | 3 | /** 4 | * Created by lrsec on 12/15/15. 5 | */ 6 | public class IMConsumerException extends RuntimeException { 7 | private String topicMessage; 8 | 9 | public IMConsumerException(Exception e, String topicMessage) { 10 | super(e); 11 | topicMessage = topicMessage; 12 | } 13 | 14 | public String getTopicMessage() { 15 | return topicMessage; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/exceptions/IMException.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.exceptions; 2 | 3 | /** 4 | * Created by lrsec on 11/13/15. 5 | */ 6 | public class IMException extends RuntimeException { 7 | public IMException (String message) { 8 | super (message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/exceptions/ServiceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.exceptions; 2 | 3 | /** 4 | * Created by lrsec on 11/13/15. 5 | */ 6 | public class ServiceNotFoundException extends Exception { 7 | public ServiceNotFoundException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/interfaces/IIMService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.interfaces; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * Created by lrsec on 8/21/15. 7 | */ 8 | public interface IIMService { 9 | Set getIMServerList(); 10 | String getPassword(long userId); 11 | 12 | void sendMessage(String fromUserId, String toId, String mineType, String content, int messageType, String creatorId); 13 | void sendSystemMessage(String targetUserId, int systemType, String content); 14 | 15 | long generateGroupId(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/AuthRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | 9 | public class AuthRequest extends ByteCreator{ 10 | // 客户端 request id 11 | @JSONField(name = "r_id") 12 | public long rId; 13 | 14 | // user chat id 15 | @JSONField(name="user_id") 16 | public String userId; 17 | 18 | // user token 19 | @JSONField(name = "token") 20 | public String token; 21 | 22 | // user ip 23 | @JSONField(name = "ip") 24 | public String ip; 25 | 26 | // user port 27 | @JSONField(name = "port") 28 | public int port; 29 | 30 | // device type 31 | @JSONField(name = "device") 32 | public String device; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class AuthResponse extends ByteCreator { 9 | // 客户端 request id 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | // user chat id 14 | @JSONField(name = "user_id") 15 | public String userId; 16 | 17 | // status code, 200 = success 18 | @JSONField(name = "status") 19 | public int status; 20 | 21 | // error message 22 | @JSONField(name = "err_msg") 23 | public String errMsg; 24 | 25 | // 发送该消息的服务器时间,毫秒 26 | @JSONField(name = "send_time") 27 | public long sendTime; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/ByteCreator.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.exceptions.IMException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * Created by lrsec on 7/4/15. 10 | * 11 | * 提供了消息体自生成可用于 netty 发送的 byte[] 的共更能 12 | */ 13 | public class ByteCreator { 14 | private static Logger logger = LoggerFactory.getLogger(ByteCreator.class); 15 | 16 | public MessageWrapper toWrapper() { 17 | MessageWrapper wrapper = new MessageWrapper(); 18 | 19 | RequestResponseType type = RequestResponseType.parse(this); 20 | 21 | if (type == null) { 22 | logger.error("无法解析当前类型 {}", this.getClass().getName()); 23 | throw new IMException("无法解析当前类型 " + this.getClass().getName()); 24 | } 25 | 26 | wrapper.type = type; 27 | wrapper.content = this; 28 | 29 | return wrapper; 30 | } 31 | 32 | public String toWrapperJson() { 33 | return JSON.toJSONString(toWrapper()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/Message.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.linbox.im.server.storage.entity.GroupMessageEntity; 5 | import com.linbox.im.server.storage.entity.SessionMessageEntity; 6 | 7 | /** 8 | * Created by lrsec on 6/29/15. 9 | */ 10 | public class Message { 11 | 12 | // 消息发送时的 rId 13 | @JSONField(name = "r_id") 14 | public long rId; 15 | 16 | // 发送方 id 17 | @JSONField(name = "from_user_id") 18 | public String fromUserId; 19 | 20 | // 目的方 id 21 | @JSONField(name = "to_user_id") 22 | public String toUserId; 23 | 24 | // 群组 id 25 | @JSONField(name = "group_id") 26 | public String groupId; 27 | 28 | // message id 29 | @JSONField(name = "msg_id") 30 | public long msgId; 31 | 32 | // 消息体类型 33 | @JSONField(name = "mime_type") 34 | public String mimeType; 35 | 36 | // 消息体内容 37 | @JSONField(name = "content") 38 | public String content; 39 | 40 | // 服务器端接收到消息的时间 41 | @JSONField(name = "send_time") 42 | public long sendTime; 43 | 44 | // 消息的类型 45 | @JSONField(name = "type") 46 | public int type; 47 | 48 | public static Message convertToMsg(SessionMessageEntity dao, String userId) { 49 | Message m = new Message(); 50 | m.rId = dao.RId; 51 | m.fromUserId = Long.toString(dao.FromUserID); 52 | m.toUserId = Long.toString(dao.ToUserID); 53 | m.msgId = dao.MsgID; 54 | m.mimeType = dao.MimeType; 55 | m.content = dao.Content; 56 | m.sendTime = dao.SendTime; 57 | m.type = MessageType.Session.getValue(); 58 | 59 | return m; 60 | } 61 | 62 | public static Message convertToMsg(GroupMessageEntity dao, String fromUserId, String groupId) { 63 | Message m = new Message(); 64 | m.rId = dao.RId; 65 | m.fromUserId = fromUserId; 66 | m.groupId = groupId; 67 | m.msgId = dao.MsgID; 68 | m.mimeType = dao.MimeType; 69 | m.content = dao.Content; 70 | m.sendTime = dao.SendTime; 71 | m.type = MessageType.Group.getValue(); 72 | 73 | return m; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | /** 4 | * Created by lrsec on 7/3/15. 5 | */ 6 | public enum MessageType { 7 | All(1, "all"), 8 | Session(2, "session"), 9 | Group(3, "group"); 10 | 11 | private int value; 12 | private String name; 13 | 14 | private MessageType(int value, String name) { 15 | this.value = value; 16 | this.name = name; 17 | } 18 | 19 | public int getValue() { 20 | return value; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public static MessageType parse(int v) { 28 | MessageType type = null; 29 | 30 | for(MessageType t : MessageType.values()) { 31 | if (t.getValue() == v) { 32 | type = t; 33 | } 34 | } 35 | 36 | return type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/MessageWrapper.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | /** 4 | * Created by lrsec on 7/10/15. 5 | */ 6 | public class MessageWrapper { 7 | public RequestResponseType type; 8 | public Object content; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/NewMessage.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class NewMessage extends ByteCreator{ 9 | @JSONField(name = "user_id") 10 | public String userId; 11 | 12 | @JSONField(name = "remote_id") 13 | public String remoteId; 14 | 15 | @JSONField(name = "group_id") 16 | public String groupId; 17 | 18 | @JSONField(name = "type") 19 | public int type; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/OfflineInfo.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 7/9/15. 7 | */ 8 | public class OfflineInfo extends ByteCreator{ 9 | // user chat id 10 | @JSONField(name="user_id") 11 | public String userId; 12 | 13 | // 返回信息,客户端显示给用户下线原因 14 | @JSONField(name = "reason") 15 | public String reason; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/Ping.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 7/6/15. 7 | */ 8 | public class Ping extends ByteCreator{ 9 | // 客户端 request id 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | // user chat id 14 | @JSONField(name="user_id") 15 | public String userId; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/Pong.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 7/6/15. 7 | */ 8 | public class Pong extends ByteCreator{ 9 | // 客户端 request id 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/PullOldMsgRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class PullOldMsgRequest extends ByteCreator{ 9 | 10 | public static final long MAX = -1l; 11 | 12 | // 客户端 request id 13 | @JSONField(name = "r_id") 14 | public long rId; 15 | 16 | // user id 17 | @JSONField(name = "user_id") 18 | public String userId; 19 | 20 | // remote id 21 | @JSONField(name = "remote_id") 22 | public String remoteId; 23 | 24 | @JSONField(name = "group_id") 25 | public String groupId; 26 | 27 | // max message id want to pull 28 | @JSONField(name = "max_msg_id") 29 | public long maxMsgId; 30 | 31 | // min message id for this pulling 32 | @JSONField(name = "min_msg_id") 33 | public long minMsgId; 34 | 35 | // pull page size 36 | @JSONField(name = "limit") 37 | public int limit; 38 | 39 | // 消息类型 40 | @JSONField(name = "type") 41 | public int type; 42 | 43 | // request 中携带的 max message id 信息,冗余仅用于简化客户端操作 44 | @JSONField(name = "max_msg_id_in_request") 45 | public long maxMsgIdInRequest; 46 | 47 | // request type ,冗余,仅用于简化客户端操作 48 | @JSONField(name = "request_type") 49 | public int requestType; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/PullOldMsgRequestType.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | /** 4 | * Created by lrsec on 8/4/15. 5 | */ 6 | 7 | import com.alibaba.fastjson.JSON; 8 | 9 | /** 10 | * 冗余类型,仅用于简化客户端操作 11 | */ 12 | public enum PullOldMsgRequestType { 13 | LATEST(1, "最新数据"), 14 | OLD(2, "历史数据"), 15 | POINTED(3,"指定数据") 16 | ; 17 | 18 | private int value; 19 | private String name; 20 | 21 | private PullOldMsgRequestType(int value, String name) { 22 | this.value = value; 23 | this.name = name; 24 | } 25 | 26 | public int getValue() { 27 | return value; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public int valueOf() { 35 | return value; 36 | } 37 | 38 | public static void main(String[] args) { 39 | class A { 40 | public PullOldMsgRequestType type = LATEST; 41 | } 42 | 43 | A a = new A(); 44 | 45 | String json = JSON.toJSONString(a); 46 | 47 | A b = JSON.parseObject(json, A.class); 48 | 49 | System.out.println(json); 50 | System.out.println(b.type.getName()); 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/PullOldMsgResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class PullOldMsgResponse extends ByteCreator { 9 | // client request id 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | // user chat id 14 | @JSONField(name = "user_id") 15 | public String userId; 16 | 17 | // remote chatter id 18 | @JSONField(name = "remote_id") 19 | public String remoteId; 20 | 21 | @JSONField(name = "group_id") 22 | public String groupId; 23 | 24 | // 消息 25 | @JSONField(name = "msgs") 26 | public Message[] msgs; 27 | 28 | // 状态码, 200 = success 29 | @JSONField(name = "status") 30 | public int status; 31 | 32 | // 错误信息 33 | @JSONField(name = "err_msg") 34 | public String errMsg; 35 | 36 | // 消息类型 37 | @JSONField(name = "type") 38 | public int type; 39 | 40 | // request 中携带的 max message id 信息,冗余仅用于简化客户端操作 41 | @JSONField(name = "max_msg_id_in_request") 42 | public long maxMsgIdInRequest; 43 | 44 | // request type ,冗余,仅用于简化客户端操作 45 | @JSONField(name = "request_type") 46 | public int requestType; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/ReadAckRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class ReadAckRequest extends ByteCreator{ 9 | 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | @JSONField(name = "user_id") 14 | public String userId; 15 | 16 | @JSONField(name = "remote_id") 17 | public String remoteId; 18 | 19 | @JSONField(name = "group_id") 20 | public String groupId; 21 | 22 | @JSONField(name = "msg_id") 23 | public long msgId; 24 | 25 | // 消息类型 26 | @JSONField(name = "type") 27 | public int type; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/ReadAckResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class ReadAckResponse extends ByteCreator { 9 | 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | @JSONField(name = "user_id") 14 | public String userId; 15 | 16 | @JSONField(name = "remote_id") 17 | public String remoteId; 18 | 19 | @JSONField(name = "group_id") 20 | public String groupId; 21 | 22 | // 消息类型 23 | @JSONField(name = "type") 24 | public int type; 25 | 26 | @JSONField(name = "status") 27 | public int status; 28 | 29 | @JSONField(name = "err_code") 30 | public String errCode; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/RequestResponseType.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.linbox.im.message.system.*; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public enum RequestResponseType { 9 | 10 | // 客户端鉴权,同时用于连接建立后试探网络环境 11 | // 鉴权信息必须是网络联通后客户端发送的第一个信息,否则连接被关闭 12 | AuthRequestMsg(1, "AuthRequest", AuthRequest.class), 13 | AuthResponseMsg(2, "AuthResponse", AuthResponse.class), 14 | 15 | // 客户端同步未读信息 16 | SyncUnreadRequestMsg(3, "SyncUnreadRequest", SyncUnreadRequest.class), 17 | SyncUnreadResponseMsg(4, "SyncUnreadResponse", SyncUnreadResponse.class), 18 | 19 | // 客户端确认未读信息已被读取 20 | ReadAckRequestMsg(5, "ReadAckRequest", ReadAckRequest.class), 21 | ReadAckResponseMsg(6, "ReadAckResponse", ReadAckResponse.class), 22 | 23 | // 客户端以反序分页拉取信息 24 | PullOldMsgRequestMsg(7, "PullOldMsgRequest", PullOldMsgRequest.class), 25 | PullOldMsgResponseMsg(8, "PullOldMsgResponse", PullOldMsgResponse.class), 26 | 27 | // 客户端发送信息 28 | SendMsgRequestMsg(9, "SendMsgRequest", SendMsgRequest.class), 29 | SendMsgResponseMsg(10, "SendMsgResponse", SendMsgResponse.class), 30 | 31 | // 新消息通知 32 | NewMsgInfo(11, "NewMessage", NewMessage.class), 33 | OfflineInfo(12, "OfflineInfo", OfflineInfo.class), 34 | 35 | // Heartbeat 36 | Ping(13, "Ping", Ping.class), 37 | Pong(14, "Pong", Pong.class), 38 | 39 | // 系统消息 40 | SystemMsgInfo(15, "SystemMessage", SystemMessage.class), 41 | SyncSystemUnreadRequestMsg(16, "SyncSystemUnreadRequest", SyncSystemUnreadRequest.class), 42 | SyncSystemUnreadResponseMsg(17, "SyncSystemUnreadResponse", SyncSystemUnreadResponse.class), 43 | SystemUnreadAckRequestMsg(18, "SystemUnreadAckRequest", SystemUnreadAckRequest.class), 44 | SystemUnreadAckResponseMsg(19, "SystemUnreadAckResponse", SystemUnreadAckResponse.class) 45 | 46 | ; 47 | 48 | private int value; 49 | private String name; 50 | private Class clazz ; 51 | 52 | private RequestResponseType(int value, String name, Class clazz) { 53 | this.value = value; 54 | this.clazz = clazz; 55 | this.name = name; 56 | } 57 | 58 | public int getValue() { 59 | return value; 60 | } 61 | 62 | public Class getClazz() { 63 | return clazz; 64 | } 65 | 66 | public String getName() { 67 | return name; 68 | } 69 | 70 | public static RequestResponseType parse(int value) { 71 | RequestResponseType type = null; 72 | 73 | for(RequestResponseType t : RequestResponseType.values()) { 74 | if(t.getValue() == value) { 75 | type = t; 76 | break; 77 | } 78 | } 79 | 80 | return type; 81 | 82 | } 83 | 84 | public static RequestResponseType parse(Object o) { 85 | RequestResponseType type = null; 86 | 87 | for (RequestResponseType t : RequestResponseType.values()) { 88 | if (t.clazz.isInstance(o)) { 89 | type = t; 90 | break; 91 | } 92 | } 93 | 94 | return type; 95 | } 96 | 97 | public static boolean isRequest(RequestResponseType t) { 98 | return t==AuthRequestMsg 99 | || t==SyncUnreadRequestMsg 100 | || t==ReadAckRequestMsg 101 | || t==PullOldMsgRequestMsg 102 | || t==SendMsgRequestMsg 103 | || t==Ping; 104 | } 105 | 106 | public static boolean isResponse(RequestResponseType t) { 107 | return t==AuthResponseMsg 108 | || t==SyncUnreadResponseMsg 109 | || t==ReadAckResponseMsg 110 | || t==PullOldMsgResponseMsg 111 | || t==SendMsgResponseMsg 112 | || t==NewMsgInfo 113 | || t==OfflineInfo 114 | || t==Pong; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/SendMsgRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class SendMsgRequest extends ByteCreator{ 9 | @JSONField(name = "r_id") 10 | public long rId; 11 | 12 | @JSONField(name = "user_id") 13 | public String userId; 14 | 15 | @JSONField(name = "remote_id") 16 | public String remoteId; 17 | 18 | @JSONField(name = "group_id") 19 | public String groupId; 20 | 21 | @JSONField(name = "msg") 22 | public Message msg; 23 | 24 | // 消息类型 25 | @JSONField(name = "type") 26 | public int type; 27 | } -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/SendMsgResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class SendMsgResponse extends ByteCreator { 9 | @JSONField(name = "r_id") 10 | public long rId; 11 | 12 | @JSONField(name = "msg_r_id") 13 | public long msgRId; 14 | 15 | @JSONField(name = "user_id") 16 | public String userId; 17 | 18 | @JSONField(name = "remote_id") 19 | public String remoteId; 20 | 21 | @JSONField(name = "group_id") 22 | public String groupId; 23 | 24 | @JSONField(name = "msg_id") 25 | public long msgId; 26 | 27 | @JSONField(name = "send_time") 28 | public long sendTime; 29 | 30 | // 消息类型 31 | @JSONField(name = "type") 32 | public int type; 33 | 34 | @JSONField(name = "status") 35 | public int status; 36 | 37 | @JSONField(name = "err_msg") 38 | public String errMsg; 39 | } -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/SyncUnreadRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class SyncUnreadRequest extends ByteCreator{ 9 | // request id 10 | @JSONField(name="r_id") 11 | public long rId; 12 | 13 | // user chat id 14 | @JSONField(name="user_id") 15 | public String userId; 16 | 17 | // remote chatter id 18 | // 非必填项,填写则指定拉取一组对话的未读数信息。不填则拉取所有未读数信息 19 | @JSONField(name = "remote_id") 20 | public String remoteId; 21 | 22 | @JSONField(name = "group_id") 23 | public String groupId; 24 | 25 | // 分页数据,本次分页起始偏移量 26 | @JSONField(name = "offset") 27 | public long offset; 28 | 29 | // 分页数据,分业内数据量 30 | @JSONField(name = "limit") 31 | public int limit; 32 | 33 | // 消息类型 34 | // 非必填项,不填写则拉取所有类型的信息 35 | // 如果填写,则必须包含对应类型的 remoteChatId 或者 groupChatId 36 | @JSONField(name = "type") 37 | public int type = 1; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/SyncUnreadResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class SyncUnreadResponse extends ByteCreator { 9 | // request id 10 | @JSONField(name="r_id") 11 | public long rId; 12 | 13 | // user chat id 14 | @JSONField(name="user_id") 15 | public String userId; 16 | 17 | // remote chatter id 18 | // 非必填项,填写则指定拉取一组对话的未读数信息。不填则拉取所有未读数信息 19 | @JSONField(name = "remote_id") 20 | public String remoteId; 21 | 22 | @JSONField(name = "group_id") 23 | public String groupId; 24 | 25 | @JSONField(name = "type") 26 | public int type = 1; 27 | 28 | // unread messages 29 | @JSONField(name="unreads") 30 | public UnreadMsg[] unreads; 31 | 32 | // 返回本次所取数据的起始 offset 33 | @JSONField(name = "offset") 34 | public long offset; 35 | 36 | // status. 200 = success 37 | @JSONField(name="status") 38 | public int status; 39 | 40 | // error message 41 | @JSONField(name="err_msg") 42 | public String errMsg; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/UnreadMsg.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 6/29/15. 7 | */ 8 | public class UnreadMsg { 9 | // user chat id 10 | @JSONField(name = "user_id") 11 | public String userId; 12 | 13 | // remote chatter id 14 | @JSONField(name = "remote_id") 15 | public String remoteId; 16 | 17 | @JSONField(name = "group_id") 18 | public String groupId; 19 | 20 | // 消息类型 21 | @JSONField(name = "type") 22 | public int type; 23 | 24 | // last message id 25 | @JSONField(name = "msg_id") 26 | public long msgId; 27 | 28 | // unread count 29 | @JSONField(name = "count") 30 | public long count; 31 | 32 | // last message 33 | @JSONField(name = "msg") 34 | public Message msg; 35 | } -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/JobNotificationMessage.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 11/9/15. 7 | */ 8 | public class JobNotificationMessage { 9 | @JSONField(name = "unread") 10 | public int unreadCount; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SyncSystemUnreadRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.linbox.im.message.ByteCreator; 5 | 6 | /** 7 | * Created by lrsec on 9/23/15. 8 | */ 9 | public class SyncSystemUnreadRequest extends ByteCreator { 10 | // 客户端 request id 11 | @JSONField(name = "r_id") 12 | public long rId; 13 | 14 | /** 要拉取未读数的用户 id */ 15 | @JSONField(name = "user_id") 16 | public String userId; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SyncSystemUnreadResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.linbox.im.message.ByteCreator; 5 | 6 | /** 7 | * Created by lrsec on 9/23/15. 8 | */ 9 | public class SyncSystemUnreadResponse extends ByteCreator { 10 | // 客户端 request id 11 | @JSONField(name = "r_id") 12 | public long rId; 13 | 14 | /** 用户 id */ 15 | @JSONField(name = "user_id") 16 | public String userId; 17 | 18 | /** 系统未读数消息,直接复用了 SystemMessage */ 19 | @JSONField(name = "unreads") 20 | public SystemMessage[] unreads; 21 | 22 | // status. 200 = success 23 | @JSONField(name="status") 24 | public int status; 25 | 26 | // error message 27 | @JSONField(name="err_msg") 28 | public String errMsg; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SystemMessage.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.linbox.im.message.ByteCreator; 5 | 6 | /** 7 | * Created by lrsec on 9/7/15. 8 | */ 9 | public class SystemMessage extends ByteCreator { 10 | 11 | @JSONField(name = "system_type") 12 | public int systemType; 13 | 14 | @JSONField(name = "content") 15 | public String content; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SystemMessageTypes.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | /** 4 | * Created by lrsec on 9/23/15. 5 | */ 6 | public enum SystemMessageTypes { 7 | 8 | /** 有添加好友信息时,通知客户端,客户端更新新朋友未读数 */ 9 | NewFriend(10, "新朋友"), 10 | /** 有新消息时,通知客户端,用于消息中心中,新通知未读数的更新 */ 11 | NoticeNotify(30, "新通知"), 12 | /** 添加或者删除好友时,通知对方,刷新好友列表,通知客户端,用于人脉关系刷新 */ 13 | RelationChange(100, "人脉关系变动"), 14 | /** 身份认证状态发生变化时,发出通知 */ 15 | Certification(110, "身份认证信息"), 16 | /** 动态有更新 */ 17 | FeedNotify(120, "动态更新"), 18 | /** 活动信息有更新 */ 19 | ActivityNotify(130, "活动更新"), 20 | /** 招聘信息相关系统消息类型 */ 21 | JobNotifify(140, "招聘信息更新"); 22 | 23 | 24 | private int value; 25 | private String name; 26 | 27 | private SystemMessageTypes(int v, String n) { 28 | this.value = v; 29 | this.name = n; 30 | } 31 | 32 | public int getValue() { 33 | return value; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SystemUnreadAckRequest.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 9/23/15. 7 | */ 8 | public class SystemUnreadAckRequest { 9 | /** 消息发送时的 rId */ 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | 13 | /** 用户 id */ 14 | @JSONField(name = "user_id") 15 | public String userId; 16 | 17 | /** 未读数类型 */ 18 | @JSONField(name = "type") 19 | public int type; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/SystemUnreadAckResponse.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 9/23/15. 7 | */ 8 | public class SystemUnreadAckResponse { 9 | // 消息发送时的 rId 10 | @JSONField(name = "r_id") 11 | public long rId; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/message/system/content/SystemUnreadContent.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.message.system.content; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | 5 | /** 6 | * Created by lrsec on 9/23/15. 7 | */ 8 | public class SystemUnreadContent { 9 | 10 | /** 未读数 */ 11 | @JSONField(name = "unread") 12 | public long unread; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/rpc/IMService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.rpc; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.google.common.base.Strings; 5 | import com.linbox.im.interfaces.IIMService; 6 | import com.linbox.im.message.Message; 7 | import com.linbox.im.message.MessageType; 8 | import com.linbox.im.message.MessageWrapper; 9 | import com.linbox.im.message.SendMsgRequest; 10 | import com.linbox.im.message.system.SystemMessage; 11 | import com.linbox.im.server.constant.MessageTopic; 12 | import com.linbox.im.server.service.IOutboxService; 13 | import com.linbox.im.server.storage.dao.IGroupDAO; 14 | import com.linbox.im.server.storage.dao.IServerDAO; 15 | import org.apache.kafka.clients.producer.KafkaProducer; 16 | import org.apache.kafka.clients.producer.ProducerRecord; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | 21 | import java.util.Set; 22 | 23 | /** 24 | * Created by lrsec on 7/27/15. 25 | */ 26 | public class IMService implements IIMService { 27 | private static Logger logger = LoggerFactory.getLogger(IMService.class); 28 | 29 | @Autowired 30 | private KafkaProducer kafkaProducer; 31 | 32 | @Autowired 33 | private IOutboxService outboxService; 34 | 35 | @Autowired 36 | private IServerDAO serverDAO; 37 | 38 | @Autowired 39 | private IGroupDAO groupDAO; 40 | 41 | @Override 42 | public Set getIMServerList() { 43 | return serverDAO.getServers(); 44 | } 45 | 46 | @Override 47 | public String getPassword(long userId) { 48 | return serverDAO.generatePassword(userId); 49 | } 50 | 51 | @Override 52 | public void sendMessage(String fromUserId, String toId, String mineType, String content, int messageType, String creatorId) { 53 | logger.debug("Received message from rpc call. From: {}. To: {}. MimeType: {}. Content: {}. MessageType: {}. CreatorID: {}", fromUserId, toId, mineType, content, messageType, creatorId); 54 | 55 | if (Strings.isNullOrEmpty(fromUserId)) { 56 | logger.error("Sender user id could not be blank"); 57 | return; 58 | } 59 | 60 | if (Strings.isNullOrEmpty(fromUserId)) { 61 | logger.error("Target user id could not be blank"); 62 | return; 63 | } 64 | 65 | if (!isLegalSender(fromUserId)) { 66 | logger.warn("User {} is not allowed to send message through im rpc service", fromUserId); 67 | return; 68 | } 69 | 70 | long current = System.currentTimeMillis(); 71 | 72 | String toUserId = null; 73 | String toGroupId = null; 74 | 75 | MessageType type = MessageType.parse(messageType); 76 | if (type == null) { 77 | logger.warn("Unknown message type {} for user {}. Stop sending messages", messageType, fromUserId); 78 | return; 79 | } 80 | 81 | switch (type) { 82 | case Session: 83 | toUserId = toId; 84 | break; 85 | case Group: 86 | toGroupId = toId; 87 | break; 88 | default: 89 | logger.warn("Unknown message type {} in sending message through rpc service.", type.getName()); 90 | return; 91 | } 92 | 93 | Message msg = new Message(); 94 | msg.rId = current; 95 | msg.fromUserId = fromUserId; 96 | msg.toUserId = toUserId; 97 | msg.groupId = toGroupId; 98 | msg.msgId = -1; 99 | msg.mimeType = mineType; 100 | msg.content = content; 101 | msg.sendTime = -1; 102 | msg.type = messageType; 103 | 104 | SendMsgRequest request = new SendMsgRequest(); 105 | request.rId = current; 106 | request.userId = fromUserId; 107 | request.remoteId = toUserId; 108 | request.groupId = toGroupId; 109 | request.msg = msg; 110 | request.type = messageType; 111 | 112 | MessageWrapper wrapper = request.toWrapper(); 113 | 114 | String json = JSON.toJSONString(wrapper); 115 | 116 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_SEND_MSG, json)); 117 | } 118 | 119 | // only allow users in white list to send message 120 | private boolean isLegalSender(String userId) { 121 | return true; 122 | } 123 | 124 | @Override 125 | public long generateGroupId() { 126 | return groupDAO.generateGroupId(); 127 | } 128 | 129 | @Override 130 | public void sendSystemMessage(String targetUserId, int systemType, String content) { 131 | SystemMessage systemMessage = new SystemMessage(); 132 | systemMessage.systemType = systemType; 133 | systemMessage.content = content; 134 | 135 | outboxService.put(targetUserId, systemMessage.toWrapperJson()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/ImTcpServer.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp; 2 | 3 | import com.linbox.im.server.connector.tcp.handler.IMChannelInitializer; 4 | import com.linbox.im.server.monitor.IConnectorMonitor; 5 | import com.linbox.im.server.storage.dao.IServerDAO; 6 | import io.netty.bootstrap.ServerBootstrap; 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.ChannelOption; 9 | import io.netty.channel.EventLoopGroup; 10 | import io.netty.channel.nio.NioEventLoopGroup; 11 | import io.netty.channel.socket.nio.NioServerSocketChannel; 12 | import io.netty.util.internal.SystemPropertyUtil; 13 | import org.joda.time.DateTime; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.annotation.Value; 18 | import org.springframework.context.support.ClassPathXmlApplicationContext; 19 | 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | 23 | /** 24 | * Created by lrsec on 6/25/15. 25 | */ 26 | public class ImTcpServer { 27 | 28 | private static final int EXECUTOR_CORE_SIZE = 29 | Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); 30 | private static final int CONNECT_TIMEOUT_MILLIS = 3 * 1000; 31 | private static final int SO_TIMEOUT_MILLIS = 3000; 32 | 33 | private static Logger logger = LoggerFactory.getLogger(ImTcpServer.class); 34 | 35 | @Autowired 36 | private IServerDAO serverDAO; 37 | 38 | @Value("${im.port}") 39 | private int port; 40 | 41 | public void run(ClassPathXmlApplicationContext appContext) throws Exception{ 42 | logger.info("Im Tcp Server Started at {}", DateTime.now()); 43 | 44 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 45 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 46 | ScheduledExecutorService executorService = Executors.newScheduledThreadPool(EXECUTOR_CORE_SIZE); 47 | 48 | try { 49 | ServerBootstrap bootstrap = new ServerBootstrap(); 50 | 51 | bootstrap.group(bossGroup, workerGroup) 52 | .channel(NioServerSocketChannel.class) 53 | .childHandler(new IMChannelInitializer(appContext, executorService)) 54 | .option(ChannelOption.SO_BACKLOG, 128) 55 | .childOption(ChannelOption.SO_KEEPALIVE, true) 56 | // .childOption(ChannelOption.SO_TIMEOUT, SO_TIMEOUT_MILLIS) 57 | .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS) 58 | .childOption(ChannelOption.TCP_NODELAY, true); 59 | 60 | ChannelFuture future = bootstrap.bind(port).sync(); 61 | serverDAO.registerServer(); 62 | future.channel().closeFuture().sync(); 63 | } catch (Exception e) { 64 | logger.error("Error in Tcp Server", e); 65 | bossGroup.shutdownGracefully(); 66 | workerGroup.shutdownGracefully(); 67 | } 68 | 69 | logger.info("Im Tcp Server Stopped at {}", DateTime.now()); 70 | } 71 | 72 | public static void main(String[] args) { 73 | ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("spring/tcp.xml"); 74 | 75 | try { 76 | IConnectorMonitor connectorMonitorService = (IConnectorMonitor)appContext.getBean("connectorMonitor"); 77 | connectorMonitorService.start(); 78 | 79 | ImTcpServer server = (ImTcpServer)appContext.getBean("imTcpServer"); 80 | server.run(appContext); 81 | } catch (Exception e) { 82 | logger.error("Exception in starting ImTcpServer", e); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/constant/HandlerName.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.constant; 2 | 3 | /** 4 | * Created by lrsec on 7/9/15. 5 | */ 6 | public interface HandlerName { 7 | String HANDLER_DECODER = "im_msg_decoder"; 8 | String HANDLER_MSG = "im_msg_handler"; 9 | String HANDLER_IDLE = "im_idle_handler"; 10 | String HANDLER_AUTH = "im_auth_handler"; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/AES.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | 4 | import com.linbox.im.exceptions.IMException; 5 | import org.apache.commons.lang.StringUtils; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import javax.crypto.Cipher; 10 | import javax.crypto.spec.IvParameterSpec; 11 | import javax.crypto.spec.SecretKeySpec; 12 | import java.util.Base64; 13 | 14 | /** 15 | * Created by lrsec on 7/10/15. 16 | */ 17 | public class AES { 18 | private static Logger logger = LoggerFactory.getLogger(AES.class); 19 | 20 | // private static final String DEFAULT_PASSWORD = "medtree-im-passwmedtree-im-passw"; 21 | private static final String DEFAULT_PASSWORD = "medtree-im-passw"; 22 | public static final String CIPHER_ALGORITHM="AES/CBC/PKCS5Padding"; 23 | public static final String KEY_ALGORITHM="AES"; 24 | protected static final String STRING_PATTERN = "UTF-8"; 25 | 26 | private SecretKeySpec keypSpec; 27 | private Cipher cipher; 28 | private byte[] password; 29 | private byte[] iv; 30 | 31 | private Base64.Decoder base64Decoder = Base64.getDecoder(); 32 | 33 | public AES() { 34 | password = DEFAULT_PASSWORD.getBytes(); 35 | iv = generateIV(DEFAULT_PASSWORD); 36 | try { 37 | keypSpec = new SecretKeySpec(password ,KEY_ALGORITHM); 38 | cipher = Cipher.getInstance(CIPHER_ALGORITHM); 39 | } catch (Exception e) { 40 | logger.error("init AES fail", e); 41 | throw new IMException("init AES fail"); 42 | } 43 | } 44 | 45 | public void resetPassword(String pw) { 46 | if (StringUtils.isBlank(pw)) { 47 | logger.error("Get a empty password in resetPassword"); 48 | return; 49 | } 50 | 51 | this.password = base64Decoder.decode(pw); 52 | this.iv = generateIV(pw); 53 | this.keypSpec = new SecretKeySpec(this.password, KEY_ALGORITHM); 54 | 55 | logger.debug("Reset password: {}. Password size: {}", pw, this.password.length * 8); 56 | } 57 | 58 | public byte[] encrypt(String content) { 59 | byte[] result = null; 60 | 61 | try { 62 | byte[] byteContent = content.getBytes(STRING_PATTERN); 63 | cipher.init(Cipher.ENCRYPT_MODE, keypSpec, new IvParameterSpec(iv)); 64 | result = cipher.doFinal(byteContent); 65 | } catch (Exception e) { 66 | logger.error("encrypt AES fail", e); 67 | throw new IMException("encrypt AES fail"); 68 | } 69 | 70 | return result; 71 | } 72 | 73 | public String decrypt(byte[] content) { 74 | String result = null; 75 | 76 | try { 77 | cipher.init(Cipher.DECRYPT_MODE, keypSpec, new IvParameterSpec(iv)); 78 | result = new String(cipher.doFinal(content), STRING_PATTERN); 79 | } catch (Exception e) { 80 | logger.error("decrypt AES fail", e); 81 | throw new IMException("decrypt AES fail"); 82 | } 83 | 84 | return result; 85 | } 86 | 87 | private byte[] generateIV(String pw) { 88 | byte[] result = null; 89 | 90 | try { 91 | result = pw.substring(0, 16).getBytes(STRING_PATTERN); 92 | } catch (Exception e) { 93 | logger.error("Create AES IV error ", e); 94 | throw new IMException("Create AES IV error"); 95 | } 96 | 97 | return result; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/AuthHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.message.*; 5 | import com.linbox.im.server.connector.tcp.constant.HandlerName; 6 | import com.linbox.im.server.service.IOutboxService; 7 | import com.linbox.im.server.storage.dao.IServerDAO; 8 | import com.linbox.im.server.storage.dao.IUserDAO; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelFuture; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.ChannelInboundHandlerAdapter; 13 | import io.netty.util.concurrent.Future; 14 | import io.netty.util.concurrent.GenericFutureListener; 15 | import org.apache.commons.lang.StringUtils; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.context.support.ClassPathXmlApplicationContext; 19 | 20 | import java.net.InetSocketAddress; 21 | import java.util.Base64; 22 | import java.util.concurrent.ScheduledExecutorService; 23 | import java.util.concurrent.ScheduledFuture; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | /** 27 | * Created by lrsec on 6/28/15. 28 | */ 29 | public class AuthHandler extends ChannelInboundHandlerAdapter { 30 | 31 | private static Logger logger = LoggerFactory.getLogger(AuthHandler.class); 32 | 33 | private ScheduledExecutorService executor; 34 | private int loopRatio = 500; 35 | private int maxHandleTimeInMills = 5 * 1000; 36 | private ScheduledFuture task; 37 | 38 | private long rId = -1; 39 | private String userId = null; 40 | private AES aes = null; 41 | private Base64.Encoder base64Encoder = null; 42 | 43 | private IOutboxService outboxService; 44 | private IUserDAO userDAO; 45 | private IServerDAO serverDAO; 46 | 47 | public AuthHandler(ClassPathXmlApplicationContext applicationContext, ScheduledExecutorService executor, int loopRatio, int maxHandleTimeInMills, AES aes) { 48 | this.executor = executor; 49 | this.loopRatio = loopRatio; 50 | this.maxHandleTimeInMills = maxHandleTimeInMills; 51 | this.aes = aes; 52 | this.outboxService = (IOutboxService)applicationContext.getBean("outboxService"); 53 | this.userDAO = (IUserDAO) applicationContext.getBean("userDAO"); 54 | this.serverDAO = (IServerDAO) applicationContext.getBean("serverDAO"); 55 | this.base64Encoder = Base64.getEncoder(); 56 | } 57 | 58 | @Override 59 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 60 | logger.error("Exception in AuthHandler. Close the connection for user: " + StringUtils.trimToEmpty(userId), cause); 61 | if (rId >= 0) { 62 | sendFailResponse(ctx, 500, cause.getMessage()); 63 | } 64 | 65 | ctx.close(); 66 | } 67 | 68 | @Override 69 | public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { 70 | logger.debug("Received AuthRequest. Message: {}", JSON.toJSONString(msg)); 71 | 72 | MessageWrapper wrapper = (MessageWrapper) msg; 73 | 74 | if (wrapper == null) { 75 | logger.error("Get an null message wrapper in message handler"); 76 | ctx.close(); 77 | return; 78 | } 79 | 80 | RequestResponseType type = wrapper.type; 81 | 82 | if (type == null || type != RequestResponseType.AuthRequestMsg) { 83 | logger.error("The first message from client: {} is not AuthRequest. Close the connection. Message type is {}", ctx.channel().remoteAddress(), msg.getClass().getName()); 84 | ctx.close(); 85 | return; 86 | } 87 | 88 | AuthRequest authRequest = (AuthRequest) wrapper.content; 89 | rId = authRequest.rId; 90 | boolean authenticated = isCertified(authRequest); 91 | 92 | if (authenticated) { 93 | logger.debug("User {} authenticated.", userId); 94 | 95 | InetSocketAddress address = (InetSocketAddress)ctx.channel().localAddress(); 96 | String addressRecord = address.toString() + Long.toString(System.currentTimeMillis()); 97 | serverDAO.registerConnection(userId, addressRecord); 98 | logger.debug("Create connection for user: {}. Remote address: {}.", userId, ctx.channel().remoteAddress()); 99 | 100 | //TODO 动态密码的生成策略,在测试时关闭 101 | // resetPassword(ctx); 102 | 103 | task = executor.scheduleAtFixedRate(new SendMessageChecker(ctx.channel(), addressRecord), 0, loopRatio, TimeUnit.MILLISECONDS); 104 | 105 | IMMessageHandler imMsgHandler = (IMMessageHandler) ctx.pipeline().get(HandlerName.HANDLER_MSG); 106 | imMsgHandler.setUserId(userId); 107 | IMIdleStateHandler imIdleStateHandler = (IMIdleStateHandler) ctx.pipeline().get(HandlerName.HANDLER_IDLE); 108 | imIdleStateHandler.setUserId(userId); 109 | 110 | ctx.pipeline().remove(this); 111 | 112 | sendSuccessResponse(ctx); 113 | 114 | return; 115 | } else { 116 | logger.error("User {} is not certified. Close the connection.", userId); 117 | sendFailResponse(ctx, 401, "User is not certified"); 118 | 119 | ctx.close(); 120 | return; 121 | } 122 | } 123 | 124 | private boolean isCertified(AuthRequest request) { 125 | userId = request.userId; 126 | return userDAO.isUserValid(request.userId, request.token); 127 | } 128 | 129 | private void resetPassword(final ChannelHandlerContext ctx) { 130 | String password = serverDAO.getPassword(Long.parseLong(userId)); 131 | 132 | if (StringUtils.isBlank(password)) { 133 | logger.error("Can not get im password for user: {}", userId); 134 | sendFailResponse(ctx, 400, "Can not find user im password"); 135 | return; 136 | } else { 137 | aes.resetPassword(password); 138 | } 139 | } 140 | 141 | private class SendMessageChecker implements Runnable { 142 | private Logger logger = LoggerFactory.getLogger(SendMessageChecker.class); 143 | 144 | private volatile boolean isConnectionClosed = false; 145 | private Channel ch; 146 | private String linkRecord; 147 | 148 | public SendMessageChecker(Channel ch, String linkRecord) { 149 | this.ch = ch; 150 | this.linkRecord = linkRecord; 151 | } 152 | 153 | public void run() { 154 | long start = System.currentTimeMillis(); 155 | while ((System.currentTimeMillis() - start <= maxHandleTimeInMills)) { 156 | try { 157 | if(shouldClose()) { 158 | terminate(); 159 | return; 160 | } 161 | 162 | final String msg = outboxService.get(userId); 163 | 164 | if (StringUtils.isBlank(msg)) { 165 | return; 166 | } 167 | 168 | final MessageWrapper wrapper = JSON.parseObject(msg, MessageWrapper.class); 169 | 170 | ChannelFuture future = ch.writeAndFlush(wrapper); 171 | 172 | logger.debug("Tcp sender send message for {}. Message type: {}. Message body: {}", userId, wrapper.type, msg); 173 | 174 | future.addListener(new GenericFutureListener() { 175 | public void operationComplete(Future future) throws Exception { 176 | if (!future.isSuccess()) { 177 | logger.info("Network I/O write fail. Should close connection for user {}.", userId); 178 | isConnectionClosed = true; 179 | 180 | Throwable t = future.cause(); 181 | if (t != null) { 182 | logger.info("Send message fail", t); 183 | } 184 | } else { 185 | logger.debug("Tcp sender send message success. user: {}. Message type: {}. Message body: {}", userId, wrapper.type, msg); 186 | } 187 | } 188 | }); 189 | 190 | } catch (Exception e) { 191 | logger.error("Exception in sending task for user: " + userId + ". But the sending loop will continue to work", e); 192 | return; 193 | } 194 | } 195 | } 196 | 197 | private boolean shouldClose() { 198 | if (!ch.isActive()) { 199 | logger.warn("Connection for user {} is inactive, should terminate the sending task.", userId); 200 | return true; 201 | } 202 | 203 | if(isConnectionClosed) { 204 | logger.warn("Connection for user {} is closed, should terminate the sending task.", userId); 205 | return true; 206 | } 207 | 208 | String record = serverDAO.getConnection(userId); 209 | 210 | if(!StringUtils.equals(record, linkRecord)) { 211 | logger.warn("Connection is updated, should terminate the sending task for user {}. Local address {}. New connection address: {}", userId, StringUtils.trimToEmpty(linkRecord), StringUtils.trimToEmpty(record)); 212 | 213 | sendOfflineInfo(); 214 | return true; 215 | } 216 | 217 | return false; 218 | } 219 | 220 | private void terminate() { 221 | logger.info("Terminate sending task for user {}", userId); 222 | 223 | task.cancel(false); 224 | 225 | try { 226 | ch.close(); 227 | } catch (Exception e) { 228 | logger.error("Exception when terminate sending task", e); 229 | } 230 | } 231 | 232 | private void sendOfflineInfo() { 233 | OfflineInfo offlineInfo = new OfflineInfo(); 234 | offlineInfo.userId = userId; 235 | 236 | MessageWrapper wrapper = offlineInfo.toWrapper(); 237 | ch.writeAndFlush(wrapper); 238 | } 239 | } 240 | 241 | private void sendSuccessResponse(ChannelHandlerContext ctx) { 242 | AuthResponse response = new AuthResponse(); 243 | response.rId = rId; 244 | response.userId = userId; 245 | response.status = 200; 246 | response.sendTime = System.currentTimeMillis(); 247 | 248 | MessageWrapper responseWrapper = response.toWrapper(); 249 | ctx.channel().writeAndFlush(responseWrapper); 250 | } 251 | 252 | private void sendFailResponse(ChannelHandlerContext ctx, int status, String errCode) { 253 | 254 | AuthResponse response = new AuthResponse(); 255 | response.rId = rId; 256 | response.userId = userId; 257 | response.status = status; 258 | response.errMsg = errCode; 259 | response.sendTime = System.currentTimeMillis(); 260 | 261 | MessageWrapper responseWrapper = response.toWrapper(); 262 | ctx.channel().writeAndFlush(responseWrapper); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/IMChannelInitializer.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import com.linbox.im.server.connector.tcp.constant.HandlerName; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.channel.socket.SocketChannel; 7 | import io.netty.handler.timeout.IdleStateHandler; 8 | import org.springframework.context.support.ClassPathXmlApplicationContext; 9 | 10 | import java.util.concurrent.ScheduledExecutorService; 11 | 12 | /** 13 | * Created by lrsec on 7/19/15. 14 | */ 15 | public class IMChannelInitializer extends ChannelInitializer { 16 | private static final int SENDER_LOOP_RATIO_MILLIS = 500; 17 | private static final int MAX_HANDLE_TIME_MILLIS = 5 * 1000; 18 | private static final int IDLE_LOOP_IN_SEC = 60; 19 | 20 | private ClassPathXmlApplicationContext appContext; 21 | private ScheduledExecutorService executorService; 22 | 23 | public IMChannelInitializer (ClassPathXmlApplicationContext appContext, ScheduledExecutorService executorService) { 24 | this.appContext = appContext; 25 | this.executorService = executorService; 26 | } 27 | 28 | @Override 29 | protected void initChannel(SocketChannel sc) throws Exception { 30 | AES aes = new AES(); 31 | 32 | ChannelPipeline p = sc.pipeline(); 33 | 34 | p.addLast(HandlerName.HANDLER_DECODER, new IMMessageDecoder(aes)); 35 | 36 | p.addLast(HandlerName.HANDLER_AUTH, new AuthHandler(appContext,executorService, SENDER_LOOP_RATIO_MILLIS, MAX_HANDLE_TIME_MILLIS, aes)); 37 | p.addLast(HandlerName.HANDLER_MSG, new IMMessageHandler(appContext)); 38 | 39 | p.addLast(new IdleStateHandler(0, 0, IDLE_LOOP_IN_SEC)); 40 | p.addLast(HandlerName.HANDLER_IDLE, new IMIdleStateHandler()); 41 | 42 | p.addLast(new IMMessageEncoder(aes)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/IMIdleStateHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import io.netty.channel.ChannelDuplexHandler; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.timeout.IdleStateEvent; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * Created by lrsec on 7/8/15. 11 | */ 12 | public class IMIdleStateHandler extends ChannelDuplexHandler { 13 | 14 | private static Logger logger = LoggerFactory.getLogger(IMIdleStateHandler.class); 15 | private String userId = null; 16 | 17 | @Override 18 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 19 | if (evt instanceof IdleStateEvent) { 20 | IdleStateEvent e = (IdleStateEvent) evt; 21 | 22 | logger.info("Can not receive heartbeat from user {}. Close the connection.", userId); 23 | 24 | ctx.close(); 25 | } 26 | } 27 | 28 | void setUserId(String u) { 29 | userId = u; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/IMMessageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.message.MessageWrapper; 5 | import com.linbox.im.message.RequestResponseType; 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.handler.codec.ByteToMessageDecoder; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Created by lrsec on 6/29/15. 16 | */ 17 | public class IMMessageDecoder extends ByteToMessageDecoder{ 18 | private static Logger logger = LoggerFactory.getLogger(IMMessageDecoder.class); 19 | 20 | private short version = -1; 21 | private RequestResponseType requestResponseType = null; 22 | private int contentSize = -1; 23 | 24 | private AES aes; 25 | 26 | public IMMessageDecoder(AES aes) { 27 | this.aes = aes; 28 | } 29 | 30 | @Override 31 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { 32 | try { 33 | if (version < 0) { 34 | if (in.readableBytes() >= 2) { 35 | version = in.readShort(); 36 | 37 | if (version < 0) { 38 | logger.error("Unknow IM Message Version for {}", version); 39 | 40 | reset(); 41 | return; 42 | } 43 | 44 | logger.debug("Version: {}", version); 45 | } 46 | } 47 | 48 | if (requestResponseType == null) { 49 | if (in.readableBytes() >= 2) { 50 | int value = in.readShort(); 51 | requestResponseType = RequestResponseType.parse(value); 52 | 53 | if (requestResponseType == null) { 54 | logger.error("Unknown IM message type for {}.", value); 55 | 56 | reset(); 57 | return; 58 | } 59 | 60 | logger.debug("Request type: {}", requestResponseType.getName()); 61 | } else { 62 | return; 63 | } 64 | } 65 | 66 | if (requestResponseType != null && contentSize < 0) { 67 | if (in.readableBytes() >= 4) { 68 | contentSize = in.readInt(); 69 | 70 | if(contentSize <= 0) { 71 | logger.error("Get illegal IM message content size: {} for message type: {}", contentSize, requestResponseType.getName()); 72 | 73 | reset(); 74 | return; 75 | } 76 | 77 | logger.debug("Request type: {}. Content Length: {}", requestResponseType.getName(), contentSize); 78 | } else { 79 | return; 80 | } 81 | } 82 | 83 | if (requestResponseType != null && contentSize > 0) { 84 | if (in.readableBytes() >= contentSize) { 85 | 86 | try { 87 | byte[] buf = in.readBytes(contentSize).array(); 88 | 89 | String json = aes.decrypt(buf); 90 | 91 | Object message = JSON.parseObject(json, requestResponseType.getClazz()); 92 | 93 | if (message == null) { 94 | logger.error("Can not parse message content from json string. Message Type: {}. Message Content Size: {}. Message Content: {}.", requestResponseType.getName()); 95 | } else { 96 | logger.debug("Request type: {}. Content Length: {}. Request: {}", requestResponseType.getName(), contentSize, new String(buf)); 97 | 98 | MessageWrapper wrapper = new MessageWrapper(); 99 | wrapper.type = requestResponseType; 100 | wrapper.content = message; 101 | 102 | out.add(wrapper); 103 | } 104 | } catch (Exception e) { 105 | logger.error("Parse request body fail. ", e); 106 | } finally { 107 | reset(); 108 | } 109 | } 110 | 111 | return; 112 | } 113 | } catch (Exception e) { 114 | logger.error("", e); 115 | } 116 | } 117 | 118 | private void reset() { 119 | version = -1; 120 | requestResponseType = null; 121 | contentSize = -1; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/IMMessageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.message.MessageWrapper; 5 | import com.linbox.im.message.RequestResponseType; 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.handler.codec.MessageToByteEncoder; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** 13 | * Created by lrsec on 7/10/15. 14 | */ 15 | public class IMMessageEncoder extends MessageToByteEncoder { 16 | private static Logger logger = LoggerFactory.getLogger(IMMessageEncoder.class); 17 | 18 | private static short CURRENT_VERISON = 1; 19 | 20 | private AES aes; 21 | 22 | public IMMessageEncoder(AES aes) { 23 | this.aes = aes; 24 | } 25 | 26 | @Override 27 | protected void encode(ChannelHandlerContext channelHandlerContext, MessageWrapper messageWrapper, ByteBuf byteBuf) throws Exception { 28 | try { 29 | RequestResponseType type = messageWrapper.type; 30 | 31 | long encodeStart = System.currentTimeMillis(); 32 | 33 | byte[] content = aes.encrypt(JSON.toJSONString(messageWrapper.content)); 34 | 35 | long encodeEnd = System.currentTimeMillis(); 36 | 37 | byteBuf.writeShort(CURRENT_VERISON); 38 | byteBuf.writeShort(type.getValue()); 39 | byteBuf.writeInt(content.length); 40 | byteBuf.writeBytes(content); 41 | } catch (Exception e) { 42 | logger.error("", e); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/connector/tcp/handler/IMMessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.connector.tcp.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.message.*; 5 | import com.linbox.im.message.system.SyncSystemUnreadRequest; 6 | import com.linbox.im.server.constant.MessageTopic; 7 | import com.linbox.im.server.monitor.IConnectorMonitor; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | import org.apache.commons.lang.StringUtils; 11 | import org.apache.kafka.clients.producer.KafkaProducer; 12 | import org.apache.kafka.clients.producer.ProducerRecord; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.context.support.ClassPathXmlApplicationContext; 16 | 17 | /** 18 | * Created by lrsec on 6/25/15. 19 | */ 20 | public class IMMessageHandler extends ChannelInboundHandlerAdapter { 21 | 22 | private static Logger logger = LoggerFactory.getLogger(IMMessageHandler.class); 23 | private String userId = null; 24 | 25 | private IConnectorMonitor connectorMonitorService; 26 | private KafkaProducer kafkaProducer; 27 | 28 | public IMMessageHandler(ClassPathXmlApplicationContext appContext) { 29 | connectorMonitorService = (IConnectorMonitor)appContext.getBean("connectorMonitor"); 30 | kafkaProducer = (KafkaProducer)appContext.getBean("kafkaProducer"); 31 | } 32 | 33 | @Override 34 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 35 | logger.debug("Active IMMessageHandler"); 36 | connectorMonitorService.incrConnCount(); 37 | } 38 | 39 | @Override 40 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 41 | logger.debug("Inactive IMMessageHandler"); 42 | connectorMonitorService.decConnCount(); 43 | } 44 | 45 | @Override 46 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 47 | MessageWrapper wrapper = (MessageWrapper) msg; 48 | 49 | if (wrapper == null) { 50 | logger.error("Get an null message wrapper in message handler"); 51 | return; 52 | } 53 | 54 | RequestResponseType type = wrapper.type; 55 | 56 | if (type == null) { 57 | logger.error("Illegal message type for user: {}. Message type: {}", userId, msg.getClass().getName()); 58 | return; 59 | } 60 | 61 | String json = JSON.toJSONString(wrapper); 62 | 63 | logger.debug("Handle {}. Message content: {}", type.getName(), json); 64 | switch(type) { 65 | case SyncUnreadRequestMsg: 66 | 67 | if (!StringUtils.equalsIgnoreCase(userId, ((SyncUnreadRequest)wrapper.content).userId)) { 68 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 69 | return; 70 | } 71 | 72 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_SYNC_UNREAD, json)); 73 | break; 74 | 75 | case ReadAckRequestMsg: 76 | 77 | if (!StringUtils.equalsIgnoreCase(userId, ((ReadAckRequest)wrapper.content).userId)) { 78 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 79 | return; 80 | } 81 | 82 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_READ_ACK, json)); 83 | break; 84 | 85 | case PullOldMsgRequestMsg: 86 | 87 | if (!StringUtils.equalsIgnoreCase(userId, ((PullOldMsgRequest)wrapper.content).userId)) { 88 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 89 | return; 90 | } 91 | 92 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_PULL_OLD_MEG, json)); 93 | break; 94 | 95 | case SendMsgRequestMsg: 96 | 97 | if (!StringUtils.equalsIgnoreCase(userId, ((SendMsgRequest)wrapper.content).userId)) { 98 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 99 | return; 100 | } 101 | 102 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_SEND_MSG,json)); 103 | break; 104 | 105 | case Ping: 106 | 107 | if (!StringUtils.equalsIgnoreCase(userId, ((Ping)wrapper.content).userId)) { 108 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 109 | return; 110 | } 111 | 112 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_PING, json)); 113 | break; 114 | 115 | case SyncSystemUnreadRequestMsg: 116 | 117 | if (!StringUtils.equalsIgnoreCase(userId, ((SyncSystemUnreadRequest)wrapper.content).userId)) { 118 | logger.error("The chat id is not match for {}. Authenticated user id: {}. Message content: {}", type.getName(), userId, json); 119 | return; 120 | } 121 | 122 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_SYNC_SYSTEM_UNREAD, json)); 123 | break; 124 | 125 | default: 126 | logger.error("{} is not handled right now. Message content: {}", type.getName(), JSON.toJSONString(msg)); 127 | } 128 | } 129 | 130 | @Override 131 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 132 | logger.error("", cause); 133 | } 134 | 135 | void setUserId(String u) { 136 | userId = u; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/constant/MessageTopic.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.constant; 2 | 3 | /** 4 | * Created by lrsec on 7/1/15. 5 | */ 6 | public interface MessageTopic { 7 | String TOPIC_SYNC_UNREAD = "IMSyncUnread"; 8 | String TOPIC_READ_ACK = "IMReadAck"; 9 | String TOPIC_PULL_OLD_MEG = "IMPullOldMsg"; 10 | String TOPIC_SEND_MSG = "IMSendMsg"; 11 | String TOPIC_PING = "IMPing"; 12 | String TOPIC_SYNC_SYSTEM_UNREAD = "IMSyncSysUnread"; 13 | 14 | String TOPIC_DISPATCH_SEND_SINGLE = "IMSendSingleDispatch"; 15 | String TOPIC_DISPATCH_SEND_GROUP = "IMSendGroupDispatch"; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/constant/RedisKey.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.constant; 2 | 3 | import com.google.common.base.CharMatcher; 4 | import org.apache.commons.lang.StringUtils; 5 | 6 | /** 7 | * Created by lrsec on 7/1/15. 8 | */ 9 | public class RedisKey { 10 | 11 | public static final String CONNECT_REGISTRY = "im:connection:registry"; 12 | public static final String IP_REGISTRY = "im:ip"; 13 | public static final String GROUP_ID = "im:group:id"; 14 | public static final String IM_USER_WHITE_LIST = "im:user:white:list"; 15 | 16 | public static String getInboxKey(String id) { 17 | return "im:inbox:" + StringUtils.trim(id); 18 | } 19 | 20 | public static String getFriendRequestsKey(long userId){ 21 | return "unread_count:"+userId+":friend_requests"; 22 | } 23 | 24 | public static String getUserUnReadCountsKey(long userId) { 25 | return "unread_count:"+userId; 26 | } 27 | 28 | public static String getUnreadMessageNotifyKey(long userID) { 29 | return "message:notify:" + userID; 30 | } 31 | 32 | public static String getIMPassword(long userId) { 33 | return "im:password:" + Long.toString(userId); 34 | } 35 | 36 | public static String getMsgIDKey(String key) { 37 | return "im:message:id:" + CharMatcher.WHITESPACE.trimFrom(key); 38 | } 39 | 40 | public static String getOutboxKey (String id) { 41 | return "im:outbox:" + CharMatcher.WHITESPACE.trimFrom(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/constant/SpecialUsers.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.constant; 2 | 3 | /** 4 | * Created by lrsec on 9/8/15. 5 | */ 6 | public class SpecialUsers { 7 | public static final String SysIMManager = "10"; 8 | public static final String SysIMManagerChatId = "10"; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/monitor/IConnectorMonitor.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.monitor; 2 | 3 | /** 4 | * Created by lrsec on 7/26/15. 5 | */ 6 | public interface IConnectorMonitor { 7 | void incrConnCount(); 8 | 9 | void decConnCount(); 10 | 11 | void start(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/monitor/impl/ConnectorMonitor.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.monitor.impl; 2 | 3 | import com.linbox.im.server.monitor.IConnectorMonitor; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicLong; 12 | 13 | /** 14 | * Created by lrsec on 7/26/15. 15 | */ 16 | public class ConnectorMonitor implements IConnectorMonitor { 17 | private static Logger logger = LoggerFactory.getLogger(ConnectorMonitor.class); 18 | 19 | private final AtomicLong totalConnections; 20 | private ScheduledExecutorService executorService; 21 | 22 | @Value("${monitor.duration.second}") 23 | private int duration; 24 | 25 | public ConnectorMonitor() { 26 | totalConnections = new AtomicLong(0); 27 | executorService = Executors.newSingleThreadScheduledExecutor(); 28 | } 29 | 30 | @Override 31 | public void incrConnCount() { 32 | long count = totalConnections.incrementAndGet(); 33 | logger.debug("Connection increase to {}", count); 34 | } 35 | 36 | @Override 37 | public void decConnCount() { 38 | long count = totalConnections.decrementAndGet(); 39 | logger.debug("Connection decrease to {}", count); 40 | } 41 | 42 | @Override 43 | public void start() { 44 | logger.info("Start connection monitor"); 45 | 46 | executorService.scheduleAtFixedRate(new Runnable() { 47 | @Override 48 | public void run() { 49 | try { 50 | long connections = totalConnections.get(); 51 | sendConnCount(connections); 52 | } catch (Exception e) { 53 | logger.error("", e); 54 | } 55 | } 56 | }, duration, duration, TimeUnit.SECONDS); 57 | } 58 | 59 | private void sendConnCount(long count) { 60 | logger.info("IM Monitor: Current Total Connection Count: {}", count); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/ImRouterServer.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router; 2 | 3 | import com.linbox.im.exceptions.IMConsumerException; 4 | import com.linbox.im.server.constant.MessageTopic; 5 | import com.linbox.im.server.router.handlers.Handler; 6 | import org.apache.kafka.clients.consumer.ConsumerRecord; 7 | import org.apache.kafka.clients.consumer.ConsumerRecords; 8 | import org.apache.kafka.clients.consumer.KafkaConsumer; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Qualifier; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.context.support.ClassPathXmlApplicationContext; 15 | 16 | import java.util.Arrays; 17 | import java.util.Properties; 18 | import java.util.concurrent.ExecutorService; 19 | import java.util.concurrent.Executors; 20 | 21 | /** 22 | * Created by lrsec on 7/2/15. 23 | */ 24 | public class ImRouterServer { 25 | private static Logger logger = LoggerFactory.getLogger(ImRouterServer.class); 26 | 27 | @Value("${bootstrap.servers}") 28 | private String bootstrapServers; 29 | 30 | @Value("${group.id}") 31 | private String groupId; 32 | 33 | @Value("${enable.auto.commit}") 34 | private String autoCommit; 35 | 36 | @Value("${auto.commit.interval.ms}") 37 | private String autoCommitInterval; 38 | 39 | @Value("${session.timeout.ms}") 40 | private String sessionTimeoutMs; 41 | 42 | @Value("${key.deserializer}") 43 | private String keyDeserializer; 44 | 45 | @Value("${value.deserializer}") 46 | private String valueDeserializer; 47 | 48 | @Autowired 49 | @Qualifier("syncUnreadHandler") 50 | private Handler syncUnreadHandler; 51 | 52 | @Autowired 53 | @Qualifier("readAckHandler") 54 | private Handler readAckHandler; 55 | 56 | @Autowired 57 | @Qualifier("pullOldMsgHandler") 58 | private Handler pullOldMsgHandler; 59 | 60 | @Autowired 61 | @Qualifier("sendMsgHandler") 62 | private Handler sendMsgHandler; 63 | 64 | @Autowired 65 | @Qualifier("pingHandler") 66 | private Handler pingHandler; 67 | 68 | @Autowired 69 | @Qualifier("syncSystemUnreadHandler") 70 | private Handler syncSystemUnreadHandler; 71 | 72 | @Autowired 73 | @Qualifier("dispatchToSingleHandler") 74 | private Handler dispatchToSingleHandler; 75 | 76 | @Autowired 77 | @Qualifier("dispatchToGroupHandler") 78 | private Handler dispatchToGroupHandler; 79 | 80 | public void run() { 81 | Properties props = new Properties(); 82 | props.put("bootstrap.servers", bootstrapServers); 83 | props.put("group.id", groupId); 84 | props.put("enable.auto.commit", autoCommit); 85 | props.put("auto.commit.interval.ms", autoCommitInterval); 86 | props.put("session.timeout.ms", sessionTimeoutMs); 87 | props.put("key.deserializer", keyDeserializer); 88 | props.put("value.deserializer", valueDeserializer); 89 | 90 | 91 | logger.info("Start IM Router Server"); 92 | 93 | logger.info("Start Sync Unread Handler"); 94 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_SYNC_UNREAD, syncUnreadHandler)).start(); 95 | 96 | logger.info("Start Read ACK Handler"); 97 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_READ_ACK, readAckHandler)).start(); 98 | 99 | logger.info("Start Pull Old Msg Handler"); 100 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_PULL_OLD_MEG, pullOldMsgHandler)).start(); 101 | 102 | logger.info("Start Send Msg Handler"); 103 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_SEND_MSG, sendMsgHandler)).start(); 104 | 105 | logger.info("Start Ping Msg Handler"); 106 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_PING, pingHandler)).start(); 107 | 108 | logger.info("Start Sync System Unread Handler"); 109 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_SYNC_SYSTEM_UNREAD, syncSystemUnreadHandler)).start(); 110 | 111 | logger.debug("Start Single Send Dispatch Consumer"); 112 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_DISPATCH_SEND_SINGLE, dispatchToSingleHandler)).start(); 113 | 114 | logger.debug("Start Group Send Dispatch Consumer"); 115 | new Thread(new ConsumerTask(props, MessageTopic.TOPIC_DISPATCH_SEND_GROUP, dispatchToGroupHandler)).start(); 116 | } 117 | 118 | private static class ConsumerTask implements Runnable{ 119 | private String topic; 120 | private Properties props; 121 | private Handler handler; 122 | private ExecutorService executorService = Executors.newFixedThreadPool(5); 123 | 124 | ConsumerTask(Properties props, String topic, Handler handler) { 125 | this.topic = topic; 126 | this.props = props; 127 | this.handler = handler; 128 | } 129 | 130 | @Override 131 | public void run() { 132 | logger.info("Thread for {} consumer started", topic); 133 | 134 | KafkaConsumer consumer = new KafkaConsumer(props); 135 | consumer.subscribe(Arrays.asList(topic)); 136 | 137 | while (true) { 138 | ConsumerRecords records = consumer.poll(100); 139 | for (ConsumerRecord record : records) { 140 | try { 141 | executorService.submit(new Runnable() { 142 | @Override 143 | public void run() { 144 | handler.handle(record); 145 | } 146 | }); 147 | } catch (IMConsumerException e1) { 148 | logger.error("IMConsumerException in consumer for Topic: " + topic + ". Message: + " + e1.getTopicMessage() + ".", e1); 149 | } catch (Exception e2) { 150 | logger.error("Exception in consumer for topic: " + topic, e2); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | public static void main(String[] args) { 158 | try { 159 | ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("spring/router.xml"); 160 | 161 | ImRouterServer server = (ImRouterServer) appContext.getBean("imRouterServer"); 162 | server.run(); 163 | } catch (Exception e) { 164 | logger.error("Exception in im router server.", e); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/Handler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | 5 | /** 6 | * Created by lrsec on 12/15/15. 7 | */ 8 | public interface Handler { 9 | void handle(ConsumerRecord record); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/PingHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.message.MessageWrapper; 7 | import com.linbox.im.message.Ping; 8 | import com.linbox.im.message.Pong; 9 | import com.linbox.im.server.service.IOutboxService; 10 | import org.apache.commons.lang.StringUtils; 11 | import org.apache.kafka.clients.consumer.ConsumerRecord; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.stereotype.Service; 17 | 18 | /** 19 | * Created by lrsec on 7/7/15. 20 | */ 21 | @Service 22 | @Qualifier("pingHandler") 23 | public class PingHandler implements Handler { 24 | private static Logger logger = LoggerFactory.getLogger(PingHandler.class); 25 | 26 | @Autowired 27 | private IOutboxService outboxService; 28 | 29 | 30 | @Override 31 | public void handle(ConsumerRecord record) { 32 | String json = record.value(); 33 | 34 | try { 35 | logger.debug("Start handling Ping: {}", json); 36 | 37 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 38 | 39 | Ping ping = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), Ping.class); 40 | wrapper.content = ping; 41 | 42 | String userId = ping.userId; 43 | if(StringUtils.isBlank(userId)) { 44 | logger.error("Can not find avaiable user id for PullOldMsgRequest {}", json); 45 | return; 46 | } 47 | 48 | sendPong(userId, wrapper); 49 | } catch (Exception e) { 50 | throw new IMConsumerException(e, json); 51 | } 52 | } 53 | 54 | private void sendPong(String userId, MessageWrapper wrapper) { 55 | Pong pong = new Pong(); 56 | pong.rId = ((Ping)(wrapper.content)).rId; 57 | outboxService.put(userId, pong.toWrapperJson()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/PullOldMsgHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.exceptions.IMException; 7 | import com.linbox.im.message.*; 8 | import com.linbox.im.server.service.IInboxService; 9 | import com.linbox.im.server.service.IOutboxService; 10 | import com.linbox.im.server.storage.dao.IGroupMessageDAO; 11 | import com.linbox.im.server.storage.dao.ISessionMessageDAO; 12 | import com.linbox.im.utils.IMUtils; 13 | import com.linbox.im.server.storage.dao.IGroupDAO; 14 | import com.linbox.im.server.storage.entity.GroupMessageEntity; 15 | import com.linbox.im.server.storage.entity.SessionMessageEntity; 16 | import org.apache.commons.lang.StringUtils; 17 | import org.apache.kafka.clients.consumer.ConsumerRecord; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.beans.factory.annotation.Qualifier; 22 | import org.springframework.stereotype.Service; 23 | 24 | import java.util.LinkedList; 25 | import java.util.List; 26 | 27 | /** 28 | * Created by lrsec on 7/2/15. 29 | */ 30 | @Service 31 | @Qualifier("pullOldMsgHandler") 32 | public class PullOldMsgHandler implements Handler { 33 | public static final Logger logger = LoggerFactory.getLogger(PullOldMsgHandler.class); 34 | 35 | @Autowired 36 | private IOutboxService outboxService; 37 | 38 | @Autowired 39 | private ISessionMessageDAO sessionMessageDAO; 40 | 41 | @Autowired 42 | private IInboxService inboxService; 43 | 44 | @Autowired 45 | private IGroupMessageDAO groupMessageDAO; 46 | 47 | @Override 48 | public void handle(ConsumerRecord record) { 49 | String json = record.value(); 50 | 51 | try { 52 | logger.debug("start handling PullOldMsgRequest: {}", json); 53 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 54 | 55 | PullOldMsgRequest request = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), PullOldMsgRequest.class); 56 | wrapper.content = request; 57 | 58 | boolean shouldCleanUnread = false; 59 | if (request.maxMsgId == PullOldMsgRequest.MAX) { 60 | request.maxMsgId = Long.MAX_VALUE; 61 | shouldCleanUnread = true; 62 | } 63 | 64 | if (request.minMsgId < 0) { 65 | request.minMsgId = 0; 66 | } 67 | 68 | String userId = request.userId; 69 | if (StringUtils.isBlank(userId)) { 70 | logger.error("Can not find avaiable user id for PullOldMsgRequest {}", json); 71 | return; 72 | } 73 | 74 | try { 75 | MessageType type = MessageType.parse(request.type); 76 | if (type == null) { 77 | logger.error("Can not parse MessageType in PullOldMsgRequest {}", json); 78 | 79 | throw new IMException("Unknown MessageType " + Integer.toString(request.type)); 80 | } 81 | 82 | List msgs = new LinkedList(); 83 | 84 | switch (type) { 85 | case Session: 86 | 87 | String remoteId = request.remoteId; 88 | if (StringUtils.isBlank(remoteId)) { 89 | logger.error("Can not find corresponding remote id for PullOldMsgRequest: {}", json); 90 | 91 | throw new IMException("Cannot find remote id for PullOldMsgRequest: " + json); 92 | } 93 | 94 | String sessionId = IMUtils.getSessionKey(userId, remoteId); 95 | List sessionDaos = sessionMessageDAO.findMsg(sessionId, request.maxMsgId, request.minMsgId, request.limit); 96 | 97 | for (SessionMessageEntity dao : sessionDaos) { 98 | Message m = Message.convertToMsg(dao, userId); 99 | msgs.add(m); 100 | } 101 | 102 | if (shouldCleanUnread && !msgs.isEmpty()) { 103 | inboxService.removeSessionMsg(userId, IMUtils.getSessionKey(userId, remoteId), msgs.get(0).msgId); 104 | } 105 | 106 | break; 107 | case Group: 108 | 109 | String groupId = request.groupId; 110 | if (StringUtils.isBlank(groupId)) { 111 | logger.error("Can not find corresponding groupId id for PullOldMsgRequest: {}", json); 112 | 113 | throw new IMException("Cannot find groupId id for PullOldMsgRequest: " + json); 114 | } 115 | 116 | List groupDaos = groupMessageDAO.findMsg(groupId, request.maxMsgId, request.minMsgId, request.limit); 117 | 118 | for (GroupMessageEntity dao : groupDaos) { 119 | 120 | String fromUserId = Long.toString(dao.FromUserID); 121 | Message m = Message.convertToMsg(dao, fromUserId, groupId); 122 | 123 | msgs.add(m); 124 | } 125 | 126 | if (shouldCleanUnread && !msgs.isEmpty()) { 127 | inboxService.removeGroupMsg(userId, groupId, msgs.get(0).msgId); 128 | } 129 | 130 | break; 131 | default: 132 | logger.error("Message type {} is not handled in SyncUnreadCallback. PullOldMsgRequest: {}", type.getValue(), json); 133 | throw new IMException("Unhandled MessageType " + type.getValue() + " for PullOldMsgCallback"); 134 | } 135 | 136 | sendSuccessResponse(userId, request, msgs); 137 | 138 | } catch (Exception e) { 139 | logger.error("Pull old message handler fail with exception. Send fail response. Message: " + json, e); 140 | sendFailResponse(userId, request, e.getMessage()); 141 | } 142 | } catch (Exception e) { 143 | throw new IMConsumerException(e, json); 144 | } 145 | } 146 | 147 | private void sendSuccessResponse(String userId, PullOldMsgRequest request, List result) { 148 | PullOldMsgResponse response = new PullOldMsgResponse(); 149 | response.rId = request.rId; 150 | response.userId = request.userId; 151 | response.remoteId = request.remoteId; 152 | response.groupId = request.groupId; 153 | response.msgs = result.toArray(new Message[0]); 154 | response.type = request.type; 155 | response.maxMsgIdInRequest = (request.maxMsgId == Long.MAX_VALUE) ? -1 : request.maxMsgId; 156 | response.requestType = request.requestType; 157 | response.status = 200; 158 | outboxService.put(userId, response.toWrapperJson()); 159 | } 160 | 161 | private void sendFailResponse(String userId, PullOldMsgRequest request, String errMsg) { 162 | PullOldMsgResponse errResp = new PullOldMsgResponse(); 163 | 164 | errResp.rId = request.rId; 165 | errResp.userId = request.userId; 166 | errResp.remoteId = request.remoteId; 167 | errResp.groupId = request.groupId; 168 | 169 | errResp.msgs = new Message[0]; 170 | errResp.type = request.type; 171 | errResp.maxMsgIdInRequest = (request.maxMsgId == Long.MAX_VALUE) ? -1 : request.maxMsgId; 172 | errResp.requestType = request.requestType; 173 | errResp.status = 500; 174 | errResp.errMsg = errMsg; 175 | 176 | outboxService.put(userId, errResp.toWrapperJson()); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/ReadAckHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.exceptions.IMException; 7 | import com.linbox.im.message.MessageType; 8 | import com.linbox.im.server.service.IOutboxService; 9 | import com.linbox.im.message.MessageWrapper; 10 | import com.linbox.im.message.ReadAckRequest; 11 | import com.linbox.im.message.ReadAckResponse; 12 | import com.linbox.im.server.service.IInboxService; 13 | import com.linbox.im.utils.IMUtils; 14 | import org.apache.commons.lang.StringUtils; 15 | import org.apache.kafka.clients.consumer.ConsumerRecord; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.beans.factory.annotation.Qualifier; 20 | import org.springframework.stereotype.Service; 21 | 22 | /** 23 | * Created by lrsec on 7/2/15. 24 | */ 25 | @Service 26 | @Qualifier("readAckHandler") 27 | public class ReadAckHandler implements Handler { 28 | private static Logger logger = LoggerFactory.getLogger(ReadAckHandler.class); 29 | 30 | @Autowired 31 | private IInboxService inboxService; 32 | 33 | @Autowired 34 | private IOutboxService outboxService; 35 | 36 | @Override 37 | public void handle(ConsumerRecord record) { 38 | String json = record.value(); 39 | 40 | try { 41 | logger.debug("Start handling ReadAckRequest: {}", json); 42 | 43 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 44 | 45 | ReadAckRequest request = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), ReadAckRequest.class); 46 | wrapper.content = request; 47 | 48 | String userId = request.userId; 49 | if(StringUtils.isBlank(userId)) { 50 | logger.error("Can not find avaiable user id for ReadAckRequest {}", json); 51 | return; 52 | } 53 | 54 | try { 55 | MessageType type = MessageType.parse(request.type); 56 | if (type == null) { 57 | logger.error("Can not parse MessageType in ReadAckRequest {}", json); 58 | 59 | throw new IMException("Unknown MessageType " + Integer.toString(request.type)); 60 | } 61 | 62 | switch(type) { 63 | case Session: 64 | String remoteId = request.remoteId; 65 | 66 | if (StringUtils.isBlank(remoteId)) { 67 | logger.error("Can not find remote id for ReadAckRequest {}", json); 68 | 69 | throw new IMException("Non existing ReadAckRequest " + json); 70 | } 71 | 72 | inboxService.removeSessionMsg(userId, IMUtils.getSessionKey(userId, remoteId), request.msgId); 73 | 74 | sendSuccessResponse(userId, request); 75 | 76 | break; 77 | case Group: 78 | String groupId = request.groupId; 79 | 80 | if (StringUtils.isBlank(groupId)) { 81 | logger.error("Can not find group id for ReadAckRequest {}", json); 82 | 83 | throw new IMException("Non existing ReadAckRequest " + json); 84 | } 85 | 86 | inboxService.removeGroupMsg(userId, groupId, request.msgId); 87 | 88 | sendSuccessResponse(userId, request); 89 | 90 | break; 91 | default: 92 | logger.error("Message type {} is not handled in ReadAckCallback. ReadAckRequest: {}", type.getValue(), json); 93 | throw new IMException("Unhandled MessageType " + type.getValue() + " for ReadAckCallback"); 94 | } 95 | 96 | } catch (Exception e) { 97 | logger.error("Exception when handling ReadAckRequest.", e); 98 | 99 | sendFailResponse(userId, request, e.getMessage()); 100 | } 101 | 102 | } catch (Exception e) { 103 | throw new IMConsumerException(e, json); 104 | } 105 | } 106 | 107 | private void sendSuccessResponse(String userId, ReadAckRequest request) { 108 | 109 | ReadAckResponse response = new ReadAckResponse(); 110 | response.rId = request.rId; 111 | response.userId = request.userId; 112 | response.remoteId = request.remoteId; 113 | response.groupId = request.groupId; 114 | response.type = request.type; 115 | response.status = 200; 116 | 117 | outboxService.put(userId, response.toWrapperJson()); 118 | } 119 | 120 | private void sendFailResponse(String userId, ReadAckRequest request, String errMsg) { 121 | 122 | ReadAckResponse response = new ReadAckResponse(); 123 | response.rId = request.rId; 124 | response.userId = request.userId; 125 | response.remoteId = request.remoteId; 126 | response.groupId = request.groupId; 127 | response.type = request.type; 128 | response.status = 500; 129 | response.errCode = errMsg; 130 | 131 | outboxService.put(userId, response.toWrapperJson()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/SendMsgHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.exceptions.IMException; 7 | import com.linbox.im.message.*; 8 | import com.linbox.im.server.router.handlers.dispatcher.SendDispatcher; 9 | import com.linbox.im.server.service.IOutboxService; 10 | import com.linbox.im.server.storage.dao.IGroupMessageDAO; 11 | import com.linbox.im.server.storage.dao.IMessageDAO; 12 | import com.linbox.im.server.storage.dao.ISessionMessageDAO; 13 | import com.linbox.im.server.storage.entity.SessionMessageEntity; 14 | import com.linbox.im.utils.IMUtils; 15 | import com.linbox.im.server.storage.entity.GroupMessageEntity; 16 | import org.apache.commons.lang.StringUtils; 17 | import org.apache.kafka.clients.consumer.ConsumerRecord; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.beans.factory.annotation.Qualifier; 22 | import org.springframework.stereotype.Service; 23 | 24 | /** 25 | * Created by lrsec on 7/2/15. 26 | */ 27 | @Service 28 | @Qualifier("sendMsgHandler") 29 | public class SendMsgHandler implements Handler { 30 | private static Logger logger = LoggerFactory.getLogger(SendMsgHandler.class); 31 | 32 | @Autowired 33 | private ISessionMessageDAO sessionMessageDAO; 34 | 35 | @Autowired 36 | private IGroupMessageDAO groupMessageDAO; 37 | 38 | @Autowired 39 | private IMessageDAO messageDAO; 40 | 41 | @Autowired 42 | private IOutboxService outboxService; 43 | 44 | @Autowired 45 | private SendDispatcher dispatcher; 46 | 47 | @Override 48 | public void handle(ConsumerRecord record) { 49 | String json = record.value(); 50 | 51 | try { 52 | logger.debug("Start handling SendMsgRequest: {}", json); 53 | 54 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 55 | 56 | SendMsgRequest request = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), SendMsgRequest.class); 57 | wrapper.content = request; 58 | 59 | if(request == null) { 60 | logger.error("SendMsgRequest format is not correct. Json string: {}", json); 61 | return; 62 | } 63 | 64 | String userId = request.userId; 65 | if(StringUtils.isBlank(userId)) { 66 | logger.error("Can not find avaiable user id for SendMsgRequest {}", json); 67 | return; 68 | } 69 | 70 | try { 71 | MessageType type = MessageType.parse(request.type); 72 | if (type == null) { 73 | logger.error("Can not parse MessageType in SendMsgRequest {}", json); 74 | 75 | throw new IMException("Unknown MessageType " + Integer.toString(request.type)); 76 | } 77 | 78 | switch(type) { 79 | case Session: 80 | 81 | String remoteId = request.remoteId; 82 | if (StringUtils.isBlank(remoteId)) { 83 | logger.error("Can not find corresponding remote id for SendMsgRequest: {}", json); 84 | 85 | throw new IMException("Cannot find remote id for SendMsgRequest: " + json); 86 | } 87 | 88 | Message body = request.msg; 89 | if (body == null) { 90 | logger.error("No msg in SendMsgRequest: {}", json); 91 | 92 | throw new IMException("Empty message between " + userId + " and " + remoteId + ". rid: " + request.rId); 93 | } 94 | 95 | SessionMessageEntity dao = sessionMessageDAO.findMsgByRId(body.rId, body.fromUserId, body.toUserId); 96 | if (dao == null) { 97 | String sessionKey = IMUtils.getSessionKey(userId, remoteId); 98 | 99 | body.msgId = messageDAO.generateMsgId(sessionKey); 100 | body.sendTime = System.currentTimeMillis(); 101 | dao = sessionMessageDAO.insert(body); 102 | 103 | logger.debug("Save SessionMessageDao into DB. Message: {}. Dao: {}", JSON.toJSONString(body), JSON.toJSONString(dao)); 104 | 105 | dispatcher.dispatchToSingle(remoteId, userId, sessionKey, MessageType.Session, body); 106 | 107 | } else { 108 | logger.debug("Find existing SessionMessageDao for message: {}. Dao: {}", JSON.toJSONString(body), JSON.toJSONString(dao)); 109 | } 110 | 111 | sendSuccessResponse(userId, request, dao); 112 | 113 | break; 114 | case Group: 115 | 116 | String groupId = request.groupId; 117 | if (StringUtils.isBlank(groupId)) { 118 | logger.error("Can not find corresponding group id for SendMsgRequest: {}", json); 119 | 120 | throw new IMException("Cannot find group id for SendMsgRequest: " + json); 121 | } 122 | 123 | Message groupMsgBody = request.msg; 124 | if (groupMsgBody == null) { 125 | logger.error("No msg in SendMsgRequest: {}", json); 126 | 127 | throw new IMException("Empty message between " + userId + " and group " + groupId + ". rid: " + request.rId); 128 | } 129 | 130 | GroupMessageEntity groupMsgDao = groupMessageDAO.findMsgByRId(groupMsgBody.rId, groupMsgBody.fromUserId, groupMsgBody.groupId); 131 | if (groupMsgDao == null) { 132 | groupMsgBody.msgId = messageDAO.generateMsgId(groupId); 133 | groupMsgBody.sendTime = System.currentTimeMillis(); 134 | groupMsgDao = groupMessageDAO.insert(groupMsgBody); 135 | 136 | logger.debug("Save GroupMessageDao into DB. Message: {}. Dao: {}", JSON.toJSONString(groupMsgBody), JSON.toJSONString(groupMsgDao)); 137 | 138 | dispatcher.dispatchToGroup(groupId, groupMsgBody); 139 | 140 | } else { 141 | logger.debug("Find existing GroupMessageDao for message: {}. Dao: {}", JSON.toJSONString(groupMsgBody), JSON.toJSONString(groupMsgDao)); 142 | } 143 | 144 | sendSuccessResponse(userId, request, groupMsgDao); 145 | 146 | break; 147 | default: 148 | logger.error("Message type {} is not handled in SendMsgRequest. SendMsgRequest: {}", type.getValue(), json); 149 | throw new IMException("Unhandled MessageType " + type.getValue() + " for SendMsgCallback"); 150 | } 151 | } catch (Exception e) { 152 | logger.error("Exception when handling SendMsgRequest.", e); 153 | sendFailResponse(userId, request, e.getMessage()); 154 | } 155 | 156 | } catch (Exception e) { 157 | throw new IMConsumerException(e, json); 158 | } 159 | } 160 | 161 | private void sendSuccessResponse(String userId, SendMsgRequest request, SessionMessageEntity sessionEntity) { 162 | SendMsgResponse resp = new SendMsgResponse(); 163 | resp.rId = request.rId; 164 | resp.msgRId = sessionEntity.RId; 165 | resp.userId = request.userId; 166 | resp.remoteId = request.remoteId; 167 | resp.groupId = request.groupId; 168 | resp.msgId = sessionEntity.MsgID; 169 | resp.sendTime = sessionEntity.SendTime; 170 | resp.type = request.type; 171 | resp.status = 200; 172 | 173 | outboxService.put(userId, resp.toWrapperJson()); 174 | } 175 | 176 | private void sendSuccessResponse(String userId, SendMsgRequest request, GroupMessageEntity groupEntity) { 177 | SendMsgResponse resp = new SendMsgResponse(); 178 | resp.rId = request.rId; 179 | resp.msgRId = groupEntity.RId; 180 | resp.userId = request.userId; 181 | resp.remoteId = request.remoteId; 182 | resp.groupId = request.groupId; 183 | resp.msgId = groupEntity.MsgID; 184 | resp.sendTime = groupEntity.SendTime; 185 | resp.type = request.type; 186 | resp.status = 200; 187 | 188 | outboxService.put(userId, resp.toWrapperJson()); 189 | } 190 | 191 | private void sendFailResponse(String userId, SendMsgRequest request, String errMsg) { 192 | 193 | SendMsgResponse errResp = new SendMsgResponse(); 194 | errResp.rId = request.rId; 195 | errResp.msgRId = request.msg.rId; 196 | errResp.userId = request.userId; 197 | errResp.remoteId = request.remoteId; 198 | errResp.groupId = request.groupId; 199 | errResp.type = request.type; 200 | errResp.status = 500; 201 | errResp.errMsg = errMsg; 202 | 203 | outboxService.put(userId, errResp.toWrapperJson()); 204 | } 205 | 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/SyncSystemUnreadHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.message.MessageWrapper; 7 | import com.linbox.im.message.system.SyncSystemUnreadRequest; 8 | import com.linbox.im.message.system.SyncSystemUnreadResponse; 9 | import com.linbox.im.message.system.SystemMessage; 10 | import com.linbox.im.message.system.SystemMessageTypes; 11 | import com.linbox.im.message.system.content.SystemUnreadContent; 12 | import com.linbox.im.server.service.IOutboxService; 13 | import com.linbox.im.server.storage.dao.IMessageDAO; 14 | import org.apache.kafka.clients.consumer.ConsumerRecord; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Qualifier; 19 | import org.springframework.stereotype.Service; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * Created by lrsec on 9/24/15. 26 | */ 27 | @Service 28 | @Qualifier("syncSystemUnreadHandler") 29 | public class SyncSystemUnreadHandler implements Handler { 30 | private static Logger logger = LoggerFactory.getLogger(SyncSystemUnreadHandler.class); 31 | 32 | @Autowired 33 | private IMessageDAO messageDAO; 34 | 35 | @Autowired 36 | private IOutboxService outboxService; 37 | 38 | @Override 39 | public void handle(ConsumerRecord record) { 40 | String json = record.value(); 41 | 42 | try { 43 | logger.debug("Start handling SyncSystemUnreadRequest: {}", json); 44 | 45 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 46 | 47 | SyncSystemUnreadRequest request = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), SyncSystemUnreadRequest.class); 48 | wrapper.content = request; 49 | 50 | if (request == null) { 51 | logger.error("SyncSystemUnreadCallback format is not correct. Json string: {}", json); 52 | return; 53 | } 54 | 55 | String userId = request.userId; 56 | 57 | try { 58 | long newFriendsCount = messageDAO.getNewFriendCount(userId); 59 | long newMessageCount = messageDAO.getNewMessageCount(userId); 60 | 61 | List messages = new ArrayList<>(2); 62 | 63 | SystemUnreadContent newFriendsContent = new SystemUnreadContent(); 64 | newFriendsContent.unread = newFriendsCount; 65 | SystemMessage newFriends = new SystemMessage(); 66 | newFriends.systemType = SystemMessageTypes.NewFriend.getValue(); 67 | newFriends.content = JSON.toJSONString(newFriendsContent); 68 | messages.add(newFriends); 69 | 70 | SystemUnreadContent newMessageContent = new SystemUnreadContent(); 71 | newMessageContent.unread = newMessageCount; 72 | SystemMessage newMessages = new SystemMessage(); 73 | newMessages.systemType = SystemMessageTypes.NoticeNotify.getValue(); 74 | newMessages.content = JSON.toJSONString(newMessageContent); 75 | messages.add(newMessages); 76 | 77 | sendSuccessResponse(userId, messages, request.rId ); 78 | } catch (Exception e) { 79 | logger.error("Exception in SyncSystemUnreadCallback", e); 80 | 81 | sendFailResponse(userId, e.toString(), request.rId); 82 | } 83 | } catch (Exception e) { 84 | throw new IMConsumerException(e, json); 85 | } 86 | } 87 | 88 | private void sendSuccessResponse(String userId, List messages, long rid) { 89 | 90 | SyncSystemUnreadResponse response = new SyncSystemUnreadResponse(); 91 | response.status = 200; 92 | response.errMsg = ""; 93 | response.userId = userId; 94 | response.rId = rid; 95 | response.unreads = messages.toArray(new SystemMessage[0]); 96 | 97 | outboxService.put(userId, response.toWrapperJson()); 98 | } 99 | 100 | private void sendFailResponse(String userId, String errMsg, long rid) { 101 | SyncSystemUnreadResponse response = new SyncSystemUnreadResponse(); 102 | response.status = 500; 103 | response.errMsg = errMsg; 104 | response.userId = userId; 105 | response.rId = rid; 106 | response.unreads = new SystemMessage[0]; 107 | 108 | outboxService.put(userId, response.toWrapperJson()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/SyncUnreadHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.linbox.im.exceptions.IMConsumerException; 6 | import com.linbox.im.exceptions.IMException; 7 | import com.linbox.im.message.*; 8 | import com.linbox.im.server.service.IInboxService; 9 | import com.linbox.im.server.service.IOutboxService; 10 | import com.linbox.im.utils.IMUtils; 11 | import com.linbox.im.server.storage.UnreadLoopData; 12 | import org.apache.commons.lang.StringUtils; 13 | import org.apache.kafka.clients.consumer.ConsumerRecord; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.annotation.Qualifier; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.util.ArrayList; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | 24 | /** 25 | * Created by lrsec on 7/2/15. 26 | */ 27 | @Service 28 | @Qualifier("syncUnreadHandler") 29 | public class SyncUnreadHandler implements Handler { 30 | 31 | private static Logger logger = LoggerFactory.getLogger(SyncUnreadHandler.class); 32 | 33 | @Autowired 34 | private IInboxService inboxService; 35 | 36 | @Autowired 37 | private IOutboxService outboxService; 38 | 39 | @Override 40 | public void handle(ConsumerRecord record) { 41 | String json = record.value(); 42 | 43 | try { 44 | logger.debug("Start handling SyncUnreadRequest: {}", json); 45 | 46 | MessageWrapper wrapper = JSON.parseObject(json, MessageWrapper.class); 47 | 48 | SyncUnreadRequest request = JSON.parseObject(((JSONObject)wrapper.content).toJSONString(), SyncUnreadRequest.class); 49 | wrapper.content = request; 50 | 51 | if(request == null) { 52 | logger.error("SyncUnreadRequest format is not correct. Json string: {}", json); 53 | return; 54 | } 55 | 56 | String userId = request.userId; 57 | if(StringUtils.isBlank(userId)) { 58 | logger.error("Can not find avaiable user id for SyncUnreadRequest {}", json); 59 | return; 60 | } 61 | 62 | try { 63 | MessageType type = MessageType.parse(request.type); 64 | if (type == null) { 65 | logger.error("Can not parse MessageType in SyncUnreadRequest {}", json); 66 | 67 | throw new IMException("Unknown MessageType " + Integer.toString(request.type)); 68 | } 69 | 70 | List unreadMsgs = new LinkedList<>(); 71 | long nextOffset = 0; 72 | 73 | switch(type) { 74 | case All: 75 | 76 | UnreadLoopData data = inboxService.getAllJson(userId, request.offset, request.limit); 77 | unreadMsgs.addAll(data.unreads); 78 | nextOffset = data.nextOffset; 79 | 80 | break; 81 | 82 | case Session: 83 | 84 | if(request.remoteId == null) { 85 | logger.error("The remote chat id is null when getting session unread message for user {}", userId); 86 | throw new IMException("The remote chat id is null when getting session unread message for user " + userId); 87 | } else { 88 | String remoteId = request.remoteId; 89 | 90 | if (StringUtils.isBlank(remoteId)) { 91 | logger.error("Can not find remote id for SyncUnreadRequest {}", json); 92 | throw new IMException("Non existing SyncUnreadRequest " + json); 93 | } 94 | 95 | unreadMsgs.add(inboxService.getSessionJson(userId, IMUtils.getSessionKey(userId, remoteId))); 96 | } 97 | 98 | break; 99 | 100 | case Group: 101 | 102 | if (request.groupId == null) { 103 | logger.error("The group id is null when getting group unread message for user {}", userId); 104 | throw new IMException("The group id is null when getting group unread message for user " + userId); 105 | } else { 106 | String groupId = request.groupId; 107 | 108 | if (StringUtils.isBlank(groupId)) { 109 | logger.error("Can not find group id for SyncUnreadRequest " + json); 110 | 111 | throw new IMException("Non existing SyncUnreadRequest " + json); 112 | } 113 | 114 | unreadMsgs.add(inboxService.getGroupJson(userId, groupId)); 115 | } 116 | 117 | break; 118 | default: 119 | logger.error("Message type {} is not handled in SyncUnreadCallback. SyncUnreadRequest: {}", type.getValue(), json); 120 | throw new IMException("Unhandled MessageType " + type.getValue() + " for SyncUnreadCallback"); 121 | } 122 | 123 | sendSuccessResponse(userId, request, unreadMsgs, nextOffset); 124 | 125 | } catch(Exception e) { 126 | logger.error("Exception when handling SyncUnreadCallback.", e); 127 | 128 | sendFailResponse(userId, request, e.getMessage()); 129 | } 130 | 131 | } catch (Exception e) { 132 | throw new IMConsumerException(e, json); 133 | } 134 | } 135 | 136 | private void sendSuccessResponse(String userId, SyncUnreadRequest request, List result, long nextOffset) { 137 | List msgs = new ArrayList<>(result.size()); 138 | 139 | for (String r : result) { 140 | UnreadMsg m = JSON.parseObject(r, UnreadMsg.class); 141 | 142 | if (m == null) { 143 | logger.error("Can not parse UnreadMsg from json: {}", r); 144 | continue; 145 | } 146 | 147 | msgs.add(m); 148 | } 149 | 150 | SyncUnreadResponse response = new SyncUnreadResponse(); 151 | response.rId = request.rId; 152 | response.userId = request.userId; 153 | response.remoteId = request.remoteId; 154 | response.groupId = request.groupId; 155 | response.type = request.type; 156 | response.offset = nextOffset; 157 | response.unreads = msgs.toArray(new UnreadMsg[0]); 158 | response.status = 200; 159 | 160 | outboxService.put(userId, response.toWrapperJson()); 161 | } 162 | 163 | private void sendFailResponse(String userId, SyncUnreadRequest request, String errMsg) { 164 | SyncUnreadResponse errResp = new SyncUnreadResponse(); 165 | errResp.rId = request.rId; 166 | errResp.userId = request.userId; 167 | errResp.remoteId = request.remoteId; 168 | errResp.groupId = request.groupId; 169 | errResp.type = request.type; 170 | errResp.offset = 0; 171 | errResp.unreads = new UnreadMsg[0]; 172 | errResp.status = 500; 173 | errResp.errMsg = errMsg; 174 | 175 | outboxService.put(userId, errResp.toWrapperJson()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/dispatcher/DispatchToGroupHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers.dispatcher; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.exceptions.IMConsumerException; 5 | import com.linbox.im.exceptions.IMException; 6 | import com.linbox.im.message.MessageType; 7 | import com.linbox.im.message.Message; 8 | import com.linbox.im.server.router.handlers.Handler; 9 | import com.linbox.im.server.storage.dao.IGroupDAO; 10 | import org.apache.kafka.clients.consumer.ConsumerRecord; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Qualifier; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.List; 18 | import java.util.concurrent.ExecutorService; 19 | import java.util.concurrent.Executors; 20 | 21 | /** 22 | * Created by lrsec on 8/26/15. 23 | */ 24 | @Service 25 | @Qualifier("dispatchToGroupHandler") 26 | public class DispatchToGroupHandler implements Handler { 27 | private static final Logger logger = LoggerFactory.getLogger(DispatchToGroupHandler.class); 28 | private final ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); 29 | 30 | @Autowired 31 | private IGroupDAO groupDAO; 32 | 33 | @Autowired 34 | private SendDispatcher dispatcher; 35 | 36 | @Override 37 | public void handle(ConsumerRecord record) { 38 | String json = record.value(); 39 | 40 | try { 41 | final Message groupMessage = JSON.parseObject(json, Message.class); 42 | 43 | if (groupMessage == null) { 44 | throw new IMException("Message could not be parsed correctly. Message: " + json); 45 | } 46 | 47 | final String groupId = groupMessage.groupId; 48 | List members = groupDAO.getGroupMembers(groupId); 49 | 50 | for(final String userId : members) { 51 | 52 | if(userId == null || userId.equalsIgnoreCase(groupMessage.fromUserId)) { 53 | continue; 54 | } 55 | 56 | service.submit(new Runnable() { 57 | @Override 58 | public void run() { 59 | dispatcher.dispatchToSingle(userId, groupId, groupId, MessageType.Group, groupMessage); 60 | } 61 | }); 62 | } 63 | } catch (Exception e) { 64 | throw new IMConsumerException(e, json); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/dispatcher/DispatchToSingleHandler.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers.dispatcher; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.exceptions.IMConsumerException; 5 | import com.linbox.im.exceptions.IMException; 6 | import com.linbox.im.message.MessageType; 7 | import com.linbox.im.message.NewMessage; 8 | import com.linbox.im.server.service.IInboxService; 9 | import com.linbox.im.server.service.IOutboxService; 10 | import com.linbox.im.message.Message; 11 | import com.linbox.im.server.router.handlers.Handler; 12 | import com.linbox.im.server.storage.dao.IUserDAO; 13 | import org.apache.kafka.clients.consumer.ConsumerRecord; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.annotation.Qualifier; 18 | import org.springframework.stereotype.Service; 19 | 20 | /** 21 | * Created by lrsec on 8/26/15. 22 | */ 23 | @Service 24 | @Qualifier("dispatchToSingleHandler") 25 | public class DispatchToSingleHandler implements Handler { 26 | private static final Logger logger = LoggerFactory.getLogger(DispatchToSingleHandler.class); 27 | 28 | @Autowired 29 | private IInboxService inboxService; 30 | 31 | @Autowired 32 | private IOutboxService outboxService; 33 | 34 | @Autowired 35 | private IUserDAO userDAO; 36 | 37 | @Override 38 | public void handle(ConsumerRecord record) { 39 | String json = record.value(); 40 | 41 | try { 42 | SendDispatchMessage dispatchMsg = JSON.parseObject(json, SendDispatchMessage.class); 43 | if (dispatchMsg == null) { 44 | throw new IMException("DispatchMessage could not be parsed correctly. Message: " + json); 45 | } 46 | 47 | MessageType type = dispatchMsg.getType(); 48 | 49 | switch (type) { 50 | case Session: 51 | dealSingleMessage(dispatchMsg); 52 | break; 53 | case Group: 54 | dealGroupMessage(dispatchMsg); 55 | break; 56 | default: 57 | throw new IMException("Get an unknown message type " + type.name() + " for SingleSendDispatchCallback"); 58 | } 59 | } catch (Exception e) { 60 | throw new IMConsumerException(e, json); 61 | } 62 | } 63 | 64 | private void dealSingleMessage(SendDispatchMessage dispatchMessage) { 65 | String userId = dispatchMessage.getUserId(); 66 | String remoteId = dispatchMessage.getRemoteId(); 67 | String sessionKey = dispatchMessage.getSessionKey(); 68 | Message message = dispatchMessage.getMessage(); 69 | 70 | inboxService.updateSessionMsg(userId, sessionKey, message); 71 | sendNewMsgToUser(userId, remoteId, MessageType.Session); 72 | sendPush(message); 73 | } 74 | 75 | private void dealGroupMessage(SendDispatchMessage dispatchMessage) { 76 | String userId = dispatchMessage.getUserId(); 77 | String remoteId = dispatchMessage.getRemoteId(); 78 | String sessionKey = dispatchMessage.getSessionKey(); 79 | Message message = dispatchMessage.getMessage(); 80 | 81 | inboxService.updateGroupMsg(userId, sessionKey, message); 82 | sendNewMsgToUser(userId, remoteId, MessageType.Group); 83 | sendConsultPush(userId, message); 84 | } 85 | 86 | private void sendNewMsgToUser(String userId, String remoteId, MessageType type) { 87 | NewMessage newMessage = new NewMessage(); 88 | 89 | newMessage.userId = userId; 90 | newMessage.remoteId = remoteId; 91 | 92 | newMessage.type = type.getValue(); 93 | 94 | if (type == MessageType.Group) { 95 | newMessage.groupId = remoteId; 96 | } 97 | 98 | outboxService.put(userId, newMessage.toWrapperJson()); 99 | } 100 | 101 | private void sendPush(Message message) { 102 | // String fromUserId = message.fromUserId; 103 | // String fromUserName = userDAO.getUserName(fromUserId); 104 | // String toUserId = message.toUserId; 105 | 106 | // PushMessage pushMessage = new PushMessage(); 107 | // 108 | // if (fromUserId.equalsIgnoreCase("10000")) { 109 | // pushMessage.Type = SystemMsgType.IM_SYSTEM_ASSISTANT; 110 | // } else { 111 | // pushMessage.Type = SystemMsgType.IM_SESSION; 112 | // } 113 | // 114 | // pushMessage.ActionType = MessageAction.Text; 115 | // 116 | // pushMessage.From = Long.parseLong(fromUserId); 117 | // pushMessage.To = Long.parseLong(toUserId); 118 | // pushMessage.Badge = ServiceFactory.getInboxService().getTotalUnreadCount(toUserId) + 1; 119 | // 120 | // if (message.mimeType.startsWith("text")) { 121 | // pushMessage.Description = fromUserName + ": " + message.content; 122 | // pushMessage.Message = fromUserName + ": " + message.content; 123 | // } else if (message.mimeType.startsWith("audio")) { 124 | // pushMessage.Description = fromUserName + ": " + "[发来一条语音消息]"; 125 | // pushMessage.Message = fromUserName + ": " + "[发来一条语音消息]"; 126 | // } else if (message.mimeType.startsWith("image")) { 127 | // pushMessage.Description = fromUserName + ": " + "[发来一张图片]"; 128 | // pushMessage.Message = fromUserName + ": " + "[发来一张图片]"; 129 | // } 130 | // 131 | // IPushService pushService = ServiceFactory.getPushService(); 132 | // pushService.sendPush(pushMessage.From, pushMessage); 133 | } 134 | 135 | private void sendConsultPush(String userId, Message message) { 136 | // String fromUserId = message.fromUserId; 137 | // String fromUserName = userDAO.getUserName(fromUserId); 138 | 139 | // PushMessage pushMessage = new PushMessage(); 140 | // pushMessage.ActionType = MessageAction.Text; 141 | // pushMessage.Type = SystemMsgType.IM_CONSULT; 142 | // 143 | // pushMessage.From = Long.parseLong(fromUserId); 144 | // pushMessage.To = Long.parseLong(userId); 145 | // pushMessage.Badge = ServiceFactory.getInboxService().getTotalUnreadCount(userId) + 1; 146 | // 147 | // if (message.mimeType.startsWith("text")) { 148 | // pushMessage.Description = fromUserName + ": " + message.content; 149 | // pushMessage.Message = fromUserName + ": " + message.content; 150 | // } else if (message.mimeType.startsWith("audio")) { 151 | // pushMessage.Description = fromUserName + ": " + "[发来一条语音消息]"; 152 | // pushMessage.Message = fromUserName + ": " + "[发来一条语音消息]"; 153 | // } else if (message.mimeType.startsWith("image")) { 154 | // pushMessage.Description = fromUserName + ": " + "[发来一张图片]"; 155 | // pushMessage.Message = fromUserName + ": " + "[发来一张图片]"; 156 | // } 157 | // 158 | // IPushService pushService = ServiceFactory.getPushService(); 159 | // pushService.sendPush(pushMessage.From, pushMessage); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/dispatcher/SendDispatchMessage.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers.dispatcher; 2 | 3 | import com.linbox.im.message.MessageType; 4 | import com.linbox.im.message.Message; 5 | 6 | /** 7 | * Created by lrsec on 8/26/15. 8 | */ 9 | public class SendDispatchMessage { 10 | 11 | private String userId; 12 | private String remoteId; 13 | private String sessionKey; 14 | private Message message; 15 | private MessageType type; 16 | 17 | public String getUserId() { 18 | return userId; 19 | } 20 | 21 | public void setUserId(String userId) { 22 | this.userId = userId; 23 | } 24 | 25 | public String getRemoteId() { 26 | return remoteId; 27 | } 28 | 29 | public void setRemoteId(String remoteId) { 30 | this.remoteId = remoteId; 31 | } 32 | 33 | public String getSessionKey() { 34 | return sessionKey; 35 | } 36 | 37 | public void setSessionKey(String sessionKey) { 38 | this.sessionKey = sessionKey; 39 | } 40 | 41 | public Message getMessage() { 42 | return message; 43 | } 44 | 45 | public void setMessage(Message message) { 46 | this.message = message; 47 | } 48 | 49 | public MessageType getType() { 50 | return type; 51 | } 52 | 53 | public void setType(MessageType type) { 54 | this.type = type; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/router/handlers/dispatcher/SendDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.router.handlers.dispatcher; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.exceptions.IMException; 5 | import com.linbox.im.message.MessageType; 6 | import com.linbox.im.message.Message; 7 | import com.linbox.im.server.constant.MessageTopic; 8 | import org.apache.kafka.clients.producer.KafkaProducer; 9 | import org.apache.kafka.clients.producer.ProducerRecord; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | 15 | /** 16 | * Created by lrsec on 8/26/15. 17 | */ 18 | @Service 19 | public class SendDispatcher { 20 | private static Logger logger = LoggerFactory.getLogger(SendDispatcher.class); 21 | 22 | @Autowired 23 | private KafkaProducer kafkaProducer; 24 | 25 | // dispatch to single user 26 | public void dispatchToSingle(String userId, String remoteId, String sessionKey, MessageType type, Message msg) { 27 | try { 28 | SendDispatchMessage message = new SendDispatchMessage(); 29 | message.setUserId(userId); 30 | message.setRemoteId(remoteId); 31 | message.setSessionKey(sessionKey); 32 | message.setMessage(msg); 33 | message.setType(type); 34 | 35 | String json = JSON.toJSONString(message); 36 | if (json == null) { 37 | throw new IMException("Message to user " + userId + "can not be parsed to json correctly."); 38 | } 39 | 40 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_DISPATCH_SEND_SINGLE, json)); 41 | } catch (Exception e) { 42 | logger.error("Dispatch send message to single user " + userId + "fail.", e); 43 | } 44 | } 45 | 46 | //dispatch to group 47 | public void dispatchToGroup(String groupId, Message message) { 48 | try { 49 | String json = JSON.toJSONString(message); 50 | if (json == null) { 51 | throw new IMException("Message to group " + groupId + "can not be parsed to json correctly."); 52 | } 53 | 54 | kafkaProducer.send(new ProducerRecord(MessageTopic.TOPIC_DISPATCH_SEND_GROUP, json)); 55 | } catch (Exception e) { 56 | logger.error("Dispatch send message to group " + groupId + " fail.", e); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/IInboxService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service; 2 | 3 | import com.linbox.im.message.Message; 4 | import com.linbox.im.server.storage.UnreadLoopData; 5 | 6 | /** 7 | * Created by lrsec on 7/2/15. 8 | */ 9 | public interface IInboxService { 10 | void updateSessionMsg(String id, String sessionId, Message msg); 11 | void updateGroupMsg(String id, String groupId, Message msg) ; 12 | 13 | void removeSessionMsg(String id, String sessionId, long msgId); 14 | void removeGroupMsg(String id, String groupId, long msgId); 15 | 16 | UnreadLoopData getAllJson(String id, long offset, int limit); 17 | String getSessionJson(String id, String sessionId); 18 | String getGroupJson(String id, String groupId); 19 | 20 | int getTotalUnreadCount(String id); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/IOutboxService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service; 2 | 3 | /** 4 | * Created by lrsec on 7/3/15. 5 | */ 6 | public interface IOutboxService { 7 | void put(String userId, String msg); 8 | String get(String userId); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/IPushService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service; 2 | 3 | import com.linbox.im.message.Message; 4 | 5 | /** 6 | * Created by lrsec on 7/16/15. 7 | */ 8 | public interface IPushService { 9 | void sendPush(Message message); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/StopcockService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.locks.ReentrantLock; 5 | 6 | /** 7 | * Created by lrsec on 7/2/15. 8 | * 9 | * 注意,该类为了使用泛型,因此取消了对 locks pool 使用 static 变量,因此请确保使用单例模式。 10 | * 非单例模式下无法正常工作。 11 | */ 12 | public class StopcockService { 13 | private final ConcurrentHashMap locks = new ConcurrentHashMap(); 14 | private ThreadLocal lockHolder = new ThreadLocal(); 15 | 16 | protected void lock(T id) { 17 | ReentrantLock myLock = new ReentrantLock(); 18 | myLock.lock(); 19 | 20 | lockHolder.set(myLock); 21 | 22 | ReentrantLock waitLock = locks.putIfAbsent(id, myLock); 23 | 24 | while (waitLock != null) { 25 | waitLock.lock(); 26 | 27 | ReentrantLock newLock = null; 28 | try { 29 | newLock = locks.putIfAbsent(id, myLock); 30 | } finally { 31 | waitLock.unlock(); 32 | waitLock = newLock; 33 | } 34 | } 35 | } 36 | 37 | protected void unlock(T id) { 38 | ReentrantLock myLock = lockHolder.get(); 39 | locks.remove(id, myLock); 40 | 41 | if (myLock != null) { 42 | myLock.unlock(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/impl/InboxService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.linbox.im.message.UnreadMsg; 5 | import com.linbox.im.server.constant.RedisKey; 6 | import com.linbox.im.server.service.IInboxService; 7 | import com.linbox.im.server.service.StopcockService; 8 | import com.linbox.im.message.Message; 9 | import com.linbox.im.server.storage.UnreadLoopData; 10 | import org.apache.commons.lang.StringUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | import redis.clients.jedis.Jedis; 16 | import redis.clients.jedis.JedisPool; 17 | import redis.clients.jedis.ScanParams; 18 | import redis.clients.jedis.ScanResult; 19 | 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | /** 25 | * Created by lrsec on 7/2/15. 26 | */ 27 | @Service 28 | public class InboxService extends StopcockService implements IInboxService { 29 | public static final Logger logger = LoggerFactory.getLogger(InboxService.class); 30 | 31 | @Autowired 32 | private JedisPool jedisPool; 33 | 34 | public void updateSessionMsg(String id, String sessionId, Message msg) { 35 | updateMsg(id, sessionId, msg); 36 | } 37 | 38 | public void updateGroupMsg(String id, String groupId, Message msg) { 39 | updateMsg(id, groupId, msg); 40 | } 41 | 42 | private void updateMsg(String id, String field, Message msg) { 43 | if (StringUtils.isBlank(id) || StringUtils.isBlank(field)) { 44 | logger.error("Get empty id or field when updating unread message. id: {}. field: {}", StringUtils.trimToEmpty(id), StringUtils.trimToEmpty(field)); 45 | return; 46 | } 47 | 48 | String redisKey = RedisKey.getInboxKey(id); 49 | String json = JSON.toJSONString(msg); 50 | 51 | if (StringUtils.isBlank(json)) { 52 | logger.error("Message format is not correct. Get a empty json string when updating unread for user: {}. field: {}", StringUtils.trimToEmpty(id), StringUtils.trimToEmpty(field)); 53 | return; 54 | } 55 | 56 | lock(redisKey); 57 | 58 | try { 59 | try (Jedis jedis = jedisPool.getResource()) { 60 | String oldMsg = jedis.hget(redisKey, field); 61 | UnreadMsg oldUnreadMsg = JSON.parseObject(oldMsg, UnreadMsg.class); 62 | 63 | UnreadMsg newUnreadMsg; 64 | if (oldUnreadMsg == null || (oldUnreadMsg.msgId < msg.msgId)) { 65 | newUnreadMsg = new UnreadMsg(); 66 | newUnreadMsg.msg = msg; 67 | newUnreadMsg.msgId = msg.msgId; 68 | newUnreadMsg.count = oldUnreadMsg == null ? 1 : (oldUnreadMsg.count + msg.msgId - oldUnreadMsg.msgId); 69 | newUnreadMsg.userId = (StringUtils.equals(id, msg.fromUserId)) ? msg.fromUserId : msg.toUserId; 70 | newUnreadMsg.remoteId = (StringUtils.equals(id, msg.fromUserId)) ? msg.toUserId : msg.fromUserId; 71 | newUnreadMsg.type = msg.type; 72 | } else { 73 | newUnreadMsg = oldUnreadMsg; 74 | } 75 | 76 | if (newUnreadMsg == null) { 77 | logger.error("The message is stale. Message in redis: {}. Message received: {}", JSON.toJSONString(oldUnreadMsg), JSON.toJSONString(msg)); 78 | return; 79 | } 80 | 81 | jedis.hset(redisKey, field, JSON.toJSONString(newUnreadMsg)); 82 | } 83 | } finally { 84 | unlock(redisKey); 85 | } 86 | } 87 | 88 | public void removeSessionMsg (String id, String sessionId, long msgId) { 89 | removeMsg(id, sessionId, msgId); 90 | } 91 | 92 | public void removeGroupMsg(String id, String groupId, long msgId) { 93 | removeMsg(id, groupId, msgId); 94 | } 95 | 96 | private void removeMsg(String id, String field, long msgId) { 97 | if (StringUtils.isBlank(id) || StringUtils.isBlank(field)) { 98 | logger.error("Get empty id or field when removing unread message. id: {}. field: {}", StringUtils.trimToEmpty(id), StringUtils.trimToEmpty(field)); 99 | return; 100 | } 101 | 102 | String redisKey = RedisKey.getInboxKey(id); 103 | 104 | lock(redisKey); 105 | 106 | try { 107 | 108 | try(Jedis jedis = jedisPool.getResource()) { 109 | String json = jedis.hget(redisKey, field); 110 | UnreadMsg msg = JSON.parseObject(json, UnreadMsg.class); 111 | 112 | if (msg != null) { 113 | if (msg.msgId <= msgId) { 114 | jedis.hdel(redisKey, field); 115 | } else { 116 | msg.count = msg.msgId - msgId; 117 | jedis.hset(redisKey, field, JSON.toJSONString(msg)); 118 | } 119 | } 120 | } 121 | } catch(Exception e) { 122 | logger.error("Remove message fail.", e); 123 | } finally { 124 | unlock(redisKey); 125 | } 126 | } 127 | 128 | public String getSessionJson(String id, String sessionId) { 129 | return getJson(id, sessionId); 130 | } 131 | 132 | public String getGroupJson(String id, String groupId) { 133 | return getJson(id, groupId); 134 | } 135 | 136 | public UnreadLoopData getAllJson(String id, long offset, int limit) { 137 | UnreadLoopData data = new UnreadLoopData(); 138 | 139 | if (StringUtils.isBlank(id)) { 140 | logger.error("Get empty id when getting all unread message. id: {}.", StringUtils.trimToEmpty(id)); 141 | return data; 142 | } 143 | 144 | String redisKey = RedisKey.getInboxKey(id); 145 | 146 | lock(redisKey); 147 | 148 | try { 149 | try(Jedis jedis = jedisPool.getResource()) { 150 | long cusor = offset; 151 | int currentLimit = limit; 152 | 153 | ScanParams params = new ScanParams(); 154 | do { 155 | params.count(currentLimit); 156 | 157 | ScanResult> result = jedis.hscan(redisKey, Long.toString(cusor), params); 158 | 159 | List> entities = result.getResult(); 160 | if (entities != null) { 161 | for(Map.Entry e : entities) { 162 | data.unreads.add(e.getValue()); 163 | } 164 | } 165 | 166 | cusor = Long.parseLong(result.getStringCursor()); 167 | currentLimit = limit - data.unreads.size(); 168 | } while (data.unreads.size() < limit && cusor != 0); 169 | } 170 | } finally { 171 | unlock(redisKey); 172 | } 173 | 174 | return data; 175 | } 176 | 177 | private String getJson(String id, String field) { 178 | String result = null; 179 | 180 | if (StringUtils.isBlank(id) || StringUtils.isBlank(field)) { 181 | logger.error("Get empty id or session_id when getting group unread message. user id: {}. field: {}", StringUtils.trimToEmpty(id), StringUtils.trimToEmpty(field)); 182 | return null; 183 | } 184 | 185 | String redisKey = RedisKey.getInboxKey(id); 186 | 187 | lock(redisKey); 188 | 189 | try { 190 | try (Jedis jedis = jedisPool.getResource()) { 191 | result = jedis.hget(redisKey, field); 192 | } 193 | } finally { 194 | unlock(redisKey); 195 | } 196 | 197 | return result; 198 | } 199 | 200 | @Override 201 | public int getTotalUnreadCount(String id) { 202 | int count = 0; 203 | 204 | if (StringUtils.isBlank(id)) { 205 | logger.error("Id is empty when getting total unread count"); 206 | return count; 207 | } 208 | 209 | List unreads = new LinkedList<>(); 210 | 211 | String redisKey = RedisKey.getInboxKey(id); 212 | 213 | lock(redisKey); 214 | 215 | try { 216 | try(Jedis jedis = jedisPool.getResource()) { 217 | unreads.addAll(jedis.hvals(redisKey)); 218 | 219 | for (String s : unreads) { 220 | UnreadMsg msg = JSON.parseObject(s, UnreadMsg.class); 221 | 222 | if (msg == null) { 223 | logger.error("A unread record could not be parsed to UnreadMsg. JSON: {}. User id: {}", s, id); 224 | continue; 225 | } 226 | 227 | count += msg.count; 228 | } 229 | } 230 | } finally { 231 | unlock(redisKey); 232 | } 233 | 234 | return count; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/impl/OutboxService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service.impl; 2 | 3 | import com.linbox.im.server.constant.RedisKey; 4 | import com.linbox.im.server.service.IOutboxService; 5 | import org.apache.commons.lang.StringUtils; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import redis.clients.jedis.Jedis; 11 | import redis.clients.jedis.JedisPool; 12 | 13 | /** 14 | * Created by lrsec on 7/3/15. 15 | */ 16 | @Service 17 | public class OutboxService implements IOutboxService { 18 | private static Logger logger = LoggerFactory.getLogger(OutboxService.class); 19 | 20 | @Autowired 21 | private JedisPool jedisPool; 22 | 23 | @Override 24 | public void put(String userId, String msg) { 25 | if(StringUtils.isBlank(userId)) { 26 | logger.error("Get empty user id when saving outbox message"); 27 | return; 28 | } 29 | 30 | String key = RedisKey.getOutboxKey(userId); 31 | 32 | try(Jedis jedis = jedisPool.getResource()) { 33 | jedis.lpush(key, msg); 34 | logger.debug("Save message to outbox for {}. Message: {}", userId, StringUtils.trimToEmpty(msg)); 35 | } 36 | } 37 | 38 | @Override 39 | public String get(String userId) { 40 | String result = null; 41 | 42 | if(StringUtils.isBlank(userId)) { 43 | logger.error("Get empty user id when getting outbox message"); 44 | return null; 45 | } 46 | 47 | String key = RedisKey.getOutboxKey(userId); 48 | 49 | try(Jedis jedis = jedisPool.getResource()) { 50 | result = jedis.lpop(key); 51 | logger.trace("Get message from outbox for {}. Message: {}. Thread: {}", userId, StringUtils.trimToEmpty(result), Thread.currentThread().getId()); 52 | 53 | } 54 | 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/service/impl/PushService.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.TypeReference; 5 | import com.alibaba.fastjson.annotation.JSONField; 6 | import com.alibaba.fastjson.parser.Feature; 7 | import com.linbox.im.utils.IdGenerator; 8 | import com.linbox.im.message.Message; 9 | import com.linbox.im.server.constant.RedisKey; 10 | import com.linbox.im.server.service.IInboxService; 11 | import com.linbox.im.server.service.IPushService; 12 | import com.linbox.im.server.storage.dao.IUserDAO; 13 | import org.apache.commons.lang.StringUtils; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | import redis.clients.jedis.Jedis; 19 | import redis.clients.jedis.JedisPool; 20 | 21 | import java.io.Serializable; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | 26 | /** 27 | * Created by lrsec on 7/16/15. 28 | */ 29 | @Service 30 | public class PushService implements IPushService { 31 | private static Logger logger = LoggerFactory.getLogger(PushService.class); 32 | 33 | @Autowired 34 | private JedisPool jedisPool; 35 | 36 | @Autowired 37 | private IInboxService inboxService; 38 | 39 | @Autowired 40 | private IUserDAO userDAO; 41 | 42 | public void sendPush(Message message) { 43 | if (message == null) { 44 | logger.error("Get an empty message for sending push"); 45 | return; 46 | } 47 | 48 | PushMessage pushMsg = new PushMessage(); 49 | 50 | pushMsg.ID = IdGenerator.getUUID(); 51 | pushMsg.Type = "InstanceMessage"; 52 | pushMsg.Title = "医树"; 53 | 54 | String realName = message.fromUserId.equals("10000") || message.fromUserId.equals("10001") ? "医树" : userDAO.getUserName(message.fromUserId); 55 | pushMsg.Title = realName; 56 | 57 | if (StringUtils.startsWith(message.mimeType, "text")) { 58 | pushMsg.Description = message.content; 59 | pushMsg.Message = message.content; 60 | } else if (StringUtils.startsWith(message.mimeType, "image")) { 61 | pushMsg.Description = "[图片]"; 62 | pushMsg.Message = "[图片]"; 63 | } else if (StringUtils.startsWith(message.mimeType, "audio")) { 64 | pushMsg.Description = "[音频]"; 65 | pushMsg.Message = "[音频]"; 66 | } else if (StringUtils.startsWith(message.mimeType, "video")){ 67 | pushMsg.Description = "[视频]"; 68 | pushMsg.Message = "[视频]"; 69 | } 70 | 71 | pushMsg.ActionType = "Text"; 72 | pushMsg.BatchId = 0; 73 | pushMsg.From = Long.parseLong(message.fromUserId); 74 | pushMsg.To = Long.parseLong( message.toUserId ); 75 | pushMsg.Badge = getTotalUnread(message.toUserId); 76 | pushMsg.Channel = "unknown"; 77 | pushMsg.Creator = Long.parseLong(message.fromUserId); 78 | pushMsg.Created = System.currentTimeMillis() / 1000l; 79 | pushMsg.Updated = pushMsg.Created; 80 | pushMsg.State = "Readed"; 81 | 82 | UserMessage msg = new UserMessage(); 83 | msg.data = pushMsg; 84 | msg.addMeta("isPush", true); 85 | 86 | //TODO push not work now 87 | // NSQHelper.produce(NSQTopic.TOPIC_PUSH, msg); 88 | 89 | logger.info("join push message nsq: {}", pushMsg.ID); 90 | } 91 | 92 | private int getTotalUnread(String userId) { 93 | int unread=0; 94 | 95 | if (StringUtils.isBlank(userId)) { 96 | logger.debug("Blank userId when getting total unread"); 97 | return unread; 98 | } 99 | 100 | try (Jedis jedis = jedisPool.getResource()) { 101 | //获取缓存中新朋友的集合的长度 即新朋友的个数 102 | unread += Math.max(0, jedis.hlen(RedisKey.getFriendRequestsKey(Long.parseLong(userId))).intValue()); 103 | 104 | //获取缓存中新评论的个数 105 | String newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewCommentsCount); 106 | if (StringUtils.isNotBlank(newCount)) { 107 | unread += Math.max(0, Integer.parseInt(newCount)); 108 | } 109 | 110 | //获取缓存中未读帮帮忙的个数 111 | newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewForumHelpCommentCount); 112 | if (StringUtils.isNotBlank(newCount)) { 113 | unread += Math.max(0, Integer.parseInt(newCount)); 114 | } 115 | 116 | newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewForumHelpInviteCount); 117 | if (StringUtils.isNotBlank(newCount)) { 118 | unread += Math.max(0, Integer.parseInt(newCount)); 119 | } 120 | 121 | newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewForumHelpPointCount); 122 | if (StringUtils.isNotBlank(newCount)) { 123 | unread += Math.max(0, Integer.parseInt(newCount)); 124 | } 125 | 126 | newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewForumHelpCommentLikeCount); 127 | if (StringUtils.isNotBlank(newCount)) { 128 | unread += Math.max(0, Integer.parseInt(newCount)); 129 | } 130 | 131 | //获取缓存中未读赞的个数 132 | newCount = jedis.hget(RedisKey.getUserUnReadCountsKey(Long.parseLong(userId)), NotifyMessageDataItems.NewLikesCount); 133 | if (StringUtils.isNotBlank(newCount)) { 134 | unread += Math.max(0, Integer.parseInt(newCount)); 135 | } 136 | 137 | // 获取未读消息总数 138 | unread += inboxService.getTotalUnreadCount(userId); 139 | } catch (Exception e) { 140 | logger.error("Get exception for getting total unread for user " + userId, e); 141 | } 142 | 143 | unread = Math.max(1, unread); 144 | 145 | return unread; 146 | } 147 | 148 | private static class PushMessage implements Serializable { 149 | public long ID; 150 | public String Type = "Unknown"; 151 | public String Title; 152 | public String Description; 153 | public String Message; 154 | public String ActionType = "Text"; 155 | public long BatchId; 156 | public String URL; 157 | public long From; 158 | public Long To; 159 | public int Badge=1; 160 | public String Channel = "unknown"; 161 | public long Creator; 162 | public long Created; 163 | public long Updated; 164 | public String State = "Readed"; 165 | 166 | @Override 167 | public String toString() { 168 | return "ID:"+ID+",Type:"+Type+",Title:"+Title+",Description:"+ Description+ 169 | ",Message:"+Message+",ActionType:"+ActionType+",BatchId:"+BatchId+ 170 | ",URL:"+URL+",From:"+From+",To:"+To+",Badge:"+Badge+",Channel:"+Channel+",Creator:"+Creator; 171 | } 172 | } 173 | 174 | //TODO this should be deleted 175 | private static class UserMessage extends NSQMessageBase { 176 | public UserMessage() { 177 | } 178 | 179 | @JSONField( 180 | serialize = false 181 | ) 182 | public long getUserId() { 183 | Object uid = this.getMeta("user_id"); 184 | return uid != null?Long.parseLong(uid.toString()):0L; 185 | } 186 | 187 | public void setUserId(long userId) { 188 | this.addMeta("user_id", String.valueOf(userId)); 189 | } 190 | 191 | @JSONField( 192 | serialize = false 193 | ) 194 | public UserMessage getMessage(String json) { 195 | return (UserMessage) JSON.parseObject(json, new TypeReference() { 196 | }, new Feature[0]); 197 | } 198 | } 199 | 200 | //TODO This should be deleted 201 | private static class NSQMessageBase implements Serializable { 202 | public HashMap meta; 203 | public List list = new ArrayList(); 204 | public T data; 205 | 206 | public NSQMessageBase() { 207 | } 208 | 209 | public Object getMeta(String key) { 210 | return this.meta != null && this.meta.containsKey(key)?this.meta.get(key):null; 211 | } 212 | 213 | public NSQMessageBase addMeta(String key, Object data) { 214 | if(this.meta == null) { 215 | this.meta = new HashMap(); 216 | } 217 | 218 | this.meta.put(key, data); 219 | return this; 220 | } 221 | 222 | public NSQMessageBase getMessage(String json) { 223 | return (NSQMessageBase)JSON.parseObject(json, new TypeReference() { 224 | }, new Feature[0]); 225 | } 226 | } 227 | 228 | private static class NotifyMessageDataItems{ 229 | public static String NewLikesCount="new_likes_count"; 230 | public static String NewCommentsCount="new_comments_count"; 231 | 232 | public static String NewForumHelpCount="new_forum_help_count"; 233 | public static String NewForumHelpInviteCount="new_forum_help_invite_count"; 234 | public static String NewForumHelpCommentCount="new_forum_help_comment_count"; 235 | public static String NewForumHelpPointCount="new_forum_help_point_count"; 236 | public static String NewForumHelpCommentLikeCount="new_forum_help_comment_like_count"; 237 | 238 | public static String ContactMobileMatchedUserCount="contact_mobile_match_count"; 239 | public static String ContactNameMatchedUserCount="contact_name_match_count"; 240 | public static String FriendRequestCount="friend_request_count"; 241 | 242 | public static String IMNewMessageCount="im_new_message_count"; 243 | 244 | //推荐好友和好友动态最后一条的用户头像 245 | @Deprecated 246 | public static String UserAvatar="user_avatar"; 247 | public static String NewFriendFeedUserAvatar="new_feeds_user_avatar"; 248 | public static String NewFriendRecommendUserAvatar="new_recommend_user_avatar"; 249 | } 250 | } 251 | 252 | 253 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/UnreadLoopData.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by lrsec on 7/23/15. 8 | * 用于分页获取 unread 数据时使用的中间数据局。 9 | */ 10 | public class UnreadLoopData { 11 | 12 | // 下一次分页请求所需要使用的 offset 信息 13 | public int nextOffset = 0; 14 | 15 | // 本次分页的数据 16 | public List unreads = new LinkedList<>(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/IGroupDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Created by lrsec on 7/4/15. 7 | */ 8 | public interface IGroupDAO { 9 | List getGroupMembers(String groupId); 10 | long generateGroupId(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/IGroupMessageDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | import com.linbox.im.message.Message; 4 | import com.linbox.im.server.storage.entity.GroupMessageEntity; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by lrsec on 7/4/15. 10 | */ 11 | public interface IGroupMessageDAO { 12 | GroupMessageEntity insert(Message msg); 13 | List findMsg(String groupId, long maxMsgId, long minMsgId, int limit); 14 | GroupMessageEntity findMsgByRId(long rId, String fromUserId, String groupId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/IMessageDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | /** 4 | * Created by lrsec on 12/29/15. 5 | */ 6 | public interface IMessageDAO { 7 | long getNewMessageCount(String userId); 8 | long getNewFriendCount(String userId); 9 | 10 | long generateMsgId(String key); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/IServerDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * Created by lrsec on 12/29/15. 7 | */ 8 | public interface IServerDAO { 9 | void registerServer(); 10 | Set getServers(); 11 | String generatePassword(long userId); 12 | String getPassword(long userId); 13 | void registerConnection(String userId, String address); 14 | String getConnection(String userId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/ISessionMessageDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | import com.linbox.im.message.Message; 4 | import com.linbox.im.server.storage.entity.SessionMessageEntity; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by lrsec on 7/2/15. 10 | */ 11 | public interface ISessionMessageDAO { 12 | SessionMessageEntity insert(Message msg); 13 | List findMsg(String sessioinId, long maxMsgId, long minMsgId, int limit); 14 | SessionMessageEntity findMsgByRId(long rId, String fromUserId, String toUserId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/IUserDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao; 2 | 3 | /** 4 | * Created by lrsec on 7/3/15. 5 | */ 6 | public interface IUserDAO { 7 | String getUserName(String userId); 8 | Boolean isUserValid(String userID, String token); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/GroupDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.linbox.im.exceptions.IMException; 4 | import com.linbox.im.server.constant.RedisKey; 5 | import com.linbox.im.server.storage.dao.IGroupDAO; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.sql2o.Connection; 11 | import org.sql2o.Sql2o; 12 | import redis.clients.jedis.Jedis; 13 | import redis.clients.jedis.JedisPool; 14 | 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | 18 | /** 19 | * Created by lrsec on 7/4/15. 20 | */ 21 | @Service 22 | public class GroupDAO implements IGroupDAO { 23 | private static Logger logger = LoggerFactory.getLogger(GroupDAO.class); 24 | 25 | @Autowired 26 | private Sql2o sql2o; 27 | 28 | @Autowired 29 | private JedisPool jedisPool; 30 | 31 | @Override 32 | public List getGroupMembers(String groupId) { 33 | List members = new LinkedList<>(); 34 | 35 | String sql = "SELECT AccountId FROM group_members WHERE ConsultationId = :groupId "; 36 | 37 | try(Connection conn = sql2o.open()) { 38 | members.addAll(conn.createQuery(sql) 39 | .addParameter("groupId", groupId) 40 | .executeAndFetch(String.class)); 41 | } 42 | 43 | return members; 44 | } 45 | 46 | @Override 47 | public long generateGroupId() { 48 | try (Jedis jedis = jedisPool.getResource()) { 49 | if(!jedis.exists(RedisKey.GROUP_ID)) { 50 | throw new IMException( RedisKey.GROUP_ID + "不存在,无法获取合法的 Group Id"); 51 | } 52 | 53 | return jedis.incr(RedisKey.GROUP_ID); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/GroupMessageDao.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.linbox.im.server.storage.dao.IGroupMessageDAO; 4 | import com.linbox.im.server.storage.entity.GroupMessageEntity; 5 | import com.linbox.im.message.Message; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.sql2o.Connection; 9 | import org.sql2o.Sql2o; 10 | 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | 14 | 15 | /** 16 | * Created by lrsec on 7/4/15. 17 | */ 18 | @Service 19 | public class GroupMessageDao implements IGroupMessageDAO { 20 | 21 | @Autowired 22 | private Sql2o sql2o; 23 | 24 | @Override 25 | public GroupMessageEntity insert(Message msg) { 26 | String sql = "INSERT INTO im_group_message set RId = :rId, GroupId = :groupId, FromUserID = :fromUserId, MsgID = :msgId, MimeType = :mimeType, Content = :content, SendTime = :sendTime, Created = :created"; 27 | 28 | GroupMessageEntity dao = GroupMessageEntity.convertToGroupMsgDao(msg); 29 | 30 | try (Connection conn = sql2o.open()) { 31 | conn.createQuery(sql) 32 | .addParameter("rId", dao.RId) 33 | .addParameter("groupId", dao.GroupId) 34 | .addParameter("fromUserId", dao.FromUserID) 35 | .addParameter("msgId", dao.MsgID) 36 | .addParameter("mimeType", dao.MimeType) 37 | .addParameter("content", dao.Content) 38 | .addParameter("sendTime", dao.SendTime) 39 | .addParameter("created", dao.Created) 40 | .executeUpdate(); 41 | } 42 | 43 | return dao; 44 | 45 | } 46 | 47 | @Override 48 | public List findMsg(String groupId, long maxMsgId, long minMsgId, int limit) { 49 | 50 | String sql = "select * from im_group_message where GroupId = :groupId AND MsgID <= :maxMsgId AND MsgID > :minMsgId ORDER BY MsgID DESC LIMIT :limit"; 51 | 52 | List daos = new LinkedList(); 53 | 54 | try(Connection conn = sql2o.open()) { 55 | List result = conn.createQuery(sql) 56 | .addParameter("groupId", groupId) 57 | .addParameter("maxMsgId", maxMsgId) 58 | .addParameter("minMsgId", minMsgId) 59 | .addParameter("limit", limit) 60 | .executeAndFetch(GroupMessageEntity.class); 61 | 62 | daos.addAll(result); 63 | } 64 | 65 | return daos; 66 | 67 | } 68 | 69 | @Override 70 | public GroupMessageEntity findMsgByRId(long rId, String fromUserId, String groupId) { 71 | String sql = "SELECT * FROM im_group_message WHERE RId = :rId AND FromUserID = :fromUserId AND GroupId = :groupId"; 72 | 73 | try(Connection conn = sql2o.open()) { 74 | return conn.createQuery(sql) 75 | .addParameter("rId", rId) 76 | .addParameter("fromUserId", fromUserId) 77 | .addParameter("groupId", groupId) 78 | .executeAndFetchFirst(GroupMessageEntity.class); 79 | 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/MessageDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.linbox.im.exceptions.IMException; 4 | import com.linbox.im.server.constant.RedisKey; 5 | import com.linbox.im.server.storage.dao.IMessageDAO; 6 | import org.apache.commons.lang.StringUtils; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import redis.clients.jedis.Jedis; 12 | import redis.clients.jedis.JedisPool; 13 | 14 | /** 15 | * Created by lrsec on 12/29/15. 16 | */ 17 | @Service 18 | public class MessageDAO implements IMessageDAO { 19 | private static Logger logger = LoggerFactory.getLogger(MessageDAO.class); 20 | 21 | @Autowired 22 | private JedisPool jedisPool; 23 | 24 | @Override 25 | public long getNewMessageCount(String userId) { 26 | long newMessageCount = 0; 27 | 28 | try (Jedis jedis = jedisPool.getResource()) { 29 | newMessageCount = jedis.llen(RedisKey.getUnreadMessageNotifyKey(Long.parseLong(userId))); 30 | } 31 | 32 | return newMessageCount; 33 | } 34 | 35 | @Override 36 | public long getNewFriendCount(String userId) { 37 | long newFriendsCount = 0; 38 | 39 | try (Jedis jedis = jedisPool.getResource()) { 40 | newFriendsCount = jedis.hlen(RedisKey.getFriendRequestsKey(Long.parseLong(userId))); 41 | } 42 | 43 | return newFriendsCount; 44 | } 45 | 46 | @Override 47 | //TODO 目前使用 redis 实现,之后可以考虑使用 zk 提高可用性,避免由于 redis crash 导致的 id 不一致问题 48 | public long generateMsgId(String key) { 49 | if (StringUtils.isBlank(key)) { 50 | logger.error("Key should not be empty."); 51 | throw new IMException("Key should not be empty."); 52 | } 53 | 54 | key = RedisKey.getMsgIDKey(key); 55 | 56 | try(Jedis jedis = jedisPool.getResource()) { 57 | return jedis.incr(key); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/ServerDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.linbox.im.exceptions.IMException; 4 | import com.linbox.im.server.constant.RedisKey; 5 | import com.linbox.im.server.storage.dao.IServerDAO; 6 | import com.linbox.im.utils.AESUtils; 7 | import org.apache.commons.lang.StringUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | import redis.clients.jedis.Jedis; 14 | import redis.clients.jedis.JedisPool; 15 | 16 | import java.util.LinkedHashSet; 17 | import java.util.Set; 18 | 19 | /** 20 | * Created by lrsec on 12/29/15. 21 | */ 22 | @Service 23 | public class ServerDAO implements IServerDAO { 24 | private static Logger logger = LoggerFactory.getLogger(ServerDAO.class); 25 | 26 | @Autowired 27 | private JedisPool jedisPool; 28 | 29 | @Value("${im.ip}") 30 | private String ip; 31 | 32 | @Value("${im.port}") 33 | private String port; 34 | 35 | @Override 36 | public void registerServer() { 37 | if (StringUtils.isBlank(ip) || StringUtils.isBlank(port)) { 38 | logger.error("ip or port is empty when insert ServierInfoDao. ip: {}. port: {}", StringUtils.trimToEmpty(ip), StringUtils.trimToEmpty(port)); 39 | throw new IMException("ip or port could not be empty. ip: " + StringUtils.trimToEmpty(ip) + ". port: " + StringUtils.trimToEmpty(port)); 40 | } 41 | 42 | String info = StringUtils.trim(ip) + ":" + StringUtils.trim(port); 43 | 44 | logger.debug("Register server: {}", info); 45 | 46 | try (Jedis jedis = jedisPool.getResource()) { 47 | jedis.zadd(RedisKey.IP_REGISTRY, 0, info); 48 | } 49 | } 50 | 51 | @Override 52 | public Set getServers() { 53 | Set result = new LinkedHashSet<>(); 54 | 55 | try (Jedis jedis = jedisPool.getResource()) { 56 | result.addAll(jedis.zrange(RedisKey.IP_REGISTRY, 0, -1)); 57 | } catch (Exception e) { 58 | logger.error("Get IM server list from redis fail.", e); 59 | } 60 | 61 | return result; 62 | } 63 | 64 | @Override 65 | public String generatePassword(long userId) { 66 | String token = AESUtils.generatePassword(); 67 | 68 | String key= RedisKey.getIMPassword(userId); 69 | 70 | try (Jedis jedis = jedisPool.getResource()) { 71 | jedis.setex(key,3600, token); 72 | } 73 | 74 | return token; 75 | } 76 | 77 | @Override 78 | public String getPassword(long userId) { 79 | String password = null; 80 | try (Jedis jedis = jedisPool.getResource()) { 81 | password = jedis.get(RedisKey.getIMPassword(userId)); 82 | } 83 | 84 | return password; 85 | } 86 | 87 | @Override 88 | public void registerConnection(String userId, String address) { 89 | try (Jedis jedis = jedisPool.getResource()) { 90 | jedis.hset(RedisKey.CONNECT_REGISTRY, userId, address); 91 | } 92 | } 93 | 94 | @Override 95 | public String getConnection(String userId) { 96 | String record = null; 97 | 98 | try (Jedis jedis = jedisPool.getResource()) { 99 | record = jedis.hget(RedisKey.CONNECT_REGISTRY, userId); 100 | } 101 | 102 | return record; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/SessionMessageDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.linbox.im.message.Message; 4 | import com.linbox.im.server.storage.dao.ISessionMessageDAO; 5 | import com.linbox.im.server.storage.entity.SessionMessageEntity; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.sql2o.Connection; 9 | import org.sql2o.Sql2o; 10 | 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by lrsec on 7/2/15. 16 | */ 17 | @Service 18 | public class SessionMessageDAO implements ISessionMessageDAO { 19 | 20 | @Autowired 21 | private Sql2o sql2o; 22 | 23 | @Override 24 | public SessionMessageEntity insert(Message msg) { 25 | String sql = "insert into im_session_message set RId = :rId, SessionId = :sessionId, MsgID = :msgId, FromUserID = :fromUserId, ToUserID = :toUserId, MimeType = :mimeType, Content = :content, SendTime = :sendTime, Created = :created"; 26 | 27 | SessionMessageEntity dao = SessionMessageEntity.convertToDao(msg); 28 | try (Connection conn = sql2o.open()) { 29 | conn.createQuery(sql) 30 | .addParameter("rId", dao.RId) 31 | .addParameter("sessionId", dao.SessionId) 32 | .addParameter("msgId", dao.MsgID) 33 | .addParameter("fromUserId", dao.FromUserID) 34 | .addParameter("toUserId", dao.ToUserID) 35 | .addParameter("mimeType", dao.MimeType) 36 | .addParameter("content", dao.Content) 37 | .addParameter("sendTime", dao.SendTime) 38 | .addParameter("created", dao.Created) 39 | .executeUpdate(); 40 | } 41 | 42 | return dao; 43 | } 44 | 45 | @Override 46 | public List findMsg(String sessionId, long maxMsgId, long minMsgId, int limit) { 47 | String sql = "select * from im_session_message where SessionId = :sessionId AND MsgID <= :maxMsgId AND MsgID > :minMsgId ORDER BY MsgID DESC LIMIT :limit"; 48 | 49 | List daos = new LinkedList(); 50 | 51 | try(Connection conn = sql2o.open()) { 52 | List result = conn.createQuery(sql) 53 | .addParameter("sessionId", sessionId) 54 | .addParameter("maxMsgId", maxMsgId) 55 | .addParameter("minMsgId", minMsgId) 56 | .addParameter("limit", limit) 57 | .executeAndFetch(SessionMessageEntity.class); 58 | 59 | daos.addAll(result); 60 | } 61 | 62 | return daos; 63 | } 64 | 65 | @Override 66 | public SessionMessageEntity findMsgByRId(long rId, String fromUserId, String toUserId) { 67 | String sql = "select * from im_session_message where RId = :rid AND FromUserID = :fromUserId AND ToUserID = :toUserId"; 68 | 69 | try(Connection conn = sql2o.open()) { 70 | return conn.createQuery(sql) 71 | .addParameter("rid", rId) 72 | .addParameter("fromUserId", fromUserId) 73 | .addParameter("toUserId", toUserId) 74 | .executeAndFetchFirst(SessionMessageEntity.class); 75 | 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/dao/impl/UserDAO.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.dao.impl; 2 | 3 | import com.google.common.base.Strings; 4 | import com.linbox.im.server.storage.dao.IUserDAO; 5 | import org.apache.commons.lang.StringUtils; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.sql2o.Connection; 11 | import org.sql2o.Sql2o; 12 | 13 | /** 14 | * Created by lrsec on 7/3/15. 15 | */ 16 | @Service 17 | public class UserDAO implements IUserDAO { 18 | 19 | public static final Logger logger = LoggerFactory.getLogger(UserDAO.class); 20 | 21 | @Autowired 22 | private Sql2o sql2o; 23 | 24 | public String getUserName(String userId) { 25 | if (StringUtils.isBlank(userId)) { 26 | logger.warn("Query user name for null or empty user id"); 27 | return ""; 28 | } 29 | 30 | String sql = "select RealName from profile where AccountID = :accountId"; 31 | 32 | 33 | String realName; 34 | 35 | try (Connection conn = sql2o.open()) { 36 | realName = conn.createQuery(sql) 37 | .addParameter("accountId", userId) 38 | .executeAndFetchFirst(String.class); 39 | } 40 | 41 | return Strings.nullToEmpty(realName); 42 | } 43 | 44 | @Override 45 | public Boolean isUserValid(String userID, String token) { 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/entity/GroupEntity.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.entity; 2 | 3 | /** 4 | * Created by lrsec on 7/4/15. 5 | */ 6 | public class GroupEntity { 7 | public long GroupId; 8 | 9 | public String GroupName; 10 | 11 | public long Creator; 12 | 13 | public long Created; 14 | 15 | public long Updated; 16 | 17 | public long Expire; 18 | 19 | public int Status; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/entity/GroupMessageEntity.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.entity; 2 | 3 | import com.linbox.im.message.Message; 4 | 5 | /** 6 | * Created by lrsec on 7/3/15. 7 | */ 8 | public class GroupMessageEntity { 9 | // 消息发送方的 rId 10 | public long RId; 11 | 12 | // 群组 id 13 | public String GroupId; 14 | 15 | // 发送方 id 16 | public long FromUserID; 17 | 18 | // message id 19 | public long MsgID; 20 | 21 | // 消息体类型 22 | public String MimeType; 23 | 24 | // 消息体内容 25 | public String Content; 26 | 27 | // 服务器端接收到消息的时间 28 | public long SendTime; 29 | 30 | // 记录创建时间 31 | public long Created; 32 | 33 | public static GroupMessageEntity convertToGroupMsgDao(Message msg) { 34 | if (msg == null) { 35 | return null; 36 | } 37 | 38 | GroupMessageEntity dao = new GroupMessageEntity(); 39 | 40 | dao.RId = msg.rId; 41 | dao.FromUserID = Long.parseLong(msg.fromUserId); 42 | dao.GroupId = msg.groupId; 43 | dao.MsgID = msg.msgId; 44 | dao.MimeType = msg.mimeType; 45 | dao.Content = msg.content; 46 | dao.SendTime = msg.sendTime == 0 ? System.currentTimeMillis() : msg.sendTime; 47 | dao.Created = dao.SendTime; 48 | 49 | return dao; 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/server/storage/entity/SessionMessageEntity.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.server.storage.entity; 2 | 3 | import com.linbox.im.utils.IMUtils; 4 | import com.linbox.im.message.Message; 5 | 6 | /** 7 | * Created by lrsec on 7/3/15. 8 | */ 9 | public class SessionMessageEntity { 10 | // 消息发送方的 rId 11 | public long RId; 12 | 13 | public String SessionId; 14 | 15 | // 发送方 id 16 | public long FromUserID; 17 | 18 | // 目的方 id 19 | public long ToUserID; 20 | 21 | // message id 22 | public long MsgID; 23 | 24 | // 消息体类型 25 | public String MimeType; 26 | 27 | // 消息体内容 28 | public String Content; 29 | 30 | // 服务器端接收到消息的时间 31 | public long SendTime; 32 | 33 | // 记录创建时间 34 | public long Created; 35 | 36 | public static SessionMessageEntity convertToDao(Message msg) { 37 | if (msg == null) { 38 | return null; 39 | } 40 | 41 | SessionMessageEntity dao = new SessionMessageEntity(); 42 | 43 | dao.RId = msg.rId; 44 | dao.SessionId = IMUtils.getSessionKey(msg.fromUserId, msg.toUserId); 45 | dao.FromUserID = Long.parseLong(msg.fromUserId); 46 | dao.ToUserID = Long.parseLong(msg.toUserId); 47 | dao.MsgID = msg.msgId; 48 | dao.MimeType = msg.mimeType; 49 | dao.Content = msg.content; 50 | dao.SendTime = msg.sendTime == 0 ? System.currentTimeMillis() : msg.sendTime; 51 | dao.Created = dao.SendTime; 52 | 53 | return dao; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/utils/AESUtils.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.crypto.KeyGenerator; 7 | import javax.crypto.SecretKey; 8 | import java.util.Base64; 9 | 10 | /** 11 | * Created by lrsec on 7/13/15. 12 | */ 13 | public class AESUtils { 14 | private static Logger logger = LoggerFactory.getLogger(AESUtils.class); 15 | private static final String KEY_ALGORITHM="AES"; 16 | private static final int KEY_LENGTH=128; 17 | 18 | private static Base64.Encoder base64Encoder = Base64.getEncoder(); 19 | 20 | public static String generatePassword() { 21 | byte[] pw; 22 | 23 | try { 24 | KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM); 25 | keyGen.init(KEY_LENGTH); // for example 26 | SecretKey s=keyGen.generateKey(); 27 | 28 | pw = s.getEncoded(); 29 | 30 | logger.debug("New AES password created: {}. Bit size: {}", pw, pw.length * 8 ); 31 | 32 | } catch (Exception e) { 33 | logger.error("Fail to generate key.", e); 34 | throw new RuntimeException("Fail to generate key."); 35 | } 36 | 37 | return base64Encoder.encodeToString(pw); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/utils/IMUtils.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.utils; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | /** 8 | * Created by lrsec on 7/3/15. 9 | */ 10 | public class IMUtils { 11 | public static final Logger logger = LoggerFactory.getLogger(IMUtils.class); 12 | 13 | public static String getSessionKey(String id1, String id2) { 14 | if(StringUtils.isBlank(id1) || StringUtils.isBlank(id2)) { 15 | logger.error("id should not be empty for create session_id. id1: {}. id2: {}", StringUtils.trimToEmpty(id1), StringUtils.trimToEmpty(id2)); 16 | throw new IllegalArgumentException("id should not be empty for create session_id"); 17 | } 18 | 19 | return id1.compareTo(id2) < 0 ? concrat(id1, id2) : concrat(id2, id1); 20 | } 21 | 22 | private static String concrat(String id1, String id2) { 23 | return id1 + "_" + id2; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/linbox/im/utils/IdGenerator.java: -------------------------------------------------------------------------------- 1 | package com.linbox.im.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加)) 10 | * @author Polim 11 | */ 12 | public class IdGenerator { 13 | 14 | private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class); 15 | 16 | private final static long twepoch = 1278834974657L; 17 | // 机器标识位数 18 | private final static long workerIdBits = 5L; 19 | // 数据中心标识位数 20 | private final static long datacenterIdBits = 5L; 21 | // 机器ID最大值 22 | private final static long maxWorkerId = -1L ^ (-1L << workerIdBits); 23 | // 数据中心ID最大值 24 | private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); 25 | // 毫秒内自增位 26 | private final static long sequenceBits = 12L; 27 | // 机器ID偏左移12位 28 | private final static long workerIdShift = sequenceBits; 29 | // 数据中心ID左移17位 30 | private final static long datacenterIdShift = sequenceBits + workerIdBits; 31 | // 时间毫秒左移22位 32 | private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; 33 | 34 | private final static long sequenceMask = -1L ^ (-1L << sequenceBits); 35 | 36 | private static long lastTimestamp = -1L; 37 | 38 | private long sequence = 0L; 39 | private final long workerId; 40 | private final long datacenterId; 41 | 42 | public IdGenerator(long workerId, long datacenterId) { 43 | if (workerId > maxWorkerId || workerId < 0) { 44 | throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0"); 45 | } 46 | if (datacenterId > maxDatacenterId || datacenterId < 0) { 47 | throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0"); 48 | } 49 | this.workerId = workerId; 50 | this.datacenterId = datacenterId; 51 | } 52 | 53 | public synchronized long nextId() { 54 | long timestamp = timeGen(); 55 | if (timestamp < lastTimestamp) { 56 | try { 57 | throw new Exception("Clock moved backwards. Refusing to generate id for "+ (lastTimestamp - timestamp) + " milliseconds"); 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | 63 | if (lastTimestamp == timestamp) { 64 | // 当前毫秒内,则+1 65 | sequence = (sequence + 1) & sequenceMask; 66 | if (sequence == 0) { 67 | // 当前毫秒内计数满了,则等待下一秒 68 | timestamp = tilNextMillis(lastTimestamp); 69 | } 70 | } else { 71 | sequence = 0; 72 | } 73 | lastTimestamp = timestamp; 74 | // ID偏移组合生成最终的ID,并返回ID 75 | long nextId = ((timestamp - twepoch) << timestampLeftShift) 76 | | (datacenterId << datacenterIdShift) 77 | | (workerId << workerIdShift) | sequence; 78 | 79 | return nextId; 80 | } 81 | 82 | private long tilNextMillis(final long lastTimestamp) { 83 | long timestamp = this.timeGen(); 84 | while (timestamp <= lastTimestamp) { 85 | timestamp = this.timeGen(); 86 | } 87 | return timestamp; 88 | } 89 | 90 | private long timeGen() { 91 | return System.currentTimeMillis(); 92 | } 93 | 94 | //TODO 检测配置更新 95 | static IdGenerator w=null; 96 | 97 | static { 98 | init(); 99 | } 100 | 101 | synchronized static void init(){ 102 | long randId=new Random().nextInt(30)+1; 103 | long wid = randId; 104 | long did = 1l; 105 | logger.debug("id generator use workerid:{},datacenter:{}",wid,did); 106 | w = new IdGenerator(wid,did); 107 | } 108 | 109 | public static long getWorkerId(){ 110 | if(w==null){init();} 111 | return w.workerId; 112 | } 113 | public static long getDataCenterId(){ 114 | if(w==null){init();} 115 | return w.datacenterId; 116 | } 117 | 118 | 119 | public static long getUUID(){ 120 | if(w==null){init();} 121 | return w.nextId(); 122 | } 123 | } 124 | 125 | --------------------------------------------------------------------------------