ぷろじぇくと、みすじら。

ILI9340な液晶をNetduinoで使う

Created at:

液晶+Netduino plus 2

aitendoで2.2インチのQVGAなSPI接続のTFT液晶を買いまして、 Arduinoで使おうと思っていろいろやったついでにNetduino plus 2で使えるかどうか試してみました。 Adafruitの2.2インチ液晶と同じコントローラ(ILI9340)を持っているものっぽいです。

ILI9340の利用例はRaspberry PiとかArduinoではちょこちょこあるしArduinoにはライブラリがあるのですが、 Netduinoでの利用例は見当たらなかったので直接コントロールして表示しています。

ちなみにこの液晶はロジックの電圧に3.3Vを要求していて5Vでつなぐとダメで、 Arduino Unoに直結すると動かないのですが(なのでTrinket ProやArduino M0 Proで先に動かした)、 Netduinoは3.3Vなので動かせそうとだなと思ってつないでコードを書いたところちゃんと動作しました。

接続はArduinoのSPIと同じで以下のような感じ。

LCD側ピンNetduino側ピン備考
VCC 3.3V
GND GND
CS D10 SPI / チップセレクト(SS)
RESET D8
D/C D9 コマンド用
SDI/MOSI D11 SPI
SCK D13 SPI / クロック(SCLK)
LED 3.3V
SDO/MISO D12 SPI

実装にはaitendoのデモコードとImageWriter: Raspberry Pi (9) LCD表示 1Adafruitのライブラリを参考にして、 SPIでひたすらコマンドとデータを転送する形になっています。実際のコードは以下の通りです。

なお、Netduino plus 2はRAMが100KBちょっとということもあり、QVGAのピクセルデータを全部保持することもできないので、 線や図形を適宜描画するか、画像はSDカードから読み込んで表示するといったことが必要となります。

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using System.IO;

namespace NetduinoApplication2
{
    public class Program
    {
        // DC はコマンドを発行するときに操作する用の線
        static OutputPort _dcPort;
        // RST はリセット用の線
        static OutputPort _resetPort;
        // SPI
        static SPI _spi;

        public static void Main()
        {
            _dcPort = new OutputPort(Pins.GPIO_PIN_D9, true);
            _resetPort = new OutputPort(Pins.GPIO_PIN_D8, false);

            // SPIをセットアップ
            _spi = new SPI(new SPI.Configuration(Pins.GPIO_PIN_D10, false, 0, 0, true, true, 8000, SPI_Devices.SPI1));

            // リセットをかける
            _resetPort.Write(false); // LOW (Reset)
            Thread.Sleep(100);
            _resetPort.Write(true); // HIGH (Reset off)
            Thread.Sleep(100);

            // ILI9340を初期化する
            InitializeLcd();

            // ちなみに初期化後は白でも黒でもない中途半端な状態になるので塗りつぶしをしないとしましました風に見える

            if (false)
            { 
                // パターン1: SDカードのファイルから読み込んで画像を表示する
                // ファイルの中身は240x320の RRGGGBB のバイト列

                // まず描画先のアドレスを指定する: (0, 0) -> (239, 319)
                SetAddress(0, 0, 239, 319);

                // ファイルを読んで流し込む
                var path = @"SD\nanikabitmapdata.bin";
                var buffer = new byte[16 * 1024]; // 16KB ごとに読み出す
                using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    int readLen = 0;

                    while (true)
                    {
                        readLen = stream.Read(buffer, 0, buffer.Length);

                        if (readLen == 0) break;

                        // byte列で送信しないととても遅いので注意
                        if (readLen != buffer.Length)
                        {
                            var tmp = new byte[readLen];
                            Array.Copy(buffer, tmp, readLen);

                            WriteData(tmp);
                        }
                        else
                        {
                            WriteData(buffer);
                        }
                    }
                }
            }
            else
            {
                // パターン2: ピクセルを直接操作して矩形を書く
                ushort x = 100;
                ushort y = 100;
                ushort width = 40;
                ushort height = 40;
                ushort color = 0x07E0; // 緑

                // まず描画先のアドレスを指定する: (100, 100) -> (139, 139)
                SetAddress(x, y, (ushort)(x + width - 1), (ushort)(y + height - 1));
                // ピクセルデータを流し込む
                for (var i = 0; i < width * height; i++)
                {
                    WriteData(color);
                }
            }
        }

