ActiveBasic

例外処理について

これから数回、プログラミング言語の機能で、積極的に利用すべきものであるのに、未だに利用していない機能でも語っていこうと思います。第一回は例外処理です。

プログラミング言語の機能として、例外処理というものがあります。オブジェクト指向プログラミングを取り入れたプログラミング言語によく見られる機能であり、エラー処理を簡単に記述したり、エラー処理忘れを防止できたりする機能です。また、例外処理では、エラーの詳しい情報も取得できます。

しかしながら、私は本格的に例外処理機能を使ってプログラムを組んだことがありません。何故かと言えば、単純に例外処理を導入するのが面倒なのもありますし、ライブラリが例外処理に対応してなかったりすると、そこに例外処理を入れてしまうと、何か中途半端となってしまうからです。

Javaのように、例外が積極的に使われているどころか、無視することができない場合は、積極的に例外処理を利用しますが、特別例外処理を必要とせずとも、プログラムすることの出来る場合は、例外処理を使おうとは思いません。むしろ、先に述べたことの繰り返しとなりますが、既存のライブラリなどが、例外処理を使っていない場合、使っているコードと使っていないコードを混在させるのは、何か気持ち悪くてできないのです。

というわけで、特にパフォーマンスが気にならなければ、積極的に例外処理を使ったコードを書くべきだとは思っているのですが、なかなかそのようなスタイルが身につきません。

あと、個人的な話としては、例外処理をやるとtry-catch-finallyでインデントが1つ増えてしまって、コードのインデントがやけに多くなってきてしまうので、そこのところ、どうにかうまい書き方はないのかと思ったりもしています。

勝手にヘルプの形式でも考えてみる

そろそろAB5のヘルプの形式を考えてみるのも悪くないと思うので、ちょっと考えてみた。というよりも、個人的にはADCのヘルプが非常に見やすいので、それを真似てみたくなった。ADCには、主にガイドとリファレンスに分かれている。ガイドを見れば、これをするためには、どういったものを使って、どういった考え方で使えばいいのだろうか、ということが主に示されている。リファレンスは、単なるクラスリファレンスであるが、サンプルコードはここに含まれない。

ところで、MSDNは見づらいと常々思っているのだが、その理由を挙げてみよう。

  • リファレンスで、関数ごとに1ページ使うのはどうかと思う
  • リファレンスが用途別に並んでない
  • 全体が見渡せる資料がどこにあるかわからない
  • わけのわからないページに飛ばされることが多い

一番上はメリットもあります。たいていの関数にサンプルコードが付いているので、それを見ればすぐに使い方がわかる場合もあるからです。とまあ、こんなこともふまえつつ、ちょっとプログラミングガイド的なものを、サンプルとして書いてみたわけです。

ストリーム プログラミングガイド

サンプルとして書いたので、書きかけだったり、少しいい加減な部分があるので、あんまり本気で読まないほうがいいです。

今日これを書いてわかったことは、さすがに実装するほうが時間はかかると思いますが、ヘルプを書くのにも、相当に時間と労力を必要としそうです。

私の考えるActiveBasic5のリリース地点

実はAB5のリリース時期や要件があまり議論されたことはなく、終わりの無い開発になっているように見えますが、ある程度なんとなく決まっている感じもします。最近だいぶAB5が見えてきた気もするので、このあたりで私の考えるAB5のリリース地点を箇条書きにしてみようと思います。

コンパイラ,エディタ

  • GCのパフォーマンス改善
  • プロジェクトエディタの作り直し

ライブラリ

  • 基本的なデータ構造のサポート
  • 文字エンコーディング
  • ファイル関連
  • UI
  • ネットワーク
  • XML
  • 初心者用ライブラリ

コンパイラのほうは、今まで実装された物が多すぎて忘れましたので、今足りない物だけを書いています。今のGCのパフォーマンスはかなり悪いので、どうにか解決しなければなりません。エディタは全面的に書き直しになることでしょう。余裕があればAB5で作りたい物です。

ライブラリのほうは、基本的なデータ構造とファイル関連は、もうほとんど実装されています。XMLとUIもまあまあ進んできていて、文字エンコーディングもベースの部分はできている感じです。ネットワークは、このブログでもお伝えしているように、私が現在実装中です。ソケットからHTTPまで持っていくつもりです。クッキーも用意したいです。

