C++ 网络编程下的socket编程(TCP\UDP),连接下位机

正常情况下我们需要对下位机进行通信需要使用Socket进行连接操作,而在网络编程中又分为面向连接(TCP)和面向无连接(UDP)这两种,针对这两种方式,我们不做具体的原理解释,只说各自的特点和各自的应用场景:

UDP的特性是:数据报,无连接,简单,不可靠,会丢包,会乱序(实际中遇到的主要是丢包)
TCP的特性是:流式,有连接,复杂,可靠,延迟较大、带宽占用较大(均是相对于UDP来说)

有这样的特性其实是非常明显的,在早期,通常来说,我们对于传输数据量比较大的数据交互时一般都会使用UDP连接,对于文字消息我们一般采用TCP连接,但是因为保密性和数据丢包的原因,现在的聊天软件的视频通信我们一般也不再使用UDP,转而使用HTTP协议,但是UDP的弊端也恰恰是他最大的优点,对于一些对于数据量较大,且对数据准确性没有特殊请求的连接时我们还是会使用UDP协议(比如笔者在做PC机连接下位机时,通常数据超过2M/s时TCP就会出现明显的卡顿)。这里我们以连接下位机为例,来详细讲解一下基于UDP和TCP的网络编程过程(PC连接下位机),还是老规矩,先放一张图片,来看看我们想要得到的效果:需要下载示例Demo的请点击此处下载(注:此Demo仅包含客户端|上位机的创建,下位机可以像笔者一样使用TCP工具进行模拟或者直接连接对应的硬件设备)

接下来,我们大致了解一下网络编程中标准的基于Socket的TCP/UDP编程步骤:

基于TCP(面向连接)的socket编程的服务器端程序如下:

1、创建套接字(socket)
2、将套接字绑定到一个本地地址和端口上(bind)
3、将套接字设为监听模式,准备接收客户端请求(listen)
4、等待客户请求到来,当请求到来后,接收连接请求,返回一个新的对应于此次连接的套接字(accept)
5、用返回的套接字和客户端进行通信(send/recv)
6、返回,等待另一客户请求
7、关闭套接字

基于TCP(面向连接)的socket编程的客户端程序如下:

1、创建套接字(socket)
2、向服务器发出连接请求(connect)
3、和服务器端进行通信(send/recv)
4、关闭套接字

 基于UDP(面向对象)的socket编程的服务器端程序如下:

1、创建套接字(socket)
2、将套接字绑定到一个本地地址和端口上(bind)
3、等待接收数据(recvfrom)
4、关闭套接字

基于UDP(面向对象)的socket编程的客户端程序如下:

1、创建套接字(socket)
2、向服务器发送数据(sendto)
3、关闭套接字

 好了,接下来,详细介绍我们在MFC下实现客户端|上位机的具体步骤

Step1:我们需要新建一个MFC工程,并添加一个按钮用来触发我们的连接操作

Step2:然后我们在Dlg文件中调用Socket,名字随意

SOCKET m_socket;

Step3:添加自定义消息,首先是在头文件中宏定义

#define WM_Sock WM_USER+1

然后在类向导中添加自定义消息(注名称必须一致),否则运行过程会出现错误 

完成添加后我们编辑消息处理函数的内容(这里只放最简单的一个判断,不添加其他任何操作或者处理)

	afx_msg LRESULT Csocket_demoDlg::OnSock(WPARAM wParam, LPARAM lParam)
	{
		char cs[512] = "";  //定义一个用来存放接收数据的字符串cs
		if (lParam == FD_READ) //sock收到消息,触发FD_READ
		{
			if (SOCKET_ERROR == recv(m_socket, cs, 512, NULL))//使用recv函数来进行判别收到的内容
			{
				MessageBox("接收数据失败!"); //如果触发连接错误,则弹出接收数据失败提示框
				return FALSE;
			}
			else{
			}
		}
		return 0;
	}

