view paper/chapter4.tex @ 24:b6a6413ac3ca

add files
author Taninari YU <you@cr.ie.u-ryukyu.ac.jp>
date Mon, 03 Feb 2014 16:56:17 +0900
parents
children 2d6118b66367
line wrap: on
line source

\chapter{画面共有システムTreeVNCの実装}

\section{TightVNCのアップデートへの対応}
TightVNCは現在も開発が続いていて、アップデートされている。このアップデートに対応するために、私が作成しているTreeVNCにも対応させる必要がある。\\
卒業論文後から私は2種類のアップデートを行った。その2種類のアップデートの対応について説明する。\\
まず、はじめに行ったアップデートはVersionが1.xから2.xへ変わるメジャーアップデートと呼ばれる大型アップデートである。\\
このアップデートでは、パッケージ構成が追加され、元のソースコードがほとんど残っていない状態であった。このような大型なアップデートに対応するには、新しいTightVNCを元にして、作成したTreeVNCの機能を一つづつ移行していく必要がある。このソースコードのアップデートに加えソースコードの質を高めるためにリファクタリングを行った。
リファクタリングとは、将来の仕様変更に柔軟に対応できるようにソースコードの手直しを行うことである。



\section{UIの実装}
授業やゼミなどで使用してみて、必要な機能の提案が出てきたので、実装を行った。

\subsection{マルチディスプレイへの対応}
VNCでは画面の情報を矩形型にして送信する。もし複数のディスプレイが存在する場合すべてのディスプレイの情報が送られてくる。しかし、発表などに使用するディスプレイは一つである場合が多い。\\
必要な画像が一つのディスプレイなのに、すべてのディスプレイのデータを送ると無駄なデータが発生する。そこで、ディスプレイを指定して、その画像だけ送信する機能を追加することで、無駄なデータ送信を省くことができる。

RFBプロトコルでは、FramebufferUpdateによって、矩形状の画像データが送信されてくる。\\
FrameBufferUpdateの概要を(表\ref{tb:framebufferupdate},表\ref{tb:framebufferupdate2})に示す。

\begin{table}[htbp]
\caption{FramebufferUpdate}
\label{tb:framebufferupdate}
\begin{center}
  \begin{tabular}{|c|c|c|} \hline
    バイト数& 型 & 説明  \\ \hline
    1 & U8 & message-type \\ \hline
    1 & U8 & padding\\ \hline
    2 & U16 & number-of-rectangles \\ \hline
  \end{tabular}
\end{center}
\end{table}

この後にnumber-of-rectanglesの数だけ矩形のピクセルデータが続く。各矩形は(表\ref{tb:framebufferupdate2})に示す。

\begin{table}[htbp]
\caption{FramebufferUpdate}
\label{tb:framebufferupdate2}
\begin{center}
  \begin{tabular}{|c|c|c|} \hline
    バイト数& 型 & 説明  \\ \hline
    2 & U16 & x-position \\ \hline
    2 & U16 & y-position \\ \hline
    2 & U16 & width  \\ \hline
    2 & U16 & height \\ \hline
    4 & S32 & encoding-type \\ \hline
  \end{tabular}
\end{center}
\end{table}

ここまでがheaderとして送信されるデータである。矩形の画像なのでx-position、y-position、width、heightの4つの値で画像の位置と大きさを決めることができる。\\
headerに続いて、実際の画像データが送信されてくる。\\
画像データはZRLEエンコーディングで送信される。最初の4バイトはデータの大きさを表現して、次にその大きさ分のzlibDataが送信される(表\ref{tb:ZRLE})。

\begin{table}[htbp]
\caption{ZRLEデータ}
\label{tb:ZRLE}
\begin{center}
  \begin{tabular}{|c|c|c|} \hline
    バイト数& 型 & 説明  \\ \hline
    4 & U32 & length \\ \hline
    length & U8 array & zlibData \\ \hline
  \end{tabular}
\end{center}
\end{table}

送られてきたzlibDataは展開されると左から右、上から下へ並んだ、64*64ピクセルのタイル群画像データとなる。

