基于WebSocket和Android的多人实时聊天系统(Demo)的实现

实现效果
在这里插入图片描述

服务端实现

服务端参考之前的这篇文件Android初学 使用WebSocket与服务器进行通信.
这里做了一些修改, 就是再服务端收到消息后, 将消息群发给所有在线的客户端

// 服务端的Bean实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgBean {
    private String mUserName;
    private String mMsg;
}
// 服务端的WebSocketClient
// 客户端可以通过 ws://ip:port/test 进行connnect
@ServerEndpoint("/test")
@Component
@Slf4j
public class WebSocketController {
    /**
     * 存放所有在线的客户端
     */
    private static Map<String, Session> clients = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        log.info("有新的客户端连接了: {}", session.getId());
        //将新用户存入在线的组
        clients.put(session.getId(), session);
    }

    /**
     * 客户端关闭
     *
     * @param session session
     */
    @OnClose
    public void onClose(Session session) {
        log.info("有用户断开了, id为:{}", session.getId());
        // 将掉线的用户移除在线的组里
        clients.remove(session.getId());
    }

    /**
     * 发生错误
     *
     * @param throwable e
     */
    @OnError
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }

    /**
     * 收到客户端发来消息
     *
     * @param message 消息对象
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("服务端收到客户端发来的消息: {}", message);
        this.sendAll(message);
    }

    /**
     * 群发消息
     *
     * @param message 消息内容
     */
    private void sendAll(String message) {
        for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
            sessionEntry.getValue().getAsyncRemote().sendText(message);
        }
    }
}

启动SpringBoot之后, 可以先使用PostMan测试链接是否成功

客户端实现

主页面的实现

首先写聊天页面的布局, 因为使用了binding, 所以布局长这个样子
主要组件的一个RecycleView, EditTextButton

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/chat_background"
            tools:context=".fragment.ChatFragment">

        <androidx.constraintlayout.widget.Guideline
                android:orientation="horizontal"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintGuide_percent="0.9"
                android:id="@+id/guideline2" />

        <EditText
                android:layout_width="230dp"
                android:hint="..."
                android:layout_height="38dp"
                android:text=""
                android:background="#99FFFFFF"
                android:id="@+id/msg_edit_text"
                app:layout_constraintEnd_toStartOf="@+id/guideline3"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="@+id/guideline2"
                app:layout_constraintBottom_toBottomOf="parent"
                android:minHeight="48dp"
                android:layout_marginStart="10dp"
                android:layout_marginEnd="10dp" />

        <androidx.constraintlayout.widget.Guideline
                android:orientation="vertical"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintGuide_begin="266dp"
                android:id="@+id/guideline3"
                app:layout_constraintGuide_percent="0.7" />

        <Button
                android:text="发送"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/send_btn"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="@+id/guideline3"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toTopOf="@+id/guideline2" />

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/msg_list_recycle_view"
                android:layout_width="350dp"
                android:scrollbars="vertical"
                android:layout_height="600dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toTopOf="@+id/guideline2"
                android:layout_marginStart="10dp"
                android:layout_marginEnd="10dp"
                android:layout_marginBottom="20dp"
                android:layout_marginTop="20dp" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

上面的布局的background设置的是@drawable/chat_background, drawable/chat_background.xml文件内容如下

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
        android:src="@drawable/zhouye"
        android:gravity="center_vertical" />

这样做的目的是为了保持图片的纵横比.

实现效果图如下:
在这里插入图片描述

消息的布局

QQ中的每条消息, 大概长这个样子 这里只简单实现一下
在这里插入图片描述
每条消息包含两个TextView, 分别为用户名和消息, 布局文件如下

<?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"
        android:layout_width="match_parent"
        android:layout_gravity="left"
        android:layout_height="wrap_content">

    <TextView
            android:text="ZhangSan"
            android:textSize="20sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/user_name"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <TextView
            android:text="HiHiHiHiHiiHiHHiHiHiHiHiHiHiHi"
            android:layout_width="wrap_content"
            android:textSize="15sp"
            android:layout_height="wrap_content"
            android:id="@+id/user_msg"
            android:background="@drawable/msg_background"
            app:layout_constraintTop_toBottomOf="@+id/user_name"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="10dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

为了实现消息的圆角和背景色, 这里设置了android:background="@drawable/msg_background", 其中drawable/msg_background的文件内容如下

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
    <!--    填充色-->
    <solid android:color="@color/teal_200" />
    <!--    圆角弧度-->
    <corners android:radius="10dp" />
    <!--    四周内边距-->
    <padding
            android:left="20px"
            android:right="20px"
            android:bottom="15px"
            android:top="15px" />
    <!--    边框颜色-->
    <stroke
            android:color="@color/teal_700"
            android:width="1dp" />