Step4:最后就是在按钮的响应事件中添加处理函数

首先是标准的TCP模式:

//这是一个标准的TCP连接过程
void Csocket_demoDlg::OnBnClickedButton1()
{
	// 这是一个标准的TCP连接过程
	CString serv_addr = "192.168.0.104", serv_port = "5050";
	// TODO:  在此添加控件通知处理程序代码
	int port;
	SOCKADDR_IN addr;
	WORD   wVersionRequested;//定义socket1.1或者socket2.0     
	WSADATA   wsaData;   //定义装载socket版本的变量  
	int   err;   //错误变量  

	wVersionRequested = MAKEWORD(2, 2);   //定义连接为socket2.0  

	err = WSAStartup(wVersionRequested, &wsaData);   //装载socket2.0支持  
	if (0 != err)//判断是否装载成功  
	{
		return;
	}

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)//判断版本号,是否和定义的一样  
	{
		WSACleanup();   //若出问题,卸载支持,并结束程序返回-1  
		return;
	}

	//创建TCP套接字
	m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);

	//判断套接字错误
	if (INVALID_SOCKET == m_socket)
	{
		MessageBox("创建套接字失败!");
		return;
	}

	//判断注册网络错误
	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, WM_Sock, FD_READ))
	{
		MessageBox("注册网络读取事件失败!");
		return;
	}

	//判断服务器地址和端口号错误
	if (serv_port == "" || serv_addr == "")
	{
		MessageBox("服务器地址或端口不能为空!!!");

	}
	else
	{
		port = atoi(serv_port.GetBuffer(1));//将端口字符串转换为整形
		addr.sin_family = AF_INET;
		addr.sin_addr.S_un.S_addr = inet_addr(serv_addr.GetBuffer(1));//转换服务器ip地址
		addr.sin_port = ntohs(port);

		//设置非阻塞模式
		unsigned long ul = 1;
		int ret = ioctlsocket(m_socket, FIONBIO, (unsigned long*)&ul);
		if (ret == SOCKET_ERROR)
			exit(0);

		//TCP模式下调用Connect()
		connect(m_socket, (SOCKADDR*)&addr, sizeof(SOCKADDR));

		//对下位机发送指令
		CString connected;
		connected = "设备已经连接";
		send(m_socket, connected, 50, 0);
	}
}

然后是一个标准的UDP连接过程代码:

// 这是一个标准的UDP连接过程
void Csocket_demoDlg::OnBnClickedButton1()
{
	// 这是一个标准的UDP连接过程
	CString serv_addr = "192.168.0.104", serv_port = "5050";
	// TODO:  在此添加控件通知处理程序代码
	int port;
	SOCKADDR_IN addr;
	WORD   wVersionRequested;//定义socket1.1或者socket2.0     
	WSADATA   wsaData;   //定义装载socket版本的变量  
	int   err;   //错误变量  

	wVersionRequested = MAKEWORD(2, 2);   //定义连接为socket2.0  

	err = WSAStartup(wVersionRequested, &wsaData);   //装载socket2.0支持  
	if (0 != err)//判断是否装载成功  
	{
		return;
	}

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)//判断版本号,是否和定义的一样  
	{
		WSACleanup();   //若出问题,卸载支持,并结束程序返回-1  
		return;
	}

	//创建UDP套接字
	m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);

	//判断套接字错误
	if (INVALID_SOCKET == m_socket)
	{
		MessageBox("创建套接字失败!");
		return;
	}

	//判断注册网络错误
	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, WM_Sock, FD_READ))
	{
		MessageBox("注册网络读取事件失败!");
		return;
	}

	//判断服务器地址和端口号错误
	if (serv_port == "" || serv_addr == "")
	{
		MessageBox("服务器地址或端口不能为空!!!");

	}
	else
	{
		port = atoi(serv_port.GetBuffer(1));//将端口字符串转换为整形
		addr.sin_family = AF_INET;
		addr.sin_addr.S_un.S_addr = inet_addr(serv_addr.GetBuffer(1));//转换服务器ip地址
		addr.sin_port = ntohs(port);

		//设置非阻塞模式
		unsigned long ul = 1;
		int ret = ioctlsocket(m_socket, FIONBIO, (unsigned long*)&ul);
		if (ret == SOCKET_ERROR)
			exit(0);

		//对下位机发送指令
		CString connected;
		connected = "设备已经连接";
		sendto(m_socket, connected, 50, 0,NULL,NULL);
	}
}

