当前位置: 首页 > article >正文

TCP并发服务器的实现

一请求一线程

问题

当客户端数量较多时,使用单独线程为每个客户端处理请求可能导致系统资源的消耗过大和性能瓶颈。

资源消耗:
  • 线程创建和管理开销:每个线程都有其创建和销毁的开销,特别是在高并发环境中,这种开销会显著增加。
  • 内存消耗:每个线程通常需要分配一定的栈空间,这会增加内存使用量。
  • 上下文切换:操作系统需要频繁地切换线程上下文,这会消耗CPU资源。
性能瓶颈:
  • 线程竞争:大量线程会导致线程之间竞争共享资源,如内存和CPU时间,降低整体性能。
  • 调度开销:操作系统调度大量线程时的开销可能会影响应用程序的响应时间和吞吐量。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <libgen.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>

#define BUFFER_LENGTH 1024

// 客户端处理线程的例程
void *client_routine(void* arg) {
    int clientfd = *(int*)arg;  // 获取传入的客户端套接字描述符

    while (1) {
        char buffer[BUFFER_LENGTH];  // 定义接收缓冲区
        int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);  // 接收数据

        if (len < 0) {
            // 接收数据出错
            perror("recv error");
            close(clientfd);  // 关闭客户端套接字
            break;
        } else if (len == 0) {
            // 客户端关闭连接
            close(clientfd);  // 关闭客户端套接字
            break;
        } else {
            // 打印接收到的数据
            printf("Recv: %s, %d byte(s)\n", buffer, len);
        }
    }
    return NULL;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        // 参数错误,未提供端口号
        printf("usage: %s port\n", basename(argv[0]));
        return -1;
    }

    int port = atoi(argv[1]);  // 从命令行参数获取端口号

    // 创建监听用的套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return 1;
    }

    // 配置套接字地址
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));  // 清空地址结构
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);  // 转换端口号为网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的接口

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in))) {
        perror("bind failed");
        return 2;
    }

    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        return 3;
    }

    while (1) {
        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(struct sockaddr_in));  // 清空客户端地址结构
        socklen_t client_len = sizeof(client_addr);

        // 接受客户端连接
        int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
        if (clientfd < 0) {
            perror("accept failed");
            continue;
        }

        // 为每个客户端创建一个线程
        pthread_t thread_id;
        if (pthread_create(&thread_id, NULL, client_routine, &clientfd) != 0) {
            perror("pthread_create failed");
            close(clientfd);  // 创建线程失败时关闭客户端套接字
        }

        // 可选:分离线程以避免线程资源泄漏
        pthread_detach(thread_id);
    }

    // 关闭监听套接字(实际上这部分代码永远不会到达)
    close(sockfd);
    return 0;
}

使用ifconfig查看服务器程序所在主机的IP地址。

首先启动所写的tcp服务器,即确保tcp_server_test.cpp已经编译并运行在虚拟机上,监听指定的端口(8888)。

打开三个网络调试助手(NetAssist),在每个助手中配置远端主机地址为你的tcp服务器地址(在虚拟机用ifconfig查看),端口设置为 8888,点击连接。可以分别向tcp服务器写数据。

利用epoll

优点:

高效:

epoll采用事件驱动的方式,仅在有事件发生时通知应用程序,避免了轮询带来的性能开销。

可扩展性

能够处理大量的文件描述符,适合高并发应用。

边缘触发

支持边缘触发(EPOLLET),在数据到达时通知一次,适合需要高效处理大量事件的场景。

缺点

复杂性

编程模型较为复杂,需要正确处理事件并维持数据流动性,可能导致代码较难维护。

资源消耗

虽然epoll高效,但在高负载情况下,资源使用仍然会增加,如内存和系统调用次数。

边缘触发处理

需要确保处理所有数据,否则可能错过事件,增加了编程的复杂性。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <libgen.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <sys/epoll.h>

#define BUFFER_LENGTH 1024
#define EPOLL_SIZE 1024

