guacamole协议与客户端源码解析

guacamole协议

协议组成

Guacamole 协议由若干指令组成。每条指令是一个逗号分隔的列表,最后以分号终止,其中列表中的第一个元素是指令操作码,其后的元素是该指令的参数:

OPCODE,ARG1,ARG2,ARG3,……;

指令列表中的每个元素都是由一个正的十进制整数前缀和一个具体的元素值组成,其中前缀由一个英文句点( . )分隔。整数前缀表示具体的元素值的Unicode字符的数量,字符由UTF-8编码:

LENGTH.VALUE

若干条完整的指令组成一条消息,该消息从客户端发送到服务器,或者从服务器发送到客户端。客户端到服务器的指令通常是控制指令(用于建立连接或断开连接)和事件(鼠标事件和键盘事件)。服务器到客户端的指令通常是将客户端用作远程显示器的绘制指令(缓存,剪切,绘制图像)。

例如,将显示尺寸设置为1024*768的完整有效的指令是:

4.size,1.0,4.1024,3.768;

对于这个指令,会被服务器解析为四个元素:“size” ,作为size指令的操作码,“0”,是图像默认层的索引,“1024”,为所需的像素宽度,“768”,为所需的像素高度。

正因为Guacamole协议的设计方式,使得它可以流式传输协议,同时也可以很容易地被JavaScript解析。

JS确实原生支持类似于XML、JSON这样格式的信息,但是这类格式的信息都不能以流的形式传输。JS在解析这类格式的信息前必须接收到完整的XML或者JSON的包,而guacamole协议的信息,却可以一边接收一边解析。它的指令内每个元素的长度前缀使得解析器不用遍历每个字符就可以完成指令之间的跳转。

握手阶段

新建连接

握手的过程是guacamole协议建立连接的过程。当客户端发送“select”指令时,握手阶段就开始了,select指令告诉服务器要加载哪个协议:

6.select,3.vnc;

收到“select”指令后,服务器会加载对应的客户端组件,并且回复一个“args”指令,这个指令指明了服务器端需要的参数列表

4.args,13.VERSION_1_1_0,8.hostname,4.port,8.password,13.swap-red-blue,9.read-only;

其中的协议版本用于协商客户端和服务器的不同版本之间的兼容性,从而允许双方协商最高支持的版本并启用或禁用与该版本关联的功能。不支持该指令的旧版本的Guacamole客户端将无提示地将其忽略为空连接参数。有效协议版本如下:

版本号对应参数值描述
1.0.0VERSION_1_0_0这是默认版本,适用于1.1.0之前的任何版本。协议的版本1.0.0不支持协议协商,并且要求握手指令以特定顺序传递,并且存在(即使为空)。
1.1.0VERSION_1_1_0协议版本1.1.0引入了对协议版本协商,握手指令的任意顺序的支持,并支持在握手期间传递时区指令。

客户端接收到服务端可接受的参数列表后,需要回复给服务器,自己支持的音频(audio),视屏(video),图像(image),最佳屏幕尺寸(size)及时区(timezone),并在最后回复所有的服务器要求的参数的值(connect),即使是空,也要回复。任意要求没有满足,连接都将被关闭。客户端回复给服务端的消息如下:

4.size,4.1024,3.768,2.96;
5.audio,9.audio/ogg;
5.video;
5.image,9.image/png,10.image/jpeg;
8.timezone,16.America/New_York;
7.connect,13.VERSION_1_1_0,9.localhost,4.5900,0.,0.,0.;

如上,在客户端回复服务端的参数中,回复了三个0.给服务端,意味着客户端这三个参数为空,没有值,所以留空,长度为0,回复0.恰好。

在实际协议中,指令之间是紧挨着的不存在换行符,如果一条指令之后除了新指令的开始之外还有其他内容,则连接将关闭。

以下是握手过程中的指令的说明:

