windows C++ TCP客户端

news2024/12/27 4:22:06

demo有一下功能

1、心跳包
2、断开重连
3、非阻塞
4、接受数据单独线程处理


#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <string>
#include <process.h>  // 用于Windows下的线程相关操作

#pragma comment(lib, "ws2_32.lib")

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 6000
#define RECV_BUF_SIZE 1024
#define HEARTBEAT_INTERVAL 5000  // 心跳包发送间隔,单位:毫秒
#define HEARTBEAT_TIMEOUT 10000  // 心跳包超时时间,单位:毫秒
#define MAX_RECONNECT_ATTEMPTS 10 // 最大重连尝试次数
#define RECONNECT_INTERVAL_SECONDS 2  // 重连间隔时间(秒)

class TCPClient
{
public:
	TCPClient();
	~TCPClient();

	bool connectToServer();
	void disconnect();
	int sendData(const std::string& data);

private:
	SOCKET m_socket;
	sockaddr_in m_serverAddr;
	bool m_connected;

	// 心跳包相关变量和函数
	DWORD m_lastHeartbeatTime;
	bool m_heartbeatSent;
	HANDLE m_heartbeatThreadHandle;
	bool m_heartbeatThreadRunning;
	static unsigned int __stdcall HeartbeatThread(void* param);
	bool sendHeartbeat();
	bool checkHeartbeatResponse();

	// 用于设置套接字为非阻塞模式
	bool setSocketNonBlocking();
	// 尝试重连服务器
	bool reconnect();
	// 初始化Winsock库
	bool initializeWinsock();
	// 关闭套接字并清理相关资源
	void closeSocket();

	// 接收数据线程相关函数和变量
	static unsigned int __stdcall ReceiveDataThread(void* param);
	HANDLE m_receiveThreadHandle;
	bool m_receiveThreadRunning;
};

// 构造函数,初始化成员变量并初始化Winsock库
TCPClient::TCPClient() : m_socket(INVALID_SOCKET), m_connected(false),
	m_lastHeartbeatTime(0), m_heartbeatSent(false),
	m_heartbeatThreadHandle(NULL), m_heartbeatThreadRunning(false),
	m_receiveThreadHandle(NULL), m_receiveThreadRunning(false) 
{
		if (!initializeWinsock()) 
		{
			std::cerr << "初始化Winsock库失败" << std::endl;
		}

		m_serverAddr.sin_family = AF_INET;
		m_serverAddr.sin_port = htons(SERVER_PORT);
		if (inet_pton(AF_INET, SERVER_IP, &(m_serverAddr.sin_addr)) <= 0) 
		{
			std::cerr << "inet_pton转换IP地址错误" << std::endl;
		}
}

// 析构函数,断开连接并清理Winsock库,同时关闭心跳包线程和接收数据线程
TCPClient::~TCPClient()
{
	disconnect();
	if (m_heartbeatThreadHandle!= NULL) 
	{
		m_heartbeatThreadRunning = false;
		// 等待心跳包线程结束
		WaitForSingleObject(m_heartbeatThreadHandle, INFINITE);
		CloseHandle(m_heartbeatThreadHandle);
	}
	if (m_receiveThreadHandle!= NULL) 
	{
		m_receiveThreadRunning = false;
		// 等待接收数据线程结束
		WaitForSingleObject(m_receiveThreadHandle, INFINITE);
		CloseHandle(m_receiveThreadHandle);
	}
	WSACleanup();
}

