注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:16 线程同步和异步套接字编程  

2013-02-24 10:38:16|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:16 线程同步和异步套接字编程 - jasonyang9 - jasonyang9的博客
========================
线程同步和异步套接字编程
========================

关于事件对象
* 事件对象(和互斥对象一样)也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值
* 有两种不同类型的事件对象,一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程都变为可调度线程;当一个自动重置的事件得到通知时,等待该事件的线程中只有一个变为可调度线程(同时,操作系统将此事件对象自动重置为无信号状态)

新建Win32 Console Application, Event。

#include <windows.h>
#include <iostream.h>

DWORD WINAPI fun1Proc( // 声明线程入口函数fun1Proc
LPVOID lpParameter // thread data
);

DWORD WINAPI fun2Proc( // 声明线程入口函数fun2Proc
LPVOID lpParameter // thread data
);

int tickets = 100; // 剩余的火车票数量
HANDLE g_hEvent; // 定义全局句柄变量,保存事件对象句柄

void main()
{
HANDLE hThread1, hThread2; // 定义线程的句柄
hThread1 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun1Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
hThread2 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun2Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
CloseHandle(hThread1); // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
CloseHandle(hThread2);

g_hEvent = CreateEvent(NULL, // 创建事件对象,第一个参数是安全属性
TRUE, // 人工重置的事件对象
FALSE, // 初始为无信号状态
NULL); // 匿名
SetEvent(g_hEvent); // 将事件对象设置为有信号状态(由于创建该对象时初始为无信号状态,所以必须手动设置)

Sleep(4000); // 主线程放弃执行4秒

CloseHandle(g_hEvent); // 关闭句柄
}

DWORD WINAPI fun1Proc( // fun1Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // WaitForSingleObject等待某个对象(参数1)的状态变为有信号或超时(参数2)才返回,否则等待(放弃执行权),INFINITE表示永远等待,直到对象变成有信号状态
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
cout << "thread1 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
}

return 0;
}

DWORD WINAPI fun2Proc( // fun2Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // 同fun1Proc
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 同fun1Proc
cout << "thread2 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
}

return 0;
}


编译后运行,输出:

thread1 sell ticket : 100
thread2 sell ticket : 99
thread1 sell ticket : 98
thread2 sell ticket : 97
...
thread1 sell ticket : 2
thread2 sell ticket : 1
thread1 sell ticket : 0 <--- 注意!这里出现了0


分析原因,还是由于在tickets等于1时,线程1在执行if (tickets > 0)后它的时间片结束了,切换到线程2执行(注意:此时tickets还没有递减为0),线程2打印出"thread2 sell ticket : 1"后,tickets被递减为0,于是线程2正常退出,线程1再次获得执行的机会,但此时tickets已经为0,且继续执行的语句是cout<<"thread1 sell ticket : " << tickets-- << endl;,于是就会打印出"thread1 sell ticket : 0"这个违反逻辑的错误内容。也就是说,线程间的同步失败了。
如果将代码改为:

#include <windows.h>
#include <iostream.h>

DWORD WINAPI fun1Proc( // 声明线程入口函数fun1Proc
LPVOID lpParameter // thread data
);

DWORD WINAPI fun2Proc( // 声明线程入口函数fun2Proc
LPVOID lpParameter // thread data
);

int tickets = 100; // 剩余的火车票数量
HANDLE g_hEvent; // 定义全局句柄变量,保存事件对象句柄

void main()
{
HANDLE hThread1, hThread2; // 定义线程的句柄
hThread1 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun1Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
hThread2 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun2Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
CloseHandle(hThread1); // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
CloseHandle(hThread2);

g_hEvent = CreateEvent(NULL, // 创建事件对象,第一个参数是安全属性
TRUE, // 人工重置的事件对象
FALSE, // 初始为无信号状态
NULL); // 匿名
SetEvent(g_hEvent); // 将事件对象设置为有信号状态(由于创建该对象时初始为无信号状态,所以必须手动设置)

Sleep(4000); // 主线程放弃执行4秒

CloseHandle(g_hEvent); // 关闭句柄
}

DWORD WINAPI fun1Proc( // fun1Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // WaitForSingleObject等待某个对象(参数1)的状态变为有信号或超时(参数2)才返回,否则等待(放弃执行权),INFINITE表示永远等待,直到对象变成有信号状态

ResetEvent(g_hEvent); // 执行关键代码前,用ResetEvent将事件对象设为无信号状态

if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
cout << "thread1 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程

SetEvent(g_hEvent); // 处理完关键代码后,用SetEvent将事件对象设为有信号状态
}

return 0;
}

DWORD WINAPI fun2Proc( // fun2Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // 同fun1Proc

ResetEvent(g_hEvent); // 执行关键代码前,用ResetEvent将事件对象设为无信号状态

if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 同fun1Proc
cout << "thread2 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程

SetEvent(g_hEvent); // 处理完关键代码后,用SetEvent将事件对象设为有信号状态

}

return 0;
}

还是会输出thread1 sell ticket : 0,问题在于单核环境下,多线程是轮流执行的,线程1执行到WaitForSingleObject(g_hEvent, INFINITE)后,时间片结束了,那么ResetEvent就没有机会执行,事件对象仍然是有信号状态,于是线程2就能得到事件,并开始执行。
多核环境下,线程是同时在各核心中运行的,那么ResetEvent也不会起作用,因为当线程1想调用ResetEvent时,线程2已经通过WaitForSingleObject得到了事件对象。多个线程同时访问一个资源的结果是不可预料的。

对于线程同步,只能使用自动重置的事件对象。

#include <windows.h>
#include <iostream.h>

DWORD WINAPI fun1Proc( // 声明线程入口函数fun1Proc
LPVOID lpParameter // thread data
);

DWORD WINAPI fun2Proc( // 声明线程入口函数fun2Proc
LPVOID lpParameter // thread data
);

int tickets = 100; // 剩余的火车票数量
HANDLE g_hEvent; // 定义全局句柄变量,保存事件对象句柄

void main()
{
HANDLE hThread1, hThread2; // 定义线程的句柄
hThread1 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun1Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
hThread2 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun2Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
CloseHandle(hThread1); // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
CloseHandle(hThread2);

g_hEvent = CreateEvent(NULL, // 创建事件对象,第一个参数是安全属性
FALSE, // 自动重置的事件对象
FALSE, // 初始为无信号状态
NULL); // 匿名
SetEvent(g_hEvent); // 将事件对象设置为有信号状态(由于创建该对象时初始为无信号状态,所以必须手动设置)

Sleep(4000); // 主线程放弃执行4秒

CloseHandle(g_hEvent); // 关闭句柄
}

DWORD WINAPI fun1Proc( // fun1Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // WaitForSingleObject等待某个对象(参数1)的状态变为有信号或超时(参数2)才返回,否则等待(放弃执行权),INFINITE表示永远等待,直到对象变成有信号状态
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
cout << "thread1 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
SetEvent(g_hEvent); // 必须调用SetEvent将自动事件对象设置回有信号状态,否则线程1执行一次循环后事件对象就永远处于无信号状态了
}

return 0;
}

DWORD WINAPI fun2Proc( // fun2Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
WaitForSingleObject(g_hEvent, INFINITE); // 同fun1Proc
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 同fun1Proc
cout << "thread2 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
SetEvent(g_hEvent); // 同fun1Proc
}

return 0;
}


编译运行,没有问题。

thread1 sell ticket : 100
thread2 sell ticket : 99
thread1 sell ticket : 98
thread2 sell ticket : 97
...
thread1 sell ticket : 2
thread2 sell ticket : 1


用事件对象限制只允许一个程序进程。

#include <windows.h>
#include <iostream.h>

HANDLE g_hEvent;

void main()
{
g_hEvent = CreateEvent(NULL, FALSE, FALSE, "tickets"); // 创建一个命名事件对象
if (g_hEvent) // 如果创建成功(或返回了已有的对象句柄)
{
if (ERROR_ALREADY_EXIST == GetLastError()) // 检查GetLastError返回值是不是ERROR_ALREADY_EXIST
{
cout << "only one instance can run!" << endl;
return;
}
}

CloseHandle(g_hEvent);
}

关于关键代码段
* 关键代码段(临界区)工作在用户模式下
* 关键代码段(临界区)是指一个小代码段,在代码能够执行前,必须独占对某些资源的访问权

新建工程Win32 Console Application, Critical。

#include <windows.h>
#include <iostream.h>

DWORD WINAPI fun1Proc( // 声明线程入口函数fun1Proc
LPVOID lpParameter // thread data
);

DWORD WINAPI fun2Proc( // 声明线程入口函数fun2Proc
LPVOID lpParameter // thread data
);

