LoginSignup
5
6

More than 5 years have passed since last update.

dotnet コマンド Ubuntu 14.04 と Docker でも動く、CoreClr Socket Application作ってみた

Posted at

この記事は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から

Ubuntu14.04
$ 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!

開発中のせいか、依存関係の警告を吐き出しながら、動いているようです。

なんか作ってみよう

これだと面白くないので、ちょっとしたプログラムを作ってみることにしました。

dnxTcpEcho

Socketを使って、TCP5999で待ち受け、
クライアントの接続を受けたら、クライアントが送ってきたものをそのまま返す。

あらかじめ
これを動かしてみます

Ubuntu14.04
$ 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」します。

client
hheelllloo wwoorrlldd

雑なキーストローク単位のエコーなのですごいことになります。
サーバー側ではこんな感じに接続きたよーの表示。

Server
InitSocket 0.0.0.0:5999
Accept Client [::ffff:xxx.xxx.xxx.xxx]:63508

おおーたのしー!
実はちょっと小細工していて・・・
サーバー側で「hello world」入力すると。

client1
hello world
client2
hello world

おおーちゃんと動いてるー。
サーバー側止めるときは、Ctrl+Cで

Docker でも動くか

Dockerの場合は、イメージを作ります。

Dockerfileで構築する話

まず、イメージを構築するための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

dotnet0.0.1-alpha-onbuild
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である前提になっています。

Ubuntu14.04
$ 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

イメージは、できましたー!確認してみましょう

Ubuntu14.04
$ 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にマッピングします。

Ubuntu14.04
$ sudo docker run -d -t -p 6999:5999 dnxtcpecho
5255c7d3a30e7217f7e82ec1edb.......

確認

Ubuntu14.04
$ 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につないで遊んでみます。

client
hheelllloo wwoorrlldd

ソースコード

ちょっとだけ新機能使ったりと面白味はありませんが、C#カレンダーなので、少しでも成分出しておきます!
https://github.com/darkcrash/dnxTcpEcho/blob/master/src/dnxTcpEcho/Program.cs
コメントがないので、下記に簡単に記述しておきました。

実はライブラリ依存関係で困った、parallelを意地でも使ってみた。
このparallelのスレッド動作が気持ちいいーと感じたから依存関係困ってでも使いたかっただけかもしれない。

Program.cs.Main
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));
        }

コンソール用にスレッドを開け渡すために、分離

Program.cs.サーバーソケット
        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();

        }

クライアント用のタスク

Program.cs.クライアントソケット
        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からの取得なので、冒頭のリンクから該当アセンブリのページで確認することができます。

project.json
{
  "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

本当はこれもやりたかったのですが
今回のソースで

Ubuntu14.04
dotnet compile -n

とした場合、一部のアセンブリがないというエラーになり失敗しました。
これをした場合、Connect();の紹介ではネイティブなイメージを作ることができる!
という面白いものでした。
dotnet run ではオーバーヘッドがかなりあり、私の試した環境では、早くても起動まで数秒かかりますが
これが大幅になくなるとすれば、もう少し変わった使い方もできそうな気がします。

最後に

そんなこんなで、ほぼ遊びでRC1を堪能しました。
まだまだ、見えない部分や、ドキュメントがそろっていないところもあるようですが
このOSS化されて、クロスプラットフォームとしては少し特殊な動作方法(!virtualMachine && Runtime => native)をしているCoreCLRが楽しみな日々を過ごしております。

クリスマスが近づいていく一日の出来事でした。
最後まで読んでくださってありがとうございました。

明日はneueccさん、よろしくお願いします!

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6