聊天室
前言
这周是我学习Android的第四周,上周写了一个聊天室,这周来总结一下。聊天室分为两个部分,一个部分是客户端,就是我们平时看到的部分,还有一个非常重要的部分,那就是服务端。客户端主要是和用户进行交互,接收用户所传来的指令,在服务端才会将客户端传来的数据进行处理。
在刚开始的时候,我还不知道应该如何去写,没有思路,最后是先做的客户端,将客户端的UI写好之后,再来处理服务端和客户端与服务端的交接的部分。这是我写聊天室的大体思路,我来分享一下。
客户端
首先写的是客户端,因为刚开始,不知道如何去从服务端下手写,所以就先写了客户端的UI。首先,最好写的是布局,我写的聊天室也比较简单,没有多少功能,所以布局也是比较简单的首先来简单介绍一下,我有两个布局,一个是登录的布局,一个是进入之后的聊天布局。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffcc"
>
<LinearLayout
android:layout_marginTop="100dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000"
android:text="账户" />
<EditText
android:id="@+id/Account_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000"
android:text="密码" />
<EditText
android:id="@+id/password_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="textPassword"
android:maxLines="1"
/>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:background="#FFCC00"
android:text="登录" />
<Button
android:id="@+id/create_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="80dp"
android:background="#FF6600"
android:text="注册" />
</RelativeLayout>
</LinearLayout>
以上是登陆布局,这个布局没什么说的,就是简单的布局和一些简单的控件所组成。
下面是登陆进去的聊天界面。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<include layout="@layout/title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/ip_edit"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="255.255.255.255" />
<Button
android:id="@+id/record_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="聊天记录" />
<Button
android:id="@+id/clearAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清除聊天记录"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<EditText
android:id="@+id/inputText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"
/>
<Button
android:id="@+id/clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清屏" />
</LinearLayout>
</LinearLayout>
由于上面使用了RecyclerView,所以就会有每个RecyclerView的小布局。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:id="@+id/left_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@drawable/message_left">
<TextView
android:id="@+id/left_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#000" />
</LinearLayout>
<TextView
android:id="@+id/ip_text_left"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/left_layout"
android:textColor="#000" />
<TextView
android:id="@+id/time_left"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/ip_text_left"
android:textColor="#000" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<ImageView
android:id="@+id/my_image"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_alignParentRight="true"
android:background="#000" />
<LinearLayout
android:id="@+id/right_layout"
android:layout_width="wrap_content"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_gravity="right"
android:layout_marginRight="60dp"
android:background="@drawable/message_right">
<TextView
android:id="@+id/right_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#000" />
</LinearLayout>
<TextView
android:id="@+id/ip_text_right"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/right_layout"
android:textColor="#000" />
<TextView
android:id="@+id/time_right"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
android:layout_toLeftOf="@+id/ip_text_right"
android:textColor="#000" />
</RelativeLayout>
</LinearLayout>
上面就是UI的全部代码了,还有一个标题,我没展示出来,那个标题很简单,就一个TextView协商标题就行了。上面那些UI感觉没什么说的,都是一些非常常见的控件和布局,效果我就不展示了,因为太丑了。
下来就是写每个活动中的内容。首先是登录的活动。
package com.example.chatui;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
import org.litepal.LitePal;
import java.util.List;
public class Account extends AppCompatActivity {
private EditText account_edit;
private EditText password_edit;
private Button login_button;
private Button create_button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account);
account_edit = (EditText) findViewById(R.id.Account_edit);
password_edit = (EditText) findViewById(R.id.password_edit);
login_button = (Button) findViewById(R.id.login_button);
create_button = (Button) findViewById(R.id.create_button);
create_button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = account_edit.getText().toString();
String password = password_edit.getText().toString();
if (("".equals(account) || account == null) && ("".equals(password) || password == null)) {
Toast.makeText(Account.this, "账号密码都不能为空", Toast.LENGTH_SHORT).show();
} else if ("".equals(password) || password == null) {
Toast.makeText(Account.this, "密码不能为空", Toast.LENGTH_SHORT).show();
} else if ("".equals(account) || account == null) {
Toast.makeText(Account.this, "账号不能为空", Toast.LENGTH_SHORT).show();
} else {
LitePal.getDatabase();
Login login = new Login();
login.setAccount(account);
login.setPassword(password);
login.save();
account_edit.setText("");
password_edit.setText("");
Toast.makeText(Account.this, "注册成功,请登录", Toast.LENGTH_SHORT).show();
}
}
});
login_button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = account_edit.getText().toString();
String password = password_edit.getText().toString();
List<Login> list = LitePal.findAll(Login.class);
if (("".equals(account) || account == null) && ("".equals(password) || password == null)) {
Toast.makeText(Account.this, "账号密码都不能为空", Toast.LENGTH_SHORT).show();
} else if ("".equals(password) || password == null) {
Toast.makeText(Account.this, "密码不能为空", Toast.LENGTH_SHORT).show();
} else if ("".equals(account) || account == null) {
Toast.makeText(Account.this, "账号不能为空", Toast.LENGTH_SHORT).show();
} else {
for (Login login : list) {
String a = login.getAccount();
String p = login.getPassword();
if (a.equals(account) && p.equals(password)) {
Intent intent = new Intent(Account.this, MainActivity.class);
startActivity(intent);
finish();
return;
}
}
Toast.makeText(Account.this, "账号或密码不正确", Toast.LENGTH_SHORT).show();
account_edit.setText("");
password_edit.setText("");
}
}
});
}
}
上面就是为之前的登录UI中的每个Button创建了点击事件。我使用LitePal去操作数据库,保存了账号信息,本来是想用账号去做成那种可以一个账号保存一个信息的,但是最后发现知识储备不够,只能做一个本地数据库,所以账号和密码只是能够进入我们的聊天界面。因为它是本地数据库,当我们换个设备之后就没有另一个设备上的信息,我们就要重新创建我们的信息。
这个完成后,我就开始做我的聊天的活动了,这个是花费了我好长的时间。
package com.example.chatui;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.litepal.LitePal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<Msg> msgList = new ArrayList<>();
private String ip = "192.168.1.135";
private int port = 12345;
private Socket socket;
private Button send;
private EditText inputText;
private static RecyclerView msgRecyclerView;
private static MsgAdapter adapter;
private Button clear;
private Button record;
private EditText ip_edit;
private Button clearAll;
private BufferedReader br;
private PrintStream ps;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LitePal.getDatabase();
inputText = (EditText) findViewById(R.id.inputText);
send = (Button) findViewById(R.id.send);
clear = (Button) findViewById(R.id.clear);
record = (Button) findViewById(R.id.record_button);
ip_edit = (EditText) findViewById(R.id.ip_edit);
clearAll = (Button) findViewById(R.id.clearAll);
msgRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
msgRecyclerView.setLayoutManager(layoutManager);
adapter = new MsgAdapter(msgList);
msgRecyclerView.setAdapter(adapter);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
new Thread() {
@Override
public void run() {
try {
socket = new Socket(ip, port);
br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));
ps = new PrintStream(socket.getOutputStream(), true, "GBK");
new Thread(new Receive()).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = inputText.getText().toString();
if (!"".equals(content) && socket != null ) {
try {
String ip = null;
ip = ip_edit.getText().toString();
String time = getCurrentTime();
int i = Msg.TYPE_SEND;
Msg msg = new Msg(content, ip, i, time);
msgList.add(msg);
Record record = new Record();
record.setContent(content);
record.setIp(ip);
record.setTime(time);
record.setType(i);
record.save();
new Thread() {
@Override
public void run() {
ps.println(msg.getContent());
ps.println(msg.getIp());
ps.println(msg.getTime());
}
}.start();
} catch (Exception e) {
e.printStackTrace();
}
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
inputText.setText("");
} else if (socket == null) {
Toast.makeText(MainActivity.this, "没有连接到服务器,请检查是否连接服务器!", Toast.LENGTH_SHORT).show();
}
}
});
clear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
msgList.clear();
adapter.notifyDataSetChanged();
}
});
record.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
msgList.clear();
adapter.notifyDataSetChanged();
List<Record> list = LitePal.findAll(Record.class);
for (Record record : list) {
Msg msg = new Msg(record.getContent(),
record.getIp(), record.getType(),record.getTime());
msgList.add(msg);
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
}
}
});
clearAll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("警告!!!");
dialog.setMessage("你确定要删除所有的聊天记录吗?此操作不能撤回!");
dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
LitePal.deleteAll(Record.class);
msgList.clear();
adapter.notifyDataSetChanged();
}
});
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
dialog.show();
}
});
}
private static String getCurrentTime() {
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
return sdf.format(d);
}
private class Receive implements Runnable {
@Override
public void run() {
while (true) {
try {
String content = br.readLine();
String time = br.readLine();
String IP = br.readLine();
Log.d("receive", content + time);
if (content == null || content == "") {
continue;
} else {
Msg msg = new Msg(content, IP, Msg.TYPE_RECEIVED, time);
msgList.add(msg);
Record record = new Record();
record.setContent(msg.getContent());
record.setTime(msg.getTime());
record.setType(Msg.TYPE_RECEIVED);
record.setIp(msg.getIp());
record.save();
runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
其实这个也没多少内容,但这个确实占用了我做这个项目的大部分时间。首先,要做每个Button的点击事件,这是最基本的,接下来我觉得可能是这个项目最重要的部分了,那就是连接服务器了,我先是在网上找了如何查找自己IP的代码,但是获取到的IP是127.0.0.1,由于要在局域网内进行聊天,肯定不能是一个设备,所以这样不能获取到服务器的IP,最后实在没办法了,就只能使用最笨的办法了,那就是将我的电脑IP直接写进去,这样就直接获取到了我的服务器IP,这样虽然可以,但是没换一个局域网,IP就会发生改变,就需要去重新指定IP,我现在也没有什么好的办法。
还有需要注意的就是操作一些耗时的操作时,要重新开启一条线程,不然就会抛出异常。因为我们发送消息时是按下发送键才会发送消息,这样就有一个依据去什么时候调用发送的指令,但是接受消息没有这样具体的操作,没有人知道什么时候要接受消息,所以我们要一直接收消息,接收消息是一个无限循环,这样不管你什么时候发来的消息,就都可以接收。
还有就是和之前账户一样,存储的聊天记录也是在本地的,还是和之前一样,还是使用LitePal去存储的。我们存储聊天记录时,只需要在两个地方去存储,一个是发送时,另一个就是在接收时。删除聊天记录的操作也很简单,只需要清空数据库中的数据就将聊天记录全部删除了。
服务端
服务端也是非常重要的,首先来看一下服务端的代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
public class Server1 {
private static final int port = 12345;
private static HashMap<String,Socket> hm;
private static ServerSocket server;
public static void main(String[] args) throws IOException {
hm = new HashMap<>();
startServer();
}
public static void startServer() throws IOException {
new Thread() {
@Override
public void run() {
try {
server = new ServerSocket(port);
while (true) {
Socket socket = server.accept();
System.out.println(1);
StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
sb.deleteCharAt(0);
hm.put(sb.toString(), socket);
System.out.println(sb.toString());
handle(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
private static void handle(Socket socket) throws IOException {
new Thread() {
@Override
public void run() {
try {
while (true) {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String content = br.readLine();
String ip = br.readLine();
String time = br.readLine();
if ("255.255.255.255".equals(ip)) {
System.out.println("发给全部人");
String str = null;
for (String sendIP : hm.keySet()) {
try {
StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
sb.deleteCharAt(0);
str = sb.toString();
if (str.equals(sendIP)) {
continue;
}
Socket s = hm.get(sendIP);
PrintStream ps = new PrintStream(s.getOutputStream(), true, "GBK");
ps.println(content);
System.out.println(content);
ps.println(time);
ps.println(str);
} catch (Exception e) {
hm.remove(str);
}
}
System.out.println("发完了");
} else {
Socket s = null;
for (String sendIP : hm.keySet()) {
try {
if (ip.equals(sendIP)) {
s = hm.get(sendIP);
break;
}
} catch (Exception e) {
hm.remove(ip);
}
}
if (s != null) {
StringBuilder sb = new StringBuilder(socket.getInetAddress().toString());
String str = sb.deleteCharAt(0).toString();
PrintStream ps = new PrintStream(s.getOutputStream(), true, "GBK");
ps.println(content);
System.out.println(content);
ps.println(time);
ps.println(str);
System.out.println("发送给" + socket.getInetAddress());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
这是服务端的代码,服务端只需要指定端口号,如何获取服务端的IP并且连接服务端这就不是服务端干的事了,这是客户端应该关心的,服务端只需要想象已经连接到客户端之后该如何操作。我们要判断一下我们客户端时要发送给谁的,就是发送过来的IP,如果是“255.255.255.255”,那就是发送给全部人,然后就将传入的消息除了自己,给连接在服务端上的每个客户端都发送出去。如果是发给某个IP的话,
就找到那个客户端,如果已连接,就将消息发送出去,如果没有连接服务器,就不会将消息发送出去。
还有需要注意的就是编码问题,因为在虚拟机上只能发送字母,没有汉字,发送的消息和接收的消息是一样的,但是如果在真机上测试,我们默认的汉字是“GBK”码表,但是发送后转换成了“utf-8”码表,所以我们要指定码表发送,指定码表接收。
以下是我的客户端全部代码,服务端的代码在上面就是全部了。