指令名称描述
audio客户端支持的音频编码解码器。在上面的例子中指定了audio/ogg作为支持的编码解码器
connect这是握手阶段的最后一个指令,它表明握手阶段已经结束,并且连接正常建立,可以继续进行。这条指令后续跟着服务器中args指令发送的连接参数的参数值。在上面的例子中,参数指定了在5900端口与localhost进行连接,后续三个参数值为空。
iamge客户端支持的图像格式,按首选项顺序。上例中的客户端同时支持PNG和JPEG。
timezone客户端的时区,采用IANA区域密钥格式。上例的时区是美国纽约
video客户端支持的视频编码解码器。上例的客户端是不支持任何视频编解码器。

客户端在握手中发送的指令的顺序是任意的,除了最后一条指令connect将结束握手并尝试开始连接。

客户端发送完这些指令后,服务器将尝试使用接收到的参数初始化连接,如果成功,则以“ready”指令进行响应。这条指令中包含新客户端连接的ID,并标记交互阶段的开始。这个ID是一个任意字符串,但是保证这个ID在所有活动链接以及受支持协议中是唯一的:

5.ready,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;

当服务器发送’ready‘后,真正的交互阶段就开始了。客户端和服务器端之间相互传递绘图和事件指令,直到关闭连接。

加入现有连接

握手阶段完成后,如果通过“select”指令提供了ID而不是协议名称,则这个连接将被认为是活跃的并且是能被加入的。

6.select,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;

加入现有连接的其余阶段与握手的阶段是相同的。与新连接一样,这次连接的其他参数由握手期间提供的参数值决定。

流与对象

guacamole支持传输剪切板内容,音频内容,视频内容和图像数据,以及文件和任意的命名管道。

特殊语义的指令将会通过新分配的流传送。例如,用于播放媒体文件的“audio”或“video”指令。用于传输文件的“file”指令,用于在客户端和服务端传输任意数据的“pipe”指令。在某些情况下,将通过已命名的流传送的结构化集合对象的方式来显示指明流的能力范围和语义。

流一旦被创建,将通过“blob”指令一块一块地传送数据,通过“ack”来确认已收到的消息,流的结束通过一个“end”指令来标识。

客户端源码解析

在这里插入图片描述

guacaclient目录结构 如上图所示 为官方guacamole-common完整项目

因个人能力有限 所以源码不会一一解读 只看几个我看的懂的。

接下来将按包目录展开

IO

一看名字就知道是负责客户端读取输出的包,包中共有两个接口,两个实现类。

GuacamoleReader

是一个接口,注释说明这个接口是提供对guacamole指令流的抽象和原始字符的读取访问。

接口中一共提供了三个了方法

public boolean available() throws GuacamoleException;

available()方法返回指令信息是否可以继续读取,返回true表示可以继续读取,false则不行。具体含义结合实现类看比较容易懂。

public char[] read() throws GuacamoleException;

read()方法负责读取流中的指令。此方法一次至少读取一个完整的guacamole指令,返回的缓存中包括一个或多个完整指令,但不会包含不完整指令。

public GuacamoleInstruction readInstruction() 

readInstruction()方法读取一个完整的指令并返回完整解析的指令。

看到这可能觉得read(),readInstruction() 方法类似,看完了实现类会发现两者区别还是挺大的,一个是从流中截取一段段的guacamole指令,一个是将指令的前缀数值去掉直接获取指令的opcode和args。

ReaderGuacamoleReader

ReaderGuacamoleReader是GuacamoleReader的实现类

注释说明了这个类 使用了标准的java的Reader对guacamole的指令流进行读取。

这个类一共有四个成员变量,实现了GuacamoleReader的所有方法。

四个成员变量:

private Reader input;
public ReaderGuacamoleReader(Reader input){this.input = input}

这个input对象作为Reader类的对象将负责整个类的数据读取,在后续的read()方法中有出现;input在构造函数中进行了初始化:

private int parseStart;

parseStart变量用来记录当前解析到的数据在数据堆(后续的char类型数组)中的位置, 防止重复或超前解析。在read()方法中主要做一个记录的作用

private cahr[] buffer = new char[20480];

buffer数组用来接受存放所有未解析的数据,也就是说通过input读取的数据都会放置在这个数组中。

private int usedLength = 0;

记录buffer数组中的实际字符数,也就是记录当前buffer数组的实际使用长度。

实现的三个方法:

