非同期でTCP このエントリをはてなブックマークに追加

9月 7th, 2008

前回は軽くDnsのメソッドを非同期化してみたので、今回はソケットのブロッキングが発生するメソッドの非同期版を実装してみました。前回は.NET風味な非同期の実装をしたのですが、今回は自分勝手にちょっとやってみました。ちなみに、前回IAsyncResultが理解できないとかなんとか言ってましたが、どうやら.NETにはBeginInvoke,EndInvokeとかいう大変便利そうなメソッドがあるらしく、これを使うと任意のメソッドが非同期で呼び出せるらしいです。これを使って他のBegin…,End…の非同期メソッドが実装されていると予測すると、IAsyncResultが出てくるのは当然だというわけです。

getting...
非同期なので取得中ウインドウを動かせます。当然のことですが。

コードはこちら。前回同様、最新のリポジトリ+展開したNet.zipをablib/src/Classesに放り込むと実行することが出来ます。

#require <Classes/ActiveBasic/Windows/UI/Form.ab>
#require <Classes/ActiveBasic/Windows/UI/Application.ab>
#require <Classes/ActiveBasic/Windows/UI/Button.ab>
#require <Classes/ActiveBasic/Windows/UI/EditBox.ab>
#require <Classes/ActiveBasic/Windows/UI/TaskMsg.ab>
#require <Classes/ActiveBasic/Windows/UI/ListBox.ab>

#require <api_ws2tcpip.sbp>
#require <Classes/System/Net/misc.ab>
#require <Classes/System/Net/Dns.ab>
#require <Classes/System/Net/EndPoint.ab>
#require <Classes/System/Net/IPAddress.ab>
#require <Classes/System/Net/IPHostEntry.ab>
#require <Classes/System/Net/Sockets/misc.ab>
#require <Classes/System/Net/Sockets/Socket.ab>

Imports ActiveBasic.Windows.UI
Imports System
Imports System.Collections.Generic
Imports System.Net
Imports System.Net.Sockets
Imports System.IO

#resource "UI_Sample.rc"

Class MyApplication
	Inherits Form
Public
	Sub MyApplication()
		AddCreate(AddressOf(OnCreate))
	End Sub

Protected
	Override Sub GetCreateStruct(ByRef cs As CREATESTRUCT)
		Super.GetCreateStruct(cs)
		With cs
			.style = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_DLGFRAME Or WS_MINIMIZEBOX
			.cx = 400
			.cy = 400
		End With
	End Sub