        static void InitializeLcd()
        {
            // 診断用コード
            Debug.Print("Display Power Mode: 0x" + ReadCommand(0x0A).ToString("x"));
            Debug.Print("MADCTL Mode: 0x" + ReadCommand(0x0B).ToString("x"));
            Debug.Print("Pixel Format: 0x" + ReadCommand(0x0C).ToString("x"));
            Debug.Print("Image Format: 0x" + ReadCommand(0x0A).ToString("x"));
            Debug.Print("Self Diagnostic: 0x" + ReadCommand(0x0F).ToString("x"));

            // この辺はILI9340の初期化コード
            // コマンドとデータはAdafruitのライブラリを参考にするとてっとり早いです
            // https://github.com/adafruit/Adafruit_ILI9340/blob/master/Adafruit_ILI9340.h
            WriteCommandAndData(0xEF, new byte[] { 0x03, 0x80, 0x02 });
            WriteCommandAndData(0xCF, new byte[] { 0x00, 0xC1, 0x30 });
            WriteCommandAndData(0xED, new byte[] { 0x64, 0x03, 0x12, 0x81 });
            WriteCommandAndData(0xE8, new byte[] { 0x85, 0x00, 0x78 });
            WriteCommandAndData(0xCB, new byte[] { 0x39, 0x2C, 0x00, 0x34, 0x02 });
            WriteCommandAndData(0xF7, new byte[] { 0x20 });
            WriteCommandAndData(0xEA, new byte[] { 0x00, 0x00 });
            WriteCommandAndData(0xC0, new byte[] { 0x23 });
            //Power control
            //SAP[2:0];BT[3:0]
            WriteCommandAndData(0xC1, new byte[] { 0x10 });
            //VCM control
            WriteCommandAndData(0xC5, new byte[] { 0x3e, 0x28 });
            //VCM control2
            WriteCommandAndData(0xC7, new byte[] { 0x86 });
            // Memory Access Control
            WriteCommandAndData(0x36, new byte[] { 0x48 }); // 回転はこの辺
            WriteCommandAndData(0x3A, new byte[] { 0x55 });
            WriteCommandAndData(0xB1, new byte[] { 0x00, 0x18 });
            // Display Function Control
            WriteCommandAndData(0xB6, new byte[] { 0x08, 0x82, 0x27 });
            // 3Gamma Function Disable
            WriteCommandAndData(0xF2, new byte[] { 0x00 });
            //Gamma curve selected 
            WriteCommandAndData(0x26, new byte[] { 0x01 });
            //Set Gamma
            WriteCommandAndData(0xE0, new byte[] { 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00 });
            //Set Gamma
            WriteCommandAndData(0XE1, new byte[] { 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F });
            //Exit Sleep
            WriteCommand(0x11);

            Thread.Sleep(120);

            //Display on
            WriteCommand(0x29);
        }

        /// <summary>
        /// 描画先アドレスを設定する
        /// </summary>
        /// <param name="x">開始X座標</param>
        /// <param name="y">開始Y座標</param>
        /// <param name="x2">終了X座標</param>
        /// <param name="y2">終了Y座標</param>
        static void SetAddress(ushort x, ushort y, ushort x2, ushort y2)
        {
            WriteCommand(0x2A);
            WriteData(x);
            WriteData(x2);
            WriteCommand(0x2B);
            WriteData(y);
            WriteData(y2);
            WriteCommand(0x2C);
        }

        /// <summary>
        /// コマンドを発行して結果を取得します。
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        static byte ReadCommand(byte command)
        {
            var buffer = new byte[1];
            buffer[0] = command;

            _dcPort.Write(false);
            _spi.WriteRead(buffer, buffer);

            _dcPort.Write(true);
            buffer[0] = 0x0;
            _spi.WriteRead(buffer, buffer);

            return buffer[0];
        }

        /// <summary>
        /// コマンドを発行します。
        /// </summary>
        /// <param name="command"></param>
        static void WriteCommand(byte command)
        {
            var buffer = new byte[1];
            buffer[0] = command;

            _dcPort.Write(false);
            _spi.WriteRead(buffer, buffer);
        }

