Java与C++ - MFC中的TCP通讯

178 阅读9分钟

业务场景需求

标题:Java - C++之MFC通讯

1 业务场景描述

本人岗位背景:Java开发菜狗一条;

业务描述:需要对接各大厂家的设备;

对接信息:对方只提供了x86的库文件给我;

我他喵的,我哪会啊.........被迫学习。

2 解决方案

目前有如下但不局限于这些的解决方法

  • 方案一:通过 Windows rundll32.exe执行x86库文件;
  • 方案二:使用Python做接口暴露;
  • 方案三:使用C++做接口暴露;

本文采取了方案三。

3 技术点

主要分为三个部分

  1. C++ / Java的异步多线程
  2. C++ MFC框架的使用
  3. C++ / Java的TCP通讯

我们根据上述的思路开始下面的打码。

一、C++代码

提示:这里可以添加本文要记录的大概内容

1 编译环境的部署

首先必须将所需要的模块进行安装,如MFC等。

这一块就不大想说太多了,如图所示。

2 完整代码片段

先上代码再说

// MatrixSDKTestDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "MatrixSDKTest.h"
#include "MatrixSDKTestDlg.h"
#include "afxdialogex.h"
#include <vector>
#include <sstream>

//获取地址信息
#include <WINSOCK2.H>
#include <WS2TCPIP.H>
#pragma comment (lib, "WS2_32.lib")
 

// 关于TCP的...
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include "winsock2.h"
#include<cstdlib>
#include <thread> // 这个用于多线程开辟
using namespace std;
char recvBuf[100];
bool connectStatus = false;
SOCKET sockConn;
// ...over
#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// ...此处省略无关文章的业务
// ...


///////////////////////////////// 功能枚举 /////////////////////////////////////////////
const CString code_100001 = "100001";// 初始化
const CString code_100002 = "100002";// 代表XXX功能
const CString code_100003 = "100003";// 代表XXX功能
const CString code_100004 = "100004";// 代表XXX功能
const CString code_100005 = "100005";// 代表XXX功能

///////////////////////////////// TCP程序 /////////////////////////////////////////////

// 切割方法
vector<string> split(string s, char token) {
	stringstream iss(s);
	string word;
	vector<string> vs;
	while (getline(iss, word, token)) {
		vs.push_back(word);
	}
	return vs;
}

/**
 * TCP
 * 在一个新的线程里面接收数据
 */
DWORD WINAPI Fun(LPVOID lpParamter)
{
	while (true) {
		memset(recvBuf, 0, sizeof(recvBuf));
		//接收数据
		recv(sockConn, recvBuf, sizeof(recvBuf), 0);
		printf("%s\n", recvBuf);
		// 数据处理代码 - 根据过来的消息分配函数 - 处理的比较无脑简单
		CString strCode = recvBuf;
		// 初始化 字符串切割
		string s1First = strCode;
		vector<string> sData = split(s1First, ',');
		if (strCode.Find(code_100001) != -1) {
			// 初始化
			string ipAddr = sData[1];
			char* ipAddr_char = (char*)ipAddr.data();
			unsigned short wLocalPort = 5060;
			// 执行初始化动作
			bool bRt = InitDevice(ipAddr_char, wLocalPort);
			if (bRt)
			{
				send(sockConn, "初始化成功", sizeof("初始化成功"), 0);
			}
			else
			{
				send(sockConn, "初始化失败", sizeof("初始化失败"), 0);
			}
		}
		else if (strCode.Find(code_100002) != -1) {
			// ...
		}
		else if(strCode.Find(code_100003) != -1){
			// ...
		}
		else {
			int iSendDDD3 = send(sockConn, "-1", 2, 0);
		}
		
		// 消息回复 Demo
		//char sendbuf[] = "你好,我是服务端,我已经收到了你的消息";
		// 消息发送 Demo --- 如果回去的数据长度不够 看看是否是第三个参数限制太短了
		//int iSend = send(sockConn, sendbuf, sizeof(sendbuf), 0);
	}
	closesocket(sockConn);
}

