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