Socket编程权威指南(五)高性能 Socket 编程实战

2024-06-10 11:44

本文主要是介绍Socket编程权威指南(五)高性能 Socket 编程实战,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


在前几篇文章中,我们深入探讨了 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) {// 处理错误return;}m_pollfds.push_back({m_listensock, POLLIN, 0});
}void TcpServer::accept_connection() {struct sockaddr_in peeraddr;socklen_t peerlen = sizeof(peeraddr);int connfd = ::accept(m_listensock, (struct sockaddr*)&peeraddr, &peerlen);if (connfd == -1) {// 处理错误return;}std::cout << "New connection from " << inet_ntoa(peeraddr.sin_addr) << ":"<< ntohs(peeraddr.sin_port) << std::endl;m_pollfds.push_back({connfd, POLLIN, 0});
}ssize_t TcpServer::read_data(int sockfd, std::string& inbuf) {char buf[1024];ssize_t nbytes = ::recv(sockfd, buf, sizeof(buf), 0);if (nbytes > 0) {inbuf.append(buf, nbytes);}return nbytes;
}ssize_t TcpServer::write_data(int sockfd, const std::string& data) {return ::send(sockfd, data.data(), data.size(), 0);
}

init_server_socket() 函数中,我们创建服务器套接字、设置地址重用选项、绑定地址、进入监听状态,并将监听套接字加入 pollfds 数组。

accept_connection() 函数用于接受新连接,并将新的数据套接字加入 pollfds。

read_data()write_data() 则分别负责读取和发送数据。

需要注意的是,在所有函数中我们都添加了错误处理机制,确保异常情况下资源能正确释放。

你可以根据需要,扩展这些函数的功能,如添加日志记录、连接管理等。


三、客户端实现


相较于服务器端,客户端的实现则相对简单一些,主要包括连接服务器、发送数据和接收响应等操作。

下面是客户端类的代码框架:

class TcpClient {
public:TcpClient(const std::string& ip, int port): m_ip(ip), m_port(port), m_sockfd(-1) {}~TcpClient() {if (m_sockfd != -1) {::close(m_sockfd);}}bool connect() {m_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (m_sockfd == -1) {// 处理错误return false;}struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr(m_ip.c_str());servaddr.sin_port = htons(m_port);if (::connect(m_sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {// 处理错误return false;}return true;}ssize_t send(const std::string& data) {return ::send(m_sockfd, data.data(), data.size(), 0);}ssize_t receive(std::string& buf) {char temp[1024];ssize_t nbytes = ::recv(m_sockfd, temp, sizeof(temp), 0);if (nbytes > 0) {buf.append(temp, nbytes);}return nbytes;}private:std::string m_ip;int m_port;int m_sockfd;
};

在客户端类中,我们首先实现了 connect() 函数,用于创建套接字并连接到服务器端。

send()receive() 函数则分别用于发送数据和接收响应数据。

与服务器端类似,我们也在客户端类的每个关键环节添加了错误处理机制,并在析构函数中释放套接字资源。

你可以根据实际需求,扩展客户端类的功能,如支持重连、超时控制等。


四、优化


完成了服务器端和客户端的实现后,我们就可以进行性能优化了。

在性能优化方面,我们可以考虑以下几个方面:

1、I/O 模型优化: 虽然 poll() 已经比 select() 更高效,但在大规模并发场景下,它的性能将遭受限制。我们可以尝试使用更高效的 epoll 模型。

2、网络优化: 合理设置 TCP 参数,如发送/接收缓冲区大小、延迟确认等,可以提升网络吞吐量。另外,也可以考虑使用 UDP 代替 TCP,减少协议开销。

3、多线程/多进程: 在多核 CPU 环境下,使用多线程或多进程可以发挥更大的并行能力。不过这也会增加同步开销,需要权衡利弊。

4、异步 I/O: 传统的同步 I/O 模型会导致大量线程阻塞,我们可以尝试使用异步 I/O 或者基于事件驱动的 Reactor 模式。

5、数据压缩: 在网络带宽有限的情况下,使用合适的压缩算法可以减少网络传输量,提升吞吐量。

我们将使用更高效的epoll模型以及基于事件驱动的Reactor模式,并支持多线程以发挥更大的并行能力。

下面是优化后的服务器端代码:

#include <sys/epoll.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>// Reactor模式中的事件处理器接口
class EventHandler {
public:virtual ~EventHandler() {}virtual void handleInput(int sockfd) = 0;virtual void handleOutput(int sockfd) = 0;
};// 服务器端主类
class TcpServer {
public:TcpServer(const std::string& ip, int port, int numThreads): m_ip(ip), m_port(port), m_numThreads(numThreads) {}void start() {// 初始化服务器套接字init_server_socket();// 创建epoll实例m_epollfd = epoll_create1(0);if (m_epollfd == -1) {// 处理错误return;}// 启动工作线程std::vector<std::thread> threads;for (int i = 0; i < m_numThreads; ++i) {threads.emplace_back(&TcpServer::worker_thread, this);}// 事件循环while (true) {// 等待就绪事件int nfds = epoll_wait(m_epollfd, m_events.data(), m_events.size(), -1);if (nfds == -1) {// 处理错误continue;}// 分发就绪事件std::vector<EventHandler*> handlers;for (int i = 0; i < nfds; ++i) {int sockfd = m_events[i].data.fd;if (sockfd == m_listensock) {accept_connection();} else {if (m_events[i].events & EPOLLIN) {handlers.push_back(m_handlers[sockfd].get());}if (m_events[i].events & EPOLLOUT) {handlers.push_back(m_handlers[sockfd].get());}}}// 分发事件给工作线程distribute_events(std::move(handlers));}// 等待工作线程退出for (auto& thread : threads) {thread.join();}}private:void init_server_socket() {// 同前面的实现}void accept_connection() {// 同前面的实现m_handlers[connfd] = std::make_unique<ConnectionHandler>(connfd);struct epoll_event ev;ev.events = EPOLLIN | EPOLLET;ev.data.fd = connfd;epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, &ev);}void worker_thread() {while (true) {EventHandler* handler = nullptr;{std::unique_lock<std::mutex> lock(m_eventQueueMutex);m_eventQueueCond.wait(lock, [this] { return !m_eventQueue.empty(); });handler = m_eventQueue.front();m_eventQueue.pop();}if (handler) {handler->handleInput(handler->getSockfd());handler->handleOutput(handler->getSockfd());}}}void distribute_events(std::vector<EventHandler*>&& handlers) {{std::unique_lock<std::mutex> lock(m_eventQueueMutex);for (auto handler : handlers) {m_eventQueue.push(handler);}}m_eventQueueCond.notify_all();}std::string m_ip;int m_port;int m_numThreads;int m_listensock;int m_epollfd;std::vector<struct epoll_event> m_events;std::unordered_map<int, std::unique_ptr<EventHandler>> m_handlers;std::queue<EventHandler*> m_eventQueue;std::mutex m_eventQueueMutex;std::condition_variable m_eventQueueCond;
};// 连接处理器
class ConnectionHandler : public EventHandler {
public:ConnectionHandler(int sockfd) : m_sockfd(sockfd) {}void handleInput(int sockfd) override {// 读取数据并处理}void handleOutput(int sockfd) override {// 发送数据}int getSockfd() const { return m_sockfd; }private:int m_sockfd;
};

在这个优化后的实现中,我们引入了以下几个关键改进:

1、使用epoll代替poll:epoll是Linux下更高效的I/O复用机制,能够监视更多的文件描述符,并且无需像poll那样在每次调用时重新复制整个文件描述符集合。

2、采用Reactor模式:我们将事件处理逻辑与主循环分离,引入了EventHandler接口,并使用工作线程池来并行处理事件。主循环只负责等待就绪事件并分发给工作线程,而实际的事件处理则在工作线程中完成。

3、支持多线程:通过创建多个工作线程,可以充分利用多核CPU的并行计算能力,提高并发处理能力。工作线程通过条件变量和线程安全队列来获取待处理的事件。

4、使用智能指针管理资源:我们使用std::unique_ptr来管理ConnectionHandler实例,确保在连接关闭时资源能够正确释放。

5、使用EPOLLONESHOT和边缘触发模式:在epoll_ctl中,我们使用EPOLLONESHOT标志和EPOLLET标志,分别表示每次只监视一次事件,以及采用边缘触发模式。这样可以减少不必要的事件通知,提高效率。

通过这些优化,我们的网络服务器将拥有更高的并发处理能力和更好的性能表现。不过,由于引入了多线程,程序的复杂度也有所增加,需要格外注意线程安全和资源管理等问题。


五、socket 编程最佳实践和注意事项


在 C++ 中进行 socket 编程时,遵循最佳实践和注意事项是非常重要的。

以下是一些关键点示例:

1、定义明确的协议

使用固定长度的头部来定义消息结构,例如:

struct Message {size_t length; // 消息长度char type;    // 消息类型char data[];  // 消息数据
};

2、处理粘包和半包问题

接收消息时,根据头部信息读取完整的消息体:

size_t recvMessage(int sockfd, char* buffer, size_t bufferSize) {struct Message messageHeader;size_t bytesRead = recv(sockfd, &messageHeader, sizeof(messageHeader), 0);if (bytesRead == sizeof(messageHeader)) {size_t totalBytes = sizeof(messageHeader) + messageHeader.length;if (totalBytes > bufferSize) {// 缓冲区不足,处理错误return 0;}bytesRead += recv(sockfd, buffer, messageHeader.length, 0);}return bytesRead;
}

3、使用非阻塞 I/O

设置 socket 为非阻塞模式:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);

4、考虑使用缓冲区

使用 recv 函数读取数据到缓冲区:

char buffer[1024];
ssize_t bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);

5、注意字节序问题

使用 htonshtonl 进行网络字节序转换:

unsigned short port = htons(8080);
unsigned int ip = htonl(INADDR_ANY);

6、异常和错误处理

检查系统调用的返回值,并适当处理错误:

if (sockfd < 0) {perror("socket creation failed");exit(EXIT_FAILURE);
}

7、资源管理

确保关闭 socket 描述符:

close(sockfd);

8、性能优化

禁用 Nagle 算法:

int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(flag));

9、多线程和多进程

使用线程安全的方法处理并发:

std::thread clientThread(&handleClient, sockfd);
clientThread.detach();

这些代码示例提供了 C++ socket 编程的一些基本框架和考虑事项。在实际应用中,可能需要根据具体需求进行调整和扩展。


六、结语


需要说明的是,上面的代码只是一个基本框架,你可以根据实际需求进一步扩展和完善,比如添加连接管理、数据缓冲区、日志记录等功能模块。通过不断地探索和实践,相信你一定能掌握高性能网络编程的精髓,构建出卓越的网络应用!

这篇关于Socket编程权威指南(五)高性能 Socket 编程实战的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1048071

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG