基于vs实现的socket—udp通信实例详解

文章目录

  • 一、UDP协议
  • 二、vs 实现udp通信
    • 2.1 服务器端编程的步骤
    • 2.2 客户端编程的步骤
    • 2.3 udp通信图解
  • 三、知识分解
    • 3.1 pragma comment(lib,"Ws2_32.lib")
    • 3.2 WASDATA
    • 3.3 MAKEWORD(a, b)
    • 3.4 WSAStartup(sockVersion, &wsadata)
    • 3.5 sockaddr结构
    • 3.6 c_str
    • 3.7 memset
    • 3.8 recvfrom
    • 3.9 sendto
  • 四、实战篇——代码分析
    • 4.1 服务端
    • 4.2 客户端
    • 4.3 通信
    • 4.4 发送结束标志
    • 4.5 问题error C4996

一、UDP协议

  • udp是一种面向无连接,不可靠的传输层协议
  • upd的连接过程
    基于vs实现的socket—udp通信实例详解_第1张图片

二、vs 实现udp通信

2.1 服务器端编程的步骤

  1. 创建套接字(socket)
  2. 将套接字和IP地址、端口号绑定在一起(bind)
  3. 等待客户端发起数据通信(recvfrom/recvto)
  4. 关闭套接字

2.2 客户端编程的步骤

  1. 创建套接字(socket)
  2. 向服务器发起通信(recvfrom/recvto)
  3. 关闭套接字

2.3 udp通信图解

基于vs实现的socket—udp通信实例详解_第2张图片

三、知识分解

  • 在vs中一般使用Winsock2实现网络通信功能,所以需要引进头文件winsock2.h和库文件"ws2_32.lib"

    1. WinSock2 是连接系统和用户使用的软件之间用于交流的一个接口,这个功能就是修复软件与系统正确的通讯的作用。
    2. Winsock2 SPI(Service Provider Interface)服务提供者接口建立在Windows开放系统架构WOSA(Windows Open System Architecture)之上,是Winsock系统组件提供的面向系统底层的编程接口。

    Winsock系统组件向上面向用户应用程序提供一个标准的API接口;向下在Winsock组件和Winsock服务提供者(比如TCP/IP协议栈)之间提供一个标准的SPI接口。

    各种服务提供者是Windows支持的DLL,挂载在Winsock2 的Ws2_32.dll模块下。

    对用户应用程序使用的Winsock2 API中定义的许多内部函数来说,这些服务提供者都提供了它们的对应的运作方式(例如API函数WSAConnect有相应的SPI函数WSPConnect)。

    多数情况下,一个应用程序在调用Winsock2 API函数时,Ws2_32.dll会调用相应的Winsock2 SPI函数,利用特定的服务提供者执行所请求的服务。

    详细参考

  • Windows下的库文件目录(以便以后使用[Windows10]):

    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86

3.1 pragma comment(lib,“Ws2_32.lib”)

表示链接Ws2_32.lib这个库。
和在工程设置里写上链入Ws2_32.lib的效果一样,不过这种方法写的程序别人在使用你的代码的时候就不用再设置工程settings了

告诉连接器连接的时候要找ws2_32.lib,这样你就不用在linker的lib设置里指定这个lib了。

ws2_32.lib是winsock2的库文件。
WinSock2就相当于连接系统和你使用的软件之间交流的一个接口,可能这个功能就是修复软件与系统正确的通讯的作用。

3.2 WASDATA

WSADATA,一种数据结构。这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据。它包含Winsock.dll执行的数据。

定义位置:Winsock.h

	结构原型: 
	typedef struct WSAData {
	        WORD                    wVersion;   
	        WORD                    wHighVersion;
	#ifdef _WIN64
	        unsigned short          iMaxSockets;
	        unsigned short          iMaxUdpDg;
	        char FAR *              lpVendorInfo;
	        char                    szDescription[WSADESCRIPTION_LEN+1];
	        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
	#else
	        char                    szDescription[WSADESCRIPTION_LEN+1];
	        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
	        unsigned short          iMaxSockets;
	        unsigned short          iMaxUdpDg;
	        char FAR *              lpVendorInfo;
	#endif
	} WSADATA,  FAR * LPWSADATA;
	
	各参数的含义: https://baike.baidu.com/item/WSADATA

3.3 MAKEWORD(a, b)

makeword是将两个byte型合并成一个word型,一个在高8位(b),一个在低8位(a)

返回值:一个无符号16位整形数。