int startUpTCP()
{
	WSADATA wsaData;
	int port = 12022;// 端口号
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		printf("初始化失败");
		return 0;
	}
	//创建用于监听的套接字,即服务端的套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(port); //1024以上的端口号
	/**
	 * INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
	 */
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	//int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	//if (retVal == SOCKET_ERROR) {
	//	printf("连接失败:%d\n", WSAGetLastError());
	//	return 0;
	//}

	if (listen(sockSrv, 10) == SOCKET_ERROR) {
		printf("监听失败:%d", WSAGetLastError());
		return 0;
	}

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while (1)
	{
		//等待客户请求到来
		sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		if (sockConn == SOCKET_ERROR) {
			printf("等待请求失败:%d", WSAGetLastError());
			break;
		}

		printf("客户端的IP是:[%s]\n", inet_ntoa(addrClient.sin_addr));

		//发送数据
		char sendbuf[] = "你好,我是服务端,咱们一起聊天吧";
		int iSend = send(sockConn, sendbuf, sizeof(sendbuf), 0);
		if (iSend == SOCKET_ERROR) {
			printf("发送失败");
			break;
		}

		HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
		CloseHandle(hThread);

	}

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

// TCP up
void CMatrixSDKTestDlg::TcpUp()
{
	// TODO: 在此添加控件通知处理程序代码
	// 连接状态未开启的情况下
	if (!connectStatus) {
		connectStatus = true;
		CString strshow;
		strshow.Format("TCP ----- UP");
		addinforshow(strshow);
		// 启动中 要异步执行 --- join()会导致主线程阻塞
		thread th1(startUpTCP);
		th1.detach();
		//th1.join();
	}
	else {
		// 连接状态已经开启了
		CString strshow;
		strshow.Format("请勿重复启动TCP");
		addinforshow(strshow);
	}
}

// TCP down
void CMatrixSDKTestDlg::TcpDown()
{
	// TODO: 在此添加控件通知处理程序代码


}

调试工具的效果如图

MFC界面的效果图

然后开始对代码情况进行简单说明。

3 第一步:为控件添加事件处理程序

打开界面查看版,进行添加事件处理程序

此时在弹出的选择框内可以选择类

我们可以通过属性按钮,修改控件的名称和一些相关属性

大概就是这样。

4 第二步:异步多线程开启Socket通信

注意,在线程启动的时候,如果选择thread1.join();,会导致主线程被阻塞。

/**
 * TCP
 * 在一个新的线程里面接收数据
 */
DWORD WINAPI Fun(LPVOID lpParamter)
{
	while (true) {
		memset(recvBuf, 0, sizeof(recvBuf));
		//接收数据
		recv(sockConn, recvBuf, sizeof(recvBuf), 0);
		printf("%s\n", recvBuf);
		// 消息回复 Demo
		char sendbuf[] = "你好,我是服务端,我已经收到了你的消息";
		// 消息发送 Demo --- 如果回去的数据长度不够 看看是否是第三个参数限制太短了
		int iSend = send(sockConn, sendbuf, sizeof(sendbuf), 0);
	}
	closesocket(sockConn);
}

int startUpTCP()
{
	WSADATA wsaData;
	int port = 12022;// 端口号
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		printf("初始化失败");
		return 0;
	}
	//创建用于监听的套接字,即服务端的套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(port); //1024以上的端口号
	/**
	 * INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
	 */
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	//int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
	//if (retVal == SOCKET_ERROR) {
	//	printf("连接失败:%d\n", WSAGetLastError());
	//	return 0;
	//}

	if (listen(sockSrv, 10) == SOCKET_ERROR) {
		printf("监听失败:%d", WSAGetLastError());
		return 0;
	}

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while (1)
	{
		//等待客户请求到来
		sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		if (sockConn == SOCKET_ERROR) {
			printf("等待请求失败:%d", WSAGetLastError());
			break;
		}

		printf("客户端的IP是:[%s]\n", inet_ntoa(addrClient.sin_addr));

		//发送数据
		char sendbuf[] = "你好,我是服务端,咱们一起聊天吧";
		int iSend = send(sockConn, sendbuf, sizeof(sendbuf), 0);
		if (iSend == SOCKET_ERROR) {
			printf("发送失败");
			break;
		}

		HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
		CloseHandle(hThread);

	}

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

