8月2008

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

今と1年前のIntelとPPC

別に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対応について

アクセスログを見たところ、ただ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系の構造体の宣言が微妙に違うので注意しましょう。

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が確実に失敗する。

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

getaddrinfoを使ってみる

無駄にGUI。そしてgetaddrinfo使っときながらIPv6無視。つまり、UIとgetaddrinfoを使いたかっただけです。

#ifdef UNICODE
Const _FuncName_getaddrinfo = "getaddrinfoW"
Const _FuncName_freeaddrinfo = "freeaddrinfoW"
#else
Const _FuncName_getaddrinfo = "getaddrinfo"
Const _FuncName_freeaddrinfo = "freeaddrinfo"
#endif

Type addrinfo
	ai_flags As Long
	ai_family As Long
	ai_socktype As Long
	ai_protocol As Long
	ai_addrlen As DWord
	ai_canonname As PTSTR
	ai_addr As *sockaddr
	ai_next As *addrinfo
End Type

Declare Function getaddrinfo Lib "Ws2_32" Alias _FuncName_getaddrinfo (pNodeName As LPCTSTR, pServiceName As LPCTSTR, ByRef pHints As addrinfo, ByRef ppResult As *addrinfo) As Long
Declare Sub freeaddrinfo Lib "Ws2_32" Alias _FuncName_freeaddrinfo (ByRef ai As addrinfo)

#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, 10, 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 = "取得中..."

		Dim wsaData As WSADATA
		WSAStartup(MAKEWORD(2,2), wsaData)

		Dim hints As addrinfo
		Dim results As *addrinfo
		hints.ai_socktype = SOCK_STREAM
		hints.ai_family = AF_INET
		hints.ai_protocol = IPPROTO_TCP

		If getaddrinfo(ToTCStr(hostname.Text), NULL, hints, results) <> 0 Then
			iplist.Text = "失敗"
			Exit Sub
		End If

		Dim s As String
		Dim ai = results As *addrinfo
		Dim addr As *sockaddr_in
		Dim ip As String
		Do
			addr = ai->ai_addr
			ip = New String( inet_ntoa(addr.sin_addr.s_addr) As PCSTR )
			s += ip + Ex"¥r¥n"
			ai = ai->ai_next
		Loop Until ai = NULL
		iplist.Text = s

		freeaddrinfo(results As addrinfo)
		WSACleanup()
	End Sub

	button As Button
	hostname As EditBox
	iplist As EditBox
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New GetIPAddress
f.CreateForm()
Application.Run(f)

ところで、*addrinfo型の変数をByRef addrinfo型に突っ込む時、ちょうど上でresultsをfreeaddrinfo()関数に入れる時、As addrinfoでキャストしてるんですが、これはちゃんとキャストできてるのか、仕様がよくわからないので、あとで調査する必要がありそうです。

そもそもインスタンス化の時ですらクラスが決まらない

昨日の記事の見えないクラス群の続きっぽくなりますが、実はObjective-Cでは、クラスをインスタンス化する時にすら、何のクラスがインスタンス化されるかを制限していません。このことが、よりクラスの隠蔽化を容易にしているでしょう。

一般に、Objective-Cでクラスをインスタンス化する方法は2つあります。1つは、alloc,initを使って、生成,初期化する方法。こちらは使い終わったらreleaseする必要があります。もう1つは、classnameからはじまる一時オブジェクトを返すメソッドを使う方法。こちらは既にオブジェクトにautoreleaseが送られているので、自分で解放する必要がありません。

NSDate *today = [[NSDate alloc] init];
NSDate *today = [NSDate date];

これらのメソッドの宣言を見てみましょう。

+ (id)alloc
- (id)init
+ (id)date

ちなみに、+がクラスメソッド,-がインスタンスメソッドであり、()内が戻り値の型を表し、それに続いてメソッド名となります。

注目すべきところは、戻り値のid型です。このid型というのは、インスタンスを表す汎用的な型です。実際には、インスタンスを示すただのポインタです。つまり、id型はインスタンスであることしか情報を持ちません。

