socketio依赖及版本
- server: netty-socketio v1.7.17
- client-java: socket.io-client-java v1.0.0
- client-js: socket.io-client-js v2.1.1
关键代码
Message javaBean
@Data
public class MyMessage {
private String username;
private Integer age;
private String content;
}
server端 代码片段
// 监听事件
@OnEvent("message")
public void onMessage(SocketIOClient client, MyMessage message) {
log.info("接收消息: sessionId={}, message={}", client.getSessionId(), message.toString());
}
client-js端 代码片段
var msg = {
username: "icetea",
age: 18,
content: "my name is icetea."
};
socket.emit('message', msg);
client-java端 代码片段
MyMessage msg = new MyMessage("icetea", 18, "my name is icetea.");
socket.emit("message", msg); // 直接传javaBean不行
// 或者 socket.emit("message", JSON.toJSONString(msg)); javaBean转为json string也不行
问题描述
使用socketio js客户端时可以正常解析参数,但使用 socketio java客户端发送java bean消息时,netty-socketio无法解析传来的参数,并且将javabean转换成json字符串也会报错
报错详情
ERROR 11236 — [ntLoopGroup-3-3] c.c.socketio.JsonSupportWrapper: Can’t read value: [“message-model”,“MyMessage(username=icetea, age=18, content=my name is icetea.)”] for type: class com.corundumstudio.socketio.protocol.Event
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of
life.icetea.test.nettysocketio.domain.MyMessage
(although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value (‘MyMessage(username=icetea, age=19, content=my name is icetea.)’)
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 18]
问题解析
查看netty-socketio官方仓库的issues发现有相同的问题,https://github.com/mrniko/netty-socketio/issues/258, 里面有人说是client传参序列化的问题,并且给出了解决方案。如下:
参考1:https://github.com/mrniko/netty-socketio/issues/258#issuecomment-417607871
//Android client use json parser is "org.json",when emit message,you should like this:
ChatEvent data = new ChatEvent();
mSocket.emit("message", new JSONObject(data));
// 该方法中的org.json的JSONObject构造只能传递一个map类型
参考2:https://github.com/mrniko/netty-socketio/issues/258#issuecomment-449638424
//try and convert the object you are trying to send to a string using GSON library
mSocket.emit("message", new JSONObject(new Gson().toJson(data)));
// 该方法使用Gson所以需要引入Gson依赖
参考3: https://github.com/mrniko/netty-socketio/issues/258#issuecomment-465497609
此回答解释了为什么js-client会成功而java-client会失败的原因,因为java-client在序列化参数时存在问题。
参考后得出的解决方法
参考前两个回答,都是将javaBean转成对应依赖(Gson和org.json)的Json对象,因为我的项目使用的是fastJson,而fastJson也有对应的JSONObject,所以照葫芦画瓢,示例如下:
MyMessage msg = new MyMessage("icetea", 18, "my name is icetea.");
socket.emit("message", JSON.toJSON(msg));
// 该方法能够让server正常接收参数
总结
找到解决方法,然后来详细总结一下为什么在socket.emit()方法中直接传递javabean或者传递json的字符串不行,debug找到socketio-java-client底层序列化参数的源码片段如下:
// io.socket.client.Socket#emit(java.lang.String, java.lang.Object[], io.socket.client.Ack)
// ······
// 要发送的参数都放到了org.json.JSONArray对象
JSONArray jsonArgs = new JSONArray();
jsonArgs.put(event);
if (args != null) {
for (Object arg : args) {
jsonArgs.put(arg);
}
}
// 将JSONArray参数封装成packet
Packet<JSONArray> packet = new Packet<JSONArray>(Parser.EVENT, jsonArgs);
// ······
从以上片段看出要发送的参数都放到了org.json.JSONArray对象中并封装成Packet,然后再往下debug找到了将参数encode成String的方法,代码片段如下:
// io.socket.parser.IOParser.Encoder#encodeAsString
// ······
private String encodeAsString(Packet obj) {
StringBuilder str = new StringBuilder("" + obj.type);
// ······
if (obj.data != null) {
str.append(obj.data);
}
// ······
return str.toString();
}
//
从以上片段看出,Packet有被解析成一串字符串,且要传送的参数是字符串中的一部分,然后往下debug就是直接send方法。到此参数封装结束。那么从以上分析得出,java-client的参数封装和JSONArray这个类有很大关系,所以可以测试一下JSONArray的toString方法看看展示出的结果是什么样子:
public static void main(String[] args) {
MyMessage msg = new MyMessage("icetea", 18, "my name is icetea.");
// 直接put java bean
JSONArray jsonArray = new JSONArray();
jsonArray.put("message");
jsonArray.put(msg);
System.out.println(jsonArray);
// 转成fastJson的JSONObject
JSONArray jsonArray1 = new JSONArray();
jsonArray1.put("message");
jsonArray1.put(JSON.toJSON(msg));
System.out.println(jsonArray1);
// 使用Map测试一下,因为fastJson的JSONObject其实就是一个Map
JSONArray jsonArray2 = new JSONArray();
jsonArray2.put("message");
HashMap<String, Object> map = new HashMap<>();
map.put("username", "icetea");
map.put("age", 18);
map.put("content", "my name is icetea.");
jsonArray2.put(map);
System.out.println(jsonArray2);
// 将javaBean转成json string测试
JSONArray jsonArray3 = new JSONArray();
jsonArray3.put("message");
jsonArray3.put(JSON.toJSONString(msg));
System.out.println(jsonArray3);
}
// 输出如下:
["message","MyMessage(username=icetea, age=18, content=my name is icetea.)"]
["message",{"age":18,"content":"my name is icetea.","username":"icetea"}]
["message",{"age":18,"content":"my name is icetea.","username":"icetea"}]
["message","{\"age\":18,\"content\":\"my name is icetea.\",\"username\":\"icetea\"}"]
从测试结果可以看出,第二种方式(转成JSONObject)和第三种方式(使用Map封装参数)得出的结果符合json字符串的标准,且能够被netty-socketio反序列化成javaBean。
总结的出另一种方式
// 注意:使用map方式属性必须都是基本类型,不能嵌套有javaBean,否则还会报错,所以推荐使用JSONObject方式
Map<String, Object> msg = new HashMap<>();
msg.put("username", "icetea");
msg.put("age", 18);
msg.put("content", "my name is icetea.");
socket.emit("message", msg);