ここで、画像データがどのように送られてくるのかを調べてみたところ、2つディスプレイがあるとすると、両ディスプレイにまたがった画像更新が来ることがないことがわかった。\\
図\ref{fig:rawdata}の黒い部分が画像データだとすると、図\ref{fig:rawdata}のようなFramebufferUpdateは送られてくることはない。

\begin{figure}[!htbp]
\begin{center}
\includegraphics[width=110mm]{./images/sendscreenimage.pdf}
\end{center}
\caption{画面更新時に来る可能性のないUpdateRectangle}
\label{fig:sendscreenimage}
\end{figure}

以上のことを踏まえ、FramebufferUpdateで送信されてきたheaderを確認し、x-positionを確認することで、どの画面の画像データを送信するかを選択することができる。\\
例えば、図\ref{fig:rawdata}では、左側の画面を送信したいときは、x-positionが1920より小さい場合送信し、右側を送信したい場合は1920以上のデータを送信するようにフィルタリングすることで実現できる。

\newpage 

\subsection{表示画面の切り替え}
ゼミなど発表者が多数いる状況でVNCを使用すると、発表者が切り替わるごとにサーバを立ち上げなおさなければならない。\\
画面の切替手順を図\ref{fig:changevncserver}に示す。

\begin{figure}[!htbp]
\begin{center}
\includegraphics[width=150mm]{./images/changevncserver.pdf}
\end{center}
\caption{画面切り替えの流れ}
\label{fig:changevncserver}
\end{figure}

図\ref{fig:changevncserver}で使用されている関数の説明を表\ref{tab:changevnc_functions}に示す。

\begin{table}[!htbp]
 \caption{画面切り替えの関数}
 \label{tab:changevnc_functions}
 \begin{center}
  \begin{tabular}{|c||p{25zw}|} \hline
   名称 & 説明 \\ \hline \hline
   changeVNCServer("10.3") & 受け取った引数のアドレスへ切り替えるための命令を出す関数。 \\ \hline
   requestVNC() & changeVNCServer()で指定されたホストに対して接続要求を出す関数。\\ \hline
   acceptConnection() & Root Nodeから接続要求が来たコンピュータが、要求に対して許可したことをRoot Nodeへ報告する関数。 \\ \hline
   CloseConnection() & 今まで使用していた画面共有のストリームを閉じるための関数 \\ \hline
   newServer() & Nodeに画面が切り替わったことを報告する関数。この命令でNodeは新しいストリームを受け取るようになる。 \\ \hline
  \end{tabular}
 \end{center}
\end{table}


\newpage
newServer()の内部処理ををListing\ref{src:changescreen}に示す。これは、Root Nodeが子供に対して、画面の切り替えが起こったことを知らせるソースコードである。\\
clientListは、現在接続されているクライアント情報が入っている。クライアントにそれぞれTCP接続を行い、サーバが変わったので接続し直させる命令を送信する。

\begin{lstlisting}[language=java,frame=lrbt,label=src:changescreen,caption=画面が切り替わったことを知らせるプログラム,numbers=left]
  for (String client : clientList) {
      Socket echoSocket = new Socket(client, 10001);
      DataOutputStream os = new DataOutputStream(echoSocket.getOutputStream());
      os.writeBytes("reconnection\n");
      os.close();
  }
\end{lstlisting}


\newpage
\section{Authentication}
Root Nodeがサーバに対してVNC接続を行う際、ハンドシェイクが必要となる。
ハンドシェイクの手順として、まず始めにRoot Nodeがサーバに接続を行うと、
サーバがサポートする最新のプロトコルバージョンが送られてくる。
Root Nodeはサーバから送られてきたプロトコルバージョン以下の使用できるバージョンを
サーバに対し送る。現時点で公開されているプロトコルバージョンは3.3、3.7、3.8だけである。
今回TreeVNCは3.855というバージョンを用意して3.855が来るとTreeVNCを使用するようにした。

プロトコルバージョンが決定すると、サーバ及びNodeは、
その接続で使用されるセキュリティに合意しなければならない。
バージョン3.7以降ではサーバは自身のサポートするセキュリティタイプの一覧を提示する。
Nodeのサポートする有効なセキュリティタイプを少なくとも一つサーバが提示した場合、Nodeはその接続上で使用されるセキュリティタイプを表す単一バイトを送り返す。