</shape>

整体的每条消息的布局效果如下图
在这里插入图片描述

先写一下Service

因为是写完功能之后才写的总结, 所以描述有点混乱...

为了保证应用在后台的时候也能够接收到WebSocket发送的消息, 将WebSocketClient放在了Service中. 同时使用心跳机制保证连接.

心跳机制最开始是在大数据中听说的, 服务器集群的master会定时给每个node发送心跳包, 检测该节点是否掉线, 如果掉线会进行一些数据的恢复等其他操作. 本文的心跳指的是检测一下与服务之间的连接状态. 如果连接失败, 则进行重连

Service的完整代码如下

public class WebSocketService extends Service {
    private static final String TAG = WebSocketClient.class.getSimpleName();
    // 发送心跳Message 在handle中的what, 
    private static final int WEB_SOCKET_HEART_BERT = 0x01;
    // WebSocket的Client
    WebSocketClient mChatSocketClient = null;
    // 与服务器通信的WebSocket的URI
    private final URI mURI = URI.create("ws://192.168.69.205:8080/test");
    private Handler mHandler;
    // bind service 之后, 会返回这个对象, 消息的发送和接收都是通过这个对象来实现的
    private final SocketBinder mSocketBinder = new SocketBinder();

    public WebSocketService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mHandler = new WebSocketHandler(getMainLooper());
        initWebSocket();
    }

    private void initWebSocket() { // 初始化WebSocket Client
        mChatSocketClient = new WebSocketClient(mURI) {
            @Override
            public void onOpen(ServerHandshake handshakedata) { Log.e(TAG, "onOpen: "); }

            @Override
            public void onMessage(String message) {
                Log.e(TAG, "mChatSocketClient onMessage: " + message);
                // 调用Binder中的onMessage, binder中通过CallBack将消息发送给Fragment
                mSocketBinder.onMessage(message);
            }

            @Override
            public void onClose(int code, String reason, boolean remote) { Log.e(TAG, "onClose: " + "close"); }

            @Override
            public void onError(Exception ex) { Log.e(TAG, "onError: " + ex.getMessage()); }
        };
        mChatSocketClient.connect();
        // 连接之后就启动心跳
        webSocketHeartBeat();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mSocketBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.e(TAG, "onUnbind: ");
        if (mChatSocketClient != null) {
            mChatSocketClient.close();
        }
        return super.onUnbind(intent);
    }

    public class SocketBinder extends Binder {
    	// 获取此对象后需要调用setSocketCallBack, 实现此接口, 然后通过回调来接收Socket接收到的消息
    	// 异步回调
    	// 这个回调也可以放在WebSocketService中, 然后再此onMessage被调用的时候, 通过Handle发送一个Message, 在Handle中回调接收到的消息. 
        private SocketCallBack mSocketCallBack;

        public void sendMsg(String msg) {
            mChatSocketClient.send(msg);
        }

        public void sendMsgBean(MsgBean msgBean) {
            mChatSocketClient.send(JSONObject.toJSONString(msgBean));
        }

        public void onMessage(String message) {
            Log.e(TAG, "SocketBinder onMessage: " + message);
            mSocketCallBack.onMessage(message);
        }

        public void setSocketCallBack(SocketCallBack socketCallBack) {
            mSocketCallBack = socketCallBack;
        }

        public SocketCallBack getSocketCallBack() {
            return mSocketCallBack;
        }
    }

    public interface SocketCallBack {
        void onMessage(String message);
    }


    class WebSocketHandler extends Handler {

        public WebSocketHandler(@NonNull Looper looper) {
            super(looper);
        }

        public WebSocketHandler(@NonNull Looper looper, @Nullable Callback callback) {
            super(looper, callback);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what) {
                case WEB_SOCKET_HEART_BERT:
                    Log.d(TAG, "handleMessage: WEB_SOCKET_HEART_BERT");
                    if (mChatSocketClient != null && mChatSocketClient.isClosed()) {
                        reconnectWebSocket();
                    } else {
                        webSocketHeartBeat();
                    }
                    break;
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    }

    private void webSocketHeartBeat() { // 心跳, 发送一个任务 延迟5s执行
        Log.d(TAG, "webSocketHeartBeat: ");
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {// 判断websocket的连接状态, 如果连接关闭 则重连, 否则, 给Handle发送一条消息, handle收到这个消息后还会判断连接状态, 如果连接失败则重连, 连接成功则再次执行webSocketHeartBeat, 发送一个任务 延迟5s执行...
                if (mChatSocketClient.isClosed()) {
                    reconnectWebSocket();
                }
                Message msg = new Message();
                msg.what = WEB_SOCKET_HEART_BERT;
                mHandler.sendMessage(msg);
            }
        }, 5000);
    }

    private void reconnectWebSocket() {// 重连 重连之后发送Message, 在Handle中检测连接是否成功...
        Log.d(TAG, "reconnectWebSocket: ");
        if (mChatSocketClient != null) {
            mChatSocketClient.reconnect();
        } else {
            Log.e(TAG, "reconnectWebSocket: mChatSocketClient is null");
        }
        Message msg = new Message();
        msg.what = WEB_SOCKET_HEART_BERT;
        mHandler.sendMessage(msg);
    }
}