available()方法

    public boolean available() throws GuacamoleException {
        try {
            return input.ready() || usedLength != 0;
        }
        catch (IOException e) {
            throw new GuacamoleServerException(e);
        }
    }

在input准备就绪和usedLength不为0时返回true;当usedLength!=0意味着buffer数组中还存在指令信息,可继续进行解析处理。

read()方法

public char[] read() throws GuacamoleException {

        try {

            // 当方法阻塞时,input会读取新数据,循环一直进行直到input读到最后的数据
            for (;;) {

                // 记录指令长度,也是协议 数值.值 中的数值部分
                int elementLength = 0;

                // 设置i的位置从parseStart开始,因为parseStart的作用就是记录解析到的位置
                int i = parseStart;

                // 从i位置开始解析直到usedLength处,因为usedLength记录了buffer数组实际使用的长度
                while (i < usedLength) {

                    // 先读取一个字符
                    char readChar = buffer[i++];

                    // 如果是数字则更新数值
                    if (readChar >= '0' && readChar <= '9'){
                        elementLength = elementLength * 10 + readChar - '0';
										}
                    //如果是 . 说明数值部分已经结束 检查当前元素块结尾是 分号(;)还是(,)号,如果都不是则报错
                    else if (readChar == '.') {

                        // 检查当前元素是否都在buffer数组中
                        if (i + elementLength < usedLength) {

                            // 获取终结符
                            char terminator = buffer[i + elementLength];

                            // 将i向后移动指令长度+1,后续直接将指令长度的字符全部读取
                            i += elementLength + 1;

                            // 重制指令长度,方便下次读取
                            elementLength = 0;

                            // 重新记录解析位置为i
                            parseStart = i;

                            // 如果终结符是‘;’,说明我们已经读取完了一个完整的指令
                            if (terminator == ';') {

                                // 复制全部指令
                                char[] instruction = new char[i];
                                System.arraycopy(buffer, 0, instruction, 0, i);

                                // 更新buffer实际使用长度,要减去 i
                                因为现在i之前的字符都已经解析过了
                                usedLength -= i;
                                parseStart = 0;
                                System.arraycopy(buffer, i, buffer, 0, usedLength);

                                return instruction;

                            }

                            // 如果终结符不是‘,‘说明传过来的协议数据不符合约定的协议格式,抛出异常
                            else if (terminator != ',')
                                throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','");

                        }

                        // 当前元素内容没有全在buffer数组中则停止解析,等待input读取后续内容
                        else
                            break;

                    }

                    // 在数值部分存在非数值字符,抛出异常
                    else
                        throw new GuacamoleServerException("Non-numeric character in element length.");

                }

                // 如果当前实际使用长度超过了buffer数组长度的一半则进行扩容,直接扩充一倍
                if (usedLength > buffer.length/2) {
                    char[] biggerBuffer = new char[buffer.length*2];
                    System.arraycopy(buffer, 0, biggerBuffer, 0, usedLength);
                    buffer = biggerBuffer;
                }

                // 因为上面进行了解析或者扩容,buffer数组肯定有空余位置了,让input继续读取新的指令信息
                int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
                if (numRead == -1)
                    return null;

                //更新buffer数组的实际使用长度 
                usedLength += numRead;

            }//至此 循环结束 

        }
        catch (SocketTimeoutException e) {
            throw new GuacamoleUpstreamTimeoutException("Connection to guacd timed out.", e);
        }
        catch (SocketException e) {
            throw new GuacamoleConnectionClosedException("Connection to guacd is closed.", e);
        }
        catch (IOException e) {
            throw new GuacamoleServerException(e);
        }

    }

read()方法的整体逻辑也是从解析guacamole协议出发的因为协议规定协议中每个元素的组成格式为 LENGTH.VALUE 所以read方法从读取LENGTH开始,当读取完length后判断后续的value是否都已经在buffer数组中了,如果是 则开始下一个元素的解析直到读取到 ‘ ; ’ 说明一条指令读取完毕 将整条指令返回。中间方法也进行了格式完整性的判断。这个方法也体现了guacamole协议设计的便利性,可以进行流式传输,一条指令可以在前后两次数据包中传输。