初心者用ライブラリというのは、完全に私の頭の中から出てきた物です。プログラミングを初めて学ぶ人は、まずConsoleの標準入出力で学んでいくと思いますが、Windowsで標準入出力をやっていても、かなりつまらないことは多くの方が経験済みかと思われます。かといって、いきなりWindowsプログラミングに走るのも、ちょっと段差が大きい気がします。

そこで、その中間に位置するようなライブラリを作ってみたいわけです。イメージとしては、ウィンドウ作成部分を省いて、簡単に文字,画像や図形をウィンドウに描画できるようなものです。キーボードやマウスなどのイベントにも対応します。やはり、文字ではなくてグラフィックを操れると、多少面白いプログラムにはなるので、こういうものを用意できたらいいなと考えています。

と思ったものの、今作成中のUIもだいぶ簡単にWindowsプログラミングが出来るようなものなので、そんなものは必要ないのかもしれません。

ABユーザーへお知らせ

お知らせというほどの知らせでもないのですが、最近C++とかffmpegとかいじった結果がどれも中途半端で、ブログのネタになるほどのものでもなかったので、とりあえずのつなぎとしての記事です。ちなみに、今日はMono 2.0がリリースされたことをiPhoneのRSSで知り、わくわく感を胸に秘めて家に帰りましたが、肝心の実行できるちょうどよいサンプルが見つからなかったので、残念ながらこれも記事に出来ず。頑張ればPaint.NETがだいたい動くレベルまでプロジェクトは進行しているそうです。

さて、どうでもよい話はここまでにして、最近NoWestさんのブログがdev.activebasic.com内に移動したようなので、一応こちらにリンクを貼っておきます。

なんちゃないにゃ

こちらの記事で最新のActiveBasic5.0を試す方法が載っているので、先進的なユーザーさんや、5.0のリリースを待ちきれない人は、ぜひ試してみてください。
最新のActiveBasic5を試す

さらに、詳しい開発状況はこちらで知ることも出来ます。他にも、wikiのトップページにABのミーティングの議事録があったりするので、気になる方は見てください。
タイムライン – ablib

とは言っても、まともなドキュメントなしに使える人は、そうそういないと思われます。ドキュメントの方は5.0リリース時には充実させる予定ですので、今はライブラリ開発に時間を費やすつもりです。

そういえば、「ライブラリ開発ってどこを開発してるんですか?」ってユーザーさんも中にはいると思うので、一応言っておこうと思いますが、ab5.0/ablib/src/Classesの中のクラス群が、おもにライブラリの部分となります。System内とCollections,IOあたりに結構使える物がありますので、たまにブログに書かれるサンプルコードをたよりに、使ってみるのもよいかもしれません。また、だいたい.NET Frameworkと同じになっているので、msdnのリファレンスがだいたい使えます。ただし、実装されているクラスやメソッドをよく確認してから使いましょう。
System.Collections.Generic 名前空間
System.IO 名前空間

ソースコードに直接リファレンスが書かれているクラスも多数ありますので、とりあえずライブラリ内のクラスを片っ端から開いていくのも良いかもしれません。

忙しいは最もつまらない言い訳

と思っていますので、とりあえず、忙しかったのでブログが更新できませんでした、とは言いません。ただ更新する気がなくて、ブログの更新の時間に割り当てなかったから、約2週間近く停滞していたわけです。

ところで、私はActiveBasicとCocoaを主にやって、それについてのことを適当にブログで語っているわけですが、両方同時に出来るわけは無いので、数ヶ月置きにABの話題を書いたり、Cocoaの話題を書いたりと、一定の期間ごとに切り替わっています。

自分では決してCocoaよりActiveBasicの記事が劣っているとは思っていないのですが、需要的な問題で、明らかにCocoaの記事を書いていた方がブログのアクセスが伸びるんですね。そういわけで、Cocoaの記事でアクセス集めた後に、ABの記事を書いて減ってたりすると、当然のこととわかっていながらも、多少モチベーションが下がるわけです。

さて、AB5のコンパイラはだいたい完成し、ライブラリも基礎的な文字列や配列などが揃ってきました。あとはこれらを使い、より高度なライブラリを作っていく作業に入っています。基礎的な部分は結構面倒だったりしましたが、より高度になるほどライブラリ作成は気分的には楽になっているので、今は前より面白い局面に入っています。

AB5は、なかなか良い出来になってきているので、今後ブログの需要が増えると期待して、今は頑張るしか無いですね。ちょっと今は訳ありでC++に奮闘中ですが、あと数日後にはネットワークライブラリ(主にソケット部分)の方を、コミットできる形に持っていこうと思っています。

