即解 VB5から使うWindows API

TCP/IPクライアントの作成

WinSock32ネットワーク
プログラミング入門

福岡寿和 FUKUOKA,Toshikazu 富士通SSL

fukuoka@ssl.fujitsu.co.jp


Visual Basic 5.0では,標準でMSWINSCK.OCX(Microsoft Winsock Control 5.0)が添付されているため,wsock32.dllを直接使う機会はほとんどないかもしれません.もちろん,MSWINSCK.OCXを使って手軽にネットワークプログラミングを楽しむのもよいかもしれませんし,簡単な業務アプリケーションなどを作成するのもよいでしょう.しかし,標準添付のMSWINSCK.OCXに障害があったときに,その代替になる日本語版のサードパーティ製Active Xコントロールが存在しないこと,そして,私が業務アプリケーションを作成するときは,Visual Basic 5.0で4.0と同程度の機能を使って開発していることもあって,必要に迫られる前に一度きちんとwsock32.dll(WinSock32 API)の使い方をまとめておきたいと思います.
 話が少しずれますが,私がVisual Basic 5.0独自の機能を使わない方針でいるのは,Windowsの標準ユーザーインターフェイスに準拠した設計をしていれば,この方法が言語の障害に出会わないで済む一番の方法であると思っているからです.もちろん,新機能をまったく使わないわけではなく,RDO2.0などは,実際に業務と同程度のデータ量を処理するテストアプリケーションを作成し検証してから,利用しています.つまり,すべて実環境に近い形で事前テストをして,新機能を検証してから提案を行なっています.

 

●WinSock32 APIは何ができるのか

 TCP/IPプロトコルやUDP/IPプロトコルを使っているときにひとつのプログラムがひとつの相手にしか接続できないとしたら,実用的なシステムは構築できません(接続相手ごとにプログラムを起動すれば,共通化できる分もメモリを消費するからです).そこで,通信相手ごとにソケットと呼ばれるものを用意して,ソケット番号により相手を特定することで,ひとつのプログラムで複数の相手と通信できるようにしています.そして,WinSock32 APIでは,TCP/IPプロトコル,UDP/IPプロトコルの他にMicrosoft Winsock Control 5.0で直接サポートされていないIPプロトコルもサポートしています.ちなみに,WinSock32 APIのプロパティの説明に「BSD Socket API for Windows」と記載されているように,Windowsのソケット関数は,カルフォルニア大学バークレイ校で開発したBSD版UNIX(BSD:BerKeley Software Distribution)のバークレイソケット関数を基本に構成されています(表1,リスト1).

表1:WinSock32 API(バークレイソケット互換)

関数名 

処理

Accept

クライアントからの接続を許可

Bind

自ネットワークアドレスとポート番号をソケットに結合

Closesocket

ソケットを削除

connect

リモート側ソケットと接続

getsockopt

自ソケットのオプション値を設定

Listen

指定したポートで接続を待つ

Recv

ソケットからデータを受信

Recvfrom

データグラムを受信

select

ソケットの現在の状態を返す

send

ソケットからデータを送信

sendto

データグラムを送信

setsockopt

自ソケットのオプション値を取得

shutdown

ソケットのデータ送受信を禁止

socket

ソケットを新規に作成

バイトオーダー変換

htol

4バイト整数をWindows形式からネットワークバイトオーダー形式に変換

htos

2バイト整数をWindows形式からネットワークバイトオーダー形式に変換

htohl

4バイト整数をネットワークバイト形式からWindows形式に変換

htohs

2バイト整数をネットワークバイト形式からWindows形式に変換

アドレス変換

inet_addr

Internetプロトコルドットアドレスから32ビットのInternetアドレスに変換

inet_ntoa

32ビットのInternetアドレスからInternetプロトコルドットアドレスに変換

その他

gethostbyaddr

32ビットのInternetアドレスからホスト情報を取得

gethostbyname

ホスト名からホスト情報を取得

gethostname

自ホスト名を取得

getpeername

ソケット接続しているリモートアドレスとポート番号を取得

getservbyname

サービス名からサービス情報を取得

getservbyport

サービスのポート番号からサービス情報を取得

getprotobyname

プロトコル名からプロトコル情報を取得

getprotobynumber

プロトコル番号からプロトコル情報を取得

getsockname

ソケットから自アドレスとポート番号を取得

ioctlsocket

ソケットの動作パラメータの取得と設定


表2:拡張WinSock32 API(初期化関数)

関数名 

処理

WSAStartup

WinSock32 APIを初期化

WSACleanup

すべての未処理のデータを送信し,ソケットを閉じる

WSAGetLastError

最後に発生したエラーを取得


リスト1:WinSock32宣言
Type HostEnt
    h_name	   As Long
    h_aliases	   As Long
    h_addrtype	   As Integer
    h_length	   As Integer
    h_addr_list    As Long
End Type

Type sockaddr
    sin_family	   As Integer
    sin_port	   As Integer
    sin_addr	   As Long
    sin_zero	   As String * 8
End Type

