2012/01/19 ポート番号をホストバイトオーダで指定していた問題を修正しました。
さて、今日もまたWinsockです。
初回に書いたブログラムは1バイトデータの送受信を前提にしていました。
というのも、TCPというプロトコルは、ある1回のsendで送信したデータのかたまりが、相手側の1回のrecvでそのまま受信されるわけではないために、ちょっと面倒な手順を踏まなければならなかったからです。
初回のプログラムは導入ということもあって、その手の面倒な手続きを記述しなくて済むように、1バイトデータのみを扱ったのでした。
今回は、何バイトものデータを送受信する方法を試してみましょう。
TCPを使ってアプリケーションレベルでのデータのかたまり(「データグラム」単位、いわゆる「パケット」単位のデータ)の送受信を行うための方法については以下の解説が詳しいです。
Winsock Programmer's FAQ - 第3章: Winsock 中級者向けの議論
3.4 - TCPのようなストリームプロトコルで、パケット単位の処理を強制するための正しい方法は?
http://www.kt.rim.or.jp/~ksk/wskfaq-ja/intermediate.html#packetscheme
Winsock Programmer's FAQ - 第7章: 論説記事: TCP を有効に使うために
http://www.kt.rim.or.jp/~ksk/wskfaq-ja/articles/effective-tcp.html
Winsock Programmer's FAQ - 第7章: 論説記事: ザ・間違いリスト
20. ストリームソケットで、メッセージフレームの区切りが保持されると仮定すること。
http://www.kt.rim.or.jp/~ksk/wskfaq-ja/articles/lame-list.html#item20
今回は、それぞれのデータグラムの先頭にデータ長をつける方式でやってみましょう。
それぞれのパケットの先頭に2バイトとのデータ長を付加します。
ただし、このデータ長の値には、データ長フィールド自身のサイズ(=2byte)は含めず、送信したい実データの長さのみを含めることにします。
また、本来TCPでは、データ含まれる2バイト以上の数値をネットワークバイトオーダと呼ばれるバイト順序(TCP/IPの場合はビッグエンディアン)に整列することが望ましいのですが(上記リンク「TCPを有効に使うために」参照)、ここでは簡単化のため、HSP(というよりx86処理系)の内部表現であるリトルエンディアンのままで渡すことにします(基本的には送信側と受信側のバイト順序が一致していれば問題ないので)。
受信について気をつけることは以上なのですが、送信についてもひとつだけ。
いくつかのWinsock解説をしているサイトでは、非同期socketにおいても、単にsend関数に渡して、戻り値がSOCKET_ERRORでなければ送信成功という形になっているのですが、実際はWinsockの内部送信バッファの空き容量によっては、sendに渡したもののうち一部のみしか送信されないこともありえます。
また、send呼び出し時に内部送信バッファがいっぱいであれば、send呼び出しはWSAEWOULDBLOCKエラーで失敗します。
正しい送信の方法は以下を参照。
Winsock Programmer's FAQ - 第3章: Winsock 中級者向けの議論
3.15 - 非同期型ソケットは信頼できない、と聞いたことがあります。それって本当ですか?
http://www.kt.rim.or.jp/~ksk/wskfaq-ja/intermediate.html#asyncreliable
一般的には、今回のように非同期socketを用いる場合には、アプリケーションレベルでの送信バッファと受信バッファを準備する必要があります。
さもないと、TCPレベルでのWinsock内部バッファがあるとはいえ、それでは上記の受信時の処理を実現することはできませんし、送信においてもデータの一部を送信しそこなう可能性があります。
さて、前回のプログラムをアレンジして、長いデータ(今回は文字列)を送受信してみます。
今回はサーバ・クライアント共に送信バッファと受信バッファを準備し、FD_WRITEとFD_CLOSEソケットイベントにも対応します。
まずはサーバから。
サーバからの文字列を含むデータの塊(パケットと呼ぶことにする)を受けたら、そのデータにちょっと文字列を付加して送り返します。
サーバで同時に複数の接続をできるようにすると、これまた処理が面倒くさくなるので(ソケットイベントの発生元判定や、各接続ごとにバッファを準備するなど)、今回はサーバとクライアント間の接続を同時に1つまでに制限します。
Winsockの初期化については手を抜いているので注意。
; 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
#cfunc 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 SOCKET_ERROR $FFFFFFFF
#define FD_READ $00000001
#define FD_WRITE $00000002
#define FD_ACCEPT $00000008
#define FD_CLOSE $00000020
#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 | FD_WRITE | FD_CLOSE
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
; 接続中クライアントソケット変数を初期化
hclient = INVALID_SOCKET
; 送受信バッファ準備
sendbuffsize = 4096 ; 現在の送信バッファサイズ
sdim sendbuff, sendbuffsize ; 送信バッファ確保
sendstoredsize = 0 ; 現在の送信バッファのデータ格納サイズ
recvbuffsize = 4096 ; 現在の受信バッファサイズ
sdim recvbuff, recvbuffsize ; 受信バッファ確保
recvstoredsize = 0 ; 現在の受信バッファのデータ格納サイズ
stop
*on_sock_notify
sock_event = lparam & 0xFFFF ; winsockイベント種別 (FD_***)
sock_error = (lparam >> 16) & 0xFFFF ; winsockエラー (0:エラーなし)
switch sock_event
case FD_ACCEPT ; クライアントからの接続要求
hnewclient = accept( hlistener, 0, 0 )
mes "接続(" + hnewclient +")"
; 接続中なら新しい接続を直ちに切断
if hclient != INVALID_SOCKET {
shutdown hnewclient
closesocket hnewclient
mes "別クライアント接続中のため切断(" + hnewclient +")"
} else {
hclient = hnewclient
}
swbreak
case FD_READ ; 受信データ読み込み可能
if wparam != hclient {
swbreak
}
; recvが0またはSOCKET_ERROR(WSAEWOULDBLOCKエラーを含む)を返すまでループ
repeat
; 受信バッファの空き領域いっぱいを指定してソケットから読み込み
recvsize = recv( hclient, varptr(recvbuff) + recvstoredsize, recvbuffsize - recvstoredsize, 0 )
if (recvsize == 0) | (recvsize == SOCKET_ERROR) {
break
}
; 受信バッファ格納データサイズ更新
recvstoredsize += recvsize
; 受信バッファに含まれる全パケットを処理するまでループ
repeat
; 格納データが2バイト(データ長領域サイズ)未満なら抜ける
if recvstoredsize < 2 {
break
}
; 先頭のパケットデータ長を取得
packetsize = wpeek(recvbuff, 0)
; 現在の受信バッファが次のパケット全体を格納できない場合はバッファ拡張
if packetsize > recvbuffsize {
recvbuffsize = packetsize + 1024
memexpand recvbuff, recvbuffsize
}
; 格納データが次のパケット長(データ長領域の2バイト含む)未満なら抜ける
if recvstoredsize < (packetsize + 2) {
break
}
; パケットデータをコピー
sdim packetdata, packetsize + 1
memcpy packetdata, recvbuff, packetsize, 0, 2
; パケット処理ルーチン
gosub *on_recv_packet
; 受信バッファ格納データサイズ更新
recvstoredsize -= packetsize + 2
; バッファの後ろのデータを先頭に移動
memcpy recvbuff, recvbuff, recvstoredsize, 0, packetsize + 2
loop
loop
swbreak
case FD_WRITE ; 送信可能
if wparam != hclient {
swbreak
}
; 送信バッファデータの送信処理
gosub *do_sock_send
swbreak
case FD_CLOSE ; 切断
if wparam != hclient {
swbreak
}
shutdown hclient, SD_BOTH
closesocket hclient
mes "切断(" + hclient +")"
hclient = INVALID_SOCKET
swbreak
swend
return
*do_sock_send
; 送信バッファデータの送信処理
; 全データ送信するか SOCKET_ERROR (WSAEWOULDBLOCKエラーを含む) を返すまでループ
while sendstoredsize > 0
sentsize = send( hclient, varptr(sendbuff), sendstoredsize, 0 )
if sentsize == SOCKET_ERROR {
_break
}
; 送信バッファ格納データサイズ更新
sendstoredsize -= sentsize
wend
return
*on_recv_packet ; パケットデータ処理ルーチン
if packetsize > 0 {
mes "データ受信(" + hclient +") [" + packetdata + "]"
; 応答用の送信データを構築
replydata = "応答 [" + packetdata + "]"
replysize = strlen(replydata)
; 送信バッファ領域に次の送信データを追加できない場合は拡張
if sendbuffsize < sendstoredsize + replysize + 2 {
sendbuffsize = sendstoredsize + replysize + 2
memexpand sendbuff, sendbuffsize
}
; 送信バッファの末尾に追加 (データサイズ(2byte)とデータ)
wpoke sendbuff, sendstoredsize, replysize
memcpy sendbuff, replydata, replysize, sendstoredsize + 2, 0
; 送信バッファ格納データサイズ更新
sendstoredsize += replysize + 2
; 送信バッファデータの送信処理
gosub *do_sock_send
mes "データ送信(" + hclient +") [" + replydata + "]"
}
return
*cleanup
; リスナーソケットをクローズ
closesocket hlistener
; クライアントソケットをクローズ
if hclient != INVALID_SOCKET {
shutdown hclient, SD_BOTH
closesocket hclient
mes "切断(" + hclient +")"
hclient = INVALID_SOCKET
}
; Winsockのクリーンアップ
WSACleanup
end
次にクライアント。
前回のプログラムでは接続・送受したら即切断でしたが、今回はプログラム起動中は接続状態を保つようにしています。
; 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
#cfunc 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 SOCKET_ERROR $FFFFFFFF
#define FD_READ $00000001
#define FD_WRITE $00000002
#define FD_CONNECT $00000010
#define FD_CLOSE $00000020
#define SD_BOTH $00000002
#define WSAEWOULDBLOCK $00002733
; Winsockの初期化
winsockver = 0x0002 ; Winsockバージョン2.0
dim wsadata, 100
WSAStartup winsockver, varptr(wsadata)
onexit goto *cleanup
; 送信用テキストボックス
sdim sendtext, 2000
input sendtext, ginfo_winx
button gosub "送信", *send_text
; 接続先(自分自身のポート10000)
target_addr = "127.0.0.1"
target_port = 10000
; ソケット生成
hclient = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )
; ソケット通知設定
WSAAsyncSelect hclient, hwnd, SOCK_NOTIFY_MESSAGE, FD_READ | FD_WRITE | FD_CONNECT | FD_CLOSE
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 hclient, varptr(sockaddr), 16
if stat != 0 {
if WSAGetLastError() != WSAEWOULDBLOCK {
dialog "接続できませんでした"
goto *cleanup
}
}
; 送受信バッファ準備
sendbuffsize = 4096 ; 現在の送信バッファサイズ
sdim sendbuff, sendbuffsize ; 送信バッファ確保
sendstoredsize = 0 ; 現在の送信バッファのデータ格納サイズ
recvbuffsize = 4096 ; 現在の受信バッファサイズ
sdim recvbuff, recvbuffsize ; 受信バッファ確保
recvstoredsize = 0 ; 現在の受信バッファのデータ格納サイズ
stop
*send_text
; 送信データを構築
datasize = strlen(sendtext)
; 送信バッファ領域に次の送信データを追加できない場合は拡張
if sendbuffsize < sendstoredsize + datasize + 2 {
sendbuffsize = sendstoredsize + datasize + 2
memexpand sendbuff, sendbuffsize
}
; 送信バッファの末尾に追加 (データサイズ(2byte)とデータ)
wpoke sendbuff, sendstoredsize, datasize
memcpy sendbuff, sendtext, datasize, sendstoredsize + 2, 0
; 送信バッファ格納データサイズ更新
sendstoredsize += datasize + 2
; 送信バッファデータの送信処理
gosub *do_sock_send
mes "データ送信(" + hclient +") [" + sendtext + "]"
return
*on_sock_notify ; winsock通知メッセージ受信時
if wparam != hclient {
return
}
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 "接続完了(" + hclient + ")"
swbreak
case FD_READ ; 受信データ読み込み可能
if wparam != hclient {
swbreak
}
; recvが0またはSOCKET_ERROR(WSAEWOULDBLOCKエラーを含む)を返すまでループ
repeat
; 受信バッファの空き領域いっぱいを指定してソケットから読み込み
recvsize = recv( hclient, varptr(recvbuff) + recvstoredsize, recvbuffsize - recvstoredsize, 0 )
if (recvsize == 0) | (recvsize == SOCKET_ERROR) {
break
}
; 受信バッファ格納データサイズ更新
recvstoredsize += recvsize
; 受信バッファに含まれる全パケットを処理するまでループ
repeat
; 格納データが2バイト(データ長領域サイズ)未満なら抜ける
if recvstoredsize < 2 {
break
}
; 先頭のパケットデータ長を取得
packetsize = wpeek(recvbuff, 0)
; 現在の受信バッファが次のパケット全体を格納できない場合はバッファ拡張
if packetsize > recvbuffsize {
recvbuffsize = packetsize + 1024
memexpand recvbuff, recvbuffsize
}
; 格納データが次のパケット長(データ長領域の2バイト含む)未満なら抜ける
if recvstoredsize < (packetsize + 2) {
break
}
; パケットデータをコピー
sdim packetdata, packetsize + 1
memcpy packetdata, recvbuff, packetsize, 0, 2
; パケット処理ルーチン
gosub *on_recv_packet
; 受信バッファ格納データサイズ更新
recvstoredsize -= packetsize + 2
; バッファの後ろのデータを先頭に移動
memcpy recvbuff, recvbuff, recvstoredsize, 0, packetsize + 2
loop
loop
swbreak
case FD_WRITE ; 送信可能
if wparam != hclient {
swbreak
}
; 送信バッファデータの送信処理
gosub *do_sock_send
swbreak
case FD_CLOSE ; 切断
if wparam != hclient {
swbreak
}
shutdown hclient, SD_BOTH
closesocket hclient
mes "切断(" + hclient +")"
hclient = INVALID_SOCKET
swbreak
swend
return
*do_sock_send
; 送信バッファデータの送信処理
; 全データ送信するか SOCKET_ERROR (WSAEWOULDBLOCKエラーを含む) を返すまでループ
while sendstoredsize > 0
sentsize = send( hclient, varptr(sendbuff), sendstoredsize, 0 )
if sentsize == SOCKET_ERROR {
_break
}
; 送信バッファ格納データサイズ更新
sendstoredsize -= sentsize
wend
return
*on_recv_packet ; パケットデータ処理ルーチン
if packetsize > 0 {
mes "データ受信(" + hclient +") [" + packetdata + "]"
}
return
*cleanup
; 接続中の場合は切断
if hclient != INVALID_SOCKET {
shutdown hclient, SD_BOTH
closesocket hclient
mes "切断(" + hclient +")"
hclient = INVALID_SOCKET
}
; Winsockのクリーンアップ
WSACleanup
end
上記のプログラムを見るとわかりますが、送受信処理と切断処理に関わる部分はサーバとクライアントとでまったく同じコードになっています。
いずれ共通化して書きたいものです。
では、今日はこの辺で。
PR