非同期でTCP

前回は軽く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の実装には入ろうかどうかってところでしょうか。

非同期に挑戦してみる

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

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を使って対応したいところです。

socketを使ってみる

まずなにより、昨日掲載したコードが、実は私のリポジトリでしか実行できないことに気がつきました。sockaddr_in6,in6_addr構造体の宣言がないためです。それで、このsockaddr_in6構造体の宣言がちょっと違うことが、昨日

なぜかIPAddressV6.ToStringのgetnameinfoが確実に失敗する。

の原因になってることにも気がつきました。

Type sockaddr_in6
	sin6_len As Char
	sin6_family As Char
	sin6_port As Word
	sin6_flowinfo As DWord
	sin6_addr As in6_addr
	sin6_scope_id As DWord
End Type
Type sockaddr_in6
	sin6_family As Word
	sin6_port As Word
	sin6_flowinfo As DWord
	sin6_addr As in6_addr
	sin6_scope_id As DWord
End Type

正しく動作するのが下ほうの宣言です。参考にしていた本が基礎からわかるTCP/IP ネットワーク実験プログラミング―Linux/FreeBSD対応だったのですが、珍しくここの宣言がWindowsとは違っていたようです。(というより、ググってみたら下の宣言しか出てこない….)

もうひとつ、昨日の話題。

ByRef sockaddrのところに*sockaddr_in, *sockaddr_in6, *sockaddr_storageとか突っ込む時、As sockaddrでいけるけど怪しい。

今日コードを走らせてみた結果、たぶんこれも大丈夫です。実際どういうように渡されているのか謎ですが。


ここから今日の話題。それなりにソケットをラップしたところで、とりあえずHTTP通信してみました。

socket

以下コード。実行するには最新のリポジトリに加え、sockaddr_in6,in6_addrの宣言が必要です。ちょっと長いのでファイルもアップロードしときました。GetAddress.ab(もはやファイル名とは違う内容に…)

#require <api_ws2tcpip.sbp>

#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>

Imports ActiveBasic.Windows.UI
Imports System

#resource "UI_Sample.rc"

Class GetIPAddress
	Inherits Form
