思维导图
为什么要引入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.2
WORD 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;
}
// 开始监听连接,最大连接数为 15
if (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.2
if (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; // 使用 IPv4
addrSrv.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.2
WSADATA wsaData; // 用于保存 WinSock 数据
int ret = WSAStartup(version, &wsaData); // 初始化 WinSock 库
if (ret != 0) { // 如果初始化失败
return -1; // 返回错误码 -1
}
SOCKET sockSrv = -1; // 定义服务器套接字,初始化为 -1 表示未初始化
do {
// 检查 WinSock 版本是否为 2.2
if (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); // 设置端口为 6000
SOCKADDR_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; // 如果已初始化,直接返回 true
WORD version = MAKEWORD(2, 2); // 设置所需的 WinSock 版本 2.2
WSADATA wsaData; // 用于保存 WinSock 数据
int ret = ::WSAStartup(version, &wsaData); // 初始化 WinSock
if (ret != 0) // 如果初始化失败,返回 false
return false;
// 检查实际使用的版本是否为 2.2
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
return false; // 版本不匹配,返回 false
m_bInit = true; // 成功初始化,标记为 true
return 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) // 如果套接字创建失败,返回 false
return 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; // 监听失败,返回 false
return 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的其他用途
分配堆内存
把堆内存包裹成对象,构造函数分配堆内存,析构函数释放堆内存。
多线程锁的获取和释放
把锁包裹成对象,构造函数获取锁,析构函数释放锁。