最后,提供一种非标准的连接方式 

//这是一种非常规的连接过程UDP+Connect
void Csocket_demoDlg::OnBnClickedButton1()
{
	//这是一种非常规的连接过程UDP+Connect
	CString serv_addr = "192.168.0.104", serv_port = "5050";
	// TODO:  在此添加控件通知处理程序代码
	int port;
	SOCKADDR_IN addr;
	WORD   wVersionRequested;//定义socket1.1或者socket2.0     
	WSADATA   wsaData;   //定义装载socket版本的变量  
	int   err;   //错误变量  

	wVersionRequested = MAKEWORD(2, 2);   //定义连接为socket2.0  

	err = WSAStartup(wVersionRequested, &wsaData);   //装载socket2.0支持  
	if (0 != err)//判断是否装载成功  
	{
		return;
	}

	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)//判断版本号,是否和定义的一样  
	{
		WSACleanup();   //若出问题,卸载支持,并结束程序返回-1  
		return;
	}

	//创建TCP套接字
	m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);

	//创建UDP套接字
	/*m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);*/

	//判断套接字错误
	if (INVALID_SOCKET == m_socket)
	{
		MessageBox("创建套接字失败!");
		return;
	}

	//判断注册网络错误
	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, WM_Sock, FD_READ))
	{
		MessageBox("注册网络读取事件失败!");
		return;
	}

	//判断服务器地址和端口号错误
	if (serv_port == "" || serv_addr == "")
	{
		MessageBox("服务器地址或端口不能为空!!!");

	}
	else
	{
		port = atoi(serv_port.GetBuffer(1));//将端口字符串转换为整形
		addr.sin_family = AF_INET;
		addr.sin_addr.S_un.S_addr = inet_addr(serv_addr.GetBuffer(1));//转换服务器ip地址
		addr.sin_port = ntohs(port);

		//设置非阻塞模式
		unsigned long ul = 1;
		int ret = ioctlsocket(m_socket, FIONBIO, (unsigned long*)&ul);
		if (ret == SOCKET_ERROR)
			exit(0);

		//TCP模式下调用Connect()
		//UDP下也可以使用connect,使用connect函数之后可不必使用sendto函数
		connect(m_socket, (SOCKADDR*)&addr, sizeof(SOCKADDR));

		//对下位机发送指令
		CString connected;
		connected = "设备已经连接";
		send(m_socket, connected, 50, 0);
	}
}

这里,在文件中我已经给了详细的注释,针对非标准的连接方式特别说明几点:

1、程序中UDP和TCP的连接仅在创建套接字时有不同

//创建TCP套接字
m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
//创建UDP套接字
m_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);

2、连接时都使用了Connect函数:这里具体的解释可以参考原文博客,这里只做简单摘要

标准的udp客户端开了套接口后,一般使用sendto和recvfrom函数来发数据,实际上,udp发送数据有两种方法供大家选用的:
方法一: 
socket----->sendto()或recvfrom() 
方法二: 
socket----->connect()----->send()或recv().(此时sendto,recvfrom仍可用)

给UDP套接口调用connect,与TCP不同的是:没有三路握手过程。内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的IP地址和端口号(取自传递给connect的套接口地址结构),然后立即返回到调用进程

猜你喜欢

转载自blog.csdn.net/qq_15029743/article/details/82429828
今日推荐