2014-09-15T22:54:19の更新内容

programming/netfx/tips/echo_server/index.wiki.txt

current previous
1,464 0,0
+
${smdncms:title,echoサーバーを作る}
+
${smdncms:header_title,echoサーバーを作る (C#・VB)}
+
${smdncms:keywords,C#,VB.NET,Socket,echoサーバー,TCP,クライアント・サーバー}
+
${smdncms:tags,api/.net,lang/c#,lang/vb}
+
${smdncms:document_versions,codelang=cs,codelang=vb}
+

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

          
+
#googleadunit
+

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

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

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

          
+
#tabpage(codelang=cs)
+
#code{{
+
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);
+

          
+
  public 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);
+
  }
+
}
+
}}
+
#tabpage(codelang=vb)
+
#code{{
+
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)
+

          
+
  Public 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
+
}}
+
#tabpage-end
+

          
+
#prompt(実行結果例){{
+
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``と入力すればサーバーに接続することができる。 接続後、適当な文字列を入力してサーバーに送信すると、同じ文字列が返送されてくる。
+

          
+
#prompt(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.
+
}}
+

          
+
このサンプルでは&msdn(netfx,member,System.Net.Sockets.Socket.Send){Send};・&msdn(netfx,member,System.Net.Sockets.Socket.Receive){Receive};メソッドを使って送受信を行っているが、送受信内容を[[Streamクラス>programming/netfx/stream/0_abstract]]で扱いたい場合は&msdn(netfx,type,System.Net.Sockets.NetworkStream){NetworkStreamクラス};を使用することができる。 ``Socket``インスタンスを``NetworkStream``クラスのコンストラクタに渡してインスタンスを作成することにより、``NetworkStream``を使った送受信を行えるようになる。 また``NetworkStream``から[[StreamReader・StreamWriter>programming/netfx/stream/2_streamreader_streamwriter]]を作成すれば、``StreamReader``・``StreamWriter``を使ったファイルの読み書きと同様に送受信することもできるようになる。
+

          
+

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

          
+
#tabpage(codelang=cs)
+
#code{{
+
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];
+

          
+
  public 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);
+
  }
+
}
+
}}
+
#tabpage(codelang=vb)
+
#code{{
+
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
+

          
+
  Public 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
+
}}
+
#tabpage-end
+

          
+
#prompt(実行結果例){{
+
connected to 127.0.0.1:22222
+
Hello, world!
+
> Hello, world!
+
こんにちわ、世界!
+
> こんにちわ、世界!
+
bye bye
+
> bye bye
+
^Z
+
}}
+

          
+

          
+