Public
	Sub GetIPAddress()
		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 = 450
			.cy = 400
		End With
	End Sub

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

		button = New Button
		With button
			.Create(This)
			.Text = "取得"
			.Move(200, 7, 80, 20)
			.AddClick(AddressOf(Button_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		hostname = New EditBox
		With hostname
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(13, 10, 185, 16)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		iplist = New EditBox
		With iplist
			.Create(This, ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOHSCROLL Or ES_AUTOVSCROLL Or WS_HSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 30, 430, 320)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Button_Click(sender As Object, e As Args)
		Try
			Dim host = Dns.GetHostEntry(hostname.Text, "http").Host As IPEndPoint
			Dim s = New Socket(AF_INET, SOCK_STREAM, 0)
			s.Connect(host)

			Dim request = Ex"GET / HTTP/1.0¥r¥n¥r¥n" As String
			s.Send(request As *Byte, request.Length, 0)

			Dim stream As System.IO.MemoryStream
			Dim buffer[ELM(1024)] As Byte
			Dim n = s.Receive(buffer, 1024, 0) As Long
			While n
				stream.Write(buffer, 0, n)
				n = s.Receive(buffer, 1024, 0)
			Wend

			stream.Position = 0
			Dim reader = New System.IO.StreamReader(stream)
			iplist.Text = reader.ReadToEnd()
		Catch ex As Exception
			TaskMsg(This, ex.ErrorCode.ToString(), ex.ToString())
		End Try
	End Sub

	button As Button
	hostname As EditBox
	iplist As EditBox
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New GetIPAddress
f.CreateForm()
Dim wsaData As WSADATA
WSAStartup(MAKEWORD(2,2), wsaData)
Application.Run(f)
WSACleanup()

Namespace Dns

Function GetHostAddresses(hostname As String) As System.Collections.Generic.List<IPAddress>
	Return GetHostEntry(hostname).IPAddressList
End Function

Function GetHostEntry(ip As IPAddress) As IPHostEntry
	Return GetHostEntry(ip.ToString())
End Function

Function GetHostEntry(hostname As String) As IPHostEntry
	Return GetHostEntry(hostname, "")
End Function

Function GetHostEntry(hostname As String, port As Word) As IPHostEntry
	Return GetHostEntry(hostname, port.ToString())
End Function

Function GetHostEntry(hostname As String, service As String) As IPHostEntry
	Dim results As *addrinfo
	If getaddrinfo(ToTCStr(hostname), ToTCStr(service), ByVal 0, results) Then
		Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Dns.GetHostEntry: Failed to getaddrinfo.")
	End If
	Return New IPHostEntry(results)
End Function

Function GetHostName() As String
	Return GetHostEntry("").HostName
End Function

End Namespace

Class Socket
'Implements IDisposable
	sock As SOCKET
Public
	Sub Socket(af As Long, t As Long, protocol As Long)
		sock = socket(af, t, protocol)
		If sock = INVALID_SOCKET Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket: Failed to create socket.")
		End If
	End Sub

	Sub Socket(s As SOCKET)
		sock = s
	End Sub

	Sub ‾Socket()
	End Sub

	Function Accept() As Socket
		Dim client As IPEndPointUnknown
		Dim s = accept(sock, client.GetBytes() As sockaddr, client.Length) As SOCKET
		If s = INVALID_SOCKET Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Accept: Failed to accept.")
		End If
		Return New Socket(s)
	End Function

	Sub Bind(localhost As IPEndPoint)
		If bind(sock, localhost.GetBytes() As sockaddr, localhost.Length) = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Bind: Failed to bind socket.")
		End If
	End Sub

	Sub Listen(backlog As Long)
		If listen(sock, backlog) = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Listen: Failed to listen.")
		End If
	End Sub

	Sub Connect(remote As EndPoint)
		If connect(sock, remote.GetBytes() As sockaddr, remote.Length()) = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Connect: Failed to connect.")
		End If
	End Sub

	Sub Connect(ip As IPAddress, port As Word)
		Connect(IPEndPoint.Create(ip, port))
	End Sub

	Sub Connect(ip As String, port As Word)
		Connect(IPAddress.Create(ip), port)
	End Sub

	Sub Close()
		If closesocket(sock) = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Close: Failed to close.")
		End If
	End Sub

	Sub Shutdown(how As Long)
		If shutdown(sock, how) = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Shutdown: Failed to shutdown.")
		End If
	End Sub

	Function Send(buffer As *Byte, count As Long, flags As Long) As Long
		Dim n = send(sock, buffer, count, flags) As Long
		If n = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Send: Failed to send.")
		End If
		Return n
	End Function

	Function Receive(buffer As *Byte, count As Long, flags As Long) As Long
		Dim n = recv(sock, buffer, count, flags) As Long
		If n = SOCKET_ERROR Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "Socket.Send: Failed to recv.")
		End If
		Return n
	End Function
/*
	Sub Dispose()
		Shutdown(SD_BOTH)
		Close()
	End Sub*/
End Class

Class IPHostEntry
	infos As *addrinfo
Public
	Sub IPHostEntry(addrinfos As *addrinfo)
		infos = addrinfos
	End Sub

	Sub ‾IPHostEntry()
		freeaddrinfo(infos As addrinfo)
	End Sub

	Function HostList() As System.Collections.Generic.List<IPEndPoint>
		Dim list As System.Collections.Generic.List<IPEndPoint>
		Dim info = infos As *addrinfo
		While info <> NULL
			Select Case info->ai_family
				Case AF_INET
					list.Add( New IPEndPointV4(info->ai_addr) )
				Case AF_INET6
					list.Add( New IPEndPointV6(info->ai_addr) )
				Case Else
					list.Add( New IPEndPointUnknown(info->ai_addr, info->ai_addrlen) )
			End Select
			info = info->ai_next
		Wend
		Return list
	End Function

	Function Host() As IPEndPoint
		Select Case infos->ai_family
			Case AF_INET
				Return New IPEndPointV4(infos->ai_addr)
			Case AF_INET6
				Return New IPEndPointV6(infos->ai_addr)
			Case Else
				Return New IPEndPointUnknown(infos->ai_addr, infos->ai_addrlen)
		End Select
	End Function

	Function IPAddressList() As System.Collections.Generic.List<IPAddress>
		Dim list As System.Collections.Generic.List<IPAddress>
		Dim info = infos As *addrinfo
		While info <> NULL
			list.Add(IPAddress.Create(info->ai_addr))
			info = info->ai_next
		Wend
		Return list
	End Function

	Function Aliases() As System.Collections.Generic.List<String>
		Dim list As System.Collections.Generic.List<String>
		Dim info = infos As *addrinfo
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		While info <> NULL
			If getnameinfo(info->ai_addr As sockaddr, info->ai_addrlen, temp, NI_MAXHOST, NULL, 0, 0) Then
				Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "IPHostEntry.Aliases: Failed to getnameinfo.")
			End If
			list.Add(New String(temp))
			info = info->ai_next
		Wend
		Return list
	End Function

	Function HostName() As String
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		If getnameinfo(infos->ai_addr As sockaddr, infos->ai_addrlen, temp, NI_MAXHOST, NULL, 0, 0) Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "IPHostEntry.HostName: Failed to getnameinfo.")
		End If
		Return New String(temp)
	End Function
