在前几篇文章中,我们深入探讨了 Socket 编程的基础知识以及 I/O 复用模型。现在,是时候把这些理论付诸实践,构建一个真正的高性能网络应用了。本文将展示如何使用 C++ 编写模块化、可扩展的服务器和客户端程序,重点关注性能、可用性、安全性等关键方面,为你打造高水准的编码体验。
一、架构设计
在动手编码之前,我们先来规划一下程序的整体架构。
我们将分别实现一个服务器端类和客户端类,通过面向对象的设计模式,实现代码的模块化和可扩展性。
另外,服务器端将采用 I/O 复用模型,可以高效处理大量并发连接。
服务器端类的主要职责包括:
- 初始化服务器套接字
- 绑定地址
- 监听连接请求
- 接收连接
- 读写数据
客户端类:
-
负责创建客户端套接字
-
连接服务器
-
发送数据
-
接收响应
为了提高程序的可用性,我们还需要在关键环节添加错误处理机制,确保意外情况下资源能安全回收,程序不会崩溃。
同时,通过使用 C++ 11 的现代语言特性,如 lambda 表达式、智能指针等,可以大幅增强代码的安全性和可读性。
二、服务器端实现
服务器端类的核心逻辑在于事件循环,它使用 poll() 函数监视套接字活动,高效地响应每一个就绪事件。
下面是服务器端类的框架代码:
class TcpServer {
public:
TcpServer(const std::string& ip, int port)
: m_ip(ip), m_port(port) {}
void start() {
// 初始化服务器套接字
// ...
while (true) {
// 使用 poll() 监视套接字活动
int nfds = ::poll(m_pollfds.data(), m_pollfds.size(), -1);
if (nfds == -1) {
// 处理错误
continue;
}
// 处理就绪事件
for (auto& pollfd : m_pollfds) {
if (pollfd.revents & POLLIN) {
if (pollfd.fd == m_listensock) {
// 接受新连接
} else {
// 读取数据
}
} else if (pollfd.revents & POLLOUT) {
// 发送数据
}
}
}
}
private:
std::string m_ip;
int m_port;
int m_listensock;
std::vector<pollfd> m_pollfds;
// ...
};
在事件循环中,我们首先调用 poll() 函数获取就绪事件列表。
然后对每一个就绪事件进行处理,如果是监听套接字就绪,则接受新连接;如果是数据套接字就绪,则读取或发送数据。
接下来,我们来实现几个关键函数,包括初始化服务器套接字、接受连接以及读写数据等。
void TcpServer::init_server_socket() {
m_listensock = ::socket(AF_INET, SOCK_STREAM, 0);
if (m_listensock == -1) {
// 处理错误
return;
}
int opt = 1;
::setsockopt(m_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(m_ip.c_str());
addr.sin_port = htons(m_port);
if (::bind(m_listensock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
// 处理错误
return;
}
if (::listen(m_listensock, SOMAXCONN) == -1) {
//