C 语言网络编程 — 高并发 TCP 网络服务器

news2024/11/18 9:49:04

目录

文章目录

  • 目录
  • TCP Socket 编程示例
    • 服务端
    • 客户端
    • 测试
  • 高并发 TCP 网络服务器
    • I/O 并发模型设计
    • 系统文件描述符数量限制
    • 完全断开连接导致的性能问题
      • 关注 TCP 连接的状态
      • 合理配置 TCP 连接内核参数
      • 使用 shutdown() 来确保 Connection 被正常关闭
    • 断开重连问题
      • 使用 Heartbeat 来判断 Connection 是否 ACTIVE
      • 使用 select() 来进行 Heartbeat 心跳检查
    • 数据缓冲问题
    • 同步或异步 I/O 模式问题
    • Session 过期问题

TCP Socket 编程示例

在这里插入图片描述
请添加图片描述

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <sys/socket.h>


#define ERR_MSG(err_code) do {                                     \
    err_code = errno;                                              \
    fprintf(stderr, "ERROR code: %d \n", err_code);                \
    perror("PERROR message");                                      \
} while (0)

const int BUF_LEN = 100;


int main(void)
{
    /* 配置 Server Sock 信息。*/
    struct sockaddr_in srv_sock_addr;
    memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
    srv_sock_addr.sin_family = AF_INET;
    srv_sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 即 0.0.0.0 表示监听本机所有的 IP 地址。
    srv_sock_addr.sin_port = htons(6666);

    /* 创建 Server Socket。*/
    int srv_socket_fd = 0;
    if (-1 == (srv_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) {
        printf("Create socket file descriptor ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }
    /* 设置 Server Socket 选项。*/
    int optval = 1;
    if (setsockopt(srv_socket_fd,
                   SOL_SOCKET,    // 表示套接字选项的协议层。
                   SO_REUSEADDR,  // 表示在绑定地址时允许重用本地地址。这样做的好处是,当服务器进程崩溃或被关闭时,可以更快地重新启动服务器,而不必等待一段时间来释放之前使用的套接字。
                   &optval,
                   sizeof(optval)) < 0)
    {
        printf("Set socket options ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 绑定 Socket 与 Sock Address 信息。*/
    if (-1 == bind(srv_socket_fd,
                   (struct sockaddr *)&srv_sock_addr,
                   sizeof(srv_sock_addr)))
    {
        printf("Bind socket ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 开始监听 Client 发出的连接请求。*/
    if (-1 == listen(srv_socket_fd, 10))
    {
        printf("Listen socket ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 初始化 Client Sock 信息存储变量。*/
    struct sockaddr cli_sock_addr;
    memset(&cli_sock_addr, 0, sizeof(cli_sock_addr));
    int cli_sockaddr_len = sizeof(cli_sock_addr);

    int cli_socket_fd = 0;

    int recv_len = 0;
    char buff[BUF_LEN] = {0};

    /* 永远接受 Client 的连接请求。*/
    while (1)
    {
        if (-1 == (cli_socket_fd = accept(srv_socket_fd,
                                          (struct sockaddr *)(&cli_sock_addr),  // 填充 Client Sock 信息。
                                          (socklen_t *)&cli_sockaddr_len)))
        {
            printf("Accept connection from client ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 接收指定 Client Socket 发出的数据,*/
        if ((recv_len = recv(cli_socket_fd, buff, BUF_LEN, 0)) < 0)
        {
            printf("Receive from client ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }
        printf("Recevice data from client: %s\n", buff);

        /* 将收到的数据重新发送给指定的 Client Socket。*/
        send(cli_socket_fd, buff, recv_len, 0);
        printf("Send data to client: %s\n", buff);

        /* 每处理完一次 Client 请求,即关闭连接。*/
        close(cli_socket_fd);
        memset(buff, 0, BUF_LEN);
    }

    close(srv_socket_fd);
    return EXIT_SUCCESS;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <sys/socket.h>


#define ERR_MSG(err_code) do {                                     \
    err_code = errno;                                              \
    fprintf(stderr, "ERROR code: %d \n", err_code);                \
    perror("PERROR message");                                      \
} while (0)

const int BUF_LEN = 100;


int main(void)
{
    /* 配置 Server Sock 信息。*/
    struct sockaddr_in srv_sock_addr;
    memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
    srv_sock_addr.sin_family = AF_INET;
    srv_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    srv_sock_addr.sin_port = htons(6666);

    int cli_socket_fd = 0;
    char send_buff[BUF_LEN];
    char recv_buff[BUF_LEN];

    /* 永循环从终端接收输入,并发送到 Server。*/
    while (1) {

        /* 创建 Client Socket。*/
        if (-1 == (cli_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
        {
            printf("Create socket ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 连接到 Server Sock 信息指定的 Server。*/
        if (-1 == connect(cli_socket_fd,
                          (struct sockaddr *)&srv_sock_addr,
                          sizeof(srv_sock_addr)))
        {
            printf("Connect to server ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 从 stdin 接收输入,再发送到建立连接的 Server Socket。*/
        fputs("Send to server> ", stdout);
        fgets(send_buff, BUF_LEN, stdin);
        send(cli_socket_fd, send_buff, BUF_LEN, 0);
        memset(send_buff, 0, BUF_LEN);

        /* 从建立连接的 Server 接收数据。*/
        recv(cli_socket_fd, recv_buff, BUF_LEN, 0);
        printf("Recevice from server: %s\n", recv_buff);
        memset(recv_buff, 0, BUF_LEN);

        /* 每次 Client 请求和响应完成后,关闭连接。*/
        close(cli_socket_fd);
    }

    return EXIT_SUCCESS;
}

测试

编译:

$ gcc -g -std=c99 -Wall tcp_server.c -o tcp_server
$ gcc -g -std=c99 -Wall tcp_client.c -o tcp_client

运行:

  1. 先启动 TCP Server:
$ ./tcp_server
  1. 查看监听 Socket 是否绑定成功:
$ netstat -lpntu | grep 6666
tcp        0      0 0.0.0.0:6666            0.0.0.0:*               LISTEN      28675/./tcp_server
  1. 启动 TCP Client
$ ./tcp_client

高并发 TCP 网络服务器

I/O 并发模型设计

  1. 多进程模型:主进程负责 Listen 和 Accept 连接请求;Accept 后,就 fock 子进程来处理 read() 和 write()。缺点是,多进程数量有限,消耗资源也多。

  2. 多线程模型:使用线程来处理 read() 和 write() 会更加高效。但无论是使用多进程还是多线程,如果一个 TCP 连接只对应了一个进程或线程,就很难逃脱 C10K 的问题。

  3. I/O 多路复用模型:例如 epoll(),可以使得一个进程或线程能够处理多个 TCP 连接。

以典型的 I/O 多路复用模型 Nginx 为例,实现了 Master + Worker 软件架构。Worker 的数量通常等于 CPU Cores 的数量,并且每个 Worker 都采用了 epoll 模型。所有 Worker 都在 80/443 端口上 Listen 连接请求,并且把监听到的 client socket fds 添加在各自的 epoll 中,然后在 Events 发生时回调。

可见,I/O 多路复用模型大大增加了每个进程可以管理的 Socket 数量,直到操作系统 fd 最大数量限制为止,通常可以设置百万级别,即:单机单进程支持百万连接,epoll 是解决 C10K 的利器,很多开源软件用到了它。

系统文件描述符数量限制

Linux 中一切皆文件,每个 Socket 都有各自的文件描述符,作为系统操作这个 Socket 的文件句柄。Linux 中的每个 User Process 都有一个 fd 数组,保存了自己拥有的所有 fds。

为了系统的安全性,Linux 为预设 socket fd 的 Limit(数量限制)。在 Shell 中可以通过 ulimit 指令来查看并设置:

  1. 进程级别限制:fs.nr_open
  2. 系统级别限制:fs.file-max

fs.nr_open 总是应该小于等于 fs.file-max,同时这两个值越大,系统消耗的资源就越多,所以需要根据实际情况来进行设置。

完全断开连接导致的性能问题

四次挥手是一个冗长的过程,由于网络环境的复杂性与 TCP 连接的可靠性相违背,所以在某些特殊的场景中会出现相应的问题。比较常见的就是在高并发网络服务器场景中,由于 “完全断开连接“ 导致的性能问题。

TCP 协议规定,C/S 双方必须完整进行四次挥手,进入到 CLOSED 状态,各自的 Kernel 才会完全释放 Socket 资源。如果由于网络连通性或其他原因导致四次挥手没有完成,那么这个 Socket 的连接就会处于假死状态,并且继续占用系统资源。

具体而言,有以下几种情况:

  1. TCP 协议规定 TIME_WAIT 状态会持续 240s(2MSL),以此来保证后面新建的连接不会受到旧连接残留的延迟重发报文的影响。所以,高并发的网络服务器通常不应该主动 close(),而是让对方主动,避免出现大量的 TIME_WAIT 连接占用系统资源。

  2. TCP 协议规定 FIN_WAIT_2 状态有 60s(默认)超时等待时间,如果对方一直不 close(),那么 FIN_WAIT_2 也会一致占用系统资源。

  3. TCP 协议规定 CLOSE_WAIT 状态有 2h(默认)超时等待时间,如果由于某些原因,使得自己一直不 close(),那么系统负载在 2h 内可能会积累到崩溃的程度。

关注 TCP 连接的状态

所以,实际上,close() 并不会马上断开 Socket Connection,在高性能网络服务器中,需要非常关注 TCP 连接的状态情况。

查看 Linux 上的 TCP 连接的状态:

$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'                                                                                                                                                                                                  127 ↵
CLOSE_WAIT 1
TIME_WAIT 1
ESTABLISHED 17

合理配置 TCP 连接内核参数

# vi /etc/sysctl.conf

# 表示开启重用。允许将 TIME-WAIT Sockets 重新用于新的 TCP 连接,默认为 0,表示关闭。
net.ipv4.tcp_tw_reuse = 1

# 表示开启 TCP 连接中 TIME-WAIT Sockets 的快速回收,默认为 0,表示关闭。
net.ipv4.tcp_tw_recycle = 1

# 表示系統等待 FIN_WAIT 超时时间。
net.ipv4.tcp_fin_timeout

使用 shutdown() 来确保 Connection 被正常关闭

推荐在 close 之前调用 shutdown 函数来确保连接会被正常关闭。

而且 shutdown 函数也提供了多种不同的关闭方式:

  • SHUT_RD:关闭读,不能使用 read / recv。常用在服务端程序,立即关闭读取客户端的请求,但仍会完成对之前请求的响应。
  • SHUT_WR:关闭写,不能使用 write / send。常用在客户端程序,立即关闭写操作,但仍可以继续将响应数据读完。
  • SHUT_RDWR:关闭读写,不能使用 read / recv / write / send。常用在对精度要求不高的场景。

断开重连问题

Socket API 没有原生的自动重连机制,需要 Application 自身实现网络断开重连功能。在执行 Send 和 Receive 之前,检查 Connection 是否 ACTIVE。

使用 Heartbeat 来判断 Connection 是否 ACTIVE

Connection 状态检测是服务端的特性,服务端以此来决定回收 Socket 资源,或者执行断开重连。而客户端只需要重新连接、重新发送即可。

问题是,初始情况下,服务端无法有效的区分客户端目前是处于 “长期空闲” 还是 “下线“ 状态。解决这个问题的思路就是通过建立 Heartbeat 心跳协议,让客户端始终忙碌,以此来排除掉客户端 “长期空闲“ 的情况。

另外,考虑到服务端的压力,Heartbeat 特性通常考虑由客户端来实现。

使用 select() 来进行 Heartbeat 心跳检查

select() 是异步的非阻塞 I/O 接口,更适用于 Heartbeat 心跳检查场景。

数据缓冲问题

send 和 recv 函数都具有数据缓冲特性。

以 recv 为例,如果发送方的数据量超出了接收方一次所允许的最大接收量,那么数据就会被截断,并将剩余的数据缓冲在接收方。然后,当接收方再次调用 recv 函数时,剩余的数据才会从缓冲区取出。经常的,为了得到一个完整的响应结果可能需要调用多次 recv 函数。

缓冲特性引入了一个 “数据完整性“ 的问题,需有程序自行保证 send 和 recv 的完整性。

同步或异步 I/O 模式问题

根据不同的场景去选择同步还是异步 I/O 模式非常重要,通常的:

  • 在高并发且不关注执行结果的场景中使用异步 I/O 模式。
  • 在对程序执行的稳定性、对执行结果响应的准确性都有很高要求的场景下使用同步 I/O 模式,并且需要保证每次 send 和 recv 的原子性。

Session 过期问题

为了更高效的进行数据传输,程序往往会在一个 Socket Connection 中维护多个 Sessions。此时,除了需要考虑 Conection 的状态之外,还需要考虑 Session 是否过期的问题。

通常的,我们需要在 Send 和 Receive 之前,首先检查 Connection 是否 ACTIVE,然后检查 Session 是否过期:

  • 如果连接失效:则重新建立 Connection,并且重新创建 Session。
  • 如果连接有效,但会话过期:则重新创建 Session。
  • 如果连接有效,会话有效:则继续发送或接收。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【强化学习】强化学习数学基础:时序差分方法

时序差分方法Temporal Difference Learning举个例子TD learning of state values算法描述TD learning of action values: SarsaTD learning of action values: Expected SarsaTD learning of action values: n-step SarsaTD learning of optimal action values: Q-learningA un…

【Redis】主从集群 实现读写分离(二)

目录 2.Redis主从 2.1.搭建主从架构 2.2.主从数据同步原理 2.2.1.全量同步 2.2.2.增量同步 2.2.3.repl_backlog原理 2.3.主从同步优化 2.4.小结 2.Redis主从 2.1.搭建主从架构 单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;…

YOLOv7、YOLOv5改进之打印热力图可视化:适用于自定义模型,丰富实验数据

💡该教程为改进YOLO高阶指南,属于《芒果书》📚系列,包含大量的原创改进方式🚀 💡更多改进内容📚可以点击查看:YOLOv5改进、YOLOv7改进、YOLOv8改进、YOLOX改进原创目录 | 唐宇迪老师联袂推荐🏆 💡🚀🚀🚀内含改进源代码 按步骤操作运行改进后的代码即可�…

【毕业设计】Java局域网聊天室系统的设计与实现

点击免费下载源码 视频聊天系统作为一种新型的通信和交流工具&#xff0c;突破了地域的限制&#xff0c;可以提供更为便捷、灵活、全面的音、视频信息的传递和服务&#xff0c;具有极其广泛的发展前景。 介绍了采用JAVA编程开发视频聊天系统的一套比较常用的解决方案。文字聊…

Spring之实例化Bean _ @Resource和@Autowired实现原理(3)

目录 1. 搜集注解信息 applyMergedBeanDefinitionPostProcessor(*) 2. 将实例化的Bean放入3级缓存中 addSingletonFactory&#xff08;***&#xff09;为循环依赖做准备 3. 根…

RS232/RS485信号接口转12路模拟信号 隔离D/A转换器LED智能调光控制

特点&#xff1a;● RS-485/232接口&#xff0c;隔离转换成12路标准模拟信号输出● 可选型输出4-20mA或0-10V控制其他设备● 模拟信号输出精度优于 0.2%● 可以程控校准模块输出精度● 信号输出 / 通讯接口之间隔离耐压3000VDC ● 宽电源供电范围&#xff1a;10 ~ 30VDC● 可靠…

搭建SpringBoot多模块微服务项目脚手架(一)

搭建SpringBoot多模块微服务项目脚手架(一) 文章目录搭建SpringBoot多模块微服务项目脚手架(一)1.概述2.微服务环境搭建介绍1.微服务环境描述2.搭建环境组件和版本清单3.搭建父模块环境3.1.创建springboot父工程1.创建springboot2.配置maven和java3.精简父模块4.pom文件配置5.父…

记录--服务端推送到Web前端有哪几种方式?

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 这个问题&#xff1f; 这个问题一般会出现在面试题里面&#xff0c;然后回答一些诸如轮询、WebSocket之类的答案。当然&#xff0c;实际开发中&#xff0c;也会遇到类似别人给你赞了&#xff0c;要通知…

华为OD机试题,用 Java 解【一种字符串压缩表示的解压】问题

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不…

20230309英语学习

What Is Sleep Talking? We Look at the Science 为什么人睡觉会说梦话&#xff1f;来看看科学咋说 Nearly everyone has a story about people talking in their sleep.Though it tends to be more common in children, it can happen at any age:A 2010 study in the jour…

如何恢复已清空的 Windows 回收站?

Windows 95 中引入的回收站文件夹&#xff08;也称为垃圾桶&#xff09;是有史以来最有用的功能之一&#xff0c;可以保护您免受自己的错误和粗心大意的影响。 通常&#xff0c;从文件夹中恢复丢失的文件就像单击桌面上的回收站图标并执行简单的拖放操作一样简单。 但是&…

Java实例实验项目大全源码企业通讯打印系统计划酒店图书学生管理进销存商城门户网站五子棋

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;java实例 获取完整源码源文件视频讲解文档资料等 文章目录1、企业通讯2、快递打印系统3、开发计划管理系统4、酒店管理系统5、图书馆管理系统6、学生成绩管理系统7、进销存管理系统8、神奇Book——图书商城9、企业门户网站…

数据库管理-第六十期 监听(20230309)

数据库管理 2023-03-09第六十期期 监听1 无法访问2 监听配置3 问题复现与解决4 静态监听5 记不住配置咋整总结第六十期期 监听 不知不觉又来到了一个整10期数&#xff0c;我承认上一期有很大的划水的。。。嫌疑吧&#xff0c;本期内容是从帮群友解决ADG前置配置时候的一个问题…

C51---定时器中断相关寄存器

1.中断系统&#xff0c;是为使CPU具有对外界紧急事件的实时处理能力而设置的。 当中央处理器CPU正在处理某件事情的时候&#xff0c;要求CPU暂停当前任务或工作&#xff0c;转而去处理这这个紧急事件。处理完以后&#xff0c;再回到原来的被中断的地方&#xff0c;继续原来的工…

华为OD机试题,用 Java 解【寻找相同子串】问题

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不…

RoCEv2网络部署实践

延续上篇RoCE网络的介绍&#xff0c;我们知道承载ROCEv2流量必须有一张无损网络。 本章主要介绍在以太网环境部署无损网络的关键点。 首先是QoS&#xff0c;包含流分类和队列调度两部分。 流分类&#xff1a;在网络接入设备&#xff08;TOR&#xff09;配置if-match类的语句&am…

一本通 2.8.1 广度优先搜索算法

1329&#xff1a;【例8.2】细胞 【题目描述】 一矩形阵列由数字0到9组成,数字1到9代表细胞,细胞的定义为沿细胞数字上下左右还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数。如:阵列 有4个细胞。 【题目分析】 遍历所有节点&#xff0c;当无标识且不为零&#xff0c;…

「Vue面试题」动态给vue的data添加一个新的属性时会发生什么?怎样去解决的?

一、直接添加属性的问题 我们从一个例子开始 定义一个p标签&#xff0c;通过v-for指令进行遍历 然后给botton标签绑定点击事件&#xff0c;我们预期点击按钮时&#xff0c;数据新增一个属性&#xff0c;界面也 新增一行 <p v-for"(value,key) in item" :key&q…

Esp8266学习4. 基于Arduino的PWM与红外信号处理

Esp8266学习4. 基于Arduino的PWM与红外信号处理一、基本概念1. PWM2. ESP8266 的 PWM功能3. node-mcu 引脚图4. 模拟写入&#xff08;1&#xff09;analogWrite&#xff08;2&#xff09;修改频率 analogWriteFreq&#xff08;3&#xff09;调节分辨率二、使用 analogWrite实现…

思腾合力深思系列 | 四款高性能 AI 服务器

深思系列 AI 服务器涵盖多种 CPU 平台&#xff0c;支持按客户需求预装 OS、驱动、DL 框架、常用 DL 库&#xff0c;节省您大量的前期调试时间&#xff0c;开机即用。 一个简单的任务&#xff0c;若想要在 AI 的脑中形成清晰的思路&#xff0c;需要大量的实验和练习。从 AI 训练…