// TCP up
void CMatrixSDKTestDlg::TcpUp()
{
	// TODO: 在此添加控件通知处理程序代码
	// 连接状态未开启的情况下
	if (!connectStatus) {
		connectStatus = true;
		CString strshow;
		strshow.Format("TCP ----- UP");
		addinforshow(strshow);
		// 启动中 要异步执行 --- join()会导致主线程阻塞
		thread th1(startUpTCP);
		th1.detach();
		//th1.join();
	}
	else {
		// 连接状态已经开启了
		CString strshow;
		strshow.Format("请勿重复启动TCP");
		addinforshow(strshow);
	}
}

5 第三步:消息接收、解析、返回

我这边思路是通过功能码区分消息类型,通过逗号将参数带入并分解。

///////////////////////////////// 功能枚举 /////////////////////////////////////////////
const CString code_100001 = "100001";// 初始化
const CString code_100002 = "100002";// 代表XXX功能
const CString code_100003 = "100003";// 代表XXX功能
const CString code_100004 = "100004";// 代表XXX功能
const CString code_100005 = "100005";// 代表XXX功能

///////////////////////////////// TCP程序 /////////////////////////////////////////////

// 切割方法
vector<string> split(string s, char token) {
	stringstream iss(s);
	string word;
	vector<string> vs;
	while (getline(iss, word, token)) {
		vs.push_back(word);
	}
	return vs;
}

/**
 * TCP
 * 在一个新的线程里面接收数据
 */
DWORD WINAPI Fun(LPVOID lpParamter)
{
	while (true) {
		memset(recvBuf, 0, sizeof(recvBuf));
		//接收数据
		recv(sockConn, recvBuf, sizeof(recvBuf), 0);
		printf("%s\n", recvBuf);
		// 数据处理代码 - 根据过来的消息分配函数 - 处理的比较无脑简单
		CString strCode = recvBuf;
		// 初始化 字符串切割
		string s1First = strCode;
		vector<string> sData = split(s1First, ',');
		if (strCode.Find(code_100001) != -1) {
			// 初始化
			string ipAddr = sData[1];
			char* ipAddr_char = (char*)ipAddr.data();
			unsigned short wLocalPort = 5060;
			// 执行初始化动作
			bool bRt = InitDevice(ipAddr_char, wLocalPort);
			if (bRt)
			{
				send(sockConn, "初始化成功", sizeof("初始化成功"), 0);
			}
			else
			{
				send(sockConn, "初始化失败", sizeof("初始化失败"), 0);
			}
		}
		else if (strCode.Find(code_100002) != -1) {
			// ...
		}
		else if(strCode.Find(code_100003) != -1){
			// ...
		}
		else {
			int iSendDDD3 = send(sockConn, "-1", 2, 0);
		}
		
		// 消息回复 Demo
		//char sendbuf[] = "你好,我是服务端,我已经收到了你的消息";
		// 消息发送 Demo --- 如果回去的数据长度不够 看看是否是第三个参数限制太短了
		//int iSend = send(sockConn, sendbuf, sizeof(sendbuf), 0);
	}
	closesocket(sockConn);
}

适配器代码已经完成,接下来编写我们的Java客户端请求代码。

二、Java代码

关于这块我不做太多描述,使用Netty框架进行TCP通讯。

1 基本通讯信息配置

public class ToaConfig {
	/** 本地适配器地址及端口 */
	public static final String IPADDR_LOCAL = "192.168.247.185";
	public static final String IPADDR_PORT = "12022";
}

2 客户端初始化

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springblade.common.tool.ThreadPoolUtils;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;

/**
 * 客户端初始化
 * @author lijiamin
 */
@Component
public class ClientInitialize {

	private static Logger log = LoggerFactory.getLogger(ClientInitialize.class);
	public static EventLoopGroup eventLoopGroup;
	public static Bootstrap bootstrap;
	public static CopyOnWriteArraySet<String> falseConnectSet = new CopyOnWriteArraySet();