readInstruction()

    public GuacamoleInstruction readInstruction() throws GuacamoleException {

        // 通过read()方法直接获得一组指令
        char[] instructionBuffer = read();

        // 如果read方法返回为null说明已经解析结束,直接返回null
        if (instructionBuffer == null)
            return null;

        // 记录当前解析到的指令位置
        int elementStart = 0;

        // 创建一个双端队列存储指令
        Deque<String> elements = new LinkedList<String>();
        while (elementStart < instructionBuffer.length) {

            // 记录LENGTH的结尾位置,初始化为 -1
            int lengthEnd = -1;
            //寻找LENGTH的结尾位置
            for (int i=elementStart; i<instructionBuffer.length; i++) {
                if (instructionBuffer[i] == '.') {
                    lengthEnd = i;
                    break;
                }
            }

            // 如果遍历指令结束都没有找到‘.’ 说明返回的指令是不完整的,抛出异常
            if (lengthEnd == -1)
                throw new GuacamoleServerException("Read returned incomplete instruction.");

            // 找到了一个完整的LENGTH,将char类型的数值转化为int
            int length = Integer.parseInt(new String(
                    instructionBuffer,
                    elementStart,
                    lengthEnd - elementStart
            ));

            // 去掉一个‘.’的位置 通过length获取一个元素
            elementStart = lengthEnd + 1;
            String element = new String(
                    instructionBuffer,
                    elementStart,
                    length
            );

            // 将获取的元素加到队列中
            elements.addLast(element);

            // 读取当前元素后的结束符,只可能是‘,’或‘.’
            elementStart += length;
            char terminator = instructionBuffer[elementStart];

            // 继续读取下一个元素
            elementStart++;

            // 当读取到‘;’时 结束循环
            if (terminator == ';')
                break;

        }

        // 从队列中弹出第一个加入队列的元素 按照协议格式 第一个元素为操作码
        String opcode = elements.removeFirst();

        // 创建GuacamoleInstruction类对象,将读取的协议传入,构建一个实体对象
        GuacamoleInstruction instruction = new GuacamoleInstruction(
                opcode,
                elements.toArray(new String[elements.size()])
        );

        // 返回解析完成的协议对象
        return instruction;

    }

readInstruction()方法的实现逻辑很简单,先从read()处获得规定格式的协议源码,再通过LENGTH.VALUE格式 逐步获取协议的操作码和后续参数,最后构建对象,返回结果。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
安装 Guacamole码需要进行以下步骤: 1. 安装依赖项:在安装 Guacamole 之前,您需要安装一些依赖项,比如 JDK、MySQL、Tomcat 等。您可以使用以下命令安装这些依赖项: ``` sudo apt-get install default-jdk mysql-server mysql-client tomcat8 tomcat8-admin tomcat8-common tomcat8-user ``` 2. 下载 Guacamole 代码:您可以从 Guacamole 的官方网站上下载代码,也可以从 GitHub 上进行下载。在本次安装中,我们将从 GitHub 上下载代码。使用以下命令从 GitHub 上获取 Guacamole代码: ``` cd ~ wget https://github.com/apache/guacamole-server/archive/refs/tags/1.3.0.tar.gz tar -xzf 1.3.0.tar.gz ``` 3. 编译和安装 Guacamole:在获取了代码之后,您需要编译和安装 Guacamole。使用以下命令编译和安装 Guacamole: ``` cd guacamole-server-1.3.0/ ./configure --with-init-dir=/etc/init.d make sudo make install sudo ldconfig ``` 4. 安装 Guacamole Web 应用程序:使用以下命令安装 Guacamole 的 Web 应用程序: ``` wget https://downloads.apache.org/guacamole/1.3.0/binary/guacamole-1.3.0.war -O /var/lib/tomcat8/webapps/guacamole.war ``` 5. 配置 Guacamole:在安装 Guacamole 之后,您需要对其进行一些配置。您可以编辑 `/usr/local/etc/guacamole/guacamole.properties` 文件来配置 Guacamole。在进行配置之后,您需要重启 Tomcat 服务: ``` sudo service tomcat8 restart ``` 以上就是从代码安装 Guacamole 的步骤,希望对您有所帮助。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值