索引
- 1.开发契机
- 2.软件概述
- 3.服务器端
- 4.客户端
- 4.1 登录界面:MainActivity
- 4.2 聊天界面:ChatRoom
- 5.特点
1.开发契机
很早之前就想自己做出一个仅实现远程聊天功能,而不带有任何冗余功能的超级轻量化聊天软件。参考了众多开源的聊天软件源代码,发现大部分是使用socket实现了私有网络的通信。当时也是苦恼了一段时间,最终使用了JSON格式对聊天记录进行存储,并通过HTTP协议对JSON数据进行传输,从而实现了这个轻量化的聊天软件。
2.软件概述
该聊天软件不涉及数据库,所有聊天记录以JSON的形式存储于服务器的内存中。用户通过在登录界面登录以后便进入了一个公共的聊天室,可以通过公共网络与任何一个同样使用这个应用的用户进行聊天。以下分别是登录界面和聊天界面。
使用的开发工具及环境:
①Eclipse Java EE IDE for Web Developers. Version: Mars.1 Release (4.5.1)
②Android Studio 3.5.3
3.服务器端
我在本机搭建了Tomcat服务器作为Web应用的容器,使用Servlet来实现聊天的业务逻辑。
由于使用到了JSON数据,所以需要在相应的Web应用项目中的 WEB-INF\lib 文件夹下导入6个jar包定义了一个DiffServlet
package com.chatroom;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
@WebServlet("/DiffServlet")
public class DiffServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static JSONArray messageList = new JSONArray();
public DiffServlet() {
super();
// TODO Auto-generated constructor stub
}
public void init()throws ServletException{ //初始化,创建一个JSON对象列表,
if(messageList.isEmpty()) { //用于存储聊天记录
JSONObject first = new JSONObject();
first.put("name", "Server");
first.put("message", "Server Gets Ready");
messageList.add(first);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 对http请求的Get方法进行响应,为客户端返回所有聊天记录
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(messageList.toString());
out.flush();
out.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 对http请求的Post方法进行响应,将客户端发来的信息添加到聊天记录列表当中
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
String username = request.getParameter("name");
String message = request.getParameter("message");
JSONObject obj = new JSONObject();
obj.put("name", username);
obj.put("message", message);
messageList.add(obj);
}
}
在web.xml对该Servlet进行注册
<servlet>
<servlet-name>DiffServlet</servlet-name>
<servlet-class>com.ChatRoom.DiffServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DiffServlet</servlet-name>
<url-pattern>/ChatRoom/DiffServlet</url-pattern>
</servlet-mapping>
4.客户端
Android客户端包含两个Activity,一个是登录界面MainActivity,一个是聊天界面ChatRoom。所有的活动要在AndroidManifest.xml文件中进行注册,为了使用户在应用中使用输入法使界面的背景图不会被压缩,所以在每个活动标签中加入
android:windowSoftInputMode="adjustPan"
这样一条代码,就可以解决背景图片被压缩的问题了。
由于项目中要使用网络,所以要在AndroidManifest.xml文件中对授权对网络的访问,添加如下代码:
<uses-permission android:name="android.permission.INTERNET"/>
注意
如果我们使用的是http协议的域名,使用Android Studio开发的应用可能会与主机连接不上,出现这种情况可参考6号楼下的大懒喵的博客 OkHttp请求http链接失败的问题
4.1 登录界面:MainActivity
布局文件:
<?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"
android:background="@drawable/picture">
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:hint="用户名"/>
<Button
android:id="@+id/btn_cnt"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:background="#07D0F3"
android:gravity="center"
android:layout_gravity="center"
android:text="连接"
android:textColor="#ffffff" />
</LinearLayout>
源代码:
package com.example.chatroom;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_cnt;
private EditText et_name;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_cnt = (Button) findViewById(R.id.btn_cnt);
et_name = findViewById(R.id.et_name);
btn_cnt.setOnClickListener(MainActivity.this);
}
public void onClick(View view) {
String name = et_name.getText().toString();
if ("".equals(name)) {
Toast.makeText(this, "请输入用户名:", Toast.LENGTH_SHORT).show();
//如果输入的用户名为空的话,那么下端会出现提示
} else {
Intent intent=new Intent(MainActivity.this,ChatRoom.class);
intent.putExtra("username",name);
startActivity(intent);
}
}
}
4.2 聊天界面:ChatRoom
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="#d8e0e8"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/msg_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@drawable/background" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#ffffff"
/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:background="#07D0F3"
android:text="发送"
android:textColor="#ffffff" />
</LinearLayout>
</LinearLayout>
源代码:
package com.example.chatroom;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ChatRoom extends AppCompatActivity implements View.OnClickListener{
private List<Msg> msgList = new ArrayList<>();
private EditText inputText;
private Button send;
private RecyclerView msgRecyclerView;
private MsgAdapter adapter;
boolean isRunning = false;
private boolean isSend=false;
private String myName;
private String responseData;
private int curr; //当前显示的消息条数
private int jsonLen; //获取到的json列表长度
private Handler handler = new Handler(Looper.myLooper()){
//获取当前进程的Looper对象传给handler
//在目前的Android开发中,子线程不能改变UI,
//所以子线程要对UI进行操作需要交给一个Handler对象来执行
@Override
public void handleMessage(Message message){
String message_Name = message.getData().getString("name");
String message_msgC = message.getData().getString("msgContent");
if(!message_msgC.equals("")){
if(message_Name.equals(myName))
addNewMessage(message_msgC, Msg.TYPE_SENT);
else
addNewMessage(message_msgC,Msg.TYPE_RECEIVED);
}
}
};
public void addNewMessage(String msg,int type){
Msg message = new Msg(msg,type);
msgList.add(message);
adapter.notifyItemInserted(msgList.size()-1);
msgRecyclerView.scrollToPosition(msgList.size()-1);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_room);
Intent intent =getIntent();
myName=intent.getStringExtra("username");
curr=0;
jsonLen=1;
isRunning=true;
inputText = findViewById(R.id.input_text);
send=findViewById(R.id.send);
send.setOnClickListener(this);
runOnUiThread(new Runnable() {
@Override
public void run() {
LinearLayoutManager layoutManager = new
LinearLayoutManager(ChatRoom.this);
msgRecyclerView= findViewById(R.id.msg_recycler_view);
msgRecyclerView.setLayoutManager(layoutManager);
adapter = new MsgAdapter(msgList);
msgRecyclerView.setAdapter(adapter);
}
});
new Thread(new Receive(), "接收线程").start();
new Thread(new Send(), "发送线程").start();
}
public void parseJSONWithJSONObject(String jsonData) { //解析JSON数据函数
try {
JSONArray jsonArray = new JSONArray(jsonData);
jsonLen = jsonArray.length();
for (; curr < jsonLen; curr++) {
JSONObject jsonObject = jsonArray.getJSONObject(curr);
String name = jsonObject.getString("name");
String msgContent = jsonObject.getString("message");
Message message = new Message();
Bundle bundle = new Bundle();
bundle.putString("name", name);
bundle.putString("msgContent", msgContent); //往Bundle中存放数据
message.setData(bundle);//mes利用Bundle传递数据
handler.sendMessage(message);//用activity中的handler发送消息
}
} catch (Exception e) {
Looper.prepare();
Toast.makeText(ChatRoom.this, "解析json错误!", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
String msgEntity;
@Override
public void onClick(View view){
String content = inputText.getText().toString();
@SuppressLint("SimpleDateFormat")
String date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
StringBuilder sb = new StringBuilder();
msgEntity = myName;
sb.append(msgEntity).append("\n"+date+"\n"+content);
msgEntity = sb.toString();
if(!"".equals(msgEntity)){
Message message = new Message();
Bundle bundle = new Bundle();
bundle.putString("name", myName);
bundle.putString("msgContent", msgEntity); //往Bundle中存放数据
message.setData(bundle);//mes利用Bundle传递数据
handler.sendMessage(message);//用activity中的handler发送消息
inputText.setText("");
isSend = true;
curr++;
}
sb.delete(0,sb.length());
}
class Send implements Runnable{
@Override
public void run(){ //发送线程
while(isRunning){
if(isSend){
RequestBody requestBody = new FormBody.Builder()
.add("name",myName)
.add("message",msgEntity)
.build();
try {
OkHttpClient client = new OkHttpClient();
Request request2 = new Request.Builder()
// 指定访问的服务器地址
.url(Resource.DiffUrl).post(requestBody)
.build();
Response response = client.newCall(request2).execute();
String responseData = response.body().string();
isSend = false;
} catch (Exception e) {
Looper.prepare();
Toast.makeText(ChatRoom.this, "发送失败!",
Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
}
}
}
class Receive implements Runnable{
public void run(){
while(isRunning){
if(!isSend) {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// 指定访问的服务器地址
.url(Resource.DiffUrl).get()
.build();
//Resource.DiffUrl为DiffSevlet的URL地址
//其需要根据你的服务端Servlet的URL地址进行修改
Response response = client.newCall(request).execute();
String responseData = response.body().string();
if (responseData != null && responseData.startsWith("\ufeff")){
responseData = responseData.substring(1);
}
parseJSONWithJSONObject(responseData);
} catch (Exception e) {
Looper.prepare();
Toast.makeText(ChatRoom.this, "连接服务器失败!!!",
Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
}
}
}
需要在build.gradle中添加库依赖
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okio:okio:2.5.0'
Msg实体类以及MsgAdapter类的相关代码参考衣侠客的博客 Android聊天室(客户端)
5.特点
①由于该软件是通过动态维护一个JSON对象列表来存储聊天记录,所以服务器端不静态存储任何用户信息,所有信息都是存储在服务器内存当中,一旦关闭服务器,所有当前的聊天记录都会消失
②聊天记录可以通过浏览器查看。通过浏览器使用URL对Servlet进行访问使用的是Get方法,该软件则调用了Web端的doGet()方法,获取了JSON数据形式的聊天记录。
Github
https://github.com/kengkengkeng41/ChatRoom