この記事はC# Advent Calendar 2015の9日目となる記事です。
前書き
先月11月のMicrosoftイベントConnect();で、
ASP.NET5のRC版が発表されたようですね!
Connect();
見ていくとこのASP.NET5はCoreCLRなるもので動くと。
CoreCLRは、軽量化され(AppDomain、セキュリティなどの機能を排除した)
OSS化された本家のサブセットのようなもののようです。
.NET Core
それまでGAC(グローバルアセンブリキャッシュ)に存在していたアセンブリは、
全てNuGetで取得するというものに。
アセンブリのバージョンはそれぞれに存在し、それぞれ自由に決めることができる。
とはいっても、依存関係があるようです。
NuGet
(個々のページに、これ以上が必要だよ!という記載がある)
ASP.NET5の動いているCoreCLRでは、ASP.NET5じゃないものも動かすことができる。
つまり、C#のコードが色々なところで動くと!
dotnet コマンドと、dnu dnx があるのですが
ここでは、dotnet コマンドでがんばってみることにします。
簡単に試せそうなもの
まず、簡単に試せそうなのが・・・
.NET Core - Getting Started
Ubuntu14.04でやってみることにします。
他のバージョンでも試してみましたが
上記リンクの手順を1~2までがうまくいきませんでした。
(ライブラリの依存関係でapt-get install に失敗するとか)
上記リンクの手順3から
$ dotnet init
$ dotnet restore
$ dotnet run
project.json(9,22): warning NU1012: Dependency conflict. Microsoft.NETCore.Runtime.CoreCLR 1.0.1-beta-23516 expected System.IO 4.0.11-beta-23516 but got 4.0.10-beta-23109
project.json(9,22): warning NU1012: Dependency conflict. Microsoft.NETCore.Runtime.Native 1.0.1-beta-23516 expected System.IO 4.0.11-beta-23516 but got 4.0.10-beta-23109
Hello World!
開発中のせいか、依存関係の警告を吐き出しながら、動いているようです。
なんか作ってみよう
これだと面白くないので、ちょっとしたプログラムを作ってみることにしました。
Socketを使って、TCP5999で待ち受け、
クライアントの接続を受けたら、クライアントが送ってきたものをそのまま返す。
あらかじめ
これを動かしてみます
$ git clone https://github.com/darkcrash/dnxTcpEcho.git
Cloning into 'dnxTcpEcho'...
remote: Counting objects: 123, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 123 (delta 0), reused 0 (delta 0), pack-reused 121
Receiving objects: 100% (123/123), 15.23 KiB | 0 bytes/s, done.
Resolving deltas: 100% (57/57), done.
Checking connectivity... done.
$ cd dnxTcpEcho
$ dotnet restore
Restore complete, 1296ms elapsed
$ cd src/dnxTcpEcho
$ dotnet run
InitSocket 0.0.0.0:5999
うごいたー!っと。
クライアントは、telnet で5999につなぎ「hello world」します。
hheelllloo wwoorrlldd
雑なキーストローク単位のエコーなのですごいことになります。
サーバー側ではこんな感じに接続きたよーの表示。
InitSocket 0.0.0.0:5999
Accept Client [::ffff:xxx.xxx.xxx.xxx]:63508
おおーたのしー!
実はちょっと小細工していて・・・
サーバー側で「hello world」入力すると。
hello world
hello world
おおーちゃんと動いてるー。
サーバー側止めるときは、Ctrl+Cで
Docker でも動くか
Dockerの場合は、イメージを作ります。
Dockerfileで構築する話
まず、イメージを構築するためのDockerfileを作ります。
FROM microsoft/dotnet:0.0.1-alpha-onbuild
WORKDIR /dotnetapp/src/dnxTcpEcho
EXPOSE 5999
このFROMは、C#でいう継承元みたいなベースのイメージを指しているようです。
以下に簡単な説明がありました。
DockerHub - microsoft/dotnet
これだけだと、わからないので、この継承元を少し見てみます。
dotnet-docker / 0.0.1-alpha / onbuild / Dockerfile
FROM microsoft/dotnet:0.0.1-alpha
RUN mkdir -p /dotnetapp
WORKDIR /dotnetapp
ENTRYPOINT ["dotnet", "run"]
ONBUILD COPY . /dotnetapp
ONBUILD RUN dotnet restore
さらにベースとなっているものがありますが、これはdotnet
を使えるようにするapt-get install
なものが入っていました。割愛します。
DockerのONBUILDというものを使って、カレントディレクトリのもの
コピーし、それをdotnet restore
実行時は、dotnet run
というもののようです。
ちょっとした失敗
準備したdnxTcpEchoのディレクトリがサンプルとしては、
あまりよくなかったことにあとで気づきました。
WORKDIR /dotnetapp/src/dnxTcpEcho
このようにしているのは、Dockerから見たエントリポイントdotnet run
が、動かなくなるためです。
dotnet run
はカレントディレクトリのproject.jsonを見ますので、
これを正しいディレクトリに書き換えているという意味になります。
dotnet:0.0.1-alpha-onbuild
が提供しているものは、dockerfile、project.jsonを含めてカレントディレクトリであるような前提になっています。
ディレクトリを合わせると、FROM書くだけでよいくらいまでになるかもしれません。
動かしてみよう
Dockerを事前にインストールしておきます。
Get Started with Docker for Linux
ビルド
まずは、ビルドします。
用意したDockerfileは、このときのカレントディレクトリがdnxTcpEchoである前提になっています。
$ git clone https://github.com/darkcrash/dnxTcpEcho.git
$ cd dnxTcpEcho
$ sudo docker build -t dnxtcpecho .
Sending build context to Docker daemon 312.8 kB
Sending build context to Docker daemon
Step 0 : FROM microsoft/dotnet:0.0.1-alpha-onbuild
# Executing 2 build triggers
Trigger 0, COPY . /dotnetapp
Step 0 : COPY . /dotnetapp
Trigger 1, RUN dotnet restore
Step 0 : RUN dotnet restore
---> Running in 5e9ae3dae6ec
Microsoft .NET Development Utility CoreClr-x64-1.0.0-rc1-16048
(中略)
Writing lock file /dotnetapp/src/dnxTcpEcho/project.lock.json
Restore complete, 27672ms elapsed
Feeds used:
https://api.nuget.org/v3-flatcontainer/
Installed:
55 package(s) to /root/.dnx/packages
---> 0964ce32bcca
Removing intermediate container 87c615aa7352
Removing intermediate container 5e9ae3dae6ec
Step 1 : WORKDIR /dotnetapp/src/dnxTcpEcho
---> Running in 1ca69492751b
---> 4a483c3687dc
Removing intermediate container 1ca69492751b
Step 2 : EXPOSE 5999
---> Running in 3634aeaf7dcc
---> 6034bd92aa24
Removing intermediate container 3634aeaf7dcc
Successfully built 6034bd92aa24
イメージは、できましたー!確認してみましょう
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
dnxtcpecho latest 6034bd92aa24 1 minutes ago 876.2 MB
microsoft/dotnet 0.0.1-alpha-onbuild 323fa5c00903 99 days ago 672 MB
ちゃんといますね。
実行
次に動かしてみようと思います。
変化をつけるために、ポートを6999にマッピングします。
$ sudo docker run -d -t -p 6999:5999 dnxtcpecho
5255c7d3a30e7217f7e82ec1edb.......
確認
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5255c7d3a30e dnxtcpecho:latest "dotnet run" 53 seconds ago Up 51 seconds 0.0.0.0:6999->5999/tcp kickass_almeida
うごいたー!
Telnetで6999につないで遊んでみます。
hheelllloo wwoorrlldd
ソースコード
ちょっとだけ新機能使ったりと面白味はありませんが、C#カレンダーなので、少しでも成分出しておきます!
https://github.com/darkcrash/dnxTcpEcho/blob/master/src/dnxTcpEcho/Program.cs
コメントがないので、下記に簡単に記述しておきました。
実はライブラリ依存関係で困った、parallelを意地でも使ってみた。
このparallelのスレッド動作が気持ちいいーと感じたから依存関係困ってでも使いたかっただけかもしれない。
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;
namespace dnxTcpEcho
{
public class Program
{
private static Socket server;
private static List<Socket> clientList = new List<Socket>();
private static object clientListLock = new object();
private static CancellationTokenSource source = new CancellationTokenSource();
private static CancellationToken token = source.Token;
public static void Main(string[] args)
{
// サーバーソケットの初期化
InitSocket();
// コンソールの入力を処理する
while (true)
{
try
{
// コンソールのキー入力を受け取る。ブロックする
var inp = Console.ReadKey(true);
if (inp.Key == ConsoleKey.Escape) break;
var data = System.Text.Encoding.ASCII.GetBytes(inp.KeyChar.ToString());
// 並列化でクライアントに一斉送信するタスク
var result = Parallel.ForEach(clientList, (client) => client.Send(data, SocketFlags.None));
}
catch (InvalidOperationException)
{
// Dockerなどコンソールを無効化された場合の応急処置
Task.Delay(1000);
}
}
Console.WriteLine("Shutdown");
// 荒っぽくすべてのタスクをキャンセルし待機
source.Cancel(true);
source.Token.WaitHandle.WaitOne();
// すべてのクライアント接続をシャットダウンする
var resultShutdown = Parallel.ForEach(clientList, (client) => client.Shutdown(SocketShutdown.Both));
}
コンソール用にスレッドを開け渡すために、分離
private static void InitSocket()
{
// TCP 5999 でバインド、待ち受け開始
server = new Socket(SocketType.Stream, ProtocolType.Tcp);
var endp = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 5999);
server.Bind(endp);
server.Listen(100);
Console.WriteLine($"{nameof(InitSocket)} {endp}");
// クライアントからの接続をポーリングするタスク
Action loopAccept = () =>
{
while (true)
{
// クライアント接続があるまでブロックされる
var client = server.Accept();
// 接続確立後は、個別のタスクとして処理
var t = new Task(_ => InitSocketClient(client), token, TaskCreationOptions.LongRunning);
t.Start();
}
};
// ポーリングするタスクの開始
var loopAcceptTask = new Task(loopAccept, token, TaskCreationOptions.LongRunning);
loopAcceptTask.Start();
}
クライアント用のタスク
private static void InitSocketClient(Socket client)
{
// 同時接続・切断時のコレクション破壊防止の排他制御
lock (clientListLock)
clientList.Add(client);
var endp = client.RemoteEndPoint.ToString();
Console.WriteLine($"Accept Client {endp}");
byte[] buf = new byte[1024];
// 無限ループ
while (true)
{
// 受信、ブロックされる
var size = client.Receive(buf);
// サイズ0は切断として処理
if (size <= 0) break;
// そのままクライアントに送信
client.Send(buf, 0, size, SocketFlags.None);
}
// 同時接続・切断時のコレクション破壊防止の排他制御
lock (clientListLock)
clientList.Remove(client);
Console.WriteLine($"Close Client {endp}");
}
}
}
というわけで、少し強引ですが3メソッドそれぞれに役割を分けてみた感じです。
本当は、コンソール側にも排他制御が必要だけど、
今回は気にしない。
project.json
ここで、必要となるアセンブリを決めるのですが
frameworks.dnxcore50.dependenciesの依存関係はある程度しっかり固めないと・・・
新しいバージョンがリリースされた時に動かなくなることもあるかもしれません。
とはいえ、初回に末尾のビルド番号合わせるとかくらいしかできることはないかもしれませんね。
ここは、NuGetからの取得なので、冒頭のリンクから該当アセンブリのページで確認することができます。
{
"version": "1.0.0-*",
"description": "dnxTcpEcho Console Application",
"authors": [ "y.m" ],
"tags": [ "" ],
"projectUrl": "",
"licenseUrl": "",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
},
"commands": {
"dnxTcpEcho": "dnxTcpEcho"
},
"frameworks": {
"dnxcore50": {
"dependencies": {
"System.Collections": "4.0.11-beta-23516",
"System.Console": "4.0.0-beta-23516",
"System.Threading": "4.0.11-beta-23516",
"System.Threading.Tasks.Parallel": "4.0.1-beta-23516",
"Microsoft.CSharp": "4.0.0",
"System.Net.Sockets": "4.1.0-beta-23516",
"System.Net.Primitives": "4.0.11-beta-23516",
"System.Net.NameResolution": "4.0.0-beta-23516",
"System.Private.Networking": "4.0.1-beta-23516",
"System.Runtime": "4.0.21-beta-23516"
}
}
}
}
native compile
本当はこれもやりたかったのですが
今回のソースで
dotnet compile -n
とした場合、一部のアセンブリがないというエラーになり失敗しました。
これをした場合、Connect();の紹介ではネイティブなイメージを作ることができる!
という面白いものでした。
dotnet run ではオーバーヘッドがかなりあり、私の試した環境では、早くても起動まで数秒かかりますが
これが大幅になくなるとすれば、もう少し変わった使い方もできそうな気がします。
最後に
そんなこんなで、ほぼ遊びでRC1を堪能しました。
まだまだ、見えない部分や、ドキュメントがそろっていないところもあるようですが
このOSS化されて、クロスプラットフォームとしては少し特殊な動作方法(!virtualMachine && Runtime => native)をしているCoreCLRが楽しみな日々を過ごしております。
クリスマスが近づいていく一日の出来事でした。
最後まで読んでくださってありがとうございました。
明日はneueccさん、よろしくお願いします!