	/**
	 * Netty初始化配置
	 */
	static {
		eventLoopGroup = new NioEventLoopGroup();
		bootstrap = new Bootstrap();
		bootstrap.group(eventLoopGroup)
			.channel(NioSocketChannel.class)
			.option(ChannelOption.TCP_NODELAY, true)
			.option(ChannelOption.SO_KEEPALIVE, true)
			.handler(new ChannelInitializer<SocketChannel>() {
				@Override
				protected void initChannel(SocketChannel socketChannel) throws Exception {
					socketChannel.pipeline().addLast(new StringDecoder());
					socketChannel.pipeline().addLast(new StringEncoder());
					socketChannel.pipeline().addLast(new ClientHandler());
				}
			});

		// 异步持续监听连接失败的地址
		CompletableFuture.runAsync(new Runnable() {
			@Override
			public void run() {
				while (true) {
					try {
						if (falseConnectSet.size() != 0) {
							// 循环集合内元素
							falseConnectSet.forEach(new Consumer<String>() {
								@Override
								public void accept(String s) {
									String[] strings = s.split(":");
									connectServer(strings[0],strings[1]);
								}
							});
						}
						Thread.sleep(10000);
					} catch (Exception e) {
						log.error("Netty初始化配置监听地址出现异常");
						e.printStackTrace();
					}
				}
			}
		}, ThreadPoolUtils.getThreadPool());
	}

	/**
	 * 服务连接
	 * @param host
	 * @param portStr
	 */
	public static void connectServer(String host, String portStr) {
		// 获取地址及端口
		int port = Integer.valueOf(portStr);
		// 异步连接tcp服务端
		bootstrap.remoteAddress(host, port).connect().addListener((ChannelFuture futureListener) -> {
			if (!futureListener.isSuccess()) {
				log.error(host + ":" + port + "连接失败!!!!!!!! 当前时间:" + new Date());
				futureListener.channel().close();
				// 连接失败信息插入Set
				falseConnectSet.add(host + ":" + port);
			} else {
				log.info(host + ":" + port + "连接成功,当前时间:" + new Date());
				// 连接成功信息从Set拔除
				falseConnectSet.remove(host + ":" + port);
			}
		});
	}

	/**
	 * 初始化方法
	 */
	@PostConstruct
	public void initialize() {
		connectServer(ToaConfig.IPADDR_LOCAL, ToaConfig.IPADDR_PORT);
	}
}

3 处理器

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;

/**
 * 客户端处理器
 * @author lijiamin
 */
public class ClientHandler extends ChannelInboundHandlerAdapter {

	private static Logger log = LoggerFactory.getLogger(ClientHandler.class);
	/** 与适配器连接的通道 */
	public static Channel channel = null;

	/**
	 * 连接建立
	 * @param ctx
	 * @throws Exception
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		channel = ctx.channel();
		super.channelActive(ctx);
	}

	/**
	 * 连接断开
	 * @param ctx
	 * @throws Exception
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		InetSocketAddress ipSocket = (InetSocketAddress) ctx.channel().remoteAddress();
		int port = ipSocket.getPort();
		String host = ipSocket.getHostString();
		log.error("与设备" + host + ":" + port + "连接断开!");

		// 连接断开后的最后处理
		ctx.pipeline().remove(this);
		ctx.deregister();
		ctx.close();

		// 将失败信息插入Set集合
		ClientInitialize.falseConnectSet.add(host + ":" + port);
		channel = null;
		super.channelInactive(ctx);
	}

	/**
	 * 通道数据读取
	 * 业务操作
	 * @param ctx
	 * @param msg
	 * @throws Exception
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		super.channelRead(ctx, msg);
	}

	/**
	 * 通道数据处理完成
	 * @param ctx
	 * @throws Exception
	 */
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		super.channelReadComplete(ctx);
	}

	/**
	 * 事件触发
	 * @param ctx
	 * @param evt
	 * @throws Exception
	 */
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		super.userEventTriggered(ctx, evt);
	}

	/**
	 * 异常触发
	 * @param ctx
	 * @param cause
	 * @throws Exception
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		super.exceptionCaught(ctx, cause);
	}
}

4 最后说明

在服务端与客户端数据通讯时,我们是通过处理器中的通道进行通讯,这块Netty的知识不再过多说明。

暂且告一段落。

总结

提示:这里对文章进行总结: 例如:以上就是今天要讲的内容。