        /// <summary>
        /// 符号なし16bit整数データを書き込みます。
        /// </summary>
        /// <param name="data"></param>
        static void WriteData(ushort data)
        {
            var hi = (byte)(data >> 8);
            var lo = (byte)(data & 0xFF);

            WriteData(new[] { hi, lo });
        }

        /// <summary>
        /// バイトデータを書き込みます。
        /// </summary>
        /// <param name="data"></param>
        static void WriteData(byte data)
        {
            WriteData(new[] { data });
        }

        /// <summary>
        /// バイト列データを書き込みます
        /// </summary>
        /// <param name="data"></param>
        static void WriteData(byte[] data)
        {
            _dcPort.Write(true);
            _spi.Write(data);
        }

        /// <summary>
        /// コマンドを発行してデータを書き込みます。
        /// </summary>
        /// <param name="command"></param>
        /// <param name="data"></param>
        static void WriteCommandAndData(byte command, byte[] data)
        {
            WriteCommand(command);
            WriteData(data);
        }
    }
}

液晶で何か表示できると結構面白そうな感じなのです。

Visual Studio Codeのエディター部分(Monaco)のみをビルドする

Created at:

Visual Studio CodeのエディターをWebページに埋め込むVisual Studio CodeのシンタックスハイライトをWebページで使うで Visual Studio Codeのエディター部分いわゆるMonaco EditorをCodeから引きはがして使っていました。

Monacoはそれ単体で配布されていたりリポジトリーがあったりするわけではないのですが、実はCodeのgulpfile.editor.jsにエディター部分(Monaco)だけをビルドするビルドタスクがあります。

まずCodeをビルドできるよう準備して、その後gulpで editor-distro タスクを実行すると以下のディレクトリにMonacoのビルド結果が出力されます。

このディレクトリー以下の一式をアプリケーションやWebサイトに配置して、エディターを組み込むことができます。Mimosaのmonaco-prebuiltはこのout-editor-minを突っ込んだものです。

ちなみにMonacoのライセンスはVisual Studio Codeの一部なのでMIT Licenseとなります。

Visual Studio CodeのエディターをWebページに埋め込む

Created at:

Visual Studio CodeのシンタックスハイライトをWebページで使うという話を少し前に書きましたが、 今回はエディタ部分をWebページに埋め込んでみようという話です。

前にも少し触れましたが、Visual Studio Codeのエディター部分、いわゆるMonacoはVisual Studio Code以前からいろいろなところで埋め込まれて使われています。 Visual Studio Online "Monaco"を始め、TypeScript PlaygroundWinJS Playground、IE/EdgeのF12開発者ツールなどなど、 エディター部分の出来の良さというか埋め込んで使われることが念頭に置かれている感じがします。

動作サンプル

まずは動作するサンプルを貼り付けてみるので、触ってみてください。

適当なところでCtrl/Cmd + スペースを押せばきちんとコードの補完(IntelliSense)が表示されます。 Visual Studio Codeと同じようにJavaScriptモードはTypeScriptモードが元になっているので、型もちゃんと認識されています(helloメソッドの戻り値の型がstringになっています)。

hの後ろでコード補完を表示した

埋め込み方

肝心の埋め込み方ですが、エディター部分を埋め込むだけであればとても簡単です。

最初にエディターのJavaScriptファイルを用意する必要がありますが、 MimosaにあらかじめVisual Studio Codeからエディター部分をビルドしたものを同梱しているのでそれを使うと簡単です。 リポジトリの monaco-prebuilt 以下を引っこ抜いてきてもよいです。

ファイルの準備ができたらHTMLを書きます。エディターの領域をdiv要素で確保してください。

<div id="editor" style="height:200px;"></div>

エディターの表示サイズはエディターにしてほしい部分の要素のサイズによって確定するので、空の要素の場合、高さが0で何も表示できなくなります。 例えばコードの長さに応じた高さ、といったことはできないので注意が必要です。 また、このエディターとなりたい部分の要素の内容は空にしておいてください(エディターが実体化されても残るので)。

次にJavaScriptでMonacoエディターを生成します。