// 连接服务器的函数
bool TCPClient::connectToServer()
{
	m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (m_socket == INVALID_SOCKET)
	{
		std::cerr << "创建套接字失败,错误码: " << WSAGetLastError() << std::endl;
		return false;
	}

	// 设置套接字为非阻塞模式
	if (!setSocketNonBlocking())
	{
		std::cerr << "设置套接字为非阻塞模式失败" << std::endl;
		closeSocket();
		return false;
	}

	int ret = connect(m_socket, (struct sockaddr*)&m_serverAddr, sizeof(m_serverAddr));
	if (ret == SOCKET_ERROR)
	{
		int errCode = WSAGetLastError();
		if (errCode!= WSAEWOULDBLOCK) 
		{
			std::cerr << "连接服务器失败,错误码: " << errCode << std::endl;
			closeSocket();
			return false;
		}
	}

	// 等待连接真正建立(非阻塞模式下需要轮询检查)
	timeval timeout;
	timeout.tv_sec = 5;  // 设置超时时间为5秒
	timeout.tv_usec = 0;
	fd_set writefds;
	FD_ZERO(&writefds);
	FD_SET(m_socket, &writefds);

	ret = select(0, NULL, &writefds, NULL, &timeout);
	if (ret == SOCKET_ERROR)
	{
		std::cerr << "select函数出错,错误码: " << WSAGetLastError() << std::endl;
		closeSocket();
		return false;
	} 
	else if (ret == 0)
	{
		std::cerr << "连接超时" << std::endl;
		closeSocket();
		return false;
	}

	if (FD_ISSET(m_socket, &writefds))
	{
		m_connected = true;
		// 创建并启动接收数据线程
		m_receiveThreadHandle = (HANDLE)_beginthreadex(NULL, 0, ReceiveDataThread, this, 0, NULL);
		if (m_receiveThreadHandle == NULL) 
		{
			std::cerr << "创建接收数据线程失败" << std::endl;
			closeSocket();
			return false;
		}
		m_receiveThreadRunning = true;

		// 创建并启动心跳包线程
		m_heartbeatThreadHandle = (HANDLE)_beginthreadex(NULL, 0, HeartbeatThread, this, 0, NULL);
		if (m_heartbeatThreadHandle == NULL) 
		{
			std::cerr << "创建心跳包线程失败" << std::endl;
			closeSocket();
			return false;
		}
		m_heartbeatThreadRunning = true;

		std::cout << "成功连接到服务器" << std::endl;
		return true;
	}

	return false;
}

// 断开与服务器连接的函数
void TCPClient::disconnect()
{
	if (m_connected) 
	{
		closesocket(m_socket);
		m_connected = false;
		std::cout << "已断开与服务器的连接" << std::endl;
	}
	m_receiveThreadRunning = false;
	m_heartbeatThreadRunning = false;
}

// 发送数据到服务器的函数
int TCPClient::sendData(const std::string& data) 
{
	if (!m_connected) 
	{
		if (reconnect())
		{

		}
		else
		{
			std::cerr << "未连接到服务器,无法发送数据" << std::endl;
			return SOCKET_ERROR;
		}
	}

	int ret = send(m_socket, data.c_str(), data.size(), 0);
	if (ret == SOCKET_ERROR) 
	{
		int errCode = WSAGetLastError();
		if (errCode == WSAEWOULDBLOCK) 
		{
			// 在非阻塞模式下,缓冲区满等情况会返回此错误,可根据需要处理
			return 0;
		} 
		else
		{
			std::cerr << "发送数据失败,错误码: " << errCode << std::endl;
			// 如果是连接断开相关错误,尝试重连
			if (errCode == WSAECONNRESET || errCode == WSAENETRESET)
			{
				if (reconnect())
				{
					// 重连成功后再次发送数据
					return sendData(data);
				}
			}
			return SOCKET_ERROR;
		}
	}
	return ret;
}

// 发送心跳包的函数
bool TCPClient::sendHeartbeat() 
{
	if (!m_connected)
	{
		return false;
	}

	const std::string heartbeatData = "HEARTBEAT_CLIENT";
	int ret = send(m_socket, heartbeatData.c_str(), heartbeatData.size(), 0);
	if (ret == SOCKET_ERROR)
	{
		int errCode = WSAGetLastError();
		if (errCode == WSAEWOULDBLOCK)
		{
			return false;
		}
		else 
		{
			std::cerr << "发送心跳包失败,错误码: " << errCode << std::endl;
			return false;
		}
	}
	m_heartbeatSent = true;
	return true;
}

