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).
|
関数名 |
処理 |
|
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 |
ソケットの動作パラメータの取得と設定 |
|
関数名 |
処理 |
|
WSAStartup |
WinSock32 APIを初期化 |
|
WSACleanup |
すべての未処理のデータを送信し,ソケットを閉じる |
|
WSAGetLastError |
最後に発生したエラーを取得 |
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).整数データをネットワークを介してバイナリ形式で送受信するときは,ビッグエンディアン方式にすることが決まっています.これをネットワークバイトオーダーと呼びます.

★プログラミングスタイル
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)を使います.

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

| ●ノンブロッキング型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を指定してください.
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に状態を記録することで状態遷移表をプログラムに取り込んでいます.

| ●インターネット標準プロトコルをサポートする |
では,最後に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クライアントは,
pintPop3Stat = pcPop3User strComm = "USER " & txtUser.Text sbrMsg.SimpleText = strComm Call subSend(strComm & vbCrLf) |
pintPop3Stat = pcPop3Pass strComm = "PASS " & txtPass.Text sbrMsg.SimpleText = strComm Call subSend(strComm & vbCrLf) |
pintPop3Stat = pcPop3List strComm = "STAT" sbrMsg.SimpleText = strComm Call subSend(strComm & vbCrLf) |
pintPop3Stat = pcPop3Top strComm = "TOP 1 10" sbrMsg.SimpleText = strComm Call subSend(strComm & vbCrLf) |
のようになっています(リスト6).
| POP3コマンド | 意味 |
| USER name | メールボックスへの接続 |
| PASS password | メールボックスに対するパスワードを設定 |
| QUIT | メールボックスと切断 |
| DELE | コマンドで指定したメールを削除 |
| LIST [msg] | メールボックス内のメールの大きさの問い合わせ |
| RETR msg | メールボックスからメールを取得 |
| DELE msg | メールボックスからのメールの削除を予約 |
| NOOP | タイムアウトクリア(長時間処理中のときに使用) |
| RSET | DELEコマンドで指定した削除対象を無効化 |
| STAT msg | 指定メールのサイズを取得 |
| TOP msg n | 指定メールの指定行数を取得 |
| UIDL [msg] | メールに対する一意なIDを取得 |
+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 |
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という道標を携えて,ぜひネットワークプログラムにチャレンジしてみてください.

参考文献:Microsoft Developer Network/Win32SDK
サンプルプログラムはこちらからダウンロードできます。