End Class

Class EndPoint
Public
	Abstract Function GetBytes() As *sockaddr
	Abstract Function Length() As Long
End Class

Class IPEndPoint
Inherits EndPoint
Public
	Static Function Create(ip As IPAddress, port As Word) As IPEndPoint
		If ip.GetType.ToString() = "IPAddressV4" Then
			Return New IPEndPointV4(ip, port)
		ElseIf ip.GetType.ToString() = "IPAddressV6" Then
			Return New IPEndPointV6(ip, port)
		Else
			Throw New Exception("IPEndPoint.Create: The IP is not supported type.")
		End If
	End Function
End Class

Class IPEndPointV4
Inherits IPEndPoint
	address As sockaddr_in
Public
	Sub IPEndPointV4(ip As IPAddress, port As Word)
		address.sin_family = AF_INET
		address.sin_port = htons(port)
		memcpy(VarPtr(address.sin_addr.s_addr), ip.GetBytes(), IPAddressV4_Length)
	End Sub

	Sub IPEndPointV4(addr As *sockaddr)
		memcpy(VarPtr(address), addr, SizeOf(sockaddr_in))
	End Sub

	Function Length() As Long
		Return SizeOf(sockaddr_in)
	End Function

	Function GetBytes() As *sockaddr
		Return VarPtr(address)
	End Function

	Override Function ToString() As String
		Dim ip = New IPAddressV4(address.sin_addr.s_addr)
		Return ip.ToString() + ":" + ntohs(address.sin_port).ToString()
	End Function
End Class

Class IPEndPointV6
Inherits IPEndPoint
	address As sockaddr_in6
Public
	Sub IPEndPointV6(ip As IPAddress, port As Word)
		address.sin6_family = AF_INET6
		address.sin6_port = htons(port)
		memcpy(VarPtr(address.sin6_addr.s6_addr), ip.GetBytes(), IPAddressV6_Length)
	End Sub

	Sub IPEndPointV6(addr As *sockaddr)
		memcpy(VarPtr(address), addr, SizeOf(sockaddr_in6))
	End Sub

	Function Length() As Long
		Return SizeOf(sockaddr_in6)
	End Function

	Function GetBytes() As *sockaddr
		Return VarPtr(address)
	End Function

	Override Function ToString() As String
		Dim ip = New IPAddressV6(address.sin6_addr.s6_addr)
		Return ip.ToString() + ":" + ntohs(address.sin6_port).ToString()
	End Function
End Class

Class IPEndPointUnknown
Inherits IPEndPoint
	address As sockaddr_storage
	length As Long
Public

	Sub IPEndPointUnknown()
		length = SizeOf(sockaddr_storage)
	End Sub

	Sub IPEndPointUnknown(addr As *sockaddr, len As Long)
		memcpy(VarPtr(address), addr, len)
		length = len
	End Sub

	Function GetBytes() As *sockaddr
		Return VarPtr(address)
	End Function

	Function Length() As Long
		Return length
	End Function

	Override Function ToString() As String
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		If getnameinfo(address As sockaddr, length, temp, NI_MAXHOST, NULL, 0, NI_NUMERICHOST or NI_NUMERICSERV) Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "IPUnknownEndPoint.ToString: Failed to getnameinfo.")
		End If
		Return New String(temp)
	End Function
End Class