MAKEWORD(1,1)和MAKEWORD(2,2)的区别在于:前者只能一次接收一次,不能马上发送,而后者能。

声明调用不同的Winsock版本:

例如:
MAKEWORD(2,2)就是调用2.2版,MAKEWORD(1,1)就是调用1.1版。

不同版本是有区别的,例如1.1版只支持TCP/IP协议,而2.0版可以支持多协议。

2.0版有良好的向后兼容性,任何使用1.1版的源代码、二进制文件、应用程序都可以不加修改地在2.0规范下使用。

此外winsock 2.0支持异步 1.1不支持异步.

宏定义:#define MAKEWORD(a, b)  ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
返回值:typedef unsigned short      WORD;
参考: https://blog.csdn.net/happy_xiahuixiax/article/details/72637370

3.4 WSAStartup(sockVersion, &wsadata)

WSAStartup,即WSA(Windows Sockets Asynchronous,Windows异步套接字)的启动命令。是Windows下的网络编程接口软件Winsock1 或 Winsock2 里面的一个命令。

WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。

	int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
	⑴ wVersionRequested:一个WORD(双字节)型数值,在最高版本的Windows Sockets支持调用者使用,高阶字节指定小版本(修订本),低位字节指定主版本号。
	⑵ lpWSAData: 指向WSADATA数据结构的指针,用来接收Windows Sockets 实现的细节。
	WindowsSockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
  
  参考:https://baike.baidu.com/item/WSAStartup/10237703?fr=aladdin

3.5 sockaddr结构

 truct sockaddr
 {
      unsigned short    sa_family;             /*addressfamily,AF_xxx*/
      char              sa_data[14];           /*14bytesofprotocoladdress*/
 } ;
sa_family: 是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族。
sa_data:14字节协议地址。

此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构(在WinSock2.h中定义):

	struct sockaddr_in {
		        short   sin_family;
		        u_short sin_port;
		        struct  in_addr sin_addr;
		        char    sin_zero[8];
	};
	sin_family指代协议族,在socket编程中只能是AF_INET
	sin_port存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留。
	sin_addr存储IP地址,使用in_addr这个数据结构
	sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向sockaddr的结构体,并代替它。也就是说,你可以使用sockaddr_in建立你所需要的信息,然后用memset函数初始化: memset((char*)&mysock,0,sizeof(mysock));//初始化

参考:https://baike.baidu.com/item/SOCKADDR_IN

3.6 c_str

c_str是Borland封装的String类中的一个函数,它返回当前字符串的首字符地址。当需要打开一个由用户自己输入文件名的文件时,可以这样写:ifstream in(st.c_str())。

vc++2017应该这样用:
    	char c[20];
	    string s="1234";
	    strcpy(c,s.c_str());
这样才不会出错,c_str()返回的是一个临时指针,不能对其进行操作.

c_str()返回的是一个分配给const char的地址,其内容已设定为不可变更,如果再把此地址赋给一个可以变更内容的char变量,就会产生冲突。但是如果放入函数调用,或者直接输出,因为这些函数和输出都是把字符串指针作为 const char*引用的,所以不会有问题。


c_str() 以const char* 类型返回 string 内含的字符串。如果一个函数要求char*参数,可以使用c_str()方法:

string s = "Hello World!";
printf("%s", s.c_str()); //输出 "Hello World!"

c_str在打开文件时的用处:
当需要打开一个由用户自己输入文件名的文件时,可以这样写:ifstream in(st.c_str());。其中st是string类型,存放的即为用户输入的文件名。

参考:https://baike.baidu.com/item/c_str/2622670

3.7 memset

基于vs实现的socket—udp通信实例详解_第3张图片
void *memset(void *s, int ch, size_t n);

  • 函数解释
    将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
  • 作用
    在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 。
  • 函数原型
    extern void *memset(void *buffer, int c, int count) 
             buffer:为指针或是数组,`
             c:是赋给buffer的值,
             count:是buffer的长度.
    

    参考:https://baike.baidu.com/item/memset/4747579?fr=aladdin

3.8 recvfrom

	
recvfrom(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, //接收数据的缓冲区     
    _In_ int len,                                                                 //缓冲区的大小
    _In_ int flags,                                                               //标志位,调用操作方式
    _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,     //sockaddr结构地址
    _Inout_opt_ int FAR * fromlen                                                 //sockaddr结构大小地址
    );

3.9 sendto

	WSAAPI
	sendto(
	    _In_ SOCKET s,                                            //socket 
	    _In_reads_bytes_(len) const char FAR * buf,               //发送数据的缓冲区   
	    _In_ int len,                                             //缓冲区大小      
	    _In_ int flags,                                           //标志位,调用操作方式
	    _In_reads_bytes_(tolen) const struct sockaddr FAR * to,   //sockaddr结构地址
	    _In_ int tolen                                            //sockaddr结构大小地址
	    );

四、实战篇——代码分析

4.1 服务端

新建项目:windows控制台应用程序 -->项目名称:server

// server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include 
#include 
#include
#include
#pragma comment(lib,"ws2_32.lib")

using namespace std;

int main() {
    //设置版本号
	WORD sockVersion = MAKEWORD(2, 2);
    //定义一个WSADATA类型的结构体,存储被WSAStartup函数调用后返回的Windows Sockets数据
	WSADATA wsadata;
	//初始化套接字,启动构建,将“ws2_32.lib”加载到内存中
	if (WSAStartup(sockVersion, &wsadata)) {
		printf("WSAStartup failed \n");
		return 0;
	}
	//创建一个套接字,即创建一个内核对象
	SOCKET hServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (hServer == INVALID_SOCKET) {
		printf("socket failed \n");
		return 0;
	}
	//创建服务器端地址并绑定端口号的IP地址
	sockaddr_in addrServer;
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(8889);
	addrServer.sin_addr.S_un.S_addr = INADDR_ANY;

	// 初始化内核对象,传参给内核对象,此时数据可能都处于未就绪链表
	int nRet = bind(hServer, (sockaddr*)&addrServer, sizeof(addrServer));
	if (nRet == SOCKET_ERROR) {
		printf("socket bind failed\n");
		closesocket(hServer);
		WSACleanup();
		return 0;
	}
     //创建一个客服端地址
	sockaddr_in  addrClient;
	int nlen = sizeof(addrClient);
	//创建一个中间变量,用于存放用户输入的信息
	//string str;

	//用于接受数据的缓冲区。
	char buffer[1024];
	//初始化缓冲区
	memset(buffer, 0, sizeof(buffer));

	int irecv;
	int isend;

	//可以循环接受数据
	while (true) {
	 //接收数据:
			//接受客户端的消息
		    irecv = recvfrom(hServer, buffer, sizeof(buffer), 0, (SOCKADDR*)&addrClient, &nlen);
			//缓冲区有数据,开始读取数据
			if (irecv > 0) {
				//判断数据是否为结束标志,若是则关闭服务器
				if (! (strcmp(buffer,"byebye"))) {
					//关闭服务器套接字
					cout << "ClientA: " << buffer << endl;
					cout << "close connection··· " << endl;
					closesocket(hServer);
					WSACleanup();
					cout << "5s后关闭控制它。" << endl;
					Sleep(5000);
					return 0;
				}
				else {
					cout << " ClientA:" << buffer<< endl;
				}
			}
			//接受数据失败
			else {
				cout << "recvFrom failed " << endl;
				closesocket(hServer);
				WSACleanup();
				cout << "5s后关闭控制台。" << endl;
				Sleep(5000);
				return 0;
			}
			//初始化缓冲区,用于下一次数据的接收
			memset(buffer, 0, sizeof(buffer));

      //发送数据:
			cout << "Server:";
			//从键盘获取数据
			cin >> buffer;
			//getline(cin, str);
			//建立发送数据缓冲区
			//const int len = sizeof(str);
			//char senddata[len];
			//strcpy_s(senddata, str.c_str());

			//发送数据
			isend=sendto(hServer, buffer, strlen(buffer), 0, (SOCKADDR*)&addrClient, nlen);
			if (isend == SOCKET_ERROR) {
				cout << "sendto failed " << endl;
				closesocket(hServer);
				WSACleanup();
				cout << "5s后关闭控制台。" << endl;
				Sleep(5000);
				return 0;
			}
			//str = "";
			//初始化缓冲区,用于下一次数据的接收
			memset(buffer, 0, sizeof(buffer));
	}

	//关闭服务器套接字
	closesocket(hServer);
	WSACleanup();
	return 0;
}

4.2 客户端

新建项目:windows控制台应用程序 -->项目名称:client

// client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include"pch.h"
#include
#include
#include 
#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main() {

	//套接字信息结构
	WSADATA wsadata;
	//设置版本号
	WORD sockVersion = MAKEWORD(2, 2);
	//建立一个客户端套接字;
	SOCKET sClient;
	//启动构建,将“为ws2_32.lib”加载到内存中,做一些初始化工作
	if (WSAStartup(sockVersion, &wsadata) != 0) {
		//判断是否构建成功,若失败,则客户端打印一句提示话。
		printf("WSAStartup failed \n");
		return 0;
	}

	//创建客户端udp套接字
    sClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (SOCKET_ERROR == sClient) {
		printf("socket failed !\n");
		return 0;
	}

	//创建服务器端地址
	sockaddr_in serverAddr;
	//创建服务器端地址
	sockaddr_in clientAddr;
	//设置服务器端地址,端口号,协议族
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8889);
	serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//获取服务器地址和客户端地址构造体的长度
	int slen = sizeof(serverAddr);
	int clen = sizeof(clientAddr);
	//设置接受数据缓冲区大小
	char buffer[1024];
	memset(buffer, 0, sizeof(buffer));
	//用于记录发送函数和接受函数的返回值
	int iSend = 0;
	int iRcv = 0;
	//string str;
	cout << "开始主动与服务器建立通信:" << endl;

	while (true) {
		
		   //从控制台获取数据
			cout << "Client: ";
			//getline(cin, str) ;
			cin >> buffer;
			//将string型数据处理成char数组型
			//const int len = sizeof(str);
			//char senddata[len];
			//strcpy_s(senddata, str.c_str());

			//发送信息给客户端
			iSend=sendto(sClient, buffer, strlen(buffer), 0, (SOCKADDR*)&serverAddr, slen);
			if (iSend== SOCKET_ERROR) {
				cout<<"sendto failed "<<endl;
				closesocket(sClient);
				WSACleanup();
				cout << "5s后关闭控制台。" << endl;
				Sleep(5000);
				return 0;
			}
			//若数据为byebye,断开连接
			if (!(strcmp(buffer, "byebye"))) {
				cout << "close connection " << endl;
				closesocket(sClient);
				WSACleanup();
				cout << "5s后关闭控制它。" << endl;
				Sleep(5000);
				return 0;
			}
			memset(buffer, 0, sizeof(buffer));

		//接受服务器端数据
			iRcv= recvfrom(sClient, buffer, sizeof(buffer), 0, (SOCKADDR*)&clientAddr,&clen);
			if (iRcv == SOCKET_ERROR) {
				cout << "recvFrom failed " << endl;
				closesocket(sClient);
				WSACleanup();
				cout << "5s后关闭控制台。" << endl;
				Sleep(5000);
				return 0;
			}
			//判断服务器是否关闭
			if (iRcv <= 0) {
				cout<< "server disconnected··· " << endl;
				closesocket(sClient);
				WSACleanup();
				cout << "5s后关闭控制台。" << endl;
				Sleep(5000);
				return 0;
			}
			else {
				cout << " Server: " << buffer << endl;
			}
			memset(buffer, 0, sizeof(buffer));

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

4.3 通信

将两个cpp文件都release后打开exe文件,进行正常通信。
基于vs实现的socket—udp通信实例详解_第4张图片

4.4 发送结束标志

发送结束标志后5s后服务器和客户端的socket都会断开连接。
基于vs实现的socket—udp通信实例详解_第5张图片
基于vs实现的socket—udp通信实例详解_第6张图片

4.5 问题error C4996

  • 刚开始使用string接收控制台的数据输入,但在转化成char*的过程中,大量数据容易出错。可以直接使用char型数组接收数据
  • 在VS2017中进行套接字编程时,
    sockaddr_in ClientAddr;
    ClientAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    
    在编译时会弹出错误提示:
    error C4996: 'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings主要原因是inet_addr()函数已经过时,推荐使用inet_pton()或者InetPton()函数。

问题解决:
可以采用三种方法解决error C4996错误:
第一种是关闭项目的SDL检查;

关闭项目的SDL检查:
SDL叫做“安全开发声明周期”检查,是VS2012中新添加的功能。主要是为了能更好地监管该法着的代码安全。

第二种是对_WINSOCK_DEPRECATED_NO_WARNINGS进行定义;

定义_WINSOCK_DEPRECATED_NO_WARNINGS在项目的预编译头文件中添加对_WINSOCK_DEPRECATED_NO_WARNINGS的定义:#define _WINSOCK_DEPRECATED_NO_WARNINGS 0

将其定义为0、1、2…均可。

第三种是使用推荐的新函数。如果想继续使用旧函数,可使用前两种方法。

使用推荐的新函数:
inet_pton()函数或者InetPton()函数在Ws2tcpip.h中定义,在使用这些新函数之前需要包含该头文件。
#include

你可能感兴趣的:(#,socket,C++)