现在的位置: 首页 > 综合 > 正文

线程同步与异步套接字编程

2014年02月14日 ⁄ 综合 ⁄ 共 11687字 ⁄ 字号 评论关闭

 

事件对象
时间对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是为通知状态的布尔值
有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件,当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
HANDLE CreateEvent(
     LPSECURITY_ATTRIBUTES lpEventAttributes,
                         // pointer to security attributes,指定安全属性
     BOOL bManualReset,     // flag for manual-reset event
//为TRUE表示手动,否则为自动,看上边文字说明
     BOOL bInitialState, // flag for initial state
//为TRUE表示有信号
     LPCTSTR lpName         // pointer to event-object name
);
事件对象代码如下(经本人修改,此代码与视频源码有一些差别):
#include "windows.h"
#include "iostream.h"

int ticket=100;
HANDLE g_hEvent;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);

void main()
{
HANDLE thread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
HANDLE thread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);

CloseHandle(thread1);
CloseHandle(thread2);

g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);//第三个参数为TRUE,这时创建时需要在有信号状态下WaitForSingleObject才能获得信号进行工作,也可以调用SetEvent()来指定对象为有信号状态。当人工重置事件被调度时,所有线程都变成可调度线程。对象,一般采用自动重置。第二个参数为FALSE。但自动重置对象为等待该事件的线程中只有一个线程变为可调度线程。WaitForSingleObject就变成非信号状态了,完成处理后需要将事件量置成有信号状态SetEvent()。
Sleep(4000);
CloseHandle(g_hEvent);//最后关闭事件对象句柄。

}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
WaitForSingleObject(g_hEvent,INFINITE);

while(TRUE)
{
     WaitForSingleObject(g_hEvent,INFINITE);
     if(tickets>0)
     {
      Sleep(1);
      cout<<"thread1 sell ticket : "<<tickets--<<endl;
     }
     else
      break;
     SetEvent(g_hEvent);
}

return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
WaitForSingleObject(g_hEvent,INFINITE);
while(TRUE)
{
     WaitForSingleObject(g_hEvent,INFINITE);
     if(tickets>0)
     {
      Sleep(1);
      cout<<"thread2 sell ticket : "<<tickets--<<endl;
     }
     else
      break;
     SetEvent(g_hEvent);
}

return 0;
}
人工重置的事件只有在调用ResetEvent()之后才变成无效状态,否则始终处于有信号状态。而自动重置信号在调用WaitForSingleObject之后就处于无信号状态了。

通过命名事件量来实现一个应用程序只有一个实例在运行。
只有当 g_hEvent=CreateEvent(NULL,FALSE,TRUE,"ticket");第四个参数有值时才能进行事件对象的判断:
if(g_hEvent)
{
     if(ERROR_ALREADY_EXISTS==GetLastError())
     {
      cout<<"Only one instance can runs!"<<endl;
      return;
     }
}

关键代码段
其他方式都工作在内核方式下中。
关键代码段(临界区)工作在用户方式下。
关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某资源的访问权。临界区有点像公用电话亭。
VOID InitializeCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection      // address of critical
                                            // section object
);//初始化一个临界区对象,也就是创建了临界区。
VOID DeleteCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection      // pointer to critical
                                            // section object
);//删除临界区

VOID EnterCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection      // pointer to critical
                                            // section object
);如果得到了临界区的访问权,就返回,否则,一直等待下去。
VOID LeaveCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection      // address of critical
                                            // section object
);//离开临界区。

原代码如下:
#include "windows.h"
#include "iostream.h"

int ticket=100;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);

CRITICAL_SECTION g_cs;//全局临界区对象
void main()
{
HANDLE thread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
HANDLE thread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);

CloseHandle(thread1);
CloseHandle(thread2);
InitializeCriticalSection(&g_cs);

Sleep(4000);

DeleteCriticalSection(&g_cs);//释放资源

}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(TRUE)
{
     EnterCriticalSection(&g_cs);//进入
     if(ticket>0)
     {
      cout<<"thread1 sells : "<<ticket--<<endl;
      Sleep(1);
     }
     else break;
     LeaveCriticalSection(&g_cs);//推出
}
return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(TRUE)
{
     EnterCriticalSection(&g_cs);
     if(ticket>0)
     {
      cout<<"thread2 sells : "<<ticket--<<endl;
      Sleep(1);
     }
     else break;
     LeaveCriticalSection(&g_cs);
}
return 0;
}

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

在MFC中进行线程同步,首选临界区代码段,在一个类的构造函数中调用InitCriticalSection(),在析构函数中调用DeleteCriticalSection(),在我们要访问的代码前面加上EnterCriticalSection,在离开时调用LeaveCriticalSection,这两个一定要成对使用。

《window内核编程》——机械工业出版社

基于消息的异步套接字
Windows套接字在两种模式下执行I/O操作,阻塞和非阻塞。在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回程序(将控制权交还给程序),所以在前面聊天程序中需要创建一个单独的线程接收消息。而在非阻塞模式下,Winsock函数无论如何都会立即返回。
Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。

Windows Sockets的异步选择函数WSAAsyncSelect()提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。自定义消息。

Win32平台支持多种不同的网络协议,采用Winsock2就可以编写可直接使用任何一种协议的网络应用程序了。通过WSAEnumProtocols可以获得系统中安装的网络协议的的相关信息。
int WSAEnumProtocols (
     LPINT lpiProtocols,         //若为NULL,返回所有可用协议信息,
//否则返回数组中所列协议信息。
     LPWSAPROTOCOL_INFO lpProtocolBuffer,     //WSAPROTOCOL_INFO用来存放或得              //到一个指定协议的完整信息
     ILPDWORD lpdwBufferLength        //指定传递给函数的缓冲区长度
);

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

调用WSAAsyncSelect请求一个windows的基于消息的网络事件通知
int WSAAsyncSelect (
     SOCKET s,           
     HWND hWnd,          
     unsigned int wMsg,  
     long lEvent         
);

接收数据:
int WSARecvFrom (
     SOCKET s,        //标识套接字的描述符
     LPWSABUF lpBuffers,      //指向WSABUF的指针,每一个包含WSABUF包含一个           //缓冲区的指针和缓冲区的长度
     DWORD dwBufferCount,     //lpBuffers数组中WSABUF结构体的长度
     LPDWORD lpNumberOfBytesRecvd,     //如果接收操作立即完成,则为指向本次调用接受            //字节数的指针
     LPDWORD lpFlags,
     struct sockaddr FAR * lpFrom,     //指向重叠操作完成后存放原地址的缓冲区
     LPINT lpFromlen,      //指向From缓冲区大小的指针,制定了 lpFrom才需要
     LPWSAOVERLAPPED lpOverlapped, //指向WSAOVERLAPPED
     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE  
//指向接收操作完成时调用的完成例程的指针
);

为了调用高版本的套接字,我们要调用WSAStartup来制定套接字版本:
在BOOL CChatApp::InitInstance()中添加代码如下:

WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD( 2, 2 );

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

if ( LOBYTE( wsaData.wVersion ) != 2 ||
           HIBYTE( wsaData.wVersion ) != 2 )
{
     WSACleanup( );
     return FALSE;
}
还需要在stdafx.h中添加头文件<winsock2.h>和链接库文件sc2_32.lib

在ChatApp类当中添加析构函数~CChatApp()中终止对套接字库的使用。
CChatApp::~CChatApp()
{
WSACleanup();
}

在CChatDlg类中增加成员变量SOCKET::m_socke并在构造函数中初始化。

在CChatDlg中增加析构函数,关闭套接字:
CChatDlg::~CChatDlg()
{
if(m_socket)//判有没有断其存不存在
closesocket(m_socket);
}

增加一个初始化库的成员函数:
BOOL CChatDlg::InitSocket();

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 lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );

s,标识套接字的描述符。
lpBuffers,[in, out],一个指向WSABUF结构体的指针。每一个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
dwBufferCount, lpBuffers数组中WSABUF结构体的数目。
lpNumberOfBytesRecvd,[out],如果接收操作立即完成,则为一个指向本次调用所接收的字节数的指针。
lpFlags,[in, out],一个指向标志位的指针。
lpFrom,[out],可选指针,指向重叠操作完成后存放源地址的缓冲区。
lpFromlen,[in, out],指向from缓冲区大小的指针,仅当指定了lpFrom才需要。
lpOverlapped,一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

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结构体的数目。
lpNumberOfBytesSent,[out],如果发送操作立即完成,则为一个指向本次调用所发送的字节数的指针。
dwFlags,指示影响操作行为的标志位。
lpTo,可选指针,指向目标套接字的地址。
iToLen,lpTo中地址的长度。
lpOverlapped,一个指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

接下来编写函数初始化套接字,步骤如下:
1。新建套接字
2。新建地址。
3。绑定
4。请求一个windows的基于消息的网络事件通知
5。在BOOL CChatDlg::OnInitDialog()中调用BOOL CChatDlg::InitSocket()
代码如下:
BOOL CChatDlg::InitSocket()
{
m_socket=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);
if(INVALID_SOCKET=m_socket)
{
     MessageBox("套接字创建失败");
     return FALSE;
}//Socket增强版
SOCKADDR_IN addrSock;
addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
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,m_hWnd,UM_SOCK,FD_READ))
{
     MessageBox("注册网络读取事件失败!");
     return FALSE;
}
}//该函数InitSocket()在InitInstance中被调用。