<script>
    require.config({
        baseUrl: '/mimosa/', // これは例によってMonaco(vsディレクトリ)が入っているディレクトリ
    });

    require(["vs/editor/editor.main"], function () {
        // エディター領域を取得する
        var editorE = document.querySelector('#editor');

        // エディターに表示する文字列を組み立てる
        var content = [
            '"use strict";',
            'const message = "コンニチハ!";',
            'class Greeter {',
            '    /** ハロー!! */',
            '    hello() {',
            '        return message;',
            '    }',
            '}',
            'new Greeter().h'
        ].join('\n');

        // エディターを生成する
        var editor = Monaco.Editor.create(editorE, {
            value: content,
            mode: "javascript",
            readOnly: false,
            //theme: "vs-dark",
            scrollBeyondLastLine: false,
            automaticLayout: true,
            autoSize: true,
            scrollbar: {
                handleMouseWheel: true
            }
        });

        // 何かイベントに応じて編集した文字列を取得する
        // console.log(editor.getValue());
    });
</script>

実質的にやることはrequire(["vs/editor/editor.main"])で読み込んで、Monaco.Editor.create メソッドでエディターを生成するということだけです。なんとお手軽。

Monaco.Editor.create メソッドの第二引数はオプションとなるオブジェクトです。 valueが初期文字列、modeが言語(csharpとかjavascriptとか)、readOnlyは編集可能かどうか、それ以外はICommonEditorOptions インターフェースあたりをご覧ください。

これだと表示するだけですので、最終的には編集結果を取得するというのが普通です。 Monaco.Editor.create メソッドの戻り値であるエディターのインスタンスに対して、 getValue メソッドを呼び出すことで編集中の文字列を取得できます。

まとめ

というわけで割と手軽に高性能エディターを自分のアプリケーションやWebページに組み込めるようになりました。 一部Visual Studio Codeの本体側に依存している機能(diagnostics)などもあるのでそういうものは使えなかったりするのですが、 それはそれとしても利用価値はあるのではと思います。

今回は単純に埋め込むところまでなので、次はこのエディターの拡張方法について書くかもしれません。

雑なHTTPサーバーをC#で書いてみる

Created at:

人生のうち一度はHTTPサーバーを書くという話をしたら意外とそうでもなくてあれー?ってなったのですが、それはそれとして。

「HTTPサーバーには人生の大切な事が全てつまっている」と言った人がいるかどうかは定かではないですが、 雑なサーバーを書いてみるのはまあまあ簡単で理解も進んで面白いのでおすすめですよ、というわけで書いてみましょうという話。

アプリにWebサーバーを組み込みたくなったりして雑なHTTPサーバーを書いてみることというのがたまにあります。 そんな時、現代ではどの言語やプラットフォームでもパッケージやライブラリであっという間にサーバーを組み込めるので自分で書くことはないかもしれません。

ですが今時だとArduinoのような省メモリであることを必要とされる環境でもHTTPサーバーが必要になったとき、 ほぼ固定機能のHTTPサーバーを実装してAPIやWebページからコントロールできるように…ということもよくあると思いますので経験や知識は無駄ではないと思います。多分。

この記事ではC#で書きますが、お好みのプラットフォームなどで書いてみるとよいかと思います。

対象読者

雑なHTTPサーバーとは

おおよそこんな感じ。

要するにお遊びなのでRFCとかも読まないし実際適当です。ほとんどのブラウザーなどからおおよそアクセスして何か表示できればよい程度を目指します。

ただし外部に出ると死ぬ(セキュリティーとか)ような感じもあるのでまじめに使う前提ではありません。

流れとHTTPについて

流れについて一応雑に説明すると…

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPクライアント: サーバーへ接続する
  3. HTTPサーバー: クライアントからの接続を受け入れる
  4. HTTPクライアント: サーバーへリクエストを送信する
  5. HTTPサーバー: クライアントからのリクエストを解釈する
  6. HTTPサーバー: レスポンスをクライアントへ送信する
  7. HTTPクライアント: レスポンスを解釈する

となっています。ですのでHTTPサーバー部分を作るので以下の部分を作ればよいことになります。

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPサーバー: クライアントからの接続を受け入れる
  3. HTTPサーバー: クライアントからのリクエストを解釈する
  4. HTTPサーバー: レスポンスをクライアントへ送信する

HTTPについてですが、ご存知の通りリクエストとレスポンスというメッセージを送受信するプロトコルです。

リクエストメッセージはこんな感じです。

POST / HTTP/1.0[CR][LF]
User-Agent: Nantoka/1.0[CR][LF]
X-Nantoka: Kantoka[CR][LF]
[CR][LF]
Body

CR+LFで区切られていて、CR+LFだけの行が来るまでがヘッダー部分、それ以降がボディ部分となっていてあったりなかったりする部分です。

レスポンスメッセージはこんな感じ。おおよそ同じです。一行目(スタート行)がちょっと違いますがそれ以外の形は同じです。

HTTP/1.0 200 OK[CR][LF]
Server: Nantoka/1.0[CR][LF]
X-Hauhau: Maumau[CR][LF]
[CR][LF]
Body

詳しくはRFC 7230をご覧ください(HTTP 1.1ですが)。実は折り返せたりして(obsoleteだけど)、本当にまじめに実装しようとすると結構面倒だったりします。

サーバーを書いてみる

と、雑な説明をしたところでサーバーを書いてみます。

クライアントからの接続を待ち受ける

まずは「クライアントからの接続を待ち受ける」部分を作ってみます。

クライアントからの接続というのはTCP接続を待ち受けるいわゆるLISTENするということで一般的にはSocketを使ったりします。 とはいえSocket直接というのはプリミティブすぎるので.NET FrameworkにはTcpListenerクラスという便利クラスが用意されているのでそれを使うと簡単です。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

接続待ち受けを開始するにはTcpListenerをどのIPアドレスとポートで待ち受けるか指定して作って、Startメソッドを呼び出すだけです。

ここではLoopback(127.0.0.1と::1、いわゆるlocalhost)のポート12345で待ち受ける指定にしています。 Loopbackにしておくと自身のコンピューターからしか接続できない状態となりますが、Loopback以外にはAny(何でも)や特定のIPアドレス(イーサネットアダプタのアドレス)などを指定できます。

そして実行してブラウザーから http://localhost:12345/ にアクセスすると……接続できませんとも言わずに何も起きないはずです。

クライアントからの接続を受け入れる

TcpListenerで待ち受ける状態になっても、「クライアントからの接続を受け入れる」処理がないのでクライアントはまだ待たされている状態です。

というわけで受け入れる処理を書きます。AcceptTcpClientAsyncメソッドを呼び出すことで受け入れが行われ、クライアントとの接続であるTcpClientクラスのインスタンスが返ってきます。 返ってきたTcpClientはIDisposableを実装しているのでusingしておきます。Disposeすればクライアントとの接続を切断できます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
{
    // 接続元を出力しておく
    Console.WriteLine(tcpClient.Client.RemoteEndPoint);
}

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

このコードを実行してブラウザーから http://localhost:12345/ にアクセスすると……多分すぐにアクセスできませんでしたというメッセージが表示されるようになるはずです。

これでデータの流れはありませんが接続の「待ち受け」→「受け入れ」→「切断」の一連ができました。

しかし、この実装では1回しか受け入れられないのでもう一度アクセスすると突き刺さります。ということで何度も受け入れられるように無限ループにしておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);
    }
}

クライアントからのリクエストを解釈する(フリをする)

次はリクエストを読み出します。が、とりあえずいったん読んで解釈したフリをすることにします。

ほとんどのブラウザーやユーザーエージェントのリクエストはGETリクエストであって、返したものをそのまま表示したり解釈したりするので最悪読み捨てても大丈夫だったりします。

クライアントとのデータの送受信はTcpClientのGetStreamメソッドで得られるStreamのインスタンスを利用します。このStreamを読めばクライアントから受信、書き込めばクライアントへ送信ということです。もちろんStreamなのでStreamReader/StreamWriterを通すと簡単に読み書きできます。

StreamReaderを作ったら、ReadLineAsyncメソッドでヘッダー部分の終わり、つまりCR+LFのみの行(=空行)まで読み込みます。 その際、読み込んだ行をコンソールなどにデバッグ目的で適当に書き出しておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読む
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));
    }
}

このコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみると、コンソールなどに以下のようなブラウザーのリクエストが出力されるはずです。

GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, image/jxr, */*
Accept-Language: en-US,en;q=0.7,ja;q=0.3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.14251
Accept-Encoding: gzip, deflate
Host: localhost:12345
Connection: Keep-Alive

とりあえず今のところはこれでよいということにします。

レスポンスをクライアントへ送信する

いよいよクライアントに対してレスポンスを返します。

レスポンスは1行目がステータスライン(ステータスコードとかを含む行)、その後ヘッダー部分、ボディ部分を送信すれば出来上がりです。つまりStreamWriterに対して書き込んでいくだけの簡単なお仕事です。

200なステータスで、Content-Typeが指定されていて、ボディがあれば大体受け入れてもらえます。というわけでこんな感じのレスポンスを返すということにします。

HTTP/1.0 200 OK
Content-Type: text/plain; charset=UTF-8

Hello! Konnichiwa!

これを今までのコードに組み込むとこうなります。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読んで捨てる
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));

        // レスポンスを返す
        // ステータスライン
        await writer.WriteLineAsync("HTTP/1.0 200 OK");
        // ヘッダー部分
        await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
        await writer.WriteLineAsync(); // 終わり
        // これ以降ボディ
        await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
    }
}

そしてこのコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみます。"Hello! Konnichiwa!"と表示されたら成功です。

というわけで超雑なHTTPサーバーができました。なんと32行。この通り、データを返すだけならばとても簡単ですね。あとはこれをベースにいじって遊んでみるとよいのではないでしょうか。

まとめ

HTTPサーバーを書いてみると、クライアントとサーバー、接続やリクエスト、レスポンスといったものを少し身近に感じることができるようになると思います。 一度書いていじってみるのは個人的にはとてもおすすめなのです。Extraとしてちょっといじったものを載せておきます。

Extra: リクエストを解釈する

ヘッダー部分を読み捨てましたが、ヘッダー部分(とリクエストライン)を解釈するともっとそれっぽくなります。

リクエストラインやヘッダー部分にはリクエストパスやリクエストに関する情報が含まれています。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // ヘッダー部分を全部読んでおく
        var requestHeaders = new List<string>();
        while (true)
        {
            var line = await reader.ReadLineAsync();
            if (String.IsNullOrWhiteSpace(line))
            {
                break;
            }
            requestHeaders.Add(line);
        }

        // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
        var requestLine = requestHeaders.FirstOrDefault();
        var requestParts = requestLine?.Split(new[] { ' ' }, 3);
        if (!requestHeaders.Any() || requestParts.Length != 3)
        {
            await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Bad Request");
            return;
        }

        // 接続元を出力しておく
        Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

        // パス
        var path = requestParts[1];
        if (path == "/")
        {
            // / のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
        }
        else if (path == "/hauhau")
        {
            // /hauhau のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hauhau!!");
        }
    }
}

パスが取れれば、ルーティングのテーブルを作って振り分けて…とかもやってみたくなりますね。

Extra: 複数リクエストをさばく

今までの実装は async/await を使ってはあったもののリクエストを受けて流す部分は1つなので並列で処理を実行することができません。 例えばある一つのリクエストの処理に時間がかかっている間はほかのリクエストを受け入れることができなくなるということです。

そこで.NET FrameworkにはTaskという便利なものがあるので空いているスレッドに処理を回すことができます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    var tcpClient = await tcpListener.AcceptTcpClientAsync();

    // 接続を受け入れたら後続の処理をTaskに放り込む
    Task.Run(async () =>
    {
        using (var stream = tcpClient.GetStream())
        using (var reader = new StreamReader(stream))
        using (var writer = new StreamWriter(stream))
        {
            // ヘッダー部分を全部読んでおく
            var requestHeaders = new List<string>();
            while (true)
            {
                var line = await reader.ReadLineAsync();
                if (String.IsNullOrWhiteSpace(line))
                {
                    break;
                }
                requestHeaders.Add(line);
            }

            // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
            var requestLine = requestHeaders.FirstOrDefault();
            var requestParts = requestLine?.Split(new[] { ' ' }, 3);
            if (!requestHeaders.Any() || requestParts.Length != 3)
            {
                await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Bad Request");
                return;
            }

            // 接続元を出力しておく
            Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

            // パス
            var path = requestParts[1];
            if (path == "/")
            {
                // / のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
            }
            else if (path == "/hauhau")
            {
                // 遅い処理をシミュレートするマン
                await Task.Delay(5000); // 5秒

                // /hauhau のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hauhau!!");
            }
        }
    });
}

まじめにやるときにはTaskと接続の管理とかが必要ですが、こんな感じで並列化ができます。

ちなみに~Async(非同期メソッド)の場合、IO待ちの際にスレッドを手放すことが多くなるので効率よく処理できるようになります。

ご報告

Created at:

ガルパンはいいぞ