int main(int argc, char* argv[]) {
    if (argc < 2) {
        // 参数错误,未提供端口号
        printf("usage: %s port\n", basename(argv[0]));
        return -1;
    }

    int port = atoi(argv[1]);  // 从命令行参数获取端口号

    // 创建监听用的套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return 1;
    }

    // 配置套接字地址
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));  // 清空地址结构
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);  // 转换端口号为网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的接口

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in))) {
        perror("bind failed");
        close(sockfd);
        return 2;
    }

    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        return 3;
    }

    // 创建 epoll 实例
    int epfd = epoll_create1(0);  // 使用 epoll_create1(0) 代替 epoll_create(0)
    if (epfd < 0) {
        perror("epoll_create failed");
        close(sockfd);
        return 4;
    }

    struct epoll_event events[EPOLL_SIZE] = {0};

    // 添加监听套接字到 epoll 实例
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;  // 设置为边缘触发模式
    ev.data.fd = sockfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) < 0) {
        perror("epoll_ctl failed");
        close(sockfd);
        close(epfd);
        return 5;
    }

    while (1) {
        // 等待事件发生
        int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1);
        if (nready < 0) {
            perror("epoll_wait failed");
            break;  // 退出循环
        }

        for (int i = 0; i < nready; i++) {
            if (events[i].data.fd == sockfd) {
                struct sockaddr_in client_addr;
                memset(&client_addr, 0, sizeof(struct sockaddr_in));  // 清空客户端地址结构
                socklen_t client_len = sizeof(client_addr);

                // 接受客户端连接
                int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
                if (clientfd < 0) {
                    perror("accept failed");
                    continue;
                }

                // 将新的客户端套接字添加到 epoll 实例中,并设置为边缘触发模式
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = clientfd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev) < 0) {
                    perror("epoll_ctl failed");
                    close(clientfd);
                }
            } else {
                // 处理客户端套接字的事件
                int clientfd = events[i].data.fd;
                char buffer[BUFFER_LENGTH];  // 定义接收缓冲区
                int len;

                // 处理所有可用的数据
                while ((len = recv(clientfd, buffer, BUFFER_LENGTH, 0)) > 0) {
                    buffer[len] = '\0';  // 添加字符串结束标志
                    printf("Recv: %s, %d byte(s)\n", buffer, len);
                }

                if (len < 0) {
                    perror("recv error");
                }

                // 客户端关闭连接或出错
                close(clientfd);  // 关闭客户端套接字
                epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL);
            }
        }
    }

    // 关闭监听套接字和 epoll 实例
    close(sockfd);
    close(epfd);
    return 0;
}

推荐一下 

0voice · GitHub


http://www.kler.cn/a/309472.html

相关文章:

  • 通过脚本,发起分支合并请求和打tag
  • Mysql数据类型面试题15连问
  • Oracle RAC的thread
  • Elasticsearch中什么是倒排索引?
  • 如何线程安全的使用HashMap
  • 实现3D热力图
  • 某思CMS V10存在SQL注入漏洞
  • 深入理解Linux中的多路复用技术:select、poll与epoll
  • 基于图卷积网络的轻量化推荐模型(论文复现)
  • 【Docker】docker的一些常用命令
  • 看Threejs好玩示例,学习创新与技术(二)
  • 创建索引遇到这个Bug,19c中还没有修复
  • echarts 自定义标注样式自定义tooltip弹窗样式
  • Redisson实现分布式锁(看门狗机制)
  • 【MySQL-初级】mysql基础操作(账户、数据库、表的增删查改)
  • 软考中级软件设计师——知识产权学习记录
  • Android Activity分屏设置
  • vue3前端开发-小兔鲜超市-本地购物车列表页面的统计计算
  • 新增的标准流程
  • Codeforces practice C++ 2024/9/11 - 2024/9/18
  • 常见数据湖的优劣对比
  • Rust表达一下中秋祝福,群发问候!
  • Spring Boot-依赖冲突问题
  • Verdin AM62 引脚复用配置
  • 检查和测绘室内防撞无人机技术详解
  • 机器学习的网络们