int tickets = 100; // 剩余的火车票数量
CRITICAL_SECTION g_cs; // 全局的临界区对象

void main()
{
HANDLE hThread1, hThread2; // 定义线程的句柄
hThread1 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun1Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
hThread2 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun2Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
CloseHandle(hThread1); // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
CloseHandle(hThread2);

InitializeCriticalSection(&g_cs); // 初始化临界区对象

Sleep(4000); // 主线程放弃执行4秒

DeleteCriticalSection(&g_cs); // 释放临界区对象(相关的所有资源)
}

DWORD WINAPI fun1Proc( // fun1Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
EnterCriticalSection(&g_cs); // 判断能否得到临界区对象的所有权(如果成功则继续,如果失败则阻塞)
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
cout << "thread1 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
LeaveCriticalSection(&g_cs); // 释放临界区对象的所有权
}
return 0;
}

DWORD WINAPI fun2Proc( // fun2Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
EnterCriticalSection(&g_cs); // 判断能否得到临界区对象的所有权(如果成功则继续,如果失败则阻塞)
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 同fun1Proc
cout << "thread2 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
LeaveCriticalSection(&g_cs); // 释放临界区对象的所有权
}
return 0;
}


编译运行,结果正常。
注意:如果其中某个线程在得到临界区的所有权后没有释放它,则另一个线程将得不到执行的机会。

关于线程死锁
* 哲学家进餐的问题
* 线程1拥有了临界区对象A,等待临界区对象B的所有权,线程2拥有了临界区对象B,等待临界区对象A的所有权,就造成了死锁

#include <windows.h>
#include <iostream.h>

DWORD WINAPI fun1Proc( // 声明线程入口函数fun1Proc
LPVOID lpParameter // thread data
);

DWORD WINAPI fun2Proc( // 声明线程入口函数fun2Proc
LPVOID lpParameter // thread data
);

int tickets = 100; // 剩余的火车票数量
CRITICAL_SECTION g_csA; // 全局的临界区对象A
CRITICAL_SECTION g_csB; // 全局的临界区对象B

void main()
{
HANDLE hThread1, hThread2; // 定义线程的句柄
hThread1 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun1Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
hThread2 = CreateThread(NULL, // 创建线程,第一个参数NULL表示使用缺省的安全属性
0, // 默认的堆栈大小(和调用线程的一样)
fun2Proc, // 线程入口函数的地址(即函数名称)
NULL, // 传递给线程的参数
0, // 创建的flags,设为0表示一旦创建立即运行
NULL); // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
CloseHandle(hThread1); // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
CloseHandle(hThread2);

InitializeCriticalSection(&g_csA); // 初始化临界区对象A
InitializeCirticalSection(&g_csB); // 初始化临界区对象B

Sleep(4000); // 主线程放弃执行4秒

DeleteCriticalSection(&g_csA); // 释放临界区对象A
DeleteCriticalSection(&g_csB); // 释放临界区对象B
}

DWORD WINAPI fun1Proc( // fun1Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
EnterCriticalSection(&g_csA); // 判断能否得到临界区对象A的所有权(如果成功则继续,如果失败则阻塞)
Sleep(1); // 放弃执行权1毫秒
EnterCirticalSection(&g_csB); // 判断能否得到临界区对象B的所有权
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
cout << "thread1 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
LeaveCriticalSection(&g_csB); // 释放临界区对象B的所有权(释放的顺序无关紧要)
LeaveCriticalSection(&g_csA); // 释放临界区对象A的所有权
}
return 0;
}

DWORD WINAPI fun2Proc( // fun2Proc函数的实现
LPVOID lpParameter // thread data
)
{
while (TRUE) // 循环
{
EnterCriticalSection(&g_csB); // 判断能否得到临界区对象B的所有权(如果成功则继续,如果失败则阻塞)
Sleep(1); // 同fun1Proc
EnterCriticalSection(&g_csA); // 判断能否得到临界区对象A的所有权
if (tickets > 0) // 判断如果还有剩余的火车票
{
Sleep(1); // 同fun1Proc
cout << "thread2 sell ticket : " << tickets-- << endl; // 输出,表示卖出了一张火车票(同时递减tickets)
}
else
break; // 卖完了就终止线程
LeaveCriticalSection(&g_csA); // 释放临界区对象A的所有权
LeaveCriticalSection(&g_csB); // 释放临界区对象B的所有权
}
return 0;
}


