一、利用WebSocket传输消息
文本与图片的即时通信都可以由SocketIO实现,看似它要一统即时通信了,可是深究起来会发现SocketIO存在很多局限,包括但不限于下列几点:
(1)SocketIO不能直接传输字节数据,只能重新编码成字符串后(比如BASE64编码)再传输,造成了额外的系统开销。
(2)SocketIO不能保证前后发送的数据被收到时仍然是同样顺序,如果业务要求实现分段数据的有序性,开发者就得自己采取某种机制确保这种有序性。
(3)SocketIO服务器只有一个main程序,不可避免地会产生性能瓶颈。倘若有许多通信请求奔涌过来,一个main程序很难应对。
为了解决上述几点问题,业界提出了一种互联网时代的Socket协议,名叫WebSocket。它支持在TCP连接上进行全双工通信,这个协议在2011年被定为互联网的标准之一,并纳入HTML5的规范体系。
相对于传统的HTTP与Socket协议来说,WebSocket具备以下几点优势:
(1)实时性更强,无须轮询即可实时获得对方设备的消息推送。
(2)利用率更高,连接创建之后,基于相同的控制协议,每次交互的数据包头部较小,节省了数据处理的开销。
(3)功能更强大,WebSocket定义了二进制帧,使得传输二进制的字节数组不在话下。
(4)扩展更方便,WebSocket接口被托管在普通的Web服务之上,跟着Web服务方便扩容,有效规避了性能瓶颈。
WebSocket不仅拥有如此丰富的特性,而且用起来也特别简单。
先在服务端的WebSocket编程,除了引入它的依赖包javaee-api-8.0.1.jar,服务器添加相关代码如下:
package com.websocket.server;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint("/testWebSocket")
public class WebSocketServer {
// 存放每个客户端对应的WebSocket对象
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
private Session mSession; // 当前的连接会话
// 连接成功后调用
@OnOpen
public void onOpen(Session session) {
System.out.println("WebSocket连接成功");
this.mSession = session;
webSocketSet.add(this);
}
// 连接关闭后调用
@OnClose
public void onClose() {
System.out.println("WebSocket连接关闭");
webSocketSet.remove(this);
}
// 连接异常时调用
@OnError
public void onError(Throwable error) {
System.out.println("WebSocket连接异常");
error.printStackTrace();
}
// 收到客户端消息时调用
@OnMessage
public void onMessage(String msg) throws Exception {
System.out.println("接收到客户端消息:" + msg);
for(WebSocketServer item : webSocketSet){
item.mSession.getBasicRemote().sendText("我听到消息啦“"+msg+"”");
}
}
}
启动服务器的Web工程,便能通过形如ws://localhost:8080/HttpServer/testWebSocket这样的地址访问WebSocket。
App端的WebSocket编程,由于WebSocket协议尚未纳入JDK,因此要引入它所依赖的jar包tyrus-standalone-client-1.17.jar。
代码方面则需自定义客户端的连接任务,注意给任务类添加注解@ClientEndpoint,表示该类属于WebSocket的客户端任务。任务内部需要重写onOpen(连接成功后调用)、processMessage(收到服务端消息时调用)、processError(收到服务端错误时调用)三个方法,还得定义一个向服务端发消息的发送方法,消息内容支持文本与二进制两种格式。
下面是处理客户端消息交互工作的示例代码:
import android.app.Activity;
import android.util.Log;
import javax.websocket.*;
@ClientEndpoint
public class AppClientEndpoint {
private final static String TAG = "AppClientEndpoint";
private Activity mAct; // 声明一个活动实例
private OnRespListener mListener; // 消息应答监听器
private Session mSession; // 连接会话
public AppClientEndpoint(Activity act, OnRespListener listener) {
mAct = act;
mListener = listener;
}
// 向服务器发送请求报文
public void sendRequest(String req) {
Log.d(TAG, "发送请求报文:"+req);
try {
if (mSession != null) {
RemoteEndpoint.Basic remote = mSession.getBasicRemote();
remote.sendText(req); // 发送文本数据
// remote.sendBinary(buffer); // 发送二进制数据
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 连接成功后调用
@OnOpen
public void onOpen(final Session session) {
mSession = session;
Log.d(TAG, "成功创建连接");
}
// 收到服务端消息时调用
@OnMessage
public void processMessage(Session session, String message) {
Log.d(TAG, "WebSocket服务端返回:" + message);
if (mListener != null) {
mAct.runOnUiThread(() -> mListener.receiveResponse(message));
}
}
// 收到服务端错误时调用
@OnError
public void processError(Throwable t) {
t.printStackTrace();
}
// 定义一个WebSocket应答的监听器接口
public interface OnRespListener {
void receiveResponse(String resp);
}
}
App的活动代码,依次执行下述步骤就能向WebSocket服务器发送消息:获取WebSocket容器→连接WebSocket服务器→调用WebSocket任务的发送方法。其中前两步涉及的初始化代码如下:
private AppClientEndpoint mAppTask; // 声明一个WebSocket客户端任务对象
// 初始化WebSocket的客户端任务
private void initWebSocket() {
// 创建文本传输任务,并指定消息应答监听器
mAppTask = new AppClientEndpoint(this, resp -> {
String desc = String.format("%s 收到服务端返回:%s",
DateUtil.getNowTime(), resp);
tv_response.setText(desc);
});
// 获取WebSocket容器
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
try {
URI uri = new URI(SERVER_URL); // 创建一个URI对象
// 连接WebSocket服务器,并关联文本传输任务获得连接会话
Session session = container.connectToServer(mAppTask, uri);
// 设置文本消息的最大缓存大小
session.setMaxTextMessageBufferSize(1024 * 1024 * 10);
// 设置二进制消息的最大缓存大小
//session.setMaxBinaryMessageBufferSize(1024 * 1024 * 10);
} catch (Exception e) {
e.printStackTrace();
}
}
因为WebSocket接口仍为网络操作,所以必须在子线程中初始化WebSocket,启动初始化线程的代码如下所示:
// 启动线程初始化WebSocket客户端
new Thread(() -> initWebSocket()).start();
同理,发送WebSocket消息也要在子线程中操作,启动消息发送线程的代码如下:
// 启动线程发送文本消息
new Thread(() -> mAppTask.sendRequest(content)).start();
最后确保后端的Web服务正在运行,再运行并测试该App,在编辑框输入待发送的文本,此时交互界面如图【成功发送WebSocket消息】所示: