Webページ上で図形を描画する

Webページ上の描画を担うのはクライアントプログラム(ブラウザ)のレンダリングエンジン.
ここに詳しく載っているが,レンダリングの大雑把な流れとしては,

1. ドキュメント(HTML, CSS)を解析してツリー構造(DOMツリー,CSSOMツリー)を作成する.
2. その過程で同時に,描画されるコンテンツの順序構造を表すレンダーツリーを構築する.
3. レンダーツリーのノードに画面上の座標を割り当てる(レイアウト).その後,ツリーの基づき描画を行う.

ブラウザはメインスレッドでループを回しており,レンダリングはイベントに応じて適宜行われる.が,上の1~3が全て実行されるわけではない(高負荷になるから).例えばユーザーがブラウザをスクロールする場合はコンテンツの表示位置が変わるのでレイアウトをし直す必要があるが,レンダーツリーは再利用される.一方,動的にコンテンツが変更される場合はレンダーツリーを作り直さなければならないが,コンテンツの序列がツリーに反映されているため,変更による影響をツリーの一部に抑えることが出来る.

Webページ上で図形を(動的に)描画する方法としては,HTMLでsvgcanvas要素などを用いることがある.

SVG

ベクター形式の画像形式で,XMLを用いて記述される.主要なブラウザで表示でき,HTMLでも直接サポートされている.
<svg>タグでは,width, height 属性で画面上の表示サイズを指定することが出来る.同時に,viewBox 属性で表示する座標領域を指定する.なにか似ていてややこしいが,svgにおける座標平面を画面にビューポート変換する際の前後領域を指定しているものと考えればよいと思う(svg平面のviewBoxで指定された領域を,width・heightで指定される画面領域に射影する).ただし,デフォルトではアスペクト比が保持されるように変換される.これはpreserveAspectRatio属性で指定できる.
そして,<line>や<text>といった要素が描画機能として提供される.
例:

<svg width="300px" height="300px" viewBox="-100 -100 200 200">
    <circle cx="50" cy="50" r="50" fill="#777777" id="circ1" />
    <line x1="-50" y1="-100" x2="50" y2="100" stroke="#002200" id="line1" />
</svg>

svgについてはここのチュートリアルがとても参考になる:
www.petercollingridge.co.uk

Canvas

ラスタ形式の画像を描画するには<canvas>要素を使うことが出来る.canvas自体はwidthとheightの属性を持つ単なる描画領域で,スクリプトによりこの領域に描画を行うことになる.Javascriptで描画を行う場合,描画コンテクスト(context)と呼ばれるインターフェースを通して描画を行う必要がある(低レベルに領域の値を直接操作することは多分できない).
描画コンテクストとしては2D描画用のCanvasRenderingContext2Dと,主に3D描画用のWebGLがある.恐らく内部実装は全く異なるんじゃないかと思う.
例:

<canvas id="tutorial" width="100" height="100"></canvas>
<script type="text/javascript">
  (function (){
    var canvas = document.getElementById('tutorial');
    if (canvas.getContext){
      // CanvasRenderingContext2D 
      var ctx = canvas.getContext('2d');
      ctx.fillRect(10, 10, 90, 90);
    }
  })();
</script>

[REF]
ブラウザのしくみ: 最新ウェブブラウザの内部構造 - HTML5 Rocks
canvas チュートリアル - ウェブデベロッパーガイド | MDN

pythonでwebサーバーを構築

Pythonを使ってwebサーバーを構築に関するメモ

http.serverというモジュールを使えば一瞬でできるが,もう少し低レベルのところまで扱いたいと思う.

Webサーバーは,クライアント(Webブラウザ)との間で,HTTPという通信規約に基づいて通信するプログラムである.

なのでサーバーに必要な機能は,クライアントとサーバーでそれぞれプログラムを実行しているプロセスの間で通信を確保し,クライアントからの要求に基づいて適当な情報を送信すること.

