System.Net.Sockets.SocketクラスでTCPクライアント・サーバーを実装する方法を理解するためのシンプルな例としてC#・VBでechoサーバーを作る。 echoサーバーは、クライアントから送信された内容をそのまま返送するだけのサーバー。

以下のコードは.NET Framework 4.5、Mono 3.8で動作確認済み。

§1 echoサーバー

以下のコードでは、サーバーに接続してきた複数のクライアントを同時に処理できるよう、個別にスレッドを作成して処理する。 コード中では無効にしているStartSessionInNewAppDomainメソッドを使用すると、クライアントごとにアプリケーションドメインを作成して処理するようになる。 文字コードはシステムのデフォルト(Encoding.Default)を使用している。 また、例外処理等は省略している。

このコードではサーバー側のアドレスとしてローカルループバックアドレス(127.0.0.1, IPAddress.Loopback)を使用しているため、同一マシンからでしかサーバーに接続できない。 IPAddress.Anyや具体的なアドレスを指定(IPAddress.Parse("192.168.0.xxx")とするなど)すれば他のマシンからも接続できるようになる。

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;

class EchoServer {
  // ローカルループバックアドレス(127.0.0.1)のポート22222番を使用する
  private static readonly IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 22222);

  static void Main()
  {
    // TCP/IPでの通信を行うソケットを作成する
    using (var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {
      // TIME_WAIT状態のソケットを再利用する
      server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

      // ソケットをアドレスにバインドする
      server.Bind(endPoint);

      // 接続の待機を開始する
      server.Listen(10);

      Console.WriteLine("server started ({0})", server.LocalEndPoint);

      var connectionCount = 0;

      for (;;) {
        // クライアントからの接続要求を待機する
        var client = server.Accept();

        connectionCount++;

        Console.WriteLine("client accepted (#{0}, {1})", connectionCount, client.RemoteEndPoint);

#if true
        // 新しくスレッドを作成してクライアントを処理する
        StartSessionInNewThread(connectionCount, client);
#else
        // アプリケーションドメインを作成してクライアントを処理する
        StartSessionInNewAppDomain(connectionCount, client);
#endif
      }
    }
  }

  private static void StartSessionInNewThread(int clientId, Socket client)
  {
    var session = new Session(clientId, client);

    session.Start();
  }

  private static void StartSessionInNewAppDomain(int clientId, Socket client)
  {
    // ソケットの複製を作成し、このプロセスのソケットを閉じる
    var duplicatedSocketInfo = client.DuplicateAndClose(Process.GetCurrentProcess().Id);

    // 新しいアプリケーションドメインを作成してクライアントを処理する
    var newAppDomain = AppDomain.CreateDomain(string.Format("client #{0}", clientId));

    var session =(Session)newAppDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName,
                                                               typeof(Session).FullName,
                                                               false,
                                                               BindingFlags.Default,
                                                               null,
                                                               new object[] {clientId, duplicatedSocketInfo},
                                                               null,
                                                               null);

    session.Start();
  }
}

// セッションを処理するクラス
// (MarshalByRefObjectの継承はアプリケーションドメインをまたがってインスタンスを使用するために必要)
public class Session : MarshalByRefObject {
  private readonly int id;
  private Socket client;

  public Session(int id, SocketInformation socketInfo)
  {
    this.id = id;
    this.client = new Socket(socketInfo);
  }

  public Session(int id, Socket socket)
  {
    this.id = id;
    this.client = socket;
  }

  public void Start()
  {
    // スレッドを作成し、クライアントを処理する
    var t = new Thread(SessionProc);

    t.Start();
  }

  private void SessionProc()
  {
    Console.WriteLine("#{0} session started", id);

    try {
      var buffer = new byte[0x100];

      for (;;) {
        // クライアントから送信された内容を受信する
        var len = client.Receive(buffer);

        if (0 < len) {
          // 受信した内容を表示する
          Console.Write("#{0}> {1}", id, Encoding.Default.GetString(buffer, 0, len));

          // 受信した内容した内容をそのままクライアントに送信する
          client.Send(buffer, len, SocketFlags.None);
        }
        else {
          // 切断された
          client.Close();

          break;
        }
      }
    }
    catch (SocketException ex) {
      if (ex.SocketErrorCode != SocketError.ConnectionReset)
        // 切断された以外の場合では例外を再スローする
        throw;
    }

    Console.WriteLine("#{0} session closed", id);
  }
}
実行結果例
server started (127.0.0.1:22222)
client accepted (#1, 127.0.0.1:51482)
#1 session started
#1> Hello, world!
#1> こんにちわ、世界!
client accepted (#2, 127.0.0.1:51483)
#2 session started
#2> hello!
#2> bye!
#2 session closed
#1> bye bye
#1 session closed

シェルでtelnet 127.0.0.1 22222と入力すればサーバーに接続することができる。 接続後、適当な文字列を入力してサーバーに送信すると、同じ文字列が返送されてくる。

telnetを使って接続する例
$ telnet 127.0.0.1 22222
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello, world!
Hello, world!
^]

telnet> quit
Connection closed.

このサンプルではSendReceiveメソッドを使って送受信を行っているが、送受信内容をStreamクラスで扱いたい場合はNetworkStreamクラスを使用することができる。 SocketインスタンスをNetworkStreamクラスのコンストラクタに渡してインスタンスを作成することにより、NetworkStreamを使った送受信を行えるようになる。 またNetworkStreamからStreamReader・StreamWriterを作成すれば、StreamReaderStreamWriterを使ったファイルの読み書きと同様に送受信することもできるようになる。

§2 クライアント

Socketクラスを使ってサーバーに接続し、コンソールから入力された文字列をサーバーに送信する。 送受信を並行して行えるよう、受信は非同期的に行うようにしている。 文字コードはシステムのデフォルト(Encoding.Default)を使用している。 Ctrl+CまたはCtrl+Z(UNIXではCtrl+D)を押せばソケットを閉じてアプリケーションを終了する。

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

class Client {
  // 接続先サーバーのエンドポイント
  private static readonly IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Loopback, 22222);

  // 受信用のバッファ
  private static readonly byte[] buffer = new byte[0x100];

  static void Main()
  {
    // TCP/IPでの通信を行うソケットを作成する
    using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {
      // Ctrl+Cが押された場合はソケットを閉じる
      Console.CancelKeyPress += (sender, args) => socket.Close();

      // 接続する
      socket.Connect(serverEndPoint);

      Console.WriteLine("connected to {0}", socket.RemoteEndPoint);

      // 非同期での受信を開始する
      socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, socket);

      for (;;) {
        // コンソールからの入力を待機する
        var input = Console.ReadLine();

        if (input == null) {
          // Ctrl+Z(UNIXではCtrl+D)が押された場合はソケットを閉じて終了する
          socket.Close();
          break;
        }

        // 入力された内容をサーバーに送信する
        socket.Send(Encoding.Default.GetBytes(input + Environment.NewLine));
      }
    }
  }

  // 非同期受信のコールバックメソッド
  private static void ReceiveCallback(IAsyncResult ar)
  {
    var socket = ar.AsyncState as Socket;

    // 受信を待機する
    var len = socket.EndReceive(ar);

    // 受信した内容を表示する
    Console.Write("> {0}", Encoding.Default.GetString(buffer, 0, len));

    // 再度非同期での受信を開始する
    socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, socket);
  }
}
実行結果例
connected to 127.0.0.1:22222
Hello, world!
> Hello, world!
こんにちわ、世界!
> こんにちわ、世界!
bye bye
> bye bye
^Z