Public Const WSA_DESCRIPTIONLEN = 256
Public Const WSA_DescriptionSize = WSA_DESCRIPTIONLEN + 1
Public Const WSA_SYS_STATUS_LEN = 128
Public Const WSA_SysStatusSize = WSA_SYS_STATUS_LEN + 1

Type WSADataType
    wVersion	   As Integer
    wHighVersion   As Integer
    szDescription  As String * WSA_DescriptionSize
    szSystemStatus As String * WSA_SysStatusSize
    iMaxSockets    As Integer
    iMaxUdpDg	   As Integer
    lpVendorInfo   As Long
End Type

'ソケット関数
Public Declare Function accept Lib "wsock32.dll" _
 (ByVal s As Long, addr As sockaddr, addrlen As Long) As Long
Public Declare Function bind Lib "wsock32.dll" _
 (ByVal s As Long, sName As sockaddr, ByVal namelen As Long) As Long
Public Declare Function closesocket Lib "wsock32.dll" _
 (ByVal s As Long) As Long
Public Declare Function connect Lib "wsock32.dll" _
 (ByVal s As Long, sName As sockaddr, ByVal namelen As Long) As Long
Public Declare Function ioctlsocket Lib "wsock32.dll" _
 (ByVal s As Long, ByVal cmd As Long, argp As Long) As Long
Public Declare Function listen Lib "wsock32.dll" _
 (ByVal s As Long, ByVal backlog As Long) As Long
Public Declare Function recv Lib "wsock32.dll" _
 (ByVal s As Long, ByVal buf As Any, ByVal lngLen As Long, ByVal flags As Long) As Long
Public Declare Function recvfrom Lib "wsock32.dll" _
 (ByVal s As Long, buf As Any, ByVal lngLen As Long, ByVal flags As Long, from As sockaddr, fromlen As Long) As Long
Public Declare Function send Lib "wsock32.dll" _
 (ByVal s As Long, buf As Any, ByVal lngLenlen As Long, ByVal flags As Long) As Long
Public Declare Function sendto Lib "wsock32.dll" _
 (ByVal s As Long, buf As Any, ByVal lngLen As Long, ByVal flags As Long, sTo As sockaddr, ByVal tolen As Long) As Long
Public Declare Function setsockopt Lib "wsock32.dll" _
 (ByVal s As Long, ByVal level As Long, ByVal optname As Long, optval As Any, ByVal optlen As Long) As Long
Public Declare Function ShutDown Lib "wsock32.dll" Alias "shutdown" _
 (ByVal s As Long, ByVal how As Long) As Long
Public Declare Function socket Lib "wsock32.dll" _
 (ByVal af As Long, ByVal lngType As Long, ByVal protocol As Long) As Long

'バイトオーダー変換
Public Declare Function htonl Lib "wsock32.dll" (ByVal hostlong As Long) As Long
Public Declare Function htons Lib "wsock32.dll" (ByVal hostshort As Long) As Integer
Public Declare Function ntohl Lib "wsock32.dll" (ByVal netlong As Long) As Long
Public Declare Function ntohs Lib "wsock32.dll" (ByVal netshort As Long) As Integer

'アドレス変換
Public Declare Function inet_addr Lib "wsock32.dll" (ByVal cp As String) As Long
Public Declare Function inet_ntoa Lib "wsock32.dll" (ByVal lngIn As Long) As Long

'データベース関数
Public Declare Function gethostbyaddr Lib "wsock32.dll" _
 (addr As Long, ByVal lngLen As Long, ByVal lngType As Long) As Long
Public Declare Function gethostbyname Lib "wsock32.dll" _
 (ByVal strName As String) As Long
Public Declare Function gethostname Lib "wsock32.dll" _
 (ByVal strName As String, ByVal namelen As Long) As Long
Public Declare Function getpeername Lib "wsock32.dll" _
 (ByVal s As Long, sName As sockaddr, namelen As Long) As Long
Public Declare Function getprotobyname Lib "wsock32.dll" _
 (ByVal strName As String) As Long
Public Declare Function getprotobynumber Lib "wsock32.dll" _
 (ByVal lngNumber As Long) As Long
Public Declare Function getservbyname Lib "wsock32.dll" _
 (ByVal strName As String, ByVal proto As String) As Long
Public Declare Function getservbyport Lib "wsock32.dll" _
 (ByVal Port As Long, ByVal proto As String) As Long
Public Declare Function getsockname Lib "wsock32.dll" _
 (ByVal s As Long, sName As sockaddr, namelen As Long) As Long
Public Declare Function getsockopt Lib "wsock32.dll" _
 (ByVal s As Long, ByVal level As Long, ByVal optname As Long, optval As Any, optlen As Long) As Long

★バイトオーダーの違い

 複数バイトで整数を表現するとき,インテル系のCPUを使っているマシンで採用しているリトルエンディアン(little endian)方式以外に,モトローラ系のCPUを使っているマシンで採用しているビッグエンディアン(big endian)方式があります(図1).整数データをネットワークを介してバイナリ形式で送受信するときは,ビッグエンディアン方式にすることが決まっています.これをネットワークバイトオーダーと呼びます.

図1:バイトオーダーの違い