Private
	Sub OnCreate(sender As Object, e As CreateArgs)
		Dim wpHFontControl = GetStockObject(DEFAULT_GUI_FONT) As WPARAM

		getButton = New Button
		With getButton
			.Create(This)
			.Text = "取得"
			.Move(280, 5, 80, 30)
			.AddClick(AddressOf(Get_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textField = New EditBox
		With textField
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(10, 10, 175, 20)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		requestField = New EditBox
		With requestField
			.Create(This, ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOVSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 40, 375, 100)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textView = New EditBox
		With textView
			.Create(This, ES_READONLY Or ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOHSCROLL Or ES_AUTOVSCROLL Or WS_HSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 150, 375, 190)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Get_Click(sender As Object, e As Args)
		InvalidForms = True
		text = New Text.StringBuilder
		textView.Text = text.ToString()
		Dns.BeginGetHostEntry(textField.Text, "http", AddressOf(GetHostCallback), Nothing)
	End Sub

	Sub GetHostCallback(ar As IAsyncResult)
		Dim host = Dns.EndGetHostEntry(ar).Host As IPEndPoint
		Dim socket = New Socket(host.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
		socket.ErrorDelegate = AddressOf(SocketDidError)
		socket.BeginConnect(host, AddressOf(SocketDidConnect))
	End Sub

	Sub SocketDidConnect(s As Socket)
		Dim request = requestField.Text + Ex"¥n¥n" As String
		s.BeginSend(request, request.Length, AddressOf(SocketDidSend))
	End Sub

	Sub SocketDidSend(s As Socket, n As Long)
		s.BeginReceive(AddressOf(SocketDidReceive))
	End Sub

	Sub SocketDidReceive(s As Socket, buffer As *Byte, len As Long)
		If len Then
			Dim receive = New String(buffer As PCTSTR, len)
			text.Append(receive)
			s.BeginReceive(AddressOf(SocketDidReceive))
		Else
			s.Close()
			textView.Text = text.ToString()
			InvalidForms = False
		End If
	End Sub

	Sub SocketDidError(s As Socket, ex As Exception)
		MessageBox(0, ex.ToString(), "エラー", MB_OK)
		s.Close()
		InvalidForms = False
	End Sub

	Sub InvalidForms(flag As Boolean)
		textField.Enabled = Not flag
		requestField.Enabled = Not flag
		getButton.Enabled = Not flag
		If flag Then
			getButton.Text = "取得中..."
		Else
			getButton.Text = "取得"
		End If
	End Sub

	getButton As Button
	textField As EditBox
	requestField As EditBox
	textView As EditBox

	text As Text.StringBuilder
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New MyApplication
f.CreateForm()
Winsock.Initialize()
Application.Run(f)
Winsock.Finalize()

「取得」イベントとかの部分。

Sub Get_Click(sender As Object, e As Args)
	InvalidForms = True
	text = New Text.StringBuilder
	textView.Text = text.ToString()
	Dns.BeginGetHostEntry(textField.Text, "http", AddressOf(GetHostCallback), Nothing)
End Sub

Sub GetHostCallback(ar As IAsyncResult)
	Dim host = Dns.EndGetHostEntry(ar).Host As IPEndPoint
	Dim socket = New Socket(host.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
	socket.ErrorDelegate = AddressOf(SocketDidError)
	socket.BeginConnect(host, AddressOf(SocketDidConnect))
End Sub

Sub SocketDidConnect(s As Socket)
	Dim request = requestField.Text + Ex"¥n¥n" As String
	s.BeginSend(request, request.Length, AddressOf(SocketDidSend))
End Sub

Sub SocketDidSend(s As Socket, n As Long)
	s.BeginReceive(AddressOf(SocketDidReceive))
End Sub

Sub SocketDidReceive(s As Socket, buffer As *Byte, len As Long)
	If len Then
		Dim receive = New String(buffer As PCTSTR, len)
		text.Append(receive)
		s.BeginReceive(AddressOf(SocketDidReceive))
	Else
		s.Close()
		textView.Text = text.ToString()
		InvalidForms = False
	End If
End Sub

Sub SocketDidError(s As Socket, ex As Exception)
	MessageBox(0, ex.ToString(), "エラー", MB_OK)
	s.Close()
	InvalidForms = False
End Sub

Sub InvalidForms(flag As Boolean)
	textField.Enabled = Not flag
	requestField.Enabled = Not flag
	getButton.Enabled = Not flag
	If flag Then
		getButton.Text = "取得中..."
	Else
		getButton.Text = "取得"
	End If
End Sub

DNSの名前解決は前回のコードと同様です。そのあとのソケットの部分を今回作りました。今回の実装は、デリゲートの引数に戻り値を入れる実装にしてみました。途中でエラーが発生した場合は、あらかじめソケットに設定しておいたエラー用のデリゲートに飛ぶようにしてあります。

さて、ソケットの基本的な部分がほとんど実装できてきたので、次は何を実装しようか迷います。TCP,UDPソケットのクラス(.NETのTCPClientとか)って必要あるのか、ちょっと疑問だったりします。とりあえず、NetworkStreamの実装には入ろうかどうかってところでしょうか。

非同期に挑戦してみる このエントリをはてなブックマークに追加

9月 1st, 2008

今日は初めて非同期に挑戦してみました。マルチスレッド関連のライブラリは既に出来上がっているので、それを利用したマルチスレッドプログラムで非同期処理をします。とりあえず非同期の雰囲気をつかむために、単純な非同期関数であるDns.BeginGetHostEntryを実装してみました。

非同期的にアドレスを取得

もうライブラリにあたるコードを載せるのは面倒なので、こちらにzipでまとめました。ablic/src/Classesの中に入れてください。
Net.zip

以下、開発中のネットワークライブラリを使用したサンプルコードです。名前空間は暫定System.Netに入れてますが、どこに入れるか考え中です。

#require <Classes/ActiveBasic/Windows/UI/Form.ab>
#require <Classes/ActiveBasic/Windows/UI/Application.ab>
#require <Classes/ActiveBasic/Windows/UI/Button.ab>
#require <Classes/ActiveBasic/Windows/UI/EditBox.ab>
#require <Classes/ActiveBasic/Windows/UI/TaskMsg.ab>
#require <Classes/ActiveBasic/Windows/UI/ListBox.ab>

#require <api_ws2tcpip.sbp>
#require <Classes/System/Net/misc.ab>
#require <Classes/System/Net/Dns.ab>
#require <Classes/System/Net/EndPoint.ab>
#require <Classes/System/Net/IPAddress.ab>
#require <Classes/System/Net/IPHostEntry.ab>
#require <Classes/System/Net/Sockets/misc.ab>
#require <Classes/System/Net/Sockets/Socket.ab>

Imports ActiveBasic.Windows.UI
Imports System
Imports System.Collections.Generic
Imports System.Net
Imports System.Net.Sockets
Imports System.IO

#resource "UI_Sample.rc"

Class MyApplication
	Inherits Form
Public
	Sub MyApplication()
		AddCreate(AddressOf(OnCreate))
	End Sub

Protected
	Override Sub GetCreateStruct(ByRef cs As CREATESTRUCT)
		Super.GetCreateStruct(cs)
		With cs
			.style = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_DLGFRAME Or WS_MINIMIZEBOX
			.cx = 300
			.cy = 200
		End With
	End Sub

Private
	Sub OnCreate(sender As Object, e As CreateArgs)
		Dim wpHFontControl = GetStockObject(DEFAULT_GUI_FONT) As WPARAM

		getButton = New Button
		With getButton
			.Create(This)
			.Text = "取得"
			.Move(205, 5, 75, 30)
			.AddClick(AddressOf(Get_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textField = New EditBox
		With textField
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(10, 10, 175, 20)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		addressList = New ListBox
		With addressList
			.Create(This, WS_VSCROLL, WS_EX_CLIENTEDGE, 0)
			.Move(10, 40, 275, 140)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Get_Click(sender As Object, e As Args)
		getButton.Enabled = False
		getButton.Text = "取得中..."
		addressList.Items.Clear()
		Dns.BeginGetHostEntry(textField.Text, String.Empty, AddressOf(GetHostCallback), Nothing)
	End Sub

	Sub GetHostCallback(ar As IAsyncResult)
		Try
			Dim address = Nothing As IPAddress
			Foreach address In Dns.EndGetHostEntry(ar).IPAddressList
				addressList.Items.Add(address.ToString())
			Next
		Catch ex As Exception
			TaskMsg(This, "取得に失敗しました", ex.ToString(), "URLが正しいか確認してください。")
		End Try
		getButton.Text = "取得"
		getButton.Enabled = True
	End Sub

	getButton As Button
	textField As EditBox
	addressList As ListBox
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New MyApplication
f.CreateForm()
Winsock.Initialize()
Application.Run(f)
Winsock.Finalize()

以下、メインであるボタンを押した時のイベントの部分だけを抜き出したもの

Sub Get_Click(sender As Object, e As Args)
	getButton.Enabled = False
	getButton.Text = "取得中..."
	addressList.Items.Clear()
	Dns.BeginGetHostEntry(textField.Text, String.Empty, AddressOf(GetHostCallback), Nothing)
End Sub

Sub GetHostCallback(ar As IAsyncResult)
	Try
		Dim address = Nothing As IPAddress
		Foreach address In Dns.EndGetHostEntry(ar).IPAddressList
			addressList.Items.Add(address.ToString())
		Next
	Catch ex As Exception
		TaskMsg(This, "取得に失敗しました", ex.ToString(), "ホスト名が正しいか確認してください。")
	End Try
	getButton.Text = "取得"
	getButton.Enabled = True
End Sub

注意:例外処理がうまくいかないので、取得できるホスト名やアドレスをしていた方がいいです。

やー、いいものですね、非同期は。GUIが固まりませんからね。画像じゃわからないのが残念です。BeginGetHostEntryで関数を呼び、指定したコールバックに終了が通知されます。そしてその完了通知の情報をEndGetHostEntryに渡すことで、戻り値を取得することが出来ます。こんな感じです。

ところで、.Netの非同期処理でよく見かける、IAsyncResultを使ったBegin… - End…のパターンが、とても理解し難いです。使うのにも理解し難い物がありますが、なんでこんな設計なのかも意味が分かりません。素直にコールバックで戻り値を投げればいいと思うのは私だけでしょうか?IAsyncResultは何が嬉しいのでしょう。

UDPを使ってみる このエントリをはてなブックマークに追加

8月 30th, 2008

3,4日前に書いた記事です。


ちょっとここ数日家にいなかったので開発が進みませんでした。今日はソケットの実装をさらに進めて、UDP通信してみました。サーバーはクライアントから送られてくるメッセージを受信するだけです。クライアントは、サーバーにメッセージを送信するだけです。サーバーは1分間何も受信しないと終了し、クライアントは何もメッセージを入力しなければ終了します。

UDP
ループバックアドレスなので、IPv6できちんと通信できているのか謎のまま。

コードが長いので、ファイルでアップロードしました。[UDPServer.ab]
サーバー側のメイン部分:

Const PORT = 31415
Const RECV_BUFFER_SIZE = 1024
Const TIMEOUT = 60*10^6

Imports System
Imports System.Collections.Generic
Imports System.Net
Imports System.Net.Sockets

#console

Winsock.Initialize()
Console.WriteLine( "Winsock version " + Winsock.Version.ToString())
Console.WriteLine( "Winsock high " + Winsock.Version.ToString())
Console.WriteLine( Winsock.Description )
Console.WriteLine( Winsock.SystemStatus )
Console.WriteLine()

Try
	Dim hostname = Dns.GetHostName As String
	Console.WriteLine("Hostname " + hostname)

	Dim hostlist = Dns.GetHostEntry(hostname, PORT).HostList As IList<IPEndPoint>
	Dim host = Nothing As IPEndPoint
	Foreach host In hostlist
		Console.WriteLine("IPAddress " + host.ToString())
	Next

	Dim sockets As List<Socket>
	Dim socket = Nothing As Socket
	Foreach host In hostlist
		socket = New Socket(host.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
		socket.Bind(host)
		sockets.Add(socket)
	Next
	Console.WriteLine()

	Dim from = Nothing As IPEndPoint
	Dim buffer[ELM(RECV_BUFFER_SIZE)] As Byte
	Dim n As Long
	Dim message As String
	Dim receivedSockets = Socket.Select(sockets, SelectMode.SelectRead, TIMEOUT) As IList<Socket>
	While receivedSockets.Count <> 0
		Foreach socket In receivedSockets
			from = IPEndPoint.Create()
			n = socket.ReceiveFrom(buffer, RECV_BUFFER_SIZE, from)
			message = New String(buffer As PCTSTR, n)
			Console.WriteLine("From " + from.ToString())
			Console.WriteLine("     " + message)
			Console.WriteLine()
		Next
		receivedSockets = Socket.Select(sockets, SelectMode.SelectRead, TIMEOUT)
	Wend

	Foreach socket In sockets
		socket.Dispose()
	Next
Catch ex As Exception
	Console.WriteLine()
	Console.WriteLine(ex.ToString())
	Console.ReadLine()
End Try

Winsock.Finalize()

クライアント側

Const PORT = 31415
Const RECV_BUFFER_SIZE = 1024

Imports System
Imports System.Net
Imports System.Net.Sockets

#console

Winsock.Initialize()

Try
	Console.Write("Hostname > ")
	Dim hostname = Console.ReadLine()

	Dim host = Dns.GetHostEntry(hostname, PORT).Host As IPEndPoint
	Console.WriteLine("To " + host.ToString())

	Dim socket = New Socket(host.AddressFamily, SocketType.Dgram, ProtocolType.Udp)

	Console.Write("> ")
	Dim s = Console.ReadLine() As String
	While Not String.IsNullOrEmpty(s)
		socket.SendTo(s As PCTSTR, s.Length, host)
		Console.Write("> ")
		s = Console.ReadLine()
	Wend
	socket.Dispose()
Catch ex As Exception
	Console.WriteLine()
	Console.WriteLine(ex.ToString())
End Try

Winsock.Finalize()

注意:実行するには、api_winsock2.sbpの修正が必要です。FD_SET関数の中のカウンタ変数iの型をDWordからLongに修正してください。

さて、ソケットAPIのラップはだいたい終了してきてるのですが、このネットワークライブラリをどこの名前空間に置くべきか考え中です。標準ライブラリのActiveBasicかSystemのどちらかに置くとしたら、やはりXPSP2以降という条件は厳しいと思うので、その辺は昔のソケットAPIを使って対応したいところです。

今と1年前のIntelとPPC このエントリをはてなブックマークに追加

8月 28th, 2008

別にCPUの話をするわけじゃありません。ついささっき、ちょっとこのブログに設置しているGoogle AnalyticsのMacintoshの統計を見てびっくりしただけです。

Google Analyticsとは、Googleが提供するアクセス解析のサービスであり、WEBサイトにJavascriptを埋め込むことで、簡単に設置することが出来ます。Macintoshの統計というのは、PC環境の統計のところで、各OSのアクセス数やシェアを見ることができます。その中でも、WindowsやMacintoshなどの固有のOSの統計を見ると、Windowsならバージョン、MacintoshならPPCかIntelの統計を見ることが出来ます。しばらく見てなかったので驚きました。統計は、2008/07/28-2008/08/27、1ヶ月分です。

このブログの内容が開発者向けのものなので、先進的なユーザーが多いことは目に見えてますが、まさかこれほどIntelへの移行が進んでいた物だとは知りませんでした。iPhone SDKがPPC Macをサポートしない気持ちもわかるような気がします。

ちなみに1年前、2007/07/28-2007/08/27、1ヶ月分の統計はこうなっています。母数がだいぶ違いますが、それほど影響はないでしょう。

ppc

注意してみてください。シェアが逆転しているので、色分けが現在の統計とは逆になっています。つまり、この1年間でIntel Macのシェアが36%→81%になっているのです。これは驚きの速度ですね。iPhoneの影響もあるのかもしれません。

ああ、早く新しいiMacが欲しいです。私のメインは未だにiBook G4ですよ。いつの間にか時代に遅れてましたか…

おまけ1: 今月(2008/07/28-2008/08/27)のWindowsのシェア
Windows
Vistaが徐々にシェアをあげてます…

おまけ2: 今月(2008/07/28-2008/08/27)のOSのシェア
OS
WindowsとMacが均衡しています。最近はActiveBasicの記事が多かったので、その影響でWindowsのシェアが多くなったのかもしません。

IPv6対応について このエントリをはてなブックマークに追加

8月 26th, 2008

アクセスログを見たところ、ただgetaddrinfoとかgetnameinfoとか使ったサンプルコードを書いただけなのに、検索でこのブログに訪れる人がいるようなので、今日はIPv6の話でもしてみます。供給が少なく需要が高い記事ですね。

IPv6に対応するソケットプログラミングは、IPv4のソケットプログラミングと比べても簡単です。流れ的には、IPアドレスやホスト名とサービス名やポート番号からgetaddrinfoを使ってサーバーの情報(sockaddr)を取得し、あとは各種ソケットAPIにsockaddrを指定して使うだけです。

recvfrom関数を使う場合、sockaddrを指定する必要がありますが、ここにはsockaddr_storage構造体を指定しましょう。IPv4の時はsockaddr_inを指定していたところで、IPv6用にもsockaddr_in6という構造体が宣言されているのですが、それよりも大きいsockaddr_storage構造体を使っておけば、プロトコルに依存しないコードを書くことが出来ます。

IPv6とは言ってますが、実はこの方法を使うと、アプリケーションレベルでプロトコルに依存しないコードを書いていることになります。肝となるAPIと構造体は、getaddrinfo, getnameinfo, addrinfo, sockaddr_storageです。ソケットAPIで何かを送受信する時には、その送受信先の情報が必要なわけですが、その情報とはsockaddrのことです。これはsockaddrinfoでIPアドレス(文字列)やホスト名から取得することができます。また、getnameinfoによりsockaddrからIPアドレス(文字列)やホスト名を取得することができます。つまり、IPアドレスをバイナリとして持つ必要はなく、さらにプロトコルに依存したsockaddrであるsockaddr_inやsockaddr_in6を使い分ける必要も無いのです。

こう見てみると、.NET Frameworkの実装は、IPv6に対応していながら、プロトコル依存な実装になっているということが推測できます(あくまで推測の域は出ません)。当たり前ですが、IPAddressクラスを作ってしまうとプロトコル依存です。getaddrinfoとgetnameinfoを使っていれば、そもそもIPアドレスを特別な構造で持つ必要などなく、IPアドレスは文字列で保持しておくだけで十分ということがわかります。

また、.NETのIPHostEntryクラスに疑問があります。Dns.GetHostEntryメソッドの戻り値でIPHostEntryが使われるのですが、GetHostEntryメソッドとはgetaddrinfoのことでしょう。ここのgetaddrinfoで取得できるsockaddrを使わない手は無いのですが、なんと.NETでは使ってないのです。おそらく、中でgethostbynameを呼び出しているDns.GetHostByNameの戻り値もIPHostEntryなので、IPHostEntryの実装はこちらに合わせているのだと思います。つまり、IPHostEntryクラスはaddrinfo構造体をラップしたのではなく、hostent構造体をラップしているということです。これにより、プロトコルに依存しないコードを書くことが出来ません。

まあ結局のところ、当分の間IPv4は使われるでしょうし、未知のプロトコルにアプリケーションが対応する必要もない気がしますが、やはりこういうより一般的なAPIが提供されている場合、そちらに対応したくなるものです。現実的な落としどころとしては、やはりMSの.NETという感じでしょうか。

文章だけでしたので、実際のコードで流れを把握したい方のために、いくつかリンクを用意しました。
WindowsでのIPv6プログラミング講座 第1回[IPv6style]
winsockプログラミング[Geekなページ]

とても参考になっている本

基礎からわかるTCP/IP ネットワーク実験プログラミング―Linux/FreeBSD対応 基礎からわかるTCP/IP ネットワーク実験プログラミング―Linux/FreeBSD対応
村山 公保

オーム社 2004-10
売り上げランキング : 221083

Amazonで詳しく見る by G-Tools

WinSockの場合、sockaddr系の構造体の宣言が微妙に違うので注意しましょう。