孙鑫VC++图文笔记14-20课
更新时间:2023-05-21 04:00:01 阅读量: 实用文档 文档下载
- 孙鑫vc深入详解 pdf推荐度:
- 相关推荐
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
在给指定IP地址的计算机上传输数据,将会由在特定端口上等待数据的网络应用程序接收数据,IP地址就相当于一个公司的总机号,端口就相当于分机号。IP地址:
IP网络中每台主机都必须有一个唯一的IP地址;IP地址是一个逻辑地址;
因特网上的IP地址具有全球唯一性;
32位,4个字节,常用点分十进制的格式表示,例如:192.168.0.16协议:
为进行网络中的数据交换(通信)而建立的规则、标准或约定。(=语义+语法+规则)不同层具有各自不同的协议。ISO/OSI七层参考模型:
OSI(OpenSystemInterconnection)参考模型将网络的不同功能划分为7层:7.应用层6.表示层5.会话层4.
传输层3.网络层2.数据链路层1.物理层
处理网络应用,为用户的应用程序提供网络通信的服务
处理被传送数据的表示问题,即信息的语法和语义,如有必要使用一种通用的数据表示格式在多种数据表示格式之间进行转换
主机间通信,在两个相护通信的应用进程间建立组织和协调相互之间的通信
端到端的连接,为源端主机到目的端主机提供可靠的传输服务,隔离网络的上下层协议,使网络应用与下层协议无关
提供IP寻址和路由,因为到达目的地可能会有多条线路,网络层就负责找出最佳的传输线路提供介质访问,加强物理层的传输功能,建立一条无差错的传输线路提供二进制传输,确定在通信信道上如何传输比特流
我们要注意的是:OSI七层参考模型并不是物理实体上存在这七层,这只是一个功能的划分,是一个抽象的网络参考模型,在进行一次网络通信时,每一层为这次通信提供本层的服务。
通信实体的对等层之间不允许直接通信。各层之间是严格单向依赖。
上层使用下层提供的服务—Serviceuser;下层向上层提供服务—Serviceprovider通信示例:
中国的老师向德国的老师问好,如图所示,这个信息的传输过程和两个主机之间的通信过程是相似的。对等层之间有一个虚拟的连接,中国教师向德国教师问好,它认为中国教师在和德国教师直接通信,实际上通信是通过下层所提供的服务完成的。同样的对于翻译来说它也认为它们之间是在直接进行通信。
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
对等层实体之间虚拟通信。
下层向上层提供服务,实际通信在最底层完成。应用层:远程登录协议Telnet、文件传输协议FTP、超文本传输协议HTTP、域名服务DNS、简单邮件传输协议SMTP(传送邮件)、邮局协议POP3等(接收邮件)。
传输层:传输控制协议TCP、用户数据报协议UDP。TCP:面向连接的可靠的传输协议。(先连接再传输数据)UDP:是无连接的,不可靠的传输协议。(不用先连接直接向一个IP传输数据)
网络层:网际协议IP、Internet互联网控制报文协议ICMP、Internet组管理协议IGMP。一台计算机要发送数据到另一台计算机,数据首先必须打包,打包的过程称为封装。封装就是在数据前面加上特定的协议头部。比如说利用TCP协议传送数据,到数据到达传输层的时候就会加上一个TCP头,当数据到达网络层的时候在数据前面还会加上一个IP头。
OSI参考模型中,对等层协议之间交换的信息单元统称为协议数据单元(PDU,ProtocolDataUnit)。OSI参考模型中每一层都要依靠下一层提供的服务。
为了提供服务,下层把上层的PDU作为本层的数据封装,然后加入本层的头部(和尾部)。头部中含有完成数据传输所需的控制信息。
这样,数据自上而下递交的过程实际上就是不断封装的过程。到达目的地后自下而上递交的过程就是不断拆封的过程。由此可知,在物理线路上传输的数据,其外面实际上被包封了多层“信封”。
但是,某一层只能识别由对等层封装的“信封”,而对于被封装在“信封”内部的数据仅仅是拆封后将其提交给上层,本层不作任何处理。TCP/IP起源于美国国防部高级研究规划署(DARPA)的一项研究计划——实现若干台主机的相互通信。现在TCP/IP已成为Internet上通信的工业标准。
TCP/IP模型包括4个层次:应用层,传输层,网络层,网络接口。网络接口层对应物理层和数据链路层。网络层对应网络层。传输层对应传输层。
应用层对应会话层,表示层,应用层。按照OSI七层模型的描述,传输层提供进程(应用程序)通信的能力。为了标识通信实体中进行通信的进程(应用程序),TCP/IP协议提出了协议端口(protocolport,简称端口)的概念。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出。
端口用一个整数型标识符来表示,即端口号。端口号跟协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立。
端口使用一个16位的数字来表示,它的范围是0~65535,1024以下的端口号保留给预定义的服务。例如:http使用80端口。
套接字(socket)的引入:
为了能够方便的开发网络应用软件,由美国伯克利大学在Unix上推出了一种应用程序访问通信协议的操作系统调用socket(套接字)。socket的出现,使程序员可以很方便地访问TCP/IP,从而开发各种网络应用的程序。
随着Unix的应用推广,套接字在编写网络软件中得到了极大的普及。后来,套接字又被引进了Windows等操作系统,成为开发网络应用程序的非常有效快捷的工具。
套接字存在于通信区域中。通信区域也叫地址族,它是一个抽象的概念,主要用于将通过套接字通信的进
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
程的共有特性综合在一起。套接字通常只与同一区域的套接字交换数据(也有可能跨区域通信,但这只在执行了某种转换进程后才能实现)。WindowsSockets只支持一个通信区域:网际域(AF_INET),这个域被使用网际协议簇通信的进程使用。不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存)。基于Intel的CPU,即我们常用的PC机采用的是低位先存。为保证数据的正确性,在网络协议中需要指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高位先存格式。在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(client/server),即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。
客户机/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户机/服务器模式的TCP/IP。
客户机/服务器模式在操作过程中采取的是主动请求的方式。首先服务器方要先启动,并根据请求提供相应的服务:
①打开一个通信通道并告知本地主机,它愿意在某一地址和端口上接收客户请求。②等待客户请求到达该端口。③接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一个新的进程(或线程)来处理这个客户请求。新进程(或线程)处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
④返回第二步,等待另一客户请求。⑤关闭服务器。客户方:
①打开一个通信通道,并连接到服务器所在主机的特定端口。②向服务器发服务请求报文,等待并接收应答;继续提出请求。③请求结束后关闭通信通道并终止。WindowsSockets是MicrosoftWindows的网络程序设计接口,它是从BerkeleySockets扩展而来的,以动态链接库的形式提供给我们使用。WindowsSockets在继承了BerkeleySockets主要特征的基础上,又对它进行了重要扩充。这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。
WindowsSockets1.1和BerkeleySockets都是基于TCP/IP协议的;WindowsSockets2从WindowsSockets1.1发展而来,与协议无关并向下兼容,可以使用任何底层传输协议提供的通信能力,来为上层应用程序完成网络数据通讯,而不关心底层网络链路的通讯情况,真正实现了底层网络通讯对应用程序的透明。流式套接字(SOCK_STREAM)提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。(基于TCP)
数据报式套接字(SOCK_DGRAM)提供无连接服务。数据包以独立包形式发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。(基于UDP)
原始套接字(SOCK_RAW)。服务器端程序:
1、创建套接字(socket)。
2、将套接字绑定到一个本地地址和端口上(bind)。3、将套接字设为监听模式,准备接收客户请求(listen)。
客户端程序:
1、创建套接字(socket)。
2、向服务器发出连接请求(connect)。3、和服务器端进行通信(send/recv)。
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
第十四章:socket编程
4、等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
5、用返回的套接字和客户端进行通信(send/recv)。6、返回,等待另一客户请求。7、关闭套接字。
游鱼编辑QQ:6241798
4、关闭套接字。
当客户端请求连接时,服务端所返回的套接字就已经包含了客户端的IP和端口号了。基于UDP(面向无连接)的socket编程:
服务器端(接收端)程序:1、创建套接字(socket)。
2、将套接字绑定到一个本地地址和端口上(bind)。3、等待接收数据(recvfrom)。4、关闭套接字。
客户端(发送端)程序:1、创建套接字(socket)。
2、向服务器发送数据(sendto)。3、关闭套接字。
=====================================================================================下面开始做实例,新建一个win32控制台工程,工程名为TcpSrv,面向连接的服务端:intWSAStartup(
WORDwVersionRequested,LPWSADATAlpWSAData);//SDK
该函数有两个作用,一个是加载套接字库,第2个作用是进行套接字库的版本协商,也就是确定我们要使用的是哪一个版本的套接字库。第1个参数用来指定所请求的版本号。第2个参数是指向WSADATA结构体的指针,主要用来接收WindowsSockets实现的细节。wVersionRequested参数用于指定准备加载的Winsock库的版本。高位字节指定所需要的Winsock库的副版本,而低位字节则是主版本。可用MAKEWORD(x,y)(其中,x是高位字节,y是低位字节)方便地获得wVersionRequested的正确值。
lpWSAData参数是指向WSADATA结构的指针,WSAStartup用其加载的库版本有关的信息填在这个结构中。typedefstructWSAData{WORDwVersion;
WORDwHighVersion;
charszDescription[WSADESCRIPTION_LEN+1];charszSystemStatus[WSASYS_STATUS_LEN+1];unsignedshortiMaxSockets;unsignedshortiMaxUdpDg;charFAR*lpVendorInfo;}WSADATA,*LPWSADATA;
WSAStartup把第一个字段wVersion设成打算使用的Winsock版本。wHighVersion参数容纳的是现有的Winsock库的最高版本。记住,这两个字段中,高位字节代表的是Winsock副版本,而低位字节代表的是Winsock主版本。szDescription和szSystemStatus这两个字段由特定的Winsock实施方案设定,事实上没有用。不要使用下面两个字段:iMaxSockets和iMaxUdpDg,它们是假定同时最多可打开多少套接字和数据报的最大长度。然而,要知道数据报的最大长度应该通过WSAEnumProtocols来查询协议信息。同时最多可打开套接字的数目不是固定的,很大程度上和可用物理内存的多少有关。最后,lpVendorInfo字段是为Winsock实施方案有关指定厂商信息预留的。任何一个Win32平台上都没有使用这个字段。
如果WinSock.dll或底层网络子系统没有被正确初始化或没有被找到,WSAStartup将返回WSASYSNOTREADY。此外这个函数允许你的应用程序协商使用某种版本的WinSock规范,如果请求的版本
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
等于或高于DLL所支持的最低版本,WSAData的wVersion成员中将包含你的应用程序应该使用的版本,它是DLL所支持的最高版本与请求版本中较小的那个。反之,如果请求的版本低于DLL所支持的最低版本,WSAStartup将返回WSAVERNOTSUPPORTED。关于WSAStartup更详细的信息,请查阅MSDN中的相关部分。
对于每一个WSAStartup的成功调用(成功加载WinSock.dll后),在最后都对应一个WSACleanUp调用,以便释放为该应用程序分配的资源。
SOCKETsocket(intaf,inttype,intprotocol
);//SDK,创建套接字,该函数返回一个套接字的描述符,其实就是一个整形。该函数接收三个参数。第一个参数af指定地址族,对于TCP/IP协议的套接字,它只能是AF_INET(也可写成PF_INET)。第二个参数指定Socket类型,对于1.1版本的Socket,它只支持两种类型的套接字,SOCK_STREAM指定产生流式套接字,SOCK_DGRAM产生数据报套接字。第三个参数是与特定的地址家族相关的协议,如果指定为0,那么它就会根据地址格式和套接字类别,自动为你选择一个合适的协议。这是推荐使用的一种选择协议的方法。
如果这个函数调用成功,它将返回一个新的SOCKET数据类型的套接字描述符。如果调用失败,这个函数就会返回一个INVALID_SOCKET,错误信息可以通过WSAGetLastError函数返回。intbind(
SOCKETs,
conststructsockaddrFAR*name,intnamelen
);//SDK,该函数将本地地址和一个套接字关联起来这个函数接收三个参数。第一个参数s指定要绑定的套接字,第二个参数指定了该套接字的本地地址信息,是指向sockaddr结构的指针变量,由于该地址结构是为所有的地址家族准备的,这个结构可能(通常会)随所使用的网络协议不同而不同,所以,要用第三个参数指定该地址结构的长度。sockaddr结构定义如下:
structsockaddr{u_shortsa_family;charsa_data[14];};
sockaddr的第一个字段sa_family指定该地址家族,在这里必须设为AF_INET。sa_data仅仅是表示要求一块内存分配区,起到占位的作用,该区域中指定与协议相关的具体地址信息。由于实际要求的只是内存区,所以对于不同的协议家族,用不同的结构来替换sockaddr。除了sa_family外,sockaddr是按网络字节顺序表示的。在TCP/IP中,我们可以用sockaddr_in结构替换sockaddr,以方便我们填写地址信息。
sockaddr_in的定义如下:structsockaddr_in{shortsin_family;
unsignedshortsin_port;structin_addrsin_addr;charsin_zero[8];};
其中,sin_family表示地址族,对于IP地址,sin_family成员将一直是AF_INET。成员sin_port指定的是将要分配给套接字的端口。成员sin_addr给出的是套接字的主机IP地址。sin_addr它的类型是一个结构体in_addr:structin_addr{
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
union{
struct{u_chars_b1,s_b2,s_b3,s_b4;}S_un_b;struct{u_shorts_w1,s_w2;}S_un_w;u_longS_addr;
}S_un;};
这个结构体中实际上是一个联合,通常将一个IP地址转化为u_long类型,然后给S_addr进行赋值。
而成员sin_zero只是一个填充数,以使sockaddr_in结构和sockaddr结构的长度一样。如果这个函数调用成功,它将返回0。如果调用失败,这个函数就会返回一个SOCKET_ERROR,错误信息可以通过WSAGetLastError函数返回。
将IP地址指定为INADDR_ANY,允许套接字向任何分配给本地机器的IP地址发送或接收数据。多数情况下,每个机器只有一个IP地址,但有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR_ANY可以简化应用程序的编写。将地址指定为INADDR_ANY,允许一个独立应用接受发自多个接口的回应。如果我们只想让套接字使用多个IP中的一个地址,就必须指定实际地址,要做到这一点,可以用inet_addr()函数,这个函数需要一个字符串作为其参数,该字符串指定了以点分十进制格式表示的IP地址(如192.168.0.16)。而且inet_addr()函数会返回一个适合分配给S_addr的u_long类型的数值。inet_ntoa()函数会完成相反的转换,它接受一个in_addr结构体类型的参数并返回一个以点分十进制格式表示的IP地址字符串。u_longhtonl(
u_longhostlong
);//SDK,该函数转换u_long类型(4字节)从主机字节序到TCP/IP网络字节序。u_shorthtons(
u_shorthostshort
);//SDK,转换u_short类型(2字节)从主机字节序到TCP/IP网络字节序。intlisten(SOCKETs,//套接字描述符
intbacklog//等待连接队列的最大长度,如果为SOMAXCONN是最大合理长度);//SDK,将套接字设为监听模式,监听连接请求。
SOCKETaccept(SOCKETs,
structsockaddrFAR*addr,//保存了发起连接客户端的IP和端口信息intFAR*addrlen//用来包含所返回地址结构的长度
);//SDK,该函数可以获取请求连接客户端的地址端口等信息。
该函数接收一个客户端的连接请求,建立连接同时返回一个新的套接字描述符,利用新的套接字就可以和连接的客户端进行通信了,而原来的套接字则继续监听客户的连接请求。
intsend(
SOCKETs,//连接对应的套接字,也就是新的套接字constcharFAR*buf,//baffer,包含了将要被传送的数据intlen,//数据长度intflags//设置这个值会影响send的调用行为,详见MSDN,本程序中设为0);//SDK,用来发送数据
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
第十四章:socket编程游鱼编辑QQ:6241798inet_ntoa函数接受in_addr类型的一个参数,然后返回一个点分十进制表示格式的IPintrecv(
SOCKETs,//建立连接之后的套接字charFAR*buf,//bufferintlen,//buffer长度intflags//影响函数调用行为参数,详见MSDN,本程序中为0);//SDK,接收数据
在我们的程序中使用了winsock库中的函数,因此要要包含#include<Winsock2.h>头文件。
在包含完头文件之后还需要链接一个库文件Ws2_32.lib,这是使用动态链接库时需要做的。Project/Settings.../Link打开后在库模块框内添加Ws2_32.lib,与前面的文件用空格隔开。#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;//结构体变量interr;
wVersionRequested=MAKEWORD(1,1);//用MAKEWORD宏请求1.1版本WinSock库
err=WSAStartup(wVersionRequested,&wsaData);//加载库if(err!=0){
return;//如果返回值不等于0,程序退出}
if(LOBYTE(wsaData.wVersion)!=1||//判断低位字节
HIBYTE(wsaData.wVersion)!=1){//判断高位WSACleanup();//如果不是1.1版本,终止对库的使用return;//程序退出}
SOCKETsockSrv=socket(AF_INET,SOCK_STREAM,0);//创建套接字,基于TCP用流式SOCKADDR_INaddrSrv;//结构体变量addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//地址转换为网络字节序addrSrv.sin_family=AF_INET;//地址族family不用为网络字节序addrSrv.sin_port=htons(6000);//端口,要用1024以上bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//绑定
//注意上面的强制转换,第2参数需要SOCKADDR结构类型,结构体大小写一样listen(sockSrv,5);//监听SOCKADDR_INaddrClient;//结构体变量intlen=sizeof(SOCKADDR);//结构体长度[inout]型先初始化,后面调用还会返回一个值while(1){
SOCKETsockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);charsendBuf[100];
sprintf(sendBuf,"Welcome%sto",
inet_ntoa(addrClient.sin_addr));//将数据格式化到buffersend(sockConn,sendBuf,strlen(sendBuf)+1,0);//发送数据
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
charrecvBuf[100];
recv(sockConn,recvBuf,100,0);//接收数据printf("%s\n",recvBuf);//将数据打印出来closesocket(sockConn);//关闭套字,释放所分配的资源}
}//在死循还内发送数据接收数据完闭后关闭当前连接的套接字,返回循还继续下一次的连接数据处理。我们可以在工作区中新建一个工程TcpClient,工作区在FileView中。这样我们就同样有了两个工程。在VC工具栏的空白处右键将BuildMiniBar取消然后选中Build,在工具栏上就会多出一个下拉列表框,在这个列表框里就可以实现对工程的切换。intconnect(SOCKETs,
conststructsockaddrFAR*name,//地址结构体指针,连接的服务器端的地址信息intnamelen//地址结构体的长度);//SDK,该函数建立一个连接
127.0.0.1该IP是本地机器回路地址,不管有没有网卡该地址都有效。inet_addr可以将点分十进制的字符串转换为u_long类型。
#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;interr;
wVersionRequested=MAKEWORD(1,1);
err=WSAStartup(wVersionRequested,&wsaData);if(err!=0){
return;}
if(LOBYTE(wsaData.wVersion)!=1||
HIBYTE(wsaData.wVersion)!=1){WSACleanup();return;}
SOCKETsockClient=socket(AF_INET,SOCK_STREAM,0);SOCKADDR_INaddrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));charrecvBuf[100];
recv(sockClient,recvBuf,100,0);//接收数据printf("%s\n",recvBuf);//打印数据
//请求连接
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
send(sockClient,"Thisislisi",strlen("Thisislisi")+1,0);//发送数据closesocket(sockClient);//关闭套接字释放为套接字分配的资源WSACleanup();//中止对套接字库的使用}
注意:对于通信程序,总是服务端先要启动,这就是面向TCP服务端与客户端的编写。
------------------------------------------------------------------------------------------------------------------------------------------------基于UDP面向无连接网络应用程序的编写:
新建一个工程UdpSrv,也就是服务端编写。注意本课的每个工程中都要加入Ws2_32.lib。
intrecvfrom(SOCKETs,//套接字charFAR*buf,//接收数据的bufferintlen,//buffer长度intflags,//影响函数调用行为,见MSDN,本程序中设为0.structsockaddrFAR*from,//地址结构体指针用来接收发送数据方地址信息intFAR*fromlen//地址结构的大小[in,out]型,也就是要先初始再有返回值,前面程序一样);//SDK,面向UDP无连接编程时我们用该函数接收数据。:
#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;interr;
wVersionRequested=MAKEWORD(1,1);
err=WSAStartup(wVersionRequested,&wsaData);if(err!=0){return;}
if(LOBYTE(wsaData.wVersion)!=1||
HIBYTE(wsaData.wVersion)!=1){WSACleanup();return;}
SOCKETsockSrv=socket(AF_INET,SOCK_DGRAM,0);//数据报套接字SOCKADDR_INaddrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//绑定SOCKADDR_INaddrClient;//结构变量
intlen=sizeof(SOCKADDR);//地址结构大小,先给它赋一个初始值charrecvBuf[100];
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);//接收printf("%s\n",recvBuf);//打印closesocket(sockSrv);//关闭套接字释放为套接字所分配的资源WSACleanup();//终止对套接字库的使用
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
}
在工作区添加一个工程UdpClient:intsendto(
SOCKETs,//套接字constcharFAR*buf,//buffer包含将要被发送的数据intlen,//buffer长度
intflags,//影响函数调用行为,见MSDN,本程序中设为0
conststructsockaddrFAR*to,//地址结构体指针,用来设定目的套接字地址信息[in]inttolen//地址结构体的长度,注意在这个函数中的只有[in],没有[out]返回);//SDK,发送数据例:
#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;interr;
wVersionRequested=MAKEWORD(1,1);
err=WSAStartup(wVersionRequested,&wsaData);if(err!=0){
return;}
if(LOBYTE(wsaData.wVersion)!=1||
HIBYTE(wsaData.wVersion)!=1){WSACleanup();return;}
SOCKETsockClient=socket(AF_INET,SOCK_DGRAM,0);SOCKADDR_INaddrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(6000);
sendto(sockClient,"Hello",strlen("Hello")+1,0,
(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//发送数据
closesocket(sockClient);WSACleanup();}
=====================================================================================对于聊天类的软件通常会采用UDP的方式实现,因为UDP不需要建立连接而且没有数据确认和数据重传的机制,实时性较高。在TCP协议中是以三步握手方式进行连接,当聊天的时候要先连接再聊天,如果不聊天时是连接还是先断开呢?因此在TCP中面向连接数据确认和重传的机制将会影响聊天的效率。gets函数:C语言的函数,它有一个参数是一个字符指针指向的buffer,该函数可以从标准输入流中获取一行数
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
据,当我们从键盘输入一行字符当回车的时候,这一行字符就会保存到buffer当中。例:
#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;interr;
wVersionRequested=MAKEWORD(1,1);
err=WSAStartup(wVersionRequested,&wsaData);if(err!=0){return;}
if(LOBYTE(wsaData.wVersion)!=1||HIBYTE(wsaData.wVersion)!=1){
WSACleanup();return;}
SOCKETsockSrv=socket(AF_INET,SOCK_DGRAM,0);SOCKADDR_INaddrSrv;//存放本地地址信息的结构体addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//绑定charrecvBuf[100];//接收信息的buffercharsendBuf[100];//发送信息的bufferchartempBuf[200];//存放中间数据SOCKADDR_INaddrClient;//客户端地址结构体intlen=sizeof(SOCKADDR);//初始化while(1){
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);//接收if('q'==recvBuf[0])//如果接收的第1个字符是q,则聊天终止{
sendto(sockSrv,"q",strlen("q")+1,0,(SOCKADDR*)&addrClient,len);//给对方也发送q
printf("Chatend!\n");//输出聊天结束break;//退出循还}
sprintf(tempBuf,"%ssay:%s",inet_ntoa(addrClient.sin_addr),recvBuf);//信息格式化printf("%s\n",tempBuf);//将接收到的信息输出printf("Pleaseinputdata:\n");gets(sendBuf);//从键盘获取数据然后放到sendBuf,下面一句就发送数据了sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrClient,len);}
closesocket(sockSrv);WSACleanup();}
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
第十四章:socket编程#include<Winsock2.h>#include<stdio.h>voidmain(){
WORDwVersionRequested;WSADATAwsaData;interr;
wVersionRequested=MAKEWORD(1,1);
err=WSAStartup(wVersionRequested,&wsaData);if(err!=0){return;}
if(LOBYTE(wsaData.wVersion)!=1||HIBYTE(wsaData.wVersion)!=1){
WSACleanup();return;}
SOCKETsockClient=socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_INaddrSrv;//服务器端地址结构addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");addrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(6000);
游鱼编辑QQ:6241798
charrecvBuf[100];//接收数据的buffercharsendBuf[100];//发送数据的bufferchartempBuf[200];//临时数据存储的bufferintlen=sizeof(SOCKADDR);//初始化while(1){
printf("Pleaseinputdata:\n");gets(sendBuf);//从键盘输入数据然后保存到buffer中sendto(sockClient,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrSrv,len);//发送数据,注意最后一个参数不是指针,只是[in]recvfrom(sockClient,recvBuf,100,0,(SOCKADDR*)&addrSrv,&len);//接收,注意&lenif('q'==recvBuf[0]){
sendto(sockClient,"q",strlen("q")+1,0,
(SOCKADDR*)&addrSrv,len);printf("Chatend!\n");break;}
sprintf(tempBuf,"%ssay:%s",inet_ntoa(addrSrv.sin_addr),recvBuf);printf("%s\n",tempBuf);//输出接收的数据}
closesocket(sockClient);WSACleanup();}
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
程序、进程和线程:
是计算机指令的集合,它以文件的形式存储在磁盘上。
进程:通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。
进程由两个部分组成:
1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
2、地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。
单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。
每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。进程地址空间:
系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB。
每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。
4GB是虚拟的地址空间,只是内存地址的一个范围。在你能成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。
4GB虚拟地址空间中,2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。
物理存储器是物理内存加页文件的大小,在任务管理器的下面就可以看到内存的数量,它实际上包括了物理内存和页文件的大小,将系统盘下的隐藏文件显示出来,可以看到一个pagefile.sys文件,这就是页文件,页文件给应用程序增加了可以使用的内存,通过在磁盘上划分出一块空间来增加内存的大小,这样的内存叫做虚拟内存。修改页文件大小:我的电脑\属性\高级\性能。
由两个部分组成:
1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。2、线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。
线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。
如果计算机拥有多个CPU,线程就能真正意义上同时运行了。
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
我们新建一个win32控制台MultiThread:
当我们的程序启动运行的时候,就会产生一个主线程,main函数就是做为主线程的入口函数,在这个线程中可以去创建一个新的线程,要创建一个线程要用到一个API函数:
HANDLECreateThread(
LPSECURITY_ATTRIBUTESlpThreadAttributes,//SDSIZE_TdwStackSize,//initialstacksizeLPTHREAD_START_ROUTINElpStartAddress,//threadfunctionLPVOIDlpParameter,//threadargumentDWORDdwCreationFlags,//creationoptionLPDWORDlpThreadId//threadidentifier);
该函数创建一个线程,第1个参数是指向SECURITY_ATTRIBUTES结构体的指针,在这里传递一个NULL使用缺省的安全性。第2个参数是指定初始提交的栈的大小,以字节为单位,系统会将这个值四舍五入为最近的页面,页面是系统在管理内存时使用的一个内存单位,不同的CPU页面大小也是不同的,X86使用的页面大小是4KB,当你保留地址空间的一个区域时,系统要确保该区域的大小是系统的页面大小的倍数,例如想保留10KB的地址空间区域,系统将自动对你的请求进行四舍五入,使保留的地址空间区域的大小是页面大小的倍数,在X86平台上系统将保留12KB的区域。如果这个参数是0或者小于缺省提交的大小,那么缺省使用和调用线程一样的大小。第3个参数是指向LPTHREAD_START_ROUTINE这个类型的应用程序定义的函数的指针,这个函数将被线程执行表示了线程的起始地址,我们知道对于主线程main函数是它的入口函数,对于我们创建的线程也需要一个入口函数,这个函数的地址就在这个参数指定,这个函数是什么类型呢:
DWORDWINAPIThreadProc(LPVOIDlpParameter//threaddata
);//我们可以直接用这个函数,该函数名是随便取的,但参数和返回类型一定要和该函数一致。第4个参数是用来指定一个单独的参数的值传递给线程。
第5个参数是用来指定控制线程创建的附加标记,如果指定CREATE_SUSPENDED这个标记,那么线程创建后处于暂停状态,不会运行,直到调用了ResumeThread这个函数。如果这个值是0,那么线程在创建之后立即运行。第6个参数[out]作为返回值使用,它指定一个变量用来接收线程的标识符,当我们创建一个线程后,系统会为这个线程分配一个ID号,如果为NULL,那么这个标识符不能被返回。
如果我们用该函数创建线程成功,它会返回一个新的线程的句柄。viodmain()
{HANDLEhThread1;//句柄
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);//创建线程CloseHandle(hThread1);//关闭线程句柄}
这里关闭句柄并没有终止新创建的线程,在这个地方只是表示在主线程中,我们对新创建的线程它的引用不感兴趣,所以将它关闭。另一方面当我们关闭句柄的时候,系统会递减新线程的内核对象的使用计数,当我们所创建的线程执行完毕之后,系统也会递减线程内核对象的使用计数,当使用计数为0的时候,系统就会释放线程内核对象,如果在主线程中没有关闭新建线程的句柄,始终都会保留一个引用,这样线程内核对象的使用计数就不会为0,即使新建的线程执行完毕,线程内核对象也不会被释放,只有等到进程终止的时候系统为这些残留的对象做清理工作。所以我们应该在不使用线程句柄的时候将其关闭掉,让这个线程的内核对象引用计数减1。
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata);voidmain(){
HANDLEhThread1;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);CloseHandle(hThread1);
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
cout<<"mainthreadisrunning"<<endl;//Sleep(10);}
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata){
cout<<"thread1isrunning"<<endl;return0;}
该程序运行后只输出了主线程在运行,当我们创建线程之后,对于主线程系统给它分配了时间片,它才能够运行,在它运行的时间段之内创建新线程,关闭句柄,输出,之后主线程就完成了,那么进程也就结束了,进程退出,在进程中所有资源包括没有执行的线程要退出。为了让新建的线程运行,就要让主线程暂停运行:
VOIDSleep(
DWORDdwMilliseconds//sleeptime);//该函数在指定时间间隔内线程暂停运行,以毫秒为单位。例:火车站售票系统#include<windows.h>#include<iostream.h>
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata);//声明DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata);inttickets=100;//总票数voidmain(){
HANDLEhThread1;HANDLEhThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(hThread1);CloseHandle(hThread2);Sleep(4000);//主线程睡眠4秒钟}
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata){
while(TRUE){
if(tickets>0)
{cout<<"thread1sellticket:"<<tickets--<<endl;}//线程1卖票后自减elsebreak;}
return0;}
DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata){
while(TRUE){
if(tickets>0)
{cout<<"thread2sellticket:"<<tickets--<<endl;}//线程2卖票后自减
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
elsebreak;}
return0;}
这个程序让两个线程在时间片内交替卖票,直到卖完为止,但还有一个潜在问题:当tickets为1时,在线程1进入if语句内时它的时间片到期了,线程1就会暂停,这时就会跳到线程2执行,线程2执行完后tickets变为了0,又跳回线程1if语句内,注意是语句内,这时就会卖出0这张票。要避免这个错误就要让一个线程内用到的资源在另一个线程内拒绝使用,就要用到互斥对象:HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,//SDBOOLbInitialOwner,//initialownerLPCTSTRlpName//objectname
);//SDK,该函数可以创建或打开一个命名的或没有名字的互斥对象。创建成功后会返回一个互斥对象句柄。第1个参数是指向SECURITY_ATTRIBUTES结构体的指针,在这里传递NULL使用默认的安全性。
第2个参数指示互斥对象初始的拥有者,如果值为真那么调用者创建互斥对象,调用的线程获得互斥对象的所有权,否则的话调用线程不获得互斥对象的所有权。
第3个参数指定互斥对象的名字,如为NULL创建的就是没有名字的互斥对象。
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。
ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。DWORDWaitForSingleObject(HANDLEhHandle,//handletoobjectDWORDdwMilliseconds//time-outinterval
);//SDK,要获得互斥对象用该函数,它当下面两种情况之一发生时返回:1.指定的对象处于有信号状态。2.超时的时间间隔流逝了。
它的第1个参数是一个对象的句柄,在我们的程序中传递的是互斥对象的句柄,一旦互斥对象变为有信号状态,函数就返回,如果互斥对象始终没有处于有信号状态,也就是处于未通知状态,那么该函数就会一直等待,函数等待就会导致线程暂停运行。第2个参数指示一个超时的时间间隔,以毫秒为单位,如果这个时间间隔流逝了,这个函数就返回,即使所等待的对象处于非信号状态,如果将这个参数设为0,那么这个函数测试对象的状态,然后立即返回,如果将这个参数设为INFINITE,那么这个函数的超时值永远不会发生,函数永远等待。DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata);DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata);intindex=0;inttickets=100;HANDLEhMutex;voidmain(){
HANDLEhThread1;HANDLEhThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(hThread1);CloseHandle(hThread2);
hMutex=CreateMutex(NULL,FALSE,NULL);
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
Sleep(4000);}
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata){
while(TRUE){
WaitForSingleObject(hMutex,INFINITE);//注意该函数要在循环内部调用if(tickets>0){
Sleep(1);
cout<<"thread1sellticket:"<<tickets--<<endl;}else
break;
ReleaseMutex(hMutex);//注意该函数要在循环内部调用,如果都放在外部所有票都会让线程1卖掉}
return0;}
DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata){
while(TRUE){
WaitForSingleObject(hMutex,INFINITE);if(tickets>0){
Sleep(1);
cout<<"thread2sellticket:"<<tickets--<<endl;}else
break;
ReleaseMutex(hMutex);}
return0;}
当我们创建互斥对象时第二个参数传递FALSE,当前就没有线程拥有互斥对象,操作系统就会将这个互斥对象设为已通知状态,也就是有信号状态。当进入第1个线程进入循环调用WaitForSingleObject(hMutex,INFINITE);因为这时互斥对象处于有信号状态,那么第一个线程就请求到了互斥对象,操作系统就会将这个互斥对象的线程ID设为第一个线程的线程ID,接下来得到这个互斥对象后操作系统会将这个互斥对象设为未通知状态,往下面运行调用Sleep暂停线程的运行,操作系统就会选择第二个线程运行,进入循环后调用WaitForSingleObject,因为这时互斥对象已经被第一个线程所拥有,这个互斥对象属于未通知的状态,第二个对象没有获得互斥对象的所有权,所以WaitForSingleObject这个函数就会处于等待状态,导致线程处于暂停执行,等线程1睡醒了继续执行卖出这张票,卖完票后应该释放互斥对象ReleaseMutex(hMutex);也就是让互斥对象处于已通知状态,当调用ReleaseMutex操作系统就会将互斥对象的线程ID设为0,然后设为已通知状态,如果这时轮到了第2个线程运行,这时该线程就得到互斥对象的所有权,线程ID也会设为第二个线程的线程ID。依次循环。
如果在创建互斥对象时将第2个参数设为TRUE,那么主线程就拥有了互斥对象,程序运行后线程1和2没有执行,所以要在主线程中释放互斥对象。还有一种情况就是在创建完互斥对象后调用WaitForSingleObject
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
请求互斥对象,那么线程1和2又不能运行了,因为主线程拥有了互斥对象后互斥对象计数器就会变为1,当请求互斥对象时,虽然是无通知状态,但判断出的线程ID是一样的,所以仍然能够请求到互斥对象,这时互斥对象的计数器就会变为2,后面调用ReleaseMutex(hMutex);后计数器变为1,因此互斥对象还是无信号状态,线程1,2就无法请求到互斥对象,因此还要调用一次ReleaseMutex(hMutex);使计数器变为0,变为有信号状态。
还有一种情况,将线程1,2改为:
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata){
WaitForSingleObject(hMutex,INFINITE);cout<<"thread1isrunning"<<endl;return0;}
在没有释放互斥对象的情况下,该程序依然将线程1,2都执行了,这是为什么呢?如果在线程中止前没有调用ReleaseMutex,操作系统一旦发现线程已经终止,会自动将线程拥有的互斥对象线程ID和计数器归为0,因为操作系统维护了线程的信息和线程相关的互斥对象的信息,所以它知道哪一个线程终止了。这个时候的线程2就可以得到互斥对象了,但我们怎么知道是正常得到互斥对象还是等到线程终止后才得到了互斥对象呢?可以通过WaitForSingleObject的返回值判断:
如果请求的互斥对象变为有信号状态,该函数就返回WAIT_OBJECT_0。
如果请求的互斥对象变为有信号状态是因为线程异常终止或终止前未调用ReleaseMutex那么返回WAIT_ABANDONED
通过这个返回值我们就知道请求的互斥对象是如何获得的所有权。经常遇到这样的程序,当打开一个实例后,然后多次打开该程序,那么就会将先前运行的实例激活带到前台来,这种功能是怎么实现的呢?可以用命名的互斥对象:
CreateMutex返回值:如果函数成功,返回值是互斥对象的句柄,如果命名的互斥对象在函数调用之前已经存在,这个函数会返回已经存在的对象的句柄,然后调用GetLastError返回ERROR_ALREADY_EXISTS,否则调用者创建互斥对象。也就是说判断GetLastError的返回值是不是ERROR_ALREADY_EXISTS,如果是则表示已经创建过互斥对象,也就是已经打开了一个程序实例,如果返回的不是这个值则是第一次运行程序。DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata);DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata);intindex=0;inttickets=100;HANDLEhMutex;voidmain(){
HANDLEhThread1;HANDLEhThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(hThread1);CloseHandle(hThread2);
hMutex=CreateMutex(NULL,TRUE,"tickets");//创建命名的互斥对象if(hMutex)//判断如果句柄有值{
if(ERROR_ALREADY_EXISTS==GetLastError())//如果先前已经创建过{
cout<<"onlyinstancecanrun!"<<endl;
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
return;//如果先前已经创建过直接返回,不再往下运行}}
WaitForSingleObject(hMutex,INFINITE);ReleaseMutex(hMutex);ReleaseMutex(hMutex);Sleep(4000);}
DWORDWINAPIFun1Proc(LPVOIDlpParameter//threaddata){
WaitForSingleObject(hMutex,INFINITE);cout<<"thread1isrunning"<<endl;return0;}
DWORDWINAPIFun2Proc(LPVOIDlpParameter//threaddata){
WaitForSingleObject(hMutex,INFINITE);cout<<"thread2isrunning"<<endl;return0;}
程序运行后再次打开时就会出现一个提示然后返回了。
=====================================================================================多线程创建网络聊天室程序:
新建一个对话框工程Chat,将上面的控件清空,添加编辑框ID改为
IDC_EDIT_RECV做为接收数据,然后摆放一个IP地址控件,再摆放一个编辑框ID为IDC_EDIT_SEND,最后摆一个Button,ID改为IDC_BTN_SEND做为发送按钮,如下图:
BOOLAfxSocketInit(WSADATA*lpwsaData=NULL);该函数有一个参数是指向WSADATA结构体的指针,在这个函数内部,会帮助我们去加载1.1版本的套接字库。这个函数还有一个好处,在我们的程序结束之前调用::WSACleanup终止套接字库的使用。MFC中声明这个函数调用要放在CWinApp::InitInstance中。BOOLCChatApp::InitInstance(){
if(!AfxSocketInit())//加载套接字库
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
{
AfxMessageBox("加载套接字库失败!");
returnFALSE;//如果加载失败弹出提示并返回假使程序结束。}
调用该函数要包含它的头文件Afxsock.h,我们可以把它放到FileView的StdAfx.h这个文件中。StdAfx.h是预编译头文件,在这个头文件中包含了MFC程序运行的一些必要的头文件。对于所有的MFC程序来说,它们第一个要包含的头文件就是StdAfx.h这个预编译头文件。
接下来在CChatDlg中定义一个函数做套接字库本身的初始化工作,右键添加成员函数,BOOL型,函数名InitSocket。然后在这个类中再增加一个成员变量,这是一个套接字描述符,类型SOCKET,变量名m_socket。
BOOLCChatDlg::InitSocket(){
m_socket=socket(AF_INET,SOCK_DGRAM,0);//创建基于UDP数据报套接字if(INVALID_SOCKET==m_socket)//socket函数如果创建失败返回INVALID_SOCKET{
MessageBox("套接字创建失败!");returnFALSE;}
SOCKADDR_INaddrSock;//地址结构体变量,作为接收端要绑定地址和端口addrSock.sin_family=AF_INET;//地址族addrSock.sin_port=htons(9000);//端口
addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//IP
intretval;
retval=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));//绑定if(SOCKET_ERROR==retval)//没有错误bind返回0,否则返回SOCKET_ERROR{
closesocket(m_socket);MessageBox("绑定失败!");returnFALSE;}
returnTRUE;
}//这就是套接字初始化的工作,在OnInitDialog()中调用一下该函数初始化的工作就完成了。//OnInitDialog()是对对话框初始化的函数。
下面编写接收端程序:
当我们在接收数据的时候,如果没有数据到来,recvfrom这个函数会阻塞,从而导致程序暂停运行,所以我们把接收数据这个操作放到一个单独的线程当中去完成。创建线程用CreateThread,我们要给这个线程传递两个参数,一个是我们创建的套接字,一个是对话框的句柄或者接收编辑框的句柄,这样在这个线程中当我们接收到数据之后可以将这个数据传回给对话框或者编辑框,经过处理之后显示在接收的编辑框上。但是CreateThread只有第4个参数能传递参数,这个参数是一个指针,既然是指针就可以指向变量,也可以指向对象,因此我们将这两个参数放到一个结构体中,在CChatDlg的头文件中我们定义这个结构体:
structRECVPARAM{
SOCKETsock;HWNDhwnd;};
超详细孙鑫VC++图文笔记14-20课,相当于一部精简教程
然后在OnInitDialog中:(注意放在InitSocket()的后面)
RECVPARAM*pRecvParam=newRECVPARAM;pRecvParam->sock=m_socket;pRecvParam->hwnd=m_hWnd;//结构体参数初始化
HANDLEhThread=CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL);//创建线程CloseHandle(hThread);
然后写线程函数,首先将线程函数的声明放到OnInitDialog函数的上面,写成一个全局的函数,如果不用全局函数,也可以将函数定义为CChatDlg类的成员函数,但是一定要是静态的成员函数,因为一个类的函数做为参数被类的另一个函数调用,要首先创建一个对象才可以,如设为静态成员函数,该函数只属于类本身:staticDWORDWINAPIRecvProc(LPVOIDlpParameter);,下面看函数的定义:DWORDWINAPICChatDlg::RecvProc(LPVOIDlpParameter){
SOCKETsock=((RECVPARAM*)lpParameter)->sock;HWNDhwnd=((RECVPARAM*)lpParameter)->hwnd;deletelpParameter;//视频讲述时,遗忘了释放内存的操作。sunxin
SOCKADDR_INaddrFrom;intlen=sizeof(SOCKADDR);
//地址结构体,用来接收发送端的地址信息
//用来接收返回的地址结构体长度,一定要初始化
charrecvBuf[200];//用来保存接收的数据chartempBuf[300];//用来存放格式化后的数据intretval;while(TRUE)//在循环中不断的接收数据,也是为了让接收线程不断的运行下去{
retval=recvfrom(sock,recvBuf,200,0,(SOCKADDR*)&addrFrom,&len);
//如果没有错误发生,recvfrom返回接收到的字节数,出错返回SOCKET_ERRORif(SOCKET_ERROR==retval)
break;//如果错误终止循环
sprintf(tempBuf,"%s说:%s",inet_ntoa(addrFrom.sin_addr),recvBuf);//格式化::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);//发送消息将数据传给对话框}//发送自定义消息应该是静态函数的原因,不能对对话框实例操作。return0;
}
然后在CChatDlg的头文件中定义这个消息的值:#defineWM_RECVDATAWM_USER+1然后是消息响应函数原型的声明:afx_msgvoidOnRecvData(WPARAMwParam,LPARAMlParam);然后在构造函数中消息映射:ON_MESSAGE(WM_RECVDATA,OnRecvData)最后是消息响应函数的实现:
voidCChatDlg::OnRecvData(WPARAMwParam,LPARAMlParam){
CStringstr=(char*)lParam;//取出数据CStringstrTemp;//用来保存旧的数据GetDlgItemText(IDC_EDIT_RECV,strTemp);//获得编辑框文本保存到strTempstr+="";//每次接收到数据都要换行str+=strTemp;//将数据加上旧的数据,以显示聊天的记录SetDlgItemText(IDC_EDIT_RECV,str);//将所有数据在编辑框显示}
正在阅读:
孙鑫VC++图文笔记14-20课05-21
专业技术职务评审申报材料目录07-25
企业合伙终止协议书范本(律师审核备注版)04-14
幼儿文学练习题以及答案07-02
火车站点售票时间05-28
美丽中国与生态文明建设 满分09-14
电气自动化专业面试最常见的个问题11-16
对商品价格的影响08-19
2012诗歌鉴赏讲座 师大附中张海波08-21
- 教学能力大赛决赛获奖-教学实施报告-(完整图文版)
- 互联网+数据中心行业分析报告
- 2017上海杨浦区高三一模数学试题及答案
- 招商部差旅接待管理制度(4-25)
- 学生游玩安全注意事项
- 学生信息管理系统(文档模板供参考)
- 叉车门架有限元分析及系统设计
- 2014帮助残疾人志愿者服务情况记录
- 叶绿体中色素的提取和分离实验
- 中国食物成分表2020年最新权威完整改进版
- 推动国土资源领域生态文明建设
- 给水管道冲洗和消毒记录
- 计算机软件专业自我评价
- 高中数学必修1-5知识点归纳
- 2018-2022年中国第五代移动通信技术(5G)产业深度分析及发展前景研究报告发展趋势(目录)
- 生产车间巡查制度
- 2018版中国光热发电行业深度研究报告目录
- (通用)2019年中考数学总复习 第一章 第四节 数的开方与二次根式课件
- 2017_2018学年高中语文第二单元第4课说数课件粤教版
- 上市新药Lumateperone(卢美哌隆)合成检索总结报告
- 孙鑫
- 图文
- 笔记
- VC
- 14
- 20
- 土壤中微生物的分离与纯化
- 管理学复习提纲_
- 怎样引导一年级孩子写日记
- 【部编】山西省晋中市太谷县2021-2021学年三年级上学期数学期末试卷
- 上海小区安全技术防范系统要求(2010版)
- 《地基与基础验收规范》 GB50202-2002
- 关于整合CRM和ERP系统的研究
- 煤矿掘进支护工培训试题库 (1)
- 2010年全球PCB市场总结及其未来发展趋势
- 上课专题二农业区位选择
- 低丘缓坡、优质耕地工作流程
- Circumnuclear HI disks in radio galaxies The case of Cen A and B2 0258+35
- 歌词(附假名)
- 最新教师招聘笔试教育学各章知识点整理总结(山香终结版)
- 住院早产儿387例并发症临床分析
- “清网行动”贵在走群众路线
- 填料塔气体传质膜系数测定实验的数据处理
- 近五年复方丹参片的研究进展
- 药房工作管理制度
- IQC检验员绩效考核量表