★プログラミングスタイル

 TCP/IPプロトコルを使うときの処理の流れは図2のようになります.TCP/IPプロトコルは,サーバー側とクライアント側のコンピュータ間がどのようなネットワーク構成になっていても,サーバーとクライアントが電話回線のように直接会話できます.このような通信サービスを接続指向サービスまたはポイントツーポイント接続サービスと呼びます.
 一方,UDP/IPプロトコルを使うときの処理の流れは図3のようになります.UDP/IPプロトコルは,ポイントツーポイントのように直接会話をするのではなく,他の人に配達を頼むような形になります.そして,その人は戻ってこないので,複数のデータを送信するときは別々の人に頼むことになります.そのため,届いたメッセージの順番が異なったり,途中で紛失してしまうこともあります.このような通信サービスを非接続指向サービスと呼びます.IPプロトコルも非接続指向サービスのひとつです.

図2:TCP/IPプログラム

図3:UDP/IPプログラム

●TCP/IPクライアントの作成

 では,実際にTCP/IPを使う例として,独自のアプリケーションプロトコルをサポートするTCP/IPクライアントプログラムを作成します(図4,リスト2).このような通信プログラムを作るときは,相手側(この場合は,サーバー側)に稼動実績があるプログラムを選ぶとよいでしょう.今回は,TCP/IPサーバーとして,本誌7月号で紹介したTCP/IPサーバー(TCP_1000.EXE)を使います.

図4:TCP/IPクライアント


リスト2:TCP/IPクライアント(TCPCL.frm抜粋)
Private Sub cmdConnect_Click()
    Dim lngRet	    As Long		'戻り値
    Dim lngIPAddr   As Long		'IPアドレス
    Dim strErrMsg   As String		'エラーメッセージ

    strErrMsg = ""
'指定ホストとの接続
    If Trim$(txtHost.Text) <> "" And Trim$(txtPort.Text) <> "" Then
    'WinSockの初期化
	lngRet = WSAStartup(&H101, musrStartup)
	If lngRet = SOCKET_ERROR Then Exit Sub
    'Socket
	mlngSock = socket(AF_INET, SOCK_STREAM, 0)
	If mlngSock = SOCKET_ERROR Then
	    strErrMsg = "Socket:生成ができませんでした。"
	    GoTo exitConnect:
	End If
    'Connect
	lngIPAddr = GetHostByNameAlias(txtHost.Text)
	musrSockBuf.sin_family = AF_INET
	musrSockBuf.sin_port = htons(CLng(txtPort.Text))
	musrSockBuf.sin_addr = lngIPAddr
	musrSockBuf.sin_zero = String$(8, 0)
	lngRet = connect(mlngSock, musrSockBuf, Len(musrSockBuf))
	DoEvents
	If lngRet = SOCKET_ERROR Then
	    strErrMsg = "connect:" & strWSAErrorGet(WSAGetLastError())
	    closesocket mlngSock
	    lngRet = WSACleanup()
	    GoTo exitConnect:
	End If
	DoEvents
    'ioctlsocket
	lngRet = ioctlsocket(mlngSock, FIONBIO, True)
	If lngRet = SOCKET_ERROR Then
	    strErrMsg = "ioctlsocket:" & strWSAErrorGet(WSAGetLastError())
	    closesocket mlngSock
	    lngRet = WSACleanup()
	    GoTo exitConnect:
	End If
    Else
	strErrMsg = "相手先を指定してください。"
    End If
exitConnect:
    On Error Resume Next
    If strErrMsg <> "" Then
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
    End If
End Sub

Private Sub cmdDisConn_Click()
    On Error Resume Next
    closesocket mlngSock
    WSACleanup
End Sub

Private Sub cmdSend_Click()
    Dim lngRet	    As Long		'戻り値
    Dim strSend     As String		'送信データ
    Dim strErrMsg   As String		'エラーメッセージ
    Dim strRecv     As String * 100	'受信データ
    Dim strRecvBuf  As String		'受信バッファ

'送信
    strErrMsg = ""
    strSend = txtSend.Text
    lngRet = send(mlngSock, ByVal strSend, Len(strSend), 0)
    If lngRet = SOCKET_ERROR Then
	strErrMsg = "send:" & strWSAErrorGet(WSAGetLastError())
	closesocket mlngSock
	lngRet = WSACleanup()
	GoTo exitSend:
    End If
'受信
    strRecvBuf = ""
    Do While True
	DoEvents
	lngRet = recv(mlngSock, ByVal strRecv, 100, 0)
	If (lngRet > 0) Then
	    strRecvBuf = strRecvBuf & Left$(strRecv, lngRet)
	    Exit Do
	ElseIf lngRet = SOCKET_ERROR Then
	    If WSAGetLastError() > 0 Then
		strRecvBuf = ""
		strErrMsg = "send:" & strWSAErrorGet(WSAGetLastError())
		closesocket mlngSock
		lngRet = WSACleanup()
		GoTo exitSend:
	    End If
	Else
	    Exit Do
	End If
    Loop
    lstRecv.AddItem strRecvBuf
exitSend:
    On Error Resume Next
    If strErrMsg <> "" Then
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
    End If
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    Call cmdDisConn_Click
End Sub