Class IPAddress
Public
	Abstract Function GetBytes() As *Byte
	Abstract Function Length() As Long

	Static Function Create(ipString As String) As IPAddress
		Dim results As *addrinfo
		If getaddrinfo(ToTCStr(ipString), NULL, ByVal 0, results) Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "IPAddress: Failed to getaddrinfo.")
			Return Nothing
		End If

		If results->ai_family = AF_INET Then
			Dim addr = results->ai_addr As *sockaddr_in
			Return New IPAddressV4(addr->sin_addr.s_addr)
		ElseIf results->ai_family = AF_INET6 Then
			Dim addr = results->ai_addr As *sockaddr_in6
			Return New IPAddressV6(addr->sin6_addr.s6_addr)
		End If

		freeaddrinfo(results As addrinfo)
	End Function

	Static Function Create(addr As *sockaddr_storage) As IPAddress
		Select Case addr->ss_family
			Case AF_INET
				Dim addr4 = addr As *sockaddr_in
				Return New IPAddressV4(addr4->sin_addr.s_addr)
			Case AF_INET6
				Dim addr6 = addr As *sockaddr_in6
				Return New IPAddressV6(addr6->sin6_addr.s6_addr)
			Case Else
				Return Nothing 'Not supported.
		End Select
	End Function
End Class

Const IPAddressV4_Length = 4
Const IPAddressV6_Length = 16

Class IPAddressV4
Inherits IPAddress
	Address[ELM(IPAddressV4_Length)] As Byte
Public

	Sub IPAddressV4(ip As DWord)
		memcpy(Address, VarPtr(ip), IPAddressV4_Length)
	End Sub

	Sub IPAddressV4(ip As *Byte)
		memcpy(Address, ip, IPAddressV4_Length)
	End Sub

	Function GetBytes() As *Byte
		Return Address
	End Function

	Function Length() As Long
		Return IPAddressV4_Length
	End Function

	Override Function ToString() As String
		Return New String(inet_ntoa(GetDWord(Address)) As PCSTR)
	End Function
End Class

Class IPAddressV6
Inherits IPAddress
	Address[ELM(IPAddressV6_Length)] As Byte
Public

	Sub IPAddressV6(bytes As *Byte)
		memcpy(Address, bytes, IPAddressV6_Length)
	End Sub

	Function GetBytes() As *Byte
		Return Address
	End Function

	Function Length() As Long
		Return IPAddressV6_Length
	End Function

	Override Function ToString() As String
		Dim addr As sockaddr_in6
		addr.sin6_family = AF_INET6
		memcpy(VarPtr(addr.sin6_addr.s6_addr), Address, IPAddressV6_Length)
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		If getnameinfo(addr As sockaddr, SizeOf(sockaddr_in6), temp, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) Then
			Throw New ActiveBasic.Windows.WindowsException(WSAGetLastError(), "IPAddressV6.ToString: Failed to getnameinfo.")
		End If
		Return New String(temp)
	End Function
End Class

コードが長いので、取得のボタンを押した時のイベントのところだけ抜き出してみます。

Sub Button_Click(sender As Object, e As Args)
	Try
		Dim host = Dns.GetHostEntry(hostname.Text, "http").Host As IPEndPoint
		Dim s = New Socket(AF_INET, SOCK_STREAM, 0)
		s.Connect(host)

		Dim request = Ex"GET / HTTP/1.0¥r¥n¥r¥n" As String
		s.Send(request As *Byte, request.Length, 0)

		Dim stream As System.IO.MemoryStream
		Dim buffer[ELM(1024)] As Byte
		Dim n = s.Receive(buffer, 1024, 0) As Long
		While n
			stream.Write(buffer, 0, n)
			n = s.Receive(buffer, 1024, 0)
		Wend

		stream.Position = 0
		Dim reader = New System.IO.StreamReader(stream)
		iplist.Text = reader.ReadToEnd()
	Catch ex As Exception
		TaskMsg(This, ex.ErrorCode.ToString(), ex.ToString())
	End Try
End Sub

今日のところは、それほどメモ書きはありません。ところで、最初はコミットする予定がなく、久々のネットワークプログラミングの練習だったのですが、少し改良すればSystem.Net名前空間に普通にコミットできそうな出来になってきてしまいましたね。まあ、もう少し様子を見てから決めます。

今日のメモ書きはないのですが、ひとつ.NETのIPHostEntryクラスで、なんでこのメソッドがないのだろう、って思うようなところがあったので、明日あたりには記事にしておきたいです。具体的に言えば、上のコードの最初の部分、Dns.GetHostEntry.Hostなんですけどね。

getnameinfoを使ってみる

APIの宣言は邪魔なのでapi_ws2tcpip.sbpに移動し、コミットしておきました。万一動かしたい場合、最新のリポジトリをチェックアウトしましょう。

google

エディタにコピーして見ることを勧めます。

#require <api_ws2tcpip.sbp>

#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>

Imports ActiveBasic.Windows.UI
Imports System

#resource "UI_Sample.rc"

