├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable
│ │ │ ├── input.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ │ ├── activity_login.xml
│ │ │ └── activity_main.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── me
│ │ │ └── wcy
│ │ │ └── cchat
│ │ │ ├── model
│ │ │ ├── Callback.java
│ │ │ ├── MsgType.java
│ │ │ ├── LoginStatus.java
│ │ │ ├── CMessage.java
│ │ │ └── LoginInfo.java
│ │ │ ├── ChatApplication.java
│ │ │ ├── AppCache.java
│ │ │ ├── ui
│ │ │ ├── LoginActivity.java
│ │ │ └── MainActivity.java
│ │ │ └── PushService.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── server
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ └── values
│ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── me
│ │ └── wcy
│ │ └── cchat
│ │ └── server
│ │ ├── model
│ │ ├── MsgType.java
│ │ ├── CMessage.java
│ │ └── LoginInfo.java
│ │ ├── PushServer.java
│ │ ├── NettyChannelMap.java
│ │ ├── UserManager.java
│ │ ├── NettyServerBootstrap.java
│ │ └── NettyServerHandler.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── art
├── netty.png
├── network.png
├── screenshot01.gif
└── screenshot02.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':server'
2 |
--------------------------------------------------------------------------------
/art/netty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/art/netty.png
--------------------------------------------------------------------------------
/art/network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/art/network.png
--------------------------------------------------------------------------------
/art/screenshot01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/art/screenshot01.gif
--------------------------------------------------------------------------------
/art/screenshot02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/art/screenshot02.gif
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CChat
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/server/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | server
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/server/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangchenyan/cchat/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/model/Callback.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.model;
2 |
3 | public interface Callback {
4 | void onEvent(int code, String msg, T t);
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/model/MsgType.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.model;
2 |
3 | public interface MsgType {
4 | int LOGIN = 1;
5 | int PING = 2;
6 | int TEXT = 3;
7 | int TIPS = 4;
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/model/MsgType.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server.model;
2 |
3 | public interface MsgType {
4 | int LOGIN = 1;
5 | int PING = 2;
6 | int TEXT = 3;
7 | int TIPS = 4;
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/model/LoginStatus.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.model;
2 |
3 | /**
4 | * Created by hzwangchenyan on 2017/12/26.
5 | */
6 | public enum LoginStatus {
7 | UNLOGIN,
8 | CONNECTING,
9 | LOGINING,
10 | LOGINED,
11 | }
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Dec 26 11:31:24 CST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/PushServer.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server;
2 |
3 | public class PushServer {
4 |
5 | public static void main(String[] args) {
6 | NettyServerBootstrap serverBootstrap = new NettyServerBootstrap(8300);
7 | serverBootstrap.bind();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/input.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/ChatApplication.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat;
2 |
3 | import android.app.Application;
4 | import android.content.Intent;
5 |
6 | /**
7 | * Created by hzwangchenyan on 2017/12/26.
8 | */
9 | public class ChatApplication extends Application {
10 |
11 | @Override
12 | public void onCreate() {
13 | super.onCreate();
14 |
15 | startService(new Intent(this, PushService.class));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/AppCache.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat;
2 |
3 | import me.wcy.cchat.model.LoginInfo;
4 |
5 | /**
6 | * Created by hzwangchenyan on 2017/12/26.
7 | */
8 | public class AppCache {
9 | private static PushService service;
10 | private static LoginInfo myInfo;
11 |
12 | public static PushService getService() {
13 | return service;
14 | }
15 |
16 | public static void setService(PushService service) {
17 | AppCache.service = service;
18 | }
19 |
20 | public static LoginInfo getMyInfo() {
21 | return myInfo;
22 | }
23 |
24 | public static void setMyInfo(LoginInfo myInfo) {
25 | AppCache.myInfo = myInfo;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/server/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 26
5 | buildToolsVersion "26.0.3"
6 |
7 | defaultConfig {
8 | minSdkVersion 14
9 | targetSdkVersion 26
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 |
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 |
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(include: ['*.jar'], dir: 'libs')
29 | implementation 'io.netty:netty-all:4.1.9.Final'
30 | implementation 'com.google.code.gson:gson:2.8.2'
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/NettyChannelMap.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server;
2 |
3 | import java.util.Map;
4 | import java.util.concurrent.ConcurrentHashMap;
5 |
6 | import io.netty.channel.Channel;
7 |
8 | public class NettyChannelMap {
9 | private static Map map = new ConcurrentHashMap<>();
10 |
11 | public static void add(String account, Channel channel) {
12 | map.put(account, channel);
13 | }
14 |
15 | public static Channel get(String clientId) {
16 | return map.get(clientId);
17 | }
18 |
19 | public static void remove(Channel channel) {
20 | for (Map.Entry entry : map.entrySet()) {
21 | if (entry.getValue() == channel) {
22 | String account = (String) entry.getKey();
23 | map.remove(account);
24 | System.out.println(account + " leave");
25 | break;
26 | }
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 26
5 | buildToolsVersion "26.0.3"
6 |
7 | defaultConfig {
8 | applicationId "me.wcy.cchat"
9 | minSdkVersion 14
10 | targetSdkVersion 26
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 |
22 | compileOptions {
23 | sourceCompatibility JavaVersion.VERSION_1_8
24 | targetCompatibility JavaVersion.VERSION_1_8
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(include: ['*.jar'], dir: 'libs')
30 | implementation 'com.android.support:appcompat-v7:26.1.0'
31 | implementation 'com.android.support.constraint:constraint-layout:1.0.2'
32 | compile 'io.netty:netty-all:4.1.9.Final'
33 | compile 'com.google.code.gson:gson:2.8.2'
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/UserManager.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | import me.wcy.cchat.server.model.LoginInfo;
7 |
8 | /**
9 | * Created by hzwangchenyan on 2017/12/27.
10 | */
11 | public class UserManager {
12 | // 已注册的账号
13 | private Set loginInfos = new HashSet<>();
14 |
15 | private static class SingletonHolder {
16 | private static UserManager instance = new UserManager();
17 | }
18 |
19 | private UserManager() {
20 | LoginInfo wcy = new LoginInfo();
21 | wcy.setAccount("test1");
22 | wcy.setToken("123456");
23 | LoginInfo wcy2 = new LoginInfo();
24 | wcy2.setAccount("test2");
25 | wcy2.setToken("123456");
26 | loginInfos.add(wcy);
27 | loginInfos.add(wcy2);
28 | }
29 |
30 | public static UserManager get() {
31 | return SingletonHolder.instance;
32 | }
33 |
34 | public boolean verify(LoginInfo loginInfo) {
35 | for (LoginInfo info : loginInfos) {
36 | if (info.equals(loginInfo)) {
37 | return true;
38 | }
39 | }
40 | return false;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/model/CMessage.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.model;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.io.Serializable;
7 |
8 | /**
9 | * Created by hzwangchenyan on 2017/12/26.
10 | */
11 | public class CMessage implements Serializable {
12 | @SerializedName("from")
13 | private String from;
14 | @SerializedName("to")
15 | private String to;
16 | @SerializedName("type")
17 | private int type;
18 | @SerializedName("content")
19 | private String content;
20 |
21 | public String toJson() {
22 | Gson gson = new Gson();
23 | return gson.toJson(this);
24 | }
25 |
26 | public String getFrom() {
27 | return from;
28 | }
29 |
30 | public void setFrom(String from) {
31 | this.from = from;
32 | }
33 |
34 | public String getTo() {
35 | return to;
36 | }
37 |
38 | public void setTo(String to) {
39 | this.to = to;
40 | }
41 |
42 | public int getType() {
43 | return type;
44 | }
45 |
46 | public void setType(int type) {
47 | this.type = type;
48 | }
49 |
50 | public String getContent() {
51 | return content;
52 | }
53 |
54 | public void setContent(String content) {
55 | this.content = content;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/model/LoginInfo.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.model;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.io.Serializable;
7 |
8 | /**
9 | * Created by hzwangchenyan on 2017/12/26.
10 | */
11 | public class LoginInfo implements Serializable {
12 | @SerializedName("account")
13 | private String account;
14 | @SerializedName("token")
15 | private String token;
16 | @SerializedName("code")
17 | private int code;
18 | @SerializedName("msg")
19 | private String msg;
20 |
21 | public String toJson() {
22 | Gson gson = new Gson();
23 | return gson.toJson(this);
24 | }
25 |
26 | public String getAccount() {
27 | return account;
28 | }
29 |
30 | public void setAccount(String account) {
31 | this.account = account;
32 | }
33 |
34 | public String getToken() {
35 | return token;
36 | }
37 |
38 | public void setToken(String token) {
39 | this.token = token;
40 | }
41 |
42 | public int getCode() {
43 | return code;
44 | }
45 |
46 | public void setCode(int code) {
47 | this.code = code;
48 | }
49 |
50 | public String getMsg() {
51 | return msg;
52 | }
53 |
54 | public void setMsg(String msg) {
55 | this.msg = msg;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/model/CMessage.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server.model;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.io.Serializable;
7 |
8 | /**
9 | * Created by hzwangchenyan on 2017/12/26.
10 | */
11 | public class CMessage implements Serializable {
12 | @SerializedName("from")
13 | private String from;
14 | @SerializedName("to")
15 | private String to;
16 | @SerializedName("type")
17 | private int type;
18 | @SerializedName("content")
19 | private String content;
20 |
21 | public String toJson() {
22 | Gson gson = new Gson();
23 | return gson.toJson(this);
24 | }
25 |
26 | public String getFrom() {
27 | return from;
28 | }
29 |
30 | public void setFrom(String from) {
31 | this.from = from;
32 | }
33 |
34 | public String getTo() {
35 | return to;
36 | }
37 |
38 | public void setTo(String to) {
39 | this.to = to;
40 | }
41 |
42 | public int getType() {
43 | return type;
44 | }
45 |
46 | public void setType(int type) {
47 | this.type = type;
48 | }
49 |
50 | public String getContent() {
51 | return content;
52 | }
53 |
54 | public void setContent(String content) {
55 | this.content = content;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
30 |
31 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/ui/LoginActivity.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.ui;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.view.View;
7 | import android.widget.EditText;
8 | import android.widget.Toast;
9 |
10 | import me.wcy.cchat.R;
11 | import me.wcy.cchat.AppCache;
12 |
13 | public class LoginActivity extends AppCompatActivity implements View.OnClickListener {
14 | private EditText etAccount;
15 | private EditText etToken;
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | setContentView(R.layout.activity_login);
21 |
22 | etAccount = findViewById(R.id.et_account);
23 | etToken = findViewById(R.id.et_token);
24 | }
25 |
26 | @Override
27 | public void onClick(View v) {
28 | AppCache.getService().login(etAccount.getText().toString(), etToken.getText().toString(), (code, msg, aVoid) -> {
29 | if (code == 200) {
30 | Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
31 | Intent intent = new Intent(this, MainActivity.class);
32 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
33 | startActivity(intent);
34 | finish();
35 | } else {
36 | Toast.makeText(this, "登录失败 code=" + code + ", msg=" + msg, Toast.LENGTH_SHORT).show();
37 | }
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/model/LoginInfo.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server.model;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.io.Serializable;
7 |
8 | /**
9 | * Created by hzwangchenyan on 2017/12/26.
10 | */
11 | public class LoginInfo implements Serializable {
12 | @SerializedName("account")
13 | private String account;
14 | @SerializedName("token")
15 | private String token;
16 | @SerializedName("code")
17 | private int code;
18 | @SerializedName("msg")
19 | private String msg;
20 |
21 | @Override
22 | public boolean equals(Object obj) {
23 | return obj instanceof LoginInfo
24 | && ((LoginInfo) obj).account != null
25 | && ((LoginInfo) obj).account.equals(this.account)
26 | && ((LoginInfo) obj).token != null
27 | && ((LoginInfo) obj).token.equals(this.token);
28 | }
29 |
30 | public String toJson() {
31 | Gson gson = new Gson();
32 | return gson.toJson(this);
33 | }
34 |
35 | public String getAccount() {
36 | return account;
37 | }
38 |
39 | public void setAccount(String account) {
40 | this.account = account;
41 | }
42 |
43 | public String getToken() {
44 | return token;
45 | }
46 |
47 | public void setToken(String token) {
48 | this.token = token;
49 | }
50 |
51 | public int getCode() {
52 | return code;
53 | }
54 |
55 | public void setCode(int code) {
56 | this.code = code;
57 | }
58 |
59 | public String getMsg() {
60 | return msg;
61 | }
62 |
63 | public void setMsg(String msg) {
64 | this.msg = msg;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
27 |
28 |
35 |
36 |
45 |
46 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/NettyServerBootstrap.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server;
2 |
3 | import io.netty.bootstrap.ServerBootstrap;
4 | import io.netty.channel.ChannelFutureListener;
5 | import io.netty.channel.ChannelInitializer;
6 | import io.netty.channel.ChannelOption;
7 | import io.netty.channel.ChannelPipeline;
8 | import io.netty.channel.nio.NioEventLoopGroup;
9 | import io.netty.channel.socket.SocketChannel;
10 | import io.netty.channel.socket.nio.NioServerSocketChannel;
11 | import io.netty.handler.codec.serialization.ClassResolvers;
12 | import io.netty.handler.codec.serialization.ObjectDecoder;
13 | import io.netty.handler.codec.serialization.ObjectEncoder;
14 |
15 | public class NettyServerBootstrap {
16 | private int port;
17 |
18 | public NettyServerBootstrap(int port) {
19 | this.port = port;
20 | }
21 |
22 | public void bind() {
23 | new ServerBootstrap()
24 | .group(new NioEventLoopGroup(), new NioEventLoopGroup())
25 | .channel(NioServerSocketChannel.class)
26 | .option(ChannelOption.SO_BACKLOG, 128)
27 | .option(ChannelOption.TCP_NODELAY, true) // 不延迟,直接发送
28 | .childOption(ChannelOption.SO_KEEPALIVE, true) // 保持长连接状态
29 | .childHandler(new ChannelInitializer() {
30 | @Override
31 | protected void initChannel(SocketChannel socketChannel) {
32 | ChannelPipeline pipeline = socketChannel.pipeline();
33 | pipeline.addLast(new ObjectEncoder());
34 | pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
35 | pipeline.addLast(new NettyServerHandler());
36 | }
37 | })
38 | .bind(port)
39 | .addListener((ChannelFutureListener) future -> {
40 | if (future.isSuccess()) {
41 | System.out.println("netty server start");
42 | } else {
43 | System.out.println("netty server start failed");
44 | }
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/ui/MainActivity.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.ui;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.text.method.ScrollingMovementMethod;
7 | import android.view.View;
8 | import android.widget.EditText;
9 | import android.widget.TextView;
10 |
11 | import me.wcy.cchat.AppCache;
12 | import me.wcy.cchat.R;
13 | import me.wcy.cchat.model.CMessage;
14 | import me.wcy.cchat.model.Callback;
15 | import me.wcy.cchat.model.MsgType;
16 |
17 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
18 | private TextView terminal;
19 | private EditText etAccount;
20 | private EditText etMessage;
21 |
22 | @Override
23 | protected void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | setContentView(R.layout.activity_main);
26 |
27 | if (AppCache.getMyInfo() == null) {
28 | Intent intent = new Intent(this, LoginActivity.class);
29 | startActivity(intent);
30 | finish();
31 | return;
32 | }
33 |
34 | terminal = findViewById(R.id.terminal);
35 | etAccount = findViewById(R.id.et_account);
36 | etMessage = findViewById(R.id.et_message);
37 |
38 | terminal.setMovementMethod(ScrollingMovementMethod.getInstance());
39 |
40 | AppCache.getService().setReceiveMsgCallback(receiveMsgCallback);
41 | }
42 |
43 | private Callback receiveMsgCallback = new Callback() {
44 | @Override
45 | public void onEvent(int code, String msg, CMessage message) {
46 | terminal.append("[接收]" + message.getFrom() + ":" + message.getContent());
47 | terminal.append("\n");
48 | }
49 | };
50 |
51 | @Override
52 | public void onClick(View v) {
53 | if (etAccount.length() == 0 || etMessage.length() == 0) {
54 | return;
55 | }
56 |
57 | String myAccount = AppCache.getMyInfo().getAccount();
58 | CMessage message = new CMessage();
59 | message.setFrom(myAccount);
60 | message.setTo(etAccount.getText().toString());
61 | message.setType(MsgType.TEXT);
62 | message.setContent(etMessage.getText().toString());
63 | AppCache.getService().sendMsg(message, (code, msg, aVoid) -> {
64 | if (code == 200) {
65 | etMessage.setText(null);
66 | terminal.append("[发送]" + message.getContent());
67 | } else {
68 | terminal.append("[发送失败]" + message.getContent() + "," + msg);
69 | }
70 | terminal.append("\n");
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/server/src/main/java/me/wcy/cchat/server/NettyServerHandler.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat.server;
2 |
3 | import com.google.gson.Gson;
4 |
5 | import io.netty.channel.Channel;
6 | import io.netty.channel.ChannelFutureListener;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.channel.SimpleChannelInboundHandler;
9 | import io.netty.util.ReferenceCountUtil;
10 | import me.wcy.cchat.server.model.CMessage;
11 | import me.wcy.cchat.server.model.LoginInfo;
12 | import me.wcy.cchat.server.model.MsgType;
13 |
14 | public class NettyServerHandler extends SimpleChannelInboundHandler {
15 |
16 | @Override
17 | public void channelInactive(ChannelHandlerContext ctx) {
18 | // Channel失效,从Map中移除
19 | NettyChannelMap.remove(ctx.channel());
20 | }
21 |
22 | @Override
23 | protected void channelRead0(ChannelHandlerContext ctx, String msg) {
24 | Gson gson = new Gson();
25 | CMessage message = gson.fromJson(msg, CMessage.class);
26 | if (message.getType() == MsgType.PING) {
27 | System.out.println("received ping from " + message.getFrom());
28 | Channel channel = NettyChannelMap.get(message.getFrom());
29 | if (channel != null) {
30 | channel.writeAndFlush(message.toJson());
31 | }
32 | } else if (message.getType() == MsgType.LOGIN) {
33 | LoginInfo loginInfo = gson.fromJson(message.getContent(), LoginInfo.class);
34 | if (UserManager.get().verify(loginInfo)) {
35 | loginInfo.setCode(200);
36 | loginInfo.setMsg("success");
37 | message.setContent(loginInfo.toJson());
38 | ctx.channel().writeAndFlush(message.toJson());
39 | NettyChannelMap.add(loginInfo.getAccount(), ctx.channel());
40 | System.out.println(loginInfo.getAccount() + " login");
41 | } else {
42 | loginInfo.setCode(400);
43 | loginInfo.setMsg("用户名或密码错误");
44 | message.setContent(loginInfo.toJson());
45 | ctx.channel().writeAndFlush(message.toJson());
46 | }
47 | } else if (message.getType() == MsgType.TEXT) {
48 | Channel channel = NettyChannelMap.get(message.getTo());
49 | if (channel != null) {
50 | channel.isWritable();
51 | channel.writeAndFlush(message.toJson()).addListener((ChannelFutureListener) future -> {
52 | if (!future.isSuccess()) {
53 | System.out.println("send msg to " + message.getTo() + " failed");
54 | }
55 | });
56 | }
57 | }
58 | ReferenceCountUtil.release(msg);
59 | }
60 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/java/me/wcy/cchat/PushService.java:
--------------------------------------------------------------------------------
1 | package me.wcy.cchat;
2 |
3 | import android.app.Service;
4 | import android.content.Intent;
5 | import android.os.Handler;
6 | import android.os.IBinder;
7 | import android.support.annotation.NonNull;
8 | import android.support.annotation.Nullable;
9 | import android.util.Log;
10 |
11 | import com.google.gson.Gson;
12 |
13 | import java.net.InetSocketAddress;
14 |
15 | import io.netty.bootstrap.Bootstrap;
16 | import io.netty.channel.ChannelFutureListener;
17 | import io.netty.channel.ChannelHandlerContext;
18 | import io.netty.channel.ChannelInitializer;
19 | import io.netty.channel.ChannelOption;
20 | import io.netty.channel.ChannelPipeline;
21 | import io.netty.channel.SimpleChannelInboundHandler;
22 | import io.netty.channel.nio.NioEventLoopGroup;
23 | import io.netty.channel.socket.SocketChannel;
24 | import io.netty.channel.socket.nio.NioSocketChannel;
25 | import io.netty.handler.codec.serialization.ClassResolvers;
26 | import io.netty.handler.codec.serialization.ObjectDecoder;
27 | import io.netty.handler.codec.serialization.ObjectEncoder;
28 | import io.netty.handler.timeout.IdleState;
29 | import io.netty.handler.timeout.IdleStateEvent;
30 | import io.netty.handler.timeout.IdleStateHandler;
31 | import io.netty.util.ReferenceCountUtil;
32 | import me.wcy.cchat.model.CMessage;
33 | import me.wcy.cchat.model.Callback;
34 | import me.wcy.cchat.model.LoginInfo;
35 | import me.wcy.cchat.model.LoginStatus;
36 | import me.wcy.cchat.model.MsgType;
37 |
38 | /**
39 | * Created by hzwangchenyan on 2017/12/26.
40 | */
41 | public class PushService extends Service {
42 | private static final String TAG = "PushService";
43 | private static final String HOST = "10.240.78.82";
44 | private static final int PORT = 8300;
45 |
46 | private SocketChannel socketChannel;
47 | private Callback loginCallback;
48 | private Callback receiveMsgCallback;
49 | private Handler handler;
50 | private LoginStatus status = LoginStatus.UNLOGIN;
51 |
52 | @Override
53 | public void onCreate() {
54 | super.onCreate();
55 | handler = new Handler();
56 | AppCache.setService(this);
57 | }
58 |
59 | @Nullable
60 | @Override
61 | public IBinder onBind(Intent intent) {
62 | return null;
63 | }
64 |
65 | public void setReceiveMsgCallback(Callback receiveMsgCallback) {
66 | this.receiveMsgCallback = receiveMsgCallback;
67 | }
68 |
69 | private void connect(@NonNull Callback callback) {
70 | if (status == LoginStatus.CONNECTING) {
71 | return;
72 | }
73 |
74 | updateStatus(LoginStatus.CONNECTING);
75 | NioEventLoopGroup group = new NioEventLoopGroup();
76 | new Bootstrap()
77 | .channel(NioSocketChannel.class)
78 | .group(group)
79 | .option(ChannelOption.SO_KEEPALIVE, true)
80 | .option(ChannelOption.TCP_NODELAY, true)
81 | .handler(new ChannelInitializer() {
82 | @Override
83 | protected void initChannel(SocketChannel socketChannel) throws Exception {
84 | ChannelPipeline pipeline = socketChannel.pipeline();
85 | pipeline.addLast(new IdleStateHandler(0, 30, 0));
86 | pipeline.addLast(new ObjectEncoder());
87 | pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
88 | pipeline.addLast(new ChannelHandle());
89 | }
90 | })
91 | .connect(new InetSocketAddress(HOST, PORT))
92 | .addListener((ChannelFutureListener) future -> {
93 | if (future.isSuccess()) {
94 | socketChannel = (SocketChannel) future.channel();
95 | callback.onEvent(200, "success", null);
96 | } else {
97 | Log.e(TAG, "connect failed");
98 | close();
99 | // 这里一定要关闭,不然一直重试会引发OOM
100 | future.channel().close();
101 | group.shutdownGracefully();
102 | callback.onEvent(400, "connect failed", null);
103 | }
104 | });
105 | }
106 |
107 | public void login(String account, String token, Callback callback) {
108 | if (status == LoginStatus.CONNECTING || status == LoginStatus.LOGINING) {
109 | return;
110 | }
111 |
112 | connect((code, msg, aVoid) -> {
113 | if (code == 200) {
114 | LoginInfo loginInfo = new LoginInfo();
115 | loginInfo.setAccount(account);
116 | loginInfo.setToken(token);
117 | CMessage loginMsg = new CMessage();
118 | loginMsg.setFrom(account);
119 | loginMsg.setType(MsgType.LOGIN);
120 | loginMsg.setContent(loginInfo.toJson());
121 | socketChannel.writeAndFlush(loginMsg.toJson())
122 | .addListener((ChannelFutureListener) future -> {
123 | if (future.isSuccess()) {
124 | loginCallback = callback;
125 | } else {
126 | close();
127 | updateStatus(LoginStatus.UNLOGIN);
128 | if (callback != null) {
129 | handler.post(() -> callback.onEvent(400, "failed", null));
130 | }
131 | }
132 | });
133 | } else {
134 | close();
135 | updateStatus(LoginStatus.UNLOGIN);
136 | if (callback != null) {
137 | handler.post(() -> callback.onEvent(400, "failed", null));
138 | }
139 | }
140 | });
141 | }
142 |
143 | public void sendMsg(CMessage message, Callback callback) {
144 | if (status != LoginStatus.LOGINED) {
145 | callback.onEvent(401, "unlogin", null);
146 | return;
147 | }
148 |
149 | socketChannel.writeAndFlush(message.toJson())
150 | .addListener((ChannelFutureListener) future -> {
151 | if (callback == null) {
152 | return;
153 | }
154 | if (future.isSuccess()) {
155 | handler.post(() -> callback.onEvent(200, "success", null));
156 | } else {
157 | handler.post(() -> callback.onEvent(400, "failed", null));
158 | }
159 | });
160 | }
161 |
162 | private void close() {
163 | if (socketChannel != null) {
164 | socketChannel.close();
165 | socketChannel = null;
166 | }
167 | }
168 |
169 | private class ChannelHandle extends SimpleChannelInboundHandler {
170 |
171 | @Override
172 | public void channelInactive(ChannelHandlerContext ctx) throws Exception {
173 | super.channelInactive(ctx);
174 | PushService.this.close();
175 | updateStatus(LoginStatus.UNLOGIN);
176 | retryLogin(3000);
177 | }
178 |
179 | @Override
180 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
181 | super.userEventTriggered(ctx, evt);
182 | if (evt instanceof IdleStateEvent) {
183 | IdleStateEvent e = (IdleStateEvent) evt;
184 | if (e.state() == IdleState.WRITER_IDLE) {
185 | // 空闲了,发个心跳吧
186 | CMessage message = new CMessage();
187 | message.setFrom(AppCache.getMyInfo().getAccount());
188 | message.setType(MsgType.PING);
189 | ctx.writeAndFlush(message.toJson());
190 | }
191 | }
192 | }
193 |
194 | @Override
195 | protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
196 | Gson gson = new Gson();
197 | CMessage message = gson.fromJson(msg, CMessage.class);
198 | if (message.getType() == MsgType.LOGIN) {
199 | LoginInfo loginInfo = gson.fromJson(message.getContent(), LoginInfo.class);
200 | if (loginInfo.getCode() == 200) {
201 | updateStatus(LoginStatus.LOGINED);
202 | AppCache.setMyInfo(loginInfo);
203 | if (loginCallback != null) {
204 | handler.post(() -> {
205 | loginCallback.onEvent(200, "success", null);
206 | loginCallback = null;
207 | });
208 | }
209 | } else {
210 | close();
211 | updateStatus(LoginStatus.UNLOGIN);
212 | if (loginCallback != null) {
213 | handler.post(() -> {
214 | loginCallback.onEvent(loginInfo.getCode(), loginInfo.getMsg(), null);
215 | loginCallback = null;
216 | });
217 | }
218 | }
219 | } else if (message.getType() == MsgType.PING) {
220 | Log.d(TAG, "receive ping from server");
221 | } else if (message.getType() == MsgType.TEXT) {
222 | Log.d(TAG, "receive text message " + message.getContent());
223 | if (receiveMsgCallback != null) {
224 | handler.post(() -> receiveMsgCallback.onEvent(200, "success", message));
225 | }
226 | }
227 |
228 | ReferenceCountUtil.release(msg);
229 | }
230 | }
231 |
232 | private void retryLogin(long mills) {
233 | if (AppCache.getMyInfo() == null) {
234 | return;
235 | }
236 | handler.postDelayed(() -> login(AppCache.getMyInfo().getAccount(), AppCache.getMyInfo().getToken(), (code, msg, aVoid) -> {
237 | if (code != 200) {
238 | retryLogin(mills);
239 | }
240 | }), mills);
241 | }
242 |
243 | private void updateStatus(LoginStatus status) {
244 | if (this.status != status) {
245 | Log.d(TAG, "update status from " + this.status + " to " + status);
246 | this.status = status;
247 | }
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android 长连接初体验(基于netty)
2 |
3 | 
4 |
5 | ## 前言
6 |
7 | 众所周知,推送和 IM 在 Android 应用中很常见,但真正自己去实现的比较少,我们大多会去选择第三方提供的成熟方案,如极光推送、云信等,因为移动网络具有不确定性,因此自己实现一套稳定的方案会耗费很多精力,这对于小公司来说是得不偿失的。
8 |
9 | 推送和 IM 我们平时用的很多,但真正了解原理的不多,真正动手实现过的不多。推送和 IM 本质上都是长连接,无非是业务方向不同,因此我们以下统称为长连接。今天我们一起来揭开长连接的神秘面纱。
10 |
11 | ## [netty](http://netty.io/) 是何物
12 |
13 | 虽然很多人都对 netty 比较熟悉了,但是可能还是有不了解的同学,因此我们先简单介绍下 netty。
14 |
15 | Netty是由 JBOSS 开发的一个 Java 开源框架
16 |
17 | > Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
18 | >
19 | > Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
20 |
21 | 这段简介摘自 netty 官网,是对 netty 的高度概括。已经帮你们翻译好了 ^ _ ^
22 |
23 | > Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
24 | > 'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.
25 | >
26 | > Netty是一个NIO客户端服务器框架,可以快速简单地开发协议服务器和客户端等网络应用程序。 它极大地简化和简化了TCP和UDP套接字服务器等网络编程。
27 | > “快速而简单”并不意味着由此产生的应用程序将受到可维护性或性能问题的困扰。 Netty的设计经验非常丰富,包括FTP,SMTP,HTTP以及各种基于二进制和文本的传统协议。 因此,Netty已经成功地找到了一个方法来实现轻松的开发,性能,稳定性和灵活性,而不用妥协。
28 |
29 | 一复制就停不下来了 =。= 主要是觉得官网介绍的很准确。
30 |
31 | 这里提到了 `事件驱动`,可能大家觉得有点陌生,事件驱动其实很简单,比如你点了下鼠标,软件执行相应的操作,这就是一个事件驱动模型,再举一个例子,Android 中的 Message Looper Handler 也是事件驱动,通过 Handler 发送一个消息,这个消息就相当于一个事件,Looper 取出事件,再由 Handler 处理。
32 |
33 | 这些特性就使得 netty 很适合用于高并发的长连接。
34 |
35 | 今天,我们就一起使用 netty 实现一个 Android IM,包括客户端和服务端。
36 |
37 | ## 构思
38 |
39 | 作为一个 IM 应用,我们需要识别用户,客户端建立长连接后需要汇报自己的信息,服务器验证通过后将其缓存起来,表明该用户在线。
40 |
41 | 客户端是一个一个的个体,服务器作为中转,比如,A 给 B 发送消息,A 先把消息发送到服务器,并告诉服务器这条消息要发给谁,然后服务器把消息发送给 B。
42 |
43 | 服务器在收到消息后可以对消息进行存储,如果 B 不在线,就等 B 上线后再将消息发送过去。
44 |
45 | 
46 |
47 | ## 实战
48 |
49 | 新建一个项目
50 |
51 | 1. 编写客户端代码
52 |
53 | 添加 netty 依赖
54 |
55 | ```
56 | implementation 'io.netty:netty-all:4.1.9.Final'
57 | ```
58 |
59 | netty 已经出了 5.x 的测试版,为了稳定,我们使用最新稳定版。
60 |
61 | - 和服务器建立连接
62 |
63 | ```
64 | // 修改为自己的主机和端口
65 | private static final String HOST = "10.240.78.82";
66 | private static final int PORT = 8300;
67 |
68 | private SocketChannel socketChannel;
69 |
70 | NioEventLoopGroup group = new NioEventLoopGroup();
71 | new Bootstrap()
72 | .channel(NioSocketChannel.class)
73 | .group(group)
74 | .option(ChannelOption.TCP_NODELAY, true) // 不延迟,直接发送
75 | .option(ChannelOption.SO_KEEPALIVE, true) // 保持长连接状态
76 | .handler(new ChannelInitializer() {
77 | @Override
78 | protected void initChannel(SocketChannel socketChannel) throws Exception {
79 | ChannelPipeline pipeline = socketChannel.pipeline();
80 | pipeline.addLast(new IdleStateHandler(0, 30, 0));
81 | pipeline.addLast(new ObjectEncoder());
82 | pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
83 | pipeline.addLast(new ChannelHandle());
84 | }
85 | })
86 | .connect(new InetSocketAddress(HOST, PORT))
87 | .addListener((ChannelFutureListener) future -> {
88 | if (future.isSuccess()) {
89 | // 连接成功
90 | socketChannel = (SocketChannel) future.channel();
91 | } else {
92 | Log.e(TAG, "connect failed");
93 | // 这里一定要关闭,不然一直重试会引发OOM
94 | future.channel().close();
95 | group.shutdownGracefully();
96 | }
97 | });
98 | ```
99 |
100 | - 身份认证
101 |
102 | ```
103 | LoginInfo loginInfo = new LoginInfo();
104 | loginInfo.setAccount(account);
105 | loginInfo.setToken(token);
106 | CMessage loginMsg = new CMessage();
107 | loginMsg.setFrom(account);
108 | loginMsg.setType(MsgType.LOGIN);
109 | loginMsg.setContent(loginInfo.toJson());
110 | socketChannel.writeAndFlush(loginMsg.toJson())
111 | .addListener((ChannelFutureListener) future -> {
112 | if (future.isSuccess()) {
113 | // 发送成功,等待服务器响应
114 | } else {
115 | // 发送成功
116 | close(); // 关闭连接,节约资源
117 | }
118 | });
119 | ```
120 |
121 | - 处理服务器发来的消息
122 |
123 | ```
124 | private class ChannelHandle extends SimpleChannelInboundHandler {
125 | @Override
126 | public void channelInactive(ChannelHandlerContext ctx) throws Exception {
127 | super.channelInactive(ctx);
128 | // 连接失效
129 | PushService.this.close();
130 | }
131 |
132 | @Override
133 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
134 | super.userEventTriggered(ctx, evt);
135 | if (evt instanceof IdleStateEvent) {
136 | IdleStateEvent e = (IdleStateEvent) evt;
137 | if (e.state() == IdleState.WRITER_IDLE) {
138 | // 空闲了,发个心跳吧
139 | CMessage message = new CMessage();
140 | message.setFrom(myInfo.getAccount());
141 | message.setType(MsgType.PING);
142 | ctx.writeAndFlush(message.toJson());
143 | }
144 | }
145 | }
146 |
147 | @Override
148 | protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
149 | Gson gson = new Gson();
150 | CMessage message = gson.fromJson(msg, CMessage.class);
151 | if (message.getType() == MsgType.LOGIN) {
152 | // 服务器返回登录结果
153 | } else if (message.getType() == MsgType.PING) {
154 | Log.d(TAG, "receive ping from server");
155 | // 收到服务器回应的心跳
156 | } else if (message.getType() == MsgType.TEXT) {
157 | Log.d(TAG, "receive text message " + message.getContent());
158 | // 收到消息
159 | }
160 |
161 | ReferenceCountUtil.release(msg);
162 | }
163 | }
164 | ```
165 |
166 | 这些代码要长期在后台执行,因此我们放在 Service 中。
167 |
168 | 2. 编写服务器代码
169 |
170 | 新建一个 Android Library 模块作为服务端,添加同样的依赖
171 |
172 | - 启动 netty 服务并绑定端口
173 |
174 | ```
175 | new ServerBootstrap()
176 | .group(new NioEventLoopGroup(), new NioEventLoopGroup())
177 | .channel(NioServerSocketChannel.class)
178 | .option(ChannelOption.SO_BACKLOG, 128)
179 | .option(ChannelOption.TCP_NODELAY, true) // 不延迟,直接发送
180 | .childOption(ChannelOption.SO_KEEPALIVE, true) // 保持长连接状态
181 | .childHandler(new ChannelInitializer() {
182 | @Override
183 | protected void initChannel(SocketChannel socketChannel) {
184 | ChannelPipeline pipeline = socketChannel.pipeline();
185 | pipeline.addLast(new ObjectEncoder());
186 | pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
187 | pipeline.addLast(new NettyServerHandler());
188 | }
189 | })
190 | .bind(port)
191 | .addListener((ChannelFutureListener) future -> {
192 | if (future.isSuccess()) {
193 | System.out.println("netty server start");
194 | } else {
195 | System.out.println("netty server start failed");
196 | }
197 | });
198 | ```
199 |
200 | - 处理客户端发来的消息
201 |
202 | ```
203 | public class NettyServerHandler extends SimpleChannelInboundHandler {
204 | @Override
205 | public void channelInactive(ChannelHandlerContext ctx) {
206 | // Channel失效,从Map中移除
207 | NettyChannelMap.remove(ctx.channel());
208 | }
209 |
210 | @Override
211 | protected void channelRead0(ChannelHandlerContext ctx, String msg) {
212 | Gson gson = new Gson();
213 | CMessage message = gson.fromJson(msg, CMessage.class);
214 | if (message.getType() == MsgType.PING) {
215 | System.out.println("received ping from " + message.getFrom());
216 | // 收到 Ping,回应一下
217 | Channel channel = NettyChannelMap.get(message.getFrom());
218 | if (channel != null) {
219 | channel.writeAndFlush(message.toJson());
220 | }
221 | } else if (message.getType() == MsgType.LOGIN) {
222 | // 用户登录
223 | LoginInfo loginInfo = gson.fromJson(message.getContent(), LoginInfo.class);
224 | if (UserManager.get().verify(loginInfo)) {
225 | loginInfo.setCode(200);
226 | loginInfo.setMsg("success");
227 | message.setContent(loginInfo.toJson());
228 | ctx.channel().writeAndFlush(message.toJson());
229 | NettyChannelMap.add(loginInfo.getAccount(), ctx.channel());
230 | System.out.println(loginInfo.getAccount() + " login");
231 | } else {
232 | loginInfo.setCode(400);
233 | loginInfo.setMsg("用户名或密码错误");
234 | message.setContent(loginInfo.toJson());
235 | ctx.channel().writeAndFlush(message.toJson());
236 | }
237 | } else if (message.getType() == MsgType.TEXT) {
238 | // 发送消息
239 | Channel channel = NettyChannelMap.get(message.getTo());
240 | if (channel != null) {
241 | channel.isWritable();
242 | channel.writeAndFlush(message.toJson()).addListener((ChannelFutureListener) future -> {
243 | if (!future.isSuccess()) {
244 | System.out.println("send msg to " + message.getTo() + " failed");
245 | }
246 | });
247 | }
248 | }
249 | ReferenceCountUtil.release(msg);
250 | }
251 | }
252 | ```
253 |
254 | 已登录的用户缓存在 NettyChannelMap 中。
255 |
256 | 这里可以加入离线消息缓存逻辑,如果消息发送失败,需要缓存起来,等待用户上线后再发送。
257 |
258 | 如果服务端在本机运行,需要和客户端在同一个局域网,如果是在公网运行则不需要。
259 |
260 | ## 运行效果
261 |
262 | 
263 | 
264 |
265 | ## 源码
266 |
267 | 只看上面的代码可能还是有点懵逼,建议大家跑一下源码,会对 netty 有一个更清晰的认识。
268 | [https://github.com/wangchenyan/CChat](https://github.com/wangchenyan/cchat)
269 |
270 | ## 总结
271 |
272 | 今天我们一起认识了 netty,并使用 netty 实现了一个简单的 IM 应用。这里我们仅仅实现了 IM 核心功能,其他比如保活机制、断线重连不在本文讨论范围之内。
273 |
274 | 我们今天实现的长连接和第三方长连接服务商提供的长连接服务其实并无太大差异,无非是后者具有成熟的保活、短线重连机制。
275 |
276 | 读完本文,是否觉得长连接其实也没那么神秘?
277 |
278 | 但是不要骄傲,我们今天学习的只是最简单的用法,这只是皮毛,要想完全了解其中的原理还是要花费很多功夫的。
279 |
--------------------------------------------------------------------------------