★WinSock32 APIの起動

 WinSock32 APIを使うときは,まず,DLLの起動を行ない,各種領域を初期化します(表2参照).このとき使われるのがWSAStartup関数です.


lngRet = WSAStartup(&H101, musrStartup)

 引数は,WinSock32 APIのバージョン番号(入力)と起動情報(出力)です.バージョン番号は,メジャーバージョンを上位バイト,マイナーバージョンを下位バイトに指定します.たとえば,バージョン1.1を使うときは,&H101を指定します.

★ソケットの作成

 DLLとの接続が終わったら,リモートホストに接続するソケットを作成します.


mlngSock=socket(AF_INET,SOCK_STREAM,0)

 引数の2番目で,使用するプロトコルを指定しています.1番目と3番目の引数は通常変更する必要はありません.

★リモートホストに対する接続

 connect関数は,接続されていないソケットを使ってリモートホストと接続します.


musrSockBuf.sin_family = AF_INET

musrSockBuf.sin_port =htons(CLng(txtPort))
musrSockBuf.sin_addr = lngIPAddr
musrSockBuf.sin_zero = String$(8, 0)
lngRet = connect(mlngSock, _
	 musrSockBuf, Len(musrSockBuf))

 第2引数の構造体musrSockBufには,接続したいリモートホストのポートとアドレスを指定しています.

★データの送受信

 今回のサンプルプログラムでは,クライアント側から送信した文字列を全角に変換して返却するプロトコル(正確には,アプリケーションプロトコル)にしてみました.


strSend = txtSend.Text
lngRet = send(mlngSock, _
 ByVal strSend, Len(strSend), 0) _
 lngRet = recv(mlngSock,  _
 ByVal strRecv, 100,0)

 今回は独自プロトコルなので,勝手にプロトコルを決めていますが,本来はRFC(Request For Comments)などを参照してアプリケーションプロトコルを実装してゆきます.

★切断

 リモートホストと切断して,システムからソケットを削除します.


closesocket mlngSock

★WinSock32 APIとの切断

 WinSock32 APIとアプリケーションの切り離しにはWSACleanup関数を使います.  注意点としては,Form_QueryUnloadプロシージャにWSACleanup関数を記述して,フォームを閉じる時に確実にAPIとの切断を行なうことです.

●ブロッキングとノンブロッキング

 Winsock APIを使うとき,注意しなければならないのが,APIが指示されたネットワーク操作から戻るまで,その呼び出し元のアプリケーション自体が停止(ブロックする)してしまうことです.このため,複数のクライアントをサポートするようなサーバープログラムを作成するときは,ノンブロッキング型の操作を行なう必要があります.ノンブロッキング型の関数は,Windowsの独自拡張関数です(表3,リスト3).

表3:拡張WinSock32 API(ノンブロッキング関数)
関数名 処理
WSAAsyncGetHostByAddr 32ビットのInternetアドレスからホスト情報を取得
WSAAsyncGetHostByName ホスト名からホスト情報を取得
WSAAsyncGetServByName サービス名からサービス情報を取得
WSAAsyncGetServByPort サービスのポート番号からサービス情報を取得
WSAAsyncGetProtoByName プロトコル名からプロトコル情報を取得
WSAAsyncGetProtoByNumbr プロトコル番号からプロトコル情報を取得
WSAAsyncSelect 指定したソケットをノンブロッキング型にする
   
第4引数の値
設定値 処理
FD_ACCEPT ソケットに接続要求が届いた
FD_CLOSE ソケットがクローズされた
FD_CONNECT 接続が完了した
FD_WRITE データを書き出し可能になった
WSACancelAyncRequest 処理が完了していないノンブロッキング型関数の処理を停止
WSACancelBlockingCall ブロッキング型として
WSAIsBlocking ソケット上でブロッキング型関数が処理されているかの判定
WSASetLastError エラーコードを設定
WSASetBlockingHook 指定した中断フック処理を登録
WSAUnhookBlockingHook 標準の中断フック処理に戻す

リスト3:拡張WinSock32宣言
'拡張機能
Public Declare Function WSAStartup Lib "wsock32.dll" _
 (ByVal wVersionRequested As Long, lpWSAData As WSADataType) As Long
Public Declare Function WSACleanup Lib "wsock32.dll"  _
 () As Long
Public Declare Function WSAAsyncGetServByName Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, ByVal strName As String, ByVal proto As String, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncGetServByPort Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, ByVal Port As Long, ByVal proto As String, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncGetProtoByName Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, ByVal proto_name As String, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncGetProtoByNumber Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, ByVal number As Long, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncGetHostByName Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, ByVal host_name As String, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncGetHostByAddr Lib "wsock32.dll"  _
 (ByVal hWnd As Long, ByVal wMsg As Long, lngAddr As Long, ByVal lngLen As Long, ByVal lngType As Long, buf As Any, ByVal buflen As Long) As Long
Public Declare Function WSAAsyncSelect Lib "wsock32.dll"  _
 (ByVal s As Long, ByVal hWnd As Long, ByVal wMsg As Long, ByVal lngEvent As Long) As Long
