Hugh小茶馆-使用Socket制作的简易即时通信APP
这是我的第一个在安卓平台上的应用程序

嘛,按照惯例,咱今儿再来分析分析代码
这样一个安卓应用是怎么做的?
这篇文章可以让对Java SE有一定了解的你快速进行Android开发,以及带你避开我踩过的坑
环境
集成开发环境IDE使用的是Android Studio,使用的编程语言是Java,界面编写使用XML(可扩展标记语言)
界面
既然是一个app,那肯定需要一个UI界面叭,不然用户该如何进行操作呢?因此,首先咱们来介绍一下这个界面
在1.1 环境中,介绍了界面编写需要使用XML这一语言。在Android Studio新建了一个应用程序后,会自动生成一些基础的根目录文件,其中res\layout\activity_main.xml是应用程序的布局文件,我们在这个文件里即可制作程序界面,下面贴上代码解释(不要怕XML,能看懂英文也能大致看明白意思)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <!-- 以上全是Android Studio自动生成的,下面的代码由我们来编写 --> <LinearLayout<!-- 这里是线性布局,可以理解为存放组件的一个盒子 --> android:layout_width="match_parent" android:layout_height="match_parent"<!-- 填充整个页面 --> android:background="#FFFCCC"<!-- 背景色 --> android:orientation="vertical"> <!-- 设置组件的排列方向为垂直向下排列 -->
<!-- 套娃使用LinearLayout也是可以的唷 --> <LinearLayout android:layout_weight="5"<!-- 这里是给我们套娃的一个LinearLayout的一个权重,在有多个存在权重的组件存在时,该数值越小,说明组件所占的比例越大(体积越大) --> android:layout_width="match_parent" android:layout_height="match_parent" > <EditText<!-- 这是一个给我们输入东西的框框,对应的上图程序界面的改名文本框 --> android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/Name"<!-- 这里的id很重要哟,我们在正式写代码的时候,需要给对应ID的控件写代码,如果不写id的话,程序将定位不到我们这里写的组件,也就是说这个组件只能看不能用 --> android:textSize="20dp" android:layout_weight="1" >
</EditText> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="3" android:text="确认改名" android:id="@+id/clearScreen" > </Button> </LinearLayout> <LinearLayout android:layout_weight="1"<!-- 存放消息框的布局组件,消息框框在程序界面中占比较大,因此权重给的比较大,为1 --> android:layout_width="match_parent" android:layout_height="match_parent"> <ListView<!-- 显示列表的一个组件 --> android:layout_width="match_parent" android:id="@+id/messageBoard" android:layout_height="match_parent">
</ListView> <!-- 后边也就没啥新奇的玩意儿了,堆代码吧! --> </LinearLayout> <LinearLayout android:layout_weight="5" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="4" android:text="在线人数" android:id="@+id/onlineList" /> <EditText android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/Text" android:layout_weight="1" >
</EditText>
</LinearLayout> <LinearLayout android:layout_weight="5" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:text="发送" android:id="@+id/sendMessage" > </Button> </LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
|
组件功能的实现
嗯,咱在布局文件里写了那么多的控件,但是实际上都是只能看不能用的。现在,我们需要给每个组件写上对应的事件监听器代码。
嘛,在MainActivity.java中,我们可以试着写一个定位到布局中的按钮的代码
1
| public static Button sendMessage;
|
好哒!我们已经声明了一个按钮对象,不过这个对象是空的(null),接下来需要给这个对象存入按钮的具体实例。
在onCreate方法中,写上这样的代码
1
| sendMessage = findViewById(R.id.sendMessage);
|
至此,我们已经把界面上的控件搬到我们的Java代码里面了。接下来,我们就可以用Java代码对该控件进行任何的操作、监听
还是以sendMessage这个按钮为例,咱给他搞个监听器
1 2 3 4 5 6 7
| sendMessage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {
} });
|
ne,就是这样,我们给这个按钮安上了一个监听器,紧接着我们可以继续写代码。
来,咱把所有的控件都一起搬到Java代码里吧!
1 2 3 4 5 6 7 8 9
| public static Button sendMessage; public static Button confirm; public static Button onlineList; public static ListView messageBoard; public static EditText Name; public static EditText Text;
public static ArrayAdapter<String> adapter;
|
好啦😃,所有组件都到齐了,接下来就是我们的网络通信环节。(没用到前面说的Socket工具类,稍微会有一点点麻烦)
切记!主线程里绝对不可以涉及到网络连接这类可能导致线程阻塞的活动,否则会导致程序闪退!
因此,为了实现网络通信,我们需要新开一个线程来负责网络连接(Socket连接到ServerSocket)。同时,我们需要在AndroidManifest.xml写入权限的一个申请,否则我们程序运行时也会因为没有权限连接互联网而卡退。
在AndroidManifest.xml的application标签之上写上这些吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> class ConnectHandler extends Thread{ public static Socket s; public ConnectHandler(){
} @Override public void run(){ try { s = new Socket("49.234.97.49",5613); s.setKeepAlive(true);
} catch (IOException e) { e.printStackTrace(); } ThreadHelper threadHelper = new ThreadHelper(ConnectHandler.s); threadHelper.start(); try{ ThreadHelper listener = new ThreadHelper(s); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream(),"UTF-8")); listener.start(); writer.write(MainActivity.ID+"<手机端>进入茶馆,欢迎光临~\n"); writer.flush(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
|
接下来的代码有个要点:其他线程不能直接更新UI界面,需要使用post一类的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class ThreadHelper extends Thread{ public Socket s; public ThreadHelper(Socket socket){ s=socket; }
@Override public void run(){ try { while(true) { BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(),"UTF-8"));
MainActivity.temp = br.readLine(); MainActivity.messageBoard.post(new Runnable() { @Override public void run() { MainActivity.adapter.add(MainActivity.temp); MainActivity.messageBoard.setAdapter(MainActivity.adapter); MainActivity.messageBoard.getBottom(); }}); } } catch (IOException e) { MainActivity.archiveMessage = "[错误]原因:目标计算机已关闭或拒绝连接"; } } } class Sender extends Thread{ @Override public void run(){ try { BufferedWriter writer; writer = new BufferedWriter(new OutputStreamWriter(ConnectHandler.s.getOutputStream(),"UTF-8")); if(MainActivity.mess != null){ writer.write(MainActivity.ID + ":"+MainActivity.mess+"\n"); writer.flush(); } } catch (IOException e) { e.printStackTrace(); }
} }
|
对了,还有一个获取在线人数的按钮,这需要对服务器加一些更改,具体的思路是客户端发送一个#command/~~/~#getonline给服务端后,服务端就能根据ServerSocket下连接的Socket数量对发送这个指令的客户端进行一个回复。
代码总览
MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
| package com.huugh.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView;
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Random;
public class MainActivity extends AppCompatActivity { public static Button sendMessage; public static Button confirm; public static Button onlineList; public static ListView messageBoard; public static EditText Name; public static EditText Text; public static String archiveMessage; public static String mess; public static ArrayAdapter<String> adapter; public static String temp; public static String ID; public static int streamID; public static ArrayList<String> data = new ArrayList<>(); @Override public boolean onKeyDown(int keyCode, KeyEvent key){ if(keyCode == KeyEvent.KEYCODE_HOME){ moveTaskToBack(false); } return super.onKeyDown(keyCode,key); } @Override protected void onDestroy() { super.onDestroy(); try { ConnectHandler.s.close(); } catch (IOException e) { e.printStackTrace(); } }
public void playMessageSound(){ Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), notification); r.play(); } @Override protected void onCreate(Bundle savedInstanceState) {
Random r = new Random(); ID = r.nextInt(32768)+"";
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Name = findViewById(R.id.Name); sendMessage = findViewById(R.id.sendMessage); onlineList = findViewById(R.id.onlineList); confirm = findViewById(R.id.clearScreen); messageBoard = findViewById(R.id.messageBoard); messageBoard.setStackFromBottom(true); messageBoard.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); Text = findViewById(R.id.Text); Name.setText(ID); ConnectHandler ConnectHandler = new ConnectHandler(); new Thread(ConnectHandler).start(); adapter = new ArrayAdapter<>(MainActivity.this,android.R.layout.simple_list_item_1,data); confirm.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ID = Name.getText().toString(); } }); onlineList.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mess = "#command/~~/~#getonline"; Sender sender = new Sender(); sender.start();
} }); sendMessage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(Text.getText()!=null){ mess = Text.getText().toString(); Sender sender = new Sender(); sender.start(); Text.setText("");
}
} }); } } class ConnectHandler extends Thread{ public static Socket s; public ConnectHandler(){
} @Override public void run(){ try { s = new Socket("49.234.97.49",5613); s.setKeepAlive(true); } catch (IOException e) { e.printStackTrace(); } ThreadHelper threadHelper = new ThreadHelper(ConnectHandler.s); threadHelper.start(); try{ ThreadHelper listener = new ThreadHelper(s); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream(),"UTF-8")); listener.start(); writer.write(MainActivity.ID+"<手机端>进入茶馆,欢迎光临~\n"); writer.flush(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
class ThreadHelper extends Thread{ public Socket s; public ThreadHelper(Socket socket){ s=socket; } @Override public void run(){ try { while(true) { BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(),"UTF-8"));
MainActivity.temp = br.readLine();
MainActivity.messageBoard.post(new Runnable() { @Override public void run() { MainActivity.adapter.add(MainActivity.temp); MainActivity.messageBoard.setAdapter(MainActivity.adapter); MainActivity.messageBoard.getBottom();
}});
} } catch (IOException e) { MainActivity.archiveMessage = "[错误]原因:目标计算机已关闭或拒绝连接"; }
} } class Sender extends Thread{ @Override public void run(){ try { BufferedWriter writer; writer = new BufferedWriter(new OutputStreamWriter(ConnectHandler.s.getOutputStream(),"UTF-8")); if(MainActivity.mess != null){ writer.write(MainActivity.ID + ":"+MainActivity.mess+"\n"); writer.flush(); } } catch (IOException e) { e.printStackTrace(); }
} }
|
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFCCC" android:orientation="vertical"> <LinearLayout android:layout_weight="5" android:layout_width="match_parent" android:layout_height="match_parent" > <EditText android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/Name" android:textSize="20dp" android:layout_weight="1" >
</EditText> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="3" android:text="确认改名" android:id="@+id/clearScreen" > </Button> </LinearLayout> <LinearLayout android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:layout_width="match_parent" android:id="@+id/messageBoard" android:layout_height="match_parent"> </ListView> </LinearLayout> <LinearLayout android:layout_weight="5" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_weight="1" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="4" android:text="在线人数" android:id="@+id/onlineList" /> <EditText android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/Text" android:layout_weight="1" >
</EditText>
</LinearLayout> <LinearLayout android:layout_weight="5" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:text="发送" android:id="@+id/sendMessage" > </Button> </LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
|
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.huugh.myapplication"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
<activity android:name=".AuthMain">
</activity> </application>
</manifest>
|
小彩蛋
如果你对我的apk进行一个解包,你会发现里面有一个message.mp3,没错,这是我编配的一个收到消息的铃声,具体位置在res\raw\message.mp3,至于为啥没添加进APP呢?因为我遇到了到现在还没解决的bug——播放一次之后就不播放了QAQ
希望以后我解决了能添加进APP吧!
试听:
谱(透明背景):
