zoukankan      html  css  js  c++  java
  • VC++学习(16):线程同步与异步套接字

    1.       事件对象

    事件对象同上一课中的互斥对象一样属于内核对象,它包含三个成员:使用读数,用于指明该事件是一个自动重置的还是人工重置的事件的布尔值,用于指明该事件处于已通知状态还是未通知状态的布尔值.

    当人工重置的事件对象得到通知时,等待该事件对象的所有纯种无变为可高度线程,而一个自动重置的事件对象得到通知时,等待该事件对象的线程中人有一个变为可高度线程.所以一般使用线程同步时使用自动重置.

    创建事件对象:

    HANDLE CreateEvent(

     LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全选项,默认为NULL

     BOOL bManualReset,                       // reset type,TRUE(人工),FALSE(自动)

     BOOL bInitialState,                      // initial state,TRUE(有信号状态)

     LPCTSTR lpName                           // object name.事件对象名

    );

    BOOL SetEvent(HANDLE hEvent);把指定的事件对象设置为有信号状态

    BOOL ReSetEvent(HANDLE hEvent);把指定的事件对象设置为无信号状态

    BOOL CloseHandle( HANDLE hObject ); // handle to object关闭事件对象

    DWORD WaitForSingleObject(//请求内核对象,一旦得到事件对象,就进入代码中

     HANDLE hHandle,        // handle to object

     DWORD dwMilliseconds   // time-out interval

    );

    以下是一个模拟火车站售票的多线程程序(使用事件对象实现线程同步)

    #include <windows.h>//加入头文件,Window API

    #include <iostream.h>//C++标准输入输出库

    int tickets = 100;//共享的资源,火车票

    HANDLE g_hEvent;//全局的事件对象句柄

    //线程处理函数原型声明

    DWORD WINAPI Thread1Proc(

             LPVOID lpParameter   // thread data

    );

    DWORD WINAPI Thread2Proc(

             LPVOID lpParameter   // thread data

    );

    void main(){

    //      g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

             //创建一个人工重置的匿名事件对象,当调用SetEvent时所有的线程都可以执行,不能实现同步

    //      SetEvent(g_hEvent);//将事件对象设置为有信号状态

             g_hEvent = CreateEvent(NULL, FALSE, FALSE, "tickets");

             //创建一个自动重置的有名事件对象,当调用SetEvent时只有一个线程可以执行

             SetEvent(g_hEvent);

             //可以通过创建有名的事件对象来实现只有一个程序实例运行

             if (g_hEvent)//有值

             {

                       if (ERROR_ALREADY_EXISTS == GetLastError())//以事件对象存在为条件实现只有一个实例运行限制,因为事件对象是内核对象,由操作系统管理,因此可以在多个线程间访问

                       {

                                cout << "only one instance can run!" << endl;

                                return;

                       }

             }

             HANDLE hThread1;

             HANDLE hThread2;

             hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);

             hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);

             CloseHandle(hThread1);//释放线程句柄

             CloseHandle(hThread2);

             Sleep(4000);

             CloseHandle(g_hEvent);//注意最后释放事件对象句柄,MFC中在类的析构函数中完成

    }

    DWORD WINAPI Thread1Proc(

             LPVOID lpParameter   // thread data

    )

    {

             //其中的SetEvent函数应该在两个判断中都调用,以防止因条件不满足而造成对象不能被设置为有信息状态

             while(TRUE){

                       WaitForSingleObject(g_hEvent, INFINITE);//无限期等待事件对象为有信号状态

                       if (tickets > 0)//进入保护代码

                       {

                                cout << "Thread1 is selling tickets : " << tickets-- << endl;

                                SetEvent(g_hEvent);

                       }

                       Else//如果票已经售完,退出循环

                       {

                                break;

                                SetEvent(g_hEvent);

                       }

             }

             return 0;

    }

    DWORD WINAPI Thread2Proc(

             LPVOID lpParameter   // thread data

    )

    {

             while(TRUE){

                       WaitForSingleObject(g_hEvent, INFINITE);

                       //等待事件对象,如果对象为有信号状态,可以请求该对象资源,并将其设置为无信息状态

                       if (tickets > 0)

                       {

                                cout << "Thread2 is selling tickets : " << tickets-- << endl;

                                SetEvent(g_hEvent);

                       }

                       else

                       {

                                break;

                                SetEvent(g_hEvent);//设置事件对象为有信号状态

                       }

             }

             return 0;

    }

    综上:为实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象

    2.       关键代码段(临界区)

    工作在用户方式下,它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权,通常把多线程访问同一种资源的那部分代码当作关键代码段.

    VOID InitializeCriticalSection(//初始化代码段

     LPCRITICAL_SECTION lpCriticalSection //[out] critical section,使用之前要构造

    );

    VOID EnterCriticalSection(//进入关键代码段(临界区)

     LPCRITICAL_SECTION lpCriticalSection // critical section

    );

    VOID LeaveCriticalSection(//离开关键代码段(临界区)

     LPCRITICAL_SECTION lpCriticalSection   // critical section

    );

    VOID DeleteCriticalSection(//删除关键代码段(临界区)

     LPCRITICAL_SECTION lpCriticalSection   // critical section

    );

    种方法比较简单!但缺点是如果使用了多少关键代码码,容易赞成线程的死锁(使用两个或以上的临界区对象或互斥对象,造成线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,形成死锁,程序无法执行下去!

    3.       互斥对象,事件对象,关键代码段的比较

    n         互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,较慢,但利用互斥对象和事件对象这俗人内核对象,可以在多个进程中的各个纯种间进行同步

    n         关键代码段工作在用户方式下,同步速度快,但很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

    4.       基于消息的异步套接字编程

    Windows套接字在两种模式下执行I/O操作:阻塞模式和非阻塞模式.

    在阻塞模式下,I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回(也就是不地将控制权交还给程序),例如,程序中调用了recvfrom函数后,如果这时网络上没有数据传送过来,该函数就会阻塞程序的执行,从而导致调用线程暂停运行,但不会阻塞主线程运行.

    在非阻塞模式下,Winsock函数无论如何都会立即返回,在该函数执行的操作完成之后,系统会采用某种方式将操作结果通知给调用线程,后者根据通知信息可以判断该操作是否正常完成.

    Windows Sockets采用了基于消息的异步存取策略以支持Windows的消息驱动机制,Windows Sockets的异步选择函数WSAAsyncSelect提供了消息机制的网络事件选择,当使用它登录的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,指示发生的网络事件,以及与该事件相关的一些信息.因此可针对不同的网络事件进行登录,一旦有数据到来,就会触发这个事件,操作系统就会通过一个消息来通知调用线程,后者就可以在相应的消息响应函数中接收这个数据.因为是在该数据到来之后,操作系统发出的通知,所以这时肯定能够接收这个数据.异步套接字能够有效的提高应用程序的性能.

    à一些主要函数

    <1>//为指定的套接字请求基于Windows消息的网络事件通知.自动设置为非阻塞模式

    int WSAAsyncSelect(

     SOCKET s,           //标识请求网络事件通知的套接字描述符

     HWND hWnd,        //标识一个网络事件发生时接收消息的窗口的句柄

     unsigned int wMsg,    //指定网络事件发生时窗口将接收到的消息,(自定义消息)

     long lEvent           //指定网络事件类型,可以位或操作组合使用

    );

    <2> 获得系统中安装的网络协议的相关信息

    int WSAEnumProtocols(

     LPINT lpiProtocols,//[in]NULL结尾的协议标识号数组.如果为NULL,返回可用信息

     LPWSAPROTOCOL_INFO lpProtocolBuffer,//[out]存放指定的完整信息

     ILPDWORD lpdwBufferLength//[in,out]输入时传递缓冲区长度,输出最小缓冲区长度

    );

    <3>初始化进程使用的WS2_32.DLL

    int WSAStartup(

     WORD wVersionRequested,//高位字节指定Winsock库的副版本,低位字节是主版本号

     LPWSADATA lpWSAData//[out]用来接收Windows Sockets实现细节

    );

    <4> 终止对套字库WS2_32.DLL的使用

    int WSACleanup (void);

    <5> Winsock库中的扩展函数WSASocket将创建套接字

    SOCKET WSASocket(

     int af,//地址簇标识

     int type,//socket类型SOCK_DGRAMUDP

     int protocol,//协议簇

     LPWSAPROTOCOL_INFO lpProtocolInfo,//定义创建套接字的特性,如果为NULL,

                                 //WinSock2.Dll使用前三个参数决定使用哪个服务提供者

     GROUP g,//保留

     DWORD dwFlags//指定套接字属性的描述,如果为WSA_FAG_OVERLAPPED则为一个重叠套接字,与文件中相似,

    );

    然后在套接字上调用WSASend, WSARecv,WSASendTo,WSARecvFrom,SWAIoctl这些函数都会立即返回,这些操作完成后,操作系统会通过某种方式来通知调用线程,后者就可以根据通知信息判断操作是否完成

    <6> WSARecvFrom接收数据报类型的数据,并保存数据发送方的地址

    int WSARecvFrom(

     SOCKET s,//套接字描述符

     LPWSABUF lpBuffers,//指向WSABUF数据指针,一个成员缓冲区指针buf,另个长度

     DWORD dwBufferCount,//lpBuffers数组中WSABUF结构体的数上,一般为1

     LPDWORD lpNumberOfBytesRecvd,//[out]接收完成后数据字节数指针

     LPDWORD lpFlags,//[in/out]标志会影响函数行为,设置为0即可

     struct sockaddr FAR *lpFrom,//[out]可选,指向重叠操作完成后存放源地址的缓冲区

     LPINT lpFromlen,//[in/out]指定lpFrom缓冲区大小的指针

     LPWSAOVERLAPPED lpOverlapped,//指向重叠套接字指针,非重叠忽略

     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//一个指定接收完成时调用的完成全程指针(非重叠套接字的忽略0

    );

    如果创建是重叠套接字,最后两个参数值要设置,因为这时将会采用重叠I/O,函数会返回,当接收数据这一操作完成后,操作系统会调用lpCompletionRoutine参数指定的例程来通知调用线程,这个例程就是一个回调函数.

    <7>WSASendTo发送数据报类型的数据

    int WSASendTo(

     SOCKET s,//套接字描述符

     LPWSABUF lpBuffers,

     DWORD dwBufferCount,

     LPDWORD lpNumberOfBytesSent,

     DWORD dwFlags,//0即可

     const struct sockaddr FAR *lpTo,//可选指针,指向目标套接字的地址

     int iToLen,//lpTo中地址长度

     LPWSAOVERLAPPED lpOverlapped,

     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

    );

    5.       一个网络聊天室程序的实现

    新建工程基于对话框,工程名为Chat,并添加一些控件主要两个编辑,IP控件和发送按钮

    [1]      加载套接字库

    需要加载套接字库并进行版本协商,AfxSocketInit只能加载1.1版本的套接字库,本例使用WSAStartup加载系统安装可用版本,CChatAppinitInstance函数加入

    //加载套接字库和进行版本的协商

             WORD wVersionRequested;

             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;

             }

    并在stdafx.h文件中加入头文件#include <winsock2.h>

    [2]      创建并初始化套接字

    CChatDlg类增加一个SOCKET类型的成员变量,m_socket,高为私有,再添加一个BOOL类型的成员函数:InitSocket,初始化该类的套接字成员

    BOOL CChatDlg::InitSocket()

    {

             //使用扩展函数创建套接字

             m_socket = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, NULL, 0);

             if (INVALID_SOCKET == m_socket)

             {

                       MessageBox("创建套接字失败!");

                       return FALSE;

             }

             //要绑定套按字的本地址和协议簇,端口号

             SOCKADDR_IN addrSock;

             addrSock.sin_addr.S_un.S_addr = htonl(ADDR_ANY);

             addrSock.sin_family = AF_INET;

             addrSock.sin_port = htons(6000);

             //绑定套接字到本地套按地址上

             if(SOCKET_ERROR == bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR))){

                       MessageBox("绑定失败!");

                       return FALSE;

             }

             //调用WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ)为网络事件定义消息!

             //此时如果发生FD_READ网络事件,系统会发送UM_SOCK(自定义)消息给应用程序!

             //使用相应的消息响应函数来处理,程序并不会阻塞在这儿了!

             if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))

             {

                       MessageBox("创建网络事件消息处理失败!");

                       return FALSE;

             }

             //剩下的就是在相应的UM_SOCK消息中进行处理了,注意的是:定义的消息要带参数,LPARAM中的低字节是保存网络事件(FD_READ),

             //高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识

             return TRUE;

    }

                  CChatDlg类的OnInitDialog函数中调用这个函数,完成套接字的初始化工作

    [3]      实现接收端的功能

    CChatDlg头文件中定义自定义的消息:UM_SOCK

    #define UM_SOCK WM_USER + 1

    CChatDlg头文件中添加UM_SOCK响应函数原型声明

    protected:

    HICON m_hIcon;

    // Generated message map functions

    //{{AFX_MSG(CChatDlg)

    virtual BOOL OnInitDialog();

    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);

    afx_msg void OnPaint();

    afx_msg HCURSOR OnQueryDragIcon();

    afx_msg void OnBtnSend();

    //}}AFX_MSG

    //定义的消息要带参数,LPARAM中的低字节是保存网络事件(FD_READ),

    //高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识

    afx_msg void OnSock(WPARAM, LPARAM);//自定义消息的响应函数原型

    DECLARE_MESSAGE_MAP()

    CChatDlg类的源文件中添加UM_SOCK消息映射

    BEGIN_MESSAGE_MAP(CChatDlg, CDialog)

    //{{AFX_MSG_MAP(CChatDlg)

    ON_WM_SYSCOMMAND()

    ON_WM_PAINT()

    ON_WM_QUERYDRAGICON()

    ON_BN_CLICKED(IDC_BTN_SEND, OnBtnSend)

    //}}AFX_MSG_MAP

    ON_MESSAGE(UM_SOCK, OnSock)//消息与其响应函数的映射

    END_MESSAGE_MAP()

                  消息响应函数的实现,因为同时可以请求多个网络事件如FD_READRDWRITE

    最好对所接受的消息进行判断后处理,本例中只有FD_READ,但仍判断处理,要注意是消息接收两个参数,低字节是保存网络事件(FD_READ),高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识.

    //自定义消息响应函数的定义

    void CChatDlg::OnSock(WPARAM wParam, LPARAM lParam){

             switch (LOBYTE(lParam))

             {

             case FD_READ://发生是网络读取事件

                       WSABUF wsaBuf;

                       char recvBuf[200];

                       wsaBuf.buf = recvBuf;

                       wsaBuf.len = 200;

                       DWORD dwRead;

                       DWORD dwFlag = 0;

                       SOCKADDR_IN addrFrom;

                       int len = sizeof(SOCKADDR);

                       if(SOCKET_ERROR == WSARecvFrom(m_socket, &wsaBuf, 1, &dwRead, &dwFlag, (SOCKADDR*)&addrFrom, &len, NULL, NULL)){

                                MessageBox("接收网络数据失败!");

                                return;

                       }

                       CString strRecv;

                       CString strTemp;

                       strRecv.Format("%s : %s", inet_ntoa(addrFrom.sin_addr), recvBuf);

                       GetDlgItemText(IDC_EDIT_RECV, strTemp);

                       strRecv += "\r\n";

                       strRecv += strTemp;

                       SetDlgItemText(IDC_EDIT_RECV, str);

                       break;

             }

    }

          

    [4]      发送端按钮的实现

    void CChatDlg::OnBtnSend()

    {

             // TODO: Add your control notification handler code here

             DWORD ip;       

             WSABUF wsaBuf;     

             SOCKADDR_IN addrTo;

             CString strSend;

             int len;

             DWORD dwSend;

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

            

             addrTo.sin_addr.S_un.S_addr = htonl(ip);

             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;

             }      

    }

     

    [5]      终止套接字库的使用

    CChatApp类增加一个析构函数,主要是在此函数中调用WSACleanup函数,终止对套接字库的使用

    CChatApp::~CChatApp()

    {

             WSACleanup();//释放套接字

    }

     

    [6]      CChatDlg类中关闭套接字,添加一个析构函数,首先判断是否该套接字库有值,如果有的话关闭套接字

    CChatDlg::~CChatDlg(){

             closesocket(m_socket);

    }

     

    4.    利用主机名实现网络访问

           struct hostent FAR *gethostbyname(

                const char FAR *name //从主机名中获取IP地址

    );

    Hostent结构体:

    struct hostent {

     char FAR *       h_name;

     char FAR * FAR * h_aliases;

     short            h_addrtype;

     short            h_length;

     char FAR * FAR * h_addr_list;//空中止的IP地址列表,是一个char*字符数组,因为一个

                                     //主机可能有多个IP,选择第一个即可

    };

    由主机IP转换成主机名

    struct HOSTENT FAR * gethostbyaddr(

     const char FAR *addr,//指向网络字节序表示的IP地址指针

     int len,//地址长度,对于AF_INET必须为4

     int type//类型AF_INET

    );

    接收方部分代码可改为;

    HOSTENT *pHost;

    pHost = gethostbyadd((char*)&addrFrom.sin_addr.S_un.S_addr, 4, AF_INET);

    str.Format(%s:%s, pHost->h_name, wsabuf.buf);

  • 相关阅读:
    java web
    java web
    java
    周末总结7
    java
    java
    java
    java
    java web
    java
  • 原文地址:https://www.cnblogs.com/forlina/p/2119785.html
Copyright © 2011-2022 走看看