Public Declare Function WSACancelAsyncRequest Lib "wsock32.dll"  _
 (ByVal hAsyncTaskHandle As Long) As Long
Public Declare Function WSACancelBlockingCall Lib "wsock32.dll"  _
 () As Long
Public Declare Function WSAGetLastError Lib "wsock32.dll"  _
 () As Long
Public Declare Function WSAIsBlocking Lib "wsock32.dll"  _
 () As Long
Public Declare Sub WSASetLastError Lib "wsock32.dll"  _
 (ByVal lngErr As Long)
Public Declare Function WSASetBlockingHook Lib "wsock32.dll"  _
 (ByVal lngFunc As Long) As Long
Public Declare Function WSAUnhookBlockingHook Lib "wsock32.dll"  _
 () As Long

★プログラミングスタイル

 ノンブロッキング型のTCP/IPクライアントを作成するときの処理の流れは図5のようになります.ノンブロッキング型のプログラムの特徴は,WSAAsyncSelect関数を使い,特定のコントロールに対して,特定のWindowsメッセージを送るように指定することです.そして,そのコントロールのイベントとして,WinSock32 APIからの処理完了を処理します.ただ,残念なことにWinSock32 APIの処理ごとにWindowsメッセージを分けることができないため,メッセージにより起動されるイベント内でどのAPIの完了通知かを判定する必要があります.

図5:ノンブロッキング型TCP/IPプログラム


●ノンブロッキング型TCP/IPクライアントの作成

 ブロッキング型のTCP/IPクライアントに比べて,ノンブロッキング型のTCP/IPクライアントの処理は複雑になります(リスト4).しかし,サーバーの処理が遅かったり,RFCに基づいてインターネットプロトコルやWANを経由した通信プロトコルをサポートするときは,ノンブロッキング型で作成することをお勧めします.ブロッキング型のときは,サーバーからの応答が遅れると,クライアントのアプリケーションの停止時間が伸びることになり,クライアントの操作性が悪化してしまうからです.

★WSAAsyncSelect

 WSAAsyncSelect関数は,第1引数で指定されたソケット上の通信で,第4引数で指定された条件を満たしたときに,第2引数のコントロールに,第3引数のWindowsメッセージを通知します.今回はWinSock32 APIからの通知をあたかもキーボードからの入力のように処理をする方式を採用するので,TextBoxコントロールに対して,KeyDownイベントを発生させます.そのために,WSAAsyncSelect関数の第3引数として,EN_SETFOCUS (&H100)を設定しています.


lngRet=WSAAsyncSelect(mlngSock, _
     txtSockIn.hWnd,&H100,FD_CONNECT)

 このとき,TextBoxコントロールが本来の機能を利用者に提供しないようにVisibleプロパティをFalseにしてください.なお,ノンブロッキングからブロッキングに戻すときは,第4引数に0を指定してください.


リスト4:ノンブロッキング型TCP/IPクライアント(TCPCL2.frm抜粋)
Private Sub cmdConnect_Click()
    Dim lngRet	    As Long		'戻り値
    Dim lngIPAddr   As Long		'IPアドレス
    Dim strErrMsg   As String		'エラーメッセージ

    strErrMsg = ""
'指定ホストとの接続
    If Trim$(txtHost.Text) <> "" And Trim$(txtPort.Text) <> "" Then
    'WinSockの初期化
	lngRet = WSAStartup(&H101, musrStartup)
	If lngRet = SOCKET_ERROR Then Exit Sub
    'Socket
	mlngSock = socket(AF_INET, SOCK_STREAM, 0)
	If mlngSock = SOCKET_ERROR Then
	    strErrMsg = "Socket:生成ができませんでした。"
	    GoTo exitConnect:
	End If
    'WSAASyncSelect
	lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hWnd, &H100, FD_CONNECT)
    'Connect
	lngIPAddr = GetHostByNameAlias(txtHost.Text)
	musrSockBuf.sin_family = AF_INET
	musrSockBuf.sin_port = htons(CLng(txtPort.Text))
	musrSockBuf.sin_addr = lngIPAddr
	musrSockBuf.sin_zero = String$(8, 0)
	lngRet = connect(mlngSock, musrSockBuf, Len(musrSockBuf))
	txtSockIn.Tag = pcWaitingForConnect
	DoEvents
	If lngRet = SOCKET_ERROR And WSAGetLastError() <> 0 Then
	    strErrMsg = "connect:" & strWSAErrorGet(WSAGetLastError())
	    GoTo exitConnect:
	End If
	DoEvents
    Else
	strErrMsg = "相手先を指定してください。"
    End If
exitConnect:
    On Error Resume Next
    If strErrMsg <> "" Then
	closesocket mlngSock
	lngRet = WSACleanup()
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
	txtSockIn.Tag = pcIdel
    End If
    Exit Sub
End Sub

Private Sub cmdDisConn_Click()
    On Error Resume Next
    txtSockIn.Tag = pcIdel
    closesocket mlngSock
    WSAUnhookBlockingHook
    WSACleanup
End Sub

