第八章WinSock 2應用實例
http://www.cic.tsinghua.edu.cn/sys/book2/eight8.htm
本章將用兩個例子來介紹Windows Sockets 2函數的使用。 第一個例子是簡單的點對點網路即時通信程式ws2echo,它介紹基本的Windows Sockets 2函數的使用;第二個是多址廣播通信程式multichat,它介紹如何使用Windows Sockets 2多址廣播通信函數。
8.1 WinSock 2基本函數的使用
為了介紹WinSock 2基本函數的使用,我們繼續前面的例子——簡單的點對點網路即時通信程式echo。該實例分兩部分:客戶程式與伺服器程式。其工作過程是:伺服器首先啟動,它創建套接字之後等待客戶的連接;客戶啟動後,創建套接字,然後和伺服器建立連接;連接建立後,客戶接收鍵盤輸入,收到一行後將資料發送到伺服器,伺服器收到資料後,只是簡單地發送回來,客戶將收到的資料在視窗中顯示;直到用戶關閉應用程式。
8.1.1 客戶程式
首先介紹客戶程式,該原始檔案取名為ws2echoc.c,其內容在下面列出。為了方便讀者,在程式中關鍵部分採用中文注釋的形式給出說明,讀者可照這些注釋加強對第七章內容的理解。
#ifndef _WINSOCKAPI_
#define _WINSOCKAPI_ /* Prevent inclusion of winsock.h in windows.h */
#endif
#include
#include
#include
#include
#include "ws2echo.h"
SOCKET Sockets; // 套接字描述符
HWND GlobalhWindow;
char EchoClassStr[] = "WS2EchoClient";
char sbuf[BUFFER_LENGTH], rbuf[BUFFER_LENGTH]; // 發送/接收緩衝區
DWORD TotalByteSent; // 總的發送位元組數
WSABUF SendBuf, RecvBuf; // 發送/接收緩衝區結構
int WINAPI
WinMain(
IN HINSTANCE InstanceHandle,
IN HINSTANCE PrevInstanceHandle,
IN LPSTR CmdLine,
IN int CmdShow)
{
MSG Message;
if (!InitWindow(InstanceHandle, PrevInstanceHandle)) {
MessageBox(NULL, "Couldn't Initialize Echo!", "Error",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
return(0);
}
GlobalhWindow = CreateWindow(EchoClassStr, "WinSock 2 Echo Client",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, InstanceHandle, NULL);
if (GlobalhWindow == NULL) {
MessageBox(NULL, "CreateWindow()!", "Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
return(0);
}
if (!InitWS2()) // 啟動WinSock 2.
return(0);
if (!MakeConnection()) // 與伺服器建立連接
return(0);
ShowWindow(GlobalhWindow, CmdShow);
UpdateWindow(GlobalhWindow);
while (GetMessage(&Message, NULL, 0, 0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
WSACleanup(); // 收到WM_QUIT消息後,結束WinSock 2.
return(Message.wParam);
} // WinMain()
long CALLBACK
MainWndProc(
IN HWND WindowHandle,
IN UINT Message,
IN WPARAM WParam,
IN LPARAM LParam)
{
int retcode;
char MsgText[128];
switch (Message) {
case UM_SOCK:
switch (LParam) {
case FD_CONNECT: //連接建立完成,置標誌。
WSAAsyncSelect(Sockets, GlobalhWindow, UM_SOCK, FD_READ);
break;
case FD_READ: //資料讀準備好,接收網路資料。
retcode = DoRecv(Sockets, &RecvBuf);
if (retcode == 1)
DisplayInfo(WindowHandle, &RecvBuf);
break;
case FD_WRITE: //寫準備好,發送資料。
retcode = DoOverlappedCallbackSend(Sockets, &SendBuf);
SendBuf.len = 0;
break;
case FD_CLOSE: //連接關閉。
closesocket(Sockets);
break;
default:
if (WSAGETSELECTERROR(LParam) != 0) {
sprintf(MsgText, "Client: LParam=%d",WSAGETSELECTERROR(LParam));
MessageBox(GlobalhWindow, MsgText,
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
closesocket(Sockets);
}
break;
}
break;
case WM_CHAR:
if (WParam == 0x0d) // 用戶鍵入回車鍵則發送一行。
PostMessage(GlobalhWindow, UM_SOCK, (WPARAM) WParam, (LPARAM)FD_WRITE);
else
SendBuf.buf[SendBuf.len ] = (char)WParam;
break;
case WM_DESTROY: // 關閉監聽套接字並退出應用程式
closesocket(Sockets);
PostQuitMessage(0);
break;
default:
return (DefWindowProc(WindowHandle, Message, WParam, LParam));
} // switch(Message)
return (0);
} // MainWndProc()
/*
* 啟動WinSock 2,協商WinSock版本號,初始化接收發送緩衝區。
* 成功返回TRUE,失敗返回FALSE。
*/
BOOL
InitWS2(void)
{
int Error; // catches return value of WSAStartup
WORD VersionRequested; // passed to WSAStartup
WSADATA WsaData; // receives data from WSAStartup
BOOL ReturnValue = TRUE; // return value
VersionRequested = MAKEWORD(VERSION_MAJOR, VERSION_MINOR);
Error = WSAStartup(VersionRequested, &WsaData); // 啟動WinSock 2
if (Error) {
MessageBox(GlobalhWindow, "Could not find high enough version of WinSock",
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
ReturnValue = FALSE;
} else {// 確認WinSock 2 DLL支援我們需要的精確版本號,否則,調用WSACleanup()。
if (LOBYTE(WsaData.wVersion) != VERSION_MAJOR
|| HIBYTE(WsaData.wVersion) != VERSION_MINOR) {
MessageBox(GlobalhWindow, "Could not find the correct version of WinSock",
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
WSACleanup();
ReturnValue = FALSE;
}
}
SendBuf.len = RecvBuf.len = 0; // 初始化接收/發送緩衝區。
SendBuf.buf = sbuf;
RecvBuf.buf = rbuf;
return(ReturnValue);
} // InitWS2()
/*
* 和伺服器建立連接。成功返回TRUE,失敗返回FALSE。
*/
BOOL
MakeConnection(void)
{
int ConnectStatus; // the return value of WSAConnect
int Error; // gets error values
BOOL ReturnValue; // holds the return value
struct sockaddr_in SockAddr; // socket address for WSAConnect
int SockAddrLen; // the length of the above
UCHAR MsgText[MAX_ERROR_TEXT];
Sockets = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (Sockets == INVALID_SOCKET) {
sprintf(MsgText, "Could not open a socket. [%x]", WSAGetLastError());
MessageBox(GlobalhWindow, MsgText, "Non-fatal error.", MB_OK | MB_SETFOREGROUND);
return(FALSE);
}
WSAAsyncSelect(Sockets, GlobalhWindow, UM_SOCK, FD_CONNECT); // 設置連接建立的事件通知
SockAddr.sin_family = AF_INET;
SockAddr.sin_port = htons(UserPort);
SockAddr.sin_addr.s_addr = inet_addr(ServerName);
SockAddrLen = sizeof(SockAddr);
// 和遠端伺服器建立連接。
ConnectStatus = WSAConnect(Sockets, (struct sockaddr *)&SockAddr, SockAddrLen,
NULL, NULL, NULL, NULL);
if (ConnectStatus == SOCKET_ERROR) {
Error = WSAGetLastError();
if (Error != WSAEWOULDBLOCK) { // 錯誤WSAEWOULDBLOCK意味著連接稍後建立
wsprintf(MsgText, "WSAConnect failed. Error code: %d", Error);
MessageBox(GlobalhWindow, MsgText, "Error",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
ReturnValue = FALSE;
}
}
return(ReturnValue);
} // MakeConnection()
/*
* 接收資料。
*/
int
DoRecv(SOCKET Socket, LPWSABUF lpRecvBuffer)
{
DWORD NumBytes; // stores how many bytes we received
int Error; // gets error values
int Result; // gets return value from WSARecv
DWORD Flags=0; // flags for WSARecv
int ReturnValue = 1; // returnValue
lpRecvBuffer->len = BUFFER_LENGTH - 1;
Result = WSARecv(Socket, lpRecvBuffer, 1, &NumBytes, &Flags, NULL, NULL); // 接收資料
if (Result == SOCKET_ERROR) {
Error = WSAGetLastError();
switch (Error) {
case WSAENETRESET:
case WSAECONNRESET:
ReturnValue = -1; // 連接被重置
break;
case WSAEWOULDBLOCK: // 沒有資料可供讀取
ReturnValue = 0;
break;
default: // 其他錯誤
ReturnValue = -2;
break;
}
}
lpRecvBuffer->len = NumBytes;
return(ReturnValue);
} // DoRecv()
/*
* 使用回調函數的重疊發送方式發送資料。
*/
int
DoOverlappedCallbackSend(
SOCKET Socket,
LPWSABUF Buffers)
{
int Size = 0; // how many bytes we send
int Error; // return value of WSASend
int Errno; // result of WSAGetLastError
DWORD BytesSent; // needed in WSASend
int ReturnValue = 1; // the return value
WSAOVERLAPPED Overlapped;
// 發送資料。完成常式設置為函數SendCompFunc()。
Error = WSASend(Socket, Buffers, 1, &BytesSent, 0, &Overlapped, SendCompFunc);
if (Error == SOCKET_ERROR) {
Errno = WSAGetLastError();
if (Errno == WSAEWOULDBLOCK)
// WSAEWOULDBLOCK意味著必須等待FD_WRITE事件才能發送
ReturnValue = 0;
else if (Errno == WSA_IO_PENDING) // 重疊操作成功初始化
ReturnValue = 1;
else // 其他錯誤
ReturnValue = -1;
}
// 如果沒有錯誤,意味著發送操作立即完成。
return(ReturnValue);
} // DoOverlappedCallbackSend()
/*
* 完成常式在已成功初始化的重疊操作完成時被調用。它用來統計發送的位元組數。
* 參數:
* Error -- 提供重疊操作的完成狀態。
* BytesTransferred -- 提供實際發送的位元組數。
* Overlapped -- 提供一個指向WSAOVERLAPPED結構的指標,其hEvent域可以用來
* 給完成常式傳遞上下文資訊。
* Flags -- 未用。
*/
void CALLBACK
SendCompFunc(
IN DWORD Error,
IN DWORD BytesTransferred,
IN LPWSAOVERLAPPED Overlapped,
IN DWORD Flags)
{
TotalByteSent = BytesTransferred; // 統計發送位元組數。
if (Error)
MessageBox(NULL, "Error during overlapped send.", "Error", MB_OK | MB_SETFOREGROUND);
} // SendCompFunc()
void //顯示接收資料副程式。
DisplayInfo(HWND hWnd, WSABUF * lpBuffer)
{
HDC dc;
int l;
char line[128];
static int row = 0;
lpBuffer->buf[lpBuffer->len] = 0;
if (dc = GetDC(hWnd)) {
l = wsprintf((LPSTR) line, "%s",lpBuffer->buf);
TextOut(dc, 10, 16*row, (LPSTR) line, lpBuffer->len);
ReleaseDC(hWnd, dc);
}
row ;
}
BOOL
InitWindow(
IN HINSTANCE InstanceHandle,
IN HINSTANCE PrevInstanceHandle)
{
WNDCLASS WndClass; // window class structure
if (!PrevInstanceHandle) {
WndClass.style = CS_HREDRAW | CS_VREDRAW;
WndClass.lpfnWndProc = MainWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hInstance = InstanceHandle;
WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE 1);
WndClass.lpszMenuName = NULL;
WndClass.lpszClassName = EchoClassStr;
if (!RegisterClass(&WndClass))
return(FALSE);
}
return(TRUE);
}
8.1.2 伺服器程式
伺服器程式原始檔案名為ws2echos.c,其內容在下面列出。為了方便讀者,在程式中關鍵部分採用中文注釋的形式給出說明,讀者可照這些注釋加強對第七章內容的理解。
#ifndef _WINSOCKAPI_
#define _WINSOCKAPI_ /* Prevent inclusion of winsock.h in windows.h */
#endif
#include
#include
#include
#include
#include "ws2echo.h"
SOCKET Sockets; // 伺服器套接字
HWND GlobalhWindow;
char EchoClassStr[] = "WS2EchoServer";
WSABUF RecvBuf; // 接收緩衝區結構
char rbuf[BUFFER_LENGTH]; // 接收緩衝區
int WINAPI
WinMain(
IN HINSTANCE InstanceHandle,
IN HINSTANCE PrevInstanceHandle,
IN LPSTR CmdLine,
IN int CmdShow)
{
MSG Message;
if (!InitWindow(InstanceHandle, PrevInstanceHandle))
return(0);
GlobalhWindow = CreateWindow(EchoClassStr, "WinSock 2 Echo Server",
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, InstanceHandle, NULL);
if (GlobalhWindow == NULL)
return(0);
if (!InitWS2()) // 啟動WinSock 2.
return(0);
if (!DoListen()) // 建立監聽套接字
return(0);
ShowWindow(GlobalhWindow, CmdShow);
UpdateWindow(GlobalhWindow);
while (GetMessage(&Message, NULL, 0, 0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
WSACleanup(); // 收到WM_QUIT消息後,結束WinSock 2.
return(Message.wParam);
} // WinMain()
long CALLBACK
MainWndProc(
IN HWND WindowHandle,
IN UINT Message,
IN WPARAM WParam,
IN LPARAM LParam)
{
static SOCKET Socket; // 數據套接字
int retcode;
char MsgText[128];
switch (Message) {
case UM_SOCK:
switch (LParam) {
case FD_ACCEPT: // 處理外來連接請求
Socket = HandleAcceptMessage(LParam);
break;
case FD_READ: //資料讀準備好,接收網路資料。
retcode = DoRecv(Socket, &RecvBuf);
if (retcode == 1)
retcode = DoOverlappedEventSend(Socket, &RecvBuf);
case FD_WRITE: //寫準備好,發送資料。
DoOverlappedEventSend(Socket, &RecvBuf);
break;
case FD_CLOSE: //連接關閉。
closesocket(Socket);
break;
default:
if (WSAGETSELECTERROR(LParam) != 0) {
sprintf(MsgText, "SERVER: Error Code = %d",
WSAGETSELECTERROR(LParam));
MessageBox(GlobalhWindow, MsgText, "Error",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
}
break;
}
break;
case WM_DESTROY: // 關閉監聽套接字並退出應用程式
closesocket(Sockets);
PostQuitMessage(0);
break;
default:
return (DefWindowProc(WindowHandle, Message, WParam, LParam));
} // switch(Message)
return (0);
} // MainWndProc()
/*
* 啟動WinSock 2,協商WinSock版本號。成功返回TRUE,失敗返回FALSE。
*/
BOOL
InitWS2(void)
{
int Error; // catches return value of WSAStartup
WORD VersionRequested; // passed to WSAStartup
WSADATA WsaData; // receives data from WSAStartup
BOOL ReturnValue = TRUE; // return value
VersionRequested = MAKEWORD(VERSION_MAJOR, VERSION_MINOR);
Error = WSAStartup(VersionRequested, &WsaData); // 啟動WinSock 2
if (Error) {
MessageBox(GlobalhWindow, "Could not find high enough version of WinSock",
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
ReturnValue = FALSE;
} else { // 確認WinSock 2 DLL支援我們需要的精確版本號,否則,調用WSACleanup()。
if (LOBYTE(WsaData.wVersion) != VERSION_MAJOR
|| HIBYTE(WsaData.wVersion) != VERSION_MINOR) {
MessageBox(GlobalhWindow, "Could not find the correct version of WinSock",
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
WSACleanup();
ReturnValue = FALSE;
}
}
RecvBuf.len = 0; // 初始化接收緩衝區
RecvBuf.buf = rbuf;
return(ReturnValue);
} // InitWS2()
/*
* 創建伺服器套接字,並使之監聽外來連接。成功返回TRUE,失敗返回FALSE。
*/
BOOL
DoListen(void)
{
struct sockaddr_in SockAddr; // 本地套接字地址
int Error;
char textBuf[MAX_ERROR_TEXT];
// 創建流式重疊套接字
Sockets = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (Sockets == INVALID_SOCKET) {
sprintf(textBuf, "Could not open a socket. [%x]", WSAGetLastError());
MessageBox(GlobalhWindow, textBuf, "Non-fatal error.", MB_OK | MB_SETFOREGROUND);
return(FALSE);
}
memset(&SockAddr, 0, sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = 0;
SockAddr.sin_port = htons (UserPort);
Error = bind (Sockets, (LPSOCKADDR) &SockAddr, sizeof (SockAddr)); //建立本地連接。
if (Error == SOCKET_ERROR) {
sprintf(textBuf, "Could not bind the socket. [%x]", WSAGetLastError());
MessageBox(GlobalhWindow, textBuf, "Non-fatal error", MB_OK | MB_SETFOREGROUND);
closesocket(Sockets);
return(FALSE);
}
Error = WSAAsyncSelect(Sockets, GlobalhWindow, UM_SOCK, FD_ACCEPT); // 設置非同步通知事件
if (Error == SOCKET_ERROR) {
MessageBox(GlobalhWindow, "Error: WSAAsyncSelect()", "Non-fatal error",
MB_OK | MB_SETFOREGROUND);
closesocket(Sockets);
return(FALSE);
}
Error = listen(Sockets, SOMAXCONN); //將套接字變為被動套接字,等待接收連接。
if (Error == SOCKET_ERROR) {
MessageBox(GlobalhWindow, "Error: listen()", "Non-fatal error",
MB_OK | MB_SETFOREGROUND);
closesocket(Sockets);
return(FALSE);
}
return(TRUE);
} // ListenAll()
/*
* 處理FD_ACCEPT網路消息,建立與用戶端的連接。返回接收的資料套接字。
*/
SOCKET
HandleAcceptMessage(IN LPARAM LParam)
{
SOCKET Socket;
struct sockaddr address;
int address_len;
int Error;
char MsgText[MAX_ERROR_TEXT];
Error = WSAGETSELECTERROR(LParam); // 檢查是否在試圖連接時有錯誤發生
if (Error) {
if (Error == WSAENETDOWN) // 網路系統失敗
MessageBox(GlobalhWindow, "The network is down!", "Uh-oh",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
else
MessageBox(GlobalhWindow, "Unknown error on FD_ACCEPT", "Error",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
return(INVALID_SOCKET);
}
address_len = sizeof (address);
Socket = WSAAccept(Sockets, &address, &address_len, NULL, (DWORD)NULL);
if (Socket == INVALID_SOCKET) {
Error = WSAGetLastError();
if (Error == WSAECONNREFUSED) // AcceptCondFunc返回CF_REJECT...
MessageBox(GlobalhWindow, "The connection attempt has been refused.",
"Connection refused.", MB_OK | MB_SETFOREGROUND);
else { // An unexpected error code.
wsprintf(MsgText, "WSAAccept failed. Error code: %d", Error);
MessageBox(GlobalhWindow, MsgText, "Error",
MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
}
return(INVALID_SOCKET);
}
WSAAsyncSelect(Socket, GlobalhWindow, UM_SOCK, FD_READ | FD_WRITE | FD_CLOSE);
return(Socket);
} // HandleAcceptMessage()
/*
* 接收資料。
*/
int
DoRecv(SOCKET Socket, LPWSABUF lpRecvBuffer)
{
DWORD NumBytes; // stores how many bytes we received
int Error; // gets error values
int Result; // gets return value from WSARecv
DWORD Flags=0; // flags for WSARecv
int ReturnValue = 1; // returnValue
lpRecvBuffer->len = BUFFER_LENGTH - 1;
Result = WSARecv(Socket, lpRecvBuffer, 1, &NumBytes, &Flags, NULL, NULL); // 接收資料
if (Result == SOCKET_ERROR) {
Error = WSAGetLastError();
switch (Error) {
case WSAENETRESET:
case WSAECONNRESET:
ReturnValue = -1; // 連接被重置
break;
case WSAEWOULDBLOCK: // 沒有資料可供讀取
ReturnValue = 0;
break;
default: // 其他錯誤
ReturnValue = -2;
break;
}
}
lpRecvBuffer->len = NumBytes;
return(ReturnValue);
} // DoRecv()
/*
* 使用事件通知的重疊發送方式發送資料。
*/
int
DoOverlappedEventSend(SOCKET Socket, LPWSABUF Buffers)
{
int Size = 0; // how many bytes we send
int Error; // return value of WSASend
int Errno; // result of WSAGetLastError
DWORD dwFlags, BytesSent; // needed in WSASend
int ReturnValue = 1; // the return value
WSAOVERLAPPED Overlapped;
Overlapped.hEvent = (WSAEVENT)CreateEvent(NULL, FALSE, FALSE, NULL);
if (Overlapped.hEvent == NULL) {
MessageBox(GlobalhWindow, "CreateEvent() in DoOverlappedEventSend()",
"Error", MB_OK | MB_ICONSTOP | MB_SETFOREGROUND);
return(-1);
}
Error = WSASend(Socket, Buffers, 1, &BytesSent, 0, &Overlapped, NULL); // 發送資料
if (Error == SOCKET_ERROR) {
Errno = WSAGetLastError();
if (Errno == WSAEWOULDBLOCK) // 不能立即發送,需等待FD_WRITE事件。
ReturnValue = 0;
else if (Errno == WSA_IO_PENDING) { // 重疊發送被成功初始化
// 等待重疊操作結果
if (WSAGetOverlappedResult (Socket, &Overlapped, &BytesSent, TRUE, &dwFlags))
ReturnValue = 1;
else
ReturnValue = -1;
}
else //其他錯誤
ReturnValue = -1;
}
Buffers->len = BytesSent; // 發送位元組數
return(ReturnValue);
} // DoOverlappedEventSend()
BOOL
InitWindow(
IN HINSTANCE InstanceHandle,
IN HINSTANCE PrevInstanceHandle)
{
WNDCLASS WndClass;
if (!PrevInstanceHandle) {
WndClass.style = CS_HREDRAW | CS_VREDRAW;
WndClass.lpfnWndProc = MainWndProc;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hInstance = InstanceHandle;
WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
WndClass.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE 1);
WndClass.lpszMenuName = NULL;
WndClass.lpszClassName = EchoClassStr;
if (!RegisterClass(&WndClass))
return(FALSE);
}
return(TRUE);
}
8.1.3 頭文件
// version of Winsock we need
#define VERSION_MAJOR 2
#define VERSION_MINOR 0
#define UM_SOCK WM_USER 100
#define MAX_ERROR_TEXT 64
#define BUFFER_LENGTH 1024
#define ServerName "127.1.1.1"
#define UserPort 6666
8.2 多址廣播程式
Windows Sockets 2為支援IP multicast而定義了一組新的與協定無關的多點通訊應用程式介面,歸納起來可以用表8.1表示。
表8.1 WinSock 2的多點通訊API
WSAEnumProtocols() 檢測多址廣播支持
WSASocket() 指定多點通訊類型
WSAJoinLeaf() 加入一個多址廣播組並指定角色(發送者和/或接收者)
WSAIoctl() SIO_MULTICAST_SCOPE 設置IP生存時間
WSAIoctl() SIO_MULTIPOINT_LOOPBACK 禁止內部回送
為了適用不同的多點通訊模式,WinSock 2定義了資料平面(data plane)和控制平面(control plane)兩個概念,每一個平面都可以是“有根(rooted)”的或“無根(non-rooted)”的。關於這些概念的詳細解釋,參見附錄B。IP multicast是一種無根的資料平面和控制平面。在使用WSASocket()函數請求一個多點通訊套接字時需要指定這些角色。
函數WSAEnumProtocols()返回當前系統中安裝的協定的詳細描述,這些資訊存放在一個協定資訊結構(WSAPROTOCOL_INFO)的陣列中。在其中的域dwServiceFlags1中的一些標識指示此服務由IP/UDP協定提供,並且位元標識XP1_SUPPORT_MULTIPOINT指示該服務支援IP multicast。
另外,在上面的API中沒有提到如何離開一個多址廣播組。唯一與協定無關的API是關閉套接字,即使用標準的closesocket()函數,它可以用來離開多址廣播組。
下面給出一個簡單的例子,它示例了如何使用這些函數實現多址廣播通信。
#include /* Include this file first to prevent inclusion of winsock.h by windows.h */
#include
#include /* tcpip specific options */
#include
#include
#include
#define HEADER_ROWS 2 /* Number of console rows reserved for header */
#define INPUT_ROWS 2 /* Number of console rows reserved for input */
#define HELP_MSG "Type a message and press to send it, CTRL\\{Z | C} exits."
#define HELP_MSG_LEN (sizeof(HELP_MSG)/sizeof(CHAR)-1)
const COORD Zero = {0, 0}; /* Zero coordinates */
/* Context associated with each multicast session */
typedef struct _MCAST_CONTEXT MCAST_CONTEXT, *PMCAST_CONTEXT;
struct _MCAST_CONTEXT {
PMCAST_CONTEXT next; /* Next record in the list */
COORD coord; /* Current screen coordinates */
LPTSTR str; /* User address argument string */
LPTSTR ifstr; /* Interface address string (if specified) */
SOCKET s; /* Multicast socket */
struct in_addr addr; /* Multicast address to join in */
SHORT port; /* Port on which to join */
struct in_addr ifad; /* Interface to register multicast address */
struct sockaddr_in from; /* Peer address */
DWORD fromlen; /* Peer address length */
WSAOVERLAPPED ovlp; /* Overlapped (for asynchronous IO) */
char buf[2048]; /* Buffer to receive peer messages */
} ;
int CheckWinsockVersion(VOID);
int ParseAddress(LPTSTR argv, PMCAST_CONTEXT *ctx);
int JoinSession(PMCAST_CONTEXT ctx);
int PostReceiveRequest(PMCAST_CONTEXT ctx);
void CALLBACK RecvCompletion(IN DWORD dwError, IN DWORD cbTransferred,
IN LPWSAOVERLAPPED lpOverlapped, IN DWORD dwFlags);
DWORD WINAPI InputThread(LPVOID param);
BOOL CALLBACK CtrlHandler(DWORD dwCtrlType);
VOID WriteMiniConsoleA(IN OUT COORD *coord, LPSTR output, DWORD len);
VOID PaintScreen(PMCAST_CONTEXT ctx);
void Usage(char * command);
HANDLE HStdout, HStdin;
CONSOLE_SCREEN_BUFFER_INFO StdScreen; /* Screen Information used by this program */
HANDLE StopEvent; /* Event to be signal when we want to stop and exit */
HANDLE HInputThread; /* Handle of the input thread */
int McastTTL = 1; /* Time-To-Live of multicast datagram */
LONG NumPendingRcvs = 0; /* Number of pending receive requests */
BOOL ReuseAddress = TRUE;
int main(int argc, char *argv[])
{
int err=0; /* Error code */
PMCAST_CONTEXT ctx; /* Current context */
DWORD idThread; /* Input thread ID */
HStdout = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
HStdin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if ((HStdout==INVALID_HANDLE_VALUE) || (HStdin==INVALID_HANDLE_VALUE)
|| !GetConsoleScreenBufferInfo(HStdout, &StdScreen))
return -1;
SetConsoleCtrlHandler(NULL, TRUE);
SetConsoleMode(HStdout, ENABLE_PROCESSED_OUTPUT);
SetConsoleMode(HStdin, ENABLE_LINE_INPUT
| ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
if ((argc<=1) || (strcmp(argv[1], "-?")==0)) {
Usage(argv[0]);
return -1;
}
err = CheckWinsockVersion(); // 檢查WinSock版本號
if (err==0) {
StopEvent = WSACreateEvent();
if (StopEvent!=NULL) {
/* Win32 console input (and output) does not provide for asynchronous (overlapped) operations,
so we create a thread dedicated to handling user input */
HInputThread = CreateThread(NULL, 0, InputThread, &ctx, CREATE_SUSPENDED, &idThread);
if (HInputThread!=NULL) {
err = ParseAddress(argv[1], &ctx); /* 分析命令行參數,提取地址 */
if (err==0) {
err = JoinSession(ctx); /* 加入多址廣播會話 */
if (err==0) {
err = PostReceiveRequest(ctx); /* 在多址廣播套接字上發佈非同步接收請求 */
ResumeThread(HInputThread); /* 啟動輸入線程 */
SetConsoleCtrlHandler(NULL, FALSE); /* Allow normal interrupt processing */
SetConsoleCtrlHandler(CtrlHandler,TRUE);/*Intercept interrupts to cleanup properly */
/* Allow processing of received packets till stop event is signaled or wait error occurs */
while (TRUE) {
/* Wait alertably to let completion routines run */
DWORD status = WaitForSingleObjectEx(StopEvent, INFINITE, TRUE);
switch (status) {
case WAIT_IO_COMPLETION: /* Continue processing IO completion */
continue;
case WAIT_OBJECT_0:
default: /* Break out in case of stop signal or error */
break;
}
break;
}
}
closesocket(ctx->s); // 關閉套接字以強制結束所有未完成的I/O請求 */
while (NumPendingRcvs>0) /* Keep this thread around until all posted requests complete */
SleepEx (INFINITE, TRUE); /* Sleep alertably to let completion routines run */
free(ctx);/* Dispose of remaining contexts */
}
CloseHandle(HInputThread);/* Close input thread handle */
}
else {
printf("Could not create input thread.\n");
err = GetLastError();
}
WSACloseEvent(StopEvent); /* Close stop event */
}
else {
printf("Could not create stop event.\n");
err = GetLastError();
}
WSACleanup(); /* 卸載WinSock DLL */
}
CloseHandle(HStdout); /* Close input handles */
if (HStdin != INVALID_HANDLE_VALUE)
CloseHandle(HStdin);
if (err==ERROR_INVALID_PARAMETER)
Usage(argv[0]); /* Print usage message if we suspect that user passed in invalid parameter */
return err;
}
void Usage(char *command) {
printf(TEXT("Multi-Chat - join in and chat on multiple multicast addresses.\n")
TEXT("Usage:\n")
TEXT(" %s mc_addr[:port][,if_addr]\n")
TEXT("Where:\n")
TEXT(" mc_addr - multicast address to join,\n")
TEXT(" port - port on which to join,\n")
TEXT(" if_addr - local interface to join on,\n")
TEXT("Example:\n")
TEXT(" %s 224.0.0.41:1002,166.111.4.82\n"),
command, command);
}
/*
* 檢查系統中是否安裝了合適版本的WinSock DLL。
*/
int CheckWinsockVersion(VOID) {
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 0); /* 非同步I/O和多址廣播只有在WinSock 2.0以上版本才支持 */
err = WSAStartup(wVersionRequested, &wsaData);
if (err==0) {
/* 確認WinSock DLL支持2.0。注意,即使WinSock DLL支持的版本高於2.0,
因為我們要求2.0,它也會在wVersion中返回2.0。 */
if ((LOBYTE(wsaData.wVersion)==2) && (HIBYTE(wsaData.wVersion)==0))
return 0; /* WinSock DLL可接受,成功返回 */
WSACleanup();
err = WSAVERNOTSUPPORTED; /* 不支持,失敗返回 */
}
/* Tell the user that we couldn't find a usable WinSock DLL.*/
printf("WinSock DLL does not support requested API version.\n");
return err;
}
/*
* 分析命令行參數,提取多址廣播和介面位址,並為多址廣播會話上下文資訊分配空間及初始化。
* 參數: argv為命令行參數,ctx為保存多址廣播上下文內容的變數。
* 返回值:
* 0——成功返回,即提取了地址並分配到空間;
* ERROR_INVALID_PARAMETER或WSAEINVAL——位址不能提取到合法位址
* ERROR_OUT_OF_MEMORY——沒有足夠的資源來分配多址廣播會話上下文資訊所需要的空間
*/
int ParseAddress(LPTSTR argv, PMCAST_CONTEXT *ctx) {
struct sockaddr_in addr; /* Address structure returned by WinSock DLL */
int len; /* Address length returned by the WinSock DLL */
LPTSTR tmpstr, pszAddr; /* Pointer to the address to parse */
int err = 0; /* Error code */
*ctx = (PMCAST_CONTEXT)malloc(sizeof(MCAST_CONTEXT));
if (*ctx==NULL) {
printf("Not enough memory to allocate session context.\n");
return ERROR_NOT_ENOUGH_MEMORY;
}
(*ctx)->str = argv;
pszAddr = strtok(argv, ","); /* Separate out multicast address and local interface */
/* Call WinSock2 DLL to parse the address */
len = sizeof(addr);
err = WSAStringToAddress(pszAddr, AF_INET, NULL, (LPSOCKADDR)&addr, &len);
if (err==0) {
(*ctx)->addr = addr.sin_addr; // 多址廣播地址
/* Get the port if it is provided */
pszAddr = strtok(NULL, ",");
tmpstr = strtok(argv, ":");
tmpstr = strtok(NULL, ":");
if (tmpstr != NULL)
(*ctx)->port = htons(atoi(tmpstr)); // 埠號
else
(*ctx)->port = 0; // 默認為0
/* Get the interface address if it is provided*/
if (pszAddr!=NULL) {
(*ctx)->ifstr = pszAddr; /* Save the pointer to interface address text*/
len = sizeof(addr);
err = WSAStringToAddress(pszAddr, AF_INET, NULL, (LPSOCKADDR)&addr, &len);
if (err==0)
(*ctx)->ifad = addr.sin_addr; // 介面位址
else {
err = WSAGetLastError();
printf("Could not parse interface address: %s", pszAddr);
}
}
else { /* 沒有指定介面位址,使用默認位址 */
(*ctx)->ifad.S_un.S_addr = INADDR_ANY;
(*ctx)->ifstr = pszAddr;
}
}
else {
err = WSAGetLastError();
printf("Could not parse multicast address: %s\n", pszAddr);
}
return err;
}
/*
* 加入多址廣播會話
* 參數:ctx為多址廣播會話上下文
*/
int JoinSession(PMCAST_CONTEXT ctx) {
int err = 0; /* Error code */
struct sockaddr_in addr; /* Address structure for WinSock DLL calls */
int len; /* Address length for WinSock DLL call