ところで、Objective-Cで任意のクラスを作成する方法は、他の言語と違い、少々難しい操作が必要になります。特に、任意の初期化メソッドを作る場合がそうです(いわゆるコンストラクタに近いです)。若干抽象的になりますが、テンプレートのようなコードです。例えば、引数を一つ持つような初期化メソッド。

+ (id)classnameWithObject:(id)anObject
{
	return [[[self alloc] initWithObject:anObject] autorelease];
}

- (id)initWithObject:(id)anObject
{
	self = [super self];
	if ( self ) {
		//ここで初期化
	}
	return self;
}

classnameからはじまる一時オブジェクトを返すメソッドの実態は、おそらくNSDateでもこの実装になっていることでしょう。

注目すべき部分は、selfに代入している点と、戻り値にselfを返している点です。つまり、初期化の時点でどのクラスを返すのか操作することも可能になっているのです。(厳密に言えば、メモリ確保の操作も行わなければならないこともあるので、allocWithZoneなどもオーバーライドする必要が出てきます。参考[シングルトンインスタンスの作成])

このような仕組みがあるため、Cocoaランタイムの中では何が行われているか予想がつかないことがあります。よくよく考えてみると、初期化メソッドの戻り値がid型であるのは、initがNSObjectで実装されているからであり、つまり、オブジェクトシステムはNSObjectで実装されているためです。Appleは強く推奨していませんが、NSObjectは自分で実装することも可能だったはずです。こう考えてくると、どこまでObjective-C言語が関わっているのか、よくわからないことになりますが、このことによりObjective-Cがより柔軟な言語とし、魅力的にしていることには違いないでしょう。

参考資料:Cocoa Fundamentals Guide > Cocoaオブジェクト > オブジェクトの作成

見えないクラス群

Objective-Cは大変面白い言語です。こんな問題を用意してみました。このプログラムのNSLogで出力される文字列は、いったいどうなるでしょうか。すべてクラス名が出力されます。

#import <foundation /Foundation.h>

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

	NSString *string1 = [NSString stringWithString:@"http://dev.activebasic.com/"];
	NSString *string2 = [string1 stringByAppendingPathComponent:@"OverTaker"];

	NSLog( [string1 className] );
	NSLog( [string2 className] );

	NSURL *URL = [NSURL URLWithString:string2];
	NSData *data = [NSData dataWithContentsOfURL:URL];

	NSLog( [URL className] );
	NSLog( [data className] );
	NSLog( [[string2 dataUsingEncoding:NSUTF8StringEncoding] className] );

	NSNumber *num1 = [NSNumber numberWithInt:0];
	NSNumber *num2 = [NSNumber numberWithBool:YES];

	NSLog( [num1 className] );
	NSLog( [num2 className] );

	NSLog( [[NSDate date] className] );
	NSLog( [[NSValue valueWithSize:NSMakeSize(0, 0)] className] );

    [pool drain];
    return 0;
}

ちなみに、リファレンスを見る限りでは、このような出力が予想されます。

NSString
NSString
NSURL
NSData
NSData
NSNumber
NSNumber
NSDate
NSValue

ブログのネタにするのですから、当然こんな結果になるとは思っていない方が多いと思われます。しかし、これほどまでに見たことのないクラス群が返ってくると、Objective-Cをつい先日始めたような方々は、びっくりされるのではないかと思います。以下、実行結果です。

NSCFString
NSPathStore2
NSURL
NSConcreteData
NSConcreteMutableData
NSCFNumber
NSCFBoolean
__NSCFDate
NSConcreteValue

個人的に予想外だったのは、NSConcreteMutableDataと__NSCFDateです。
いつの間にかプログラマも知らないクラスを扱っているとは、とても変な気分にさせます。しかし、これができるからこそObjective-Cであり、簡潔な仕組みで複雑なクラスを作ることができるのです。

察している方々もいると思いますが、これは抽象クラスと非常に似た概念です。NSStringという抽象クラスだけが表に出ており、それの実態である各種用途に特殊化したNSStringは隠されている。NSCFStringとNSPathStoreはそんな関係が予想されます。ちなみに、NSCFStringはCからも扱える驚きのクラスなのですが、それはまた別の話になりますので飛ばします。

なかなか説明が難しいのですが、もちろんAppleの優秀なドキュメントに、このデザインパターンについて書かれています。興味を持った方は、そちらをご覧になってください。