Private Sub cmdSend_Click()
    Dim lngRet	    As Long		'戻り値
    Dim strSend     As String		'送信データ
    Dim strErrMsg   As String		'エラーメッセージ

'WSAASyncSelect
    lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hWnd, &H100, FD_WRITE)
    txtSockIn.Tag = pcWaitingForWrite
'送信
    strErrMsg = ""
    strSend = txtSend.Text
    lngRet = send(mlngSock, ByVal strSend, Len(strSend), 0)
    DoEvents
    If lngRet = SOCKET_ERROR Then
	strErrMsg = "send:" & strWSAErrorGet(WSAGetLastError())
	closesocket mlngSock
	lngRet = WSACleanup()
	GoTo exitSend:
    End If
exitSend:
    On Error Resume Next
    If strErrMsg <> "" Then
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
	cmdConnect.Enabled = True
	txtSockIn.Tag = pcIdel
    End If
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    Call cmdDisConn_Click
End Sub

Private Sub txtSockIn_KeyDown(KeyCode As Integer, Shift As Integer)
    Dim lngRet	    As Long		'戻り値
    Dim strRecv     As String * 100	'受信データ
    Dim strRecvBuf  As String		'受信バッファ
    Dim strErrMsg   As String		'エラーメッセージ

    Select Case txtSockIn.Tag
    Case pcIdel
    Case pcWaitingForConnect
	lstRecv.AddItem "Connected"
    'WSAASyncSelect
	lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hWnd, 0, 0)
	txtSockIn.Tag = pcWaitingForWrite
    Case pcWaitingForRead
    '受信
	strRecvBuf = ""
	Do While True
	    DoEvents
	    lngRet = recv(mlngSock, ByVal strRecv, 100, 0)
	    If (lngRet > 0) Then
		strRecvBuf = strRecvBuf & Left$(strRecv, lngRet)
		Exit Do
	    ElseIf lngRet = SOCKET_ERROR Then
		If WSAGetLastError() > 0 Then
		    strRecvBuf = ""
		    strErrMsg = "send:" & strWSAErrorGet(WSAGetLastError())
		    closesocket mlngSock
		    lngRet = WSACleanup()
		    GoTo exitKeyDown:
		End If
	    Else
		Exit Do
	    End If
	Loop
	lstRecv.AddItem strRecvBuf
    'WSAASyncSelect
	lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hWnd, &H100, FD_READ)
	txtSockIn.Tag = pcWaitingForRead
    Case pcWaitingForWrite
	cmdSend.Enabled = True
	lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hWnd, &H100, FD_READ)
	txtSockIn.Tag = pcWaitingForRead
    Case pcConnectionClosed
	Call cmdDisConn_Click
    End Select
exitKeyDown:
    On Error Resume Next
    If strErrMsg <> "" Then
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
	txtSockIn.Tag = pcIdel
    End If
End Sub

★状態遷移表

 ノンブロッキング型の通信プログラムを作るためのもうひとつの技術的要因は状態遷移の設計です.RFCまたは独自のプロトコルの仕様に沿って,プログラムがどのような動作をしたらよいかを設計するのに,状態遷移表はとても便利です(表4).  状態遷移表の列は「状態」を表わし,行は「事象(操作と通知)」を表わします.表の中には,それぞれの「状態」で該当する「事象」が発生したときにどのような動作をするかを記述します.通常は,状態の遷移のみで済むので「→(1)」のように次の状態を記述します.状態を遷移させるまえに何か操作を行なうときには,その操作名を記述します.  サンプルプログラムでは,KeyDownイベント発生時や,WinSock32 APIの関数を呼び出したときに,txtSockIn.Tabに状態を記録することで状態遷移表をプログラムに取り込んでいます.


表4:状態遷移表


●インターネット標準プロトコルをサポートする

 では,最後にRFCに記載されているインターネット標準プロトコルをサポートしたクライアントプログラムを作成します.インターネット標準プロトコルをサポートするときの注意点は