// 检查心跳包响应的函数
bool TCPClient::checkHeartbeatResponse()
{
	if (!m_connected)
	{
		return false;
	}

	char buffer[RECV_BUF_SIZE];
	int ret = recv(m_socket, buffer, RECV_BUF_SIZE, 0);
	if (ret == SOCKET_ERROR)
	{
		int errCode = WSAGetLastError();
		if (errCode == WSAEWOULDBLOCK) 
		{
			return false;
		} 
		else
		{
			std::cerr << "接收心跳包响应失败,错误码: " << errCode << std::endl;
			return false;
		}
	}
	else if (ret == 0)
	{
		// 对方关闭了连接
		std::cerr << "服务器关闭了连接" << std::endl;
		disconnect();
		return false;
	} 
	else
	{
		std::string response(buffer, ret);
		if (response == "HEARTBEAT_ACK")
		{
			return true;
		}
	}
	return false;
}

// 设置套接字为非阻塞模式的函数
bool TCPClient::setSocketNonBlocking()
{
	u_long mode = 1;
	int ret = ioctlsocket(m_socket, FIONBIO, &mode);
	return ret!= SOCKET_ERROR;
}

// 尝试重连服务器的函数
bool TCPClient::reconnect()
{
	int attempt = 0;
	while (attempt < MAX_RECONNECT_ATTEMPTS)
	{
		attempt++;
		closeSocket();
		Sleep(RECONNECT_INTERVAL_SECONDS * 1000);  // 等待一段时间后重连

		m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (m_socket == INVALID_SOCKET)
		{
			std::cerr << "重连时创建套接字失败,错误码: " << WSAGetLastError() << std::endl;
			continue;
		}

		// 设置套接字为非阻塞模式
		if (!setSocketNonBlocking())
		{
			std::cerr << "重连时设置套接字为非阻塞模式失败" << std::endl;
			closeSocket();
			continue;
		}

		int ret = connect(m_socket, (struct sockaddr*)&m_serverAddr, sizeof(m_serverAddr));
		if (ret == SOCKET_ERROR)
		{
			int errCode = WSAGetLastError();
			if (errCode!= WSAEWOULDBLOCK)
			{
				std::cerr << "重连失败,错误码: " << errCode << std::endl;
				continue;
			}
		}

		// 等待连接真正建立(非阻塞模式下需要轮询检查)
		timeval timeout;
		timeout.tv_sec = 5;  // 设置超时时间为5秒
		timeout.tv_usec = 0;
		fd_set writefds;
		FD_ZERO(&writefds);
		FD_SET(m_socket, &writefds);

		ret = select(0, NULL, &writefds, NULL, &timeout);
		if (ret == SOCKET_ERROR)
		{
			std::cerr << "重连时select函数出错,错误码: " << WSAGetLastError() << std::endl;
			closeSocket();
			continue;
		} 
		else if (ret == 0) 
		{
			std::cerr << "重连超时" << std::endl;
			closeSocket();
			continue;
		}

		if (FD_ISSET(m_socket, &writefds))
		{
			m_connected = true;
			// 重新创建并启动接收数据线程
			if (m_receiveThreadHandle!= NULL)
			{
				m_receiveThreadRunning = false;
				WaitForSingleObject(m_receiveThreadHandle, INFINITE);
				CloseHandle(m_receiveThreadHandle);
			}
			m_receiveThreadHandle = (HANDLE)_beginthreadex(NULL, 0, ReceiveDataThread, this, 0, NULL);
			if (m_receiveThreadHandle == NULL) 
			{
				std::cerr << "重连后创建接收数据线程失败" << std::endl;
				closeSocket();
				return false;
			}
			m_receiveThreadRunning = true;

			// 重新创建并启动心跳包线程
			if (m_heartbeatThreadHandle!= NULL) 
			{
				m_heartbeatThreadRunning = false;
				WaitForSingleObject(m_heartbeatThreadHandle, INFINITE);
				CloseHandle(m_heartbeatThreadHandle);
			}
			m_heartbeatThreadHandle = (HANDLE)_beginthreadex(NULL, 0, HeartbeatThread, this, 0, NULL);
			if (m_heartbeatThreadHandle == NULL) 
			{
				std::cerr << "重连后创建心跳包线程失败" << std::endl;
				closeSocket();
				return false;
			}
			m_heartbeatThreadRunning = true;

			std::cout << "重连成功" << std::endl;
			return true;
		}
	}
	std::cerr << "达到最大重连尝试次数,重连失败" << std::endl;
	return false;
}

// 初始化Winsock库的函数
bool TCPClient::initializeWinsock() 
{
	WSADATA wsaData;
	return WSAStartup(MAKEWORD(2, 2), &wsaData) == 0;
}

// 关闭套接字并清理相关资源的函数
void TCPClient::closeSocket() 
{
	if (m_socket!= INVALID_SOCKET) 
	{
		closesocket(m_socket);
		m_socket = INVALID_SOCKET;
	}
}

// 心跳包线程函数
unsigned int __stdcall TCPClient::HeartbeatThread(void* param)
{
	TCPClient* client = static_cast<TCPClient*>(param);
	while (client->m_heartbeatThreadRunning && client->m_connected) 
	{
		DWORD currentTime = GetTickCount();
		if (currentTime - client->m_lastHeartbeatTime >= HEARTBEAT_INTERVAL)
		{
			if (client->sendHeartbeat())
			{
				client->m_lastHeartbeatTime = currentTime;
			}
		}

		if (currentTime - client->m_lastHeartbeatTime > HEARTBEAT_TIMEOUT)
		{
			std::cerr << "心跳包超时,通知主线程尝试重连" << std::endl;
			client->m_connected = false;
			break;
		}

		Sleep(100);  // 适当休眠,避免过于频繁循环检查
	}
	return 0;
}

// 接收数据线程函数
unsigned int __stdcall TCPClient::ReceiveDataThread(void* param) 
{
	TCPClient* client = static_cast<TCPClient*>(param);
	std::string receivedData;
	while (client->m_receiveThreadRunning && client->m_connected) 
	{
		char recvBuf[RECV_BUF_SIZE];
		int ret = recv(client->m_socket, recvBuf, RECV_BUF_SIZE, 0);
		if (ret == SOCKET_ERROR)
		{
			int errCode = WSAGetLastError();
			if (errCode == WSAEWOULDBLOCK)
			{
				// 在非阻塞模式下,无数据可读时会返回此错误,可根据需要处理
				continue;
			}
			else 
			{
				std::cerr << "接收数据线程中接收数据失败,错误码: " << errCode << std::endl;
				// 如果是连接断开相关错误,通知主线程尝试重连
				if (errCode == WSAECONNRESET || errCode == WSAENETRESET) 
				{
					client->m_connected = false;
					break;
				}
			}
		} 
		else if (ret == 0)
		{
			// 对方关闭了连接
			std::cerr << "服务器关闭了连接(接收数据线程中)" << std::endl;
			client->m_connected = false;
			break;
		} 
		else
		{
			receivedData.assign(recvBuf, ret);
			std::cout << "接收数据线程从服务器接收到数据: " << receivedData << std::endl;
		}
	}
	return 0;
}

int main() 
{
	TCPClient client;

	if (client.connectToServer())
	{
		while (true)
		{
			// 发送数据示例
			std::string sendDataStr = "Hello, server!\n";
			client.sendData(sendDataStr);

			// 简单的休眠,避免过于频繁循环
			Sleep(100);
		}
	}
	return 0;
}

1. 接收数据线程相关的成员变量

  • m_receiveThreadHandle:用于存储接收数据线程的句柄,通过_beginthreadex函数创建线程时获取,用于后续对线程的操作,比如等待线程结束、关闭线程句柄等。
  • m_receiveThreadRunning:布尔类型变量,用于标记接收数据线程是否正在运行,在启动线程时设置为true,当需要停止线程(比如断开连接或者程序结束时)设置为false,线程函数内部会根据这个变量来判断是否继续循环接收数据。

2. connectToServer函数

在成功连接到服务器后,不仅将m_connected标记设置为true,还会创建并启动接收数据线程。通过_beginthreadex函数创建线程,传入ReceiveDataThread函数作为线程执行的入口点,并将当前TCPClient对象指针this作为参数传递进去,以便在线程函数中能够访问对象的成员变量和函数。如果线程创建失败,会关闭套接字并返回false,表示连接失败;若线程创建成功,则将m_receiveThreadRunning设置为true,表示接收数据线程开始运行。

3. disconnect函数

除了关闭套接字并将m_connected标记设置为false外,还会将m_receiveThreadRunning设置为false,通知接收数据线程停止运行。这样线程函数在下次循环判断时就会退出循环,结束线程的执行。