Cocoa Fundamentals Guide > Cocoaオブジェクト > クラスクラスタ

iPhone – 移行編

せっかくなので、このブログでもiPhoneのネタを取り上げようと思います。どこからネタにしようか迷いましたが、iPhoneへ移行するときの状況についてから話そうと思います。

私はiPhoneを購入する前、ウィルコムのWX310Kを使っていました。2年くらい前から使っていた物で、最初の数ヶ月はパケット定額プランを契約してブラウジングしていましたが、あまりにもその速度(HTMLレタリング)が実用的ではなかったので、すぐに解約して、電話のみで使っていました。ちなみに、ウィルコム以前はDocomoを使っていたのですが、ウィルコムに切り替えた際に、メールアドレスを完全にgmailに移行していたので、メールも携帯ではなく、たいていの場合PCでやっていました。

1年くらい前に、WX310Kのファームエアのバグらしきものでデータが吹っ飛んでから、電話帳も使わなくなり、覚えている電話番号,履歴を使って電話をする、程度に使う頻度に落ち込み、充電が1週間以上持つようになります。そして、ほんとんど携帯電話というものを使わない状況になってから、iPhoneの登場というわけです。

以上の通り、ほとんど携帯電話を使っていなかった私は、iPhoneの移行に何の問題もなく、むしろ移行というよりは初めて携帯電話を持った感じでしょう。なんていっても、データ移行の手間が0ですから。しかも、普通ならば最初にアドレス帳に登録していく作業をするわけですが、そこはiTunesの同期で一発です。もともとMac上のアドレスブックにアドレスなどを登録してあるので、最初に同期した時にアドレスブックは完成しました。ちなみに、このときメールアカウント,スケジュール,ブックマークなどのデータも同期するので、設定はほとんど不要です。唯一設定すべき項目は、ソフトバンクから配布されるメールアカウントの設定で、使う予定が無いので面倒でした。

移行に関しては、特に不満はなく、むしろとても簡単に済んでしまったわけですが、ひとつだけ同期に関して不満がありました。何かといえば、SafariのRSSの登録自体は同期できるのですが、未読かどうかは同期できない点、さらには、iPhoneのSafariには自動でRSSをチェックする機能がないようです。いちいちブックマークからフィードを開かないと、更新されているかわかりません。これじゃあ、RSSの意味が無いので、iPhoneのSafariではRSS機能が使えないと思った方がいいでしょう。

しかし、何も純正のアプリケーションに頼る必要はありません。RSS機能を使う手段はいくつもあると思いますが、私はNetNewsWireを使うことで解決しました。この話は、次の記事にしようと思います。

そろそろWEBサイトを作ろうかと

このブログでは、主にプログラミングについての話をしていますが、全く異なる二つの開発環境について書いています。一つは、WindowsのActiveBasic。もう一つはMac OS XのCocoa。両者は完全に異なる環境であり、このブログのアクセス解析を見ても明らかです。(記事によって、明らかにユーザーが違う)

そういうわけで、そろそろこのブログの一方の分野、具体的にはCocoaの方を、ブログという形ではなく、WEBサイトという形で情報提供していこうかと思っています。理由は主に3つです。

  1. もともとActiveBasicの開発者としてブログをやっている
  2. 載せている記事の性質上、ブログよりWEBサイトの方が良い
  3. Cocoaの技術的な記事から、ブログ的要素を排除すべきだと思った

3点目は、例えばブログであると、Cocoaについての技術的な内容と共に、私の意見が入るということです。記事の目的である技術の伝達に際し、この自分の意見を入れるという、つまりブログ感覚で載せていくというのは、あまりよろしくないと考えています。

そういわけで、今まで通り、何か意見がある技術的な内容については、このブログでネタにすると思いますが、技術的な内容を単に紹介する場合には、それように特化したWEBサイトの方で掲載しようと思います。

と、今思いつきで書いてしまったので、この夏が終わるまでに、WEBサイトを作ってしまおうかと、目標を立てます。

ちなみに、ActiveBasicの話題は、今まで通りここに載せていきます。が、少し更新頻度は下がるかもしれませんね。