の3点です.RFCは,いろいろな本やWebサイトに日本語で翻訳されたものが掲載されていますが,一度はInterNICのWebサイト(http://ds.internic.net/rfc/)などでオリジナルドキュメントに目を通しておいた方がよいでしょう.翻訳文片手に読めば,英語で書かれたページでも段々理解できるようになってくると思います.インターネットと関連してプログラムを作成するときは,英語は避けて通れないものなので一度チャレンジしてみることをお勧めします.  そして,実際にクライアントプログラムをテストするときは,RFCに準拠したサーバーを確保して,十分テストを行なってください.もちろん,ベストといえる方法は,インターネットと切り離されたテストサーバーを用意することです.とくにSMTPクライアントなどのようにテスト方法を間違えると他のサイトに迷惑をかけてしまうようなプロトコルをサポートするときは,ある程度安定するまでテストサーバーを使ってテストして,全世界に迷惑をかけないように注意してください.そして,このテストを行なうときに,テストしているプロトコルをサポートしているクライアントプログラムとテスト中のプログラムの間で対向テストを十二分に行なってください.  なお,サーバーやクライアントのプログラムは,実際にニュースグループやメーリングリストなどで情報を収集して,使い勝手やRFCへの準拠の度合いを確認してから選択してください.「RFCに準拠」とカタログに書いてありながら実際にはきちんとサポートせず,インターネット上で迷惑を与えるようなサーバーやクライアントのソフトが見受けられます.こうしたソフトとの整合性をいくらとったところで,インターネットで迷惑をかけるプログラムができ上がってしまうからです.  インターネット標準プロトコルにも色々ありますが,ここでは,サーバーへの影響が少なく,応用範囲も広いと思われるPOP3(Post Office Protocol - Version 3)クライアントを題材にしたいと思います.

●POP3クライアントの作成

★POP3プロトコルについて

 POP3プロトコルは,RFC1725 (http://ds.internic.net/rfc/rfc1725.txt)の中に記述されています(表5).また,ヘッダ形式などは,RFC822 (http://ds.internic.net/rfc/rfc822.txt)の中で記述されています.  POP3プロトコルは,他のインターネット標準プロトコルと同様に単純なプロトコルです.基本的な流れとしては,

という流れになります.場合によっては,RETRコマンドの代わりにTOPコマンドによりヘッダ情報のみを取得することもできます.また,RETRコマンドでメール取得後,DELEコマンドによりPOP3サーバーからメールを削除する必要もあるかもしれません.  POP3クライアントを作成するにあたって,POP3サーバーとはどのようにコマンドをやりとりするのかを確認したいと思います.確認方法は簡単で,Telnetを使ってPOP3サーバーと接続して一般的な手順のPOP3コマンドを手動で送信することにより,やりとりをシミュレートできます(リスト5).  今回のPOP3クライアントは,

のようになっています(リスト6).


表5:POP3コマンド(抜粋)
POP3コマンド 意味
USER name メールボックスへの接続
PASS password メールボックスに対するパスワードを設定
QUIT メールボックスと切断
DELE コマンドで指定したメールを削除
LIST [msg] メールボックス内のメールの大きさの問い合わせ
RETR msg メールボックスからメールを取得
DELE msg メールボックスからのメールの削除を予約
NOOP タイムアウトクリア(長時間処理中のときに使用)
RSET DELEコマンドで指定した削除対象を無効化
STAT msg 指定メールのサイズを取得
TOP msg n 指定メールの指定行数を取得
UIDL [msg] メールに対する一意なIDを取得

リスト5:POP3クライアント動作ログ

+OK QPOP (version 2.2) at izumi.int21.co.jp starting.
USER fukuoka
+OK Password required for fukuoka.
PASS **************
+OK fukuoka has 6 messages (8928 octets).
STAT
+OK 6 messages (8928 octets)
LIST 2
+OK 756 octets
TOP 2 10
Date: Wed, 26 Nov 1997 00:18:26 +0900
From: Mia Yoshino 
To: fukuoka@int21.co.jp
Subject: =?ISO-2022-JP?B?GyRCJUYlOSVIJWElJCVrGyhC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit
Status: RO

B$3$l$O%F%9%H$G$9!#B

-------------------------
mia@int21.co.jp
http://pcdn.int21.co.jp/pcdn/
PC Developer Network


.
QUIT
+OK Pop server at izumi.int21.co.jp signing off.

リスト6:POP3クライアント(TCPPOP3.frm抜粋)

Private Sub txtSockIn_KeyDown(KeyCode As Integer, Shift As Integer)
    Dim lngRet	    As Long		'戻り値
    Dim strRecv     As String * 4096	'受信データ
    Dim strRecvBuf  As String		'受信バッファ
    Dim strErrMsg   As String		'エラーメッセージ
    Dim strComm     As String		'POP3コマンド

    Select Case txtSockIn.Tag
    Case pcIdel
    Case pcWaitingForConnect
	sbrMsg.SimpleText = "Connected"
    'WSAASyncSelect
	lngRet = WSAAsyncSelect(plngSock, txtSockIn.hWnd, &H100, FD_READ)
	txtSockIn.Tag = pcWaitingForRead
    Case pcWaitingForRead
    '受信
	strRecvBuf = ""
	DoEvents
	lngRet = recv(plngSock, ByVal strRecv, 4096, 0)
	If (lngRet > 0) Then
	    strRecvBuf = strRecvBuf & Left$(strRecv, lngRet)
	ElseIf lngRet = SOCKET_ERROR Then
	    If WSAGetLastError() > 0 Then
		strRecvBuf = ""
		strErrMsg = "recv:" & strWSAErrorGet(WSAGetLastError())
		closesocket plngSock
		lngRet = WSACleanup()
		GoTo exitKeyDown:
	    End If
	End If
	If InStr(StrConv(Left$(strRecvBuf, 4), vbUpperCase), "+OK") Then
	    sbrMsg.SimpleText = strRecvBuf
	    lngRet = WSAAsyncSelect(plngSock, txtSockIn.hWnd, &H100, FD_READ)
	    txtSockIn.Tag = pcWaitingForWrite
	    Select Case pintPop3Stat
	    Case pcPop3Idel
		pintPop3Stat = pcPop3User
		strComm = "USER " & txtUser.Text
		sbrMsg.SimpleText = strComm
		Call subSend(strComm & vbCrLf)
	    Case pcPop3User
		pintPop3Stat = pcPop3Pass
		strComm = "PASS " & txtPass.Text
		sbrMsg.SimpleText = "PASS " & String$(Len(txtPass.Text), "*")
		Call subSend(strComm & vbCrLf)
	    Case pcPop3Pass
		pintPop3Stat = pcPop3List
		strComm = "STAT"
		sbrMsg.SimpleText = strComm
		Call subSend(strComm & vbCrLf)
	    Case pcPop3List
		lvwMailBox.ListItems.Clear
		strRecvBuf = Mid$(strRecvBuf, InStr(strRecvBuf, " ") + 1)
		strRecvBuf = Left$(strRecvBuf, InStr(strRecvBuf, " ") - 1)
		mintMaxCnt = CInt(strRecvBuf)
		If mintMaxCnt = 0 Then
		'受信メールなし
		    Call subDisConn
		Else
		    mintMailCnt = 1
		    pintPop3Stat = pcPop3Top
		    strComm = "TOP 1 10"
		    sbrMsg.SimpleText = strComm
		    Call subSend(strComm & vbCrLf)
		End If
	    Case pcPop3Top
	    '一覧受信
		 Call subListSet(strRecvBuf)
		 If mintMaxCnt > mintMailCnt Then
		    pintPop3Stat = pcPop3Top
		    mintMailCnt = mintMailCnt + 1
		    strComm = "TOP " & CStr(mintMailCnt) & " 10"
		    sbrMsg.SimpleText = strComm
		    Call subSend(strComm & vbCrLf)
		 Else
		    Call subDisConn
		End If
	    Case pcPop3Quit
		pintPop3Stat = pcPop3Idel
		txtSockIn.Tag = pcIdel
		closesocket plngSock
		WSAUnhookBlockingHook
		WSACleanup
		sbrMsg.SimpleText = ""
	    Case Else
		strComm = "NOOP"
		sbrMsg.SimpleText = strComm
		Call subSend(strComm & vbCrLf)
	    End Select
	    txtSockIn.Tag = pcWaitingForRead
	ElseIf InStr(StrConv(Left$(strRecvBuf, 4), vbUpperCase), "-ERR") Then
	    sbrMsg.SimpleText = strRecvBuf
	    strErrMsg = "recv:" & strRecvBuf
	    closesocket plngSock
	    lngRet = WSACleanup()
	    GoTo exitKeyDown:
	Else
	    Select Case pintPop3Stat
	    Case pcPop3Top
	    '一覧受信
		 Call subListSet(strRecvBuf)
		 If mintMaxCnt > mintMailCnt Then
		    pintPop3Stat = pcPop3Top
		    mintMailCnt = mintMailCnt + 1
		    strComm = "TOP " & CStr(mintMailCnt) & " 10"
		    sbrMsg.SimpleText = strComm
		    Call subSend(strComm & vbCrLf)
		 Else
		    Call subDisConn
		End If
	    End Select
	    txtSockIn.Tag = pcWaitingForRead
	End If
    Case pcWaitingForWrite
	lngRet = WSAAsyncSelect(plngSock, txtSockIn.hWnd, &H100, FD_READ)
	txtSockIn.Tag = pcWaitingForRead
    Case pcConnectionClosed
	Call cmdDisConn_Click
    End Select
exitKeyDown:
    On Error Resume Next
    If strErrMsg <> "" Then
	MsgBox strErrMsg, vbOKOnly + vbExclamation, App.Title
	cmdConnect.Enabled = True
	txtSockIn.Tag = pcIdel
    End If
End Sub

★2バイト文字の扱い

 作成したPOP3クライアントプログラムを実行すると正しく日本語が表示されないことが分かります(図6).インターネットでは歴史的に一部の回線が7ビットしか通せないことがあったので,日本語などの2バイト文字も6ビットに収まるように変換します.これをエンコードと呼びます.エンコードしたものを元に戻す操作はデコードです.現在一般的に使われている形式はBase64と呼ばれ,これは3バイトのテキストを6ビットごとに区切り,4文字のASCII文字列(A〜Za〜z0〜9+/)に変換するものです.  なお,インターネットメールを使うときに,そのヘッダに日本語を使わない方がよいという意見は,このようにデコードを行なわなくても「誰が」「いつ」「何を」送ってきたかがかるようにするためです.  以上,WinSock32 APIの使い方について一通りまとめてみました.状態遷移表などの設計は難しいかもしれませんが,なるべくシンプル独自プロトコルとすることで,難易度もかなり低くなると思います.ネットワークプログラムはメールなどのやりとりや業務プログラム間の通信に使うだけではなく,対戦型の通信ゲームなど幅広い分野に応用できると思います.RFCという道標を携えて,ぜひネットワークプログラムにチャレンジしてみてください.

図6:POP3クライアント

参考文献:Microsoft Developer Network/Win32SDK

サンプルプログラムはこちらからダウンロードできます。


VB Magazine ライブラリ | Visual Basic WorkGroup
int21 ホームページ | PCDN ホームページ


Copyright (c) 1998 int21 Corporation All Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp