我是靠谱客的博主 无聊未来,这篇文章主要介绍VC++深入详解(12):网络编程,现在分享给大家,希望可以做个参考。

这一小节介绍网络编程。首先我们介绍一下计算机网络的基本知识,然后着重介绍一下Windows Socket程序的编写。
首先,介绍几个基本概念。什么是计算机网络?它是相互连接的独立自主的计算机的集合。它们是如何通信的呢,需要一个东西来表明我要跟哪个计算机进行通信,在网络上,为每个计算机分配了一个“IP”地址,通过地址来找到想要通信的计算机。具体的通信是计算机的某个程序实现的,一台计算机可能同时有多个程序在使用网络。为了区分它们,为每个程序提供了一个“端口号”来标识自己。具体通信时所发送的内容,成为协议,它规定了我们发送的格式:除了发送的内容以外,还包括这些东西是谁发送的,要发给谁,总共有多长等等。而信息具体是如何从一台主机,发送到另一台主机,则会有很大的不确定性:
不同的通信媒介:是通过有线传输的,还是无线网络?
不同的操作系统:Unix、Windows
不同的应用环境:移动、固定
不同的业务类型:对时延的要求、对差错控制的要求。
等等,使得实际中的相互通信的网络异常复杂,如何解决这个问题呢?国际标准化组织(ISO)提出了OSI(Open System Interconnected)七层参考模型,将网络按照不同的功能划分为7层:

应用层
表示层
会话层
传输层
网络层

数据链路层

物理层


它们的功能简述如下:
物理层:提供二进制传输,确定在通信信道上如何传输比特流。一条物理信道上所能传送的信息的最快速度是有限制的,不是我们想传送多块就能传送多快的。为了对抗复杂的传输环境(这一点在无线通信中尤其明显),物理层通常要使用非常复杂的调制、编码技术,来在在一定差错容忍度的前提下,尽可能的多发送。
数据链路层:提供介质访问,增强物理层的传输功能,建立一条无差错的传输线路。比如对于收到物理层发送过来的数据,需要通过确认请求或者简单的差错控制编码(比如奇偶效验)来判断这一帧数据是否有错误,如果错误了,则通知发送发重新发送。
网络层:IP寻址和路由。因为网络上的数据可以经由多条路线到达目的地,所以其中要考虑路由算法、拥塞控制等问题。
传输层:为源端主机到目的端主机提供可靠的数据传输服务,隔离网络的上下层协议,是得网络应用于下层协议无关。也就是应用程序与应用程序之间的连接。
会话层:两个相互通信的应用进程之间建立、组织和协调其相互之间的通信。比如电影里使用对讲机时,一句话说完后总要加一句“over”。
表示层:被传送的数据如何表示。
应用层:用户所提供的服务。
要注意,这7层模型是功能上的划分,并不是具体一定要有这七层。
下面介绍一下应用层、传输层和网络层的常见协议(这是笔试题中常考的):
应用层协议:远程登录协议(Telnet)、文件传输协议(FTP)、超文本传输协议(HTTP)、域名服务(DNS)、简单哟见传输协议(SMTP)、邮局协议(POP3)等。
传输层:传输控制协议(TCP)、用户数据报协议(UDP)。
这两个协议值得仔细说说。
TCP协议是面向连接的,也就是说,在双方通信之前,已经安排好了一条通信线路(不管它具体是什么)共他俩使用,别人不能使用,等他俩通信结束后,需要释放这条线路。TCP是通过3次握手建立的:
1.客户端给服务器发送SYN(syn = j)包,进入SYN_SEND状态。
2.服务器接收到SYN包,确认客户的SYN(ack = j+1),同时自己也发送一个SYN包(syn = k),把它俩都发送出去,服务器进入SYN_RECV状态。
3.客户端收到服务器的SYN+ACK包,向服务器发送ACK(ack = k+1)。客户端和服务器都进入ESTABLISHED状态。此时,连接已经建立完毕,可以发送消息了。
上面只是正常的建立连接过程,其中的任何一步都有可能失败,至于失败以后的操作,这里就不细说了。
下面再看看UDP协议:这是一种无连接的、不可靠的协议。这意味着可能向对方发送的消息时对方无法接到。或者,向一个根本不存在的IP地址或者端口发送消息。既然是不可靠的,为什么还要使用它呢?因为UDP协议不需要建立连接,没有数据重传,所以实时性较高。比如我们看视频时,一两个像素的错误我们根本不会发觉。
网络层:网际协议(IP),Internet互联网报文控制协议(ICMP)、Internet组管理协议IGMP。


由于7层模型在使用起来很不方便,实际中应用更广泛的是TCP/IP模型:


应用层
传输层
网络层

网络接口层


它与OSI七层模型的对应关系如下:网络接口层对应于物理层和数据链路层,网络层对应于网络层,传输层对应于传输层,应用层对应于会话层、表示层、应用层。
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机、服务器模式:客户向服务器发送请求,服务器收到请求后,提供相应的服务。为什么这么设计呢?首先,建立网络的原因是因为网络中软硬件资源、运算能力和信息分布不均,需要共享,从而拥有资源多的主机提供服务,资源少的客户请求服务。其次,网间进程通信完全是异步的,相互通信的进程间即不存在父子关系,又不存在共享的内存缓冲区,需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步。
它们的通信过程如下:
服务器先运行:
1.打开一个通信通道并告知本地主机,他愿意在某一地址和端口上接收客户的请求。
2.等待客户请求到达端口。
3.接收到重复服务请求没处理该请求并发送应答信号。收到并发服务的请求,要激活一个新的进程或者线程来处理这个客户请求。新的进程或者线程处理此客户的请求,并不需要对其他请求作出相应。等服务完成后,关闭新进程与客户的通信链路,并终止。
4.返回第二步,等待另一请求。
5.关闭服务器。


客户端:
1.打开一个通信通道并连接到服务器所在的主机的特定端口。
2.向服务器发送服务请求报文,等待接收应答,继续提出请求。
3.请求结束后关闭通信通道并终止。


讲了这么多,我们可以隐约感觉到,网络编程是一件很麻烦的事情,为了方便的开发应用软件程序,美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统套接字(socket),是得程序员可以很方便的访问TCP/IP协议,从而开发各种网络应用程序。后来,socket又引入到windows操作系统。我们先介绍与之相关的函数,然后给出几个例子:
1.使用WSAStartup进行版本协商。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested指明了版本号。高字节是副版本,低字节是主版本,可以通过MAEWORD宏来获得。lpWSAData是用来返回值的,这个函数会把它加载的版本信息填到这个结构里面。具体的使用方法可以参照MSDN给出的例子。
2.使用WSACleanup()来释放为应用程序分配的资源,与WSAStartup相对应。
3.使用socket函数来创建套接字:
SOCKET socket(  int af, int type,int protocol  );
af参数是地址族,对于TCP/IP协议,它只能是AF_INET。
type指明了socket的类型,对于1.1版本的socket,他只接受两种类型:SOCK_STREAM、SOCK_DGRAM
protocol指明了与特定地址家族相关的协议,如果为0,则根据地址格式和套接字类别,自动选择一个合适的协议。
如果函数调用成功,则会返回一个SOCK数据类型的套接字描述符:如果调用失败,则返回一个INVALID_SOCKET值。
4.使用bind函数将套接字绑定到本地的某个地址和端口上:
int bind(  SOCKET s, const struct sockaddr FAR *name,int namelen);
s指定要绑定的套接字,name是一个指向sockaddr结构体类型的指针,这个结构体表明了本地信息。
struct sockaddr 
{
  u_short    sa_family;
  char       sa_data[14];
}; 
由于这个结构是为所有地址族准备的,所以不同的协议会有一定的区别,所以用第三个参数指明结构的长度。
再回过头来看第二个参数,第一成员指明了地址族,第二个成员是14个字节的内存区域,对于不同的协议,有不同的内容。对于TCP/IP协议,使用sockaddr_in 结构来替换sockaddr结构:
struct sockaddr_in 
{
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};


