(孙鑫 十六) 线程同步与异步套接字编程

1.事件对象
事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。&&

CreateEvent创建/打开一个事件对象
The CreateEvent function creates a named or unnamed event object. 

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
                      // pointer to security attributes
  BOOL bManualReset,  // flag for manual-reset event
  BOOL bInitialState, // flag for initial state,TRUE有信号
  LPCTSTR lpName      // pointer to event-object name
);

The ResetEvent function sets the state of the specified event object to nonsignaled. 
//设置成无信号状态
BOOL ResetEvent(
  HANDLE hEvent   // handle to event object
);If the function fails, the return value is zero. To get extended error information, call GetLastError. 


2.新建win32 console application,Event

复制先前的代码:
#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
  LPVOID lpParameter   // thread data
);
DWORD WINAPI Fun2Proc(
  LPVOID lpParameter   // thread data
);

int tickets=100;

void main()
{
	HANDLE hThread1;
	HANDLE hThread2;
	hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
	hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
	CloseHandle(hThread1); //关闭线程,说明main中对此线程无兴趣。
	CloseHandle(hThread2);
	Sleep(4000);
}

DWORD WINAPI Fun1Proc(
  LPVOID lpParameter   // thread data
)
{
	return 0;
}

DWORD WINAPI Fun2Proc(
  LPVOID lpParameter   // thread data
)
{
	return 0;
}

然后在main前定义一个全局变量HANDLE g_hEvent;在两个线程函数中添加:
	while(TRUE)
	{
		WaitForSingleObject(g_hEvent,INFINITE);
		if(tickets>0)
		{
			Sleep(3);
			cout<<"thread1 sells:"<<tickets--<<endl;
		}
		else
			break;
	}
	return 0;
在主函数中创建事件对象:	
g_hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);  //这时为有信号状态,所以线程可以等到(若三个参数为FALSE则等不到了)

The SetEvent function sets the state of the specified event object to signaled. 

BOOL SetEvent(
  HANDLE hEvent   // handle to event object
);
 	//设置事件对象成有信号状态

如:SetEvent(g_hEvent);

但现在设置的是人工重置,两个线程可同时运行,所以会sell tickets 0
人工重置的事件对象,在WaitForSingleObject后仍是有信号状态,所以其他线程还可运行。
但这时若在其后调用ResetEvent和SetEvent结果也一样……
我们应该用自动重置的事件对象:
g_hEvent=CreateEvent(NULL,FALSE,TRUE,NULL);
这时在WaitForSingleObject之后,系统自动将它重置为无信号状态。
所以在线程中执行完后要设置成有信号:SetEvent(g_hEvent);

最后还要在主函数中关闭事件对象:CloseHandle(g_hEvent);
又来实现一个实例运行:
	g_hEvent=CreateEvent(NULL,FALSE,FALSE,"MyEvent");
	if(g_hEvent)	//返回先前的事件句柄(如果已经有了话)
	{
		if(ERROR_ALREADY_EXISTS==GetLastError())
		{
			cout<<"only one instance can run!"<<endl;
			return;
		}
	}

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

The InitializeCriticalSection function initializes a critical section object. 

VOID InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection   // address of critical 
                                         // section object
); //创建关键区
//就好像创建了一个公用电话亭

The EnterCriticalSection function waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership. 
//等待 线程被给予拥有权
VOID EnterCriticalSection(	//进入关键区
  LPCRITICAL_SECTION lpCriticalSection   // pointer to critical 
                                         // section object
);  

The LeaveCriticalSection function releases ownership of the specified critical section object. 

VOID LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection   // address of critical 
                                         // section object
);	//离开公用电话亭

The DeleteCriticalSection function releases all resources used by an unowned critical section object. 

VOID DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection   // pointer to critical 
                                         // section object
);	//删除

新建一个win32 console application;Critical,复制一下刚才的代码,删除和事件相关的代码。
在main前定义一个全局变量:CRITICAL_SECTION g_cs;
在main中初始化和释放:
	InitializeCriticalSection(&g_cs);
	Sleep(4000);
	DeleteCriticalSection(&g_cs);

//这样在虚拟机里的XP能运行良好,在笔记本的win7上有些问题(应该是因为笔记本是双核,在同学XP上也有问题)


4.线程死锁

哲学家进餐问题,没人一支筷子,一个交出筷子,大家就可以吃了;但害怕大家吃完了不归还筷子,所以没人交出筷子,都在等别人,所以都没得吃了。

线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,就造成了死锁。

在程序中将第一个CRITICAL_SECTION该为g_csA,再新加一个g_csB,各处代码都复制一下。
第一个线程先enter g_csB再g_csA,第二个线程反过来,再在两个Enter之间加一个Sleep(3);,则会死锁,这时两个线程都在等多方交出……


	互斥对象、事件对象与关键代码段的比较&&&

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

  要深入学习多线程编程可参数《windows核心编程》

