赞
踩
实现效果
服务端参考之前的这篇文件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
, EditText
和Button
<?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>
整体的每条消息的布局效果如下图
因为是写完功能之后才写的总结, 所以描述有点混乱...
为了保证应用在后台的时候也能够接收到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重连流程如下
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; } } } }
发送消息和接收消息的时序图如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。