《网络聊天编程.ppt》由会员分享,可在线阅读,更多相关《网络聊天编程.ppt(36页珍藏版)》请在三一文库上搜索。
1、第8章 网络聊天编程,第一部分 应用实践,8.1 为应用系统加入Socket支持,8.2 编写聊天模块前的准备,8.3 编写聊天模块代码,第二部分 知识点链接,L8.1 为应用系统加入Socket支持,L8.2 编写聊天模块代码,第8章 网络聊天编程,系统功能:单击菜单中的“交谈”“对话”,弹出“对话(Socket聊天)”对话框,单击“谁在线上”可以搜索工作组中所有用户的计算机名,双击一个计算机名,该计算机名即显示在左边文本框中,单击空白文本区,单击“我要上线”可上线等待来自其他用户发送的数据并在文本框中显示,否则不能接收消息,但能够给已上线的用户发送消息。,图8.1 双方聊天,第一部分 应用
2、实践,8.1 为应用系统加入Socket支持 为MFC程序加入Socket支持一般有两种方法,第一种是在用MFC AppWizard(exe)创建一个单文档应用程序时,在向导的第4步选中Windows Sockets,如图8.2所示。,图8.2 Windows Sockets支持,8.1 为应用系统加入Socket支持,为MFC程序加入Socket支持一般有两种方法,第一种是在用MFC AppWizard(exe)创建一个单文档应用程序时,在向导的第4步选中Windows Sockets,如图8.2所示。 (1)打开工程,切换到FileView,双击打开头文件StdAfx.h,在其中加入一行:
3、 #include / MFC socket extensions (2)mpr.lib这个库里面封装了Windows Networking(Wnet)函数,这是一组网络控制函数,比如可以利用这些函数来列举局域网内所有机器名称,IP地址以及其他相关信息。为了使用这些函数需要在StdAfx.h中加入一行: #pragma comment(lib,“mpr“) (3)切换到ClassView,打开CXSCJApp下的InitInstance函数,在函数体最前面添加如下代码以初始化Sockets: BOOL CXSCJApp:InitInstance() if (!AfxSocketInit() /
4、 是否初始化成功 AfxMessageBox(“Windows通信端口初始化失败“); return FALSE; ,8.2 编写聊天模块前的准备,(1)设置界面如图8.3所示,新建对话框,设置标题为Sockets聊天,ID为IDD_P2PCHAT,并在菜单上添加相应的项如“对话”,并编写弹出本对话框的命令代码。,图8.3 “设置Socket聊天”对话框,8.2 编写聊天模块前的准备,添加“对话”菜单项ID_P2PCHAT的COMMAND消息到CXSCJView类中,代码如下: void CXSCJView:OnP2pchat() / TODO: Add your command handle
5、r code here CDlgP2P myDlgP2P; myDlgP2P.DoModal(); 在XSCJView.cpp中添加头文件: #include “DlgP2P.h“ (2)设置接收文本框IDC_SHOWSTR控件的属性设置如图8.4(左)所示,设置发送文本框IDC_SENDSTR的属性如图8.4(右)所示。,图8.4 设置编辑框的Styles属性,8.2 编写聊天模块前的准备,(3)打开列表控件的属性对话框,将“查看”选为“小图标”,利于网络聊天时各在线用户名的完全显示。单击编排(Layout)测试(Test)后显示界面如图8.5所示。,图8.5 聊天模块界面,8.3 编写聊天
6、模块代码,(1)为对话框新建类CDlgP2P,为控件添加关联变量如图8.6所示。,图8.6 设置控件关联变量,8.3 编写聊天模块代码,(2)在对话框类CDlgP2P 的头文件DlgP2P.h中加入变量和函数声明。 class CDlgP2P : public CDialog / Construction public: CDlgP2P(CWnd* pParent = NULL); / standard constructor CString GetIP(CString username); / 得到目标机器IP void GetName(char Temp64); / 取得本机名 bool
7、connectFlag ; / 网络连接标识 void ShowRecvData(); / 显示接收的数据 void GetLanActiveComputer(); / 获取本地活动计算机机器名称 ;,8.3 编写聊天模块代码,(3)在DlgP2P.cpp文件中加入全局变量如下: static int CreatedFlag=0; / 是否已建立服务器 SOCKET m_socket,m_hSocket; / 建立套接字描述符 sockaddr_in m_addr; / sockaddr_in结构为套接字储存套接字地址信息 sockaddr_in m_raddr; sockaddr_in m_
8、caddr; char *message=NULL; / 聊天内容接收框中的消息变量 char *name=NULL; / 聊天内容接收框中的主机名变量 int nItem; / CList控件中显示内容(主机名)的序号 CString changstr; / 中间变量 CString strname; / 同上 UINT AcceptThread(LPVOID lpvoid);/ 接收线程 SOCKET m_cSocket; / 客户端Socket,8.3 编写聊天模块代码,(4)取得本机名。 void CDlgP2P:GetName(char Temp64) / 取本机(Local Mac
9、hine)的主机名(即Windows中的本机计算机名) gethostname(Temp,64); (5)添加WM_INITDIALOG消息,用本机用户名初始化全局变量changstr。 BOOL CDlgP2P:OnInitDialog() CDialog:OnInitDialog(); / TODO: Add extra initialization here char localname64; GetName(localname); changstr=localname; return TRUE; / return TRUE unless you set the focus to a c
10、ontrol / EXCEPTION: OCX Property Pages should return FALSE ,8.3 编写聊天模块代码,(6)在DlgP2P.cpp中添加GetLanActiveComputer函数,用枚举的方法找到局域网里所有打开的机器。 该函数的实现代码中用到了几个枚举网络资源的函数,其中包括:WnetOpenEnum函数、WnetEnumResource函数和WnetCloseEnum函数。 (7)根据计算机名得到IP地址。 CString CDlgP2P:GetIP(CString username) CString hostaddr; struct host
11、ent *hostname=gethostbyname(username); for(int i=0;hostname!=NULL ,8.3 编写聊天模块代码,(8)创建ShowRecvData(),该函数负责将接收到的数据显示在文本接收框中。 void CDlgP2P:ShowRecvData() CString tempstr; tempstr=“; if(name!=NULL (9)编写线程AcceptThread,作用是接收来自多个客户端的连接并发送数据。该线程参数为OnCreathost()传来的对话框指针,根据该指针可以对聊天对话框中显示接收数据的IDC_SHOWSTR文本框进行操
12、作。,8.3 编写聊天模块代码,(10)为“我要上线”按钮添加OnCreathost()事件。编写代码时,需要用到许多Sockets编程常用函数,包括:sockets()、bind()、listen()、connect()、accept()、send()、recv()和closesockets。 主要功能是打开Socket,绑定IP和端口号,开始监听,启动线程AcceptThread接受数据。,8.3 编写聊天模块代码,(11)为ListCtrl控件IDC_USER添加双击事件NM_DBLCLK。 void CDlgP2P:OnDblclkUser(NMHDR* pNMHDR, LRESULT
13、* pResult) / TODO: Add your control notification handler code here POSITION pos; m_showname.SetWindowText(“); nItem=0; pos=m_List.GetFirstSelectedItemPosition(); / 取第一个选择项的positon if(pos=NULL) return ; else while(pos) / 如果用户选择了多项,则取最后选择的一项 nItem=m_List.GetNextSelectedItem(pos); m_name=m_List.GetItem
14、Text(nItem,0); UpdateData(FALSE); / 传到“发送计算机”编辑框中,方向是变量到控件 *pResult = 0; ,8.3 编写聊天模块代码,(12)为“谁在线上”按钮添加BN_CLICKED事件,该事件负责将双击事件传来的用户名加入文本框IDC_SENDNAME中。 void CDlgP2P:OnFinduser() / TODO: Add your control notification handler code here CString strTemp; strTemp=“请稍候.“; m_FindBtn.SetWindowText(strTemp);
15、m_List.DeleteAllItems(); / 先清空原来显示的计算机名列表 GetLanActiveComputer(); / 再次取得局域网中的计算机名 Sleep(1000); / 程序冻结1000ms,获取计算机名时避免其他操作 strTemp=“谁在线上?( (13)为按钮IDC_SENDBUTTON添加OnSend事件,作用是根据文本框IDC_SENDNAME传入的用户名,选择服务端,创建发送套接字,向指定计算机发送消息。,第二部分 知识点链接,L8.1 为应用系统加入Socket支持 L1. Socket Socket的数据传输是一种特殊的I/O,Socket也是一种文件描
16、述符。Socket具有一个类似于文件打开的函数调用Socket,该函数返回一个整型的Socket描述符,随后建立连接,数据传输等操作都是通过该Socket实现的。 Socket分为三类:流式Socket(SOCK_STREAM),数据报Socket(SOCK_DGRAM)及原始Socket(SOCK_RAW)。流式Socket为面向连接,数据报Socket为面向无连接;原始Socket主要用于一些协议的开发和测试新的网络协议的实现,可以进行比较底层的操作,如IP的直接访问。 Windows Sockets是微软公司的网络程序设计接口,它是从BSD UNIX Socket扩展而来的。它不仅包含了
17、BSD UNIX Socket风格的库函数,也包含了一组对Windows的扩展库函数,使程序员能够充分利用Windows的消息驱动机制进行编程。,L8.1 为应用系统加入Socket支持,L8.2 编写聊天模块代码 L1. sockaddr_in struct sockaddr结构为套接字储存套接字地址信息。 语法: struct sockaddr unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data14; /* 14字节协议地址*/ ; 参数说明: sa_family TCP/IP协议族默认是“AF_INET“ sa_data 包
18、含套接字中的目标地址和端口信息 为了处理struct sockaddr,使用另一个类似的结构:struct sockaddr_in(“in“代“Internet“)。 struct sockaddr_in short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* Internet 地址 */ unsigned char sin_zero8; /* 与sockaddr结构的长度相同*/ ;,L8.2 编写聊天模块代码,L2. gethostname函数 ge
19、thostname函数用以返回本地主机的标准主机名。 #include / 使用前包含头文件 语法: int PASCAL FAR gethostname(char FAR *name, int namelen); 参数说明: name 一个指向将要存放主机名的缓冲区指针 namelen 缓冲区的长度 该函数把本地主机名存放入由name参数指定的缓冲区中。返回的主机名是一个以NULL结束的字符串。主机名的形式取决于Windows Sockets,它可能是一个简单的主机名,或者是一个域名。然而,返回的名字必定可以在gethostbyname()和 WSAAsyncGetHostByName()中
20、使用。 如果没有错误发生,gethostname()返回0,否则它返回SOCKET_ERROR。应用程序可以通过WSAGetLastError()来得到一个特定的错误代码。 错误代码: WSAEFAULT 名字长度参数太小 WSANOTINTIALISED 在应用这个API前,必须成功地调用WSAStartup() WSAENTDOWN Windows Sockets实现检测到了网络子系统的错误 WSAEINPROGRESS 一个阻塞的Windows Sockets操作正在进行,L8.2 编写聊天模块代码,L3. 枚举网络资源的函数 1WnetOpenEnum函数 WnetOpenEnum函数
21、用于启动对网络资源进行枚举的过程。 语法: DWORD WnetOpenEnum( DWORD dwScope, DWORD dwType, DWORD dwUsage, LPNETRESOURCE lpNetResource, LPHANDLE lphEnum ); 参数说明: DwScope 表示网络枚举的范围 DwType 表示枚举的资源类型 DwUsage 表示枚举资源的用法 LpNetResource 用于返回网络资源信息 LphEnum 表示枚举的资源句柄指针,L8.2 编写聊天模块代码,2WnetEnumResource函数 WnetEnumResource函数用于枚举网络资源。
22、 语法: DWORD WnetEnumResource( HANDLE hEnum, LPDWORD lpcCount, LPVOID lpBuffer, LPDWORD lpBufferSize ); 参数说明: hEnum 由WnetOpenEnum函数的参数lphEnum传入,表示枚举的资源句柄 lpcCount 用来决定获取的资源数目最大值 lpBuffer 指向枚举结果存放的缓冲区地址 lpBufferSize 指向枚举结果存储缓冲区大小的变量地址 3WnetCloseEnum函数 WnetCloseEnum函数用于结束一次枚举操作。 语法: DWORD WnetCloseEnum(
23、 HANDLE hEnum ); 参数说明: hEnum 由WnetOpenEnum函数的参数lphEnum传入,L8.2 编写聊天模块代码,L4. gethostbyname函数 gethostbyname函数能够通过计算机的名称返回其网络信息,这个信息中包括IP地址。 语法: struct hostent FAR * gethostbyname ( const char FAR * name ); 参数说明: name 包含计算机名称的字符串 L5. 网络字节顺序 在Internet上传输的数据和本机内存中的数据存储顺序不同。 网络字节顺序NBO(Network Byte Order):按
24、从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。 主机字节顺序HBO(Host Byte Order):不同的机器HBO不相同,与CPU设计有关。计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序(NBO)在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换。,L8.2 编写聊天模块代码,网络字节顺序和本机字节顺序的转换有两种类型:short(两个字节)和long(四个字节)。这个函数对于变量类型unsigned也适用。比如将short从本机字节顺序转换为网络字节
25、顺序函数:htons(),意思为“Host to Network Short”。当某数据必须按照NBO顺序,要调用函数(例如 htons())来将它从本机字节顺序(Host Byte Order)转换过来。 类似的还有: htons() “Host to Network Short“ htonl() “Host to Network Long“ ntohs() “Network to Host Short“ ntohl() “Network to Host Long“ 需要说明的是,在数据结构struct sockaddr_in中,sin_addr和sin_port 需要转换为网络字节顺序,而
26、sin_family不需要。原因是sin_addr和sin_port分别封装在IP和UDP层,因此,它们必须是网络字节顺序。但是sin_family域只是被主机使用来决定在数据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时sin_family没有发送到网络上,可以是本机字节顺序。,L8.2 编写聊天模块代码,L6. IP点数形式 假设已经有一个sockaddr_in结构体ina,有一个IP地址“132.241.5.10”需要储存在其中,就要用函数inet_addr()将IP地址从点数格式转换成无符号长整型。使用方法如下: ina.sin_addr.s_addr = inet_addr
27、(“132.241.5.10“); 注意,inet_addr()返回的地址已经是网络字节格式,所以无须再调用函数htonl()。 当inet_addr()发生错误时返回-1。上面的代码并不完整,编程时要先进行错误检查。 还可以将一个in_addr结构体输出成点数格式,要用到inet_ntoa()函数(“network to ascii”),比如: printf(“%s“,inet_ntoa(ina.sin_addr);,L8.2 编写聊天模块代码,它将输出IP地址。需要注意的是inet_ntoa()将结构体in_addr作为一个参数,它返回的是一个指向一个字符的指针。它是一个由inet_nto
28、a()控制的静态指针,所以每次调用inet_ntoa()时,它就将覆盖上次调用时所得的IP地址。例如: char *a1, *a2; a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */ a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */ printf(“address 1: %sn“,a1); printf(“address 2: %sn“,a2); 输出如下: address 1: 132.241.5.10 address 2: 132.241.5.10 更多信息请参考有关“网络socke
29、t编程指南”资料。,L8.2 编写聊天模块代码,L7. Sockets编程常用函数 1创建套接字socket函数 SOCKET socket(int af, int type, int protocol ); 应用程序调用socket函数来创建一个能够进行网络通信的套接字。第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET;第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_ DGRAM;第三个参数指定应用程序所使用的通信协议。 该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID
30、_ SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表,但是套接字数据结构都是在操作系统的内核缓冲里。,L8.2 编写聊天模块代码,2捆绑本地地址bind函数 int bind ( SOCKET s, const struct sockaddr FAR *name, int namelen ); 当创建了一个Socket以后
31、,套接字数据结构中有一个默认的IP地址和默认的端口号。一个服务程序必须调用bind函数来给其绑定一个IP地址和一个特定的端口号。客户程序一般不必调用bind函数来为其Socket绑定IP地址和端口号。该函数的第一个参数指定待绑定的Socket描述符;第二个参数指定一个sockaddr结构。该结构是这样定义的: struct sockaddr u_short sa_family; char sa_data14; ;,L8.2 编写聊天模块代码,其中:sa_family指定地址族,对于TCP/IP协议族的套接字,给其置AF_INET。当对TCP/IP协议族的套接字进行绑定时,通常使用另一个地址结构
32、sockaddr_in用来存储IP地址和端口。 struct sockaddr_in short int sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero8; /* Same size as struct sockaddr */ ; sin_family 指定协议族,在socket编程中只能是AF_INET sin_port 存储端口号(使用网络
33、字节顺序) sin_addr 存储IP地址,使用in_addr这个数据结构 sin_zero 仅为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留,L8.2 编写聊天模块代码,3建立套接字的连接 (1)connect()函数 int connect ( SOCKET s, const struct sockaddr FAR *name, int namelen ); 客户程序调用connect函数来使客户Socket与监听于name所指定的计算机的特定端口上的服务Socket进行连接。如果连接成功,connect返回0;如果失败则返回SOCKET_ERROR。下面是一
34、个例子: struct sockaddr_in daddr; memset(void *),L8.2 编写聊天模块代码,(2)accept()函数 SOCKET accept ( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen ); 客户端通过一个服务端正在监听(listen())的端口连接(connect())到服务端,它的连接将加入到等待服务端接受(accept())的队列中。服务端调用accept()告诉客户端有空闲的连接。该函数将返回一个新的套接字,这样就有两个套接字了,原来的一个还在侦听指定的端口,新的在准备发送(send(
35、))和接收(recv())数据。下面是一个调用accept的例子: struct sockaddr_in ServerSocketAddr; int addrlen; addrlen=sizeof(ServerSocketAddr); ServerSocket=accept(ListenSocket,(struct sockaddr *),L8.2 编写聊天模块代码,4监听连接listen函数 int listen( SOCKET s, int backlog ); 服务程序可以调用listen函数使其流套接字s处于监听状态。处于监听状态的流套接字s将维护一个客户连接请求队列,该队列最多容纳b
36、acklog个客户连接请求。假如该函数执行成功,则返回0;如果执行失败,则返回SOCKET_ERROR。 5数据发送send()函数 int send( SOCKET s,const char FAR *buf,int len,int flags ); 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。 第一个参数指定发送端套接字描述符; 第二个参数指明一个存放应用程序要发送数据的缓冲区; 第三个参数指明实际要发送的数据的字节数; 第四个参数一般置0。,L8.2 编写聊天模块
37、代码,6数据接受recv()函数 int recv( SOCKET s, char FAR *buf, int len, int flags ); 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。 第一个参数指定接收端套接字描述符; 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据; 第三个参数指明buf的长度; 第四个参数一般置0。 当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数据或者数据被协议成
38、功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据拷贝到buf中,recv函数返回其实际拷贝的字节数。如果recv在拷贝时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。,L8.2 编写聊天模块代码,7关闭套接字closesocket函数 int closesocket(SOCKET s); closesocket函数用来关闭一个描述符为s套接字。由于每个进程中都有一个套接字描述符表,表
39、中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。当调用closesocket函数时,操作系统先检查套接字数据结构中的该字段的值,如果为1,就表明只有一个套接字描述符指向它,因此操作系统就先把s在套接字描述符表中对应的那条表项清除,并且释放s对应的套接字数据结构;如果该字段大于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把s对应的套接字数据结构的引用次数减1。 closesocket函数如果执行成功就返回0,否则返回S
40、OCKET_ERROR。,L8.2 编写聊天模块代码,L8. 线程 1进程和线程 一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行。由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。 线程被分为两种:用户界面线程和工作者线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息。事实上,应用程序的
41、主执行线程CWinApp对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作者线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重要的是如何实现工作者线程任务的运行控制函数。,L8.2 编写聊天模块代码,2线程的启动 创建一个用户界面线程,首先要从CWinThread类产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声明和实现该CWinThread派生类。 第二步是根据需要重载该派生类的一些成
42、员函数,如ExitInstance、InitInstance、OnIdle、PreTranslateMessage等函数,最后启动该用户界面线程,调用其中一个版本的AfxBeginThread函数,如下面的函数原型: CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
43、其中,pThreadClass为指向定义的用户界面线程类指针变量,nPriority为线程的优先级,nStackSize为线程所对应的堆栈大小,dwCreateFlags为线程创建时的附加标志,若为CREATE_SUSPENDED则线程启动后为挂起状态,lpSecurityAttrs为指向线程安全属性的结构指针变量。,L8.2 编写聊天模块代码,对于工作者线程来说,启动一个线程,首先需要编写一个希望与应用程序的其余部分并行运行的函数。接着定义一个指向CWinThread对象的指针变量*pThread,调用另一个版本的AfxBeginThread函数,并将返回值赋给pThread。AfxBeginThread函数原型如下: CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); 其中,pfnThreadProc为指向工作者线程控制函数的指针,param是传送给工作者线程控制函数的任意32位值。,
链接地址:https://www.31doc.com/p-3227334.html