5.基于消息的异步套接字

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

The Windows Sockets WSAAsyncSelect function requests Windows message-based notification of network events for a socket.

int WSAAsyncSelect (	//此函数自动将socket设为非阻塞模式
  SOCKET s,           
  HWND hWnd,       //消息发给此窗口   
  unsigned int wMsg,  
  long lEvent         
);
  //请求 基于消息的网络事件通知


lEvent 
[in] A bitmask that specifies a combination of network events in which the application is interested.
如: FD_READ/FD_WRITE/OOB/ACCEPT……


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 ()函数的最小缓冲区长度。这个函数不能重复调用,传入的缓冲区必须足够大以便能存放所有的元素。这个规定降低了该函数的复杂度,并且由于一个机器上装载的协议数目往往是很少的,所以并不会产生问题。&&

  新建一个 MFC Chat dialog based,删除原先部件。
添加组框,“接收数据”,在其中放一个编辑框接收数据,再一个组框发送数据,还添加一个IP框,编辑框。在添加一个发送按钮。

在CAPP的InitInstance中添加:WSAStartup的例子代码
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>,还要在链接选项的库模块中添加ws2_32.lib

  然后给CAPP增加一个析构函数调用WSACleanup终止套接字库的使用。
CChatApp::~CChatApp()
{
	// TODO: add construction code here,
	// Place all significant initialization in InitInstance
	WSACleanup();
} //要在头文件的public声明一下

  在CChatDlg中添加一个成员变量SOCKET m_socket private,在构造函数中初始化为0,在CChatDlg中添加析构函数:
CChatDlg::~CChatDlg()
{
	if(m_socket)
		closesocket(m_socket);
}
  再增加一个成员函数:BOOL InitSocket public。

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,套接字属性的描述。

	m_socket=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);
	if(INVALID_SOCKET==m_socket)
	{
		MessageBox("创建套接字失败!");
		return FALSE;
	}
	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;
	}

  在OnInitDialog中(return前)调用此函数。
  定义自定义消息:
在CChatDLg头文件,添加#define UM_SOCK	WM_USER+1
在此类的protected中:afx_msg void OnSock(WPARAM,LPARAM);
在消息映射MESSAGE_MAP中:ON_MESSAGE(UM_SOCK,OnSock)

When one of the nominated network events occurs on the specified socket s, the application's window hWnd receives message wMsg. The wParam parameter identifies the socket on which a network event has occurred. The low word of lParam specifies the network event that has occurred. The high word of lParam contains any error code. //WSAAsyncSelect

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,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

再添加响应函数:

void CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)
{
	switch(LOWORD(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 str;
		CString strTemp;
		if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag,
			(SOCKADDR*)&addrFrom,&len,NULL,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;
	}
}

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,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。


  然后添加发送按钮 命令响应事件:
void CChatDlg::OnBtnSend() 
{
	// TODO: Add your control notification handler code here
	DWORD dwIP;
	CString strSend;
	WSABUF wsabuf;
	DWORD dwSend;
	int len;

	((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);

	SOCKADDR_IN addrTo;
	addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
	addrTo.sin_family=AF_INET;
	addrTo.sin_port=htons(6000);
	
	GetDlgItemText(IDC_EDIT_SEND,strSend);
	len=strSend.GetLength();
	wsabuf.buf=strSend.GetBuffer(len);	//不能直接赋值
	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;
	}
}

6.优化聊天程序

获取主机名:
The Windows Sockets gethostbyname function retrieves host information corresponding to a host name from a host database.

struct hostent FAR * gethostbyname (
  const char FAR * name  //空终止的字符串指针
);

struct hostent {
    char FAR *       h_name;
    char FAR * FAR * h_aliases;
    short            h_addrtype;
    short            h_length;
    char FAR * FAR * h_addr_list;//指针数组存放IP(网络字节序)
};

在发送按钮旁边加一个文本框,IDC_EDIT_HOSTNAME(输入对方主机名):
	DWORD dwIP;
	CString strSend;
	WSABUF wsabuf;
	DWORD dwSend;
	int len;
	CString strHostName;
	HOSTENT* pHost;
	SOCKADDR_IN addrTo;
	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]);
		//这里有点难理解,记住&&&
	}
//前面改变的代码


The Windows Sockets gethostbyaddr function retrieves the host information corresponding to a network address.

struct HOSTENT FAR * gethostbyaddr (
  const char FAR * addr,  
	//A pointer to an address in network byte order
  int len,                
  int type                
);

在消息响应函数中:(增加的部分代码)
		HOSTENT* pHost;
		pHost=gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET);
//为何是4?4个字节?DWORD
		str.Format("%s说:%s",pHost->h_name,wsabuf.buf);


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值