自定义消息响应函数步骤:
1。在ChatDlg.h中定义#define UM_SOCK     WM_USER+1
2。在
//{{AFX_MSG(CChatDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
中添加
afx_msg LRESULT OnSock(WPARAM wParam,LPARAM lParam);
//注意返回值类型
3。在
BEGIN_MESSAGE_MAP(CChatDlg, CDialog)
//{{AFX_MSG_MAP(CChatDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
中添加消息映射
ON_MESSAGE(UM_SOCK,OnSock)     //此处不加标点
4。实现消息响应函数
LRESULT CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)
{
switch(LOWORD(lParam))//在消息响应函数中判断消息的类型。LOWORD(lParam)表明网络事件HIWORD(lParam)标明错误代码。
{
case FD_READ:
     WSABUF wsabuf;
     wsabuf.buf=new char[200];
     wsabuf.len=200;
     DWORD dwRead;
     DWORD dwFlag=0;
     SOCKADDR_IN addrFrom;
     int len=sizeof(SOCKADDR);

     CString strTemp;
     CString str;
  
     if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag,
         (SOCKADDR*)&addrFrom,&len,NULL,NULL))
     {
      MessageBox("接收数据失败!");
      return FALSE;
     }
     str.Format("%s speak : %s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf);
     str+="\r\n";
     GetDlgItemText(IDC_EDIT_RECV,strTemp);
     str+=strTemp;
     SetDlgItemText(IDC_EDIT_RECV,str);

     break;
}
return 0;
}

接收按钮消息:
void CChatDlg::OnBtnSend()
{
DWORD dwIP;
CString strSend;
WSABUF wsabuf;
DWORD dwSend;
int len;
CString strHostName;
SOCKADDR_IN addrTo;
HOSTENT* pHost;
if(GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName),strHostName=="")
{
     ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
     addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
}
else
{
     pHost=gethostbyname(strHostName);
     addrTo.sin_addr.S_un.S_addr=*((DWORD*)pHost->h_addr_list[0]);
}

addrTo.sin_family=AF_INET;
addrTo.sin_port=htons(6000);

GetDlgItemText(IDC_EDIT_SEND,strSend);
len=strSend.GetLength();
wsabuf.buf=strSend.GetBuffer(len);//将cstring对象作为一个char*返回
wsabuf.len=len+1;

SetDlgItemText(IDC_EDIT_SEND,"");

if(SOCKET_ERROR==WSASendTo(m_socket,&wsabuf,1,&dwSend,0,
      (SOCKADDR*)&addrTo,sizeof(SOCKADDR),NULL,NULL))
{
     MessageBox("发送数据失败");
     return;
}

}

注意,此程序的接收端和发送端是在同一个线程下完成的,如果我们采用阻塞套接字会因为接收函数的调用而使主线程暂停运行。这样我们采用异步选择的机制完成了主线程的接收端和发送端

编写网络程序采用异步选择机制可以提高系能。

如果我们不想总是输入IP地址而是想输入主机名,可以调用函数
struct hostent* FAR gethostbyname(
     const char* name
);//将主机名转化为IP地址。
在对话框上新建一个文本框
代码:
HOSTENT * pHost;

GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName);
if(strHostName=="")
{
     ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS))->GetAddress(dwIP);
     addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
}
else
{
     pHost=gethostbyname(strHostName);
     addrTo.sin_addr.S_un.S_addr=*((DWORD*)pHost->h_addr_list[0]);
}
用DWORD取出的四个字节,正好是网络字节序表示的ULONG类型的地址。
这段代码在教学视频上在文本框中输入的是“sunxin”,我认为那是那台电脑的在网络上的主机名,而在我的电脑上只能输入“localhost”,或者我的主机名,否则程序异常中止。

如果要在接收数据框中显示主机名,在LRESULT CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)中添加如下代码:
HOSTENT *pHost; pHost=gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET);
str.Format("%s speak : %s",pHost->h_name,wsabuf.buf);
注意:指针之间可以任意转换,以上代码把一个ulong类型转换为char*,要先取地址再进行转换。

记住两个函数
The inet_ntoa function converts an (Ipv4) Internet network address into a string in Internet standard dotted format.

The inet_addr function converts a string containing an (Ipv4) Internet Protocol dotted address into a proper address for the IN_ADDR structure.

抱歉!评论已关闭.