Socket重连流程如下
在这里插入图片描述
在这里插入图片描述

接下来在Fragment中绑定服务

public class ChatFragment extends Fragment {
    private static final String TAG = ChatFragment.class.getSimpleName();
    private FragmentChatBinding mChatBinding;
    private WebSocketService.SocketBinder mSocketBinder;
    private MsgListRecycleViewAdapter mAdapter;
    private Handler mHandler;

    public ChatFragment() {
    }

    public static ChatFragment newInstance() { // 获取Fragment实例
        ChatFragment fragment = new ChatFragment();
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        startService();
        mHandler = new ChatHandler(Looper.getMainLooper());
        mChatBinding = FragmentChatBinding.inflate(getLayoutInflater());
        List<MsgBean> list = new ArrayList<>();
        mAdapter = new MsgListRecycleViewAdapter(list);
        // 设置RecycleView的布局管理器
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
        linearLayoutManager.setStackFromEnd(true);// 自动滑动尾部
        mChatBinding.msgListRecycleView.setLayoutManager(linearLayoutManager);
        mChatBinding.msgListRecycleView.setAdapter(mAdapter);
    }

    private void startService() { // 启动Service
        FragmentActivity activity = getActivity();
        if (activity != null) {
            activity.bindService(new Intent(getContext(), WebSocketService.class),
                    serviceConnection, Context.BIND_AUTO_CREATE);
        } else {
        }
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return mChatBinding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mChatBinding.sendBtn.setOnClickListener(view -> { // 发送按钮的点击事件
            String msg = mChatBinding.msgEditText.getText().toString();
            if (msg.equals("")) {
                Toast.makeText(getContext(), "请输入消息", Toast.LENGTH_SHORT).show();
            } else {
                MsgBean msgBean = new MsgBean(MsgBean.name, msg);
                mSocketBinder.sendMsgBean(msgBean);
            }
        });
    }
	
	// 匿名类 实现WebSocketService.SocketCallBack接口
    private final WebSocketService.SocketCallBack mSocketCallBack = new WebSocketService.SocketCallBack() {
        @Override
        public void onMessage(String message) { // WebSocket收到消息后会通过回调将消息发送过来
            Log.e(TAG, "mSocketCallBack: " + message);
            MsgBean msgBean = JSONObject.parseObject(message, MsgBean.class);
            mAdapter.addMsgBean(msgBean);
            Message handleMsg = new Message();// 在Handle中更新UI
            handleMsg.what = 0x01;
            mHandler.sendMessage(handleMsg);
        }
    };

	// 匿名类 实现ServiceConnection接口, 实现绑定成功/断开连接的回调方法
    private final ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) { //服务与活动成功绑定之后会回调此方法
            mSocketBinder = (WebSocketService.SocketBinder) iBinder;
            mSocketBinder.setSocketCallBack(mSocketCallBack);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) { Log.e("MainActivity", "服务与活动成功断开"); }
    };

    class ChatHandler extends Handler { // 更新UI用的
        public ChatHandler(@NonNull Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 0x01:
                    Log.e(TAG, "handleMessage: 0x01");
            		// 将RecyclerView定位到最后一行
                    mAdapter.notifyItemInserted(mAdapter.getItemCount() - 1);
                    mChatBinding.msgListRecycleView.smoothScrollToPosition(mAdapter.getItemCount() - 1);
                    Log.e(TAG, "handleMessage: 0x01 end");
                    break;
                default:
                    break;
            }
        }
    }
}

发送消息和接收消息的时序图如下:
在这里插入图片描述

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值