├── .gitignore ├── README.md ├── build.gradle ├── dependencies.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── im-client ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── bigyj │ │ │ └── client │ │ │ ├── ClientApplication.java │ │ │ ├── client │ │ │ └── ImClient.java │ │ │ ├── config │ │ │ ├── LoadBalanceConfig.java │ │ │ └── ZookeeperConfig.java │ │ │ ├── handler │ │ │ ├── ChatClientHandler.java │ │ │ ├── ExceptionHandler.java │ │ │ └── LoginRequestSendHandler.java │ │ │ ├── initializer │ │ │ └── ImClientInitializer.java │ │ │ ├── load │ │ │ └── balance │ │ │ │ ├── AbstractLoadBalance.java │ │ │ │ ├── ConsistentHashLoadBalance.java │ │ │ │ ├── LeastActiveLoadBlance.java │ │ │ │ ├── LoadBalance.java │ │ │ │ ├── RandomLoadBalance.java │ │ │ │ └── RoundRobinLoadBlance.java │ │ │ └── zk │ │ │ ├── ZkService.java │ │ │ └── ZkServiceImpl.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── load │ └── TestRoundRobinLoadBalance.java ├── im-common ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── bigyj │ ├── entity │ ├── ClientNode.java │ ├── Msg.java │ ├── MsgDto.java │ ├── ServerNode.java │ ├── SessionCache.java │ └── WeightedRoundRobin.java │ ├── message │ ├── AbstractResponseMessage.java │ ├── ChatRequestMessage.java │ ├── ChatResponseMessage.java │ ├── GroupChatRequestMessage.java │ ├── GroupChatResponseMessage.java │ ├── GroupRemoteChatRequestMessage.java │ ├── LoginRequestMessage.java │ ├── LoginResponseMessage.java │ ├── Message.java │ ├── PingMessage.java │ └── ServerPeerConnectedMessage.java │ ├── monitor │ └── DirectMemoryReporter.java │ ├── protocol │ ├── ChatMessageCodec.java │ └── ProtocolFrameDecoder.java │ ├── user │ └── User.java │ └── utils │ ├── NodeUtil.java │ └── ThreadUtils.java ├── im-server ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── bigyj │ │ │ └── server │ │ │ ├── ServerApplication.java │ │ │ ├── cach │ │ │ ├── SessionCacheSupport.java │ │ │ └── SessionCacheSupportImpl.java │ │ │ ├── config │ │ │ ├── RedisConfig.java │ │ │ └── ZookeeperConfig.java │ │ │ ├── handler │ │ │ ├── ChatRedirectHandler.java │ │ │ ├── ChatServerRedirectHandler.java │ │ │ ├── DisconnectedHandler.java │ │ │ ├── GroupMessageSendHandler.java │ │ │ ├── LoginRequestHandler.java │ │ │ └── ServerPeerConnectedHandler.java │ │ │ ├── holder │ │ │ ├── LocalSessionHolder.java │ │ │ └── ServerPeerSenderHolder.java │ │ │ ├── initializer │ │ │ └── ImServerInitializer.java │ │ │ ├── manager │ │ │ ├── MemoryUserManager.java │ │ │ └── ServerSessionManager.java │ │ │ ├── registration │ │ │ ├── CuratorZKclient.java │ │ │ ├── ZkService.java │ │ │ └── ZkServiceImpl.java │ │ │ ├── server │ │ │ ├── ImServer.java │ │ │ └── ServerPeerSender.java │ │ │ ├── session │ │ │ ├── AbstractServerSession.java │ │ │ ├── LocalSession.java │ │ │ ├── RemoteSession.java │ │ │ └── ServerSession.java │ │ │ ├── utils │ │ │ └── SpringContextUtil.java │ │ │ └── worker │ │ │ ├── ServerRouterWorker.java │ │ │ └── ServerWorker.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ ├── MemoryTest.java │ ├── Test.java │ ├── client │ ├── ClientInitializer.java │ ├── MyClient.java │ └── MyClientHandler.java │ ├── echo │ ├── EchoClient.java │ ├── EchoServer.java │ ├── InChannelHandlerA.java │ ├── InChannelHandlerB.java │ ├── InChannelHandlerC.java │ ├── OutChannelHandlerA.java │ ├── OutChannelHandlerB.java │ └── OutChannelHandlerC.java │ ├── server │ ├── MyChatHandler.java │ ├── MyChatServer.java │ └── MyChatServerInitializer.java │ └── threadlocal │ └── ThreadLocalTest.java ├── lombok.config └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | !.gitignore 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 分布式IM 2 | ## 一、概述 3 | 使用netty开发分布式Im,提供分布netty集群解决方案。服务端通过负载均衡策略与服务集群建立连接,消息发送通过服务间集群的通信进行消息转发。 4 | ## 二、自定义协议 5 | ### 1.自定义协议要素 6 | - 魔数,用来在第一时间判定是否是无效数据包 7 | - 版本号,可以支持协议的升级 8 | - 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk 9 | - 指令类型,是登录、注册、单聊、群聊… 跟业务相关 10 | - 请求序号,为了双工通信,提供异步能力 11 | - 正文长度 12 | - 消息正文 13 | ## 三、集群架构 14 | ![架构图](https://img-blog.csdnimg.cn/27c34099715546f2945239a4688708d5.png) 15 | ### 1.客户端 16 | 用户聊天客户端,客户端连接IM服务需要进行用户认证。用户认证成功之后,开始连接上线。 17 | ### 2.服务路由 18 | 服务路由负责将客户端的连接请求按照不同的负载均衡策略路由到不同的IM服务,建立长链接。负载均衡策略分为以下四种: 19 | - 一致性HASH负载均衡策略 20 | - 最少活跃数负载均衡策略 21 | - 随机调用负载均衡策略 22 | - 轮询调用负载均衡策略 23 | ### 3.IM服务集群 24 | 为了避免单节点故障,IM服务采用集群模式。集群内各个IM服务又互为对方的客户端,用于转发远程消息(消息接收客户端连接其他IM服务节点)。 25 | ### 4.ZK集群 26 | ZK集群作为IM服务的注册中心,用户IM服务的注册与发现以及服务上线、下线的事件监听通知。通过node事件,控制IM服务之间连接的建立与断开。 27 | ### 5.消息队列 28 | 消息队列用户发送离线消息、聊天消息。 29 | ### 6.MongoDB集群 30 | 存储离线消息及聊天消息。 31 | ### 7.Redis集群 32 | 存储客户端的连接session信息(客户端与服务端连接的信息) 33 | ## 四、netty集群方案 34 | 首先需要明确一个问题,netty的channel是无法存储到redis里面的。netty的channel是一个连接,是和机器的硬件绑定的,无法序列化,计算存到redis里面,取出来也无法使用。 35 | ### 1.ZK作为注册中心实现 36 | **(1)channel无法存储的问题** 37 | 38 | channel是无法存储到redis里面的,但是客户端和服务端的连接信息(例如:127.0.0.1:8080的服务端是127.0.0.1:9090)是可以存储到redis里面的,因此可以通过redis存储连接信息。key为客户端标识,value为服务端地址信息,获取客户端的连接时,直接通过客户端信息即可获取其服务信息。 39 | 40 | ![channel存储](https://img-blog.csdnimg.cn/74d482dbd4cc49db8520e50630bb8dd6.png) 41 | 42 | **(2)服务端连接的问题** 43 | 44 | 客户端连接服务端时,客户端如何知道当前服务端有哪些,需要要连接哪个?这个问题可以通过ZK解决。使用ZK作为注册中心,服务端上线后在ZK中创建node,连接服务端时,从ZK获取在线节点信息,根据负载均衡策略选择服务端连接。 45 | 46 | ![ZK注册中心](https://img-blog.csdnimg.cn/b2534128090a462bb9217e104de27996.png) 47 | 48 | **(3)消息转发的问题** 49 | 50 | 连接相同服务的客户端,可以直接通过获连接当前服取客户端信息进行消息的转发,那连接不同服务端消息如何转发?我们可以通过监听ZK中node的事件(node创建代表新的服务上线,node销毁代表服务下线),通过不同的事件方法,实现服务端之间的互相连接。 51 | 52 | ![消息转发](https://img-blog.csdnimg.cn/22951f45d82a41a39fb00065239fd5d6.png) 53 | 54 | ### 2.redis订阅与广播实现(可替换为消息队列进行处理) 55 | redis支持消息订阅与发布机制机制(消息队列),可以使用该机制实现不同服务间的消息转发。在广播消息时,需要携带能唯一标识接收者身份的字段(例如clientId)。消息广播结束后,所有服务端会 56 | 收到该消息,服务端仅仅需要判断该消息接收者的是否是连接的自己作为服务端。若发现该接收者正是连接的自己,则直接将消息转发到该客户端即可。 57 | 58 | ![消息转发](https://img-blog.csdnimg.cn/c822e0feb72f4a37b89735ccab2826ff.png) 59 | ## 五、核心功能 60 | ### 1.netty服务节点的注册与发现 61 | ### 2.netty服务节点的负载均衡策略 62 | ### 2.netty服务节点的消息转发 63 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: "dependencies.gradle" 3 | 4 | repositories { 5 | mavenLocal() 6 | maven { url 'https://maven.aliyun.com/repository/public/' } 7 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' } 8 | maven { url 'https://plugins.gradle.org/m2/' } 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | classpath libraries.springBootPlugin 14 | classpath libraries.springDependencyManagerPlugin 15 | } 16 | } 17 | 18 | allprojects { 19 | apply plugin: 'idea' 20 | 21 | group = 'com.bigyj' 22 | version '1.0.0-SNAPSHOT' 23 | 24 | repositories { 25 | mavenLocal() 26 | maven { url 'https://maven.aliyun.com/repository/public/' } 27 | mavenCentral() 28 | } 29 | } 30 | 31 | subprojects { 32 | apply plugin: 'java-library' 33 | apply plugin: pluginIds.springDependencyManager 34 | 35 | sourceCompatibility = '8' 36 | 37 | [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' 38 | 39 | configurations { 40 | compileOnly { 41 | extendsFrom annotationProcessor 42 | } 43 | } 44 | 45 | dependencies { 46 | compileOnly libraries.lombok 47 | annotationProcessor libraries.lombok 48 | } 49 | 50 | dependencyManagement { 51 | imports { 52 | mavenBom libraries.springBootBom 53 | } 54 | } 55 | 56 | tasks.withType(Javadoc) { 57 | if (JavaVersion.current().isJava8Compatible()) { 58 | options.addStringOption('Xdoclint:none', '-quiet') 59 | } 60 | if (JavaVersion.current().isJava9Compatible()) { 61 | options.addBooleanOption('html5', true) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | versions = [:] 3 | pluginIds = [:] 4 | libraries = [:] 5 | } 6 | 7 | versions.springBoot = "2.3.4.RELEASE" 8 | versions.gson = "2.8.5" 9 | versions.netty = "4.1.45.Final" 10 | 11 | pluginIds.springBoot = "org.springframework.boot" 12 | pluginIds.springDependencyManager = "io.spring.dependency-management" 13 | 14 | libraries.springBootPlugin = "org.springframework.boot:spring-boot-gradle-plugin:${versions.springBoot}" 15 | libraries.springDependencyManagerPlugin = "io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE" 16 | 17 | libraries.springBootBom = "org.springframework.boot:spring-boot-dependencies:${versions.springBoot}" 18 | 19 | libraries.springBootConfigurationProcessor = "org.springframework.boot:spring-boot-configuration-processor" 20 | libraries.springBootStarterAop = "org.springframework.boot:spring-boot-starter-aop" 21 | libraries.springBootStarterDataJdbc = "org.springframework.boot:spring-boot-starter-data-jdbc" 22 | libraries.springBootStarterDataRedis = "org.springframework.boot:spring-boot-starter-data-redis" 23 | libraries.springBootStarterJooq = "org.springframework.boot:spring-boot-starter-jooq" 24 | libraries.springBootStarterJson = "org.springframework.boot:spring-boot-starter-json" 25 | libraries.springBootStarterLog4j2 = "org.springframework.boot:spring-boot-starter-log4j2" 26 | libraries.springBootStarterThymeleaf = "org.springframework.boot:spring-boot-starter-thymeleaf" 27 | libraries.springBootStarterValidation = "org.springframework.boot:spring-boot-starter-validation" 28 | libraries.springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web" 29 | libraries.springDataCommons = "org.springframework.data:spring-data-commons" 30 | libraries.commonsLang3 = 'org.apache.commons:commons-lang3' 31 | libraries.httpComponents = "org.apache.httpcomponents:httpclient" 32 | libraries.jpa = "jakarta.persistence:jakarta.persistence-api" 33 | libraries.jaxbApi = 'javax.xml.bind:jaxb-api' 34 | libraries.jaxbRuntime = 'org.glassfish.jaxb:jaxb-runtime' 35 | libraries.jsonLib = "net.sf.json-lib:json-lib:2.4:jdk15" 36 | libraries.lombok = "org.projectlombok:lombok" 37 | libraries.micrometerRegistryPrometheus = "io.micrometer:micrometer-registry-prometheus" 38 | libraries.okhttp3 = "com.squareup.okhttp3:okhttp" 39 | libraries.servletApi = "jakarta.servlet:jakarta.servlet-api" 40 | libraries.fastjson='com.alibaba:fastjson:1.2.76' 41 | libraries.redisson='org.redisson:redisson:3.12.5' 42 | libraries.protostuffCore ='io.protostuff:protostuff-core:1.6.0' 43 | libraries.protostuffRuntime ='io.protostuff:protostuff-runtime:1.6.0' 44 | libraries.gson ='com.google.code.gson:gson:2.8.5' 45 | libraries.netty='io.netty:netty-all:4.1.45.Final' 46 | libraries.curator='org.apache.curator:curator-recipes:4.0.1' 47 | 48 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beardlessCat/im/5586157475ea54eeaab09c207371da40dfcc5d3c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /im-client/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | dependencies { 4 | implementation 'org.junit.jupiter:junit-jupiter:5.7.0' 5 | implementation project(':im-common') 6 | implementation libraries.netty 7 | implementation libraries.springBootStarterWeb 8 | implementation libraries.gson 9 | implementation libraries.fastjson 10 | implementation libraries.curator 11 | implementation libraries.springBootStarterDataRedis 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/ClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client; 2 | 3 | import com.bigyj.client.client.ImClient; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | 8 | @SpringBootApplication 9 | public class ClientApplication { 10 | public static void main(String[] args) { 11 | ConfigurableApplicationContext context = SpringApplication.run(ClientApplication.class, args); 12 | //服务启动成功后开启客户端 13 | startClient(context); 14 | } 15 | 16 | private static void startClient(ConfigurableApplicationContext context) { 17 | ImClient imClient = context.getBean(ImClient.class); 18 | imClient.doConnect(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/client/ImClient.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.client; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.client.initializer.ImClientInitializer; 6 | import com.bigyj.client.load.balance.LoadBalance; 7 | import com.bigyj.client.zk.ZkService; 8 | import com.bigyj.entity.ServerNode; 9 | import io.netty.bootstrap.Bootstrap; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.EventLoopGroup; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.nio.NioSocketChannel; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | 19 | @Service("imClient") 20 | @Slf4j 21 | public class ImClient { 22 | 23 | public static final String MANAGE_PATH = "/im/nodes"; 24 | public static final String PATH_PREFIX_NO_STRIP = "seq-"; 25 | 26 | private ZkService zkService; 27 | private LoadBalance loadBalance; 28 | private ImClientInitializer imClientInitializer ; 29 | @Autowired 30 | ImClient(ZkService zkService,LoadBalance loadBalance,ImClientInitializer imClientInitializer){ 31 | this.zkService = zkService; 32 | this.loadBalance = loadBalance ; 33 | this.imClientInitializer = imClientInitializer; 34 | } 35 | 36 | private Bootstrap bootstrap; 37 | 38 | private EventLoopGroup eventLoopGroup; 39 | 40 | public ImClient() { 41 | } 42 | 43 | /** 44 | * 重连 45 | */ 46 | public Channel doConnect() { 47 | Channel channel = null; 48 | List workers = zkService.getWorkers(MANAGE_PATH, PATH_PREFIX_NO_STRIP); 49 | ServerNode serverNode = loadBalance.selectNode(workers); 50 | try { 51 | bootstrap = new Bootstrap(); 52 | eventLoopGroup = new NioEventLoopGroup(); 53 | bootstrap.group(eventLoopGroup) 54 | .channel(NioSocketChannel.class) 55 | .handler(imClientInitializer); 56 | channel = bootstrap.connect(serverNode.getHost(), serverNode.getPort()).sync().channel(); 57 | return channel ; 58 | }catch (Exception e){ 59 | e.printStackTrace(); 60 | } 61 | return channel; 62 | } 63 | 64 | public void close() { 65 | eventLoopGroup.shutdownGracefully(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/config/LoadBalanceConfig.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.config; 2 | 3 | import com.bigyj.client.load.balance.LoadBalance; 4 | import com.bigyj.client.load.balance.RandomLoadBalance; 5 | 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class LoadBalanceConfig { 11 | @Bean 12 | public LoadBalance loadBalance(){ 13 | return new RandomLoadBalance(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/config/ZookeeperConfig.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.config; 2 | 3 | import org.apache.curator.RetryPolicy; 4 | import org.apache.curator.framework.CuratorFramework; 5 | import org.apache.curator.framework.CuratorFrameworkFactory; 6 | import org.apache.curator.retry.ExponentialBackoffRetry; 7 | 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class ZookeeperConfig { 14 | @Value("${zk.url}") 15 | private String zkUrl; 16 | 17 | @Bean("curatorFramework") 18 | public CuratorFramework getCuratorFramework(){ 19 | RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3); 20 | CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl,retryPolicy); 21 | client.start(); 22 | return client; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/handler/ChatClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.handler; 2 | 3 | import com.bigyj.message.ChatResponseMessage; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | @Slf4j 9 | public class ChatClientHandler extends SimpleChannelInboundHandler { 10 | @Override 11 | protected void channelRead0(ChannelHandlerContext ctx, ChatResponseMessage chatResponseMessage) throws Exception { 12 | if(chatResponseMessage.isSuccess()){ 13 | logger.info("收到用户的{}消息:{}",chatResponseMessage.getFrom(),chatResponseMessage.getContent()); 14 | }else { 15 | logger.info("消息发送失败:{}",chatResponseMessage.getReason()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/handler/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.handler; 2 | 3 | import io.netty.channel.ChannelHandler; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Slf4j 10 | @Service 11 | @ChannelHandler.Sharable 12 | public class ExceptionHandler extends ChannelInboundHandlerAdapter { 13 | 14 | @Override 15 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 16 | //捕捉异常信息 fixme 重连 17 | logger.error(cause.getMessage()); 18 | ctx.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/handler/LoginRequestSendHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.handler; 2 | 3 | import com.bigyj.message.*; 4 | import io.netty.channel.ChannelDuplexHandler; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.ChannelInboundHandlerAdapter; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.handler.timeout.IdleState; 9 | import io.netty.handler.timeout.IdleStateEvent; 10 | import io.netty.handler.timeout.IdleStateHandler; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import java.util.Scanner; 14 | import java.util.concurrent.CountDownLatch; 15 | import java.util.concurrent.atomic.AtomicBoolean; 16 | 17 | /** 18 | * 处理登录请求 19 | */ 20 | @Slf4j 21 | public class LoginRequestSendHandler extends ChannelInboundHandlerAdapter { 22 | Scanner scanner = new Scanner(System.in); 23 | CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1); 24 | private static final int WRITE_IDLE_GAP = 150; 25 | 26 | AtomicBoolean LOGIN = new AtomicBoolean(false); 27 | AtomicBoolean EXIT = new AtomicBoolean(false); 28 | //连接建立,触发active事件,调起控制台线程 29 | @Override 30 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 31 | logger.info("连接建立成功"); 32 | new Thread(() -> { 33 | while (true) { 34 | System.out.println("请输入用户名:"); 35 | String username = scanner.nextLine(); 36 | if (EXIT.get()) { 37 | return; 38 | } 39 | System.out.println("请输入密码:"); 40 | String password = scanner.nextLine(); 41 | if (EXIT.get()) { 42 | return; 43 | } 44 | //发送登录请求 45 | LoginRequestMessage loginRequestMessage = new LoginRequestMessage(username,password); 46 | ctx.writeAndFlush(loginRequestMessage); 47 | try { 48 | WAIT_FOR_LOGIN.await(); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | //登录失败 53 | if(!LOGIN.get()){ 54 | return; 55 | } 56 | logger.info("登录成功,请输入如下指令:"); 57 | while (true) { 58 | System.out.println("=================================="); 59 | System.out.println("send [username] [content]"); 60 | System.out.println("gSend [content]"); 61 | System.out.println("quit"); 62 | System.out.println("=================================="); 63 | String command = null; 64 | try { 65 | command = scanner.nextLine(); 66 | } catch (Exception e) { 67 | break; 68 | } 69 | if(EXIT.get()){ 70 | return; 71 | } 72 | String[] s = command.split(" "); 73 | switch (s[0]){ 74 | case "send": 75 | ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2])); 76 | break; 77 | case "gSend": 78 | ctx.writeAndFlush(new GroupChatRequestMessage(username,s[1])); 79 | break; 80 | case "quit": 81 | ctx.channel().close(); 82 | return; 83 | } 84 | } 85 | } 86 | },"scanner in").start(); 87 | 88 | } 89 | @Override 90 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 91 | //判断消息实例 92 | if(!(msg instanceof LoginResponseMessage)){ 93 | super.channelRead(ctx, msg); 94 | return; 95 | } 96 | LoginResponseMessage loginResponseMessage = (LoginResponseMessage) msg; 97 | logger.info("收到登录返回消息:{}",loginResponseMessage); 98 | //判断登录成功还是登录失败 99 | if(loginResponseMessage.isSuccess()){ 100 | //调整客户端登录状态 101 | ChannelPipeline pipeline = ctx.pipeline(); 102 | //增加聊天的handler 103 | pipeline.addLast("chat", new ChatClientHandler()); 104 | //增加心跳 105 | pipeline.addLast(new IdleStateHandler(0, WRITE_IDLE_GAP, 0)); 106 | // ChannelDuplexHandler 可以同时作为入站和出站处理器 107 | pipeline.addLast(new ChannelDuplexHandler() { 108 | // 用来触发特殊事件 109 | @Override 110 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ 111 | IdleStateEvent event = (IdleStateEvent) evt; 112 | // 触发了写空闲事件 113 | if (event.state() == IdleState.WRITER_IDLE) { 114 | logger.debug("{} 没有写数据了,发送一个心跳包",WRITE_IDLE_GAP); 115 | ctx.writeAndFlush(new PingMessage()); 116 | 117 | } 118 | } 119 | }); 120 | LOGIN.set(true); 121 | //唤醒阻塞线程 122 | WAIT_FOR_LOGIN.countDown(); 123 | }else { 124 | logger.error("用户登录失败"); 125 | WAIT_FOR_LOGIN.countDown(); 126 | } 127 | } 128 | 129 | // 在连接断开时触发 130 | @Override 131 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 132 | logger.info("连接已经断开,按任意键退出.."); 133 | EXIT.set(true); 134 | } 135 | 136 | // 在出现异常时触发 137 | @Override 138 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 139 | logger.info("连接已经断开,按任意键退出..{}", cause.getMessage()); 140 | EXIT.set(true); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/initializer/ImClientInitializer.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.initializer; 2 | 3 | import com.bigyj.client.handler.ExceptionHandler; 4 | import com.bigyj.client.handler.LoginRequestSendHandler; 5 | import com.bigyj.protocol.ChatMessageCodec; 6 | import com.bigyj.protocol.ProtocolFrameDecoder; 7 | import io.netty.channel.ChannelInitializer; 8 | import io.netty.channel.ChannelPipeline; 9 | import io.netty.channel.socket.SocketChannel; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.annotation.Lazy; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class ImClientInitializer extends ChannelInitializer { 16 | @Autowired 17 | @Lazy 18 | private ExceptionHandler exceptionHandler ; 19 | @Override 20 | protected void initChannel(SocketChannel ch) throws Exception { 21 | ChannelPipeline pipeline = ch.pipeline(); 22 | //粘报半包处理 23 | pipeline.addLast(new ProtocolFrameDecoder()); 24 | //加入新的协议编码与解码器 25 | pipeline.addLast("messageCodec",new ChatMessageCodec()); 26 | pipeline.addLast("login",new LoginRequestSendHandler()); 27 | pipeline.addLast("except",exceptionHandler); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/load/balance/AbstractLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.load.balance; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | 7 | public abstract class AbstractLoadBalance implements LoadBalance{ 8 | @Override 9 | public ServerNode selectNode(List serverNodes) { 10 | if(serverNodes.isEmpty()){ 11 | return null; 12 | } 13 | if(serverNodes.size() == 1){ 14 | return serverNodes.get(0); 15 | } 16 | return doSelect(serverNodes); 17 | } 18 | 19 | protected abstract ServerNode doSelect(List serverNodes); 20 | } 21 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/load/balance/ConsistentHashLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.load.balance; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | 7 | /** 8 | * 一致性HASH 9 | */ 10 | public class ConsistentHashLoadBalance extends AbstractLoadBalance{ 11 | public static final String NAME = "consistenthash"; 12 | @Override 13 | protected ServerNode doSelect(List serverNodes) { 14 | String remoteIp = "127.0.0.1"; 15 | int hashCode = remoteIp.hashCode(); 16 | int serverListSize = serverNodes.size(); 17 | int serverPos = hashCode % serverListSize; 18 | return serverNodes.get(serverPos); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/load/balance/LeastActiveLoadBlance.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.load.balance; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | 7 | /** 8 | * 最少活跃数 9 | */ 10 | public class LeastActiveLoadBlance extends AbstractLoadBalance{ 11 | public static final String NAME = "leastactive"; 12 | 13 | @Override 14 | protected ServerNode doSelect(List serverNodes) { 15 | return null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/load/balance/LoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.load.balance; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | public interface LoadBalance { 7 | ServerNode selectNode(List serverNodes); 8 | } 9 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/load/balance/RandomLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.load.balance; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | 5 | import java.util.List; 6 | import java.util.concurrent.ThreadLocalRandom; 7 | 8 | /** 9 | *随机调用负载均衡 10 | */ 11 | public class RandomLoadBalance extends AbstractLoadBalance{ 12 | public static final String NAME = "random"; 13 | @Override 14 | protected ServerNode doSelect(List serverNodes) { 15 | int size = serverNodes.size(); 16 | int[] weights = new int[size]; 17 | int total = 0 ; 18 | for(int i=0;i> nodes = new ConcurrentHashMap<>(); 19 | @Override 20 | protected ServerNode doSelect(List serverNodes) { 21 | ConcurrentHashMap map = nodes.computeIfAbsent(CLIENT_KEY, key ->new ConcurrentHashMap<>()); 22 | long maxValue =Long.MIN_VALUE; 23 | long now = System.currentTimeMillis(); 24 | int total = 0; 25 | ServerNode selectedServerNode = null; 26 | WeightedRoundRobin selectedWRR =null; 27 | for(ServerNode serverNode:serverNodes){ 28 | String serverKey = serverNode.toString(); 29 | Integer weight = serverNode.getWeight(); 30 | WeightedRoundRobin weightObject = map.computeIfAbsent(serverKey, key -> { 31 | WeightedRoundRobin weightedRoundRobin = new WeightedRoundRobin(); 32 | weightedRoundRobin.setWeight(weight); 33 | weightedRoundRobin.setLastUpdate(now); 34 | return weightedRoundRobin; 35 | }); 36 | //weight changed 37 | if (weight != weightObject.getWeight()) { 38 | weightObject.setWeight(weight); 39 | } 40 | long nextWeight = weightObject.increaseCurrent(); 41 | //判断加上权重是否大于当前的最大值, 42 | if(nextWeight > maxValue){ 43 | selectedServerNode = serverNode; 44 | selectedWRR = weightObject; 45 | maxValue = nextWeight; 46 | } 47 | total+=weight; 48 | }; 49 | selectedWRR.sel(total); 50 | if(selectedServerNode!=null){ 51 | logger.info("选中节点"+selectedServerNode.toString()); 52 | } 53 | return selectedServerNode; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/zk/ZkService.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.zk; 2 | 3 | import java.util.List; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | 7 | public interface ZkService { 8 | List getWorkers(String path,String prefix); 9 | } 10 | -------------------------------------------------------------------------------- /im-client/src/main/java/com/bigyj/client/zk/ZkServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.client.zk; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.alibaba.fastjson.JSONObject; 7 | import com.bigyj.entity.ServerNode; 8 | import com.bigyj.utils.NodeUtil; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.curator.framework.CuratorFramework; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Slf4j 16 | @Component 17 | public class ZkServiceImpl implements ZkService { 18 | @Autowired 19 | CuratorFramework curatorFramework; 20 | 21 | @Override 22 | public List getWorkers(String path,String prefix) { 23 | List workers = new ArrayList(); 24 | List children = null; 25 | try 26 | { 27 | children = curatorFramework.getChildren().forPath(path); 28 | } catch (Exception e) 29 | { 30 | e.printStackTrace(); 31 | return null; 32 | } 33 | 34 | for (String child : children) 35 | { 36 | logger.info("child:", child); 37 | byte[] payload = null; 38 | try 39 | { 40 | payload = curatorFramework.getData().forPath(path + "/" + child); 41 | 42 | } catch (Exception e) 43 | { 44 | e.printStackTrace(); 45 | } 46 | if (null == payload) 47 | { 48 | continue; 49 | } 50 | ServerNode serverNode = JSONObject.parseObject(payload, ServerNode.class); 51 | serverNode.setId(NodeUtil.getIdByPath(child,prefix)); 52 | workers.add(serverNode); 53 | } 54 | return workers; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /im-client/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8052 3 | 4 | zk: 5 | url: 127.0.0.1:2181 6 | timeout: 3000 7 | 8 | spring: 9 | ################################ 10 | # Redis配置 11 | ################################ 12 | redis: 13 | host: demo.redis.cn 14 | port: 6379 15 | password: 123456 -------------------------------------------------------------------------------- /im-client/src/test/java/load/TestRoundRobinLoadBalance.java: -------------------------------------------------------------------------------- 1 | package load; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.atomic.AtomicLong; 12 | 13 | public class TestRoundRobinLoadBalance { 14 | private static final String CLIENT_KEY = "IM_CLIENT" ; 15 | private List list = new ArrayList<>(); 16 | Map> nodes = new ConcurrentHashMap<>(); 17 | @BeforeEach 18 | void initBaseServer(){ 19 | list.add(new ServerNode(1L,"127.0.0.1",8001,100)); 20 | list.add(new ServerNode(2L,"127.0.0.1",8002,200)); 21 | list.add(new ServerNode(3L,"127.0.0.1",8003,300)); 22 | list.add(new ServerNode(4L,"127.0.0.1",8004,400)); 23 | list.add(new ServerNode(5L,"127.0.0.1",8005,500)); 24 | } 25 | @Test 26 | void testRoundRobinLoadBalanceSelect(){ 27 | for(int i = 0 ;i<15;i++){ 28 | doSelect(); 29 | } 30 | } 31 | 32 | private void doSelect() { 33 | ConcurrentHashMap map = nodes.computeIfAbsent(CLIENT_KEY, key ->new ConcurrentHashMap<>()); 34 | long maxValue =Long.MIN_VALUE; 35 | long now = System.currentTimeMillis(); 36 | int total = 0; 37 | ServerNode selectedServerNode = null; 38 | WeightedRoundRobin selectedWRR =null; 39 | for(ServerNode serverNode:list){ 40 | String serverKey = serverNode.toString(); 41 | Integer weight = serverNode.getWeight(); 42 | WeightedRoundRobin weightObject = map.computeIfAbsent(serverKey, key -> { 43 | WeightedRoundRobin weightedRoundRobin = new WeightedRoundRobin(); 44 | weightedRoundRobin.setWeight(weight); 45 | weightedRoundRobin.setLastUpdate(now); 46 | return weightedRoundRobin; 47 | }); 48 | //weight changed 49 | if (weight != weightObject.getWeight()) { 50 | weightObject.setWeight(weight); 51 | } 52 | long nextWeight = weightObject.increaseCurrent(); 53 | //判断加上权重是否大于当前的最大值, 54 | if(nextWeight > maxValue){ 55 | selectedServerNode = serverNode; 56 | selectedWRR = weightObject; 57 | maxValue = nextWeight; 58 | } 59 | total+=weight; 60 | }; 61 | selectedWRR.sel(total); 62 | if(selectedServerNode!=null){ 63 | System.out.println(selectedServerNode); 64 | } 65 | } 66 | 67 | protected static class WeightedRoundRobin { 68 | private int weight; 69 | private AtomicLong current = new AtomicLong(0); 70 | private long lastUpdate; 71 | 72 | public int getWeight() { 73 | return weight; 74 | } 75 | 76 | public void setWeight(int weight) { 77 | this.weight = weight; 78 | current.set(0); 79 | } 80 | 81 | public long increaseCurrent() { 82 | return current.addAndGet(weight); 83 | } 84 | 85 | public void sel(int total) { 86 | current.addAndGet(-1 * total); 87 | } 88 | 89 | public long getLastUpdate() { 90 | return lastUpdate; 91 | } 92 | 93 | public void setLastUpdate(long lastUpdate) { 94 | this.lastUpdate = lastUpdate; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /im-common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.bigyj' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | dependencies { 12 | implementation libraries.netty 13 | compileOnly libraries.lombok 14 | compileOnly libraries.springBootStarterWeb 15 | annotationProcessor libraries.lombok 16 | } 17 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/ClientNode.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ClientNode { 7 | //Netty 客户端 IP 8 | private String host; 9 | 10 | //Netty 客户端 端口 11 | private Integer port; 12 | 13 | ClientNode(String host, Integer port) { 14 | this.host = host; 15 | this.port = port; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/Msg.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import com.bigyj.user.User; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class Msg { 8 | /** 9 | * 消息类型 10 | */ 11 | private final MsgType msgType ; 12 | /** 13 | * 用户信息 14 | */ 15 | private final User user; 16 | /** 17 | * 消息是否发送成功 18 | */ 19 | private final boolean success ; 20 | /** 21 | * 消息接收用户id 22 | */ 23 | private final String toUserId ; 24 | /** 25 | * 消息内容 26 | */ 27 | private final String content ; 28 | 29 | public Msg(MsgBuilder msgBuilder){ 30 | this.msgType = msgBuilder.msgType; 31 | this.user = msgBuilder.user; 32 | this.success = msgBuilder.success; 33 | this.toUserId = msgBuilder.toUserId; 34 | this.content = msgBuilder.content; 35 | } 36 | 37 | public static MsgBuilder builder(MsgType msgType, User user) { 38 | return new MsgBuilder(msgType,user); 39 | } 40 | public static MsgBuilder builder() { 41 | return new MsgBuilder(); 42 | } 43 | public static class MsgBuilder { 44 | private MsgType msgType ; 45 | private User user; 46 | private boolean success ; 47 | private String toUserId ; 48 | private String content ; 49 | public MsgBuilder(MsgType msgType, User user){ 50 | this.msgType = msgType; 51 | this.user = user ; 52 | } 53 | public MsgBuilder(){ 54 | } 55 | public MsgBuilder setMsgType(MsgType msgType){ 56 | this.msgType = msgType ; 57 | return this; 58 | } 59 | public MsgBuilder setUser(User user){ 60 | this.user = user ; 61 | return this; 62 | } 63 | public MsgBuilder setSuccess(boolean success){ 64 | this.success = success ; 65 | return this; 66 | } 67 | public MsgBuilder setToUserId(String toUserId){ 68 | this.toUserId = toUserId ; 69 | return this; 70 | } 71 | public MsgBuilder setContent(String content){ 72 | this.content = content ; 73 | return this; 74 | } 75 | public Msg build(){ 76 | return new Msg(this); 77 | } 78 | } 79 | 80 | public enum MsgType 81 | { 82 | LOGIN_REQUEST,//登陆请求消息 83 | LOGIN_RESPONSE,//登陆响应消息 84 | CHAT,//聊天消息 85 | LOGOUT_REQUEST,// 退出请求消息 86 | LOGOUT_RESPONSE,// 退出响应消息 87 | HEART_PING,//心跳ping 88 | HEART_PONG,//心跳pong 89 | SERVER_NOTICE//服务通知 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/MsgDto.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import com.bigyj.user.User; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class MsgDto { 8 | /** 9 | * 消息类型 10 | */ 11 | private Msg.MsgType msgType ; 12 | /** 13 | * 用户信息 14 | */ 15 | private User user; 16 | /** 17 | * 消息是否发送成功 18 | */ 19 | private boolean success ; 20 | /** 21 | * 消息接收用户id 22 | */ 23 | private String toUserId ; 24 | /** 25 | * 消息内容 26 | */ 27 | private String content ; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/ServerNode.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class ServerNode implements Serializable { 9 | 10 | //worker 的Id,zookeeper负责生成 11 | private long id; 12 | 13 | //Netty 服务 IP 14 | private String host; 15 | 16 | //Netty 服务 端口 17 | private Integer port; 18 | 19 | //权重(客户端连接数量) 20 | private Integer weight = 0; 21 | 22 | public ServerNode(String host, Integer port) { 23 | this.host = host; 24 | this.port = port; 25 | } 26 | 27 | public ServerNode(long id, String host, Integer port, Integer weight) { 28 | this.id = id; 29 | this.host = host; 30 | this.port = port; 31 | this.weight = weight; 32 | } 33 | 34 | public String getAddress(){ 35 | return this.host+":"+this.port; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return host+":"+port+":"+id; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/SessionCache.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class SessionCache implements Serializable { 9 | //用户的id 10 | private String userId; 11 | //session id 12 | private String sessionId; 13 | 14 | //节点信息 15 | private ServerNode serverNode; 16 | 17 | public SessionCache(String userId, String sessionId, ServerNode serverNode) { 18 | this.userId = userId; 19 | this.sessionId = sessionId; 20 | this.serverNode = serverNode; 21 | } 22 | 23 | public SessionCache() 24 | { 25 | userId = ""; 26 | sessionId = ""; 27 | serverNode = new ServerNode("unKnown", 0); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/entity/WeightedRoundRobin.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.entity; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | public class WeightedRoundRobin { 6 | private int weight; 7 | private AtomicLong current = new AtomicLong(0); 8 | private long lastUpdate; 9 | 10 | public int getWeight() { 11 | return weight; 12 | } 13 | 14 | public void setWeight(int weight) { 15 | this.weight = weight; 16 | current.set(0); 17 | } 18 | 19 | public long increaseCurrent() { 20 | return current.addAndGet(weight); 21 | } 22 | 23 | public void sel(int total) { 24 | current.addAndGet(-1 * total); 25 | } 26 | 27 | public long getLastUpdate() { 28 | return lastUpdate; 29 | } 30 | 31 | public void setLastUpdate(long lastUpdate) { 32 | this.lastUpdate = lastUpdate; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/AbstractResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public abstract class AbstractResponseMessage extends Message{ 9 | private boolean success = true; 10 | private String reason; 11 | 12 | public AbstractResponseMessage() { 13 | } 14 | 15 | public AbstractResponseMessage(boolean success, String reason) { 16 | this.success = success; 17 | this.reason = reason; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/ChatRequestMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class ChatRequestMessage extends Message { 9 | private String content; 10 | private String to; 11 | private String from; 12 | 13 | public ChatRequestMessage() { 14 | } 15 | 16 | public ChatRequestMessage(String from, String to, String content) { 17 | this.from = from; 18 | this.to = to; 19 | this.content = content; 20 | } 21 | 22 | @Override 23 | public int getMessageType() { 24 | return ChatRequestMessage; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/ChatResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | 4 | import lombok.Data; 5 | import lombok.ToString; 6 | 7 | @Data 8 | @ToString(callSuper = true) 9 | public class ChatResponseMessage extends AbstractResponseMessage { 10 | 11 | private String from; 12 | private String content; 13 | 14 | public ChatResponseMessage(boolean success, String reason) { 15 | super(success, reason); 16 | } 17 | 18 | public ChatResponseMessage(String from, String content) { 19 | this.from = from; 20 | this.content = content; 21 | } 22 | 23 | @Override 24 | public int getMessageType() { 25 | return ChatResponseMessage; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/GroupChatRequestMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class GroupChatRequestMessage extends Message { 9 | private String content; 10 | private String from; 11 | 12 | public GroupChatRequestMessage(String from, String content) { 13 | this.content = content; 14 | this.from = from; 15 | } 16 | 17 | @Override 18 | public int getMessageType() { 19 | return GroupChatRequestMessage; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/GroupChatResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class GroupChatResponseMessage extends AbstractResponseMessage { 9 | private String from; 10 | private String content; 11 | 12 | public GroupChatResponseMessage(boolean success, String reason) { 13 | super(success, reason); 14 | } 15 | 16 | public GroupChatResponseMessage(String from, String content) { 17 | this.from = from; 18 | this.content = content; 19 | } 20 | @Override 21 | public int getMessageType() { 22 | return GroupChatResponseMessage; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/GroupRemoteChatRequestMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class GroupRemoteChatRequestMessage extends Message { 9 | private String content; 10 | private String from; 11 | 12 | public GroupRemoteChatRequestMessage(String from, String content) { 13 | this.content = content; 14 | this.from = from; 15 | } 16 | 17 | @Override 18 | public int getMessageType() { 19 | return GroupRemoteChatRequestMessage; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/LoginRequestMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class LoginRequestMessage extends Message{ 9 | private String username; 10 | private String password; 11 | 12 | public LoginRequestMessage() { 13 | } 14 | 15 | public LoginRequestMessage(String username, String password) { 16 | this.username = username; 17 | this.password = password; 18 | } 19 | 20 | @Override 21 | public int getMessageType() { 22 | return LoginRequestMessage; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/LoginResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class LoginResponseMessage extends AbstractResponseMessage{ 9 | public LoginResponseMessage(boolean success, String reason) { 10 | super(success, reason); 11 | } 12 | @Override 13 | public int getMessageType() { 14 | return LoginResponseMessage; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/Message.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | @Data 9 | public abstract class Message implements Serializable { 10 | /** 11 | * 根据消息类型字节,获得对应的消息 class 12 | * @param messageType 消息类型字节 13 | * @return 消息 class 14 | */ 15 | public static Class getMessageClass(int messageType) { 16 | return messageClasses.get(messageType); 17 | } 18 | 19 | private int sequenceId; 20 | 21 | private int messageType; 22 | 23 | public abstract int getMessageType(); 24 | 25 | public static final int LoginRequestMessage = 0; 26 | public static final int LoginResponseMessage = 1; 27 | public static final int ChatRequestMessage = 2; 28 | public static final int ChatResponseMessage = 3; 29 | public static final int GroupChatRequestMessage = 4; 30 | public static final int GroupChatResponseMessage = 5; 31 | public static final int PingMessage = 6; 32 | public static final int PongMessage = 7; 33 | public static final int ServerPeerConnectedMessage = 8 ; 34 | public static final int GroupRemoteChatRequestMessage = 9 ; 35 | 36 | /** 37 | * 请求类型 byte 值 38 | */ 39 | public static final int RPC_MESSAGE_TYPE_REQUEST = 101; 40 | /** 41 | * 响应类型 byte 值 42 | */ 43 | public static final int RPC_MESSAGE_TYPE_RESPONSE = 102; 44 | 45 | private static final Map> messageClasses = new HashMap<>(); 46 | 47 | static { 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/PingMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class PingMessage extends Message { 9 | @Override 10 | public int getMessageType() { 11 | return PingMessage; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/message/ServerPeerConnectedMessage.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.message; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString(callSuper = true) 8 | public class ServerPeerConnectedMessage extends AbstractResponseMessage{ 9 | @Override 10 | public int getMessageType() { 11 | return ServerPeerConnectedMessage; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/monitor/DirectMemoryReporter.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.monitor; 2 | 3 | import io.netty.util.internal.PlatformDependent; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.util.ReflectionUtils; 6 | 7 | import java.lang.reflect.Field; 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 | @Slf4j 14 | public class DirectMemoryReporter { 15 | private static final String BUSINESS_KEY = "netty_direct_memory"; 16 | private static final int _1K = 1024 ; 17 | private static AtomicLong directMemory; 18 | 19 | public static void init(){ 20 | Field field = ReflectionUtils.findField(PlatformDependent.class,"DIRECT_MEMORY_COUNTER"); 21 | field.setAccessible(true); 22 | try{ 23 | directMemory = ((AtomicLong)field.get(PlatformDependent.class)); 24 | ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 25 | scheduledExecutorService.scheduleAtFixedRate(DirectMemoryReporter::doReport, 1, 1, TimeUnit.SECONDS); 26 | }catch (Exception e){ 27 | } 28 | } 29 | 30 | private static void doReport() { 31 | int memoryInb = (int) (directMemory.get()/_1K); 32 | logger.error("{}:{}K",BUSINESS_KEY,memoryInb); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/protocol/ChatMessageCodec.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.protocol; 2 | 3 | import com.bigyj.message.Message; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.ByteToMessageCodec; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.ObjectInputStream; 11 | import java.io.ObjectOutputStream; 12 | import java.util.List; 13 | 14 | 15 | public class ChatMessageCodec extends ByteToMessageCodec { 16 | 17 | @Override 18 | protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { 19 | // 1. 4 字节的魔数 20 | out.writeBytes(new byte[]{1, 2, 3, 4}); 21 | // 2. 1 字节的版本, 22 | out.writeByte(1); 23 | // 3. 1 字节的序列化方式 jdk 0 , json 1 24 | out.writeByte(0); 25 | // 4. 1 字节的指令类型 26 | out.writeByte(msg.getMessageType()); 27 | // 5. 4 个字节 28 | out.writeInt(msg.getSequenceId()); 29 | // 无意义,对齐填充 30 | out.writeByte(0xff); 31 | // 6. 获取内容的字节数组 32 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 33 | ObjectOutputStream oos = new ObjectOutputStream(bos); 34 | oos.writeObject(msg); 35 | byte[] bytes = bos.toByteArray(); 36 | // 7. 长度 37 | out.writeInt(bytes.length); 38 | // 8. 写入内容 39 | out.writeBytes(bytes); 40 | } 41 | //解密 42 | @Override 43 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { 44 | int magicNum = in.readInt(); 45 | byte version = in.readByte(); 46 | byte serializerType = in.readByte(); 47 | byte messageType = in.readByte(); 48 | int sequenceId = in.readInt(); 49 | in.readByte(); 50 | int length = in.readInt(); 51 | byte[] bytes = new byte[length]; 52 | in.readBytes(bytes, 0, length); 53 | ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); 54 | Message message = (Message) ois.readObject(); 55 | out.add(message); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/protocol/ProtocolFrameDecoder.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.protocol; 2 | 3 | import io.netty.handler.codec.LengthFieldBasedFrameDecoder; 4 | 5 | public class ProtocolFrameDecoder extends LengthFieldBasedFrameDecoder { 6 | 7 | public ProtocolFrameDecoder() { 8 | this(1024, 12, 4, 0, 0); 9 | } 10 | 11 | public ProtocolFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) { 12 | super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/user/User.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.user; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | @Data 6 | @AllArgsConstructor 7 | public class User { 8 | private String uid ; 9 | private String passWord; 10 | private String userName ; 11 | } 12 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/utils/NodeUtil.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.utils; 2 | 3 | public class NodeUtil { 4 | public static long getIdByPath(String path,String prefix) { 5 | String sid = null; 6 | if (null == path) { 7 | throw new RuntimeException("节点路径有误"); 8 | } 9 | int index = path.lastIndexOf(prefix); 10 | if (index >= 0) { 11 | index += prefix.length(); 12 | sid = index <= path.length() ? path.substring(index) : null; 13 | } 14 | 15 | if (null == sid) { 16 | throw new RuntimeException("节点ID获取失败"); 17 | } 18 | return Long.parseLong(sid); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /im-common/src/main/java/com/bigyj/utils/ThreadUtils.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.utils; 2 | 3 | import java.util.concurrent.Executor; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | import java.util.concurrent.ThreadPoolExecutor; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class ThreadUtils { 9 | private static int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 10 | private static int IO_MAX = Math.max(2, CPU_COUNT * 2); 11 | private static int KEEP_ALIVE_SECONDS = 30; 12 | private static int QUEUE_SIZE = 10000 ; 13 | 14 | public static Executor getExecutor(){ 15 | ThreadPoolExecutor executor = new ThreadPoolExecutor( 16 | IO_MAX, 17 | IO_MAX, 18 | KEEP_ALIVE_SECONDS, 19 | TimeUnit.SECONDS, 20 | new LinkedBlockingQueue(QUEUE_SIZE)); 21 | return executor; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /im-server/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.bigyj' 6 | version '1.0-SNAPSHOT' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation 'org.projectlombok:lombok:1.18.20' 14 | implementation project(':im-common') 15 | implementation libraries.netty 16 | implementation libraries.springBootStarterWeb 17 | implementation libraries.gson 18 | implementation libraries.fastjson 19 | implementation libraries.curator 20 | implementation libraries.springBootStarterDataRedis 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/ServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server; 2 | 3 | import com.bigyj.server.server.ImServer; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | 8 | @SpringBootApplication 9 | public class ServerApplication { 10 | public static void main(String[] args) throws Exception { 11 | ConfigurableApplicationContext context = SpringApplication.run(ServerApplication.class, args); 12 | startChatServer(context); 13 | } 14 | 15 | /** 16 | * 启动服务器 17 | * @param context 18 | */ 19 | private static void startChatServer(ConfigurableApplicationContext context) throws Exception { 20 | ImServer imServer =context.getBean(ImServer.class); 21 | imServer.startImServer(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/cach/SessionCacheSupport.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.cach; 2 | 3 | import com.bigyj.entity.SessionCache; 4 | 5 | public interface SessionCacheSupport { 6 | //保存连接信息 7 | void save(SessionCache s); 8 | 9 | //获取连接信息 10 | SessionCache get(String userId); 11 | 12 | //删除连接信息 13 | void remove(String userId); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/cach/SessionCacheSupportImpl.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.cach; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.bigyj.entity.SessionCache; 5 | import com.google.gson.Gson; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | @Component 13 | public class SessionCacheSupportImpl implements SessionCacheSupport{ 14 | public static final String REDIS_PREFIX = "SessionCache:id:"; 15 | @Autowired 16 | protected RedisTemplate redisTemplate; 17 | //2小时需要重新登录(秒) 18 | private static final long VALIDITY_TIME = 60 * 60 * 2; 19 | 20 | @Override 21 | public void save(SessionCache sessionCache) 22 | { 23 | String key = REDIS_PREFIX + sessionCache.getUserId(); 24 | String value = new Gson().toJson(sessionCache); 25 | redisTemplate.opsForValue().set(key, value, VALIDITY_TIME, TimeUnit.SECONDS); 26 | } 27 | 28 | 29 | @Override 30 | public SessionCache get(String userId) 31 | { 32 | String key = REDIS_PREFIX + userId; 33 | String value = (String) redisTemplate.opsForValue().get(key); 34 | 35 | if (!StringUtils.isEmpty(value)) 36 | { 37 | return JSONObject.parseObject(value, SessionCache.class); 38 | } 39 | return null; 40 | } 41 | 42 | @Override 43 | public void remove( String userId) 44 | { 45 | String key = REDIS_PREFIX + userId; 46 | redisTemplate.delete(key); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.RedisSerializer; 8 | import org.springframework.data.redis.serializer.StringRedisSerializer; 9 | 10 | @Configuration 11 | public class RedisConfig { 12 | @Bean 13 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 14 | //配置redisTemplate 15 | RedisTemplate redisTemplate = new RedisTemplate<>(); 16 | // 配置连接工厂 17 | redisTemplate.setConnectionFactory(redisConnectionFactory); 18 | RedisSerializer stringSerializer = new StringRedisSerializer(); 19 | redisTemplate.setKeySerializer(stringSerializer);//key序列化 20 | redisTemplate.setHashKeySerializer(stringSerializer);//Hash key序列化 21 | redisTemplate.afterPropertiesSet(); 22 | return redisTemplate; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/config/ZookeeperConfig.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.config; 2 | 3 | import org.apache.curator.RetryPolicy; 4 | import org.apache.curator.framework.CuratorFramework; 5 | import org.apache.curator.framework.CuratorFrameworkFactory; 6 | import org.apache.curator.retry.ExponentialBackoffRetry; 7 | 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class ZookeeperConfig { 14 | @Value("${zk.url}") 15 | private String zkUrl; 16 | 17 | @Bean("curatorFramework") 18 | public CuratorFramework curatorFramework(){ 19 | RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3); 20 | CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl,retryPolicy); 21 | client.start(); 22 | return client; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/ChatRedirectHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.message.ChatRequestMessage; 4 | import com.bigyj.message.ChatResponseMessage; 5 | import com.bigyj.server.manager.MemoryUserManager; 6 | import com.bigyj.server.manager.ServerSessionManager; 7 | import com.bigyj.server.session.AbstractServerSession; 8 | import com.bigyj.server.session.LocalSession; 9 | import io.netty.channel.ChannelHandler; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.channel.SimpleChannelInboundHandler; 12 | import io.netty.util.ReferenceCountUtil; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Component; 16 | 17 | @Slf4j 18 | @Component 19 | @ChannelHandler.Sharable 20 | public class ChatRedirectHandler extends SimpleChannelInboundHandler { 21 | @Autowired 22 | private ServerSessionManager serverSessionManager ; 23 | 24 | @Override 25 | protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception { 26 | 27 | // //反向导航 fixme 服务之间连接登录状态的问题 28 | // LocalSession session = ctx.channel().attr(LocalSession.SESSION_KEY).get(); 29 | // //判断是否登录 30 | // if (null == session || !session.isLogin()) { 31 | // logger.error("用户尚未登录,不能发送消息"); 32 | // return; 33 | // } 34 | this.action(msg,ctx); 35 | } 36 | 37 | private void action(ChatRequestMessage chatRequestMessage,ChannelHandlerContext context) { 38 | String userName = chatRequestMessage.getTo(); 39 | //判断用户是否在线 40 | AbstractServerSession serverSession = serverSessionManager.getServerSession(MemoryUserManager.getUserByName(userName).getUid()); 41 | if(serverSession == null){ 42 | this.sentNotOnlineMsg(chatRequestMessage,userName,context); 43 | }else { 44 | boolean result = serverSession.writeAndFlush(chatRequestMessage); 45 | if(!result){ 46 | } 47 | } 48 | //fixme 测试 49 | ReferenceCountUtil.retain(chatRequestMessage); 50 | } 51 | 52 | /** 53 | * 告知客户端用户不在线 54 | * @param toUserId 55 | * @param ctx 56 | */ 57 | private void sentNotOnlineMsg(ChatRequestMessage chatRequestMessage,String toUserId,ChannelHandlerContext ctx) { 58 | logger.error("用户{} 不在线,消息发送失败!",toUserId); 59 | ctx.writeAndFlush(new ChatResponseMessage(false, chatRequestMessage.getTo()+"用户不存在或者不在线")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/ChatServerRedirectHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.message.ChatResponseMessage; 4 | import com.bigyj.message.GroupRemoteChatRequestMessage; 5 | import com.bigyj.server.holder.LocalSessionHolder; 6 | import com.bigyj.server.session.LocalSession; 7 | import io.netty.channel.ChannelHandler; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.SimpleChannelInboundHandler; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.Iterator; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | /** 17 | * 接收到其他服务端转发来的请求,获取所有本地连接进行消息发送 18 | */ 19 | @Slf4j 20 | @Component 21 | @ChannelHandler.Sharable 22 | public class ChatServerRedirectHandler extends SimpleChannelInboundHandler { 23 | 24 | @Override 25 | protected void channelRead0(ChannelHandlerContext ctx, GroupRemoteChatRequestMessage msg) throws Exception { 26 | //发送给连接到自身服务器的客户端 27 | ConcurrentHashMap allSession = LocalSessionHolder.getAll(); 28 | Iterator iter = allSession.keySet().iterator(); 29 | while (iter.hasNext()) { 30 | LocalSession session = allSession.get(iter.next()); 31 | session.getChannel().writeAndFlush(new ChatResponseMessage(msg.getFrom(), msg.getContent())); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/DisconnectedHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.server.cach.SessionCacheSupport; 4 | import com.bigyj.server.holder.LocalSessionHolder; 5 | import com.bigyj.server.session.LocalSession; 6 | import io.netty.channel.ChannelHandler; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.ChannelInboundHandlerAdapter; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | /** 14 | * 客户端退出处理逻辑 15 | */ 16 | @Slf4j 17 | @Component 18 | @ChannelHandler.Sharable 19 | public class DisconnectedHandler extends ChannelInboundHandlerAdapter { 20 | @Autowired 21 | private SessionCacheSupport sessionCacheSupport; 22 | 23 | @Override 24 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 25 | super.channelRead(ctx, msg); 26 | } 27 | 28 | @Override 29 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 30 | this.clientDisconnected(ctx); 31 | logger.info("客户端断开连接"); 32 | } 33 | 34 | @Override 35 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 36 | this.clientDisconnected(ctx); 37 | logger.info("客户端异常断开连接"); 38 | 39 | } 40 | 41 | /** 42 | * 客户单主动断开连接或异常断开连接 43 | * @param ctx 44 | */ 45 | private void clientDisconnected(ChannelHandlerContext ctx) { 46 | try { 47 | LocalSession session = LocalSession.getSession(ctx); 48 | String userId = session.getUserId(); 49 | LocalSessionHolder.removeServerSession(userId); 50 | //保存channel信息 51 | LocalSession serverSession = new LocalSession(ctx.channel()); 52 | serverSession.setLogin(false); 53 | //移除redis缓存新 54 | sessionCacheSupport.remove(userId); 55 | }finally { 56 | ctx.channel().close(); 57 | ctx.close(); 58 | System.gc(); 59 | } 60 | 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/GroupMessageSendHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.message.ChatResponseMessage; 4 | import com.bigyj.message.GroupChatRequestMessage; 5 | import com.bigyj.message.GroupRemoteChatRequestMessage; 6 | import com.bigyj.server.holder.LocalSessionHolder; 7 | import com.bigyj.server.holder.ServerPeerSenderHolder; 8 | import com.bigyj.server.server.ServerPeerSender; 9 | import com.bigyj.server.session.LocalSession; 10 | import io.netty.channel.ChannelHandler; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | import java.util.Iterator; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | @ChannelHandler.Sharable 19 | @Slf4j 20 | public class GroupMessageSendHandler extends SimpleChannelInboundHandler { 21 | @Override 22 | protected void channelRead0(ChannelHandlerContext ctx, GroupChatRequestMessage msg) throws Exception { 23 | //发送给连接到自身服务器的客户端 24 | ConcurrentHashMap allSession = LocalSessionHolder.getAll(); 25 | Iterator iter = allSession.keySet().iterator(); 26 | while (iter.hasNext()) { 27 | LocalSession session = allSession.get(iter.next()); 28 | session.getChannel().writeAndFlush(new ChatResponseMessage(msg.getFrom(), msg.getContent())); 29 | } 30 | //处理群发远程消息 31 | ConcurrentHashMap allRemote = ServerPeerSenderHolder.getAll(); 32 | for(Long key : allRemote.keySet()) { 33 | allRemote.get(key).getChannel().writeAndFlush(new GroupRemoteChatRequestMessage(msg.getFrom(),msg.getContent())); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/LoginRequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.message.LoginRequestMessage; 4 | import com.bigyj.message.LoginResponseMessage; 5 | import com.bigyj.server.manager.MemoryUserManager; 6 | import com.bigyj.server.manager.ServerSessionManager; 7 | import com.bigyj.server.session.LocalSession; 8 | import com.bigyj.user.User; 9 | import io.netty.channel.*; 10 | import io.netty.handler.timeout.IdleState; 11 | import io.netty.handler.timeout.IdleStateEvent; 12 | import io.netty.handler.timeout.IdleStateHandler; 13 | import io.netty.util.ReferenceCountUtil; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Component; 17 | 18 | @Slf4j 19 | @Component 20 | @ChannelHandler.Sharable 21 | public class LoginRequestHandler extends SimpleChannelInboundHandler { 22 | @Autowired 23 | private ServerSessionManager serverSessionManager ; 24 | @Autowired 25 | private DisconnectedHandler disconnectedHandler ; 26 | 27 | private GroupMessageSendHandler groupMessageSendHandler = new GroupMessageSendHandler(); 28 | @Autowired 29 | private ChatRedirectHandler chatRedirectHandler ; 30 | 31 | @Override 32 | protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage loginRequestMessage) throws Exception { 33 | 34 | String username = loginRequestMessage.getUsername(); 35 | String passWord = loginRequestMessage.getPassword(); 36 | boolean validateSuccess = this.validateUser(username, passWord); 37 | if(!validateSuccess){ 38 | //登录失败,发送失败消息 39 | LoginResponseMessage responseMessage = new LoginResponseMessage(false,"失败"); 40 | ctx.writeAndFlush(responseMessage); 41 | return; 42 | } 43 | //保存channel信息 44 | LocalSession serverSession = new LocalSession(ctx.channel()); 45 | serverSession.setUser(MemoryUserManager.getUserByName(username)); 46 | serverSession.bind(); 47 | 48 | //连接信息保存至redis数据库 49 | serverSessionManager.addServerSession(serverSession); 50 | //发送登录响应信息 51 | LoginResponseMessage responseMessage = new LoginResponseMessage(validateSuccess,"登录成功"); 52 | ctx.writeAndFlush(responseMessage).addListener(future -> { 53 | //消息发送成功 54 | if (future.isSuccess()) { 55 | //增加心跳handler 56 | ctx.pipeline().addLast(new IdleStateHandler(200, 0, 0)); 57 | // ChannelDuplexHandler 可以同时作为入站和出站处理器 58 | ctx.pipeline().addLast(new ChannelDuplexHandler() { 59 | // 用来触发特殊事件 60 | @Override 61 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ 62 | IdleStateEvent event = (IdleStateEvent) evt; 63 | // 触发了读空闲事件· 64 | if (event.state() == IdleState.READER_IDLE) { 65 | logger.info("已经 200s 没有读到数据了"); 66 | ctx.channel().close(); 67 | } 68 | } 69 | }); 70 | //增加聊天的handler 71 | ctx.pipeline().addLast("chat", chatRedirectHandler); 72 | //增加群聊handler 73 | ctx.pipeline().addLast("groupChat", groupMessageSendHandler); 74 | //增加退出的handler 75 | ctx.pipeline().addLast("logout", disconnectedHandler); 76 | }else { 77 | logger.error("登录响应消息发送失败!"); 78 | } 79 | }); 80 | } 81 | 82 | private boolean validateUser(String userName, String password) { 83 | User user = MemoryUserManager.getUserByName(userName); 84 | if(user!=null){ 85 | return user.getPassWord().equals(password); 86 | } 87 | return false; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/handler/ServerPeerConnectedHandler.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.handler; 2 | 3 | import com.bigyj.message.ServerPeerConnectedMessage; 4 | import io.netty.channel.ChannelDuplexHandler; 5 | import io.netty.channel.ChannelHandler; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.SimpleChannelInboundHandler; 8 | import io.netty.handler.timeout.IdleState; 9 | import io.netty.handler.timeout.IdleStateEvent; 10 | import io.netty.handler.timeout.IdleStateHandler; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | @ChannelHandler.Sharable 16 | @Slf4j 17 | @Component 18 | public class ServerPeerConnectedHandler extends SimpleChannelInboundHandler { 19 | @Autowired 20 | private ChatServerRedirectHandler chatServerRedirectHandler ; 21 | @Autowired 22 | private DisconnectedHandler disconnectedHandler ; 23 | 24 | @Override 25 | protected void channelRead0(ChannelHandlerContext ctx, ServerPeerConnectedMessage msg) throws Exception { 26 | ctx.pipeline().addLast(new IdleStateHandler(200, 0, 0)); 27 | // ChannelDuplexHandler 可以同时作为入站和出站处理器 28 | ctx.pipeline().addLast(new ChannelDuplexHandler() { 29 | // 用来触发特殊事件 30 | @Override 31 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ 32 | IdleStateEvent event = (IdleStateEvent) evt; 33 | // 触发了读空闲事件· 34 | if (event.state() == IdleState.READER_IDLE) { 35 | logger.info("已经 200s 没有读到数据了"); 36 | ctx.channel().close(); 37 | } 38 | } 39 | }); 40 | //增加聊天的handler 41 | ctx.pipeline().addLast("chat", chatServerRedirectHandler); 42 | //增加退出的handler 43 | ctx.pipeline().addLast("logout", disconnectedHandler); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/holder/LocalSessionHolder.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.holder; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | import com.bigyj.server.session.LocalSession; 6 | 7 | public class LocalSessionHolder { 8 | 9 | private static ConcurrentHashMap serverSessions 10 | = new ConcurrentHashMap<>(); 11 | 12 | public static void addServerSession(LocalSession serverSession){ 13 | serverSessions.putIfAbsent(serverSession.getUser().getUid(),serverSession); 14 | } 15 | 16 | public static LocalSession getServerSession(String userId){ 17 | return serverSessions.get(userId); 18 | } 19 | 20 | public static LocalSession removeServerSession(String userId){ 21 | return serverSessions.remove(userId); 22 | } 23 | 24 | public static ConcurrentHashMap getAll(){ 25 | return serverSessions; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/holder/ServerPeerSenderHolder.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.holder; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | import com.bigyj.server.server.ServerPeerSender; 6 | 7 | /** 8 | * 服务器间链接管理holder 9 | */ 10 | public class ServerPeerSenderHolder { 11 | private static ConcurrentHashMap serverSenders = 12 | new ConcurrentHashMap<>(); 13 | 14 | public static void addWorker(Long key,ServerPeerSender serverPeerSender){ 15 | serverSenders.put(key,serverPeerSender); 16 | } 17 | 18 | public static ServerPeerSender getWorker(Long key){ 19 | return serverSenders.get(key); 20 | } 21 | 22 | public static void removeWorker(Long key){ 23 | serverSenders.remove(key); 24 | } 25 | 26 | public static ConcurrentHashMap getAll(){ 27 | return serverSenders; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/initializer/ImServerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.initializer; 2 | 3 | import com.bigyj.protocol.ChatMessageCodec; 4 | import com.bigyj.protocol.ProtocolFrameDecoder; 5 | import com.bigyj.server.handler.LoginRequestHandler; 6 | import com.bigyj.server.handler.ServerPeerConnectedHandler; 7 | import io.netty.channel.ChannelInitializer; 8 | import io.netty.channel.ChannelPipeline; 9 | import io.netty.channel.socket.SocketChannel; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class ImServerInitializer extends ChannelInitializer { 15 | private LoginRequestHandler loginRequestHandler; 16 | private ServerPeerConnectedHandler serverPeerConnectedHandler ; 17 | @Autowired 18 | public ImServerInitializer(LoginRequestHandler loginRequestHandler, ServerPeerConnectedHandler serverPeerConnectedHandler) { 19 | this.loginRequestHandler = loginRequestHandler; 20 | this.serverPeerConnectedHandler = serverPeerConnectedHandler; 21 | } 22 | 23 | 24 | @Override 25 | protected void initChannel(SocketChannel ch) throws Exception { 26 | ChannelPipeline pipeline = ch.pipeline(); 27 | //处理粘包与半包 28 | pipeline.addLast(new ProtocolFrameDecoder()); 29 | //加入新的协议编码与界面器 30 | pipeline.addLast(new ChatMessageCodec()); 31 | pipeline.addLast(serverPeerConnectedHandler); 32 | pipeline.addLast("login",loginRequestHandler); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/manager/MemoryUserManager.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.manager; 2 | 3 | import com.bigyj.user.User; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | @Component 9 | public class MemoryUserManager { 10 | private static Map users = new ConcurrentHashMap<>(); 11 | static { 12 | users.put("tom",new User("1","123","tom")); 13 | users.put("jerry",new User("2","123","jerry")); 14 | users.put("betty",new User("3","123","betty")); 15 | users.put("cook",new User("4","123","cook")); 16 | 17 | } 18 | public static User getUserByName(String userName) { 19 | return users.get(userName); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/manager/ServerSessionManager.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.manager; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | import com.bigyj.entity.SessionCache; 5 | import com.bigyj.server.cach.SessionCacheSupport; 6 | import com.bigyj.server.holder.LocalSessionHolder; 7 | import com.bigyj.server.session.AbstractServerSession; 8 | import com.bigyj.server.session.LocalSession; 9 | import com.bigyj.server.session.RemoteSession; 10 | import com.bigyj.server.worker.ServerWorker; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class ServerSessionManager { 16 | @Autowired 17 | private SessionCacheSupport sessionCacheSupport; 18 | /** 19 | * 根据userid获取session 20 | * @param userId 21 | * @return 22 | */ 23 | public AbstractServerSession getServerSession(String userId){ 24 | AbstractServerSession serverSession = null; 25 | SessionCache sessionCache = sessionCacheSupport.get(userId); 26 | //redis中查询不到相关客户端 27 | if(sessionCache ==null){ 28 | return null; 29 | } 30 | //判断消息接受者是不是连接当前服务 31 | ServerNode cacheServerNode = sessionCache.getServerNode(); 32 | ServerNode serverNode = ServerWorker.instance().getServerNode(); 33 | 34 | if(serverNode.getAddress().equals(cacheServerNode.getAddress())){ 35 | //当前服务 36 | serverSession = LocalSessionHolder.getServerSession(userId); 37 | }else { 38 | //远程服务 39 | serverSession = new RemoteSession(sessionCache); 40 | } 41 | return serverSession; 42 | } 43 | 44 | /** 45 | * 保存servreSession 46 | * @param localSession 47 | */ 48 | public void addServerSession(LocalSession localSession){ 49 | //添加至本地session 50 | LocalSessionHolder.addServerSession(localSession); 51 | //存储至redis数据库中 52 | String sessionId = localSession.getSessionId(); 53 | String userId = localSession.getUserId(); 54 | ServerNode serverNode = ServerWorker.instance().getServerNode(); 55 | SessionCache sessionCache = new SessionCache(userId,sessionId,serverNode) ; 56 | sessionCacheSupport.save(sessionCache); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/registration/CuratorZKclient.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.registration; 2 | 3 | import com.bigyj.server.utils.SpringContextUtil; 4 | import org.apache.curator.framework.CuratorFramework; 5 | 6 | public class CuratorZKclient { 7 | private static CuratorFramework singleton = null; 8 | public static CuratorFramework getSingleton() 9 | { 10 | if (null == singleton) 11 | { 12 | singleton = SpringContextUtil.getBean(CuratorFramework.class); 13 | } 14 | return singleton; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/registration/ZkService.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.registration; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | 5 | public interface ZkService { 6 | boolean checkNodeExists(String path) throws Exception; 7 | 8 | String createPersistentNode(String path) throws Exception; 9 | 10 | String createNode(String prefix , ServerNode serverNode) throws Exception; 11 | } 12 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/registration/ZkServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.registration; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | import com.google.gson.Gson; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.curator.framework.CuratorFramework; 9 | import org.apache.zookeeper.CreateMode; 10 | import org.apache.zookeeper.data.Stat; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | @Slf4j 16 | @Component 17 | public class ZkServiceImpl implements ZkService { 18 | @Autowired 19 | CuratorFramework curatorFramework; 20 | 21 | 22 | @Override 23 | public boolean checkNodeExists(String path) throws Exception { 24 | Stat stat = curatorFramework.checkExists().forPath(path); 25 | return stat==null?false:true; 26 | } 27 | 28 | @Override 29 | public String createPersistentNode(String path) throws Exception { 30 | String pathRegistered = curatorFramework.create() 31 | .creatingParentsIfNeeded() 32 | .withProtection() 33 | .withMode(CreateMode.PERSISTENT) 34 | .forPath(path); 35 | return pathRegistered; 36 | } 37 | 38 | @Override 39 | public String createNode(String prefix, ServerNode serverNode) throws Exception { 40 | byte[] payload = new Gson().toJson(serverNode).getBytes(StandardCharsets.UTF_8); 41 | String pathRegistered = curatorFramework.create() 42 | .creatingParentsIfNeeded() 43 | .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) 44 | .forPath(prefix, payload); 45 | return pathRegistered; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/server/ImServer.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.server; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import com.bigyj.entity.ServerNode; 6 | import com.bigyj.monitor.DirectMemoryReporter; 7 | import com.bigyj.server.initializer.ImServerInitializer; 8 | import com.bigyj.server.registration.ZkService; 9 | import com.bigyj.server.worker.ServerRouterWorker; 10 | import com.bigyj.server.worker.ServerWorker; 11 | import com.bigyj.utils.NodeUtil; 12 | import io.netty.bootstrap.ServerBootstrap; 13 | import io.netty.channel.ChannelFuture; 14 | import io.netty.channel.EventLoopGroup; 15 | import io.netty.channel.nio.NioEventLoopGroup; 16 | import io.netty.channel.socket.nio.NioServerSocketChannel; 17 | import io.netty.util.concurrent.Future; 18 | import io.netty.util.concurrent.GenericFutureListener; 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.stereotype.Service; 24 | 25 | @Service("imServer") 26 | @Slf4j 27 | public class ImServer { 28 | @Value("${chat.server.port}") 29 | private int PORT ; 30 | private static final String MANAGE_PATH ="/im/nodes"; 31 | public static final String PATH_PREFIX = MANAGE_PATH + "/seq-"; 32 | private ZkService zkService; 33 | private ImServerInitializer imServerInitializer; 34 | @Autowired 35 | ImServer(ZkService zkService,ImServerInitializer imServerInitializer ){ 36 | this.zkService = zkService; 37 | this.imServerInitializer = imServerInitializer ; 38 | } 39 | 40 | private EventLoopGroup bossGroup ; 41 | 42 | private EventLoopGroup workerGroup ; 43 | 44 | public void startImServer() { 45 | bossGroup = new NioEventLoopGroup(); 46 | workerGroup = new NioEventLoopGroup(); 47 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 48 | try { 49 | serverBootstrap.group(bossGroup, workerGroup) 50 | //2 设置nio类型的channel 51 | .channel(NioServerSocketChannel.class) 52 | .childHandler(imServerInitializer) 53 | .localAddress(new InetSocketAddress(PORT)); 54 | // 通过调用sync同步方法阻塞直到绑定成功 55 | ChannelFuture channelFuture = serverBootstrap.bind().sync(); 56 | logger.info("服务启动, 端口 " +channelFuture.channel().localAddress()); 57 | channelFuture.addListener(new GenericFutureListener>() { 58 | @Override 59 | public void operationComplete(Future future) throws Exception { 60 | if (future.isSuccess()) { 61 | logger.info("服务端启动成功"); 62 | //注册到zookeeper 63 | //判断根节点是否存在 64 | if (zkService.checkNodeExists(MANAGE_PATH)) { 65 | zkService.createPersistentNode(MANAGE_PATH); 66 | } 67 | ServerNode serverNode = new ServerNode("127.0.0.1",PORT); 68 | String pathRegistered = zkService.createNode(PATH_PREFIX, serverNode); 69 | //为node 设置id 70 | serverNode.setId(NodeUtil.getIdByPath(pathRegistered,PATH_PREFIX)); 71 | logger.info("本地节点, path={}, id={}", pathRegistered, serverNode.getId()); 72 | ServerWorker.instance().setServerNode(serverNode); 73 | ServerRouterWorker.instance().init(); 74 | DirectMemoryReporter.init(); 75 | } else { 76 | logger.error("服务端启动成失败"); 77 | } 78 | } 79 | }); 80 | // 7 监听通道关闭事件 81 | // 应用程序会一直等待,直到channel关闭 82 | ChannelFuture closeFuture = 83 | channelFuture.channel().closeFuture(); 84 | closeFuture.sync(); 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } finally { 88 | // 8 优雅关闭EventLoopGroup, 89 | // 释放掉所有资源包括创建的线程 90 | bossGroup.shutdownGracefully(); 91 | workerGroup.shutdownGracefully(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/server/ServerPeerSender.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.server; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | import com.bigyj.message.ChatRequestMessage; 5 | import com.bigyj.message.LoginRequestMessage; 6 | import com.bigyj.message.PingMessage; 7 | import com.bigyj.message.ServerPeerConnectedMessage; 8 | import com.bigyj.protocol.ChatMessageCodec; 9 | import com.bigyj.protocol.ProtocolFrameDecoder; 10 | import com.bigyj.server.worker.ServerWorker; 11 | import io.netty.bootstrap.Bootstrap; 12 | import io.netty.channel.*; 13 | import io.netty.channel.nio.NioEventLoopGroup; 14 | import io.netty.channel.socket.SocketChannel; 15 | import io.netty.channel.socket.nio.NioSocketChannel; 16 | import io.netty.handler.timeout.IdleState; 17 | import io.netty.handler.timeout.IdleStateEvent; 18 | import io.netty.handler.timeout.IdleStateHandler; 19 | import lombok.Data; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | /** 23 | * 作为客户点连接其他服务(服务器间连接建立) 24 | */ 25 | @Data 26 | @Slf4j 27 | public class ServerPeerSender { 28 | private Channel channel ; 29 | private Bootstrap bootstrap; 30 | private EventLoopGroup eventLoopGroup; 31 | private static final int WRITE_IDLE_GAP = 150; 32 | 33 | public ServerPeerSender() { 34 | bootstrap = new Bootstrap(); 35 | eventLoopGroup = new NioEventLoopGroup(); 36 | } 37 | public void doConnectedServer(ServerNode serverNode){ 38 | try { 39 | bootstrap.group(eventLoopGroup) 40 | .channel(NioSocketChannel.class) 41 | .handler(new ChannelInitializer() { 42 | @Override 43 | protected void initChannel(SocketChannel ch) throws Exception { 44 | ChannelPipeline pipeline = ch.pipeline(); 45 | pipeline.addLast(new ProtocolFrameDecoder()); 46 | //编解码handler 47 | pipeline.addLast("codec", new ChatMessageCodec()); 48 | //增加心跳 49 | pipeline.addLast(new IdleStateHandler(0, WRITE_IDLE_GAP, 0)); 50 | // ChannelDuplexHandler 可以同时作为入站和出站处理器 51 | pipeline.addLast(new ChannelDuplexHandler() { 52 | // 用来触发特殊事件 53 | @Override 54 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ 55 | IdleStateEvent event = (IdleStateEvent) evt; 56 | // 触发了写空闲事件 57 | if (event.state() == IdleState.WRITER_IDLE) { 58 | logger.debug("{} 没有写数据了,发送一个心跳包[服务间]",WRITE_IDLE_GAP); 59 | ctx.writeAndFlush(new PingMessage()); 60 | } 61 | } 62 | //服务端认证 63 | @Override 64 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 65 | ctx.writeAndFlush(new ServerPeerConnectedMessage()); 66 | } 67 | }); 68 | //服务间的重连 fixme 69 | // pipeline.addLast("serverException",new ServerExceptionHandler()); 70 | } 71 | }); 72 | Channel connectChannel = bootstrap.connect(serverNode.getHost(), serverNode.getPort()).sync().channel(); 73 | //增加是连接成功监听、连接重试机制及连接关闭监听 74 | this.channel = connectChannel; 75 | logger.info("服务端{}作为客户端,加入{}成功", 76 | ServerWorker.instance().getServerNode().getAddress(), 77 | serverNode.getAddress()); 78 | }catch (Exception e){ 79 | e.printStackTrace(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/session/AbstractServerSession.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.session; 2 | 3 | import com.bigyj.message.GroupChatRequestMessage; 4 | 5 | public abstract class AbstractServerSession implements ServerSession{ 6 | @Override 7 | public boolean writeGroupMessage(GroupChatRequestMessage groupChatRequestMessage) { 8 | return false; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/session/LocalSession.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.session; 2 | 3 | import com.bigyj.message.ChatRequestMessage; 4 | import com.bigyj.message.ChatResponseMessage; 5 | import com.bigyj.server.holder.LocalSessionHolder; 6 | import com.bigyj.server.manager.MemoryUserManager; 7 | import com.bigyj.user.User; 8 | import io.netty.channel.Channel; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.util.AttributeKey; 11 | import lombok.Data; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | import java.util.UUID; 15 | 16 | @Slf4j 17 | @Data 18 | public class LocalSession extends AbstractServerSession { 19 | public static final AttributeKey KEY_USER_ID = 20 | AttributeKey.valueOf("KEY_USER_ID"); 21 | 22 | public static final AttributeKey SESSION_KEY = 23 | AttributeKey.valueOf("SESSION_KEY"); 24 | //通道 25 | private Channel channel; 26 | //用户 27 | private User user; 28 | //sessionId 29 | private final String sessionId; 30 | //是否登录 31 | private boolean isLogin = false; 32 | 33 | public LocalSession(Channel channel) { 34 | this.channel = channel; 35 | this.sessionId = buildNewSessionId(); 36 | } 37 | 38 | @Override 39 | public boolean writeAndFlush(ChatRequestMessage chatRequestMessage) { 40 | //获取channel,发送消息 41 | String userName = chatRequestMessage.getTo(); 42 | ChatResponseMessage chatResponseMessage = new ChatResponseMessage(chatRequestMessage.getFrom(),chatRequestMessage.getContent()) ; 43 | LocalSession localSession = LocalSessionHolder.getServerSession(MemoryUserManager.getUserByName(userName).getUid()); 44 | if(localSession== null){ 45 | return false; 46 | }else { 47 | localSession.getChannel().writeAndFlush(chatResponseMessage); 48 | return true; 49 | } 50 | } 51 | 52 | 53 | @Override 54 | public String getSessionId() { 55 | return sessionId; 56 | } 57 | 58 | @Override 59 | public boolean isValid() { 60 | return getUser() != null ? true : false; 61 | } 62 | 63 | 64 | @Override 65 | public String getUserId() { 66 | return user.getUid(); 67 | } 68 | 69 | private static String buildNewSessionId() { 70 | String uuid = UUID.randomUUID().toString(); 71 | return uuid.replaceAll("-", ""); 72 | } 73 | 74 | //反向导航 75 | public static LocalSession getSession(ChannelHandlerContext ctx) { 76 | Channel channel = ctx.channel(); 77 | return channel.attr(LocalSession.SESSION_KEY).get(); 78 | } 79 | 80 | /** 81 | * ServerSession 绑定会话 82 | */ 83 | public LocalSession bind() { 84 | logger.info(" ServerSession 绑定会话 " + channel.remoteAddress()); 85 | channel.attr(LocalSession.SESSION_KEY).set(this); 86 | LocalSessionHolder.addServerSession(this); 87 | isLogin = true; 88 | return this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/session/RemoteSession.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.session; 2 | 3 | import com.bigyj.entity.SessionCache; 4 | import com.bigyj.message.ChatRequestMessage; 5 | import com.bigyj.server.server.ServerPeerSender; 6 | import com.bigyj.server.worker.ServerRouterWorker; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class RemoteSession extends AbstractServerSession { 11 | private SessionCache sessionCache; 12 | 13 | public RemoteSession(SessionCache sessionCache) { 14 | this.sessionCache = sessionCache; 15 | } 16 | 17 | @Override 18 | public boolean writeAndFlush(ChatRequestMessage chatRequestMessage) { 19 | //获取对方用户所在的服务器,将消息转发至对方服务器(该服务器作为客户端)。 20 | long id = sessionCache.getServerNode().getId(); 21 | ServerPeerSender serverPeerSender = ServerRouterWorker.instance().router(id); 22 | serverPeerSender.getChannel().writeAndFlush(chatRequestMessage); 23 | return true; 24 | } 25 | 26 | @Override 27 | public String getSessionId() { 28 | return null; 29 | } 30 | 31 | @Override 32 | public boolean isValid() { 33 | return false; 34 | } 35 | 36 | @Override 37 | public String getUserId() { 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/session/ServerSession.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.session; 2 | 3 | import com.bigyj.message.ChatRequestMessage; 4 | import com.bigyj.message.GroupChatRequestMessage; 5 | 6 | public interface ServerSession { 7 | 8 | boolean writeAndFlush(ChatRequestMessage chatRequestMessage); 9 | 10 | boolean writeGroupMessage(GroupChatRequestMessage groupChatRequestMessage ); 11 | 12 | String getSessionId(); 13 | 14 | /** 15 | * 是否有效 16 | * @return 17 | */ 18 | boolean isValid(); 19 | 20 | /** 21 | * 获获取用户 22 | * @return 用户id 23 | */ 24 | String getUserId(); 25 | } 26 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/utils/SpringContextUtil.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Spring上下文工具, 用户获取bean或者HttpServletRequest 10 | */ 11 | @Component 12 | public class SpringContextUtil implements ApplicationContextAware 13 | { 14 | 15 | /** 16 | * 上下文对象实例 17 | */ 18 | private static ApplicationContext applicationContext; 19 | 20 | @Override 21 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 22 | { 23 | SpringContextUtil.applicationContext = applicationContext; 24 | } 25 | 26 | public static void setContext(ApplicationContext applicationContext) 27 | { 28 | SpringContextUtil.applicationContext = applicationContext; 29 | } 30 | 31 | public static ApplicationContext getApplicationContext() 32 | { 33 | return applicationContext; 34 | } 35 | 36 | /** 37 | * 通过name获取 Bean. 38 | */ 39 | public static T getBean(String name) 40 | { 41 | return (T) applicationContext.getBean(name); 42 | } 43 | 44 | /** 45 | * 通过class获取Bean. 46 | */ 47 | public static T getBean(Class clazz) 48 | { 49 | if (null == applicationContext) 50 | { 51 | return null; 52 | } 53 | return applicationContext.getBean(clazz); 54 | } 55 | 56 | /** 57 | * 通过name,以及Clazz返回指定的Bean 58 | */ 59 | public static T getBean(String name, Class clazz) 60 | { 61 | return applicationContext.getBean(name, clazz); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/worker/ServerRouterWorker.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.worker; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.bigyj.entity.ServerNode; 5 | import com.bigyj.server.holder.ServerPeerSenderHolder; 6 | import com.bigyj.server.registration.CuratorZKclient; 7 | import com.bigyj.server.server.ServerPeerSender; 8 | import com.bigyj.utils.NodeUtil; 9 | import com.bigyj.utils.ThreadUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.curator.framework.CuratorFramework; 12 | import org.apache.curator.framework.recipes.cache.ChildData; 13 | import org.apache.curator.framework.recipes.cache.PathChildrenCache; 14 | import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; 15 | import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; 16 | 17 | @Slf4j 18 | public class ServerRouterWorker { 19 | 20 | private static final String MANAGE_PATH ="/im/nodes"; 21 | public static final String PATH_PREFIX = MANAGE_PATH + "/seq-"; 22 | 23 | private static final ServerRouterWorker instance = new ServerRouterWorker(); 24 | public static ServerRouterWorker instance(){ 25 | return instance; 26 | } 27 | 28 | private boolean inited = false; 29 | 30 | public void init() throws Exception { 31 | if(inited){ 32 | return; 33 | } 34 | CuratorFramework curatorFramework = CuratorZKclient.getSingleton(); 35 | //订阅节点的增加和删除事件 36 | PathChildrenCache childrenCache = new PathChildrenCache(curatorFramework, MANAGE_PATH, true); 37 | PathChildrenCacheListener childrenCacheListener = new PathChildrenCacheListener() { 38 | @Override 39 | public void childEvent(CuratorFramework client, 40 | PathChildrenCacheEvent event) throws Exception { 41 | ChildData data = event.getData(); 42 | switch (event.getType()) { 43 | case CHILD_ADDED: 44 | logger.info("CHILD_ADDED : " + data.getPath()); 45 | processAdd(data); 46 | break; 47 | case CHILD_REMOVED: 48 | logger.info("CHILD_REMOVED : " + data.getPath()); 49 | break; 50 | case CHILD_UPDATED: 51 | logger.info("CHILD_UPDATED : " + data.getPath()); 52 | break; 53 | default: 54 | logger.debug("[PathChildrenCache]节点数据为空, path={}", data == null ? "null" : data.getPath()); 55 | break; 56 | } 57 | } 58 | }; 59 | childrenCache.getListenable().addListener( 60 | childrenCacheListener, ThreadUtils.getExecutor()); 61 | logger.info("Register zk watcher successfully!"); 62 | childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT); 63 | this.inited = true ; 64 | } 65 | 66 | /** 67 | * zk节点新增 68 | * @param data 69 | */ 70 | private void processAdd(ChildData data) { 71 | ServerPeerSender serverPeerSender = new ServerPeerSender(); 72 | long id = NodeUtil.getIdByPath(data.getPath(), PATH_PREFIX); 73 | ServerNode serverNode = JSONObject.parseObject(data.getData(), ServerNode.class); 74 | serverNode.setId(id); 75 | if(ServerWorker.instance().getServerNode().getAddress().equals(serverNode.getAddress())){ 76 | logger.info("监听到自身节点加入,无需进行连接!"); 77 | return; 78 | } 79 | serverPeerSender.doConnectedServer(serverNode); 80 | logger.info("新节点加入:{}",serverNode); 81 | ServerPeerSenderHolder.addWorker(id,serverPeerSender); 82 | } 83 | 84 | /** 85 | * 路由到某个节点 86 | * @param id 87 | * @return 88 | */ 89 | public ServerPeerSender router(long id) { 90 | ServerPeerSender worker = ServerPeerSenderHolder.getWorker(id); 91 | return worker; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /im-server/src/main/java/com/bigyj/server/worker/ServerWorker.java: -------------------------------------------------------------------------------- 1 | package com.bigyj.server.worker; 2 | 3 | import com.bigyj.entity.ServerNode; 4 | import lombok.Data; 5 | 6 | /** 7 | * 服务端zookeeper协调者(zookeepr协调客户端) 8 | */ 9 | @Data 10 | public class ServerWorker { 11 | private ServerNode serverNode ; 12 | /** 13 | *饿汉式创建ServerWorker单例 14 | */ 15 | private static final ServerWorker instance = new ServerWorker(); 16 | public static ServerWorker instance(){ 17 | return instance; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /im-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | ############### 5 | #聊天服务配置 6 | ############### 7 | chat: 8 | server: 9 | port: 18080 10 | 11 | ############### 12 | #ZK服务配置 13 | ############### 14 | zk: 15 | url: 127.0.0.1:2181 16 | timeout: 3000 17 | 18 | spring: 19 | ################################ 20 | # Redis配置 21 | ################################ 22 | redis: 23 | host: demo.redis.cn 24 | port: 6379 25 | password: 123456 -------------------------------------------------------------------------------- /im-server/src/test/java/MemoryTest.java: -------------------------------------------------------------------------------- 1 | import com.bigyj.monitor.DirectMemoryReporter; 2 | import io.netty.buffer.ByteBuf; 3 | import io.netty.buffer.ByteBufAllocator; 4 | import io.netty.buffer.PooledByteBufAllocator; 5 | 6 | public class MemoryTest { 7 | public static void main(String[] args) { 8 | DirectMemoryReporter.init(); 9 | 10 | ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; 11 | 12 | //tiny规格内存分配 会变成大于等于16的整数倍的数:这里254 会规格化为256 13 | ByteBuf byteBuf = alloc.directBuffer(254); 14 | 15 | //读写bytebuf 16 | byteBuf.writeInt(126); 17 | System.out.println(byteBuf.readInt()); 18 | 19 | //很重要,内存释放 20 | byteBuf.release(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /im-server/src/test/java/Test.java: -------------------------------------------------------------------------------- 1 | import io.netty.buffer.ByteBufAllocator; 2 | 3 | public class Test { 4 | 5 | // static final ByteBufAllocator DEFAULT_ALLOCATOR; 6 | // static { 7 | // // 系统变量中取值,若为安卓平台则使用 unpooled 8 | // // String allocType = SystemPropertyUtil.get( "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled"); 9 | // // allocType = allocType.toLowerCase(Locale.US).trim(); 10 | // ByteBufAllocator alloc; 11 | // if ("unpooled".equals(allocType)) { 12 | // alloc = UnpooledByteBufAllocator.DEFAULT; 13 | // logger.debug("-Dio.netty.allocator.type: {}", allocType); 14 | // } else if ("pooled".equals(allocType)) { 15 | // alloc = PooledByteBufAllocator.DEFAULT; 16 | // logger.debug("-Dio.netty.allocator.type: {}", allocType); 17 | // } else { // 默认为内存池 18 | // alloc = PooledByteBufAllocator.DEFAULT; 19 | // logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType); 20 | // } 21 | // DEFAULT_ALLOCATOR = alloc; 22 | // THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0); 23 | // logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE); 24 | // MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024); 25 | // logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE); 26 | // } 27 | } 28 | -------------------------------------------------------------------------------- /im-server/src/test/java/client/ClientInitializer.java: -------------------------------------------------------------------------------- 1 | package client; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.ChannelPipeline; 5 | import io.netty.channel.socket.SocketChannel; 6 | import io.netty.handler.codec.DelimiterBasedFrameDecoder; 7 | import io.netty.handler.codec.Delimiters; 8 | import io.netty.handler.codec.string.StringDecoder; 9 | import io.netty.handler.codec.string.StringEncoder; 10 | import io.netty.util.CharsetUtil; 11 | 12 | public class ClientInitializer extends ChannelInitializer { 13 | @Override 14 | protected void initChannel(SocketChannel ch) throws Exception { 15 | ChannelPipeline pipeline = ch.pipeline(); 16 | pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter())); 17 | pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); 18 | pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); 19 | pipeline.addLast(new MyClientHandler()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /im-server/src/test/java/client/MyClient.java: -------------------------------------------------------------------------------- 1 | package client; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.EventLoopGroup; 6 | import io.netty.channel.nio.NioEventLoopGroup; 7 | import io.netty.channel.socket.nio.NioSocketChannel; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | 13 | public class MyClient { 14 | public static void main(String[] args) throws InterruptedException, IOException { 15 | EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); 16 | try { 17 | Bootstrap bootstrap = new Bootstrap(); 18 | bootstrap.group(eventLoopGroup) 19 | .channel(NioSocketChannel.class) 20 | .handler(new ClientInitializer()); 21 | Channel channel = bootstrap.connect("", 8080).sync().channel(); 22 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); 23 | for (;;){ 24 | channel.writeAndFlush(bufferedReader.readLine()+"\r\n"); 25 | } 26 | }finally { 27 | eventLoopGroup.shutdownGracefully(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /im-server/src/test/java/client/MyClientHandler.java: -------------------------------------------------------------------------------- 1 | package client; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.SimpleChannelInboundHandler; 5 | 6 | public class MyClientHandler extends SimpleChannelInboundHandler { 7 | @Override 8 | protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { 9 | System.out.println(msg); 10 | } 11 | } -------------------------------------------------------------------------------- /im-server/src/test/java/echo/EchoClient.java: -------------------------------------------------------------------------------- 1 | 2 | package echo; 3 | 4 | import io.netty.bootstrap.Bootstrap; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.*; 8 | import io.netty.channel.nio.NioEventLoopGroup; 9 | import io.netty.channel.socket.SocketChannel; 10 | import io.netty.channel.socket.nio.NioSocketChannel; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | 15 | public final class EchoClient { 16 | 17 | static final String HOST = System.getProperty("host", "127.0.0.1"); 18 | static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); 19 | static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); 20 | 21 | public static void main(String[] args) throws Exception { 22 | 23 | EventLoopGroup group = new NioEventLoopGroup(); 24 | try { 25 | Bootstrap b = new Bootstrap(); 26 | b.group(group) 27 | .channel(NioSocketChannel.class) 28 | .option(ChannelOption.TCP_NODELAY, true) 29 | .handler(new ChannelInitializer() { 30 | @Override 31 | public void initChannel(SocketChannel ch) throws Exception { 32 | ChannelPipeline p = ch.pipeline(); 33 | p.addLast(new ChannelInboundHandlerAdapter(){ 34 | @Override 35 | public void channelActive(ChannelHandlerContext ctx) throws InterruptedException { 36 | for(int i=0;i<30;i++){ 37 | ByteBuf byteBuf = getByteBuf(); 38 | ctx.writeAndFlush(byteBuf); 39 | TimeUnit.SECONDS.sleep(1); 40 | } 41 | } 42 | }); 43 | } 44 | }); 45 | ChannelFuture f = b.connect(HOST, PORT).sync(); 46 | f.channel().closeFuture().sync(); 47 | } finally { 48 | group.shutdownGracefully(); 49 | } 50 | } 51 | //创建一个byteBuf用户传输 52 | private static ByteBuf getByteBuf() { 53 | final ByteBuf firstMessage; 54 | firstMessage = Unpooled.buffer(EchoClient.SIZE); 55 | for (int i = 0; i < firstMessage.capacity(); i ++) { 56 | firstMessage.writeByte((byte) i); 57 | } 58 | return firstMessage; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/EchoServer.java: -------------------------------------------------------------------------------- 1 | 2 | package echo; 3 | 4 | import com.bigyj.monitor.DirectMemoryReporter; 5 | import io.netty.bootstrap.ServerBootstrap; 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.channel.*; 8 | import io.netty.channel.nio.NioEventLoopGroup; 9 | import io.netty.channel.socket.SocketChannel; 10 | import io.netty.channel.socket.nio.NioServerSocketChannel; 11 | import io.netty.handler.logging.LogLevel; 12 | import io.netty.handler.logging.LoggingHandler; 13 | import io.netty.util.ReferenceCountUtil; 14 | 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * Echoes back any received data from a client. 19 | */ 20 | public final class EchoServer { 21 | 22 | static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); 23 | static AtomicInteger count = new AtomicInteger(); 24 | public static void main(String[] args) throws Exception { 25 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 26 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 27 | try { 28 | ServerBootstrap b = new ServerBootstrap(); 29 | b.group(bossGroup, workerGroup) 30 | .channel(NioServerSocketChannel.class) 31 | .option(ChannelOption.SO_BACKLOG, 100) 32 | .handler(new LoggingHandler(LogLevel.INFO)) 33 | .childHandler(new ChannelInitializer() { 34 | @Override 35 | public void initChannel(SocketChannel ch) throws Exception { 36 | ChannelPipeline p = ch.pipeline(); 37 | p.addLast(new InChannelHandlerA()); 38 | p.addLast(new InChannelHandlerB()); 39 | p.addLast(new InChannelHandlerC()); 40 | p.addLast(new OutChannelHandlerA()); 41 | p.addLast(new OutChannelHandlerB()); 42 | p.addLast(new OutChannelHandlerC()); 43 | } 44 | }); 45 | ChannelFuture f = b.bind(PORT).sync(); 46 | // DirectMemoryReporter.init(); 47 | f.channel().closeFuture().sync(); 48 | } finally { 49 | bossGroup.shutdownGracefully(); 50 | workerGroup.shutdownGracefully(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/InChannelHandlerA.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelInboundHandlerAdapter; 5 | 6 | public class InChannelHandlerA extends ChannelInboundHandlerAdapter { 7 | @Override 8 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 9 | System.out.println("InChannelHandlerA——channelRead()"); 10 | ctx.fireChannelRead(msg); 11 | } 12 | 13 | @Override 14 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 15 | ctx.pipeline().fireChannelRead("hello"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/InChannelHandlerB.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelInboundHandlerAdapter; 5 | 6 | public class InChannelHandlerB extends ChannelInboundHandlerAdapter { 7 | @Override 8 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 9 | System.out.println("InChannelHandlerB——channelRead()"); 10 | ctx.fireChannelRead(msg); } 11 | } 12 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/InChannelHandlerC.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelInboundHandlerAdapter; 5 | 6 | public class InChannelHandlerC extends ChannelInboundHandlerAdapter { 7 | @Override 8 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 9 | System.out.println("InChannelHandlerC——channelRead()"); 10 | ctx.fireChannelRead(msg); } 11 | } 12 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/OutChannelHandlerA.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelOutboundHandlerAdapter; 5 | import io.netty.channel.ChannelPromise; 6 | 7 | public class OutChannelHandlerA extends ChannelOutboundHandlerAdapter { 8 | @Override 9 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 10 | System.out.println("InChannelHandlerA——write()"); 11 | super.write(ctx, msg, promise); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/OutChannelHandlerB.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelOutboundHandlerAdapter; 5 | import io.netty.channel.ChannelPromise; 6 | 7 | public class OutChannelHandlerB extends ChannelOutboundHandlerAdapter { 8 | @Override 9 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 10 | System.out.println("InChannelHandlerB——write()"); 11 | super.write(ctx, msg, promise); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /im-server/src/test/java/echo/OutChannelHandlerC.java: -------------------------------------------------------------------------------- 1 | package echo; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.ChannelOutboundHandlerAdapter; 5 | import io.netty.channel.ChannelPromise; 6 | 7 | public class OutChannelHandlerC extends ChannelOutboundHandlerAdapter { 8 | @Override 9 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 10 | System.out.println("InChannelHandlerC——write()"); 11 | super.write(ctx, msg, promise); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /im-server/src/test/java/server/MyChatHandler.java: -------------------------------------------------------------------------------- 1 | package server; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.channel.group.ChannelGroup; 8 | import io.netty.channel.group.DefaultChannelGroup; 9 | import io.netty.util.concurrent.GlobalEventExecutor; 10 | 11 | public class MyChatHandler extends ChannelInboundHandlerAdapter { 12 | private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); 13 | @Override 14 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 15 | System.out.println(msg); 16 | } 17 | 18 | @Override 19 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 20 | super.exceptionCaught(ctx, cause); 21 | cause.printStackTrace(); 22 | ctx.close(); 23 | } 24 | //客户端连接建立 25 | @Override 26 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 27 | Channel channel = ctx.channel(); 28 | if(!channels.contains(channel)){ 29 | channels.writeAndFlush("new client "+channel.remoteAddress()+" join"+"\n"); 30 | channels.add(channel); 31 | } 32 | } 33 | //客户端移除 34 | @Override 35 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 36 | Channel channel = ctx.channel(); 37 | //自动移除,无需手动移除 38 | //channels.remove(ctx.channel()); 39 | channels.writeAndFlush("client "+channel.remoteAddress()+" leave"+"\n"); 40 | channel.close(); 41 | ctx.close(); 42 | } 43 | 44 | @Override 45 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 46 | System.out.println(ctx.channel().remoteAddress() + "online"+"\n"); 47 | } 48 | 49 | @Override 50 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 51 | System.out.println(ctx.channel().remoteAddress() + "ooffline"+"\n"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /im-server/src/test/java/server/MyChatServer.java: -------------------------------------------------------------------------------- 1 | package server; 2 | 3 | import com.bigyj.monitor.DirectMemoryReporter; 4 | import io.netty.bootstrap.ServerBootstrap; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.EventLoopGroup; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | import io.netty.channel.socket.nio.NioServerSocketChannel; 9 | 10 | public class MyChatServer { 11 | public static void main(String[] args) throws InterruptedException { 12 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 13 | EventLoopGroup workerGroup = new NioEventLoopGroup(2); 14 | try { 15 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 16 | serverBootstrap.group(bossGroup,workerGroup) 17 | .channel(NioServerSocketChannel.class) 18 | .childHandler(new MyChatServerInitializer()); 19 | ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); 20 | DirectMemoryReporter.init(); 21 | channelFuture.channel().closeFuture().sync(); 22 | }finally { 23 | bossGroup.shutdownGracefully(); 24 | workerGroup.shutdownGracefully(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /im-server/src/test/java/server/MyChatServerInitializer.java: -------------------------------------------------------------------------------- 1 | package server; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.ChannelPipeline; 5 | import io.netty.channel.socket.SocketChannel; 6 | import io.netty.handler.codec.DelimiterBasedFrameDecoder; 7 | import io.netty.handler.codec.Delimiters; 8 | import io.netty.handler.codec.string.StringDecoder; 9 | import io.netty.handler.codec.string.StringEncoder; 10 | import io.netty.util.CharsetUtil; 11 | 12 | 13 | public class MyChatServerInitializer extends ChannelInitializer { 14 | 15 | @Override 16 | protected void initChannel(SocketChannel ch) throws Exception { 17 | ChannelPipeline pipeline = ch.pipeline(); 18 | pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter())); 19 | pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); 20 | pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); 21 | pipeline.addLast(new MyChatHandler()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /im-server/src/test/java/threadlocal/ThreadLocalTest.java: -------------------------------------------------------------------------------- 1 | package threadlocal; 2 | 3 | public class ThreadLocalTest { 4 | public static void main(String[] args) { 5 | ThreadLocal localName = new ThreadLocal(); 6 | localName.set("tom"); 7 | String name = localName.get(); 8 | Thread thread = Thread.currentThread(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | ## 2 | ## Key : lombok.accessors.chain 3 | ## Type: boolean 4 | ## 5 | ## Generate setters that return 'this' instead of 'void' (default: false). 6 | # 7 | clear lombok.accessors.chain 8 | lombok.accessors.chain = true 9 | 10 | ## 11 | ## Key : lombok.log.fieldName 12 | ## Type: identifier-name 13 | ## 14 | ## Use this name for the generated logger fields (default: 'log'). 15 | # 16 | clear lombok.log.fieldName 17 | lombok.log.fieldName = logger 18 | config.stopBubbling=true 19 | lombok.equalsAndHashCode.callSuper=call -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'im' 2 | include 'im-common' 3 | include 'im-client' 4 | include 'im-server' 5 | 6 | --------------------------------------------------------------------------------