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

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

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);
  }
}
Imports System
Imports System.Diagnostics
Imports System.Net
Imports System.Net.Sockets
Imports System.Reflection
Imports System.Text
Imports System.Threading

Class EchoServer
  ' ローカルループバックアドレス(127.0.0.1)のポート22222番を使用する
  Private Shared ReadOnly endPoint As New IPEndPoint(IPAddress.Loopback, 22222)

  Shared Sub Main()
    ' TCP/IPでの通信を行うソケットを作成する
    Using server As 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)

      Dim connectionCount As Integer = 0

      Do
        ' クライアントからの接続要求を待機する
        Dim client As Socket = server.Accept()

        connectionCount += 1

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

#If True
        ' 新しくスレッドを作成してクライアントを処理する
        StartSessionInNewThread(connectionCount, client)
#Else
        ' アプリケーションドメインを作成してクライアントを処理する
        StartSessionInNewAppDomain(connectionCount, client)
#End If
      Loop
    End Using
  End Sub

  Private Shared Sub StartSessionInNewThread(ByVal clientId As Integer, ByVal client As Socket)
    Dim session As New Session(clientId, client)

    session.Start()
  End Sub

  Private Shared Sub StartSessionInNewAppDomain(ByVal clientId As Integer, ByVal client As Socket)
    ' ソケットの複製を作成し、このプロセスのソケットを閉じる
    Dim duplicatedSocketInfo As SocketInformation = client.DuplicateAndClose(Process.GetCurrentProcess().Id)

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

    Dim instance As Object = newAppDomain.CreateInstanceAndUnwrap([Assembly].GetExecutingAssembly().FullName, _
                                                                  GetType(Session).FullName, _
                                                                  False, _
                                                                  BindingFlags.Default, _
                                                                  Nothing, _
                                                                  New Object() {clientId, duplicatedSocketInfo}, _
                                                                  Nothing, _
                                                                  Nothing)

    Dim session As Session = DirectCast(instance, Session)

    session.Start()
  End Sub
End Class

' セッションを処理するクラス
' (MarshalByRefObjectの継承はアプリケーションドメインをまたがってインスタンスを使用するために必要)
Public Class Session
  Inherits MarshalByRefObject

  Private ReadOnly id As Integer
  Private client As Socket

  Public Sub New(ByVal id As Integer, ByVal socketInfo As SocketInformation)
    Me.id = id
    Me.client = New Socket(socketInfo)
  End Sub

  Public Sub New(ByVal id As Integer, ByVal socket As Socket)
    Me.id = id
    Me.client = socket
  End Sub

  Public Sub Start()
    ' スレッドを作成し、クライアントを処理する
    Dim t As New Thread(AddressOf SessionProc)

    t.Start()
  End Sub

  Private Sub SessionProc()
    Console.WriteLine("#{0} session started", id)

    Try
      Dim buffer(&HFF) As Byte

      Do
        ' クライアントから送信された内容を受信する
        Dim len As Integer = client.Receive(buffer)

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

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

          Exit Do
        End If
      Loop
    Catch ex As SocketException
      ' 切断された以外の場合では例外を再スローする
      If ex.SocketErrorCode <> SocketError.ConnectionReset Then Throw
    End Try

    Console.WriteLine("#{0} session closed", id)
  End Sub
End Class
実行結果例
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を使ったファイルの読み書きと同様に送受信することもできるようになる。

クライアント

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);
  }
}
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.Text

Class Client
  ' 接続先サーバーのエンドポイント
  Private Shared ReadOnly serverEndPoint As New IPEndPoint(IPAddress.Loopback, 22222)

  ' 受信用のバッファ
  Private Shared ReadOnly buffer(&HFF) As Byte

  Shared Sub Main()
    ' TCP/IPでの通信を行うソケットを作成する
    Using socket As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
      ' Ctrl+Cが押された場合はソケットを閉じる
      AddHandler Console.CancelKeyPress, Sub(sender, args) socket.Close()

      ' 接続する
      socket.Connect(serverEndPoint)

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

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

      Do
        ' コンソールからの入力を待機する
        Dim input As String = Console.ReadLine()

        If input Is Nothing Then
          ' Ctrl+Z(UNIXではCtrl+D)が押された場合はソケットを閉じて終了する
          socket.Close()
          Exit Do
        End If

        ' 入力された内容をサーバーに送信する
        socket.Send(Encoding.Default.GetBytes(input + Environment.NewLine))
      Loop
    End Using
  End Sub

  ' 非同期受信のコールバックメソッド
  Private Shared Sub ReceiveCallback(ByVal ar As IAsyncResult)
    Dim socket As Socket = DirectCast(ar.AsyncState, Socket)

    ' 受信を待機する
    Dim len As Integer = socket.EndReceive(ar)

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

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