登録されているセキュリティタイプの一例として(表 \ref{tb:authtype})のようなものがある。

\begin{table}[htbp]
\caption{AuthType}
\label{tb:authtype}
\begin{center}
\begin{tabular} {|l|l|}
  \hline
  {\bf 値}&名称\\
  \hline
  {\bf 0}&Invalid\\
  \hline
  {\bf 2}&None\\
  \hline
  {\bf 5}&RA2\\
  \hline
  {\bf 18}&TLS\\
  \hline
  {\bf 21}&MD5 ハッシュ認証\\
  \hline
\end{tabular}
\end{center}
\end{table}

MAC OS X SnowLeopardで起動しているVNCサーバに接続するときには
MAC専用の認証の値35がありこれでパスワード認証を行うことができていた。

しかしMAC OS X Lionでパスワード認証を行おうとすると、
MAC OS X Lionにしてパスワード認証ができなくなったので、
別の認証方法で認証を行うことにした。

調べてみるとMAC OS Xが返してくる認証番号は[30, 31, 32, 2, 35]がある。
32はサーバに対して画面要求の認証を求めるタイプの認証であることがわかった。
この認証を用いるとサーバに対してRoot Nodeが接続する際にサーバ側に確認画面が出るようになる。
サーバ側がこれを容認すると認証が成立する。

\newpage

\section{BroadcastとMulticast}
4章で述べたとおり、Broadcastを使用する場合は一回に送信するパケットのサイズを64000byte以下にしなければならない。もし、1920*1080のサイズの画像データを送信する際、約600万byte(1920*1080*3)となってしまう。これでは送信することができないのでデータを分割し64000byte以下にして送信しなければならない。\\
作成したTreeVNCでは、サーバから受け取った画像データをRoot Nodeが一旦解凍して、再圧縮し、Nodeたちに送信するという方式を取っている。\\
一旦解凍した後はRawDataとなるのでデータを分割することができる。TreeVNCではZRLEエンコーディングを使用して、これを解凍すると左から右へ上から下への順の64*64のタイル郡のRawDataとなる。\\
解凍されたRawDataを図\ref{fig:rawdata}に示す。


\begin{figure}[!htbp]
\begin{center}
\includegraphics[width=110mm]{./images/rawdata.pdf}
\end{center}
\caption{RawDataの構造}
\label{fig:rawdata}
\end{figure}

ひとまずテストで、\ref{fig:comparenormalandtree}のように2列づつに分割して送信するプログラムを書いた(Listing\ref{src:blocking})。\\
毎回バッファをコピーして送っているため画面共有に支障をきたすほど処理が遅くなってしまった。\\
\newpage
\begin{lstlisting}[language=java,frame=lrbt,label=src:blocking,caption=画像データを分割するプログラム,numbers=left]
  LinkedList<ByteBuffer> output = new LinkedList<ByteBuffer>();
  int width = header.getShort(8);
  int height = header.getShort(10);
  int y = header.getShort(6);
  int dataLen = width * 64 * 3 * 2;
  int temp = 0;
  int count = height / 128;

  if (width \% 64 == 0)
    dataLen += (width / 64) * 2;
  else
    dataLen += (((width / 64) + 1) * 2);

  for (int i = 0; i < count; i++) {
    int tempDataLen = dataLen - temp;

    while (tempDataLen > INFLATE_BUFSIZE) {
      output.addLast(input.poll());
      tempDataLen -= INFLATE_BUFSIZE;
    }
    if (tempDataLen == INFLATE_BUFSIZE) {
      output.addLast(input.poll());
      createHeader(header, i, height, y);
      zipSplitData(header, output);
      output.clear();
      temp = INFLATE_BUFSIZE;
    } else {
      ByteBuffer tempBuf = input.poll();

      ByteBuffer buf1 = ByteBuffer.allocate(INFLATE_BUFSIZE);
      if(tempBuf.remaining()>tempDataLen)
      tempBuf.get(buf1.array(), 0, tempDataLen);
      buf1.limit(tempDataLen);
      output.addLast(buf1);
      createHeader(header, i, height, y);
      zipSplitData(header, output);
      output.clear();

      buf1 = ByteBuffer.allocate(INFLATE_BUFSIZE);
      tempBuf.get(buf1.array(), 0, tempBuf.remaining());
      
      buf1.limit(INFLATE_BUFSIZE - tempDataLen);
      output.addLast(buf1);
      temp = INFLATE_BUFSIZE - tempDataLen;
    }
\end{lstlisting}

BroadcastとMulticastでどのくらいパケットロスするのかもテストを行った。\\
BroadcastとMulticastを用いてそれぞれのbyte数を100packetずつ送信しパケットロス率を表したのが表\ref{tb:testofbroadcastandmulticast}である。\\
表\ref{tb:testofbroadcastandmulticast}にあるようにMulticastを用いても4000byteを100packet送信すると37\%もパケットロスしてしまう。

\begin{table}[htbp]
\caption{BroadcastとMulticastのテスト}
\label{tb:testofbroadcastandmulticast}
\begin{center}
  \begin{tabular}{|c|c|c|} \hline
    & 256byte & 4000byte  \\ \hline
    Broadcasst & 47\% &87\%  \\ \hline
    Multicast & 0\% &37\%  \\ \hline
  \end{tabular}
\end{center}
\end{table}

結果として、データ分割の処理が重い、且つ予想以上のパケットロス率という結果をになったので、BraodcastやMulticastを用いた実装を行うことはもう少し工夫が必要になることがわかった。


\section{接続先自動検索システムの実装}
Broadcastを用いてTreeVNCのRoot Nodeを検索するシステムを実装した。

Listing\ref{src:gethost}はBroadcastを使用して、データを送信するプログラムである。
\begin{lstlisting}[language=java,frame=lrbt,label=src:gethost,caption=Broadcastを用いてサーバを探すプログラム,numbers=left]
  public GetHostClient(String _str) {
    str = _str;
  }

  public void sendData() {
    buf = str.getBytes();
    DatagramPacket sendPacket = new DatagramPacket(buf, str.length(), mAddr, PORT);
    try {
      soc.send(sendPacket);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
\end{lstlisting}
Listing\ref{src:gethost}は、Broadcast Packetを受け取ると、受け取ったIPアドレスに対し、ReplyBroadCast()の内部でTCPコネクションを張り、現在起動しているVNCServerの一覧を送る。
\begin{lstlisting}[language=java,frame=lrbt,label=src:getbroadcast,caption=Broadcastを受け取るプログラム,numbers=left]
byte[] buf = new byte[BufSize];
byte[] resorve = new byte[BufSize];
try {
  InetAddress mAddr = InetAddress.getByName(McastAddr);
  MulticastSocket soc = new MulticastSocket(Port);
  DatagramPacket recvPacket = new DatagramPacket(buf, BufSize);
  soc.joinGroup(mAddr);
  while (!stopFlag) {
    soc.receive(recvPacket);
    address = getAddress(recvPacket.getSocketAddress());
    inputStream = new ByteArrayInputStream(recvPacket.getData());
    inputStream.read(resorve);
    if(str.equals(castString(resorve)))
      replyBroadCast();
    if(stopFlag) break;
  } catch (IOException e) {
    e.printStackTrace();
  }
}
\end{lstlisting}


Listing\ref{src:gethost}は、Root Nodeから受け取ったVNCServer一覧を表示する部分である。ここで使用されているtextは、javaのGUIコンポーネントであるJFrameを継承したクラスのインスタンスである。
\begin{lstlisting}[language=java,frame=lrbt,label=src:getaddr,caption=起動サーバ一覧を表示するプログラム,numbers=left]
  Socket socket = server.accept();  
  is = new BufferedReader(new InputStreamReader(
  socket.getInputStream()));
  proxyAddr = is.readLine();
  if(proxyAddr!=null)
  text.checkBox(proxyAddr);
  text.setButton();
  text.visible();
\end{lstlisting}




%各64*64のタイル群の先頭には、1byteのsubencordingタイプが設定されている。