思维导图
为什么要引入RAII
有一个简单的服务器例子。在Windows
系统上写一个C++
程序,在客户端请求连接时,给客户端发一条"Hello World"
消息,然后关闭连接。不需要保证客户端一定能收到。
程序实现流程
- 创建
socket
- 绑定
IP
地址和端口号 - 在该
IP
地址和端口号上启动监听,循环等待客户端连接。客户端连接成功后,发消息,然后断开连接。
实现版本一
在Windows
上使用网络通信API
时,需要通过 WSAStartup
函数初始化 WinSock
库,并在程序结束时使用 WSACleanup
进行清理。代码中充斥着用于避免出错的重复资源清理逻辑closesocket(sockSrv)
和WSACleanup()
#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib")int main() {// 初始化 WinSock 库,设置使用版本为 2.2WORD version = MAKEWORD(2, 2);WSADATA wsaData;int ret = WSAStartup(version, &wsaData);if (ret != 0) {// 初始化失败,清理并退出WSACleanup();return -1;}// 检查是否成功获得所请求的版本if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {// 如果不匹配,清理并退出WSACleanup();return -2;}// 创建服务器套接字,指定使用 IPv4 协议、TCP 协议SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET) {// 创建套接字失败,清理并退出WSACleanup();return -3;}// 设置服务器地址结构SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 绑定到所有可用的网络接口addrSrv.sin_family = AF_INET; // 使用 IPv4 协议addrSrv.sin_port = htons(6000); // 设置监听端口为 6000// 绑定套接字到指定地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0) {// 绑定失败,清理并退出closesocket(sockSrv);WSACleanup();return -4;}// 开始监听连接,最大连接数为 15if (listen(sockSrv, 15) != 0) {// 监听失败,清理并退出closesocket(sockSrv);WSACleanup();return -5;}// 客户端连接地址结构SOCKADDR_IN addrClient;int len = sizeof(SOCKADDR);// 进入服务器主循环,等待并接受客户端连接while (true) {// 接受客户端连接SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET) {// 接受连接失败,跳出循环break;}// 向客户端发送 "hello world" 字符串const char* message = "hello world";send(sockClient, message, strlen(message), 0);// 关闭与客户端的连接closesocket(sockClient);}// 关闭服务器套接字closesocket(sockSrv);// 清理 WinSock 库WSACleanup();return 0;
}
先分配资源,再进行相关操作,在任意中间步骤出错时都对响应的资源进行回收,如果中间部分没有出错,就在资源使用完毕后对其进行回收。
这样编写代码容易出错,而且会造成大量代码重复。
实现版本二
使用goto
语句跳转到统一的清理点进行资源清理操作。
#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib")int main() {SOCKADDR_IN addrSrv; // 服务器地址结构SOCKET sockSrv; // 服务器套接字SOCKADDR_IN addrClient; // 客户端地址结构int len = sizeof(SOCKADDR); // 地址结构的大小const char* message = "hello world"; // 发送的消息// 初始化 WinSock 库,使用 2.2 版本WORD version = MAKEWORD(2, 2);WSADATA wsaData;int ret = WSAStartup(version, &wsaData);if (ret != 0) { // 如果初始化失败,跳转到 cleanup2 清理资源goto cleanup2;return -1; // 返回初始化失败的错误码}// 检查所初始化的 WinSock 版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {goto cleanup2;return -2; // 如果版本不匹配,清理资源并返回错误码}// 创建一个 TCP 套接字sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET) { // 如果创建套接字失败,跳转到 cleanup2 清理资源goto cleanup2;return -3; // 返回创建套接字失败的错误码}// 填充服务器地址结构addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 设置为任意 IP 地址addrSrv.sin_family = AF_INET; // 使用 IPv4addrSrv.sin_port = htons(6000); // 监听端口 6000// 绑定套接字到指定的地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0) { goto cleanup1; // 如果绑定失败,跳转到 cleanup1 清理服务器套接字return -4; // 返回绑定失败的错误码}// 开始监听客户端连接,最多允许 15 个客户端排队if (listen(sockSrv, 15) != 0) {goto cleanup1; // 如果监听失败,跳转到 cleanup1 清理服务器套接字return -5; // 返回监听失败的错误码}// 接受客户端连接while (true) {SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET) { // 如果接受连接失败,跳出循环break;}// 向客户端发送消息send(sockClient, message, strlen(message), 0);closesocket(sockClient); // 关闭客户端套接字}// 跳转到 cleanup1 清理服务器套接字goto cleanup1;cleanup1:closesocket(sockSrv); // 关闭服务器套接字
cleanup2:WSACleanup(); // 清理 WinSock 库资源return 0; // 程序结束
}
goto
语句要慎用,会使得程序结构混乱。
实现版本三
使用do{...}while(0)
循环的break
特性将资源回收集中到一个地方。
#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib,"ws2_32.lib") // 链接 ws2_32 库,这是 WinSock 编程所必需的int main() {WORD version = MAKEWORD(2, 2); // 定义要使用的 WinSock 版本 2.2WSADATA wsaData; // 用于保存 WinSock 数据int ret = WSAStartup(version, &wsaData); // 初始化 WinSock 库if (ret != 0) { // 如果初始化失败return -1; // 返回错误码 -1}SOCKET sockSrv = -1; // 定义服务器套接字,初始化为 -1 表示未初始化do {// 检查 WinSock 版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)break; // 如果版本不匹配,跳出循环sockSrv = socket(AF_INET, SOCK_STREAM, 0); // 创建一个 TCP 套接字if (sockSrv == INVALID_SOCKET) // 如果套接字创建失败break; // 跳出循环SOCKADDR_IN addrSrv; // 服务器地址结构addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 绑定到所有可用的网络接口addrSrv.sin_family = AF_INET; // 使用 IPv4 协议addrSrv.sin_port = htons(6000); // 设置端口为 6000SOCKADDR_IN addrClient; // 客户端地址结构int len = sizeof(SOCKADDR); // 地址结构的大小const char* message = "hello world"; // 发送的消息内容// 绑定套接字到指定的地址和端口if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0)break; // 绑定失败,跳出循环// 开始监听客户端连接,最大排队 15 个连接请求if (listen(sockSrv, 15) != 0)break; // 监听失败,跳出循环// 持续接受客户端连接while (true) {SOCKET sockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len); // 等待客户端连接if (sockClient == INVALID_SOCKET) // 如果接受连接失败break; // 跳出循环send(sockClient, message, strlen(message), 0); // 向客户端发送消息closesocket(sockClient); // 关闭客户端套接字}} while (0); // 结束 do-while 循环if (sockSrv != -1) // 如果服务器套接字有效closesocket(sockSrv); // 关闭服务器套接字WSACleanup(); // 清理 WinSock 库return 0; // 程序成功结束
}
很巧妙,但C++
有更好的写法。
实现版本四
使用RAII
(资源获取就是初始化),资源在我们拿到时就初始化,一旦不需要改资源,就自动释放资源。
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <stdio.h>#pragma comment(lib, "ws2_32.lib")// 创建一个 ServerSocket 类,封装了网络编程的各个步骤
class ServerSocket {
public:// 构造函数,初始化成员变量ServerSocket() {m_ListenSocket = -1; // 初始化监听套接字为无效值}// 析构函数,释放资源~ServerSocket() {// 关闭监听套接字if (m_ListenSocket != -1)::closesocket(m_ListenSocket);}// 初始化 WinSock 库和套接字static bool DoInit() {if (m_bInit) return true; // 如果已初始化,直接返回 trueWORD version = MAKEWORD(2, 2); // 设置所需的 WinSock 版本 2.2WSADATA wsaData; // 用于保存 WinSock 数据int ret = ::WSAStartup(version, &wsaData); // 初始化 WinSockif (ret != 0) // 如果初始化失败,返回 falsereturn false;// 检查实际使用的版本是否为 2.2if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)return false; // 版本不匹配,返回 falsem_bInit = true; // 成功初始化,标记为 truereturn true;}// 清理 WinSock 库static void DoCleanup() {if (m_bInit) {::WSACleanup();m_bInit = false;}}// 创建并初始化监听套接字bool DoListen(const char* ip, short port = 6000) {if (!DoInit()) return false; // 如果 WinSock 初始化失败,返回 false// 创建一个 TCP 套接字m_ListenSocket = ::socket(AF_INET, SOCK_STREAM, 0);if (m_ListenSocket == INVALID_SOCKET) // 如果套接字创建失败,返回 falsereturn false;SOCKADDR_IN addrSrv; // 服务器地址结构addrSrv.sin_addr.S_un.S_addr = inet_addr(ip); // 设置 IP 地址addrSrv.sin_family = AF_INET; // 使用 IPv4 协议addrSrv.sin_port = htons(port); // 设置端口(默认 6000)// 绑定套接字到指定地址和端口if (::bind(m_ListenSocket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) != 0)return false; // 绑定失败,返回 false// 启动监听,backlog 是请求队列的最大长度(默认为 15)if (::listen(m_ListenSocket, 15) != 0)return false; // 监听失败,返回 falsereturn true; // 成功开始监听,返回 true}// 接受客户端连接并发送消息void DoAccept() {SOCKADDR_IN addrClient; // 客户端地址结构int len = sizeof(SOCKADDR); // 地址结构的大小const char* message = "hello world"; // 发送的消息while (true) {// 接受客户端连接请求SOCKET sockClient = ::accept(m_ListenSocket, (SOCKADDR*)&addrClient, &len);if (sockClient == INVALID_SOCKET)break; // 如果连接失败,跳出循环// 向客户端发送消息::send(sockClient, message, strlen(message), 0);// 关闭客户端套接字::closesocket(sockClient);}}private:static bool m_bInit; // 是否初始化的标志位,静态成员SOCKET m_ListenSocket; // 监听套接字
};// 静态成员初始化
bool ServerSocket::m_bInit = false;int main() {ServerSocket serverSocket; // 创建 ServerSocket 对象// 绑定服务器 IP 和端口,若失败则退出if (!serverSocket.DoListen("0.0.0.0", 6000))return false;// 接受客户端连接并发送消息,若失败则退出serverSocket.DoAccept();// 清理 WinSock 库ServerSocket::DoCleanup();return 0; // 程序正常结束
}
RAII的其他用途
分配堆内存
把堆内存包裹成对象,构造函数分配堆内存,析构函数释放堆内存。
多线程锁的获取和释放
把锁包裹成对象,构造函数获取锁,析构函数释放锁。
推荐一下
https://github.com/0voice