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数据形式的聊天记录。

项目下载:
CSDN链接
https://download.csdn.net/download/qq_43599564/12553464
Github
https://github.com/kengkengkeng41/ChatRoom

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