4. reconnect`函数

在重连成功后,除了进行之前的一些连接相关的设置外,还需要重新创建并启动接收数据线程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2266158.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为:数字化转型只有“起点”,没有“终点”

上个月&#xff0c;我收到了一位朋友的私信&#xff0c;他询问我是否有关于华为数字化转型的资料。幸运的是&#xff0c;我手头正好收藏了一些&#xff0c;于是我便分享给他。 然后在昨天&#xff0c;他又再次联系我&#xff0c;并感慨&#xff1a;“如果当初我在进行企业数字…

count(1)、count(_)与count(列名)的区别?

大家好&#xff0c;我是锋哥。今天分享关于【count(1)、count(_)与count(列名)的区别&#xff1f;】面试题。希望对大家有帮助&#xff1b; count(1)、count(_)与count(列名)的区别&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 SQL 中&#xff0c…

AAAI-2024 | 大语言模型赋能导航决策!NavGPT:基于大模型显式推理的视觉语言导航

作者&#xff1a;Gengze Zhou, Yicong Hong, Qi Wu 单位&#xff1a;阿德莱德大学&#xff0c;澳大利亚国立大学 论文链接&#xff1a; NavGPT: Explicit Reasoning in Vision-and-Language Navigation with Large Language Models &#xff08;https://ojs.aaai.org/index.p…

Linux高级--2.4.1 网络概念(分层、TCP)

关于网络分层理解的难点 对于一般人&#xff08;不参与设计和维护网络协议栈的人&#xff09;来讲&#xff0c;物理层和应用层很容易理解&#xff0c;也很好记住。首先&#xff0c;物理层是看的到的网线、基站的实体。再者&#xff0c;应用层是用户自己参与编写的程序。 而那…

使用VSCode Debugger 调试 React项目

一般我们调试代码时&#xff0c;用的最多的应该就是console.log方式了&#xff0c;还有的是使用Chrome DevTools 通过在对应的 sourcemap代码位置打断点进行调试&#xff0c;除了上面两种方式外还有一种更好用的调试方式&#xff1a; VSCode Debugger。 VSCode Debugger可以直…

Redis-十大数据类型

Reids数据类型指的是value的类型&#xff0c;key都是字符串 redis-server:启动redis服务 redis-cli:进入redis交互式终端 常用的key的操作 redis的命令和参数不区分大小写 &#xff0c;key和value区分 查看当前库所有的key keys * 判断某个key是否存在 exists key 查看key是什…

Git--tag标签远程管理

目录 一、git 标签 tag管理 1.创建一个轻量级标签 2.创建一个带有附注的标签 3.删除标签 二、标签推送 1.再创建两个分支 2.把多个标签推送到远程 三、标签拉取 四、删除远程标签 1.命令 2.查看远程仓库&#xff0c;标签被删除 3.远程标签删除后本地标签不会消失&a…

通过nginx设置一个图片服务器,并使用 Nginx 作为反向代理

通过nginx设置一个图片服务器&#xff0c;并使用 Nginx 作为反向代理 安装nginx 首先需要去官网下载一个nginx&#xff0c;我这里下载了最新的稳定版本&#xff1a;nginx-1.26.2&#xff0c;下载下来是一个压缩包&#xff0c;解压之后就可以直接用了。 修改nginx的配置文件 …

第十六届“蓝桥杯”全国软件和信息技术专业人才大赛简介及资料大全

蓝桥杯全国软件和信息技术专业人才大赛是由工业和信息化部人才交流中心主办的一项全国性竞赛&#xff0c;面向全国高校大学生&#xff0c;累计参赛院校超过1200余所&#xff0c;参赛人数达40万人&#xff0c;是我国极有影响力的高校IT类赛事。 “第十六届蓝桥杯全国软件和信息…

快速理解24种设计模式

简单工厂模式 建立产品接口类&#xff0c;规定好要实现方法。 建立工厂类&#xff0c;根据传入的参数&#xff0c;实例化所需的类&#xff0c;实例化的类必须实现指定的产品类接口 创建型 单例模式Singleton 保证一个类只有一个实例&#xff0c;并提供一个访问他它的全局…

【山西长治】《长治市市直部门政务信息化建设项目预算编制规范和预算编制标准》(长财行[2022]25号)-省市费用标准解读系列32

《长治市市直部门政务信息化建设项目预算编制规范和预算编制标准(试行)》&#xff08;长财行[2022]25号&#xff09;于2022年8月1日开始试行&#xff0c;此标准由长治市财政局、长治市行政审批管理局编制&#xff0c;是对信息化建设项目预算管理的基本要求&#xff0c;主要适用…

Docker 入门:如何使用 Docker 容器化 AI 项目(二)

四、将 AI 项目容器化&#xff1a;示例实践 - 完整的图像分类与 API 服务 让我们通过一个更完整的 AI 项目示例&#xff0c;展示如何将 AI 项目容器化。我们以一个基于 TensorFlow 的图像分类模型为例&#xff0c;演示如何将训练、推理、以及 API 服务过程容器化。 4.1 创建 …

Java和Go语言的优劣势对比

文章目录 Java和Go语言的优劣势对比一、引言二、设计哲学与语法特性1、设计哲学2、语法特性 三、性能与内存管理1、性能2、内存管理和垃圾回收 四、并发编程模型五、使用示例1、Go语言示例代码2、Java语言示例代码 六、对比表格七、总结 Java和Go语言的优劣势对比 一、引言 在…

Docker怎么关闭容器开机自启,批量好几个容器一起操作?

环境&#xff1a; WSL2 docker v25 问题描述&#xff1a; Docker怎么关闭容器开机自启&#xff0c;批量好几个容器一起操作&#xff1f; 解决方案&#xff1a; 在 Docker 中&#xff0c;您可以使用多种方法来关闭容器并配置它们是否在系统启动时自动启动。以下是具体步骤和…

Pytorch | 利用BIM/I-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用BIM/I-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集BIM介绍基本原理算法流程 BIM代码实现BIM算法实现攻击效果 代码汇总bim.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器&#xff1a; Pytorch | 从零构建AlexNet对CIFAR10进行分类 Py…

网狐旗舰版源码搭建概览

简单的列一下&#xff1a; 服务端源码内核源码移动端源码核心移动端源码AI控制工具源码多款子游戏源码前端、管理后台、代理网站源码数据库自建脚本UI工程源码配置工具及二次开发帮助文档 编译环境要求 VS2015 和 Cocos3.10 环境&#xff0c;支持移动端 Android 一键编译&am…

【QT】:QT(介绍、下载安装、认识 QT Creator)

背景 &#x1f680; 在我们的互联网中的核心岗位主要有以下几种 开发&#xff08;程序员&#xff09;测试运维&#xff08;管理机器&#xff09;产品经理&#xff08;非技术岗位&#xff0c;提出需求&#xff09; 而我们这里主要关注的是开发方向&#xff0c;开发岗位又分很…

MySQL 数据”丢失”事件之 binlog 解析应用

事件背景 客户反馈在晚间数据跑批后,查询相关表的数据时,发现该表的部分数据在数据库中不存在 从应用跑批的日志来看,跑批未报错,且可查到日志中明确显示当时那批数据已插入到数据库中 需要帮忙分析这批数据丢失的原因。 备注:考虑信息敏感性,以下分析场景测试环境模拟,相关数据…

熊军出席ACDU·中国行南京站,详解SQL管理之道

12月21日&#xff0c;2024 ACDU中国行在南京圆满收官&#xff0c;本次活动分为三个篇章——回顾历史、立足当下、展望未来&#xff0c;为线上线下与会观众呈现了一场跨越时空的技术盛宴&#xff0c;吸引了众多业内人士的关注。云和恩墨副总经理熊军出席此次活动并发表了主题演讲…

Spring01 - 工厂篇

Spring入门(上)-工厂篇 文章目录 Spring入门(上)-工厂篇一&#xff1a;引言1&#xff1a;EJB存在的问题2&#xff1a;什么是Spring3&#xff1a;设计模式和反射工厂 二&#xff1a;第一个spring程序1&#xff1a;环境搭建2&#xff1a;核心API - ApplicationContext2.1&#xf…