2012/01/19 ポート番号をホストバイトオーダで指定していた問題を修正しました。イノビア様、ご指摘ありがとうございました。
HSPでソケットを使うにはhspsockなどの拡張プラグインがありますけど、せっかくHSP3でメッセージ割り込み処理が可能になっているというのに、ループを作ってその中で接続や受信をチェックして…などというのは無駄が多いと思うのですよ。
それよりも、WIndowsソケット(winsock)のWSAAsyncSelect関数による非同期ソケットのほうが相性が良いと思うのです。接続やら受信やらのイベント通知をウィンドウメッセージで受け取ることができますからね。
ということで、今回はHSPでの非同期ソケットに挑戦してみました。
サーバ 側とクライアント側を別々に作成します。
どちらの場合も、socket関数でソケットを生成したら直ちにWSAAsyncSelect関数に渡すことで、ソケットイベントがウィンドウメッセージとして通知されるようにしておきます。
まずはサーバから。最初のリスナソケット(listenを行うソケット)はWSAAsyncSelectに渡しますが、そのリスナソケットのaccept時に受け取るクライアント接続のソケットは自動的に非同期ソケットになっているので、こちらのほうはわざわざWSAAsyncSelectに渡す必要はありません。
ポート番号はネットワークバイトオーダ(TCP/IPの場合はビッグエンディアン)で指定しなければならないので、htons関数をかませてあります。ちなみに、本来なら2バイト値を返すhtonsに対しては
(htons(listener_port) & 0xFFFF)
という値を取るべきなんですが、wpokeが下位2バイトのみをセットしてくれるのでそのままにしています。
; Winsock通知時のウィンドウメッセージ
#define SOCK_NOTIFY_MESSAGE 0x1000
; 外部関数(WinAPI)定義
#uselib "ws2_32.dll"
#func WSAStartup "WSAStartup" int,int
#func WSACleanup "WSACleanup"
#cfunc htons "htons" int
#cfunc socket "socket" int,int,int
#func WSAAsyncSelect "WSAAsyncSelect" int,int,int,int
#func bind "bind" int,int,int
#func listen "listen" int,int
#cfunc accept "accept" int,int,int
#func closesocket "closesocket" int
#func send "send" int,int,int,int
#cfunc recv "recv" int,int,int,int
#func shutdown "shutdown" int,int
#define AF_INET $00000002
#define SOCK_STREAM $00000001
#define IPPROTO_TCP $00000006
#define INVALID_SOCKET $FFFFFFFF
#define FD_READ $00000001
#define FD_ACCEPT $00000008
#define ADDR_ANY $00000000
#define SD_BOTH $00000002
#define WSAEWOULDBLOCK $00002733
; Winsockの初期化
winsockver = 0x0002 ; バージョン2.0
dim wsadata, 100
WSAStartup winsockver, varptr(wsadata)
onexit goto *cleanup
; リスンポート番号
listener_port = 10000
; リスナーソケット生成
hlistener = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )
; ソケット通知設定
WSAAsyncSelect hlistener, hwnd, SOCK_NOTIFY_MESSAGE, FD_READ | FD_ACCEPT
oncmd gosub *on_sock_notify, SOCK_NOTIFY_MESSAGE
; sockaddr_in 構造体のセット
dim sockaddr, 4
wpoke sockaddr, 0, AF_INET
wpoke sockaddr, 2,
htons(listener_port)
lpoke sockaddr, 4, ADDR_ANY
; バインド
bind hlistener, varptr(sockaddr), 16
if stat != 0 : dialog "バインドエラー" : goto *cleanup
; リスン開始
listen hlistener, 5
stop
*on_sock_notify
sock_event = lparam & 0xFFFF ; winsockイベント種別
sock_error = (lparam >> 16) &0xFFFF ; winsockエラー
switch sock_event
case FD_ACCEPT ; クライアントからの接続要求
hclient = accept( hlistener, 0, 0 )
mes "接続" + hclient
swbreak
case FD_READ ; 受信データ読み込み可能
hclient = wparam
; ソケットから読み込み(1バイトだけ)
sdim recv_data, 4
recv_size = recv( hclient, varptr(recv_data), 1, 0 )
; 受信完了時
if recv_size >= 0 {
mes "受信:" + recv_data
; ソケットに書き込み(1バイトだけ)
send_data = "A"
send hclient, varptr(send_data), 1, 0
mes "応答送信:" + send_data
; クローズ
shutdown hclient, SD_BOTH
closesocket hclient
}
swbreak
swend
return
*cleanup
; リスナーソケットをクローズ
closesocket hlistener
; Winsockのクリーンアップ
WSACleanup
end
クライアントから接続要求を受けたときはFD_ACCEPTを受けるので、そのときにacceptを行います。
受信データ時にFD_READを受け取るので、そのときにrecvを行えばOK。ここでは要求データ受信直後に応答データ「A」1文字を送信しています。
今回は簡略化のため1バイトデータのやり取りのみを行っています。というのも、TCPは1回のsendされたデータを1回のrecvで受け取るとは限らず、パケットが結合されたり分割されたりするので、本来はそれに備えた処理を入れる必要があるのですが、それを省くために1バイトデータのみのやり取りとしました。
また、今回は必要最低限のエラー処理のみ入れておきますが、実際には他のエラー処理も入れる必要があります。
次にクライアント。connectで接続要求をした後はFD_CONNECT通知を待つことになります。
FD_CONNECT通知をエラーなしで受け取った場合は書き込み。接続確立後にデータ「R」1文字を送信し、応答を待ちます。
応答を受信するとFD_READ通知を受けるので、recvで受信するようにしています。
; Winsock通知時のウィンドウメッセージ
#define SOCK_NOTIFY_MESSAGE 0x1000
; 外部関数(WinAPI)定義
#uselib "ws2_32.dll"
#func WSAStartup "WSAStartup" int,int
#func WSACleanup "WSACleanup"
#cfunc htons "htons" int
#cfunc socket "socket" int,int,int
#func WSAAsyncSelect "WSAAsyncSelect" int,int,int,int
#func connect "connect" int,int,int
#func closesocket "closesocket" int
#func send "send" int,int,int,int
#cfunc recv "recv" int,int,int,int
#func shutdown "shutdown" int,int
#cfunc inet_addr "inet_addr" sptr
#cfunc WSAGetLastError "WSAGetLastError"
#define AF_INET $00000002
#define SOCK_STREAM $00000001
#define IPPROTO_TCP $00000006
#define INVALID_SOCKET $FFFFFFFF
#define FD_READ $00000001
#define FD_CONNECT $00000010
#define SD_BOTH $00000002
#define WSAEWOULDBLOCK $00002733
; Winsockの初期化
winsockver = 0x0002 ; Winsockバージョン2.0
dim wsadata, 100
WSAStartup winsockver, varptr(wsadata)
onexit goto *cleanup
; 接続先(自分自身のポート10000)
target_addr = "127.0.0.1"
target_port = 10000
; ソケット生成
hsock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )
; ソケット通知設定
WSAAsyncSelect hsock, hwnd, SOCK_NOTIFY_MESSAGE, FD_READ | FD_CONNECT
oncmd gosub *on_sock_notify, SOCK_NOTIFY_MESSAGE
; sockaddr_in 構造体のセット
dim sockaddr, 4
wpoke sockaddr, 0, AF_INET
wpoke sockaddr, 2,
htons(target_port)
lpoke sockaddr, 4, inet_addr(target_addr)
; 接続
connect hsock, varptr(sockaddr), 16
if stat != 0 {
if WSAGetLastError() != WSAEWOULDBLOCK {
dialog "接続できませんでした"
goto *cleanup
}
}
stop
*on_sock_notify ; winsock通知メッセージ受信時
sock_event = lparam & 0xFFFF ; winsockイベント種別
sock_error = (lparam >> 16) &0xFFFF ; winsockエラー番号
switch sock_event
case FD_CONNECT ; 接続完了(or接続エラー)通知
if (sock_error != 0) {
dialog "接続できませんでした"
goto *cleanup
}
mes "接続完了"
; ソケットに書き込み(1バイトだけ)
send_data = "R"
send hsock, varptr(send_data), 1, 0
mes "送信:" + send_data
swbreak
case FD_READ ; 受信データ読み込み可能
; ソケットから読み込み(1バイトだけ)
sdim recv_data, 4
recv_size = recv( hsock, varptr(recv_data), 1, 0)
if recv_size >= 0 {
mes "受信:" + recv_data
shutdown hsock, SD_BOTH
closesocket hsock
}
swbreak
swend
return
*cleanup
; Winsockのクリーンアップ
WSACleanup
end
各種Winsock APIについての説明は省いていますので、分からなければ検索してみてください。
(いずれ専用の解説ページ作りたいとは思っていますが、いかんせん時間が…)
PR