プロセス間通信(IPC)には色々なやり方がある.ファイルを媒体にするとか,パイプを経由するとか.Webの場合はプログラムを実行しているプラットフォームが限定されない(異なるOSとか)が,そういう場合はソケットを使う方法が支配的らしい.

ソケットはプロセス間通信のAPI(アプリケーションプログラミングインターフェース)である.こういう言い方をするとなんかややこしいけれど,つまるところはプロセス間通信をプログラムする上でのインタフェースとなる部分の仕組みの1つがソケットということ.プロセス間通信でもどんなプロトコルを採用するかなどの違いがあるけれど,どんな方式にしても共通のインターフェースとしてソケットを利用するのが一般的なんだとか.OSI参照モデルでいうと,ソケットはアプリケーション層が,トランスポート層TCPとか)やネットワーク層(IPとか)とデータをやり取りするためのインターフェースになる.

では実際にどういう仕組なのかというと,ソケットという端点を用意し,そこにサーバー・クライアントの接続を確立してデータをやり取りするというものになっている.より具体的には下の図を参考にしてほしい.ソケットを作成する際にはプロトコルを指定するが,それにより通信の方法も変わってくる.例えばbindで接続先のアドレスを指定するが,TCPならばこれがIPアドレスとポート番号の組み合わせとなり,さらにUDPならば接続を確立する必要はないため,listenやconnectなどの作業は不要となる,といった具合.


f:id:simcode:20181007152903p:plain
ソケット通信の概要図


では実際にwebサーバーのプログラムを作っていく.やることはソケットを使って通信を確保し情報をやり取りすること.これ自体はかなり単純だけれど,1対1の通信ということがwebサーバーとしてはネックとなる.あるクライアントと通信している間は処理がブロックされてしまうため,他のクライアントとの通信が行えない.通信がハングしたら終わりである.そのため要求のたびにクライアントとの通信に使うソケット(クライアントソケット)を複数作って,並列処理させることが必要になる.マルチスレッドやマルチプロセスを使うことも方法の1つだが,かなり多くの通信をさばくことを考えると非同期処理が良い.ということで,下のREFのサイトを参考に書いたコードが以下.

HTTP/1.1ではTCP接続が標準で持続的(Keep-Alive)となる.よって,レスポンスのヘッダで送信するデータ量を明示しないと,(サーバー側から接続を切らない限り)クライアントはデータを待ち続けてハングする.なので Content-Type と Content-Length はきちんと送らないとダメ.

import socket
import select

# IP, TCPを使う
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# ノンブロッキング
serversocket.setblocking(0)
# IPアドレス-ポートの組み合わせでアドレスを割当
serversocket.bind(('localhost', 1234))
# 接続要求は5つまで受け入れる
serversocket.listen(5)

# recvを待つソケットリスト
r_list = [serversocket]
# sendを待つソケットリスト
w_list = []

# どんな要求に対してもこのメッセージを返す
res_message = b"""
HTTP/1.1 200 OK
Server: test-python-server
Content-Type: text/html; charset=UTF-8
Content-Length: 123

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1>test message</h1>
  </body>
</html>
"""

while r_list:
    # recv/send出来るソケットのリストを取得
    readable, writable, exceptional = select.select(r_list, w_list, r_list, 60)

    for sock in readable:
        if sock is serversocket:
            # 接続する準備が出来た
            clientsocket, client_address = sock.accept()
            clientsocket.setblocking(0)
            r_list.append(clientsocket)
        else:
            # クライアントソケットでrecv
            data = sock.recv(1024)
            if data:
                if sock not in w_list:
                    w_list.append(sock)
            else:
                # ソケットがshutdownされた.接続を切る.
                if sock in w_list:
                    w_list.remove(sock)
                r_list.remove(sock)
                sock.close()

    for sock in writable:
        # メッセージを送信
        sock.send(res_message)
        w_list.remove(sock)

    for sock in exceptional:
        r_list.remove(sock)
        if sock in w_list:
            w_list.remove(sock)
        sock.close()

[REF]
ソケットプログラミング HOWTO — Python 3.6.5 ドキュメント
How to Work with TCP Sockets in Python (with Select Example)