├── .gitignore ├── .travis.yml ├── README.md ├── client-samples ├── Dockerfile ├── pom.xml └── src │ ├── assembly │ └── assembly.xml │ ├── main │ ├── bin │ │ ├── start-docker.sh │ │ └── wait-for-it.sh │ └── java │ │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── client │ │ ├── sample │ │ ├── MyClient.java │ │ └── MyClientApplication.java │ │ └── test │ │ ├── TestClient.java │ │ └── TestClientApplication.java │ └── resources │ └── logback.xml ├── client ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── client │ │ ├── ClientModule.java │ │ ├── ClientRestServiceProvider.java │ │ ├── ImClient.java │ │ ├── api │ │ ├── ChatApi.java │ │ ├── ClientMsgListener.java │ │ └── UserApi.java │ │ ├── context │ │ ├── RelationCache.java │ │ ├── UserContext.java │ │ └── impl │ │ │ └── MemoryRelationCache.java │ │ ├── domain │ │ ├── Friend.java │ │ ├── RelationReq.java │ │ └── UserReq.java │ │ ├── handler │ │ ├── ClientConnectorHandler.java │ │ └── code │ │ │ ├── AesDecoder.java │ │ │ └── AesEncoder.java │ │ └── service │ │ ├── ClientRestClient.java │ │ └── ClientRestService.java │ └── test │ ├── groovy │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── client │ │ └── test │ │ ├── ImClientConnectorTest.groovy │ │ └── RelationCacheTest.groovy │ └── resources │ └── logback-test.xml ├── common ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── common │ │ ├── code │ │ ├── MsgDecoder.java │ │ └── MsgEncoder.java │ │ ├── domain │ │ ├── ResponseCollector.java │ │ ├── ResultWrapper.java │ │ ├── UserInfo.java │ │ ├── conn │ │ │ ├── AbstractConn.java │ │ │ ├── Conn.java │ │ │ ├── ConnContext.java │ │ │ ├── ConnectorConn.java │ │ │ └── MemoryConnContext.java │ │ ├── constant │ │ │ ├── ImConstant.java │ │ │ └── MsgVersion.java │ │ └── po │ │ │ ├── DbModel.java │ │ │ ├── Offline.java │ │ │ ├── Relation.java │ │ │ ├── RelationDetail.java │ │ │ └── User.java │ │ ├── exception │ │ └── ImException.java │ │ ├── function │ │ └── ImBiConsumer.java │ │ ├── parse │ │ ├── AbstractByEnumParser.java │ │ ├── AbstractMsgParser.java │ │ ├── AckParser.java │ │ ├── InternalParser.java │ │ └── ParseService.java │ │ ├── rest │ │ └── AbstractRestService.java │ │ └── util │ │ ├── Encryption.java │ │ ├── IdWorker.java │ │ ├── SnowFlake.java │ │ └── TokenGenerator.java │ └── test │ ├── groovy │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── common │ │ └── test │ │ ├── ConnContextTest.groovy │ │ ├── EncryptionTest.groovy │ │ └── ResponseCollectorTest.groovy │ └── resources │ └── logback-test.xml ├── connector ├── Dockerfile ├── pom.xml └── src │ ├── assembly │ └── assembly.xml │ ├── main │ ├── bin │ │ ├── start-docker.sh │ │ └── wait-for-it.sh │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── yuanrw │ │ │ └── im │ │ │ └── connector │ │ │ ├── config │ │ │ ├── ConnectorConfig.java │ │ │ ├── ConnectorModule.java │ │ │ └── ConnectorRestServiceFactory.java │ │ │ ├── domain │ │ │ ├── ClientConn.java │ │ │ └── ClientConnContext.java │ │ │ ├── handler │ │ │ ├── ConnectorClientHandler.java │ │ │ └── ConnectorTransferHandler.java │ │ │ ├── service │ │ │ ├── ConnectorService.java │ │ │ ├── OfflineService.java │ │ │ ├── UserOnlineService.java │ │ │ └── rest │ │ │ │ ├── ConnectorRestClient.java │ │ │ │ └── ConnectorRestService.java │ │ │ └── start │ │ │ ├── ConnectorClient.java │ │ │ ├── ConnectorServer.java │ │ │ └── ConnectorStarter.java │ └── resources │ │ ├── connector-docker.properties │ │ ├── connector.properties │ │ └── logback.xml │ └── test │ ├── groovy │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── connector │ │ ├── ClientConnContextTest.groovy │ │ ├── ConnectorClientTest.groovy │ │ └── ConnectorTransferTest.groovy │ └── resources │ └── logback-test.xml ├── docker ├── docker-compose.yml └── sql │ └── client-sample-quick-start.sql ├── mvnw ├── mvnw.cmd ├── pic └── im-structure.png ├── pom.xml ├── protobuf ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── protobuf │ │ ├── constant │ │ └── MsgTypeEnum.java │ │ └── generate │ │ ├── Ack.java │ │ ├── Chat.java │ │ └── Internal.java │ └── resources │ └── proto │ ├── ack.proto │ ├── chat.proto │ └── internal.proto ├── rest ├── pom.xml ├── rest-spi │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── rest │ │ └── spi │ │ ├── UserSpi.java │ │ └── domain │ │ ├── LdapUser.java │ │ └── UserBase.java └── rest-web │ ├── Dockerfile │ ├── pom.xml │ └── src │ ├── assembly │ └── assembly.xml │ ├── main │ ├── bin │ │ ├── start-docker.sh │ │ └── wait-for-it.sh │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── yuanrw │ │ │ └── im │ │ │ └── rest │ │ │ └── web │ │ │ ├── RestStarter.java │ │ │ ├── config │ │ │ └── RestConfig.java │ │ │ ├── filter │ │ │ ├── GlobalErrorAttributes.java │ │ │ ├── GlobalErrorWebExceptionHandler.java │ │ │ ├── HeaderFilter.java │ │ │ └── TokenManager.java │ │ │ ├── handler │ │ │ ├── OfflineHandler.java │ │ │ ├── RelationHandler.java │ │ │ ├── UserHandler.java │ │ │ └── ValidHandler.java │ │ │ ├── mapper │ │ │ ├── OfflineMapper.java │ │ │ ├── RelationMapper.java │ │ │ └── UserMapper.java │ │ │ ├── router │ │ │ └── RestRouter.java │ │ │ ├── service │ │ │ ├── OfflineService.java │ │ │ ├── RelationService.java │ │ │ ├── UserService.java │ │ │ └── impl │ │ │ │ ├── OfflineServiceImpl.java │ │ │ │ ├── RelationServiceImpl.java │ │ │ │ └── UserServiceImpl.java │ │ │ ├── spi │ │ │ ├── SpiFactory.java │ │ │ └── impl │ │ │ │ ├── DefaultUserSpiImpl.java │ │ │ │ └── LdapUserSpiImpl.java │ │ │ ├── task │ │ │ ├── CleanOfflineMsgTask.java │ │ │ └── OfflineListen.java │ │ │ └── vo │ │ │ ├── RelationReq.java │ │ │ └── UserReq.java │ └── resources │ │ ├── application-docker.properties │ │ ├── application.properties │ │ ├── logback-spring.xml │ │ ├── mapper │ │ ├── OfflineMapper.xml │ │ └── RelationMapper.xml │ │ └── rest.sql │ └── test │ ├── java │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── rest │ │ └── web │ │ └── test │ │ ├── OfflineTest.java │ │ ├── RelationTest.java │ │ └── UserTest.java │ └── resources │ ├── application.properties │ ├── logback-test.xml │ └── rest-test.sql ├── transfer ├── Dockerfile ├── pom.xml └── src │ ├── assembly │ └── assembly.xml │ ├── main │ ├── bin │ │ ├── start-docker.sh │ │ └── wait-for-it.sh │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── yuanrw │ │ │ └── im │ │ │ └── transfer │ │ │ ├── config │ │ │ ├── TransferConfig.java │ │ │ └── TransferModule.java │ │ │ ├── domain │ │ │ └── ConnectorConnContext.java │ │ │ ├── handler │ │ │ └── TransferConnectorHandler.java │ │ │ ├── service │ │ │ └── TransferService.java │ │ │ └── start │ │ │ ├── TransferMqProducer.java │ │ │ ├── TransferServer.java │ │ │ └── TransferStarter.java │ └── resources │ │ ├── logback.xml │ │ ├── transfer-docker.properties │ │ └── transfer.properties │ └── test │ ├── groovy │ └── com │ │ └── github │ │ └── yuanrw │ │ └── im │ │ └── transfer │ │ └── TransferConnectorTest.groovy │ └── resources │ └── logback-test.xml └── user-status ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── yuanrw │ └── im │ └── user │ └── status │ ├── factory │ └── UserStatusServiceFactory.java │ └── service │ ├── UserStatusService.java │ └── impl │ ├── MemoryUserStatusServiceImpl.java │ └── RedisUserStatusServiceImpl.java └── test └── resources └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .DS_Store 26 | .idea/ 27 | target/ 28 | *.iml 29 | .mvn/ 30 | docker/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | sudo: false # run on container-based infrastructure 4 | 5 | cache: 6 | directories: 7 | - $HOME/.m2/repository/ 8 | 9 | jdk: 10 | - openjdk8 11 | - openjdk11 12 | 13 | install: true 14 | 15 | services: 16 | - rabbitmq 17 | - redis-server 18 | 19 | script: mvn clean package 20 | 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /client-samples/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for client-samples 2 | # docker build -t yuanrw/client-samples:$VERSION . 3 | # docker run -d --name client-samples client-samples 4 | 5 | FROM adoptopenjdk/openjdk11:alpine-jre 6 | MAINTAINER yuanrw <295415537@qq.com> 7 | 8 | ENV SERVICE_NAME client-samples 9 | ENV VERSION 1.0.0 10 | 11 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \ 12 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \ 13 | && apk update upgrade \ 14 | && apk add --no-cache procps unzip curl bash tzdata \ 15 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 16 | && echo "Asia/Shanghai" > /etc/timezone 17 | 18 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip 19 | 20 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \ 21 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \ 22 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \ 23 | && echo "tail -f /dev/null" >> start.sh 24 | 25 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} 26 | 27 | COPY src/main/bin/start-docker.sh . 28 | COPY src/main/bin/wait-for-it.sh . 29 | 30 | CMD /bin/bash wait-for-it.sh -t 0 connector:9081 --strict -- \ 31 | /bin/bash start-docker.sh -------------------------------------------------------------------------------- /client-samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | client-samples 13 | 14 | 15 | com.github.yuanrw.im.client.test.TestClientApplication 16 | 17 | 18 | 19 | 20 | com.github.yuanrw.im 21 | client 22 | 1.0.0 23 | 24 | 25 | org.apache.commons 26 | commons-lang3 27 | 28 | 29 | 30 | 31 | 32 | 33 | maven-jar-plugin 34 | 35 | 36 | maven-assembly-plugin 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /client-samples/src/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | 4 | zip 5 | 6 | 7 | 8 | true 9 | lib 10 | 11 | 12 | 13 | 14 | src/main/resources 15 | / 16 | 17 | logback*.xml 18 | *-docker.* 19 | 20 | unix 21 | 22 | 23 | target 24 | / 25 | 26 | ${project.artifactId}-*.jar 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client-samples/src/main/bin/start-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SERVICE_NAME="client-samples" 3 | VERSION="1.0.0" 4 | 5 | LOG_DIR=/tmp/IM_logs 6 | mkdir -p $LOG_DIR 7 | 8 | # Find Java 9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 10 | java="$JAVA_HOME/bin/java" 11 | elif type -p java > /dev/null 2>&1; then 12 | java=$(type -p java) 13 | elif [[ -x "/usr/bin/java" ]]; then 14 | java="/usr/bin/java" 15 | else 16 | echo "Unable to find Java" 17 | exit 1 18 | fi 19 | 20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" 21 | 22 | echo "JAVA_HOME: $JAVA_HOME" 23 | $java $JAVA_OPTS -jar $SERVICE_NAME-$VERSION.jar connector http://rest-web:8082 24 | echo "SERVICE_NAME started...." -------------------------------------------------------------------------------- /client-samples/src/main/java/com/github/yuanrw/im/client/sample/MyClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.sample; 2 | 3 | import java.util.Scanner; 4 | 5 | /** 6 | * Date: 2019-08-26 7 | * Time: 13:24 8 | * 9 | * @author yrw 10 | */ 11 | public class MyClientApplication { 12 | 13 | private final static String CONNECTOR_HOST = "127.0.0.1"; 14 | private final static Integer CONNECTOR_PORT = 19081; 15 | private final static String REST_URL = "http://127.0.0.1:8082"; 16 | 17 | public static void main(String[] args) { 18 | System.out.println("please login"); 19 | 20 | Scanner scan = new Scanner(System.in); 21 | 22 | String username = scan.next(); 23 | String password = scan.next(); 24 | 25 | MyClient myClient = new MyClient(CONNECTOR_HOST, CONNECTOR_PORT, REST_URL, username, password); 26 | 27 | System.out.println("\r\nlogin successfully (^_^)\r\n"); 28 | 29 | myClient.printUserInfo(); 30 | 31 | System.out.println("\r\nnow send msg to your friends\r\n\r\n"); 32 | 33 | while (scan.hasNext()) { 34 | String userId = scan.next(); 35 | String text = scan.next(); 36 | myClient.send(userId, text); 37 | } 38 | scan.close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client-samples/src/main/java/com/github/yuanrw/im/client/test/TestClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | /** 10 | * Date: 2019-05-15 11 | * Time: 13:57 12 | * 13 | * @author yrw 14 | */ 15 | public class TestClientApplication { 16 | 17 | public static void main(String[] args) { 18 | List testClientList = new ArrayList<>(); 19 | String[] usernameForTest = { 20 | "Adela", "Alice", "Bella", "Cynthia", "Freda", "Honey", 21 | "Irene", "Iris", "Joa", "Juliet", "Lisa", "Mandy", "Nora", 22 | "Olive", "Tom", "xianyy", "yuanrw", 23 | }; 24 | 25 | //login all user 26 | for (int i = 0; i < 17; i++) { 27 | testClientList.add(new TestClient(args[0], 9081, 28 | args[1], usernameForTest[i], "123abc")); 29 | } 30 | 31 | //print test result every 5 seconds 32 | ScheduledExecutorService printExecutor = Executors.newScheduledThreadPool(1); 33 | 34 | doInExecutor(printExecutor, 5, () -> { 35 | System.out.println("\n\n"); 36 | System.out.println(String.format("sentMsg: %d, readMsg: %d, hasSentAck: %d, " + 37 | "hasDeliveredAck: %d, hasReadAck: %d, hasException: %d", 38 | TestClient.sendMsg.get(), TestClient.readMsg.get(), TestClient.hasSentAck.get(), 39 | TestClient.hasDeliveredAck.get(), TestClient.hasReadAck.get(), TestClient.hasException.get())); 40 | System.out.println("\n\n"); 41 | }); 42 | 43 | 44 | //start simulate send 45 | ScheduledExecutorService clientExecutor = Executors.newScheduledThreadPool(20); 46 | 47 | testClientList.forEach(testClient -> doInExecutor(clientExecutor, 2, testClient::randomSendTest)); 48 | } 49 | 50 | private static void doInExecutor(ScheduledExecutorService executorService, int period, Runnable doFunction) { 51 | executorService.scheduleAtFixedRate(doFunction, 0, period, TimeUnit.SECONDS); 52 | } 53 | } -------------------------------------------------------------------------------- /client-samples/src/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | client-samples 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName - %msg%n 8 | 9 | 10 | 11 | 12 | /tmp/IM_logs/client-samples.log 13 | 14 | client-samples.%d{yyyy-MM-dd}.log 15 | 16 | 17 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | client 13 | 14 | 15 | 16 | io.netty 17 | netty-all 18 | 19 | 20 | com.github.yuanrw.im 21 | common 22 | 23 | 24 | com.google.inject.extensions 25 | guice-assistedinject 26 | 27 | 28 | org.objenesis 29 | objenesis 30 | 3.0.1 31 | test 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-source-plugin 40 | 41 | 42 | maven-jar-plugin 43 | 44 | 45 | org.codehaus.gmavenplus 46 | gmavenplus-plugin 47 | 48 | 49 | org.jacoco 50 | jacoco-maven-plugin 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/ClientModule.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client; 2 | 3 | import com.github.yuanrw.im.client.context.RelationCache; 4 | import com.github.yuanrw.im.client.context.impl.MemoryRelationCache; 5 | import com.github.yuanrw.im.client.service.ClientRestService; 6 | import com.google.inject.AbstractModule; 7 | 8 | /** 9 | * Date: 2019-07-03 10 | * Time: 16:43 11 | * 12 | * @author yrw 13 | */ 14 | public class ClientModule extends AbstractModule { 15 | 16 | @Override 17 | protected void configure() { 18 | bind(RelationCache.class).to(MemoryRelationCache.class); 19 | bind(ClientRestService.class).toProvider(ClientRestServiceProvider.class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/ClientRestServiceProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client; 2 | 3 | import com.github.yuanrw.im.client.service.ClientRestService; 4 | import com.google.inject.Provider; 5 | 6 | /** 7 | * Date: 2019-08-08 8 | * Time: 17:02 9 | * 10 | * @author yrw 11 | */ 12 | public class ClientRestServiceProvider implements Provider { 13 | 14 | public static String REST_URL; 15 | 16 | @Override 17 | public ClientRestService get() { 18 | return new ClientRestService(REST_URL); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/api/ChatApi.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.api; 2 | 3 | import com.github.yuanrw.im.client.context.UserContext; 4 | import com.github.yuanrw.im.client.handler.ClientConnectorHandler; 5 | import com.github.yuanrw.im.common.domain.constant.MsgVersion; 6 | import com.github.yuanrw.im.common.exception.ImException; 7 | import com.github.yuanrw.im.common.util.IdWorker; 8 | import com.github.yuanrw.im.protobuf.generate.Ack; 9 | import com.github.yuanrw.im.protobuf.generate.Chat; 10 | import com.google.protobuf.ByteString; 11 | import com.google.protobuf.Message; 12 | import io.netty.util.CharsetUtil; 13 | 14 | /** 15 | * Date: 2019-05-14 16 | * Time: 10:29 17 | * 18 | * @author yrw 19 | */ 20 | public class ChatApi { 21 | 22 | private UserContext userContext; 23 | private ClientConnectorHandler handler; 24 | 25 | public ChatApi(UserContext userContext, ClientConnectorHandler handler) { 26 | this.userContext = userContext; 27 | this.handler = handler; 28 | } 29 | 30 | public Chat.ChatMsg.Builder chatMsgBuilder() { 31 | return Chat.ChatMsg.newBuilder(); 32 | } 33 | 34 | public Long send(Chat.ChatMsg chat) { 35 | checkLogin(); 36 | 37 | sendToConnector(chat, chat.getId()); 38 | 39 | return chat.getId(); 40 | } 41 | 42 | public Long text(String toId, String text) { 43 | checkLogin(); 44 | 45 | Chat.ChatMsg chat = Chat.ChatMsg.newBuilder() 46 | .setId(IdWorker.genId()) 47 | .setFromId(userContext.getUserId()) 48 | .setDestId(toId) 49 | .setDestType(Chat.ChatMsg.DestType.SINGLE) 50 | .setCreateTime(System.currentTimeMillis()) 51 | .setMsgType(Chat.ChatMsg.MsgType.TEXT) 52 | .setVersion(MsgVersion.V1.getVersion()) 53 | .setMsgBody(ByteString.copyFrom(text, CharsetUtil.UTF_8)) 54 | .build(); 55 | 56 | sendToConnector(chat, chat.getId()); 57 | 58 | return chat.getId(); 59 | } 60 | 61 | private void checkLogin() { 62 | if (userContext.getUserId() == null) { 63 | throw new ImException("client has not login!"); 64 | } 65 | } 66 | 67 | private void sendToConnector(Message msg, Long id) { 68 | userContext.getClientConnectorHandler().writeAndFlush(msg, id); 69 | } 70 | 71 | public void confirmRead(Chat.ChatMsg msg) { 72 | Ack.AckMsg read = Ack.AckMsg.newBuilder() 73 | .setId(IdWorker.genId()) 74 | .setVersion(MsgVersion.V1.getVersion()) 75 | .setFromId(msg.getDestId()) 76 | .setDestId(msg.getFromId()) 77 | .setCreateTime(System.currentTimeMillis()) 78 | .setDestType(msg.getDestType() == Chat.ChatMsg.DestType.SINGLE ? Ack.AckMsg.DestType.SINGLE : Ack.AckMsg.DestType.GROUP) 79 | .setMsgType(Ack.AckMsg.MsgType.READ) 80 | .setAckMsgId(msg.getId()) 81 | .build(); 82 | 83 | handler.getCtx().writeAndFlush(read); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/api/ClientMsgListener.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.api; 2 | 3 | import com.github.yuanrw.im.protobuf.generate.Chat; 4 | import io.netty.channel.ChannelHandlerContext; 5 | 6 | /** 7 | * Date: 2019-05-18 8 | * Time: 23:46 9 | * 10 | * @author yrw 11 | */ 12 | public interface ClientMsgListener { 13 | 14 | /** 15 | * do when the client connect to connector successfully 16 | */ 17 | void online(); 18 | 19 | /** 20 | * read a msg 21 | * 22 | * @param chatMsg 23 | */ 24 | void read(Chat.ChatMsg chatMsg); 25 | 26 | /** 27 | * do when a msg has been sent 28 | * 29 | * @param id chatMsg msg id 30 | */ 31 | void hasSent(Long id); 32 | 33 | /** 34 | * do when a msg has been delivered 35 | * 36 | * @param id chatMsg msg id 37 | */ 38 | void hasDelivered(Long id); 39 | 40 | /** 41 | * do when a msg has been read 42 | * 43 | * @param id chatMsg msg id 44 | */ 45 | void hasRead(Long id); 46 | 47 | /** 48 | * do when the client disconnect to connector 49 | */ 50 | void offline(); 51 | 52 | /** 53 | * a exception is occurred 54 | * 55 | * @param ctx 56 | * @param cause 57 | */ 58 | void hasException(ChannelHandlerContext ctx, Throwable cause); 59 | } -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/context/RelationCache.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.context; 2 | 3 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Date: 2019-07-03 9 | * Time: 16:24 10 | * 11 | * @author yrw 12 | */ 13 | public interface RelationCache { 14 | 15 | /** 16 | * add multiple relations 17 | * 18 | * @param relations 19 | */ 20 | void addRelations(List relations); 21 | 22 | /** 23 | * add a relation 24 | * 25 | * @param relation 26 | */ 27 | void addRelation(RelationDetail relation); 28 | 29 | /** 30 | * get relation by userId 31 | * 32 | * @param userId1 33 | * @param userId2 34 | * @param token 35 | * @return 36 | */ 37 | RelationDetail getRelation(String userId1, String userId2, String token); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/context/UserContext.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.context; 2 | 3 | import com.github.yuanrw.im.client.handler.ClientConnectorHandler; 4 | import com.github.yuanrw.im.common.domain.po.Relation; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | import com.google.inject.Inject; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * user's info 12 | * Date: 2019-07-03 13 | * Time: 15:25 14 | * 15 | * @author yrw 16 | */ 17 | public class UserContext { 18 | 19 | private String userId; 20 | 21 | private String token; 22 | 23 | private RelationCache relationCache; 24 | 25 | private ClientConnectorHandler clientConnectorHandler; 26 | 27 | @Inject 28 | public UserContext(RelationCache relationCache) { 29 | this.relationCache = relationCache; 30 | } 31 | 32 | public ClientConnectorHandler getClientConnectorHandler() { 33 | return clientConnectorHandler; 34 | } 35 | 36 | public void setClientConnectorHandler(ClientConnectorHandler clientConnectorHandler) { 37 | this.clientConnectorHandler = clientConnectorHandler; 38 | } 39 | 40 | public String getUserId() { 41 | return userId; 42 | } 43 | 44 | public void setUserId(String userId) { 45 | this.userId = userId; 46 | } 47 | 48 | public RelationCache getRelationCache() { 49 | return relationCache; 50 | } 51 | 52 | public void setRelationCache(RelationCache relationCache) { 53 | this.relationCache = relationCache; 54 | } 55 | 56 | public String getToken() { 57 | return token; 58 | } 59 | 60 | public void setToken(String token) { 61 | this.token = token; 62 | } 63 | 64 | public void addRelations(List relations) { 65 | relationCache.addRelations(relations); 66 | } 67 | 68 | public void addRelation(RelationDetail relation) { 69 | relationCache.addRelation(relation); 70 | } 71 | 72 | public Relation getRelation(String userId1, String userId2) { 73 | return relationCache.getRelation(userId1, userId2, token); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/context/impl/MemoryRelationCache.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.context.impl; 2 | 3 | import com.github.yuanrw.im.client.context.RelationCache; 4 | import com.github.yuanrw.im.client.service.ClientRestService; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | import com.google.inject.Inject; 7 | import com.google.inject.Singleton; 8 | 9 | import java.util.List; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.ConcurrentMap; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * store relation in memory 16 | * Date: 2019-07-03 17 | * Time: 16:25 18 | * 19 | * @author yrw 20 | */ 21 | @Singleton 22 | public class MemoryRelationCache implements RelationCache { 23 | 24 | private ConcurrentMap relationMap; 25 | private ClientRestService clientRestService; 26 | 27 | @Inject 28 | public MemoryRelationCache(ClientRestService clientRestService) { 29 | this.clientRestService = clientRestService; 30 | this.relationMap = new ConcurrentHashMap<>(); 31 | } 32 | 33 | @Override 34 | public void addRelations(List relations) { 35 | relationMap.putAll(relations.stream().collect(Collectors.toMap( 36 | r -> generateKey(r.getUserId1(), r.getUserId2()), 37 | r -> r) 38 | )); 39 | } 40 | 41 | @Override 42 | public void addRelation(RelationDetail relation) { 43 | relationMap.put(generateKey(relation.getUserId1(), relation.getUserId2()), relation); 44 | } 45 | 46 | @Override 47 | public RelationDetail getRelation(String userId1, String userId2, String token) { 48 | RelationDetail relation = relationMap.get(generateKey(userId1, userId2)); 49 | if (relation == null) { 50 | relation = getRelationFromRest(userId1, userId2, token); 51 | if (relation != null) { 52 | relationMap.put(generateKey(userId1, userId2), relation); 53 | } 54 | } 55 | return relation; 56 | } 57 | 58 | private RelationDetail getRelationFromRest(String userId1, String userId2, String token) { 59 | return clientRestService.relation(userId1, userId2, token); 60 | } 61 | 62 | private String generateKey(String userId1, String userId2) { 63 | String max = userId1.compareTo(userId2) >= 0 ? userId1 : userId2; 64 | String min = max.equals(userId1) ? userId2 : userId1; 65 | return min + "_" + max; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/domain/Friend.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.domain; 2 | 3 | /** 4 | * Date: 2019-05-24 5 | * Time: 15:08 6 | * 7 | * @author yrw 8 | */ 9 | public class Friend { 10 | 11 | private String userId; 12 | 13 | private String username; 14 | 15 | private String encryptKey; 16 | 17 | public String getUserId() { 18 | return userId; 19 | } 20 | 21 | public void setUserId(String userId) { 22 | this.userId = userId; 23 | } 24 | 25 | public String getUsername() { 26 | return username; 27 | } 28 | 29 | public void setUsername(String username) { 30 | this.username = username; 31 | } 32 | 33 | public String getEncryptKey() { 34 | return encryptKey; 35 | } 36 | 37 | public void setEncryptKey(String encryptKey) { 38 | this.encryptKey = encryptKey; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/domain/RelationReq.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.domain; 2 | 3 | /** 4 | * Date: 2019-06-23 5 | * Time: 21:04 6 | * 7 | * @author yrw 8 | */ 9 | public class RelationReq { 10 | 11 | private String userId1; 12 | private String userId2; 13 | 14 | public String getUserId1() { 15 | return userId1; 16 | } 17 | 18 | public void setUserId1(String userId1) { 19 | this.userId1 = userId1; 20 | } 21 | 22 | public String getUserId2() { 23 | return userId2; 24 | } 25 | 26 | public void setUserId2(String userId2) { 27 | this.userId2 = userId2; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/domain/UserReq.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.domain; 2 | 3 | /** 4 | * Date: 2019-04-21 5 | * Time: 14:43 6 | * 7 | * @author yrw 8 | */ 9 | public class UserReq { 10 | private String username; 11 | private String pwd; 12 | 13 | public UserReq(String username, String pwd) { 14 | this.username = username; 15 | this.pwd = pwd; 16 | } 17 | 18 | public String getUsername() { 19 | return username; 20 | } 21 | 22 | public void setUsername(String username) { 23 | this.username = username; 24 | } 25 | 26 | public String getPwd() { 27 | return pwd; 28 | } 29 | 30 | public void setPwd(String pwd) { 31 | this.pwd = pwd; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/handler/code/AesDecoder.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.handler.code; 2 | 3 | import com.github.yuanrw.im.client.context.UserContext; 4 | import com.github.yuanrw.im.common.domain.po.Relation; 5 | import com.github.yuanrw.im.common.util.Encryption; 6 | import com.github.yuanrw.im.protobuf.generate.Chat; 7 | import com.google.protobuf.ByteString; 8 | import com.google.protobuf.Message; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.handler.codec.MessageToMessageDecoder; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Date: 2019-05-11 16 | * Time: 13:34 17 | * 18 | * @author yrw 19 | */ 20 | public class AesDecoder extends MessageToMessageDecoder { 21 | 22 | private UserContext userContext; 23 | 24 | public AesDecoder(UserContext userContext) { 25 | this.userContext = userContext; 26 | } 27 | 28 | @Override 29 | protected void decode(ChannelHandlerContext ctx, Message msg, List out) throws Exception { 30 | if (msg instanceof Chat.ChatMsg) { 31 | Chat.ChatMsg cm = (Chat.ChatMsg) msg; 32 | Relation relation = userContext.getRelation(cm.getFromId(), cm.getDestId()); 33 | String[] keys = relation.getEncryptKey().split("\\|"); 34 | 35 | byte[] decodeBody = Encryption.decrypt(keys[0], keys[1], cm.getMsgBody().toByteArray()); 36 | 37 | Chat.ChatMsg decodeMsg = Chat.ChatMsg.newBuilder().mergeFrom(cm) 38 | .setMsgBody(ByteString.copyFrom(decodeBody)).build(); 39 | 40 | out.add(decodeMsg); 41 | } else { 42 | out.add(msg); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/handler/code/AesEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.handler.code; 2 | 3 | import com.github.yuanrw.im.client.context.UserContext; 4 | import com.github.yuanrw.im.common.code.MsgDecoder; 5 | import com.github.yuanrw.im.common.domain.po.Relation; 6 | import com.github.yuanrw.im.common.util.Encryption; 7 | import com.github.yuanrw.im.protobuf.generate.Chat; 8 | import com.google.protobuf.ByteString; 9 | import com.google.protobuf.Message; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.handler.codec.MessageToMessageEncoder; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * Date: 2019-05-11 19 | * Time: 12:49 20 | * 21 | * @author yrw 22 | */ 23 | public class AesEncoder extends MessageToMessageEncoder { 24 | private static final Logger logger = LoggerFactory.getLogger(MsgDecoder.class); 25 | 26 | private UserContext userContext; 27 | 28 | public AesEncoder(UserContext userContext) { 29 | this.userContext = userContext; 30 | } 31 | 32 | @Override 33 | protected void encode(ChannelHandlerContext ctx, Message msg, List out) throws Exception { 34 | try { 35 | if (msg instanceof Chat.ChatMsg) { 36 | Chat.ChatMsg cm = (Chat.ChatMsg) msg; 37 | Relation relation = userContext.getRelation(cm.getFromId(), cm.getDestId()); 38 | String[] keys = relation.getEncryptKey().split("\\|"); 39 | 40 | byte[] encodeBody = Encryption.encrypt(keys[0], keys[1], cm.getMsgBody().toByteArray()); 41 | 42 | Chat.ChatMsg encodeMsg = Chat.ChatMsg.newBuilder().mergeFrom(cm) 43 | .setMsgBody(ByteString.copyFrom(encodeBody)).build(); 44 | 45 | logger.debug("[encode] encode message: {}", encodeMsg.toString()); 46 | 47 | out.add(encodeMsg); 48 | } else { 49 | out.add(msg); 50 | } 51 | } catch (Exception e) { 52 | logger.error("[encode] has error", e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/service/ClientRestClient.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.service; 2 | 3 | import com.github.yuanrw.im.client.domain.UserReq; 4 | import com.github.yuanrw.im.common.domain.ResultWrapper; 5 | import com.github.yuanrw.im.common.domain.UserInfo; 6 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 7 | import retrofit2.Call; 8 | import retrofit2.http.*; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Date: 2019-04-21 14 | * Time: 17:03 15 | * 16 | * @author yrw 17 | */ 18 | public interface ClientRestClient { 19 | 20 | @Headers("Content-Type: application/json") 21 | @POST("/user/login") 22 | Call> login(@Body UserReq user); 23 | 24 | @POST("/user/logout") 25 | Call> logout(@Header("token") String token); 26 | 27 | @GET("/relation/{id}") 28 | Call>> friends(@Path("id") String userId, @Header("token") String token); 29 | 30 | @GET("/relation") 31 | Call> relation( 32 | @Query("userId1") String userId1, @Query("userId2") String userId2, 33 | @Header("token") String token); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/main/java/com/github/yuanrw/im/client/service/ClientRestService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.service; 2 | 3 | import com.github.yuanrw.im.client.domain.UserReq; 4 | import com.github.yuanrw.im.common.domain.UserInfo; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | import com.github.yuanrw.im.common.rest.AbstractRestService; 7 | import com.google.inject.Inject; 8 | import com.google.inject.assistedinject.Assisted; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * request for rest module 14 | * Date: 2019-04-21 15 | * Time: 16:45 16 | * 17 | * @author yrw 18 | */ 19 | public class ClientRestService extends AbstractRestService { 20 | 21 | @Inject 22 | public ClientRestService(@Assisted String url) { 23 | super(ClientRestClient.class, url); 24 | } 25 | 26 | public UserInfo login(String username, String password) { 27 | return doRequest(() -> 28 | restClient.login(new UserReq(username, password)).execute()); 29 | } 30 | 31 | public Void logout(String token) { 32 | return doRequest(() -> restClient.logout(token).execute()); 33 | } 34 | 35 | public List friends(String userId, String token) { 36 | return doRequest(() -> restClient.friends(userId, token).execute()); 37 | } 38 | 39 | public RelationDetail relation(String userId1, String userId2, String token) { 40 | return doRequest(() -> restClient.relation(userId1, userId2, token).execute()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/src/test/groovy/com/github/yuanrw/im/client/test/RelationCacheTest.groovy: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.client.test 2 | 3 | import com.github.yuanrw.im.client.context.impl.MemoryRelationCache 4 | import com.github.yuanrw.im.client.service.ClientRestService 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail 6 | import spock.lang.Specification 7 | 8 | /** 9 | * Date: 2019-09-07 10 | * Time: 07:57 11 | * @author yrw 12 | */ 13 | class RelationCacheTest extends Specification { 14 | 15 | def "test add relation"() { 16 | given: 17 | def clientRestService = Mock(ClientRestService) 18 | def relationCache = new MemoryRelationCache(clientRestService) 19 | 20 | when: 21 | def relation = new RelationDetail() 22 | relation.setUserId1("123") 23 | relation.setUserId2("456") 24 | relation.setUsername1("123 name") 25 | relation.setUsername2("456 name") 26 | relationCache.addRelation(relation) 27 | 28 | then: 29 | def relationDetail = relationCache.getRelation("123", "456", "token") 30 | 31 | relationDetail.userId1 == "123" 32 | relationDetail.userId2 == "456" 33 | relationDetail.username1 == "123 name" 34 | relationDetail.username2 == "456 name" 35 | 36 | 0 * clientRestService.relation(_ as String, _ as String, _ as String) 37 | } 38 | 39 | def "test add relations"() { 40 | given: 41 | def clientRestService = Mock(ClientRestService) 42 | def relationCache = new MemoryRelationCache(clientRestService) 43 | 44 | when: 45 | def list = new LinkedList() 46 | def relation1 = new RelationDetail() 47 | relation1.setUserId1("123") 48 | relation1.setUserId2("456") 49 | relation1.setUsername1("123 name") 50 | relation1.setUsername2("456 name") 51 | 52 | def relation2 = new RelationDetail() 53 | relation2.setUserId1("131") 54 | relation2.setUserId2("068") 55 | relation2.setUsername1("131 name") 56 | relation2.setUsername2("068 name") 57 | 58 | list.add(relation1) 59 | list.add(relation2) 60 | 61 | relationCache.addRelations(list) 62 | 63 | then: 64 | def relationDetail1 = relationCache.getRelation("123", "456", "token") 65 | relationDetail1.userId1 == "123" 66 | relationDetail1.userId2 == "456" 67 | relationDetail1.username1 == "123 name" 68 | relationDetail1.username2 == "456 name" 69 | 70 | def relationDetail2 = relationCache.getRelation("131", "068", "token") 71 | relationDetail2.userId1 == "131" 72 | relationDetail2.userId2 == "068" 73 | relationDetail2.username1 == "131 name" 74 | relationDetail2.username2 == "068 name" 75 | } 76 | 77 | def "test get relation not exist"() { 78 | given: 79 | def clientRestService = Mock(ClientRestService) 80 | def relationCache = new MemoryRelationCache(clientRestService) 81 | 82 | when: 83 | relationCache.getRelation("123", "456", "token") 84 | 85 | then: 86 | relationCache.getRelation("123", "456", "token") == null 87 | 1 * clientRestService.relation("123", "456", "token") 88 | } 89 | } -------------------------------------------------------------------------------- /client/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | common 13 | 14 | 15 | 16 | cglib 17 | cglib-nodep 18 | 19 | 20 | org.spockframework 21 | spock-core 22 | 23 | 24 | org.codehaus.groovy 25 | groovy 26 | 27 | 28 | ch.qos.logback 29 | logback-classic 30 | 31 | 32 | ch.qos.logback 33 | logback-core 34 | 35 | 36 | io.netty 37 | netty-all 38 | 39 | 40 | com.google.inject 41 | guice 42 | 43 | 44 | commons-codec 45 | commons-codec 46 | 47 | 48 | com.squareup.retrofit2 49 | retrofit 50 | 51 | 52 | com.squareup.retrofit2 53 | converter-jackson 54 | 55 | 56 | com.squareup.okhttp3 57 | logging-interceptor 58 | 3.12.0 59 | 60 | 61 | com.github.yuanrw.im 62 | protobuf 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.codehaus.gmavenplus 70 | gmavenplus-plugin 71 | 72 | 73 | org.jacoco 74 | jacoco-maven-plugin 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/code/MsgDecoder.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.code; 2 | 3 | import com.github.yuanrw.im.common.parse.ParseService; 4 | import com.google.protobuf.Message; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.handler.codec.ByteToMessageDecoder; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Date: 2019-04-14 16 | * Time: 15:06 17 | * 18 | * @author yrw 19 | */ 20 | public class MsgDecoder extends ByteToMessageDecoder { 21 | private static final Logger logger = LoggerFactory.getLogger(MsgDecoder.class); 22 | 23 | private ParseService parseService; 24 | 25 | public MsgDecoder() { 26 | this.parseService = new ParseService(); 27 | } 28 | 29 | @Override 30 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { 31 | 32 | in.markReaderIndex(); 33 | 34 | if (in.readableBytes() < 4) { 35 | in.resetReaderIndex(); 36 | return; 37 | } 38 | 39 | int length = in.readInt(); 40 | 41 | if (length < 0) { 42 | ctx.close(); 43 | logger.error("[IM msg decoder]message length less than 0, channel closed"); 44 | return; 45 | } 46 | 47 | if (length > in.readableBytes() - 4) { 48 | in.resetReaderIndex(); 49 | return; 50 | } 51 | 52 | int code = in.readInt(); 53 | ByteBuf byteBuf = Unpooled.buffer(length); 54 | 55 | in.readBytes(byteBuf); 56 | 57 | byte[] body = byteBuf.array(); 58 | 59 | Message msg = parseService.getMsgByCode(code, body); 60 | out.add(msg); 61 | 62 | logger.debug("[IM msg decoder]received message: content length {}, msgTypeCode: {}", length, code); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/code/MsgEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.code; 2 | 3 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum; 4 | import com.google.protobuf.Message; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.handler.codec.MessageToByteEncoder; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** 13 | * Date: 2019-04-14 14 | * Time: 16:35 15 | * 16 | * @author yrw 17 | */ 18 | public class MsgEncoder extends MessageToByteEncoder { 19 | private static final Logger logger = LoggerFactory.getLogger(MsgEncoder.class); 20 | 21 | @Override 22 | protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { 23 | try { 24 | byte[] bytes = msg.toByteArray(); 25 | int code = MsgTypeEnum.getByClass(msg.getClass()).getCode(); 26 | int length = bytes.length; 27 | 28 | ByteBuf buf = Unpooled.buffer(8 + length); 29 | buf.writeInt(length); 30 | buf.writeInt(code); 31 | buf.writeBytes(bytes); 32 | out.writeBytes(buf); 33 | 34 | logger.debug("send message, remoteAddress: {}, content length {}, msgTypeCode: {}", ctx.channel().remoteAddress(), length, code); 35 | } catch (Exception e) { 36 | logger.error("[client] msg encode has error", e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/ResponseCollector.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain; 2 | 3 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 | import com.google.protobuf.Message; 5 | import io.netty.util.HashedWheelTimer; 6 | import io.netty.util.Timeout; 7 | 8 | import java.time.Duration; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.TimeoutException; 12 | 13 | /** 14 | * Date: 2019-05-18 15 | * Time: 13:50 16 | * 17 | * @author yrw 18 | */ 19 | public class ResponseCollector { 20 | 21 | private static final HashedWheelTimer TIMER = new HashedWheelTimer(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("response-timer-%d").build()); 22 | private final Duration responseTimeout; 23 | 24 | private final String debugMsg; 25 | private CompletableFuture future; 26 | 27 | public ResponseCollector(Duration responseTimeout, String debugMsg) { 28 | this.responseTimeout = responseTimeout; 29 | this.future = new CompletableFuture<>(); 30 | this.debugMsg = debugMsg; 31 | applyResponseTimeout(future, responseTimeout); 32 | } 33 | 34 | private void applyResponseTimeout(CompletableFuture responseFuture, Duration duration) { 35 | Duration durationTime = duration != null ? duration : responseTimeout; 36 | 37 | Timeout hwtTimeout = TIMER.newTimeout(ignored -> 38 | responseFuture.completeExceptionally(new TimeoutException(debugMsg)) 39 | , durationTime.toMillis(), TimeUnit.MILLISECONDS); 40 | 41 | responseFuture.whenComplete((ignored1, ignored2) -> hwtTimeout.cancel()); 42 | } 43 | 44 | public CompletableFuture getFuture() { 45 | return future; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/ResultWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain; 2 | 3 | /** 4 | * Date: 2019-04-21 5 | * Time: 22:19 6 | * 7 | * @author yrw 8 | */ 9 | public class ResultWrapper { 10 | 11 | private final static String SUCCESS = "SUCCESS"; 12 | 13 | private Integer status; 14 | private String msg; 15 | private T data; 16 | 17 | public ResultWrapper() { 18 | } 19 | 20 | public ResultWrapper(Integer status, String msg, T data) { 21 | this.status = status; 22 | this.msg = msg; 23 | this.data = data; 24 | } 25 | 26 | public static ResultWrapper success() { 27 | ResultWrapper resultWrapper = new ResultWrapper<>(); 28 | resultWrapper.setStatus(200); 29 | resultWrapper.setMsg(SUCCESS); 30 | 31 | return resultWrapper; 32 | } 33 | 34 | public static ResultWrapper success(T data) { 35 | ResultWrapper resultWrapper = success(); 36 | resultWrapper.setData(data); 37 | return resultWrapper; 38 | } 39 | 40 | 41 | public static ResultWrapper fail(String message) { 42 | ResultWrapper resultWrapper = new ResultWrapper<>(); 43 | resultWrapper.setStatus(500); 44 | resultWrapper.setMsg(message); 45 | return resultWrapper; 46 | } 47 | 48 | public static ResultWrapper wrapBol(boolean success) { 49 | return success ? success() : fail("operation failed, please try again!"); 50 | } 51 | 52 | public Integer getStatus() { 53 | return status; 54 | } 55 | 56 | public void setStatus(Integer status) { 57 | this.status = status; 58 | } 59 | 60 | public T getData() { 61 | return data; 62 | } 63 | 64 | public void setData(T data) { 65 | this.data = data; 66 | } 67 | 68 | public String getMsg() { 69 | return msg; 70 | } 71 | 72 | public void setMsg(String msg) { 73 | this.msg = msg; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain; 2 | 3 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Date: 2019-04-21 9 | * Time: 16:57 10 | * 11 | * @author yrw 12 | */ 13 | public class UserInfo { 14 | 15 | private String id; 16 | 17 | private String username; 18 | 19 | private String token; 20 | 21 | private List relations; 22 | 23 | public String getUsername() { 24 | return username; 25 | } 26 | 27 | public void setUsername(String username) { 28 | this.username = username; 29 | } 30 | 31 | public String getId() { 32 | return id; 33 | } 34 | 35 | public void setId(String id) { 36 | this.id = id; 37 | } 38 | 39 | public String getToken() { 40 | return token; 41 | } 42 | 43 | public void setToken(String token) { 44 | this.token = token; 45 | } 46 | 47 | public List getRelations() { 48 | return relations; 49 | } 50 | 51 | public void setRelations(List relations) { 52 | this.relations = relations; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/conn/AbstractConn.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.conn; 2 | 3 | import io.netty.channel.ChannelFuture; 4 | import io.netty.channel.ChannelHandlerContext; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Date: 2019-05-02 10 | * Time: 14:50 11 | * 12 | * @author yrw 13 | */ 14 | public abstract class AbstractConn implements Conn { 15 | 16 | private Serializable netId; 17 | private ChannelHandlerContext ctx; 18 | 19 | public AbstractConn(ChannelHandlerContext ctx) { 20 | this.ctx = ctx; 21 | this.netId = generateNetId(ctx); 22 | this.ctx.channel().attr(Conn.NET_ID).set(netId); 23 | } 24 | 25 | /** 26 | * 生成连接id 27 | * 28 | * @param ctx 29 | * @return 30 | */ 31 | protected abstract Serializable generateNetId(ChannelHandlerContext ctx); 32 | 33 | @Override 34 | public Serializable getNetId() { 35 | return netId; 36 | } 37 | 38 | @Override 39 | public ChannelHandlerContext getCtx() { 40 | return ctx; 41 | } 42 | 43 | @Override 44 | public ChannelFuture close() { 45 | return ctx.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/conn/Conn.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.conn; 2 | 3 | import io.netty.channel.ChannelFuture; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.util.AttributeKey; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 连接 11 | * Date: 2019-05-02 12 | * Time: 14:50 13 | * 14 | * @author yrw 15 | */ 16 | public interface Conn { 17 | 18 | AttributeKey NET_ID = AttributeKey.valueOf("netId"); 19 | 20 | /** 21 | * 获取连接id 22 | * 23 | * @return 24 | */ 25 | Serializable getNetId(); 26 | 27 | /** 28 | * 获取ChannelHandlerContext 29 | * 30 | * @return 31 | */ 32 | ChannelHandlerContext getCtx(); 33 | 34 | /** 35 | * 关闭连接 36 | * 37 | * @return 38 | */ 39 | ChannelFuture close(); 40 | } 41 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/conn/ConnContext.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.conn; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 存储连接的容器 9 | * Date: 2019-05-04 10 | * Time: 11:46 11 | * 12 | * @author yrw 13 | */ 14 | public interface ConnContext { 15 | 16 | /** 17 | * 获取连接 18 | * 19 | * @param ctx 20 | * @return 21 | */ 22 | C getConn(ChannelHandlerContext ctx); 23 | 24 | /** 25 | * 获取连接 26 | * 27 | * @param netId 28 | * @return 29 | */ 30 | C getConn(Serializable netId); 31 | 32 | /** 33 | * 添加连接 34 | * 35 | * @param conn 36 | */ 37 | void addConn(C conn); 38 | 39 | /** 40 | * 删除连接 41 | * 42 | * @param netId 43 | */ 44 | void removeConn(Serializable netId); 45 | 46 | /** 47 | * 删除连接 48 | * 49 | * @param ctx 50 | */ 51 | void removeConn(ChannelHandlerContext ctx); 52 | 53 | /** 54 | * 删除所有连接 55 | */ 56 | void removeAllConn(); 57 | } 58 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/conn/ConnectorConn.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.conn; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Date: 2019-02-09 9 | * Time: 23:42 10 | * 11 | * @author yrw 12 | */ 13 | public class ConnectorConn extends AbstractConn { 14 | 15 | public ConnectorConn(ChannelHandlerContext ctx) { 16 | super(ctx); 17 | } 18 | 19 | @Override 20 | protected Serializable generateNetId(ChannelHandlerContext ctx) { 21 | return ctx.channel().attr(Conn.NET_ID).get(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/conn/MemoryConnContext.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.conn; 2 | 3 | import com.google.inject.Singleton; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.Serializable; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.ConcurrentMap; 11 | 12 | /** 13 | * 使用内存存储连接 14 | * Date: 2019-05-04 15 | * Time: 12:52 16 | * 17 | * @author yrw 18 | */ 19 | @Singleton 20 | public class MemoryConnContext implements ConnContext { 21 | private static final Logger logger = LoggerFactory.getLogger(MemoryConnContext.class); 22 | 23 | protected ConcurrentMap connMap; 24 | 25 | public MemoryConnContext() { 26 | this.connMap = new ConcurrentHashMap<>(); 27 | } 28 | 29 | @Override 30 | public C getConn(ChannelHandlerContext ctx) { 31 | Serializable netId = ctx.channel().attr(Conn.NET_ID).get(); 32 | if (netId == null) { 33 | logger.warn("Conn netId not found in ctx, ctx: {}", ctx.toString()); 34 | return null; 35 | } 36 | 37 | C conn = connMap.get(netId); 38 | if (conn == null) { 39 | logger.warn("Conn not found, netId: {}", netId); 40 | } 41 | return conn; 42 | } 43 | 44 | @Override 45 | public C getConn(Serializable netId) { 46 | C conn = connMap.get(netId); 47 | if (conn == null) { 48 | logger.warn("Conn not found, netId: {}", netId); 49 | } 50 | return conn; 51 | } 52 | 53 | @Override 54 | public void addConn(C conn) { 55 | logger.debug("add a conn, netId: {}", conn.getNetId()); 56 | connMap.put(conn.getNetId(), conn); 57 | } 58 | 59 | @Override 60 | public void removeConn(Serializable netId) { 61 | connMap.computeIfPresent(netId, (id, c) -> { 62 | c.close(); 63 | return null; 64 | }); 65 | } 66 | 67 | @Override 68 | public void removeConn(ChannelHandlerContext ctx) { 69 | Serializable netId = ctx.channel().attr(Conn.NET_ID).get(); 70 | if (netId == null) { 71 | logger.warn("Can't find a netId for the ctx"); 72 | } else { 73 | removeConn(netId); 74 | } 75 | } 76 | 77 | @Override 78 | public void removeAllConn() { 79 | connMap.clear(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/constant/ImConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.constant; 2 | 3 | /** 4 | * Date: 2019-05-15 5 | * Time: 22:44 6 | * 7 | * @author yrw 8 | */ 9 | public class ImConstant { 10 | 11 | public static final String MQ_EXCHANGE = "im"; 12 | public static final String MQ_OFFLINE_QUEUE = "im_offline"; 13 | public static final String MQ_ROUTING_KEY = "im_offline"; 14 | } -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/constant/MsgVersion.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.constant; 2 | 3 | import java.util.stream.Stream; 4 | 5 | /** 6 | * Date: 2019-09-07 7 | * Time: 07:42 8 | * 9 | * @author yrw 10 | */ 11 | public enum MsgVersion { 12 | 13 | /** 14 | * version 1 15 | */ 16 | V1(1); 17 | 18 | private int version; 19 | 20 | MsgVersion(int version) { 21 | this.version = version; 22 | } 23 | 24 | public static MsgVersion get(int version) { 25 | return Stream.of(values()).filter(n -> n.version == version) 26 | .findFirst().orElseThrow(IllegalArgumentException::new); 27 | } 28 | 29 | public int getVersion() { 30 | return version; 31 | } 32 | } -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/po/DbModel.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.po; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Date: 2019-02-09 7 | * Time: 14:03 8 | * 9 | * @author yrw 10 | */ 11 | public class DbModel { 12 | 13 | private Long id; 14 | 15 | private Date gmtCreate; 16 | 17 | private Date gmtUpdate; 18 | 19 | private boolean deleted; 20 | 21 | public Long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(Long id) { 26 | this.id = id; 27 | } 28 | 29 | public Date getGmtCreate() { 30 | return gmtCreate; 31 | } 32 | 33 | public void setGmtCreate(Date gmtCreate) { 34 | this.gmtCreate = gmtCreate; 35 | } 36 | 37 | public Date getGmtUpdate() { 38 | return gmtUpdate; 39 | } 40 | 41 | public void setGmtUpdate(Date gmtUpdate) { 42 | this.gmtUpdate = gmtUpdate; 43 | } 44 | 45 | public boolean isDeleted() { 46 | return deleted; 47 | } 48 | 49 | public void setDeleted(boolean deleted) { 50 | this.deleted = deleted; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/po/Offline.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.po; 2 | 3 | /** 4 | * Date: 2019-05-05 5 | * Time: 09:47 6 | * 7 | * @author yrw 8 | */ 9 | public class Offline extends DbModel { 10 | 11 | private Long msgId; 12 | 13 | private Integer msgCode; 14 | 15 | private String toUserId; 16 | 17 | private byte[] content; 18 | 19 | private Boolean hasRead; 20 | 21 | public Boolean getHasRead() { 22 | return hasRead; 23 | } 24 | 25 | public void setHasRead(Boolean hasRead) { 26 | this.hasRead = hasRead; 27 | } 28 | 29 | public Long getMsgId() { 30 | return msgId; 31 | } 32 | 33 | public void setMsgId(Long msgId) { 34 | this.msgId = msgId; 35 | } 36 | 37 | public Integer getMsgCode() { 38 | return msgCode; 39 | } 40 | 41 | public void setMsgCode(Integer msgCode) { 42 | this.msgCode = msgCode; 43 | } 44 | 45 | public String getToUserId() { 46 | return toUserId; 47 | } 48 | 49 | public void setToUserId(String toUserId) { 50 | this.toUserId = toUserId; 51 | } 52 | 53 | public byte[] getContent() { 54 | return content; 55 | } 56 | 57 | public void setContent(byte[] content) { 58 | this.content = content; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/po/Relation.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.po; 2 | 3 | /** 4 | * Date: 2019-02-09 5 | * Time: 20:44 6 | * 7 | * @author yrw 8 | */ 9 | public class Relation extends DbModel { 10 | 11 | private String userId1; 12 | 13 | private String userId2; 14 | 15 | private String encryptKey; 16 | 17 | public String getUserId1() { 18 | return userId1; 19 | } 20 | 21 | public void setUserId1(String userId1) { 22 | this.userId1 = userId1; 23 | } 24 | 25 | public String getUserId2() { 26 | return userId2; 27 | } 28 | 29 | public void setUserId2(String userId2) { 30 | this.userId2 = userId2; 31 | } 32 | 33 | public String getEncryptKey() { 34 | return encryptKey; 35 | } 36 | 37 | public void setEncryptKey(String encryptKey) { 38 | this.encryptKey = encryptKey; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/po/RelationDetail.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.po; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | /** 6 | * Date: 2019-08-17 7 | * Time: 20:28 8 | * 9 | * @author yrw 10 | */ 11 | @JsonIgnoreProperties({"deleted", "gmtUpdate"}) 12 | public class RelationDetail extends Relation { 13 | 14 | private String username1; 15 | 16 | private String username2; 17 | 18 | public String getUsername1() { 19 | return username1; 20 | } 21 | 22 | public void setUsername1(String username1) { 23 | this.username1 = username1; 24 | } 25 | 26 | public String getUsername2() { 27 | return username2; 28 | } 29 | 30 | public void setUsername2(String username2) { 31 | this.username2 = username2; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/domain/po/User.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.domain.po; 2 | 3 | /** 4 | * Date: 2019-02-09 5 | * Time: 14:02 6 | * 7 | * @author yrw 8 | */ 9 | public class User extends DbModel { 10 | 11 | private String username; 12 | 13 | private String pwdHash; 14 | 15 | private String salt; 16 | 17 | public String getUsername() { 18 | return username; 19 | } 20 | 21 | public void setUsername(String username) { 22 | this.username = username; 23 | } 24 | 25 | public String getPwdHash() { 26 | return pwdHash; 27 | } 28 | 29 | public void setPwdHash(String pwdHash) { 30 | this.pwdHash = pwdHash; 31 | } 32 | 33 | public String getSalt() { 34 | return salt; 35 | } 36 | 37 | public void setSalt(String salt) { 38 | this.salt = salt; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/exception/ImException.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.exception; 2 | 3 | /** 4 | * Date: 2019-02-09 5 | * Time: 13:53 6 | * 7 | * @author yrw 8 | */ 9 | public class ImException extends RuntimeException { 10 | 11 | public ImException(String message, Throwable e) { 12 | super(message, e); 13 | } 14 | 15 | public ImException(Throwable e) { 16 | super(e); 17 | } 18 | 19 | public ImException(String message) { 20 | super(message); 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | String s = getClass().getName(); 26 | String message = getLocalizedMessage(); 27 | return (message != null) ? (s + ": " + message) : s; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/function/ImBiConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.function; 2 | 3 | /** 4 | * Date: 2019-05-19 5 | * Time: 22:33 6 | * 7 | * @author yrw 8 | */ 9 | @FunctionalInterface 10 | public interface ImBiConsumer { 11 | 12 | /** 13 | * Performs this operation on the given arguments. 14 | * 15 | * @param t the first input argument 16 | * @param u the second input argument 17 | * @throws Exception 18 | */ 19 | void accept(T t, U u) throws Exception; 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/parse/AbstractByEnumParser.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.parse; 2 | 3 | import com.github.yuanrw.im.common.function.ImBiConsumer; 4 | import com.google.protobuf.Message; 5 | import com.google.protobuf.ProtocolMessageEnum; 6 | import io.netty.channel.ChannelHandlerContext; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | /** 13 | * Date: 2019-05-23 14 | * Time: 18:40 15 | * 16 | * @author yrw 17 | */ 18 | public abstract class AbstractByEnumParser { 19 | 20 | private Map> parseMap; 21 | 22 | public AbstractByEnumParser(int size) { 23 | this.parseMap = new HashMap<>(size); 24 | } 25 | 26 | public void register(E type, ImBiConsumer consumer) { 27 | parseMap.put(type, consumer); 28 | } 29 | 30 | /** 31 | * 获取枚举 32 | * 33 | * @param msg 34 | * @return 35 | */ 36 | protected abstract E getType(M msg); 37 | 38 | public ImBiConsumer generateFun() { 39 | return (m, ctx) -> Optional.ofNullable(parseMap.get(getType(m))) 40 | .orElseThrow(() -> new IllegalArgumentException("Invalid msg enum")) 41 | .accept(m, ctx); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/parse/AbstractMsgParser.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.parse; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.github.yuanrw.im.common.function.ImBiConsumer; 5 | import com.github.yuanrw.im.protobuf.generate.Internal; 6 | import com.google.protobuf.Message; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * Date: 2019-05-18 16 | * Time: 16:17 17 | * 18 | * @author yrw 19 | */ 20 | public abstract class AbstractMsgParser { 21 | private Logger logger = LoggerFactory.getLogger(AbstractMsgParser.class); 22 | 23 | private Map, ImBiConsumer> parserMap; 24 | 25 | protected AbstractMsgParser() { 26 | this.parserMap = new HashMap<>(); 27 | registerParsers(); 28 | } 29 | 30 | public static void checkFrom(Message message, Internal.InternalMsg.Module module) { 31 | if (message instanceof Internal.InternalMsg) { 32 | Internal.InternalMsg m = (Internal.InternalMsg) message; 33 | if (m.getFrom() != module) { 34 | throw new ImException("[unexpected msg] expect msg from: " + module.name() + 35 | ", but received msg from: " + m.getFrom().name() + "\n\rmsg: " + m.toString()); 36 | } 37 | } 38 | } 39 | 40 | public static void checkDest(Message message, Internal.InternalMsg.Module module) { 41 | if (message instanceof Internal.InternalMsg) { 42 | Internal.InternalMsg m = (Internal.InternalMsg) message; 43 | if (m.getDest() != module) { 44 | throw new ImException("[unexpected msg] expect msg to: " + module.name() + 45 | ", but received msg to: " + m.getFrom().name()); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * 注册msg处理方法 52 | */ 53 | public abstract void registerParsers(); 54 | 55 | protected void register(Class clazz, ImBiConsumer consumer) { 56 | parserMap.put(clazz, consumer); 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | public void parse(Message msg, ChannelHandlerContext ctx) { 61 | ImBiConsumer consumer = parserMap.get(msg.getClass()); 62 | if (consumer == null) { 63 | logger.warn("[message parser] unexpected msg: {}", msg.toString()); 64 | } 65 | doParse(consumer, msg.getClass(), msg, ctx); 66 | } 67 | 68 | private void doParse(ImBiConsumer consumer, Class clazz, Message msg, ChannelHandlerContext ctx) { 69 | T m = clazz.cast(msg); 70 | try { 71 | consumer.accept(m, ctx); 72 | } catch (Exception e) { 73 | throw new ImException("[msg parse] has error", e); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/parse/AckParser.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.parse; 2 | 3 | import com.github.yuanrw.im.protobuf.generate.Ack; 4 | 5 | /** 6 | * Date: 2019-05-26 7 | * Time: 20:37 8 | * 9 | * @author yrw 10 | */ 11 | public class AckParser extends AbstractByEnumParser { 12 | 13 | public AckParser(int size) { 14 | super(size); 15 | } 16 | 17 | @Override 18 | protected Ack.AckMsg.MsgType getType(Ack.AckMsg msg) { 19 | return msg.getMsgType(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/parse/InternalParser.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.parse; 2 | 3 | import com.github.yuanrw.im.protobuf.generate.Internal; 4 | 5 | /** 6 | * Date: 2019-05-26 7 | * Time: 20:36 8 | * 9 | * @author yrw 10 | */ 11 | public class InternalParser extends AbstractByEnumParser { 12 | 13 | public InternalParser(int size) { 14 | super(size); 15 | } 16 | 17 | @Override 18 | protected Internal.InternalMsg.MsgType getType(Internal.InternalMsg msg) { 19 | return msg.getMsgType(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/parse/ParseService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.parse; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum; 5 | import com.github.yuanrw.im.protobuf.generate.Ack; 6 | import com.github.yuanrw.im.protobuf.generate.Chat; 7 | import com.github.yuanrw.im.protobuf.generate.Internal; 8 | import com.google.protobuf.InvalidProtocolBufferException; 9 | import com.google.protobuf.Message; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * Date: 2019-04-14 16 | * Time: 16:09 17 | * 18 | * @author yrw 19 | */ 20 | public class ParseService { 21 | 22 | private Map parseFunctionMap; 23 | 24 | public ParseService() { 25 | parseFunctionMap = new HashMap<>(MsgTypeEnum.values().length); 26 | 27 | parseFunctionMap.put(MsgTypeEnum.CHAT, Chat.ChatMsg::parseFrom); 28 | parseFunctionMap.put(MsgTypeEnum.INTERNAL, Internal.InternalMsg::parseFrom); 29 | parseFunctionMap.put(MsgTypeEnum.ACK, Ack.AckMsg::parseFrom); 30 | } 31 | 32 | public Message getMsgByCode(int code, byte[] bytes) throws InvalidProtocolBufferException { 33 | MsgTypeEnum msgType = MsgTypeEnum.getByCode(code); 34 | Parse parseFunction = parseFunctionMap.get(msgType); 35 | if (parseFunction == null) { 36 | throw new ImException("[msg parse], no proper parse function, msgType: " + msgType.name()); 37 | } 38 | return parseFunction.process(bytes); 39 | } 40 | 41 | @FunctionalInterface 42 | public interface Parse { 43 | /** 44 | * parse msg 45 | * 46 | * @param bytes msg bytes 47 | * @return 48 | * @throws InvalidProtocolBufferException 49 | */ 50 | Message process(byte[] bytes) throws InvalidProtocolBufferException; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/rest/AbstractRestService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.rest; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.yuanrw.im.common.domain.ResultWrapper; 6 | import com.github.yuanrw.im.common.exception.ImException; 7 | import okhttp3.logging.HttpLoggingInterceptor; 8 | import retrofit2.Response; 9 | import retrofit2.Retrofit; 10 | import retrofit2.converter.jackson.JacksonConverterFactory; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * Date: 2019-04-21 16 | * Time: 16:45 17 | * 18 | * @author yrw 19 | */ 20 | public abstract class AbstractRestService { 21 | 22 | protected R restClient; 23 | 24 | public AbstractRestService(Class clazz, String url) { 25 | 26 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); 27 | logging.setLevel(HttpLoggingInterceptor.Level.BODY); 28 | 29 | ObjectMapper objectMapper = new ObjectMapper(); 30 | objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 31 | 32 | Retrofit retrofit = new Retrofit.Builder() 33 | .baseUrl(url) 34 | .addConverterFactory(JacksonConverterFactory.create(objectMapper)) 35 | .build(); 36 | 37 | this.restClient = retrofit.create(clazz); 38 | } 39 | 40 | protected T doRequest(RestFunction function) { 41 | try { 42 | Response> response = function.doRequest(); 43 | if (!response.isSuccessful()) { 44 | throw new ImException("[rest service] status is not 200, response body: " + response.toString()); 45 | } 46 | if (response.body() == null) { 47 | throw new ImException("[rest service] response body is null"); 48 | } 49 | if (response.body().getStatus() != 200) { 50 | throw new ImException("[rest service] status is not 200, response body: " + new ObjectMapper().writeValueAsString(response.body())); 51 | } 52 | return response.body().getData(); 53 | } catch (IOException e) { 54 | throw new ImException("[rest service] has error", e); 55 | } 56 | } 57 | 58 | @FunctionalInterface 59 | protected interface RestFunction { 60 | /** 61 | * 执行一个http请求 62 | * 63 | * @return 64 | * @throws IOException 65 | */ 66 | Response> doRequest() throws IOException; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/util/Encryption.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.util; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import org.apache.commons.codec.binary.Base64; 5 | 6 | import javax.crypto.Cipher; 7 | import javax.crypto.SecretKey; 8 | import javax.crypto.spec.IvParameterSpec; 9 | import javax.crypto.spec.SecretKeySpec; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * Date: 2019-05-06 14 | * Time: 17:34 15 | * 16 | * @author yrw 17 | */ 18 | public class Encryption { 19 | 20 | public static byte[] encrypt(String key, String initVector, byte[] value) { 21 | try { 22 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 23 | SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); 24 | IvParameterSpec ivParameterSpec = new IvParameterSpec(initVector.getBytes(StandardCharsets.UTF_8)); 25 | cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); 26 | return Base64.encodeBase64(cipher.doFinal(value)); 27 | } catch (Exception e) { 28 | throw new ImException("[Encryption] encrypt chat msg failed", e); 29 | } 30 | } 31 | 32 | public static byte[] decrypt(String key, String initVector, byte[] encrypted) { 33 | try { 34 | SecretKeySpec sKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); 35 | IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(StandardCharsets.UTF_8)); 36 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 37 | cipher.init(Cipher.DECRYPT_MODE, sKeySpec, iv); 38 | return cipher.doFinal(Base64.decodeBase64(encrypted)); 39 | } catch (Exception e) { 40 | throw new ImException("[Encryption] decrypt chat msg failed", e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/util/IdWorker.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.util; 2 | 3 | /** 4 | * Date: 2019-05-06 5 | * Time: 20:09 6 | * 7 | * @author yrw 8 | */ 9 | public class IdWorker { 10 | 11 | private static SnowFlake snowFlake; 12 | 13 | static { 14 | snowFlake = new SnowFlake(1, 1); 15 | } 16 | 17 | public static Long genId() { 18 | return snowFlake.nextId(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/util/SnowFlake.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.util; 2 | 3 | /** 4 | * Date: 2019-05-06 5 | * Time: 15:02 6 | * 7 | * @author yrw 8 | */ 9 | public class SnowFlake { 10 | 11 | /** 12 | * 起始的时间戳 13 | */ 14 | private final static long START_STMP = 1480166465631L; 15 | 16 | /** 17 | * 每一部分占用的位数 18 | */ 19 | private final static long SEQUENCE_BIT = 12; //序列号占用的位数 20 | private final static long MACHINE_BIT = 5; //机器标识占用的位数 21 | private final static long DATACENTER_BIT = 5;//数据中心占用的位数 22 | 23 | /** 24 | * 每一部分的最大值 25 | */ 26 | private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); 27 | private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); 28 | private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); 29 | 30 | /** 31 | * 每一部分向左的位移 32 | */ 33 | private final static long MACHINE_LEFT = SEQUENCE_BIT; 34 | private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; 35 | private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; 36 | 37 | private long datacenterId; //数据中心 38 | private long machineId; //机器标识 39 | private long sequence = 0L; //序列号 40 | private long lastStmp = -1L;//上一次时间戳 41 | 42 | public SnowFlake(long datacenterId, long machineId) { 43 | if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { 44 | throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); 45 | } 46 | if (machineId > MAX_MACHINE_NUM || machineId < 0) { 47 | throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); 48 | } 49 | this.datacenterId = datacenterId; 50 | this.machineId = machineId; 51 | } 52 | 53 | /** 54 | * 产生下一个ID 55 | * 56 | * @return 57 | */ 58 | public synchronized long nextId() { 59 | long currStmp = getNewstmp(); 60 | if (currStmp < lastStmp) { 61 | throw new RuntimeException("Clock moved backwards. Refusing to generate id"); 62 | } 63 | 64 | if (currStmp == lastStmp) { 65 | //相同毫秒内,序列号自增 66 | sequence = (sequence + 1) & MAX_SEQUENCE; 67 | //同一毫秒的序列数已经达到最大 68 | if (sequence == 0L) { 69 | currStmp = getNextMill(); 70 | } 71 | } else { 72 | //不同毫秒内,序列号置为0 73 | sequence = 0L; 74 | } 75 | 76 | lastStmp = currStmp; 77 | 78 | return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分 79 | | datacenterId << DATACENTER_LEFT //数据中心部分 80 | | machineId << MACHINE_LEFT //机器标识部分 81 | | sequence; //序列号部分 82 | } 83 | 84 | private long getNextMill() { 85 | long mill = getNewstmp(); 86 | while (mill <= lastStmp) { 87 | mill = getNewstmp(); 88 | } 89 | return mill; 90 | } 91 | 92 | private long getNewstmp() { 93 | return System.currentTimeMillis(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /common/src/main/java/com/github/yuanrw/im/common/util/TokenGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.util; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Date: 2019-02-09 7 | * Time: 15:34 8 | * 9 | * @author yrw 10 | */ 11 | public class TokenGenerator { 12 | 13 | public static String generate() { 14 | return UUID.randomUUID().toString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /common/src/test/groovy/com/github/yuanrw/im/common/test/ConnContextTest.groovy: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.test 2 | 3 | import com.github.yuanrw.im.common.domain.conn.Conn 4 | import com.github.yuanrw.im.common.domain.conn.ConnectorConn 5 | import com.github.yuanrw.im.common.domain.conn.MemoryConnContext 6 | import io.netty.channel.Channel 7 | import io.netty.channel.ChannelHandlerContext 8 | import io.netty.util.Attribute 9 | import spock.lang.Specification 10 | 11 | /** 12 | * Date: 2019-06-02 13 | * Time: 16:04 14 | * @author yrw 15 | */ 16 | class ConnContextTest extends Specification { 17 | 18 | def "test internal conn"() { 19 | given: 20 | def ctx = Mock(ChannelHandlerContext) { 21 | channel() >> Mock(Channel) { 22 | attr(Conn.NET_ID) >> Mock(Attribute) { 23 | get() >> givenNetId 24 | } 25 | } 26 | } 27 | ConnectorConn conn = new ConnectorConn(ctx) 28 | 29 | expect: 30 | conn.getCtx() == ctx 31 | conn.getNetId() == expectedNetId 32 | 33 | where: 34 | givenNetId | expectedNetId 35 | "987986892353" | "987986892353" 36 | "agarteag" | "agarteag" 37 | } 38 | 39 | def "test memory conn context"() { 40 | given: 41 | def netId = "987986892353"; 42 | def ctx = Mock(ChannelHandlerContext) { 43 | channel() >> Mock(Channel) { 44 | attr(Conn.NET_ID) >> Mock(Attribute) { 45 | get() >> netId 46 | } 47 | } 48 | } 49 | def context = new MemoryConnContext() 50 | 51 | when: 52 | ConnectorConn conn = new ConnectorConn(ctx) 53 | context.addConn(conn) 54 | 55 | then: 56 | context.getConn(netId) == conn 57 | 58 | when: 59 | context.removeConn(netId) 60 | context.removeConn("123135135") 61 | 62 | then: 63 | context.getConn(netId) == null 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /common/src/test/groovy/com/github/yuanrw/im/common/test/EncryptionTest.groovy: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.test 2 | 3 | import com.github.yuanrw.im.common.util.Encryption 4 | import io.netty.util.CharsetUtil 5 | import spock.lang.Specification 6 | 7 | /** 8 | * Date: 2019-06-01 9 | * Time: 14:21 10 | * @author yrw 11 | */ 12 | class EncryptionTest extends Specification { 13 | 14 | def 'test encode and decode'() { 15 | given: 16 | def key = "ezxqNccrQpKA88bP" 17 | def initVector = "8422365539486988" 18 | def msg = "this is a message".getBytes(CharsetUtil.UTF_8) 19 | 20 | when: 21 | def encryptedMsg = Encryption.encrypt(key, initVector, msg) 22 | def decryptedMsg = Encryption.decrypt(key, initVector, encryptedMsg) 23 | 24 | then: 25 | decryptedMsg == msg 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /common/src/test/groovy/com/github/yuanrw/im/common/test/ResponseCollectorTest.groovy: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.common.test 2 | 3 | import com.github.yuanrw.im.common.domain.ResponseCollector 4 | import com.github.yuanrw.im.common.domain.constant.MsgVersion 5 | import com.github.yuanrw.im.common.util.IdWorker 6 | import com.github.yuanrw.im.protobuf.generate.Internal 7 | import spock.lang.Specification 8 | import spock.lang.Timeout 9 | 10 | import java.time.Duration 11 | import java.util.concurrent.ExecutionException 12 | import java.util.concurrent.TimeUnit 13 | 14 | /** 15 | * Date: 2019-05-31 16 | * Time: 18:49 17 | * @author yrw 18 | */ 19 | class ResponseCollectorTest extends Specification { 20 | 21 | @Timeout(value = 4500, unit = TimeUnit.MILLISECONDS) 22 | def "test wait time out"() { 23 | given: 24 | def msgResponseCollector = new ResponseCollector(Duration.ofSeconds(2), "test") 25 | 26 | when: 27 | def timeStart = System.currentTimeMillis() 28 | msgResponseCollector.future.get() 29 | 30 | then: 31 | thrown(ExecutionException) 32 | 33 | def timeEnd = System.currentTimeMillis() 34 | timeEnd - timeStart >= 1800 35 | println(timeEnd - timeStart) 36 | } 37 | 38 | @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) 39 | def "test when completed"() { 40 | given: 41 | def collector = new ArrayList() 42 | def msgResponseCollector = new ResponseCollector(Duration.ofSeconds(2), "test") 43 | msgResponseCollector.getFuture().whenComplete({ m, e -> collector.add(m) }) 44 | 45 | when: 46 | Internal.InternalMsg msg = Internal.InternalMsg.newBuilder() 47 | .setVersion(MsgVersion.V1.getVersion()) 48 | .setId(IdWorker.genId()) 49 | .setCreateTime(System.currentTimeMillis()) 50 | .setMsgType(Internal.InternalMsg.MsgType.ACK) 51 | .setMsgBody("123") 52 | .setFrom(Internal.InternalMsg.Module.CONNECTOR) 53 | .setDest(Internal.InternalMsg.Module.CLIENT) 54 | .build() 55 | 56 | msgResponseCollector.getFuture().complete(msg) 57 | msgResponseCollector.getFuture().get() 58 | 59 | then: 60 | collector.size() == 1 61 | 62 | Internal.InternalMsg res = collector.get(0) 63 | res.getVersion() == 1 64 | res.getMsgType() == Internal.InternalMsg.MsgType.ACK 65 | res.getMsgBody() == "123" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /common/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /connector/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for connector 2 | # docker build -t yuanrw/connector:$VERSION . 3 | # docker run -p 9081:9081 -d -v /tmp/IM_logs:/tmp/IM_logs --name connector connector 4 | 5 | FROM adoptopenjdk/openjdk11:alpine-jre 6 | MAINTAINER yuanrw <295415537@qq.com> 7 | 8 | ENV SERVICE_NAME connector 9 | ENV VERSION 1.0.0 10 | 11 | EXPOSE 9081 12 | 13 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \ 14 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \ 15 | && apk update upgrade \ 16 | && apk add --no-cache procps unzip curl bash tzdata \ 17 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 18 | && echo "Asia/Shanghai" > /etc/timezone 19 | 20 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip 21 | 22 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \ 23 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \ 24 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \ 25 | && echo "tail -f /dev/null" >> start.sh 26 | 27 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} 28 | 29 | COPY src/main/resources/connector-docker.properties . 30 | COPY src/main/bin/start-docker.sh . 31 | COPY src/main/bin/wait-for-it.sh . 32 | 33 | CMD /bin/bash wait-for-it.sh -t 0 transfer:9082 --strict -- \ 34 | /bin/bash start-docker.sh -------------------------------------------------------------------------------- /connector/src/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | 4 | zip 5 | 6 | 7 | 8 | true 9 | lib 10 | 11 | 12 | 13 | 14 | src/main/resources 15 | / 16 | 17 | logback*.xml 18 | *-docker.* 19 | 20 | unix 21 | 22 | 23 | target 24 | / 25 | 26 | ${project.artifactId}-*.jar 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /connector/src/main/bin/start-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SERVICE_NAME="connector" 3 | VERSION="1.0.0" 4 | 5 | LOG_DIR=/tmp/IM_logs 6 | mkdir -p $LOG_DIR 7 | 8 | # Find Java 9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 10 | java="$JAVA_HOME/bin/java" 11 | elif type -p java > /dev/null 2>&1; then 12 | java=$(type -p java) 13 | elif [[ -x "/usr/bin/java" ]]; then 14 | java="/usr/bin/java" 15 | else 16 | echo "Unable to find Java" 17 | exit 1 18 | fi 19 | 20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" 21 | 22 | echo "JAVA_HOME: $JAVA_HOME" 23 | $java $JAVA_OPTS -Dconfig=$SERVICE_NAME-docker.properties -jar $SERVICE_NAME-$VERSION.jar 24 | echo "SERVICE_NAME started...." -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/config/ConnectorConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.config; 2 | 3 | /** 4 | * Date: 2019-06-14 5 | * Time: 18:29 6 | * 7 | * @author yrw 8 | */ 9 | public class ConnectorConfig { 10 | 11 | private Integer port; 12 | private String[] transferUrls; 13 | private String restUrl; 14 | private String redisHost; 15 | private Integer redisPort; 16 | private String redisPassword; 17 | 18 | public String getRedisHost() { 19 | return redisHost; 20 | } 21 | 22 | public void setRedisHost(String redisHost) { 23 | this.redisHost = redisHost; 24 | } 25 | 26 | public Integer getRedisPort() { 27 | return redisPort; 28 | } 29 | 30 | public void setRedisPort(Integer redisPort) { 31 | this.redisPort = redisPort; 32 | } 33 | 34 | public String getRedisPassword() { 35 | return redisPassword; 36 | } 37 | 38 | public void setRedisPassword(String redisPassword) { 39 | this.redisPassword = redisPassword; 40 | } 41 | 42 | public Integer getPort() { 43 | return port; 44 | } 45 | 46 | public void setPort(Integer port) { 47 | this.port = port; 48 | } 49 | 50 | public String[] getTransferUrls() { 51 | return transferUrls; 52 | } 53 | 54 | public void setTransferUrls(String[] transferUrls) { 55 | this.transferUrls = transferUrls; 56 | } 57 | 58 | public String getRestUrl() { 59 | return restUrl; 60 | } 61 | 62 | public void setRestUrl(String restUrl) { 63 | this.restUrl = restUrl; 64 | } 65 | } -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/config/ConnectorModule.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.config; 2 | 3 | import com.github.yuanrw.im.user.status.factory.UserStatusServiceFactory; 4 | import com.github.yuanrw.im.user.status.service.UserStatusService; 5 | import com.github.yuanrw.im.user.status.service.impl.RedisUserStatusServiceImpl; 6 | import com.google.inject.AbstractModule; 7 | import com.google.inject.assistedinject.FactoryModuleBuilder; 8 | 9 | /** 10 | * Date: 2019-08-08 11 | * Time: 16:31 12 | * 13 | * @author yrw 14 | */ 15 | public class ConnectorModule extends AbstractModule { 16 | 17 | @Override 18 | protected void configure() { 19 | install(new FactoryModuleBuilder() 20 | .implement(UserStatusService.class, RedisUserStatusServiceImpl.class) 21 | .build(UserStatusServiceFactory.class)); 22 | install(new FactoryModuleBuilder().build(ConnectorRestServiceFactory.class)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/config/ConnectorRestServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.config; 2 | 3 | import com.github.yuanrw.im.connector.service.rest.ConnectorRestService; 4 | 5 | /** 6 | * Date: 2019-08-08 7 | * Time: 17:44 8 | * 9 | * @author yrw 10 | */ 11 | public interface ConnectorRestServiceFactory { 12 | 13 | /** 14 | * create a ConnectorRestService 15 | * //todo: need to be singleton 16 | * 17 | * @param url 18 | * @return 19 | */ 20 | ConnectorRestService createService(String url); 21 | } -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/domain/ClientConn.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.domain; 2 | 3 | import com.github.yuanrw.im.common.domain.conn.AbstractConn; 4 | import io.netty.channel.ChannelHandlerContext; 5 | 6 | import java.io.Serializable; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * Date: 2019-05-04 11 | * Time: 11:54 12 | * 13 | * @author yrw 14 | */ 15 | public class ClientConn extends AbstractConn { 16 | 17 | private static final AtomicLong NETID_GENERATOR = new AtomicLong(0); 18 | 19 | private String userId; 20 | 21 | public ClientConn(ChannelHandlerContext ctx) { 22 | super(ctx); 23 | } 24 | 25 | @Override 26 | protected Serializable generateNetId(ChannelHandlerContext ctx) { 27 | return NETID_GENERATOR.getAndIncrement(); 28 | } 29 | 30 | public String getUserId() { 31 | return userId; 32 | } 33 | 34 | public void setUserId(String userId) { 35 | this.userId = userId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/domain/ClientConnContext.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.domain; 2 | 3 | import com.github.yuanrw.im.common.domain.conn.MemoryConnContext; 4 | import com.google.inject.Singleton; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.Serializable; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.ConcurrentMap; 11 | 12 | /** 13 | * 保存客户端连接容器 14 | * Date: 2019-05-02 15 | * Time: 14:55 16 | * 17 | * @author yrw 18 | */ 19 | @Singleton 20 | public class ClientConnContext extends MemoryConnContext { 21 | private static final Logger logger = LoggerFactory.getLogger(ClientConnContext.class); 22 | 23 | private ConcurrentMap userIdToNetId; 24 | 25 | public ClientConnContext() { 26 | this.connMap = new ConcurrentHashMap<>(); 27 | this.userIdToNetId = new ConcurrentHashMap<>(); 28 | } 29 | 30 | public ClientConn getConnByUserId(String userId) { 31 | logger.debug("[get conn on this machine] userId: {}", userId); 32 | 33 | Serializable netId = userIdToNetId.get(userId); 34 | if (netId == null) { 35 | logger.debug("[get conn this machine] netId not found"); 36 | return null; 37 | } 38 | ClientConn conn = connMap.get(netId); 39 | if (conn == null) { 40 | logger.debug("[get conn this machine] conn not found"); 41 | userIdToNetId.remove(userId); 42 | } else { 43 | logger.debug("[get conn this machine] found conn, userId:{}, connId: {}", userId, conn.getNetId()); 44 | } 45 | return conn; 46 | } 47 | 48 | @Override 49 | public void addConn(ClientConn conn) { 50 | String userId = conn.getUserId(); 51 | 52 | if (userIdToNetId.containsKey(userId)) { 53 | removeConn(userIdToNetId.containsKey(userId)); 54 | } 55 | logger.debug("[add conn on this machine] user: {} is online, netId", userId, conn.getNetId()); 56 | 57 | connMap.putIfAbsent(conn.getNetId(), conn); 58 | userIdToNetId.put(conn.getUserId(), conn.getNetId()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/service/OfflineService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.service; 2 | 3 | import com.github.yuanrw.im.common.domain.po.Offline; 4 | import com.github.yuanrw.im.common.parse.ParseService; 5 | import com.github.yuanrw.im.connector.config.ConnectorRestServiceFactory; 6 | import com.github.yuanrw.im.connector.service.rest.ConnectorRestService; 7 | import com.github.yuanrw.im.connector.start.ConnectorStarter; 8 | import com.google.inject.Inject; 9 | import com.google.protobuf.InvalidProtocolBufferException; 10 | import com.google.protobuf.Message; 11 | 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Date: 2019-05-28 18 | * Time: 00:24 19 | * 20 | * @author yrw 21 | */ 22 | public class OfflineService { 23 | 24 | private ConnectorRestService connectorRestService; 25 | private ParseService parseService; 26 | 27 | @Inject 28 | public OfflineService(ConnectorRestServiceFactory connectorRestServiceFactory, ParseService parseService) { 29 | this.connectorRestService = connectorRestServiceFactory.createService(ConnectorStarter.CONNECTOR_CONFIG.getRestUrl()); 30 | this.parseService = parseService; 31 | } 32 | 33 | public List pollOfflineMsg(String userId) { 34 | List msgs = connectorRestService.offlines(userId); 35 | return msgs.stream() 36 | .map(o -> { 37 | try { 38 | return parseService.getMsgByCode(o.getMsgCode(), o.getContent()); 39 | } catch (InvalidProtocolBufferException e) { 40 | e.printStackTrace(); 41 | } 42 | return null; 43 | }).filter(Objects::nonNull).collect(Collectors.toList()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/service/rest/ConnectorRestClient.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.service.rest; 2 | 3 | import com.github.yuanrw.im.common.domain.ResultWrapper; 4 | import com.github.yuanrw.im.common.domain.po.Offline; 5 | import retrofit2.Call; 6 | import retrofit2.http.GET; 7 | import retrofit2.http.Path; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Date: 2019-05-28 13 | * Time: 00:18 14 | * 15 | * @author yrw 16 | */ 17 | public interface ConnectorRestClient { 18 | 19 | @GET("/offline/poll/{id}") 20 | Call>> pollOfflineMsg(@Path("id") String userId); 21 | } 22 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/service/rest/ConnectorRestService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.service.rest; 2 | 3 | import com.github.yuanrw.im.common.domain.po.Offline; 4 | import com.github.yuanrw.im.common.rest.AbstractRestService; 5 | import com.google.inject.Inject; 6 | import com.google.inject.assistedinject.Assisted; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Date: 2019-05-28 12 | * Time: 00:18 13 | * 14 | * @author yrw 15 | */ 16 | public class ConnectorRestService extends AbstractRestService { 17 | 18 | @Inject 19 | public ConnectorRestService(@Assisted String url) { 20 | super(ConnectorRestClient.class, url); 21 | } 22 | 23 | public List offlines(String token) { 24 | return doRequest(() -> restClient.pollOfflineMsg(token).execute()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/start/ConnectorClient.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.start; 2 | 3 | import com.github.yuanrw.im.common.code.MsgDecoder; 4 | import com.github.yuanrw.im.common.code.MsgEncoder; 5 | import com.github.yuanrw.im.common.exception.ImException; 6 | import com.github.yuanrw.im.connector.handler.ConnectorTransferHandler; 7 | import io.netty.bootstrap.Bootstrap; 8 | import io.netty.channel.*; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.nio.NioSocketChannel; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.concurrent.ExecutionException; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.concurrent.TimeoutException; 17 | 18 | /** 19 | * Date: 2019-05-02 20 | * Time: 17:50 21 | * 22 | * @author yrw 23 | */ 24 | public class ConnectorClient { 25 | 26 | private static Logger logger = LoggerFactory.getLogger(ConnectorClient.class); 27 | 28 | static void start(String[] transferUrls) { 29 | for (String transferUrl : transferUrls) { 30 | String[] url = transferUrl.split(":"); 31 | 32 | EventLoopGroup group = new NioEventLoopGroup(); 33 | Bootstrap b = new Bootstrap(); 34 | ChannelFuture f = b.group(group) 35 | .channel(NioSocketChannel.class) 36 | .handler(new ChannelInitializer() { 37 | @Override 38 | protected void initChannel(NioSocketChannel ch) throws Exception { 39 | ChannelPipeline p = ch.pipeline(); 40 | p.addLast("MsgDecoder", ConnectorStarter.injector.getInstance(MsgDecoder.class)); 41 | p.addLast("MsgEncoder", ConnectorStarter.injector.getInstance(MsgEncoder.class)); 42 | p.addLast("ConnectorTransferHandler", ConnectorStarter.injector.getInstance(ConnectorTransferHandler.class)); 43 | } 44 | }).connect(url[0], Integer.parseInt(url[1])) 45 | .addListener((ChannelFutureListener) future -> { 46 | if (future.isSuccess()) { 47 | logger.info("[connector] connect to transfer successfully"); 48 | } else { 49 | throw new ImException("[connector] connect to transfer failed! transfer url: " + transferUrl); 50 | } 51 | }); 52 | 53 | try { 54 | f.get(10, TimeUnit.SECONDS); 55 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 56 | throw new ImException("[connector] connect to transfer failed! transfer url: " + transferUrl, e); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/start/ConnectorServer.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.start; 2 | 3 | import com.github.yuanrw.im.common.code.MsgDecoder; 4 | import com.github.yuanrw.im.common.code.MsgEncoder; 5 | import com.github.yuanrw.im.common.exception.ImException; 6 | import com.github.yuanrw.im.connector.handler.ConnectorClientHandler; 7 | import io.netty.bootstrap.ServerBootstrap; 8 | import io.netty.channel.*; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.SocketChannel; 11 | import io.netty.channel.socket.nio.NioServerSocketChannel; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.net.InetSocketAddress; 16 | import java.util.concurrent.ExecutionException; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.concurrent.TimeoutException; 19 | 20 | /** 21 | * Date: 2019-02-09 22 | * Time: 23:27 23 | * 24 | * @author yrw 25 | */ 26 | public class ConnectorServer { 27 | private static final Logger logger = LoggerFactory.getLogger(ConnectorServer.class); 28 | 29 | static void start(int port) { 30 | EventLoopGroup bossGroup = new NioEventLoopGroup(); 31 | EventLoopGroup workGroup = new NioEventLoopGroup(); 32 | 33 | ServerBootstrap bootstrap = new ServerBootstrap() 34 | .group(bossGroup, workGroup) 35 | .channel(NioServerSocketChannel.class) 36 | .childHandler(new ChannelInitializer() { 37 | @Override 38 | protected void initChannel(SocketChannel channel) throws Exception { 39 | ChannelPipeline pipeline = channel.pipeline(); 40 | pipeline.addLast("MsgDecoder", ConnectorStarter.injector.getInstance(MsgDecoder.class)); 41 | pipeline.addLast("MsgEncoder", ConnectorStarter.injector.getInstance(MsgEncoder.class)); 42 | pipeline.addLast("ConnectorClientHandler", ConnectorStarter.injector.getInstance(ConnectorClientHandler.class)); 43 | } 44 | }); 45 | 46 | ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).addListener((ChannelFutureListener) future -> { 47 | if (future.isSuccess()) { 48 | logger.info("[connector] start successfully at port {}, waiting for clients to connect...", port); 49 | } else { 50 | throw new ImException("[connector] start failed"); 51 | } 52 | }); 53 | 54 | try { 55 | f.get(10, TimeUnit.SECONDS); 56 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 57 | throw new ImException("[connector] start failed", e); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /connector/src/main/java/com/github/yuanrw/im/connector/start/ConnectorStarter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector.start; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.github.yuanrw.im.connector.config.ConnectorConfig; 5 | import com.github.yuanrw.im.connector.config.ConnectorModule; 6 | import com.google.inject.Guice; 7 | import com.google.inject.Injector; 8 | 9 | import java.io.FileInputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.Properties; 13 | 14 | /** 15 | * Date: 2019-05-02 16 | * Time: 17:59 17 | * 18 | * @author yrw 19 | */ 20 | public class ConnectorStarter { 21 | public static ConnectorConfig CONNECTOR_CONFIG = new ConnectorConfig(); 22 | public static Injector injector = Guice.createInjector(new ConnectorModule()); 23 | 24 | public static void main(String[] args) throws IOException { 25 | //parse start parameter 26 | ConnectorStarter.CONNECTOR_CONFIG = parseConfig(); 27 | 28 | //connector to transfer 29 | ConnectorClient.start(CONNECTOR_CONFIG.getTransferUrls()); 30 | 31 | //start connector server 32 | ConnectorServer.start(CONNECTOR_CONFIG.getPort()); 33 | } 34 | 35 | private static ConnectorConfig parseConfig() throws IOException { 36 | Properties properties = getProperties(); 37 | 38 | ConnectorConfig connectorConfig = new ConnectorConfig(); 39 | try { 40 | connectorConfig.setPort(Integer.parseInt(properties.getProperty("port"))); 41 | connectorConfig.setTransferUrls((properties.getProperty("transfer.url")).split(",")); 42 | connectorConfig.setRestUrl(properties.getProperty("rest.url")); 43 | connectorConfig.setRedisHost(properties.getProperty("redis.host")); 44 | connectorConfig.setRedisPort(Integer.parseInt(properties.getProperty("redis.port"))); 45 | connectorConfig.setRedisPassword(properties.getProperty("redis.password")); 46 | } catch (Exception e) { 47 | throw new ImException("there's a parse error, check your config properties", e); 48 | } 49 | 50 | System.setProperty("log.path", properties.getProperty("log.path")); 51 | System.setProperty("log.level", properties.getProperty("log.level")); 52 | 53 | return connectorConfig; 54 | } 55 | 56 | private static Properties getProperties() throws IOException { 57 | InputStream inputStream; 58 | String path = System.getProperty("config"); 59 | if (path == null) { 60 | throw new ImException("connector.properties is not defined"); 61 | } else { 62 | inputStream = new FileInputStream(path); 63 | } 64 | 65 | Properties properties = new Properties(); 66 | properties.load(inputStream); 67 | return properties; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /connector/src/main/resources/connector-docker.properties: -------------------------------------------------------------------------------- 1 | port=9081 2 | 3 | # all transfer services url 4 | transfer.url=transfer:9082 5 | rest.url=http://rest-web:8082 6 | redis.host=im-redis 7 | redis.port=6379 8 | redis.password= 9 | 10 | log.path=/tmp/IM_logs 11 | log.level=info -------------------------------------------------------------------------------- /connector/src/main/resources/connector.properties: -------------------------------------------------------------------------------- 1 | port=9081 2 | 3 | # all transfer services url 4 | transfer.url=127.0.0.1:9082 5 | rest.url=http://127.0.0.1:8082 6 | redis.host=127.0.0.1 7 | redis.port=6379 8 | redis.password= 9 | 10 | log.path=/tmp/IM_logs 11 | log.level=info -------------------------------------------------------------------------------- /connector/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | ${log.path}/connector.log 13 | 14 | rest.%d{yyyy-MM-dd}.log 15 | 16 | 17 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /connector/src/test/groovy/com/github/yuanrw/im/connector/ClientConnContextTest.groovy: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.connector 2 | 3 | import com.github.yuanrw.im.common.domain.conn.Conn 4 | import com.github.yuanrw.im.connector.domain.ClientConn 5 | import com.github.yuanrw.im.connector.domain.ClientConnContext 6 | import io.netty.channel.Channel 7 | import io.netty.channel.ChannelHandlerContext 8 | import io.netty.util.Attribute 9 | import spock.lang.Specification 10 | 11 | /** 12 | * Date: 2019-06-01 13 | * Time: 17:39 14 | * @author yrw 15 | */ 16 | class ClientConnContextTest extends Specification { 17 | 18 | def "test client conn"() { 19 | given: 20 | def attribute = Mock(Attribute) 21 | def c = Mock(Channel) { 22 | attr(Conn.NET_ID) >> attribute 23 | } 24 | def ctx = Mock(ChannelHandlerContext) { 25 | channel() >> c 26 | } 27 | 28 | when: 29 | ClientConn conn = new ClientConn(ctx) 30 | conn.setUserId("123") 31 | then: 32 | conn.getCtx() == ctx 33 | conn.getUserId() == "123" 34 | conn.getNetId() >= 0L 35 | 36 | 1 * ctx.channel().attr(Conn.NET_ID).set(_ as Long) 37 | } 38 | 39 | def "test client conn context"() { 40 | given: 41 | def attribute = Mock(Attribute) 42 | def ctx = Mock(ChannelHandlerContext) { 43 | channel() >> Mock(Channel) { 44 | attr(Conn.NET_ID) >> attribute 45 | } 46 | } 47 | def context = new ClientConnContext() 48 | 49 | when: 50 | def userId = "7073059" 51 | def conn = new ClientConn(ctx) 52 | conn.setUserId(userId) 53 | context.addConn(conn) 54 | 55 | ctx.channel().attr(Conn.NET_ID).get() >> conn.getNetId() 56 | 57 | then: 58 | context.getConn(ctx) == conn 59 | context.getConn(conn.getNetId()) == conn 60 | context.getConnByUserId(userId) == conn 61 | 62 | context.getConn("sndigso") == null 63 | context.getConnByUserId("2222") == null 64 | 65 | when: 66 | context.removeConn(conn.getNetId()) 67 | context.removeConn("123135135") 68 | 69 | then: 70 | context.getConn(conn.getNetId()) == null 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /connector/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | client-samples: 5 | image: yuanrw/client-samples:1.0.0 6 | container_name: client-samples 7 | depends_on: 8 | - rest-web 9 | - connector 10 | - transfer 11 | volumes: 12 | - /tmp/IM_logs/client-samples.log:/tmp/IM_logs/client-samples.log 13 | 14 | rest-web: 15 | image: yuanrw/rest-web:1.0.0 16 | container_name: rest-web 17 | depends_on: 18 | - im-mysql 19 | - im-redis 20 | - im-rabbit 21 | ports: 22 | - "8082:8082" 23 | volumes: 24 | - /tmp/IM_logs/rest.log:/tmp/IM_logs/rest.log 25 | 26 | connector: 27 | image: yuanrw/connector:1.0.0 28 | container_name: connector 29 | depends_on: 30 | - transfer 31 | ports: 32 | - "9081:9081" 33 | volumes: 34 | - /tmp/IM_logs/connector.log:/tmp/IM_logs/connector.log 35 | 36 | transfer: 37 | image: yuanrw/transfer:1.0.0 38 | container_name: transfer 39 | depends_on: 40 | - rest-web 41 | ports: 42 | - "9082:9082" 43 | volumes: 44 | - /tmp/IM_logs/transfer.log:/tmp/IM_logs/transfer.log 45 | 46 | im-redis: 47 | image: redis:alpine 48 | container_name: im-redis 49 | ports: 50 | - "6379:6379" 51 | 52 | im-rabbit: 53 | image: rabbitmq:3-management 54 | container_name: im-rabbit 55 | ports: 56 | - "15672:15672" 57 | - "5672:5672" 58 | environment: 59 | RABBITMQ_ERLANG_COOKIE: 6085e2412b6fa88647466c6a81c0cea0 60 | RABBITMQ_DEFAULT_USER: rabbitmq 61 | RABBITMQ_DEFAULT_PASS: rabbitmq 62 | RABBITMQ_DEFAULT_VHOST: / 63 | volumes: 64 | - ./data/rabbitmq:/var/lib/rabbitmq/mnesia/rabbit@app-rabbitmq:cached 65 | 66 | im-mysql: 67 | container_name: im-mysql 68 | image: mysql/mysql-server:5.7 69 | environment: 70 | TZ: Asia/Shanghai 71 | MYSQL_DATABASE: im 72 | MYSQL_ROOT_PASSWORD: 123456 73 | MYSQL_ROOT_HOST: '%' 74 | ports: 75 | - "13306:3306" 76 | volumes: 77 | - ./sql:/docker-entrypoint-initdb.d 78 | -------------------------------------------------------------------------------- /pic/im-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/52im/IM/0dd4a599b1b2d90319271648630b21e5b867043f/pic/im-structure.png -------------------------------------------------------------------------------- /protobuf/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | protobuf 13 | 14 | 15 | 16 | com.google.protobuf 17 | protobuf-java 18 | 19 | 20 | ch.qos.logback 21 | logback-core 22 | 23 | 24 | ch.qos.logback 25 | logback-classic 26 | 27 | 28 | com.google.inject 29 | guice 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.xolstice.maven.plugins 37 | protobuf-maven-plugin 38 | 0.5.0 39 | true 40 | 41 | ${project.basedir}/src/main/resources/proto 42 | /Users/yrw/Desktop/protoc-3.7.1/bin/protoc 43 | ${project.basedir}/src/main/java 44 | false 45 | ${project.build.directory}/protoc-dependencies 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /protobuf/src/main/java/com/github/yuanrw/im/protobuf/constant/MsgTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.protobuf.constant; 2 | 3 | import com.github.yuanrw.im.protobuf.generate.Ack; 4 | import com.github.yuanrw.im.protobuf.generate.Chat; 5 | import com.github.yuanrw.im.protobuf.generate.Internal; 6 | 7 | import java.util.stream.Stream; 8 | 9 | /** 10 | * 消息类型 11 | * Date: 2019-04-14 12 | * Time: 15:38 13 | * 14 | * @author yrw 15 | */ 16 | public enum MsgTypeEnum { 17 | 18 | /** 19 | * 聊天消息 20 | */ 21 | CHAT(0, Chat.ChatMsg.class), 22 | 23 | /** 24 | * 内部消息 25 | */ 26 | INTERNAL(1, Internal.InternalMsg.class), 27 | 28 | /** 29 | * ack消息 30 | */ 31 | ACK(2, Ack.AckMsg.class); 32 | 33 | int code; 34 | Class clazz; 35 | 36 | MsgTypeEnum(int code, Class clazz) { 37 | this.code = code; 38 | this.clazz = clazz; 39 | } 40 | 41 | public static MsgTypeEnum getByCode(int code) { 42 | return Stream.of(values()).filter(t -> t.code == code) 43 | .findFirst().orElseThrow(IllegalArgumentException::new); 44 | } 45 | 46 | public static MsgTypeEnum getByClass(Class clazz) { 47 | return Stream.of(values()).filter(t -> t.clazz == clazz) 48 | .findFirst().orElseThrow(IllegalArgumentException::new); 49 | } 50 | 51 | public int getCode() { 52 | return code; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /protobuf/src/main/resources/proto/ack.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package com.github.yuanrw.im.protobuf.generate; 4 | option java_outer_classname = "Ack"; 5 | 6 | message AckMsg { 7 | required int32 version = 1; 8 | //协议版本号。第一版本:1,以此类推。 9 | 10 | required int64 id = 2; 11 | //消息id 12 | 13 | required DestType destType = 3; 14 | //接收者类型。 15 | 16 | required string fromId = 4; 17 | //发送者userId 18 | 19 | required string destId = 5; 20 | //接收者userId 21 | 22 | required int64 createTime = 6; 23 | //发送时间 24 | 25 | required MsgType msgType = 7; 26 | //消息类型 27 | 28 | required int64 ackMsgId = 8; 29 | //消息体 30 | 31 | enum DestType { 32 | SINGLE = 0; 33 | GROUP = 1; 34 | } 35 | 36 | enum MsgType { 37 | DELIVERED = 0; 38 | READ = 1; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /protobuf/src/main/resources/proto/chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package com.github.yuanrw.im.protobuf.generate; 4 | option java_outer_classname = "Chat"; 5 | 6 | message ChatMsg { 7 | required int32 version = 1; 8 | //协议版本号。第一版本:1,以此类推。 9 | 10 | required int64 id = 2; 11 | //消息id 12 | 13 | required DestType destType = 3; 14 | //接收者类型。 15 | 16 | required string fromId = 4; 17 | //发送者userId 18 | 19 | required string destId = 5; 20 | //接收者userId 21 | 22 | required int64 createTime = 6; 23 | //发送时间 24 | 25 | required MsgType msgType = 7; 26 | //消息类型 27 | 28 | required bytes msgBody = 8; 29 | //消息体,json,格式由消息类型决定 30 | 31 | optional string addition = 32; 32 | 33 | enum DestType { 34 | SINGLE = 0; 35 | GROUP = 1; 36 | } 37 | 38 | enum MsgType { 39 | TEXT = 0; 40 | FILE = 1; 41 | } 42 | } 43 | 44 | message TextBody { 45 | required string text = 1; 46 | //文字内容 47 | } 48 | 49 | message FileBody { 50 | required string fileId = 1; 51 | //媒体文件上传到得到的KEY,用于生成下载URL。 52 | 53 | required int32 media_crc32 = 2; 54 | //文件的 CRC32 校验码。 55 | 56 | required int32 fSize = 3; 57 | //文件大小(字节数) 58 | 59 | required string fName = 4; 60 | //文件名字 61 | } 62 | -------------------------------------------------------------------------------- /protobuf/src/main/resources/proto/internal.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package com.github.yuanrw.im.protobuf.generate; 4 | option java_outer_classname = "Internal"; 5 | 6 | message InternalMsg { 7 | required int32 version = 1; 8 | //协议版本号。第一版本:1,以此类推。 9 | 10 | required int64 id = 2; 11 | //消息id 12 | 13 | required Module from = 3; 14 | //发送模块 15 | 16 | required Module dest = 4; 17 | //接收模块 18 | 19 | required int64 createTime = 5; 20 | //发送时间 21 | 22 | required MsgType msgType = 6; 23 | //消息类型 24 | 25 | optional string msgBody = 7; 26 | //消息体 27 | 28 | enum Module { 29 | CONNECTOR = 0; 30 | TRANSFER = 1; 31 | CLIENT = 2; 32 | } 33 | 34 | enum MsgType { 35 | GREET = 0; 36 | ACK = 1; 37 | ERROR = 2; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rest/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | rest 13 | pom 14 | 15 | rest-web 16 | rest-spi 17 | 18 | 19 | -------------------------------------------------------------------------------- /rest/rest-spi/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | rest 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | rest-spi 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-webflux 18 | 19 | 20 | com.github.yuanrw.im 21 | common 22 | 23 | 24 | com.google.inject 25 | guice 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /rest/rest-spi/src/main/java/com/github/yuanrw/im/rest/spi/UserSpi.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.spi; 2 | 3 | import com.github.yuanrw.im.rest.spi.domain.UserBase; 4 | 5 | /** 6 | * Date: 2019-07-03 7 | * Time: 17:43 8 | * 9 | * @author yrw 10 | */ 11 | public interface UserSpi { 12 | 13 | /** 14 | * get user by username and password, return user(id can not be null) 15 | * if username and password are right, else return null. 16 | *