sin_family指明了地址族,应使用AF_INET;sin_port指明了端口号;sin_addr指明了主机的IP地址;sin_zero则只是为了填充字节,是得sockaddr_in 与sockaddr长度相同。其中sin_addr又是一个结构体,定义如下:
typedef struct in_addr 
{
union 
{
struct
{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct 
{
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR


这个结构其实是一个联合体,通常我们都是将点分十进制的IP地址转换为u_long类型,并赋值给S_addr成员。
一般情况下,我们可以使用INADDR_ANY允许套接字向任何分配本地机器的IP地址发送或者接收数据。这个参数的实际意义在于,一般情况下,一台主机只有一个IP地址,但如果主机有两个网卡,那么他会有两个IP地址,如果我们只想使用其中的一个供套接字使用,我们可以使用inet_addr函数将本地的IP地址(点分十进制字符串形式),转化为unsigned long并赋给S_addr;与之相反的转化为inet_ntoa函数,将S_addr转化为点分十进制,供打印输出使用。
5.listen函数将指定的套接字设置为监听模式。
int listen(  SOCKET s, int backlog  );
s 为指定的套接字;backlog为等待队列的最大长度。
6.使用accept函数来接受客户端的连接请求
SOCKET accept(  SOCKET s,  struct sockaddr FAR *addr,  int FAR *addrlen);
s为已被设置为监听模式的套接字;addr为指向客户端的sockaddr的地址,通过函数来获取值;addrlen为地址长度。
6.通过send函数发送消息
int send(SOCKET s,const char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf为要发送消息的地址,len为消息长度,flags一般设为0即可。
7.通过recv函数获取消息:
int recv(SOCKET s,char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf用来保存接收的数据,len表示缓冲区的长度,flags一般填0即可。
8.使用connect函数与特定的套接字连接
int connect(  SOCKET s,const struct sockaddr FAR *name,int namelen);
s为即将建立连接的那个套接字;name指定了服务器端的地址信息,s为地址信息长度。
9.使用recvfrom接受消息
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR *from,int FAR *fromlen);
s为准备接受数据的套接字,buf为接收数据的缓冲区,len为缓冲区的长度,flag一般填0,from指针用来存储发送方的地址信息,fromlen为地址的长度。
10.使用sendto向一个特定的目的方发送数据。
int sendto(  SOCKET s,const char FAR *buf,int len,int flags,const struct sockaddr FAR *to,int tolen );
s为套接字,buf为发送的数据的地址,len为数据的长度,flags一般为0,to指针指向目标套接字的地址,tolen为地址的长度
11.字节序的转换函数。
首先先搞清楚什么是字节序。一般情况下,我们使用电脑都是低位在前、高位在后的,这被称为小端字节序;而网络传输时使用的是低位在前,高位在后的大端字节序。如果不进行转化一个16为数据0X1234就被网络认为是0X3412了。转换的函数有两个:
u_short htons(  u_short hostshort  );将一个16位数转换为网络字节序
u_long htonl(  u_long hostlong  );将一个32位数转换为网络字节序


介绍完函数,我们就先举一个利用TCP协议编写的简单的网络通信的例子。我们先看一下基本步骤:
服务器端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.将套接字设为监听状态(listen)。
4.接受客户端的发送请求(accept)。
5.发送或者接收数据(send/recv)。
6.关闭套接字(closesocket),一次通信结束。
7.转4.


客户端端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.连接到服务器(connect)。
4.发送或者接收消息(send/recv)。
5.关闭套接字(closesocket)。
6.释放资源(WSACleanup)。

再看我们的程序:

//服务器端程序
#include <Winsock2.h>
#include <stdio.h>


int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}
	
	//创建套接字
	SOCKET socksrv = socket(AF_INET,SOCK_STREAM,0);

	//填写本地信息
	SOCKADDR_IN addrSrv;
	//本机IP地址
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	//协议族
	addrSrv.sin_family = AF_INET;
	//端口信息,必须使用1024以上,注意字节序转换
	addrSrv.sin_port = htons(6000);
	
	//绑定套接字
	bind(socksrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

	//设为监听模式
	listen(socksrv,5);

	//客户端地址信息结构体
	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while(1)
	{
		//接受客户端请求,返回值为已经建立连接的SOCKET
		SOCKET sockConn = accept(socksrv,(SOCKADDR*)&addrClient,&len);
		//存储数据的缓冲区
		char sendBuf[100];
		//将数据格式化到缓冲区中
		sprintf(sendBuf,"Welcom %s to the Service!",
			inet_ntoa(addrClient.sin_addr));
		//发送数据
		send(sockConn,sendBuf,strlen(sendBuf)+1,0);
		//接收数据的缓冲区
		char recvBuf[100];
		//接收数据
		recv(sockConn,recvBuf,100,0);
		printf("%sn",recvBuf);
		//关闭套接字
		closesocket(sockConn);

	}
	//由于服务器一直处理工作状态,所以不调用WSACleanup释放资源
	return 0;
}

//客户端程序
#include <Winsock2.h>
#include <stdio.h>


int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}

	//创建套接字
	SOCKET sockClient = socket(AF_INET,SOCK_STREAM,0);
	//填写服务器信息
	SOCKADDR_IN addrSrv;
	//连接地址为回环地址
	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));
	
	//接收数据缓冲区
	char revcBuf[100];
	//接收服务器的数据
	recv(sockClient,revcBuf,100,0);
	//打印数据
	printf("%s",revcBuf);
	
	//发送数据缓
	send(sockClient,"hello",strlen("hello")+1,0);
	closesocket(sockClient);

	//释放资源
	WSACleanup();


	return 0;
}
下面我们再看看基于UDP编写的客户端/服务器的应用程序的步骤:
服务器:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.绑定套接字(bind)。
4.接收或者发送消息(recvfrom/sendto)。
5.关闭套接字(closesocket)。
6.释放资源(WSACleanup)。
客户端:
1.进行版本协商(WSAStartup)。
2.创建套接字(socket)。
3.发送或接收消息(sendto/recvfrom)。
4.关闭套接字(closesocket)。
5.释放资源(WSACleanup)。


对应的程序如下:

//服务器程序
#include <Winsock2.h>
#include <stdio.h>