编译运行后,线程1和线程2都没能执行。原因在于:线程1执行到EnterCriticalSection(&g_csA);后放弃执行权1毫秒,转到线程2执行,执行到EnterCriticalSection(&g_csB);后放弃执行权1毫秒,回到线程1执行,执行到EnterCriticalSection(&g_csB);时由于临界区对象B的所有权刚被线程2获得,无法返回,发生阻塞。执行权再次来到线程2,线程2接着往下执行,执行到EnterCriticalSection(&g_csA);时,由于临界区对象A的所有权被线程1获得,也发生阻塞。如此,线程1和线程2都等待对方释放某个临界区对象的所有权,而又因为都处于阻塞状态,永远等不到,就发生了死锁。

互斥对象、事件对象和关键代码段的比较
* 互斥对象和事件对象属于内核对象,利用内核对象进行线程同步速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步(因此也可以实现只运行一个进程的功能)
* 关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

推荐书目:《Windows核心编程》机械工业出版社

基于消息的异步套接字
* Windows套接字在两种模式下执行I/O操作——阻塞和非阻塞。在阻塞模式下,I/O操作完成前,执行操作的Winsock函数(如recvfrom函数)会一直等待下去,不会立即返回程序(即将控制权交还给程序)。而在非阻塞模式下,Winsock函数会立即返回(后,利用消息通知主线程,操作的执行结果)。
* Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,对网络事件采用了基于消息的异步存取策略。
* Windows Sockets的异步选择函数WSAAsyncSelect()提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件以及和事件相关的一些信息。

相关函数说明
int WSAEnumProtocols(LPINT lpiProtocols, LPWSAPROTOCOL_INFO lpProtocolBuffer, ILPDWORD lpdwBufferLength);
* Win32平台支持多种不同的网络协议,采用Winsock2,就可以编写可直接使用任何一种协议的网络应用程序了。通过WSAEnumProtocols函数可以获得系统中安装的网络协议的相关信息。
* lpiProtocols,一个以NULL结尾的协议标识号数组。这个参数是可选的,如果lpiProtocols为NULL,则返回所有可用协议的信息,否则只返回数组中列出的协议信息。
* lpProtocolBuffer, [out],一个用WSAPROTOCOL_INFO结构体填充的缓冲区。WSAPROTOCOL_INFO结构体用来存放或得到一个指定协议的完整信息。
* lpdwBufferLength, [in, out],在输入时,指定传递给WSAEnumProtocols()函数的lpProtocolBuffer缓冲区的长度;在输出时,存有获取所有请求信息需传递给WSAEnumProtocols()函数的最小缓冲区长度。这个函数不能重复调用,传入的缓冲区必须足够大,以便能存放所有的元素,这个规定降低了该函数的复杂度,并且由于一个机器上装载的协议数目往往是很少的,所以并不会产生问题。

SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);
* 前三个参数和socket()函数的前三个参数含义相同。
* lpProtocolInfo,一个指向WSAPROTOCOL_INFO结构体的指针,该结构体定义了所创建的套接字的特性。如果lpProtocolInfo为NULL,则WinSock2 DLL使用前三个参数来决定使用哪一个服务提供者,它会选择能够支持规定的地址族、套接字类型和协议值的第一个传输提供者。如果lpProtocolInfo不为NULL,则套接字绑定到和指定的结构WSAPROTOCOL_INFO相关的提供者。
* g,保留的。
* dwFlags,套接字属性的描述。

int WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR *lpFrom, LPINT lpFormlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
* s,标识套接字的描述符。
* lpBuffers, [in, out],一个指向WSABUF结构体的指针。每个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
* dwBufferCount,lpBuffer数组中WSABUF结构体的数目。(和lpBuffers配合,定义多个接收数据的缓冲区,可以在某些情况下减少工作量,免去切分数据的麻烦)
* lpNumberOfBytesRecvd, [out],如果接收操作立即完成,则为一个指向本次调用所接收的字节数的指针。
* lpFlags, [in, out],一个指向标志位的指针。
* lpFrom, [out],可选指针,指向重叠操作完成后存放源地址的缓冲区。
* lpFromlen, [in, out],指向from缓冲区大小的指针,仅当指定了lpFrom才需要。
* lpOverlapped,一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)
* lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)(重叠套接字模式下的WSARecvFrom会在调用后立即返回,在操作完成后,操作系统调用完成例程(一个回调函数)通知程序)。

int WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR *lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
* s,标识一个套接字(可能已链接)的描述符。
* lpBuffers,一个指向WSABUF结构体的指针。每个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
* dwBufferCount,lpBuffers数组中WSABUF结构体的数目。
* lpNumberOfBytesSend, [out],如果发送操作立即完成,则为一个指向本次调用所发送的字节数的指针。
* dwFlags,指示影响操作行为的标志位。
* lpTo,可选指针,指向目标套接字的地址。
* iToLen,lpTo中地址的长度。
* lpOverlapped,一个指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)。
* lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

新建一个MFC AppWizard工程,基于对话框,Chat。
删除自带的控件,增加组框(“接收数据”),编辑框(IDC_EDIT_RECV),组框(“发送数据”),IP地址控件(IDC_IPADDRESS1),编辑框(IDC_EDIT_SEND),按钮(IDC_BUTTON_SEND,“发送”)。
在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)

// Stdafx.h
#include <winsock2.h> // 包含winsock2.h

// Chat.h
#define UM_SOCK WM_USER + 1 // 自定义消息

class CChatApp : public CWinApp
{
public:
...
~CChatApp(); // 增加析构函数声明
...
};

class CChatDlg :: public CDialog
{
public:
...
~CChatDlg(); // 增加析构函数声明
BOOL InitSocket(); // 增加初始化套接字函数声明
...
private:
SOCKET m_socket; // 在CChatDlg中增加私有变量m_socket
...
protected:
...
afx_msg void OnSock(WPARAM, LPARAM); // 消息响应函数(注意,需要把参数加上,用于传递事件的相关信息)
...
};

// Chat.cpp
CChatApp::~CChatApp()
{
WSACleanup(); // 终止对套接字库的使用
}

BOOL CChatApp::InitInstance()
{
WORD wVersionRequested; // 从MSDN中拷贝的代码
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(2, 2); // 这里用到2.2,是最高版本

err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
return FALSE;
}

if (LOBYTE(wsaData.wVersion) != 2 ||
HIBYTE(wsaData.wVersion) != 2)
{
WSACleanup();
return FALSE;
}

...
}

// ChatDlg.cpp
BEGIN_MESSAGE_MAP(CChatDlg, CDialog)
...
ON_MESSAGE(UM_SOCK, OnSock) // 消息映射
END_MESSAGE_MAP()

CChatDlg::CChatDlg(CWnd* pParent) : CDialog(CChatDlg::IDD, pParent)
{
...
m_socket = 0; // 初始化m_socket
}

CChatDlg::~CChatDlg()
{
if (m_socket) // 如果套接字有效
closesocket(m_socket); // 关闭套接字
}

BOOL CChatDlg::OnInitDialog()
{
...
InitSocket(); // 调用InitSocket初始化套接字
return TRUE;
}

BOOL CChatDlg::InitSocket()
{
m_socket = WSASocket(AF_INET, // 地址族
SOCK_DGRAM, // 数据报套接字
0, // 根据地址格式和套接字类别自动选择一个合适的协议
NULL, // 自动决定服务提供者
0, // 保留的
0); // flags
if (INVALID_SOCKET == m_socket)
{
MessageBox("创建套接字失败!");
return FALSE;
}

SOCKADDR_IN addrSock; // 地址结构变量
addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 任意IP地址
addrSock.sin_family = AF_INET; // 地址族
addrSock.sin_port = htons(6000); // 端口号

if (SOCKET_ERROR == bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR))) // 绑定,如果失败则提示并退出
{
MessageBox("绑定失败!");
return FALSE;
}

if (SOCKET_ERROR == WSAAsyncSelect(m_socket, // 请求Windows基于消息的网络事件通知,第一个参数是套接字
m_hWnd, // 第二个参数是接收消息的窗口句柄
UM_SOCK, // 第三个参数是消息(自定义的UM_SOCK)
FD_READ)) // 第四个参数是请求的消息(这里只关心读取事件)
{
MessageBox("注册网络读取事件失败!");
return FALSE;
}

return TRUE;
}