Class GetIPAddress
	Inherits Form
Public
	Sub GetIPAddress()
		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

		button = New Button
		With button
			.Create(This)
			.Text = "取得"
			.Move(200, 7, 80, 20)
			.AddClick(AddressOf(Button_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		hostname = New EditBox
		With hostname
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(13, 10, 185, 16)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		iplist = New EditBox
		With iplist
			.Create(This, ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOHSCROLL Or ES_AUTOVSCROLL Or WS_HSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 30, 280, 170)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Button_Click(sender As Object, e As Args)
		iplist.Text = Dns.GetHostEntry(hostname.Text).Aliases.ToString()
	End Sub

	button As Button
	hostname As EditBox
	iplist As EditBox
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New GetIPAddress
f.CreateForm()
Dim wsaData As WSADATA
WSAStartup(MAKEWORD(2,2), wsaData)
Application.Run(f)
WSACleanup()

Namespace Dns

Function GetHostAddresses(hostname As String) As System.Collections.Generic.List<IPAddress>
	Return GetHostEntry(hostname).IPAddressList
End Function

Function GetHostEntry(ip As IPAddress) As IPHostEntry
	Return GetHostEntry(ip.ToString())
End Function

Function GetHostEntry(hostname As String) As IPHostEntry
	Dim results As *addrinfo
	If getaddrinfo(ToTCStr(hostname), NULL, ByVal 0, results) Then
		Throw New Exception("Dns.GetHostEntry: Failed to getaddrinfo.")
	End If
	Return New IPHostEntry(results)
End Function

Function GetHostName() As String
	Return GetHostEntry("").HostName
End Function

End Namespace

Class IPHostEntry
	infos As *addrinfo
Public
	Sub IPHostEntry(addrinfos As *addrinfo)
		infos = addrinfos
	End Sub

	Sub ‾IPHostEntry()
		freeaddrinfo(infos As addrinfo)
	End Sub

	Function IPAddressList() As System.Collections.Generic.List<IPAddress>
		Dim list As System.Collections.Generic.List<IPAddress>
		Dim info = infos As *addrinfo
		While info <> NULL
			list.Add(IPAddress.Create(info->ai_addr))
			info = info->ai_next
		Wend
		Return list
	End Function

	Function Aliases() As System.Collections.Generic.List<String>
		Dim list As System.Collections.Generic.List<String>
		Dim info = infos As *addrinfo
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		While info <> NULL
			If getnameinfo(info->ai_addr As sockaddr, info->ai_addrlen, temp, NI_MAXHOST, NULL, 0, 0) Then
				Throw New Exception("IPHostEntry.Aliases: Failed to getnameinfo.")
			End If
			list.Add(New String(temp))
			info = info->ai_next
		Wend
		Return list
	End Function

	Function HostName() As String
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		If getnameinfo(infos->ai_addr As sockaddr, infos->ai_addrlen, temp, NI_MAXHOST, NULL, 0, 0) Then
			Throw New Exception(Ex"IPHostEntry.HostName: Failed to getnameinfo.")
		End If
		Return New String(temp)
	End Function
End Class

Class IPEndPoint
Public
	Abstract Function GetBytes() As *sockaddr

	Static Function Create(ip As IPAddress, port As Word) As IPEndPoint
		If ip.GetType.ToString() = "IPAddressV4" Then
			Return New IPEndPointV4(ip, port)
		ElseIf ip.GetType.ToString() = "IPAddressV6" Then
			Return New IPEndPointV6(ip, port)
		Else
			Throw New Exception("IPEndPoint.Create: The IP is not supported type.")
		End If
	End Function
End Class

Class IPEndPointV4
Inherits IPEndPoint
	address As sockaddr_in
Public
	Sub IPEndPointV4(ip As IPAddress, port As Word)
		address.sin_family = AF_INET
		address.sin_port = port
		memcpy(VarPtr(address.sin_addr.s_addr), ip.GetBytes(), IPAddressV4_Length)
	End Sub

	Function GetBytes() As *sockaddr
		Return VarPtr(address)
	End Function

	Override Function ToString() As String
		Dim ip = New IPAddressV4(address.sin_addr.s_addr)
		Return ip.ToString() + ":" + address.sin_port.ToString()
	End Function
End Class

Class IPEndPointV6
Inherits IPEndPoint
	address As sockaddr_in6
Public
	Sub IPEndPointV6(ip As IPAddress, port As Word)
		address.sin6_family = AF_INET6
		address.sin6_port = port
		memcpy(VarPtr(address.sin6_addr.s6_addr), ip.GetBytes(), IPAddressV6_Length)
	End Sub

	Function GetBytes() As *sockaddr
		Return VarPtr(address)
	End Function

	Override Function ToString() As String
		Dim ip = New IPAddressV6(address.sin6_addr.s6_addr)
		Return ip.ToString() + ":" + address.sin6_port.ToString()
	End Function
End Class

Class IPAddress
Public
	Abstract Function GetBytes() As *Byte

	Static Function Create(ipString As String) As IPAddress
		Dim results As *addrinfo
		If getaddrinfo(ToTCStr(ipString), NULL, ByVal 0, results) Then
			Throw New Exception("IPAddress: Failed to getaddrinfo.")
		End If

		If results->ai_family = AF_INET Then
			Dim addr = results->ai_addr As *sockaddr_in
			Return New IPAddressV4(addr->sin_addr.s_addr)
		ElseIf results->ai_family = AF_INET6 Then
			Dim addr = results->ai_addr As *sockaddr_in6
			Return New IPAddressV6(addr->sin6_addr.s6_addr)
		End If

		freeaddrinfo(results As addrinfo)
	End Function

	Static Function Create(addr As *sockaddr_storage) As IPAddress
		Select Case addr->ss_family
			Case AF_INET
				Dim addr4 = addr As *sockaddr_in
				Return New IPAddressV4(addr4->sin_addr.s_addr)
			Case AF_INET6
				Dim addr6 = addr As *sockaddr_in6
				Return New IPAddressV6(addr6->sin6_addr.s6_addr)
			Case Else
				Return Nothing 'Not supported.
		End Select
	End Function
End Class

Const IPAddressV4_Length = 4
Const IPAddressV6_Length = 16

Class IPAddressV4
Inherits IPAddress
	Address[ELM(IPAddressV4_Length)] As Byte
Public

	Sub IPAddressV4(ip As DWord)
		memcpy(Address, VarPtr(ip), IPAddressV4_Length)
	End Sub

	Sub IPAddressV4(ip As *Byte)
		memcpy(Address, ip, IPAddressV4_Length)
	End Sub

	Function GetBytes() As *Byte
		Return Address
	End Function

	Override Function ToString() As String
		Return New String(inet_ntoa(GetDWord(Address)) As PCSTR)
	End Function
End Class

Class IPAddressV6
Inherits IPAddress
	Address[ELM(IPAddressV6_Length)] As Byte
Public

	Sub IPAddressV6(bytes As *Byte)
		memcpy(Address, bytes, IPAddressV6_Length)
	End Sub

	Function GetBytes() As *Byte
		Return Address
	End Function

	Override Function ToString() As String
		Dim addr As sockaddr_in6
		addr.sin6_len = SizeOf(sockaddr_in6)
		addr.sin6_family = AF_INET6
		memcpy(VarPtr(addr.sin6_addr.s6_addr), Address, IPAddressV6_Length)
		Dim temp[ELM(NI_MAXHOST)] As TCHAR
		If getnameinfo(addr As sockaddr, addr.sin6_len, temp, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) Then
			Throw New Exception("IPAddressV6.ToString: Failed to getnameinfo.")
		End If
		Return New String(temp)
	End Function
End Class

いろいろクラス化してみた。最初は.NET関係なしに好き勝手やる予定だったけど、クラス名に迷って.NETと同じ名前をつけていたら、いつの間にか.NETの実装に引きずられていた。恐ろしい。

addrinfo,sockaddr_storageを使えばプロトコルに依存しないようなコードが書ける、というのはIPAddressクラスを作らない場合できるわけで、IPアドレス扱おうとすると、どうしても固有の実装をせざるを得ない。特に、sockaddrからIPアドレスをバイナリで取得するようなコードを書きたいとき。

いっそのことsockaddrをラップしてIPAddressクラス作りたい気分だけど、単にIPAddressだけ扱いたい時を考えると大きすぎる。

ところどころEnumeratorにしたい気分だったけど、なんか変なところでエラーが出るのであきらめた。もっと簡単なコードで検証が必要。

ByRef sockaddrのところに*sockaddr_in, *sockaddr_in6, *sockaddr_storageとか突っ込む時、As sockaddrでいけるけど怪しい。

なぜかIPAddressV6.ToStringのgetnameinfoが確実に失敗する。

以上、今日のメモ書き。
あしたはソケットいじりたい。