17 | * be sure that your password has been properly encrypted 18 | * 19 | * @param username 20 | * @param pwd 21 | * @return 22 | */ 23 | T getUser(String username, String pwd); 24 | 25 | /** 26 | * get user by id, if id not exist then return null. 27 | * 28 | * @param id 29 | * @return 30 | */ 31 | T getById(String id); 32 | } -------------------------------------------------------------------------------- /rest/rest-spi/src/main/java/com/github/yuanrw/im/rest/spi/domain/LdapUser.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.spi.domain; 2 | 3 | /** 4 | * Date: 2019-07-07 5 | * Time: 17:08 6 | * 7 | * @author yrw 8 | */ 9 | public class LdapUser extends UserBase { 10 | 11 | private String email; 12 | 13 | public String getEmail() { 14 | return email; 15 | } 16 | 17 | public void setEmail(String email) { 18 | this.email = email; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rest/rest-spi/src/main/java/com/github/yuanrw/im/rest/spi/domain/UserBase.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.spi.domain; 2 | 3 | /** 4 | * Date: 2019-07-07 5 | * Time: 13:15 6 | * 7 | * @author yrw 8 | */ 9 | public class UserBase { 10 | 11 | private String id; 12 | private String username; 13 | 14 | public String getId() { 15 | return id; 16 | } 17 | 18 | public void setId(String id) { 19 | this.id = id; 20 | } 21 | 22 | public String getUsername() { 23 | return username; 24 | } 25 | 26 | public void setUsername(String username) { 27 | this.username = username; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rest/rest-web/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for rest-web 2 | # docker build -t yuanrw/rest-web:$VERSION . 3 | # docker run -p 8082:8082 -d -v /tmp/IM_logs:/tmp/IM_logs --name rest-web rest-web 4 | 5 | FROM adoptopenjdk/openjdk11:alpine-jre 6 | MAINTAINER yuanrw <295415537@qq.com> 7 | 8 | ENV SERVICE_NAME rest-web 9 | ENV VERSION 1.0.0 10 | 11 | EXPOSE 8082 12 | 13 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \ 14 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \ 15 | && apk update upgrade \ 16 | && apk add --no-cache procps unzip curl bash tzdata \ 17 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 18 | && echo "Asia/Shanghai" > /etc/timezone 19 | 20 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip 21 | 22 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \ 23 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \ 24 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \ 25 | && echo "tail -f /dev/null" >> start-docker.sh 26 | 27 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} 28 | 29 | COPY src/main/resources/application-docker.properties . 30 | COPY src/main/bin/start-docker.sh . 31 | COPY src/main/bin/wait-for-it.sh . 32 | 33 | CMD /bin/bash wait-for-it.sh -t 0 im-mysql:3306 --strict -- \ 34 | /bin/bash wait-for-it.sh -t 0 im-redis:6379 --strict -- \ 35 | /bin/bash wait-for-it.sh -t 0 im-rabbit:5672 --strict -- \ 36 | /bin/bash start-docker.sh -------------------------------------------------------------------------------- /rest/rest-web/src/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | 4 | zip 5 | 6 | 7 | 8 | true 9 | lib 10 | 11 | 12 | 13 | 14 | src/main/resources 15 | / 16 | 17 | logback*.xml 18 | *-docker.* 19 | 20 | unix 21 | 22 | 23 | target 24 | / 25 | 26 | ${project.artifactId}-*.jar 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/bin/start-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SERVICE_NAME="rest-web" 3 | VERSION="1.0.0" 4 | 5 | LOG_DIR=/tmp/IM_logs 6 | mkdir -p $LOG_DIR 7 | 8 | # Find Java 9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 10 | java="$JAVA_HOME/bin/java" 11 | elif type -p java > /dev/null 2>&1; then 12 | java=$(type -p java) 13 | elif [[ -x "/usr/bin/java" ]]; then 14 | java="/usr/bin/java" 15 | else 16 | echo "Unable to find Java" 17 | exit 1 18 | fi 19 | 20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" 21 | 22 | echo "JAVA_HOME: $JAVA_HOME" 23 | $java $JAVA_OPTS -jar $SERVICE_NAME-$VERSION.jar --spring.config.location=application-docker.properties 24 | echo "SERVICE_NAME started...." -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/RestStarter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | /** 9 | * Date: 2019-02-11 10 | * Time: 12:09 11 | * 12 | * @author yrw 13 | */ 14 | @EnableScheduling 15 | @ComponentScan(basePackages = {"com.github.yuanrw.im.rest"}) 16 | @SpringBootApplication 17 | public class RestStarter { 18 | public static void main(String[] args) { 19 | SpringApplication.run(RestStarter.class, args); 20 | } 21 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/config/RestConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.config; 2 | 3 | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; 4 | import com.baomidou.mybatisplus.core.config.GlobalConfig; 5 | import com.github.yuanrw.im.common.domain.constant.ImConstant; 6 | import com.github.yuanrw.im.common.domain.po.DbModel; 7 | import com.github.yuanrw.im.rest.web.handler.ValidHandler; 8 | import org.mybatis.spring.annotation.MapperScan; 9 | import org.springframework.amqp.core.AcknowledgeMode; 10 | import org.springframework.amqp.core.Queue; 11 | import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; 12 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.ComponentScan; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.context.annotation.Primary; 17 | import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; 18 | import org.springframework.data.redis.core.ReactiveRedisTemplate; 19 | import org.springframework.data.redis.core.RedisTemplate; 20 | import org.springframework.data.redis.serializer.RedisSerializationContext; 21 | 22 | import javax.validation.Validator; 23 | 24 | /** 25 | * Date: 2019-04-21 26 | * Time: 15:08 27 | * 28 | * @author yrw 29 | */ 30 | @Configuration 31 | @MapperScan(value = "com.github.yuanrw.im.rest.web.mapper") 32 | @ComponentScan(basePackages = "com.github.yuanrw.im.rest.web.service") 33 | public class RestConfig { 34 | 35 | @Bean 36 | @Primary 37 | public MybatisPlusProperties mybatisPlusProperties() { 38 | MybatisPlusProperties properties = new MybatisPlusProperties(); 39 | GlobalConfig globalConfig = new GlobalConfig(); 40 | 41 | properties.setTypeAliasesSuperType(DbModel.class); 42 | properties.setMapperLocations(new String[]{"classpath*:/mapper/**/*.xml"}); 43 | properties.setGlobalConfig(globalConfig); 44 | 45 | GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig(); 46 | dbConfig.setTablePrefix("im_"); 47 | globalConfig.setDbConfig(dbConfig); 48 | 49 | return properties; 50 | } 51 | 52 | @Bean 53 | public Integer init(Validator validator, RedisTemplate redisTemplate) { 54 | ValidHandler.setValidator(validator); 55 | return 1; 56 | } 57 | 58 | @Bean 59 | public SimpleRabbitListenerContainerFactory listenerFactory(ConnectionFactory connectionFactory) { 60 | SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); 61 | factory.setConnectionFactory(connectionFactory); 62 | factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); 63 | return factory; 64 | } 65 | 66 | @Bean 67 | public Queue offlineQueue() { 68 | return new Queue(ImConstant.MQ_OFFLINE_QUEUE); 69 | } 70 | 71 | @Bean 72 | public ReactiveRedisTemplate reactiveRedisTemplateString 73 | (ReactiveRedisConnectionFactory connectionFactory) { 74 | return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/GlobalErrorAttributes.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.filter; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.reactive.function.server.ServerRequest; 9 | import org.springframework.web.server.ResponseStatusException; 10 | 11 | import java.util.Date; 12 | import java.util.LinkedHashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * Date: 2019-02-12 17 | * Time: 10:43 18 | * 19 | * @author yrw 20 | */ 21 | @Configuration 22 | public class GlobalErrorAttributes extends DefaultErrorAttributes { 23 | 24 | private Logger logger = LoggerFactory.getLogger(GlobalErrorWebExceptionHandler.class); 25 | 26 | @Override 27 | public Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) { 28 | Map errorAttributes = new LinkedHashMap<>(); 29 | errorAttributes.put("timestamp", new Date()); 30 | errorAttributes.put("path", request.path()); 31 | errorAttributes.put("status", 500); 32 | Throwable error = this.getError(request); 33 | 34 | logger.error("[rest] unknown error", this.getError(request)); 35 | 36 | if (error instanceof ResponseStatusException) { 37 | return super.getErrorAttributes(request, includeStackTrace); 38 | } 39 | if (error instanceof ImException) { 40 | ImException e = (ImException) error; 41 | errorAttributes.put("msg", e.getMessage()); 42 | } else if (error instanceof IllegalArgumentException) { 43 | IllegalArgumentException e = (IllegalArgumentException) error; 44 | errorAttributes.put("msg", e.getMessage()); 45 | } else { 46 | errorAttributes.put("msg", "服务器繁忙,请稍后再试!"); 47 | } 48 | return errorAttributes; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/GlobalErrorWebExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.filter; 2 | 3 | import org.springframework.boot.autoconfigure.web.ResourceProperties; 4 | import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; 5 | import org.springframework.boot.web.reactive.error.ErrorAttributes; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.core.annotation.Order; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.http.codec.ServerCodecConfigurer; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.reactive.function.BodyInserters; 13 | import org.springframework.web.reactive.function.server.*; 14 | import reactor.core.publisher.Mono; 15 | 16 | import java.util.Map; 17 | 18 | /** 19 | * Date: 2019-02-12 20 | * Time: 10:37 21 | * 22 | * @author yrw 23 | */ 24 | @Component 25 | @Order(-2) 26 | public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { 27 | 28 | public GlobalErrorWebExceptionHandler( 29 | ErrorAttributes errorAttributes, 30 | ResourceProperties resourceProperties, 31 | ApplicationContext applicationContext, 32 | ServerCodecConfigurer serverCodecConfigurer 33 | ) { 34 | super(errorAttributes, resourceProperties, applicationContext); 35 | this.setMessageWriters(serverCodecConfigurer.getWriters()); 36 | this.setMessageReaders(serverCodecConfigurer.getReaders()); 37 | } 38 | 39 | @Override 40 | protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { 41 | return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); 42 | } 43 | 44 | private Mono renderErrorResponse(ServerRequest request) { 45 | 46 | Map errorPropertiesMap = getErrorAttributes(request, false); 47 | 48 | return ServerResponse.status(HttpStatus.OK) 49 | .contentType(MediaType.APPLICATION_JSON_UTF8) 50 | .body(BodyInserters.fromObject(errorPropertiesMap)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/HeaderFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.filter; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.server.ServerWebExchange; 6 | import org.springframework.web.server.WebFilter; 7 | import org.springframework.web.server.WebFilterChain; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Date: 2019-04-21 12 | * Time: 15:51 13 | * 14 | * @author yrw 15 | */ 16 | @Component 17 | public class HeaderFilter implements WebFilter { 18 | 19 | private TokenManager tokenManager; 20 | 21 | public HeaderFilter(TokenManager tokenManager) { 22 | this.tokenManager = tokenManager; 23 | } 24 | 25 | @Override 26 | public Mono filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) { 27 | String path = serverWebExchange.getRequest().getPath().value(); 28 | 29 | if ("/user/login".equals(path) || path.startsWith("/offline")) { 30 | return webFilterChain.filter(serverWebExchange); 31 | } 32 | if (!serverWebExchange.getRequest().getHeaders().containsKey("token")) { 33 | return Mono.error(new ImException("[rest] user is not login")); 34 | } 35 | 36 | String token = serverWebExchange.getRequest().getHeaders().getFirst("token"); 37 | 38 | return tokenManager.validateToken(token).flatMap(b -> b != null ? webFilterChain.filter(serverWebExchange) : 39 | Mono.error(new ImException("[rest] user is not login"))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/TokenManager.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.filter; 2 | 3 | import com.github.yuanrw.im.common.util.TokenGenerator; 4 | import org.springframework.data.redis.core.ReactiveRedisTemplate; 5 | import org.springframework.stereotype.Service; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.time.Duration; 9 | 10 | /** 11 | * Date: 2019-07-04 12 | * Time: 15:17 13 | * 14 | * @author yrw 15 | */ 16 | @Service 17 | public class TokenManager { 18 | 19 | private static final String SESSION_KEY = "IM:TOKEN:"; 20 | private ReactiveRedisTemplate template; 21 | 22 | public TokenManager(ReactiveRedisTemplate template) { 23 | this.template = template; 24 | } 25 | 26 | public Mono validateToken(String token) { 27 | return template.opsForValue().get(SESSION_KEY + token).map(id -> { 28 | template.expire(SESSION_KEY + token, Duration.ofMinutes(30)); 29 | return id; 30 | }).switchIfEmpty(Mono.empty()); 31 | } 32 | 33 | public Mono createNewToken(String userId) { 34 | String token = TokenGenerator.generate(); 35 | return template.opsForValue().set(SESSION_KEY + token, userId) 36 | .flatMap(b -> b ? template.expire(SESSION_KEY + token, Duration.ofMinutes(30)) : Mono.just(false)) 37 | .flatMap(b -> b ? Mono.just(token) : Mono.empty()); 38 | } 39 | 40 | public Mono expire(String token) { 41 | return template.delete(SESSION_KEY + token).map(l -> l > 0); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/handler/OfflineHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.handler; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.github.yuanrw.im.common.domain.ResultWrapper; 5 | import com.github.yuanrw.im.common.exception.ImException; 6 | import com.github.yuanrw.im.rest.web.service.OfflineService; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.reactive.function.server.ServerRequest; 9 | import org.springframework.web.reactive.function.server.ServerResponse; 10 | import reactor.core.publisher.Mono; 11 | 12 | import static org.springframework.http.MediaType.APPLICATION_JSON; 13 | import static org.springframework.web.reactive.function.BodyInserters.fromObject; 14 | 15 | /** 16 | * Date: 2019-05-27 17 | * Time: 09:52 18 | * 19 | * @author yrw 20 | */ 21 | @Component 22 | public class OfflineHandler { 23 | 24 | private OfflineService offlineService; 25 | 26 | public OfflineHandler(OfflineService offlineService) { 27 | this.offlineService = offlineService; 28 | } 29 | 30 | public Mono pollOfflineMsg(ServerRequest request) { 31 | 32 | String id = request.pathVariable("id"); 33 | 34 | return Mono.fromSupplier(() -> { 35 | try { 36 | return offlineService.pollOfflineMsg(id); 37 | } catch (JsonProcessingException e) { 38 | throw new ImException(e); 39 | } 40 | }).map(ResultWrapper::success).flatMap(res -> 41 | ServerResponse.ok().contentType(APPLICATION_JSON).body(fromObject(res))); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/handler/ValidHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.handler; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.google.common.collect.Iterables; 5 | import org.springframework.web.reactive.function.server.ServerRequest; 6 | import org.springframework.web.reactive.function.server.ServerResponse; 7 | import reactor.core.publisher.Mono; 8 | 9 | import javax.validation.ConstraintViolation; 10 | import javax.validation.Validator; 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | 14 | /** 15 | * Date: 2019-03-01 16 | * Time: 14:51 17 | * 18 | * @author yrw 19 | */ 20 | public class ValidHandler { 21 | 22 | private static Validator validator; 23 | 24 | public static Mono requireValidBody( 25 | Function, Mono> block, 26 | ServerRequest request, Class bodyClass) { 27 | 28 | return request 29 | .bodyToMono(bodyClass) 30 | .flatMap(body -> { 31 | Set> msg = validator.validate(body); 32 | if (msg.isEmpty()) { 33 | return block.apply(Mono.just(body)); 34 | } else { 35 | ConstraintViolation v = Iterables.get(msg, 0); 36 | throw new ImException(v.getPropertyPath() + " " + v.getMessage()); 37 | } 38 | } 39 | ); 40 | } 41 | 42 | public static void setValidator(Validator validator) { 43 | ValidHandler.validator = validator; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/OfflineMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.github.yuanrw.im.common.domain.po.Offline; 5 | 6 | /** 7 | * Date: 2019-05-05 8 | * Time: 09:46 9 | * 10 | * @author yrw 11 | */ 12 | public interface OfflineMapper extends BaseMapper { 13 | 14 | /** 15 | * read offline msg from db, cas 16 | * 17 | * @param msgId 18 | * @return 19 | */ 20 | int readMsg(Long msgId); 21 | } 22 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/RelationMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.github.yuanrw.im.common.domain.po.Relation; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Date: 2019-02-11 12 | * Time: 17:21 13 | * 14 | * @author yrw 15 | */ 16 | public interface RelationMapper extends BaseMapper { 17 | 18 | /** 19 | * list user's friends 20 | * 21 | * @param userId 22 | * @return 23 | */ 24 | List listFriends(@Param("userId") String userId); 25 | } 26 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.github.yuanrw.im.common.domain.po.User; 5 | 6 | /** 7 | * Date: 2019-02-09 8 | * Time: 19:11 9 | * 10 | * @author yrw 11 | */ 12 | public interface UserMapper extends BaseMapper { 13 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/router/RestRouter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.router; 2 | 3 | import com.github.yuanrw.im.rest.web.handler.OfflineHandler; 4 | import com.github.yuanrw.im.rest.web.handler.RelationHandler; 5 | import com.github.yuanrw.im.rest.web.handler.UserHandler; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.reactive.function.server.RouterFunction; 9 | import org.springframework.web.reactive.function.server.RouterFunctions; 10 | import org.springframework.web.reactive.function.server.ServerResponse; 11 | 12 | import static org.springframework.http.MediaType.APPLICATION_JSON; 13 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; 14 | 15 | /** 16 | * Date: 2019-02-09 17 | * Time: 12:56 18 | * 19 | * @author yrw 20 | */ 21 | @Configuration 22 | public class RestRouter { 23 | 24 | @Bean 25 | public RouterFunction userRoutes(UserHandler userHandler) { 26 | return RouterFunctions 27 | .route(POST("/user/login").and(contentType(APPLICATION_JSON)).and(accept(APPLICATION_JSON)), 28 | userHandler::login) 29 | .andRoute(GET("/user/logout").and(accept(APPLICATION_JSON)), 30 | userHandler::logout); 31 | } 32 | 33 | @Bean 34 | public RouterFunction relationRoutes(RelationHandler relationHandler) { 35 | return RouterFunctions 36 | .route(GET("/relation/{id}").and(accept(APPLICATION_JSON)), 37 | relationHandler::listFriends) 38 | .andRoute(GET("/relation").and(accept(APPLICATION_JSON)), 39 | relationHandler::getRelation) 40 | .andRoute(POST("/relation").and(contentType(APPLICATION_JSON)).and(accept(APPLICATION_JSON)), 41 | relationHandler::saveRelation) 42 | .andRoute(DELETE("/relation/{id}").and(accept(APPLICATION_JSON)), 43 | relationHandler::deleteRelation); 44 | } 45 | 46 | @Bean 47 | public RouterFunction offlineRoutes(OfflineHandler offlineHandler) { 48 | //only for connector 49 | return RouterFunctions 50 | .route(GET("/offline/poll/{id}").and(accept(APPLICATION_JSON)), 51 | offlineHandler::pollOfflineMsg); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/OfflineService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.github.yuanrw.im.common.domain.po.Offline; 6 | import com.github.yuanrw.im.protobuf.generate.Ack; 7 | import com.github.yuanrw.im.protobuf.generate.Chat; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Date: 2019-05-05 13 | * Time: 09:48 14 | * 15 | * @author yrw 16 | */ 17 | public interface OfflineService extends IService { 18 | 19 | /** 20 | * save offline chat msg 21 | * 22 | * @param msg 23 | * @return 24 | */ 25 | void saveChat(Chat.ChatMsg msg); 26 | 27 | /** 28 | * save offline ack msg 29 | * 30 | * @param msg 31 | * @return 32 | */ 33 | void saveAck(Ack.AckMsg msg); 34 | 35 | /** 36 | * get a user's all offline msgs 37 | * 38 | * @param userId 39 | * @return 40 | * @throws JsonProcessingException 41 | */ 42 | List pollOfflineMsg(String userId) throws JsonProcessingException; 43 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/RelationService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.github.yuanrw.im.common.domain.po.Relation; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Date: 2019-04-07 11 | * Time: 18:47 12 | * 13 | * @author yrw 14 | */ 15 | public interface RelationService extends IService { 16 | 17 | /** 18 | * return the friends list of the user 19 | * 20 | * @param id userId 21 | * @return 22 | */ 23 | List friends(String id); 24 | 25 | /** 26 | * add an relation between user1 and user2 27 | * insure that the same relation can only be saved once. 28 | * by default, use mysql union unique index. 29 | * if the db don't support a union unique index, then you need to check the exist relation 30 | * 31 | * @param userId1 id of user1l 32 | * @param userId2 id of user2 33 | * @return if success, return relation id, else return Mono.empty() 34 | */ 35 | Long saveRelation(String userId1, String userId2); 36 | } 37 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.github.yuanrw.im.common.domain.po.User; 5 | 6 | /** 7 | * Date: 2019-04-07 8 | * Time: 18:35 9 | * 10 | * @author yrw 11 | */ 12 | public interface UserService extends IService { 13 | 14 | /** 15 | * 验证用户密码,成功则返回用户,失败返回null 16 | * 17 | * @param username 用户名 18 | * @param pwd 密码 19 | * @return 20 | */ 21 | User verifyAndGet(String username, String pwd); 22 | } 23 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/OfflineServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.github.yuanrw.im.common.domain.po.DbModel; 6 | import com.github.yuanrw.im.common.domain.po.Offline; 7 | import com.github.yuanrw.im.common.exception.ImException; 8 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum; 9 | import com.github.yuanrw.im.protobuf.generate.Ack; 10 | import com.github.yuanrw.im.protobuf.generate.Chat; 11 | import com.github.yuanrw.im.rest.web.mapper.OfflineMapper; 12 | import com.github.yuanrw.im.rest.web.service.OfflineService; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.util.Comparator; 17 | import java.util.List; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * Date: 2019-05-05 22 | * Time: 09:49 23 | * 24 | * @author yrw 25 | */ 26 | @Service 27 | public class OfflineServiceImpl extends ServiceImpl implements OfflineService { 28 | 29 | @Override 30 | public void saveChat(Chat.ChatMsg msg) { 31 | Offline offline = new Offline(); 32 | offline.setMsgId(msg.getId()); 33 | offline.setMsgCode(MsgTypeEnum.CHAT.getCode()); 34 | offline.setToUserId(msg.getDestId()); 35 | offline.setContent(msg.toByteArray()); 36 | 37 | saveOffline(offline); 38 | } 39 | 40 | @Override 41 | public void saveAck(Ack.AckMsg msg) { 42 | Offline offline = new Offline(); 43 | offline.setMsgId(msg.getId()); 44 | offline.setMsgCode(MsgTypeEnum.getByClass(Ack.AckMsg.class).getCode()); 45 | offline.setToUserId(msg.getDestId()); 46 | offline.setContent(msg.toByteArray()); 47 | 48 | saveOffline(offline); 49 | } 50 | 51 | private void saveOffline(Offline offline) { 52 | if (!save(offline)) { 53 | throw new ImException("[offline] save chat msg failed"); 54 | } 55 | } 56 | 57 | @Override 58 | @Transactional(rollbackFor = Exception.class) 59 | public List pollOfflineMsg(String userId) { 60 | List unreadList = list(new LambdaQueryWrapper() 61 | .eq(Offline::getToUserId, userId)); 62 | 63 | return unreadList.stream().filter(offline -> 64 | baseMapper.readMsg(offline.getId()) > 0) 65 | .sorted(Comparator.comparing(DbModel::getId)) 66 | .collect(Collectors.toList()); 67 | } 68 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/RelationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import com.github.yuanrw.im.common.domain.po.Relation; 5 | import com.github.yuanrw.im.common.domain.po.RelationDetail; 6 | import com.github.yuanrw.im.common.exception.ImException; 7 | import com.github.yuanrw.im.rest.spi.UserSpi; 8 | import com.github.yuanrw.im.rest.spi.domain.UserBase; 9 | import com.github.yuanrw.im.rest.web.mapper.RelationMapper; 10 | import com.github.yuanrw.im.rest.web.service.RelationService; 11 | import com.github.yuanrw.im.rest.web.spi.SpiFactory; 12 | import org.apache.commons.lang3.RandomStringUtils; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * Date: 2019-04-07 19 | * Time: 18:48 20 | * 21 | * @author yrw 22 | */ 23 | @Service 24 | public class RelationServiceImpl extends ServiceImpl implements RelationService { 25 | 26 | private UserSpi userSpi; 27 | 28 | public RelationServiceImpl(SpiFactory spiFactory) { 29 | this.userSpi = spiFactory.getUserSpi(); 30 | } 31 | 32 | @Override 33 | public List friends(String id) { 34 | return baseMapper.listFriends(id); 35 | } 36 | 37 | @Override 38 | public Long saveRelation(String userId1, String userId2) { 39 | if (userId1.equals(userId2)) { 40 | throw new ImException("[rest] userId1 and userId2 can not be same"); 41 | } 42 | if (userSpi.getById(userId1 + "") == null || userSpi.getById(userId2 + "") == null) { 43 | throw new ImException("[rest] user not exist"); 44 | } 45 | String max = userId1.compareTo(userId2) >= 0 ? userId1 : userId2; 46 | String min = max.equals(userId1) ? userId2 : userId1; 47 | 48 | Relation relation = new Relation(); 49 | relation.setUserId1(min); 50 | relation.setUserId2(max); 51 | relation.setEncryptKey(RandomStringUtils.randomAlphanumeric(16) + "|" + RandomStringUtils.randomNumeric(16)); 52 | 53 | if (save(relation)) { 54 | return relation.getId(); 55 | } else { 56 | throw new ImException("[rest] save relation failed"); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.github.yuanrw.im.common.domain.po.User; 6 | import com.github.yuanrw.im.rest.web.mapper.UserMapper; 7 | import com.github.yuanrw.im.rest.web.service.UserService; 8 | import org.apache.commons.codec.digest.DigestUtils; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * Date: 2019-04-07 13 | * Time: 18:36 14 | * 15 | * @author yrw 16 | */ 17 | @Service 18 | public class UserServiceImpl extends ServiceImpl implements UserService { 19 | 20 | @Override 21 | public User verifyAndGet(String username, String pwd) { 22 | User user = getOne(new LambdaQueryWrapper().eq(User::getUsername, username)); 23 | return user != null ? verityPassword(pwd, user.getSalt(), user.getPwdHash()) ? user : null : null; 24 | } 25 | 26 | private boolean verityPassword(String pwd, String salt, String pwdHash) { 27 | String hashRes = DigestUtils.sha256Hex(pwd + salt); 28 | return hashRes.equals(pwdHash); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/SpiFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.spi; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.github.yuanrw.im.rest.spi.UserSpi; 5 | import com.github.yuanrw.im.rest.spi.domain.UserBase; 6 | import com.github.yuanrw.im.rest.web.spi.impl.DefaultUserSpiImpl; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.StringUtils; 12 | 13 | /** 14 | * Date: 2019-07-03 15 | * Time: 17:50 16 | * 17 | * @author yrw 18 | */ 19 | @Component 20 | public class SpiFactory implements ApplicationContextAware { 21 | 22 | private UserSpi userSpi; 23 | private ApplicationContext applicationContext; 24 | 25 | @Value("${spi.user.impl.class}") 26 | private String userSpiImplClassName; 27 | 28 | @Override 29 | public void setApplicationContext(ApplicationContext applicationContext) { 30 | this.applicationContext = applicationContext; 31 | } 32 | 33 | public UserSpi getUserSpi() { 34 | if (StringUtils.isEmpty(userSpiImplClassName)) { 35 | return applicationContext.getBean(DefaultUserSpiImpl.class); 36 | } 37 | try { 38 | if (userSpi == null) { 39 | Class userSpiImplClass = Class.forName(userSpiImplClassName); 40 | userSpi = (UserSpi) applicationContext.getBean(userSpiImplClass); 41 | } 42 | return userSpi; 43 | } catch (ClassNotFoundException e) { 44 | throw new ImException("can not find class: " + userSpiImplClassName); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/impl/DefaultUserSpiImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.spi.impl; 2 | 3 | import com.github.yuanrw.im.common.domain.po.User; 4 | import com.github.yuanrw.im.rest.spi.UserSpi; 5 | import com.github.yuanrw.im.rest.spi.domain.UserBase; 6 | import com.github.yuanrw.im.rest.web.service.UserService; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | * Date: 2019-07-03 11 | * Time: 17:49 12 | * 13 | * @author yrw 14 | */ 15 | @Service 16 | public class DefaultUserSpiImpl implements UserSpi { 17 | 18 | private UserService userService; 19 | 20 | public DefaultUserSpiImpl(UserService userService) { 21 | this.userService = userService; 22 | } 23 | 24 | @Override 25 | public UserBase getUser(String username, String pwd) { 26 | User user = userService.verifyAndGet(username, pwd); 27 | if (user == null) { 28 | return null; 29 | } 30 | 31 | UserBase userBase = new UserBase(); 32 | userBase.setId(user.getId() + ""); 33 | userBase.setUsername(user.getUsername()); 34 | return userBase; 35 | } 36 | 37 | @Override 38 | public UserBase getById(String id) { 39 | User user = userService.getById(Long.parseLong(id)); 40 | if (user == null) { 41 | return null; 42 | } 43 | 44 | UserBase userBase = new UserBase(); 45 | userBase.setId(userBase.getId()); 46 | userBase.setUsername(userBase.getUsername()); 47 | return userBase; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/impl/LdapUserSpiImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.spi.impl; 2 | 3 | import com.github.yuanrw.im.rest.spi.UserSpi; 4 | import com.github.yuanrw.im.rest.spi.domain.LdapUser; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.ldap.core.ContextMapper; 8 | import org.springframework.ldap.core.DirContextAdapter; 9 | import org.springframework.ldap.core.LdapTemplate; 10 | import org.springframework.ldap.filter.AndFilter; 11 | import org.springframework.ldap.filter.EqualsFilter; 12 | import org.springframework.ldap.query.ContainerCriteria; 13 | import org.springframework.ldap.query.SearchScope; 14 | import org.springframework.stereotype.Service; 15 | 16 | import static org.springframework.ldap.query.LdapQueryBuilder.query; 17 | 18 | /** 19 | * Date: 2019-07-06 20 | * Time: 22:01 21 | * 22 | * @author yrw 23 | */ 24 | @Service 25 | public class LdapUserSpiImpl implements UserSpi { 26 | 27 | @Value("${ldap.searchBase}") 28 | private String searchBase; 29 | 30 | @Value("${ldap.mapping.objectClass}") 31 | private String objectClassAttrName; 32 | 33 | @Value("${ldap.mapping.loginId}") 34 | private String loginIdAttrName; 35 | 36 | @Value("${ldap.mapping.userDisplayName}") 37 | private String userDisplayNameAttrName; 38 | 39 | @Value("${ldap.mapping.email}") 40 | private String emailAttrName; 41 | 42 | @Autowired 43 | private LdapTemplate ldapTemplate; 44 | 45 | @Override 46 | public LdapUser getUser(String username, String pwd) { 47 | AndFilter filter = new AndFilter() 48 | .and(new EqualsFilter(emailAttrName, username)); 49 | boolean authenticate = ldapTemplate.authenticate(searchBase, filter.encode(), pwd); 50 | return authenticate ? ldapTemplate.searchForObject(ldapQueryCriteria() 51 | .and(emailAttrName).is(username), ldapUserInfoMapper) : null; 52 | } 53 | 54 | @Override 55 | public LdapUser getById(String id) { 56 | return ldapTemplate.searchForObject(ldapQueryCriteria() 57 | .and(loginIdAttrName).is(id), ldapUserInfoMapper); 58 | } 59 | 60 | private ContextMapper ldapUserInfoMapper = (ctx) -> { 61 | DirContextAdapter contextAdapter = (DirContextAdapter) ctx; 62 | LdapUser ldapUser = new LdapUser(); 63 | ldapUser.setId(contextAdapter.getStringAttribute(loginIdAttrName)); 64 | ldapUser.setEmail(contextAdapter.getStringAttribute(emailAttrName)); 65 | ldapUser.setUsername(contextAdapter.getStringAttribute(userDisplayNameAttrName)); 66 | return ldapUser; 67 | }; 68 | 69 | private ContainerCriteria ldapQueryCriteria() { 70 | return query().searchScope(SearchScope.SUBTREE) 71 | .where("objectClass").is(objectClassAttrName); 72 | } 73 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/task/CleanOfflineMsgTask.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.task; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 4 | import com.github.yuanrw.im.common.domain.po.Offline; 5 | import com.github.yuanrw.im.rest.web.service.OfflineService; 6 | import com.google.common.collect.Lists; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * Date: 2019-09-02 17 | * Time: 18:24 18 | * 19 | * @author yrw 20 | */ 21 | @Component 22 | public class CleanOfflineMsgTask { 23 | private static Logger logger = LoggerFactory.getLogger(CleanOfflineMsgTask.class); 24 | 25 | private OfflineService offlineService; 26 | 27 | public CleanOfflineMsgTask(OfflineService offlineService) { 28 | this.offlineService = offlineService; 29 | } 30 | 31 | @Scheduled(cron = "0 0/5 * * * *") 32 | public void cleanReadMsg() { 33 | List readIds = offlineService.list(new LambdaQueryWrapper() 34 | .select(Offline::getId) 35 | .eq(Offline::getHasRead, true)).stream() 36 | .map(Offline::getId).collect(Collectors.toList()); 37 | 38 | logger.info("[clean task] clean read offline msg, size: {}", readIds.size()); 39 | 40 | Lists.partition(readIds, 1000).forEach(offlineService::removeByIds); 41 | } 42 | } -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/task/OfflineListen.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.task; 2 | 3 | import com.github.yuanrw.im.common.domain.constant.ImConstant; 4 | import com.github.yuanrw.im.common.parse.ParseService; 5 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum; 6 | import com.github.yuanrw.im.protobuf.generate.Ack; 7 | import com.github.yuanrw.im.protobuf.generate.Chat; 8 | import com.github.yuanrw.im.rest.web.service.OfflineService; 9 | import com.rabbitmq.client.Channel; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.amqp.core.Message; 13 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 14 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 15 | import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; 16 | import org.springframework.stereotype.Component; 17 | 18 | import javax.annotation.PostConstruct; 19 | 20 | /** 21 | * Date: 2019-05-15 22 | * Time: 22:58 23 | * 24 | * @author yrw 25 | */ 26 | @Component 27 | public class OfflineListen implements ChannelAwareMessageListener { 28 | private Logger logger = LoggerFactory.getLogger(OfflineListen.class); 29 | 30 | private ParseService parseService; 31 | private OfflineService offlineService; 32 | 33 | public OfflineListen(OfflineService offlineService) { 34 | this.parseService = new ParseService(); 35 | this.offlineService = offlineService; 36 | } 37 | 38 | @PostConstruct 39 | public void init() { 40 | logger.info("[OfflineConsumer] Start listening Offline queue......"); 41 | } 42 | 43 | @Override 44 | @RabbitHandler 45 | @RabbitListener(queues = ImConstant.MQ_OFFLINE_QUEUE, containerFactory = "listenerFactory") 46 | public void onMessage(Message message, Channel channel) throws Exception { 47 | logger.info("[OfflineConsumer] getUserSpi msg: {}", message.toString()); 48 | try { 49 | int code = message.getBody()[0]; 50 | 51 | byte[] msgBody = new byte[message.getBody().length - 1]; 52 | System.arraycopy(message.getBody(), 1, msgBody, 0, message.getBody().length - 1); 53 | 54 | com.google.protobuf.Message msg = parseService.getMsgByCode(code, msgBody); 55 | if (code == MsgTypeEnum.CHAT.getCode()) { 56 | offlineService.saveChat((Chat.ChatMsg) msg); 57 | } else { 58 | offlineService.saveAck((Ack.AckMsg) msg); 59 | } 60 | 61 | } catch (Exception e) { 62 | logger.error("[OfflineConsumer] has error", e); 63 | } finally { 64 | channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/vo/RelationReq.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.vo; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | /** 6 | * Date: 2019-06-23 7 | * Time: 21:04 8 | * 9 | * @author yrw 10 | */ 11 | public class RelationReq { 12 | 13 | @NotEmpty 14 | private String userId1; 15 | 16 | @NotEmpty 17 | private String userId2; 18 | 19 | public String getUserId1() { 20 | return userId1; 21 | } 22 | 23 | public void setUserId1(String userId1) { 24 | this.userId1 = userId1; 25 | } 26 | 27 | public String getUserId2() { 28 | return userId2; 29 | } 30 | 31 | public void setUserId2(String userId2) { 32 | this.userId2 = userId2; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/vo/UserReq.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.vo; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | /** 6 | * Date: 2019-04-21 7 | * Time: 14:43 8 | * 9 | * @author yrw 10 | */ 11 | public class UserReq { 12 | 13 | @NotEmpty 14 | // @Length(min = 6, max = 30) 15 | private String username; 16 | 17 | @NotEmpty 18 | private String pwd; 19 | 20 | public String getUsername() { 21 | return username; 22 | } 23 | 24 | public void setUsername(String username) { 25 | this.username = username; 26 | } 27 | 28 | public String getPwd() { 29 | return pwd; 30 | } 31 | 32 | public void setPwd(String pwd) { 33 | this.pwd = pwd; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | 3 | logging.level.root=info 4 | log.path=/tmp/IM_logs 5 | 6 | spring.datasource.url=jdbc:mysql://im-mysql:3306/im?useUnicode=true&characterEncoding=utf-8&useSSL=false 7 | spring.datasource.username=root 8 | spring.datasource.password=123456 9 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 10 | 11 | spring.redis.host=im-redis 12 | spring.redis.port=6379 13 | spring.redis.password= 14 | 15 | #rabbitmq 16 | spring.rabbitmq.host=im-rabbit 17 | spring.rabbitmq.port=5672 18 | spring.rabbitmq.username=rabbitmq 19 | spring.rabbitmq.password=rabbitmq 20 | 21 | spi.user.impl.class= 22 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl 23 | 24 | spring.ldap.base=dc=example,dc=org 25 | # admin 26 | spring.ldap.username=cn=admin,dc=example,dc=org 27 | spring.ldap.password=admin 28 | spring.ldap.urls=ldap://127.0.0.1:389 29 | # user filter,use the filter to search user when login in 30 | spring.ldap.searchFilter= 31 | # search base eg. ou=dev 32 | ldap.searchBase= 33 | # user objectClass 34 | ldap.mapping.objectClass=inetOrgPerson 35 | ldap.mapping.loginId=uid 36 | ldap.mapping.userDisplayName=gecos 37 | ldap.mapping.email=mail -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | 3 | logging.level.root=info 4 | log.path=/tmp/IM_logs 5 | 6 | spring.datasource.url=jdbc:mysql://127.0.0.1:3306/im?useUnicode=true&characterEncoding=utf-8&useSSL=false 7 | spring.datasource.username=root 8 | spring.datasource.password=123456 9 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 10 | 11 | spring.redis.host=127.0.0.1 12 | spring.redis.port=6379 13 | spring.redis.password= 14 | 15 | #rabbitmq 16 | spring.rabbitmq.host=127.0.0.1 17 | spring.rabbitmq.port=5672 18 | spring.rabbitmq.username=rabbitmq 19 | spring.rabbitmq.password=rabbitmq 20 | 21 | spi.user.impl.class= 22 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl 23 | 24 | spring.ldap.base=dc=example,dc=org 25 | # admin 26 | spring.ldap.username=cn=admin,dc=example,dc=org 27 | spring.ldap.password=admin 28 | spring.ldap.urls=ldap://127.0.0.1:389 29 | # user filter,use the filter to search user when login in 30 | spring.ldap.searchFilter= 31 | # search base eg. ou=dev 32 | ldap.searchBase= 33 | # user objectClass 34 | ldap.mapping.objectClass=inetOrgPerson 35 | ldap.mapping.loginId=uid 36 | ldap.mapping.userDisplayName=gecos 37 | ldap.mapping.email=mail -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rest 5 | 6 | 7 | 8 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | ${log.path}/rest.log 14 | 15 | rest.%d{yyyy-MM-dd}.log 16 | 17 | 18 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/mapper/OfflineMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | update im_offline set has_read = true 8 | where id = #{id} and has_read = false 9 | 10 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/mapper/RelationMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /rest/rest-web/src/main/resources/rest.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE im; 2 | DROP TABLE IF EXISTS `im_user`; 3 | CREATE TABLE `im_user` ( 4 | `id` bigint(20) NOT NULL, 5 | `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '用户名', 6 | `pwd_hash` char(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '密码加密后的hash值', 7 | `salt` char(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '盐', 8 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 10 | `deleted` tinyint(1) NOT NULL DEFAULT '0', 11 | PRIMARY KEY (`id`), 12 | UNIQUE KEY `username` (`username`) 13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 14 | 15 | DROP TABLE IF EXISTS `im_relation`; 16 | CREATE TABLE `im_relation` ( 17 | `id` bigint(20) NOT NULL, 18 | `user_id1` varchar(100) NOT NULL COMMENT '用户1的id', 19 | `user_id2` varchar(100) NOT NULL COMMENT '用户2的id', 20 | `encrypt_key` char(33) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '密钥', 21 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 23 | `deleted` tinyint(1) NOT NULL DEFAULT '0', 24 | PRIMARY KEY (`id`), 25 | UNIQUE KEY `USERID1_USERID2` (`user_id1`,`user_id2`) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 27 | 28 | DROP TABLE IF EXISTS `im_offline`; 29 | CREATE TABLE `im_offline` ( 30 | `id` bigint(20) NOT NULL, 31 | `msg_id` bigint(20) NOT NULL, 32 | `msg_code` int(2) NOT NULL, 33 | `to_user_id` varchar(100) NOT NULL, 34 | `content` varbinary(5000) NOT NULL DEFAULT '', 35 | `has_read` tinyint(1) NOT NULL DEFAULT '0', 36 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 37 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 38 | `deleted` tinyint(1) NOT NULL DEFAULT '0' 39 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -------------------------------------------------------------------------------- /rest/rest-web/src/test/java/com/github/yuanrw/im/rest/web/test/OfflineTest.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.rest.web.test; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.annotation.DirtiesContext; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | import org.springframework.test.web.reactive.server.WebTestClient; 11 | 12 | /** 13 | * Date: 2019-07-05 14 | * Time: 18:07 15 | * 16 | * @author yrw 17 | */ 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest 20 | @AutoConfigureWebTestClient 21 | @DirtiesContext 22 | public class OfflineTest { 23 | 24 | @Autowired 25 | private WebTestClient webClient; 26 | 27 | @Test 28 | public void pollAllMsg() { 29 | webClient.get().uri("/offline/poll/1142773797275836418") 30 | .exchange() 31 | .expectStatus().isOk() 32 | .expectBody() 33 | .jsonPath("$.status").isEqualTo(200) 34 | .jsonPath("$.msg").isEqualTo("SUCCESS") 35 | .jsonPath("$.data.length()").isEqualTo(2) 36 | .jsonPath("$.data[0].id").isNotEmpty() 37 | .jsonPath("$.data[0].toUserId").isNotEmpty() 38 | .jsonPath("$.data[0].content").isNotEmpty() 39 | .jsonPath("$.data[1].id").isNotEmpty() 40 | .jsonPath("$.data[1].toUserId").isNotEmpty() 41 | .jsonPath("$.data[1].content").isNotEmpty(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rest/rest-web/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | logging.level.root=info 3 | spring.datasource.url=jdbc:h2:mem:im;INIT=runscript from 'src/test/resources/rest-test.sql'; 4 | spring.datasource.driver-class-name=org.h2.Driver 5 | spring.redis.host=127.0.0.1 6 | spring.redis.port=6379 7 | spring.redis.password= 8 | #rabbitmq 9 | spring.rabbitmq.host=127.0.0.1 10 | spring.rabbitmq.port=5672 11 | spring.rabbitmq.username=rabbitmq 12 | spring.rabbitmq.password=rabbitmq 13 | spi.user.impl.class= 14 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl 15 | spring.ldap.base=dc=example,dc=org 16 | # admin 17 | spring.ldap.username=cn=admin,dc=example,dc=org 18 | spring.ldap.password=admin 19 | spring.ldap.urls=ldap://127.0.0.1:389 20 | # user filter,use the filter to search user when login in 21 | spring.ldap.searchFilter= 22 | # search base eg. ou=dev 23 | ldap.searchBase= 24 | # user objectClass 25 | ldap.mapping.objectClass=inetOrgPerson 26 | ldap.mapping.loginId=uid 27 | ldap.mapping.userDisplayName=gecos 28 | ldap.mapping.email=mail -------------------------------------------------------------------------------- /rest/rest-web/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /transfer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for transfer 2 | # docker build -t yuanrw/transfer:$VERSION . 3 | # docker run -p 9082:9082 -d -v /tmp/IM_logs:/tmp/IM_logs --name transfer transfer 4 | 5 | FROM adoptopenjdk/openjdk11:alpine-jre 6 | MAINTAINER yuanrw <295415537@qq.com> 7 | 8 | ENV SERVICE_NAME transfer 9 | ENV VERSION 1.0.0 10 | 11 | EXPOSE 9082 12 | 13 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \ 14 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \ 15 | && apk update upgrade \ 16 | && apk add --no-cache procps unzip curl bash tzdata \ 17 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 18 | && echo "Asia/Shanghai" > /etc/timezone 19 | 20 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip 21 | 22 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \ 23 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \ 24 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \ 25 | && echo "tail -f /dev/null" >> start.sh 26 | 27 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} 28 | 29 | COPY src/main/resources/transfer-docker.properties . 30 | COPY src/main/bin/start-docker.sh . 31 | COPY src/main/bin/wait-for-it.sh . 32 | 33 | CMD /bin/bash wait-for-it.sh -t 0 rest-web:8082 --strict -- \ 34 | /bin/bash start-docker.sh -------------------------------------------------------------------------------- /transfer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | transfer 13 | 14 | 15 | com.github.yuanrw.im.transfer.start.TransferStarter 16 | 17 | 18 | 19 | 20 | io.netty 21 | netty-all 22 | 23 | 24 | commons-codec 25 | commons-codec 26 | 27 | 28 | org.apache.commons 29 | commons-lang3 30 | 31 | 32 | com.github.yuanrw.im 33 | common 34 | 35 | 36 | com.fasterxml.jackson.core 37 | jackson-databind 38 | 39 | 40 | com.rabbitmq 41 | amqp-client 42 | 43 | 44 | com.github.yuanrw.im 45 | user-status 46 | 1.0.0 47 | 48 | 49 | 50 | 51 | 52 | 53 | maven-jar-plugin 54 | 55 | 56 | maven-assembly-plugin 57 | 58 | 59 | org.codehaus.gmavenplus 60 | gmavenplus-plugin 61 | 62 | 63 | org.jacoco 64 | jacoco-maven-plugin 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /transfer/src/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | 4 | zip 5 | 6 | 7 | 8 | true 9 | lib 10 | 11 | 12 | 13 | 14 | src/main/resources 15 | / 16 | 17 | logback*.xml 18 | *-docker.* 19 | 20 | unix 21 | 22 | 23 | target 24 | / 25 | 26 | ${project.artifactId}-*.jar 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /transfer/src/main/bin/start-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SERVICE_NAME="transfer" 3 | VERSION="1.0.0" 4 | 5 | LOG_DIR=/tmp/IM_logs 6 | mkdir -p $LOG_DIR 7 | 8 | # Find Java 9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then 10 | java="$JAVA_HOME/bin/java" 11 | elif type -p java > /dev/null 2>&1; then 12 | java=$(type -p java) 13 | elif [[ -x "/usr/bin/java" ]]; then 14 | java="/usr/bin/java" 15 | else 16 | echo "Unable to find Java" 17 | exit 1 18 | fi 19 | 20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" 21 | 22 | echo "JAVA_HOME: $JAVA_HOME" 23 | $java $JAVA_OPTS -Dconfig=$SERVICE_NAME-docker.properties -jar $SERVICE_NAME-$VERSION.jar 24 | echo "SERVICE_NAME started...." -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/config/TransferConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.config; 2 | 3 | /** 4 | * Date: 2019-06-09 5 | * Time: 15:18 6 | * 7 | * @author yrw 8 | */ 9 | public class TransferConfig { 10 | 11 | private Integer port; 12 | 13 | private String redisHost; 14 | 15 | private Integer redisPort; 16 | 17 | private String redisPassword; 18 | 19 | private String rabbitmqHost; 20 | 21 | private Integer rabbitmqPort; 22 | 23 | private String rabbitmqUsername; 24 | 25 | private String rabbitmqPassword; 26 | 27 | public Integer getPort() { 28 | return port; 29 | } 30 | 31 | public void setPort(Integer port) { 32 | this.port = port; 33 | } 34 | 35 | public String getRedisHost() { 36 | return redisHost; 37 | } 38 | 39 | public void setRedisHost(String redisHost) { 40 | this.redisHost = redisHost; 41 | } 42 | 43 | public Integer getRedisPort() { 44 | return redisPort; 45 | } 46 | 47 | public void setRedisPort(Integer redisPort) { 48 | this.redisPort = redisPort; 49 | } 50 | 51 | public String getRedisPassword() { 52 | return redisPassword; 53 | } 54 | 55 | public void setRedisPassword(String redisPassword) { 56 | this.redisPassword = redisPassword; 57 | } 58 | 59 | public String getRabbitmqHost() { 60 | return rabbitmqHost; 61 | } 62 | 63 | public void setRabbitmqHost(String rabbitmqHost) { 64 | this.rabbitmqHost = rabbitmqHost; 65 | } 66 | 67 | public Integer getRabbitmqPort() { 68 | return rabbitmqPort; 69 | } 70 | 71 | public void setRabbitmqPort(Integer rabbitmqPort) { 72 | this.rabbitmqPort = rabbitmqPort; 73 | } 74 | 75 | public String getRabbitmqUsername() { 76 | return rabbitmqUsername; 77 | } 78 | 79 | public void setRabbitmqUsername(String rabbitmqUsername) { 80 | this.rabbitmqUsername = rabbitmqUsername; 81 | } 82 | 83 | public String getRabbitmqPassword() { 84 | return rabbitmqPassword; 85 | } 86 | 87 | public void setRabbitmqPassword(String rabbitmqPassword) { 88 | this.rabbitmqPassword = rabbitmqPassword; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/config/TransferModule.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.config; 2 | 3 | import com.github.yuanrw.im.user.status.factory.UserStatusServiceFactory; 4 | import com.github.yuanrw.im.user.status.service.UserStatusService; 5 | import com.github.yuanrw.im.user.status.service.impl.RedisUserStatusServiceImpl; 6 | import com.google.inject.AbstractModule; 7 | import com.google.inject.assistedinject.FactoryModuleBuilder; 8 | 9 | /** 10 | * Date: 2019-06-09 11 | * Time: 15:52 12 | * 13 | * @author yrw 14 | */ 15 | public class TransferModule extends AbstractModule { 16 | 17 | @Override 18 | protected void configure() { 19 | install(new FactoryModuleBuilder() 20 | .implement(UserStatusService.class, RedisUserStatusServiceImpl.class) 21 | .build(UserStatusServiceFactory.class)); 22 | } 23 | } -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/domain/ConnectorConnContext.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.domain; 2 | 3 | import com.github.yuanrw.im.common.domain.conn.ConnectorConn; 4 | import com.github.yuanrw.im.common.domain.conn.MemoryConnContext; 5 | import com.github.yuanrw.im.user.status.factory.UserStatusServiceFactory; 6 | import com.github.yuanrw.im.user.status.service.UserStatusService; 7 | import com.google.inject.Inject; 8 | import com.google.inject.Singleton; 9 | 10 | import java.util.Properties; 11 | 12 | import static com.github.yuanrw.im.transfer.start.TransferStarter.TRANSFER_CONFIG; 13 | 14 | /** 15 | * 存储transfer和connector的连接 16 | * 以及用户和connector的关系 17 | * Date: 2019-04-12 18 | * Time: 18:22 19 | * 20 | * @author yrw 21 | */ 22 | @Singleton 23 | public class ConnectorConnContext extends MemoryConnContext { 24 | 25 | private UserStatusService userStatusService; 26 | 27 | @Inject 28 | public ConnectorConnContext(UserStatusServiceFactory userStatusServiceFactory) { 29 | Properties properties = new Properties(); 30 | properties.put("host", TRANSFER_CONFIG.getRedisHost()); 31 | properties.put("port", TRANSFER_CONFIG.getRedisPort()); 32 | properties.put("password", TRANSFER_CONFIG.getRedisPassword()); 33 | this.userStatusService = userStatusServiceFactory.createService(properties); 34 | } 35 | 36 | public ConnectorConn getConnByUserId(String userId) { 37 | String connectorId = userStatusService.getConnectorId(userId); 38 | if (connectorId != null) { 39 | ConnectorConn conn = getConn(connectorId); 40 | if (conn != null) { 41 | return conn; 42 | } else { 43 | //connectorId已过时,而用户还没再次上线 44 | userStatusService.offline(userId); 45 | } 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/handler/TransferConnectorHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.handler; 2 | 3 | import com.github.yuanrw.im.common.parse.AbstractMsgParser; 4 | import com.github.yuanrw.im.common.parse.InternalParser; 5 | import com.github.yuanrw.im.protobuf.generate.Ack; 6 | import com.github.yuanrw.im.protobuf.generate.Chat; 7 | import com.github.yuanrw.im.protobuf.generate.Internal; 8 | import com.github.yuanrw.im.transfer.domain.ConnectorConnContext; 9 | import com.github.yuanrw.im.transfer.service.TransferService; 10 | import com.google.inject.Inject; 11 | import com.google.protobuf.Message; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.channel.SimpleChannelInboundHandler; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import static com.github.yuanrw.im.common.parse.AbstractMsgParser.checkDest; 18 | import static com.github.yuanrw.im.common.parse.AbstractMsgParser.checkFrom; 19 | 20 | /** 21 | * Date: 2019-04-12 22 | * Time: 18:17 23 | * 24 | * @author yrw 25 | */ 26 | public class TransferConnectorHandler extends SimpleChannelInboundHandler { 27 | private Logger logger = LoggerFactory.getLogger(TransferConnectorHandler.class); 28 | 29 | private TransferService transferService; 30 | private ConnectorConnContext connectorConnContext; 31 | private FromConnectorParser fromConnectorParser; 32 | 33 | @Inject 34 | public TransferConnectorHandler(TransferService transferService, ConnectorConnContext connectorConnContext) { 35 | this.fromConnectorParser = new FromConnectorParser(); 36 | this.transferService = transferService; 37 | this.connectorConnContext = connectorConnContext; 38 | } 39 | 40 | @Override 41 | protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception { 42 | logger.debug("[transfer] get msg: {}", msg.toString()); 43 | 44 | checkFrom(msg, Internal.InternalMsg.Module.CONNECTOR); 45 | checkDest(msg, Internal.InternalMsg.Module.TRANSFER); 46 | 47 | fromConnectorParser.parse(msg, ctx); 48 | } 49 | 50 | @Override 51 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 52 | connectorConnContext.removeConn(ctx); 53 | } 54 | 55 | class FromConnectorParser extends AbstractMsgParser { 56 | 57 | @Override 58 | public void registerParsers() { 59 | InternalParser parser = new InternalParser(3); 60 | parser.register(Internal.InternalMsg.MsgType.GREET, (m, ctx) -> transferService.doGreet(m, ctx)); 61 | 62 | register(Chat.ChatMsg.class, (m, ctx) -> transferService.doChat(m)); 63 | register(Ack.AckMsg.class, (m, ctx) -> transferService.doSendAck(m)); 64 | register(Internal.InternalMsg.class, parser.generateFun()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/service/TransferService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.service; 2 | 3 | import com.github.yuanrw.im.common.domain.conn.Conn; 4 | import com.github.yuanrw.im.common.domain.conn.ConnectorConn; 5 | import com.github.yuanrw.im.common.domain.constant.ImConstant; 6 | import com.github.yuanrw.im.common.domain.constant.MsgVersion; 7 | import com.github.yuanrw.im.common.util.IdWorker; 8 | import com.github.yuanrw.im.protobuf.generate.Ack; 9 | import com.github.yuanrw.im.protobuf.generate.Chat; 10 | import com.github.yuanrw.im.protobuf.generate.Internal; 11 | import com.github.yuanrw.im.transfer.domain.ConnectorConnContext; 12 | import com.github.yuanrw.im.transfer.start.TransferMqProducer; 13 | import com.github.yuanrw.im.transfer.start.TransferStarter; 14 | import com.google.inject.Inject; 15 | import com.google.protobuf.Message; 16 | import com.rabbitmq.client.MessageProperties; 17 | import io.netty.channel.ChannelHandlerContext; 18 | 19 | import java.io.IOException; 20 | 21 | /** 22 | * Date: 2019-05-04 23 | * Time: 13:47 24 | * 25 | * @author yrw 26 | */ 27 | public class TransferService { 28 | 29 | private ConnectorConnContext connContext; 30 | private TransferMqProducer producer; 31 | 32 | @Inject 33 | public TransferService(ConnectorConnContext connContext) { 34 | this.connContext = connContext; 35 | this.producer = TransferStarter.producer; 36 | } 37 | 38 | public void doChat(Chat.ChatMsg msg) throws IOException { 39 | ConnectorConn conn = connContext.getConnByUserId(msg.getDestId()); 40 | 41 | if (conn != null) { 42 | conn.getCtx().writeAndFlush(msg); 43 | } else { 44 | doOffline(msg); 45 | } 46 | } 47 | 48 | public void doSendAck(Ack.AckMsg msg) throws IOException { 49 | ConnectorConn conn = connContext.getConnByUserId(msg.getDestId()); 50 | 51 | if (conn != null) { 52 | conn.getCtx().writeAndFlush(msg); 53 | } else { 54 | doOffline(msg); 55 | } 56 | } 57 | 58 | public void doGreet(Internal.InternalMsg msg, ChannelHandlerContext ctx) { 59 | ctx.channel().attr(Conn.NET_ID).set(msg.getMsgBody()); 60 | ConnectorConn conn = new ConnectorConn(ctx); 61 | connContext.addConn(conn); 62 | 63 | ctx.writeAndFlush(getInternalAck(msg.getId())); 64 | } 65 | 66 | private Internal.InternalMsg getInternalAck(Long msgId) { 67 | return Internal.InternalMsg.newBuilder() 68 | .setVersion(MsgVersion.V1.getVersion()) 69 | .setId(IdWorker.genId()) 70 | .setFrom(Internal.InternalMsg.Module.TRANSFER) 71 | .setDest(Internal.InternalMsg.Module.CONNECTOR) 72 | .setCreateTime(System.currentTimeMillis()) 73 | .setMsgType(Internal.InternalMsg.MsgType.ACK) 74 | .setMsgBody(msgId + "") 75 | .build(); 76 | } 77 | 78 | private void doOffline(Message msg) throws IOException { 79 | producer.basicPublish(ImConstant.MQ_EXCHANGE, ImConstant.MQ_ROUTING_KEY, 80 | MessageProperties.PERSISTENT_TEXT_PLAIN, msg); 81 | } 82 | } -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferMqProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.start; 2 | 3 | import com.github.yuanrw.im.common.domain.constant.ImConstant; 4 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum; 5 | import com.google.inject.Singleton; 6 | import com.google.protobuf.Message; 7 | import com.rabbitmq.client.*; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeoutException; 13 | 14 | /** 15 | * Date: 2019-05-06 16 | * Time: 14:27 17 | * 18 | * @author yrw 19 | */ 20 | @Singleton 21 | public class TransferMqProducer { 22 | private static Logger logger = LoggerFactory.getLogger(TransferMqProducer.class); 23 | 24 | private Channel channel; 25 | 26 | public TransferMqProducer(String host, int port, String username, String password) 27 | throws IOException, TimeoutException { 28 | ConnectionFactory factory = new ConnectionFactory(); 29 | factory.setHost(host); 30 | factory.setPort(port); 31 | factory.setUsername(username); 32 | factory.setPassword(password); 33 | 34 | Connection connection = factory.newConnection(); 35 | Channel channel = connection.createChannel(); 36 | 37 | channel.exchangeDeclare(ImConstant.MQ_EXCHANGE, BuiltinExchangeType.DIRECT, true, false, null); 38 | channel.queueDeclare(ImConstant.MQ_OFFLINE_QUEUE, true, false, false, null); 39 | channel.queueBind(ImConstant.MQ_OFFLINE_QUEUE, ImConstant.MQ_EXCHANGE, ImConstant.MQ_ROUTING_KEY); 40 | 41 | this.channel = channel; 42 | logger.info("[transfer] producer start success"); 43 | } 44 | 45 | public void basicPublish(String exchange, String routingKey, AMQP.BasicProperties properties, Message message) throws IOException { 46 | int code = MsgTypeEnum.getByClass(message.getClass()).getCode(); 47 | 48 | byte[] srcB = message.toByteArray(); 49 | byte[] destB = new byte[srcB.length + 1]; 50 | destB[0] = (byte) code; 51 | 52 | System.arraycopy(message.toByteArray(), 0, destB, 1, message.toByteArray().length); 53 | 54 | channel.basicPublish(exchange, routingKey, properties, destB); 55 | } 56 | 57 | public Channel getChannel() { 58 | return channel; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferServer.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.start; 2 | 3 | import com.github.yuanrw.im.common.code.MsgDecoder; 4 | import com.github.yuanrw.im.common.code.MsgEncoder; 5 | import com.github.yuanrw.im.common.exception.ImException; 6 | import com.github.yuanrw.im.transfer.handler.TransferConnectorHandler; 7 | import io.netty.bootstrap.ServerBootstrap; 8 | import io.netty.channel.*; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.SocketChannel; 11 | import io.netty.channel.socket.nio.NioServerSocketChannel; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.net.InetSocketAddress; 16 | import java.util.concurrent.ExecutionException; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.concurrent.TimeoutException; 19 | 20 | /** 21 | * Date: 2019-04-12 22 | * Time: 18:16 23 | * 24 | * @author yrw 25 | */ 26 | public class TransferServer { 27 | private static Logger logger = LoggerFactory.getLogger(TransferServer.class); 28 | 29 | static void startTransferServer(int port) { 30 | EventLoopGroup bossGroup = new NioEventLoopGroup(); 31 | EventLoopGroup workGroup = new NioEventLoopGroup(); 32 | 33 | ServerBootstrap bootstrap = new ServerBootstrap() 34 | .group(bossGroup, workGroup) 35 | .channel(NioServerSocketChannel.class) 36 | .childHandler(new ChannelInitializer() { 37 | @Override 38 | protected void initChannel(SocketChannel channel) throws Exception { 39 | ChannelPipeline pipeline = channel.pipeline(); 40 | pipeline.addLast("MsgDecoder", TransferStarter.injector.getInstance(MsgDecoder.class)); 41 | pipeline.addLast("MsgEncoder", TransferStarter.injector.getInstance(MsgEncoder.class)); 42 | pipeline.addLast("TransferClientHandler", TransferStarter.injector.getInstance(TransferConnectorHandler.class)); 43 | } 44 | }); 45 | 46 | ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).addListener((ChannelFutureListener) future -> { 47 | if (future.isSuccess()) { 48 | logger.info("[transfer] start successful at port {}, waiting for connectors to connect...", port); 49 | } else { 50 | throw new ImException("[transfer] start failed"); 51 | } 52 | }); 53 | 54 | try { 55 | f.get(10, TimeUnit.SECONDS); 56 | } catch (InterruptedException | ExecutionException | TimeoutException e) { 57 | throw new ImException("[transfer] start failed"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferStarter.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.transfer.start; 2 | 3 | import com.github.yuanrw.im.common.exception.ImException; 4 | import com.github.yuanrw.im.transfer.config.TransferConfig; 5 | import com.github.yuanrw.im.transfer.config.TransferModule; 6 | import com.google.inject.Guice; 7 | import com.google.inject.Injector; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.Properties; 14 | 15 | /** 16 | * Date: 2019-05-07 17 | * Time: 20:39 18 | * 19 | * @author yrw 20 | */ 21 | public class TransferStarter { 22 | public static TransferConfig TRANSFER_CONFIG = new TransferConfig(); 23 | public static TransferMqProducer producer; 24 | static Injector injector = Guice.createInjector(new TransferModule()); 25 | 26 | public static void main(String[] args) { 27 | try { 28 | //parse start parameter 29 | TransferStarter.TRANSFER_CONFIG = parseConfig(); 30 | 31 | //start rabbitmq server 32 | producer = new TransferMqProducer(TRANSFER_CONFIG.getRabbitmqHost(), TRANSFER_CONFIG.getRabbitmqPort(), 33 | TRANSFER_CONFIG.getRabbitmqUsername(), TRANSFER_CONFIG.getRabbitmqPassword()); 34 | 35 | //start transfer server 36 | TransferServer.startTransferServer(TRANSFER_CONFIG.getPort()); 37 | } catch (Exception e) { 38 | LoggerFactory.getLogger(TransferStarter.class).error("[transfer] start failed", e); 39 | } 40 | } 41 | 42 | private static TransferConfig parseConfig() throws IOException { 43 | Properties properties = getProperties(); 44 | 45 | TransferConfig transferConfig = new TransferConfig(); 46 | try { 47 | transferConfig.setPort(Integer.parseInt((String) properties.get("port"))); 48 | transferConfig.setRedisHost(properties.getProperty("redis.host")); 49 | transferConfig.setRedisPort(Integer.parseInt(properties.getProperty("redis.port"))); 50 | transferConfig.setRedisPassword(properties.getProperty("redis.password")); 51 | transferConfig.setRabbitmqHost(properties.getProperty("rabbitmq.host")); 52 | transferConfig.setRabbitmqUsername(properties.getProperty("rabbitmq.username")); 53 | transferConfig.setRabbitmqPassword(properties.getProperty("rabbitmq.password")); 54 | transferConfig.setRabbitmqPort(Integer.parseInt(properties.getProperty("rabbitmq.port"))); 55 | } catch (Exception e) { 56 | throw new ImException("there's a parse error, check your config properties"); 57 | } 58 | 59 | System.setProperty("log.path", properties.getProperty("log.path")); 60 | System.setProperty("log.level", properties.getProperty("log.level")); 61 | 62 | return transferConfig; 63 | } 64 | 65 | private static Properties getProperties() throws IOException { 66 | InputStream inputStream; 67 | String path = System.getProperty("config"); 68 | if (path == null) { 69 | throw new ImException("transfer.properties is not defined"); 70 | } else { 71 | inputStream = new FileInputStream(path); 72 | } 73 | 74 | Properties properties = new Properties(); 75 | properties.load(inputStream); 76 | return properties; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /transfer/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | transfer 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | ${log.path}/transfer.log 13 | 14 | rest.%d{yyyy-MM-dd}.log 15 | 16 | 17 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /transfer/src/main/resources/transfer-docker.properties: -------------------------------------------------------------------------------- 1 | port=9082 2 | 3 | redis.host=im-redis 4 | redis.port=6379 5 | redis.password= 6 | 7 | rabbitmq.host=im-rabbit 8 | rabbitmq.port=5672 9 | rabbitmq.username=rabbitmq 10 | rabbitmq.password=rabbitmq 11 | 12 | log.path=/tmp/IM_logs 13 | log.level=info -------------------------------------------------------------------------------- /transfer/src/main/resources/transfer.properties: -------------------------------------------------------------------------------- 1 | port=9082 2 | 3 | redis.host=127.0.0.1 4 | redis.port=6379 5 | redis.password= 6 | 7 | rabbitmq.host=127.0.0.1 8 | rabbitmq.port=5672 9 | rabbitmq.username=rabbitmq 10 | rabbitmq.password=rabbitmq 11 | 12 | log.path=/tmp/IM_logs 13 | log.level=info -------------------------------------------------------------------------------- /transfer/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /user-status/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | IM 7 | com.github.yuanrw.im 8 | 1.0.0 9 | 10 | 4.0.0 11 | 12 | user-status 13 | 14 | 15 | 16 | com.github.yuanrw.im 17 | common 18 | 19 | 20 | redis.clients 21 | jedis 22 | 23 | 24 | org.yaml 25 | snakeyaml 26 | 27 | 28 | com.google.inject.extensions 29 | guice-assistedinject 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /user-status/src/main/java/com/github/yuanrw/im/user/status/factory/UserStatusServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.user.status.factory; 2 | 3 | import com.github.yuanrw.im.user.status.service.UserStatusService; 4 | 5 | import java.util.Properties; 6 | 7 | /** 8 | * Date: 2019-06-09 9 | * Time: 15:51 10 | * 11 | * @author yrw 12 | */ 13 | public interface UserStatusServiceFactory { 14 | 15 | /** 16 | * create a userStatusService 17 | * 18 | * @param properties 19 | * @return 20 | */ 21 | UserStatusService createService(Properties properties); 22 | } 23 | -------------------------------------------------------------------------------- /user-status/src/main/java/com/github/yuanrw/im/user/status/service/UserStatusService.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.user.status.service; 2 | 3 | /** 4 | * Date: 2019-06-09 5 | * Time: 15:55 6 | * 7 | * @author yrw 8 | */ 9 | public interface UserStatusService { 10 | 11 | /** 12 | * user online 13 | * 14 | * @param userId 15 | * @param connectorId 16 | * @return the user's previous connection id, if don't exist then return null 17 | */ 18 | String online(String userId, String connectorId); 19 | 20 | /** 21 | * user offline 22 | * 23 | * @param userId 24 | */ 25 | void offline(String userId); 26 | 27 | /** 28 | * get connector id by user id 29 | * 30 | * @param userId 31 | * @return 32 | */ 33 | String getConnectorId(String userId); 34 | } 35 | -------------------------------------------------------------------------------- /user-status/src/main/java/com/github/yuanrw/im/user/status/service/impl/MemoryUserStatusServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.user.status.service.impl; 2 | 3 | import com.github.yuanrw.im.user.status.service.UserStatusService; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | 8 | /** 9 | * it's for test 10 | * Date: 2019-09-02 11 | * Time: 13:33 12 | * 13 | * @author yrw 14 | */ 15 | public class MemoryUserStatusServiceImpl implements UserStatusService { 16 | 17 | private ConcurrentMap userIdConnectorIdMap; 18 | 19 | public MemoryUserStatusServiceImpl() { 20 | this.userIdConnectorIdMap = new ConcurrentHashMap<>(); 21 | } 22 | 23 | @Override 24 | public String online(String userId, String connectorId) { 25 | return userIdConnectorIdMap.put(userId, connectorId); 26 | } 27 | 28 | @Override 29 | public void offline(String userId) { 30 | userIdConnectorIdMap.remove(userId); 31 | } 32 | 33 | @Override 34 | public String getConnectorId(String userId) { 35 | return userIdConnectorIdMap.get(userId); 36 | } 37 | } -------------------------------------------------------------------------------- /user-status/src/main/java/com/github/yuanrw/im/user/status/service/impl/RedisUserStatusServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.yuanrw.im.user.status.service.impl; 2 | 3 | import com.github.yuanrw.im.user.status.service.UserStatusService; 4 | import com.google.inject.Inject; 5 | import com.google.inject.assistedinject.Assisted; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import redis.clients.jedis.Jedis; 9 | import redis.clients.jedis.JedisPool; 10 | import redis.clients.jedis.JedisPoolConfig; 11 | 12 | import java.util.Properties; 13 | 14 | /** 15 | * manage user status in redis 16 | * Date: 2019-05-19 17 | * Time: 21:14 18 | * 19 | * @author yrw 20 | */ 21 | public class RedisUserStatusServiceImpl implements UserStatusService { 22 | private static final Logger logger = LoggerFactory.getLogger(RedisUserStatusServiceImpl.class); 23 | private static final String USER_CONN_STATUS_KEY = "IM:USER_CONN_STATUS:USERID:"; 24 | 25 | private JedisPool jedisPool; 26 | 27 | @Inject 28 | public RedisUserStatusServiceImpl(@Assisted Properties properties) { 29 | JedisPoolConfig config = new JedisPoolConfig(); 30 | config.setMaxWaitMillis(2 * 1000); 31 | String password = properties.getProperty("password"); 32 | jedisPool = new JedisPool(config, properties.getProperty("host"), (Integer) properties.get("port"), 33 | 2 * 1000, password != null && !password.isEmpty() ? password : null); 34 | } 35 | 36 | @Override 37 | public String online(String userId, String connectorId) { 38 | logger.debug("[user status] user online: userId: {}, connectorId: {}", userId, connectorId); 39 | 40 | try (Jedis jedis = jedisPool.getResource()) { 41 | String oldConnectorId = jedis.hget(USER_CONN_STATUS_KEY, String.valueOf(userId)); 42 | jedis.hset(USER_CONN_STATUS_KEY, String.valueOf(userId), connectorId); 43 | return oldConnectorId; 44 | } catch (Exception e) { 45 | logger.error(e.getMessage(), e); 46 | return null; 47 | } 48 | } 49 | 50 | @Override 51 | public void offline(String userId) { 52 | logger.debug("[user status] user offline: userId: {}", userId); 53 | 54 | try (Jedis jedis = jedisPool.getResource()) { 55 | jedis.hdel(USER_CONN_STATUS_KEY, String.valueOf(userId)); 56 | } catch (Exception e) { 57 | logger.error(e.getMessage(), e); 58 | } 59 | } 60 | 61 | @Override 62 | public String getConnectorId(String userId) { 63 | try (Jedis jedis = jedisPool.getResource()) { 64 | return jedis.hget(USER_CONN_STATUS_KEY, userId); 65 | } catch (Exception e) { 66 | logger.error(e.getMessage(), e); 67 | return null; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /user-status/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rest 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------