void CChatDlg::OnSock(WPARAM wParam, LPARAM lParam)
{
switch (LOWORD(lParam)) // lParam的低字节是网络事件
{
case FD_READ: // 如果是FD_READ(读取)事件
WSABUF wsabuf; // 定义WSABUF结构体变量
wsabuf.buf = new char[200]; // 分配200个字节
wsabuf.len = 200; // 指定长度为200
DWORD dwRead; // 接收实际读取数据的变量
DWORD dwFlag = 0; // 标记(初始化为0)
SOCKADDR_IN addrFrom; // 定义地址结构体变量,用于接收对方的地址信息
int len = sizeof(SOCKADDR); // 定义变量为地址结构体长度
CString str; // 存放对方地址信息的变量
CString strTemp; // 存放原接收

if (SOCKET_ERROR == WSARecvFrom(m_socket, // 调用WSARecvFrom接收数据,第一个参数是套接字
&wsabuf, // 指向wsabuf的指针(用于接收数据)
1, // 1个结构体
&dwRead, // 返回实际接收的数据
&dwFlag, // 标记的指针
(SOCKADDR*)&addrFrom, // 地址结构体的指针(用于接收对方的地址信息)
&len, // 地址的长度
NULL, // 一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)
NULL)) // 一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)
{
MessageBox("接收数据失败!"); // 通常不会失败,这里仅为代码结构考虑
return;
}

str.Format("%s说:%s", inet_ntoa(addrFrom.sin_addr), wsabuf, buf); // 格式化
str += "\r\n"; // 加上换行(注意:编辑框必须有Multiline属性)
GetDlgItemText(IDC_EDIT_RECV, strTemp); // 获取原接收编辑框的内容
str += strTemp; // 将新收到的内容加在原内容之前
SetDlgItemText(IDC_EDIT_RECV, str); // 设置接收编辑框内容
break;
default:
break;
}
}


将“发送”按钮设置为Default button,编写响应函数。

void CChatDlg::OnBtnSend()
{
DWORD dwIP; // 接收IP地址变量
CString strSend; // 发送的数据内容
WSABUF wsabuf; // WSABUF结构体
DWORD dwSend; // 用于接收实际发送的字节数
int len; // 保存strSend中字符串的长度

((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP); // 从控件获得IP地址

SOCKADDR_IN addrTo; // 地址结构体变量
addrTo.sin_addr.S_un.S_addr = htonl(dwIP); // 目的IP地址
addrTo.sin_family = AF_INET; // 地址族
addrTo.sin_port = htons(6000); // 端口号

GetDlgItemText(IDC_EDIT_SEND, strSend); // 获取要发送的内容

len = strSend.GetLength(); // 获得strSend中字符串的长度
wsabuf.buf = strSend.GetBuffer(len); // 将CString对象转换为char*返回,并赋值给wsabuf中的buf成员
wsabuf.len = len + 1; // 将strSend中字符串的长度+1赋值给wsabuf中的len成员

SetDlgItemText(IDC_EDIT_SEND, ""); // 清空发送编辑框

if (SOCKET_ERROR == WSASendTo(m_socket, // 调用WSASendTo发送数据,第一个参数是套接字
&wsabuf, // 指向WSABUF结构体的指针
1, // 1个WSABUF结构体
&dwSend, // 返回实际发送的字节数
0, // 标志位
(SOCKADDR*)&addrTo, // 目标套接字的地址
sizeof(SOCKADDR*), // 地址的长度
NULL, // 指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)
NULL)) // 指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)
{
MessageBox("发送数据失败!"); // 如果失败则提示
return;
}
}


采用异步套接字方式,即使没有多线程,也可以实现类似的效果,即主线程不会因为recvfrom等函数阻塞(不需要将recvfrom等函数放到单独的线程中执行)。

到目前位置,发送数据时采用的都是对方主机的IP地址来进行通讯,如果想利用更方便的主机名,可调用gethostbyname函数。
增加编辑框(IDC_EDIT_HOSTNAME),修改OnBtnSend。

void CChatDlg::OnBtnSend()
{
DWORD dwIP; // 接收IP地址变量
CString strSend; // 发送的数据内容
WSABUF wsabuf; // WSABUF结构体
DWORD dwSend; // 用于接收实际发送的字节数
int len; // 保存strSend中字符串的长度
CString strHostName; // 用于保存主机名
SOCKADDR_IN addrTo; // 地址结构体变量
HOSTENT* pHost; // HOSTENT结构体指针

if (GetDlgItemText(IDC_EDIT_HOSTNAME, strHostName), strHostName == "") // 获取IDC_EDIT_HOSTNAME中的内容并判断是否为空
{
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP); // 如果主机名控件中没有内容,则获取IP地址控件的IP地址
addrTo.sin_addr.S_un.S_addr = htonl(dwIP); // 赋值目的IP地址到地址结构体变量
}
else
{
pHost = gethostbyname(strHostName); // 用gethostbyname解析主机名为IP地址
addrTo.sin_addr.S_un.S_addr = *((DWORD*)pHost->h_addr_list[0]); // 将char*转换为u_long赋值给地址结构体变量(先取出pHost->h_addr_list[0]这个char*(以网络字节序表示的地址),转换为(DWORD*)类型(即u_long型指针),再用*号获取其内容(即u_long类型数据,占4个字节))(在写程序时,特别是进行类型转换时,头脑中要有相应的内存模型)(都是网络字节序)
}

addrTo.sin_family = AF_INET; // 地址族
addrTo.sin_port = htons(6000); // 端口号

GetDlgItemText(IDC_EDIT_SEND, strSend); // 获取要发送的内容

len = strSend.GetLength(); // 获得strSend中字符串的长度
wsabuf.buf = strSend.GetBuffer(len); // 将CString对象转换为char*返回,并赋值给wsabuf中的buf成员
wsabuf.len = len + 1; // 将strSend中字符串的长度+1赋值给wsabuf中的len成员

SetDlgItemText(IDC_EDIT_SEND, ""); // 清空发送编辑框

if (SOCKET_ERROR == WSASendTo(m_socket, // 调用WSASendTo发送数据,第一个参数是套接字
&wsabuf, // 指向WSABUF结构体的指针
1, // 1个WSABUF结构体
&dwSend, // 返回实际发送的字节数
0, // 标志位
(SOCKADDR*)&addrTo, // 目标套接字的地址
sizeof(SOCKADDR*), // 地址的长度
NULL, // 指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)
NULL)) // 指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)
{
MessageBox("发送数据失败!"); // 如果失败则提示
return;
}
}


为了将接收编辑框中的IP地址变为主机名,可利用gethostbyaddr函数。修改OnSock函数。

void CChatDlg::OnSock(WPARAM wParam, LPARAM lParam)
{
switch (LOWORD(lParam)) // lParam的低字节是网络事件
{
case FD_READ: // 如果是FD_READ(读取)事件
WSABUF wsabuf; // 定义WSABUF结构体变量
wsabuf.buf = new char[200]; // 分配200个字节
wsabuf.len = 200; // 指定长度为200
DWORD dwRead; // 接收实际读取数据的变量
DWORD dwFlag = 0; // 标记(初始化为0)
SOCKADDR_IN addrFrom; // 定义地址结构体变量,用于接收对方的地址信息
int len = sizeof(SOCKADDR); // 定义变量为地址结构体长度
CString str; // 存放对方地址信息的变量
CString strTemp; // 存放原接收
HOSTENT *pHost; // 定义HOSTENT结构体指针

if (SOCKET_ERROR == WSARecvFrom(m_socket, // 调用WSARecvFrom接收数据,第一个参数是套接字
&wsabuf, // 指向wsabuf的指针(用于接收数据)
1, // 1个结构体
&dwRead, // 返回实际接收的数据
&dwFlag, // 标记的指针
(SOCKADDR*)&addrFrom, // 地址结构体的指针(用于接收对方的地址信息)
&len, // 地址的长度
NULL, // 一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)
NULL)) // 一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)
{
MessageBox("接收数据失败!"); // 通常不会失败,这里仅为代码结构考虑
return;
}

pHost = gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr, // 将u_long转换为const char*(先取u_long变量地址,将u_long转换为u_long*,再将u_long*转换为char*)(都是网络字节序)
4, // 第二个参数必须是4(即地址的长度)
AF_INET); // 地址族

// str.Format("%s说:%s", inet_ntoa(addrFrom.sin_addr), wsabuf, buf); // 格式化
str.Format("%s说:%s", pHost->h_name, wsabuf, buf); // IP地址换为主机名

str += "\r\n"; // 加上换行(注意:编辑框必须有Multiline属性)
GetDlgItemText(IDC_EDIT_RECV, strTemp); // 获取原接收编辑框的内容
str += strTemp; // 将新收到的内容加在原内容之前
SetDlgItemText(IDC_EDIT_RECV, str); // 设置接收编辑框内容
break;
default:
break;
}
}

  评论这张
 
阅读(203)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017