int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}
	//创建套接字
	SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
	//填写服务器信息
	SOCKADDR_IN addrSrv;
	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_IN addrClient;
	int len = sizeof(addrClient);
	char recvBuf[100];
	//接收数据
	recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);
	printf("%sn",recvBuf);
	
	//关闭套接字
	closesocket(sockSrv);
	//释放资源
	WSACleanup();
	return 0;
}

//客户端程序
#include <Winsock2.h>
#include <stdio.h>


int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}
	
	//创建套接字
	SOCKET sockClient = socket(AF_INET,SOCK_DGRAM,0);

	//填写地址信息
	SOCKADDR_IN addrSrv;
	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();
	return 0;
}

感觉要简单许多,这里稍微总结一下TPC与UDP编写程序的区别:

TCP服务器端要先设定为监听(listen)模式,然后要等待响应(accept),并且利用响应的套接字进行发送(send)和接受(recv)。由于是面向连接的方式,所以发送和接受时都不需要指明地址信息。而客户端要连接到服务器(connect)。而UDP的过程相对简单,没有监听、连接、响应的过程,直接可以使用sendto和recvfrom来发送和接收消息。由于UDP是面向非连接的,所以要在参数中指明要发送或者接收的对象。


最后,我们利用UDP协议实现一个简单的聊天程序。为什么时候UDP协议?因为聊天时候,稍微错一点不要紧,可以通过上下文、语义,甚至让对法重新发送来克服;而且,聊天时经常出现一个屌丝向一个女神搭讪,但是女神迟迟不回复的情况,这时是应该关闭连接呢,还是应该保持连接,让它们继续占用资源?这是一个很纠结的问题。相比之下,UDP协议的优势就体现出来了。
再考虑一个问题,如何结束聊天?可以通过发送一个特定的字符来实现,当服务器收到这个特定的字符时,向客户端也发送这个字符,并且关闭自己的套接字;同理客户端如果收到这个字符,也可以向服务器发送这个字符(尽管可能此时服务器的套接字已经关闭了),并关闭自己的套接字。


想清楚了这两点,我们就开始编写程序吧:

//服务器程序
#include <Winsock2.h>
#include <stdio.h>

int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}

	SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
	SOCKADDR_IN addrSrv;
	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(addrSrv));

	char recvBuf[100];
	char sendBuf[100];
	char tempBuf[100];

	SOCKADDR_IN addClient;
	int len = sizeof(SOCKADDR);
	while(1)
	{
		//接收数据
		recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addClient,&len);
		//判断对话是否应该被终止
		if('#' == recvBuf[0])
		{
			sendto(sockSrv,"#",strlen("#")+1,0,(SOCKADDR*)&addClient,len);
			printf("chat end!n");
			break;
		}

		//打印接收的数据
		sprintf(tempBuf,"%s says: %s",inet_ntoa(addClient.sin_addr),recvBuf);
		printf("%sn",tempBuf);

		//发送数据
		printf("please input data:n");
		gets(sendBuf);
		sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addClient,len);

	}

	closesocket(sockSrv);
	WSACleanup( );
	return 0;
}

//客户端程序
#include <Winsock2.h>
#include <stdio.h>


int main()
{
	//进行版本协商
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	
	wVersionRequested = MAKEWORD( 1, 1 );

	err = WSAStartup( wVersionRequested, &wsaData );
	if ( err != 0 )                             
		return -1;
	
	
	if ( LOBYTE( wsaData.wVersion ) != 1 ||
         HIBYTE( wsaData.wVersion ) != 1 ) 
	{

		WSACleanup( );
		return -1; 
	}

	//创建套接字
	SOCKET sockClient = socket(AF_INET,SOCK_DGRAM,0);

	//本地信息
	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);

	char recvBuf[100];
	char sendBUf[100];
	char tempBuf[100];

	int len = sizeof(SOCKADDR);
	while(1)
	{
		//客户端线发送数据:

		//输入发送数据
		printf("please input data:n");
		gets(sendBUf);
		//发送数据
		sendto(sockClient,sendBUf,100,0,(SOCKADDR*)&addrSrv,len);
		//接收数据
		recvfrom(sockClient,recvBuf,100,0,(SOCKADDR*)&addrSrv,&len);
		//判断对话是否终止
		if('#' == recvBuf[0])
		{
			sendto(sockClient,"#",strlen("#")+1,0,(SOCKADDR*)&addrSrv,len);
			printf("chat over!");
			break;
		}
		
		//打印接收的数据
		sprintf(tempBuf,"%s say: %s",inet_ntoa(addrSrv.sin_addr),recvBuf);
		printf("%s",tempBuf);

	}
	closesocket(sockClient);
	WSACleanup( );
	return 0;
}




最后

以上就是无聊未来最近收集整理的关于VC++深入详解(12):网络编程的全部内容,更多相关VC++深入详解(12)内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部