编辑:我回答了反序列化(如果您打算对服务器来自的结果做一些事情,您可能仍然需要反序列化),但您要求进行序列化。第一部分仍然是请求的反序列化,但在休息之后,我将展示如何序列化请求,再次使用与我们在这个问题中所拥有的信息一样有限的信息
请求反序列化
它可能比文档中的示例大,但适用相同的规则。对于像这样的请求,我们将其分解如下,在| 字符上进行拆分。
7
第 7 版。
0
没有设置标志。
48
接下来的 48 个标记是字符串表,用它们构建一个数组。这些字符串将代表所传递数据的类型,以及实际的 Java 字符串。
所以我们将字符串读入 String[48],然后所有剩余的数字要么是引用,要么是原语。像ZRCrAA 和VnTkM$A 这样的数据可能是一个base64 编码的long。也将其视为字符串列表,我们将在开始从数据中请求对象时使用它。
正如文档所述,我们现在从第二个列表中读取引用(以1|2|3|4|1|5|6|7|0|0|8... 开头)。随着我们继续反序列化,我们还需要四个东西:应用程序的 url、策略的强名称、我们将要调用的服务类以及该服务类中的方法名称。
由于这些都是字符串对象,我们将读取字符串。在 com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader 中,实际的 Java 类会读取这个,我们看到 readString() 看起来像这样:
@Override
public String readString() throws SerializationException {
return getString(readInt());
}
所以首先我们读取一个 int,然后用它来查找一个字符串。这是getString:
@Override
protected String getString(int index) {
if (index == 0) {
return null;
}
// index is 1-based
assert (index > 0);
assert (index <= stringTable.length);
return stringTable[index - 1];
}
现在,我们的第一条数据是基本 url,读取为字符串。我们看到 1 是我们读取的 int,然后我们在上面创建的 String[48] 中获得第 (1 - 1) 个字符串:
https://aps2.senasa.gov.ar/embalaje-madera-web/embalajeApp/
接下来是强策略名称2,我们将其解读为
03152A2DEBABDCE5D33BF4C88511DD1E
服务器将使用它并找到一个名为 03152A2DEBABDCE5D33BF4C88511DD1E.gwt.rpc 的文件,其中概述了可以在服务器上创建的内容的安全策略(以禁止黑客在您的服务器上创建任何类型的对象)。
接下来我们寻找服务类,3:
net.customware.gwt.dispatch.client.standard.StandardDispatchService
最后是调用的方法,4:
执行
从这里,你需要知道StandardDispatchService.execute是什么——我们知道它只需要一个参数,可能是一个Action实例,因为我们看到1表示一个参数,然后5,如果解码为一个 Object 意味着我们读取了第 5 个字符串(看看 readObject 是如何工作的,看看为什么)。在不知道 Action 或其他类有哪些字段(如下所列)的情况下,我们无法确定接下来会发生什么:
- net.customware.gwt.dispatch.shared.Action
- gov.senasa.embalajemadera.shared.rpc.actions.IngresarDeclaracionJuradaAction
- gov.senasa.embalajemadera.shared.domain.DeclaracionJurada
- gov.senasa.embalajemadera.shared.domain.TipoEmbalajeCantidad
- gov.senasa.embalajemadera.shared.domain.TipoEmbalaje
- gov.senasa.embalajemadera.shared.domain.Contenedor
- gov.senasa.embalajemadera.shared.domain.Despachante
(请注意,我只是猜测这些是可序列化的类,它们的包和名称 - 它们可能只是某人决定通过网络发送的纯字符串,作为其请求的一部分!)
请求序列化
我们已经介绍了许多试图分解此消息的基础知识,因此让我们尝试将其组合在一起。 GWT 客户端代码中管理它的类是com.google.gwt.user.client.rpc.impl.ClientSerializationStreamWriter。 prepareToWrite() 和 toString() 方法有助于搭建舞台,展示我们将围绕工作做的第一件事和最后一件事:
/**
* Call this method before attempting to append any tokens. This method
* implementation <b>must</b> be called by any overridden version.
*/
@Override
public void prepareToWrite() {
super.prepareToWrite();
encodeBuffer = new StringBuilder();
// Write serialization policy info
writeString(moduleBaseURL);
writeString(serializationPolicyStrongName);
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
writeHeader(buffer);
writeStringTable(buffer);
writePayload(buffer);
return buffer.toString();
}
prepareToWrite() 方法以添加两个字符串开始流 - 模块基本 url 和策略强名称,您将从反序列化过程中识别出的字符串。 toString() 方法显示了我们将写出的三个阶段:标头、字符串表和“有效负载”,或对象引用和原始值。
为什么我们跟踪的字符串与其他叶值不同?这样,我们将所有字符串放在一个位置,这样我们就可以多次引用它们,并且每次只发送一次。与 XML 或 JSON 相比,每次您想使用一个值时,您都必须重新写入该值,即使它完全相同。
标头包含版本(最新为 7)和要设置的标志(在您的示例中,只有 0 就可以了)。
在超类AbstractSerializationStreamWriter中,有四个字段:
private int objectCount;
private Map<Object, Integer> objectMap = new IdentityHashMap<Object, Integer>();
private Map<String, Integer> stringMap = new HashMap<String, Integer>();
private List<String> stringTable = new ArrayList<String>();
第一个是每个对象的当前索引 - 我们将使用它来跟踪我们以前见过的对象。接下来objectMap,这样我们就可以检查每个对象并检查我们以前是否见过它,如果是,在哪里,这样我们就可以写回那个位置的引用。 stringMap 字段做同样的事情,但对于字符串 - JS 特别对待字符串键。最后,stringTable 本身,我们见过的所有字符串的列表,每个只添加一次。
如果您为 List<String> filterStrings(List<String> strings, String startsWith) 之类的服务方法拆分已编译应用程序生成的 Java,您将看到如下内容:
ClientSerializationStreamWriter streamWriter = ...;//create with serializer
streamWriter.prepareToWrite();
streamWriter.writeString("com.acme.project.shared.MyService");//service interface
streamWriter.writeString("filterStrings");//method name
streamWriter.writeInt(2);//number of arguments to be found in the stream
streamWriter.writeObject(strings);
streamWriter.writeString(startsWith);
确切地知道每个方法会写什么取决于知道 Java 中的方法签名是什么 - 仅使用编译的 GWT JS 和有效负载示例,逆向工程有点困难。但让我们继续,看看接下来会发生什么。
writeObject 的实现获取对象并首先记录其类型。如果对象为空,那么我们只需要写一个空字符串(a.k.a.0)就可以了。否则,我们检查我们之前是否已经写过这个对象(因此写一个负数来查看有效载荷中的位置),或者我们需要查找如何编写该对象的其余部分,并序列化每个字段。
每个可以序列化的对象都必须有一个 FieldSerializer,它描述了如何对该对象进行编码和解码。 GWT 中有很多 CustomFieldSerializer,用于特定目的的自定义实现,它们告诉 RPC 不要自动生成序列化器。一个例子可能是 ArrayList,如果我们将它传递给 - ArrayList_CustomFieldSerializer 代表 Collection_CustomFieldSerializerBase,它会这样做:
public static void serialize(SerializationStreamWriter streamWriter,
Collection instance) throws SerializationException {
int size = instance.size();
streamWriter.writeInt(size);
for (Object obj : instance) {
streamWriter.writeObject(obj);
}
}
首先我们写入列表的大小,以便反序列化器知道要读取多少个元素,然后我们写入列表中的每个项目。在我们的例子中,我们将这些都写成字符串。然后,我们将编写一个 more 字符串,作为方法的第二个参数。
所以,我们的字符串表中有这些数据:
- baseUrl
- 策略字符串名称
- “com.acme.project.shared.MyService”
- “过滤器字符串”
- “java.util.ArrayList”
- “java.lang.String”
- 参数
strings中的每个字符串,以及startsWith中的字符串,但由于我们不知道是否有重复,我们无法知道是否会有相同数量的字符串。李>
在我们的有效负载中,假设我们调用了filterStrings(["a", "ab", "abc", "a"], "ab"),我们将有引用 1(baseurl 字符串索引)、2(策略强名称字符串索引)、3(服务名称字符串索引)、4(方法名称字符串索引), 2 (int 表示期望的字段数), 5 (strings 列表参数的类名), 4 (列表中的项目数), 6 (列表中第一项的类型, 字符串), 7(“a”的内容)、6(字符串类型)、8(“ab”的内容)、6(字符串类型)、9(“abc”的内容)、6(字符串类型)、7(“的内容” a")、6(第二个参数的字符串类型),最后是 8(又是 "ab" 的内容)。
这对于 Action、DeclaracionJurada 等类会如何?在不知道有哪些字段以及它们发生的顺序的情况下,我们无法确定。仅从有效负载中重建内容没有好方法,但如果您可以在发送有效负载之前调试正在运行的应用程序,您可以观察要序列化的对象的结构,并使用它来决定您已经在流中找到。我观察到样本流中有几个负数,这表明负值对用例很重要,或者有反向引用,这不是一个简单的对象树,而是一个完整的图,这将使事情变得稍微复杂一点。
RPC 序列化格式并不复杂——我强烈建议阅读各种com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamReader 子类中的代码以了解它的作用。从那里,您应该能够将这两个值列表(字符串和引用/基元)解析为实际对象,以及可能通过网络发送的所有类的结构,并用任何语言或框架重新实现它.