欢迎光临
我们一直在努力

Linux C/C++ 学习日记(18):实现一个简易的百万并发服务器(一):Linux中万物皆文件

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

这篇博文难以理解的话可以去看一下:

Linux C/C++ 学习日记(11)select/poll/epoll 实现网络io的多路复用-CSDN博客

首先要想实现一个简易百万并发服务器,你必须要理解它受什么因素限制,这样遇到问题才会解决。代码事实上很简单,最重要的是理解如何修改系统参数。我会跟大家分几个博文逐层深入剖析的!

这篇博文首先先介绍“Linux中的文件”

一、Linux中万物皆文件。

1. 文件概念:

在 Linux 中,“文件” 的本质不是 “存储在磁盘上的数据流”,而是一个包含 “标识符(路径)” 和 “标准操作接口(open/read/write/close)” 的资源载体。

无论是硬盘里的文档、键盘鼠标、网络连接,还是正在运行的进程,Linux 都会给它们分配一个唯一的 “文件路径”(比如 /dev/mouse 对应鼠标,/proc/1 对应 PID 为 1 的进程),并允许用相同的系统调用(如 read() 读数据、write() 写数据)操作它们。

简单说:Linux 把所有资源都 “伪装” 成文件的样子,让操作逻辑统一。

2.常见的 “非普通文件” 类型

普通文件(如文本文件、二进制程序)只是 Linux 文件体系的一部分,更多 “文件” 其实是对硬件、进程、通信通道的抽象。通过 ls -l 命令查看文件权限时,最开头的字符就是 “文件类型标识”:

文件类型标识类型名称作用与示例(典型场景)操作逻辑类比
普通文件 存储数据流(文档、程序、图片等)。特例:/proc/cpuinfo(内核动态生成的 CPU 信息)、/etc/passwd(用户信息) 读:获取内容;写:修改内容(部分 proc 文件可写以调内核参数)
d 目录文件 存储文件名与索引的映射(类似文件夹),如 /home、/etc 读:ls 查看目录内容;写:mkdir 创建子目录、rmdir 删除空目录
c 字符设备文件

按字符流交互的硬件,如:

– 键盘 /dev/input/event0

– 串口 /dev/ttyS0

读:获取键盘输入 / 串口数据;写:向串口发送数据
b 块设备文件

按固定块(如 4KB)交互的硬件,如:

– 硬盘 /dev/sda

– U 盘分区 /dev/sdb1

读:读取硬盘某块数据;写:写入某块数据(需文件系统支持)
p 管道文件(命名管道 / FIFO)

文件系统中存在实体路径的进程间通信管道(与匿名管道不同),如:

– 用 mkfifo mypipe 创建的 mypipe

进程 1:echo "hi" > mypipe(写入);

进程 2:cat mypipe(读取)

读:接收另一进程写入的数据;写:向管道写入数据供其他进程读取(单向,先进先出,需双进程配合)
s 套接字文件

网络 / 本地进程双向通信通道,如:

– 网络 socket(TCP/UDP)

– 本地 (/var/run/docker.sock)

读:接收网络 / 进程数据;写:发送网络 / 进程数据(双向)
l 符号链接文件 指向其他文件的快捷方式,如 ln -s /etc/hosts hosts_link 生成的 hosts_link 读 / 写:等同于操作目标文件(目标不存在则失效)

3. 典型例子:操作 “设备文件” 就是操作硬件

以 “键盘” 为例:

  • Linux 会在 /dev/input/ 目录下创建一个对应键盘的设备文件(如 event0);
  • 当你在终端敲键盘时,内核会把键盘按键数据 “写入” /dev/input/event0;
  • 终端程序(如 bash)通过 “读取” /dev/input/event0 这个 “文件”,就能获取你的按键操作 ——它不需要专门写 “读取键盘硬件” 的逻辑,只需要用读普通文件的 read() 函数即可。

再比如 “硬盘”:

  • 硬盘对应的块设备文件是 /dev/sda(第一块硬盘),其中分区是 /dev/sda1;
  • 格式化分区、挂载分区的操作,本质是对 /dev/sda1 这个 “文件” 的配置;
  • 读写硬盘数据时,程序不需要直接和硬盘控制器交互,只需对 /dev/sda1 调用 read()/write()。

4. “万物皆文件” 的核心设计优势

优势类别核心逻辑(解决的问题)具体示例 / 说明
简化开发 统一操作接口,无需记忆不同资源的专属逻辑

– 无统一接口:读键盘需read_keyboard()、读文件需read_file()、读网络需read_network()

– 有统一接口:无论读什么,均调用read()函数,仅需传入不同文件路径(如读键盘用/dev/stdin,读普通文件用/tmp/test.txt)

简化系统管理 统一操作命令,无需区分 “文件 / 硬件 / 进程” 的操作差异

– 查 CPU 信息:cat /proc/cpuinfo(读 proc 文件,无需硬件工具)

– 查硬盘分区:ls /dev/sd*(列块设备文件,无需检测工具)

– 测串口通信:echo "hello" > /dev/ttyS0(写串口设备文件,无需串口工具)

灵活扩展 新资源仅需适配文件接口,无需设计全新操作逻辑

新增自定义传感器:

1. 编写驱动程序,在/dev目录创建对应设备文件(如/dev/sensor0)

2. 实现open()/read()/write()接口

3. 现有程序直接操作/dev/sensor0即可交互,无需修改代码

5. 常见误区澄清

误区表述正确解读
“所有文件都存在磁盘上” /dev(设备文件)、/proc(进程 / 系统信息文件)不占用磁盘空间(如du -sh /dev/mouse结果为 0),是内核在内存中临时生成的 “接口映射”,断电后消失
“所有文件都能修改”

– 只读文件:/proc/cpuinfo仅能读取 CPU 信息,无法修改

– 权限限制:/dev/sda(硬盘设备文件)修改需 root 权限,且错误操作会导致硬件损坏

“Windows 没有类似设计” Windows 有设备文件(如\\\\.\\PhysicalDrive0对应第一块硬盘),但未将 “进程、目录、网络” 完全抽象为文件,操作逻辑不统一(如读文件用ReadFile(),读网络用recv()),无 “万物皆文件” 特性

6. 核心总结

总结维度关键结论
核心本质 并非 “所有东西都是磁盘文件”,而是 “所有系统资源(硬件 / 进程 / 网络等)都能用文件的方式操作”,核心是接口统一
设计价值 通过统一接口,简化开发、简化系统管理、降低新资源扩展成本,是 Linux 稳定性和灵活性的重要基础
理解关键 需将 “文件” 看作 “资源的操作入口”,而非局限于 “磁盘上的数据流”

二、/dev/stdin 和 /dev/input/event0 的区别

1.二者对比

对比维度/dev/stdin/dev/input/event0
文件类型与本质 通常是符号链接(指向 /dev/fd/0 或 /dev/pts/[终端编号]),最终对应 “标准输入流” 的抽象,不属于独立硬件设备文件 典型的字符设备文件(类型标识 c),是内核为 “具体输入硬件”(如键盘、鼠标)创建的专属接口
功能定位 为程序提供 “统一的输入来源入口”,屏蔽输入来源的差异(不管输入是键盘、文件重定向还是管道) 为程序提供 “单个硬件设备的原始事件数据”,仅对应某一个具体硬件(如 /dev/input/event0 通常是第一个输入设备,多为键盘)
数据内容 经过处理的 “字符流”(或文本流),比如按键盘 a,读取到的是字符 'a';若输入来自文件重定向(如 cat < test.txt),读取到的是文件内容

未经处理的 “原始硬件事件数据”(二进制格式),包含:

– 事件类型(按键按下 / 松开、鼠标移动)

– 时间戳- 原始扫描码(而非最终字符,如键盘 a 的扫描码需映射才是 'a')

使用场景

普通程序获取输入(无需关心来源),比如:

– read 命令(shell 脚本)- scanf/read()(C 语言程序)

– cat(默认读 /dev/stdin)

需直接操作硬件的场景(需 root 权限),比如:- 自定义键盘快捷键工具(捕获原始按键)

– 输入设备测试(检测硬件是否正常发送事件)

– 开发输入设备驱动(调试原始数据)

2. 直观示例:用 cat 命令看差异

通过 cat 读取两者,能最直接感受到数据内容的不同:

2.1读取 /dev/stdin:

执行 cat /dev/stdin 后,按键盘 hello 再按回车,终端会输出 hello—— 因为 /dev/stdin 传递的是 “处理后的字符流”,和你直接在终端输入的内容一致。

敲入hello按回车,屏幕上显示hello

2.2 读取 /dev/input/event1:

执行 sudo cat /dev/input/event1(需 root 权限),再按键盘,终端会输出一堆乱码 —— 因为 /dev/input/event1 传递的是 “原始二进制事件数据”,cat 无法解析,只能按字节显示为乱码。

执行sudo evtest查看物理键盘对应的文件:发现为event1

输入sudo cat /dev/input/event1

回车(按回车就已经输出乱码了),输入“hello”(不按回车),发现屏幕乱码。

3.核心总结

  • 如果你是普通开发者 / 用户:写程序或用命令时,用 /dev/stdin(或直接用 “标准输入”)即可,它能帮你统一处理所有输入来源,不用关心硬件;
  • 如果你是硬件 / 驱动开发者:需要获取某一输入设备的原始数据(如调试键盘按键),才需要用 /dev/input/event0 这类设备文件,且需专门解析数据格式。

三、文件描述符的位图:判断哪些文件描述符是否可以被分配

1.内核层与应用层中的位图

层级名称本质核心作用典型操作 / 示例
内核层 open_fds files_struct 结构体中的位图(bitmask) 标记进程中文件描述符的 “已打开” 状态,供内核快速判断是否可用

进程调用 open() 时,内核将对应位标记为 “已占用”;

分配新 FD 时,内核查询位图找到最小未占用位

应用层 fd_set POSIX 标准定义的文件描述符集结构体(本质是位图) 跟踪文件描述符的 “可读 / 可写 / 异常” 状态,供 I/O 多路复用判断是否 “就绪”

– FD_SET(fd, &fdset):将 FD 加入集合

– FD_ISSET(fd, &fdset):检查 FD 是否在集合中(常用于 select()/pselect() 函数)

两者均基于 “位图” 设计,通过高效的位运算(如标记、查询)快速处理文件描述符状态,只是应用场景不同:open_fds 服务于内核的 FD 分配管理,fd_set 服务于用户态的 I/O 就绪判断。

2.系统默认分配的 3 个标准文件描述符

在 Linux 系统中,每个新进程启动时,内核会默认自动为其分配 3 个标准文件描述符(File Descriptor,FD),它们是进程与外界交互的 “默认输入输出通道”,编号和功能固定,具体如下:

文件描述符编号(FD)名称对应设备文件核心功能典型使用场景
0 STDIN_FILENO /dev/stdin 标准输入(进程从这里获取输入数据) 键盘输入、cat < 文件名 重定向输入
1 STDOUT_FILENO /dev/stdout 标准输出(进程向这里输出正常数据) 终端显示、echo "内容"、> 文件名 重定向输出
2 STDERR_FILENO /dev/stderr 标准错误输出(进程向这里输出错误信息) 程序报错(如 ls 不存在的文件 的错误提示)、2> 错误日志 重定向错误

补充说明:

  • “默认分配” 的本质:这些 FD 不是 “手动创建” 的 —— 当进程通过 fork() 或 exec() 启动时,内核会自动为其 “打开” 这 3 个 FD,并关联到当前终端(如 /dev/pts/0,即你正在操作的命令行窗口)。除非手动关闭(用 close(0)/close(1)/close(2)),否则进程运行期间始终存在。

  • 设备文件的关联性:/dev/stdin/stdout/stderr 本质是符号链接,默认指向 “当前终端设备”(如 ls -l /dev/stdin 会显示 lrwxrwxrwx 1 root root 15 … /dev/stdin -> /proc/self/fd/0)。当使用 “重定向”(如 ./app < input.txt 或 ./app 2> error.log)时,这些 FD 会被重新关联到文件 / 管道,而非终端。

  • 如何验证默认 FD 存在:可以通过查看当前进程的 FD 目录(/proc/[进程PID]/fd)确认,例如在终端中执行:

  • 这张图展示的是 Linux 系统中当前 Shell 进程的 “文件描述符(File Descriptor,FD)” 与设备的关联情况,可以通俗理解为 “进程和外界交互的通道”,具体解释如下:

    • 第一行 total 0:表示这些 “文件描述符的链接” 本身不占用磁盘空间(因为它们是内存中 “指向设备 / 文件的快捷方式”,不是真正的磁盘文件)。
    • 编号 0、1、2:是 Linux 给进程默认分配的 3 个标准文件描述符:
      • 0 -> /dev/pts/0:对应 “标准输入(STDIN)”,默认让进程从当前终端(你操作命令的窗口)接收输入(比如键盘敲命令);
      • 1 -> /dev/pts/0:对应 “标准输出(STDOUT)”,默认让进程把正常结果显示到当前终端(比如 ls 命令列出的文件);
      • 2 -> /dev/pts/0:对应 “标准错误输出(STDERR)”,默认让进程把错误信息显示到当前终端(比如 ls 不存在的文件 时的报错)。
    • 编号 255 -> /dev/pts/0:通常是当前 Shell 自身与 “控制终端(当前操作窗口)” 的关联,作用和 0/1/2 类似,也是让 Shell 能和终端交互。

    总结来说:当前 Shell 进程的 “输入、输出、错误提示”,都默认通过 /dev/pts/0 这个终端窗口来完成~

    3. 总结:fd的分配规则

    类别具体说明
    文件描述符表的基本信息

    – 定义:每个新进程创建时(如fork()、exec()、初始进程),内核会为其分配独立的 “文件描述符表”(属于进程的内核数据结构)。

    – 核心作用:

    1. 记录每个 fd(整数索引)的可用性(标记 “已占用” 或 “空闲”);

    2. 存储 fd 与内核资源的映射(已占用 fd 关联对应的struct file对象,如文件、套接字)。

    – 进程私有性:不同进程的 fd 表相互独立,即使 fd 号相同(如均为 3),也可能指向完全不同的内核资源。

    fd 的分配规则

    (1)初始状态:新进程默认打开 3 个标准 fd,初始为 “已占用”:

    – fd=0:标准输入(stdin,默认关联键盘);

    – fd=1:标准输出(stdout,默认关联终端);

    – fd=2:标准错误(stderr,默认关联终端)。

    因此,首次调用open()、socket()等创建资源时,优先分配最小空闲 fd(从fd=3开始)。

    (2)后续分配:遵循 “最小未使用” 原则:若较小 fd 被释放(如close(0)),后续新创建资源会优先复用该空闲 fd(而非使用更大的 fd)。

    示例说明

    1. 进程初始:fd=0(占用)、1(占用)、2(占用)、3(空闲)、4(空闲)…

    2. 调用open("a.txt"):分配最小空闲fd=3(此时fd=3变为占用)。

    3. 调用close(3):fd=3变为空闲。4. 再次调用open("b.txt"):优先分配fd=3(而非4)。

    总结 新进程均有独立的文件描述符表记录 fd 可用性,fd 分配遵循 “优先最小空闲值” 规则:初始 0-2 被标准 fd 占用,首次分配从 3 开始;若较小 fd 被释放,后续会复用该 fd,而非一味递增。

    四、fd “共用”文件资源的情景

    类别具体说明
    fd “共用” 的核心定义 指 “不同的 fd 指向同一个内核对象(如文件、套接字、管道等)”,而非 fd 号本身的共享;fd 功能是否相同,取决于其指向的内核对象是否一致。
    fd 为什么会 “共用”

    原理:fd 是进程内核空间 “文件描述符表” 的整数索引,每个索引条目指向内核的 “文件对象”(struct file),该对象记录文件状态、偏移量、引用计数等;当多个 fd 指向同一个 “文件对象” 时,即形成 “共用”。常见场景:

    1. dup()/dup2()复制 fd:如int new_fd = dup(old_fd),新 fd 与原 fd 指向同一文件对象(引用计数 + 1)。

    2. 进程fork()继承 fd:子进程复制父进程文件描述符表,子进程与父进程同名 fd 指向同一文件对象(引用计数 + 1)。

    3. 套接字对 / 管道创建:如管道读端 fd 经dup复制后,两个读端 fd 指向同一文件对象(写端 fd 单独指向另一对象)。

    共用的 fd 功能是否相同

    1. 指向同一文件对象:功能等价,操作作用于同一资源。- 例 1:两 fd 指向同一文件,read(new_fd)与read(old_fd)共享文件偏移量(一个读后,另一个偏移量同步变化)。- 例 2:两 fd 指向同一套接字,send(new_fd, …)与send(old_fd, …)均通过该套接字发数据,效果一致。

    2. 指向不同文件对象:功能完全独立,操作不同资源(即使 fd 号相同)。- 例:进程 A 的 fd=3 指向a.txt,进程 B 的 fd=3 指向b.txt,两者读写互不影响。

    注意点

    1. fd 的进程私有性:fd 仅在本进程内有效,不同进程的 fd 号无关联(同一 fd 号可能指向不同内核对象)。

    2. 关闭不立即释放资源:关闭共用 fd 仅减少文件对象引用计数,需所有共用 fd 均关闭(引用计数 = 0),底层资源才被内核释放。

    3. 操作相互影响:如对共用文件 fd 执行lseek会改变共享偏移量;对共用套接字 fd 执行close仅减引用计数(未到 0 时不关闭连接)。

    总结

    fd 可通过复制、继承等方式 “共用” 底层内核资源(指向同一对象),此时操作功能等价;

    若指向不同对象,即使 fd 号相同,功能也独立。“共用” 本质是内核资源的共享,而非 fd 号的共享。

    注意:

    struct file是跨进程的内核级对象,其引用计数(f_count)由内核统一管理,与 “哪个进程的 fd 引用它” 无关。只要有新的 fd(无论属于哪个进程)指向该资源,引用计数就会 +1。

    同样的,无论哪个进程关闭指向该资源的 fd,都会导致引用计数 -1;只有当所有进程的相关 fd 都被关闭(引用计数减至 0),内核才会释放该struct file资源。

    五、编写一个TCP服务器,感受一下fd的分配

    netstat -anop | grep port 可以查看端口port的使用情况

    // 标准输入输出库(printf、perror等函数)
    #include <stdio.h>
    // socket相关系统调用(socket、bind、listen、accept等)
    #include <sys/socket.h>
    // epoll相关系统调用(epoll_create、epoll_ctl、epoll_wait等)
    #include <sys/epoll.h>
    // 网络地址结构(sockaddr_in、AF_INET等)
    #include <netinet/in.h>
    // 字符串操作(strlen等,此处未直接使用但包含常规网络编程依赖)
    #include <string.h>
    // 系统调用(close等)
    #include <unistd.h>
    // 错误码定义(errno变量,用于判断具体错误类型)
    #include <errno.h>

    int main()
    {
    // 定义监听socket的文件描述符(用于接收客户端连接请求)
    int socketfd;
    // 定义IPv4地址结构(存储服务器监听的IP和端口)
    struct sockaddr_in addr;
    addr.sin_family = AF_INET; // 地址族:IPv4
    addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地网卡IP(0.0.0.0)
    addr.sin_port = htons(8080); // 端口:8080(htons将主机字节序转为网络字节序)

    // 创建TCP socket(流式套接字)
    // 参数1:AF_INET(IPv4);参数2:SOCK_STREAM(TCP协议);参数3:0(默认协议,此处为IPPROTO_TCP)
    socketfd = socket(AF_INET, SOCK_STREAM, 0);
    if (socketfd == -1) // 创建失败(返回-1)
    {
    perror("socket creation failed"); // 打印错误原因(依赖errno)
    return -1;
    }
    printf("socketfd: %d\\n", socketfd);

    // 启用端口复用(解决服务器重启时"Address already in use"错误)
    int reuse = 1; // 1表示启用复用
    // setsockopt:设置socket选项
    // 参数1:目标socket(监听socket);参数2:选项层级(SOL_SOCKET表示socket层)
    // 参数3:选项名(SO_REUSEADDR表示允许端口复用);参数4:选项值地址;参数5:选项值长度
    if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
    {
    perror("setsockopt SO_REUSEADDR failed");
    close(socketfd); // 失败时关闭已创建的socket
    return -1;
    }

    // 绑定socket到指定IP和端口(将socket与addr结构关联)
    // 参数1:监听socket;参数2:通用地址结构指针(需强转);参数3:地址结构长度
    if (bind(socketfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
    perror("bind failed");
    close(socketfd); // 绑定失败关闭socket
    return -1;
    }

    // 将socket设置为监听状态(开始接收客户端连接请求)
    // 参数2:监听队列最大长度(最多同时等待处理5个连接请求)
    if (listen(socketfd, 5) == -1)
    {
    perror("listen failed");
    close(socketfd); // 监听失败关闭socket
    return -1;
    }

    // 创建epoll实例(用于I/O多路复用,管理多个socket的事件)
    // 参数:已忽略(只需传入一个大于0的数即可),返回epoll实例的文件描述符
    int epollfd = epoll_create(1);
    printf("epollfd: %d\\n", epollfd);
    if (epollfd == -1)
    {
    perror("epoll_create failed");
    close(socketfd); // 创建失败关闭监听socket
    return -1;
    }

    // 定义epoll事件结构(用于描述监控的事件和关联的数据)
    struct epoll_event ev, events[10]; // ev:待添加的事件;events:存放触发的事件
    ev.events = EPOLLIN; // 监控"可读事件"(客户端连接请求或数据到来)
    ev.data.fd = socketfd; // 事件关联的文件描述符(监听socket)

    // 将监听socket添加到epoll实例中(开始监控其事件)
    // 参数2:操作类型(EPOLL_CTL_ADD表示添加事件);参数3:目标文件描述符(监听socket)
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, socketfd, &ev) == -1)
    {
    perror("epoll_ctl failed");
    close(socketfd); // 失败关闭监听socket
    close(epollfd); // 关闭epoll实例
    return -1;
    }

    // 服务器主循环(持续处理事件)
    while (1)
    {
    // 等待epoll实例中的事件触发(阻塞等待,直到有事件发生)
    // 参数2:输出参数,存放触发的事件;参数3:最多处理10个事件;参数4:超时时间(-1表示永不超时)
    int nfds = epoll_wait(epollfd, events, 10, -1);
    if (nfds == -1) // 等待失败(如被信号中断)
    {
    perror("epoll_wait failed");
    break; // 退出循环,结束服务器
    }

    // 遍历所有触发的事件(nfds为触发的事件数量)
    for (int n = 0; n < nfds; ++n)
    {
    // 判断事件是否来自监听socket(新客户端连接请求)
    if (events[n].data.fd == socketfd)
    {
    // 接受客户端连接(获取与客户端通信的socket描述符connfd)
    // 参数2、3:客户端地址信息(此处为NULL,表示不关心客户端地址)
    int connfd = accept(socketfd, NULL, NULL);
    if (connfd == -1) // 接受连接失败
    {
    perror("accept failed");
    continue; // 继续处理其他事件
    }
    printf("clientfd: %d\\n", connfd); // 打印新连接的fd

    // 定义新连接的epoll事件(监控其可读事件)
    struct epoll_event ev;
    ev.events = EPOLLIN; // 监控"可读事件"(客户端发送数据)
    ev.data.fd = connfd; // 关联新连接的fd

    // 将新连接的socket添加到epoll实例(开始监控其数据到来)
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1)
    {
    perror("epoll_ctl add connfd failed");
    close(connfd); // 添加失败关闭该连接
    continue;
    }
    }
    // 事件来自已连接的socket(客户端发送数据或断开连接)
    else
    {
    char buffer[1024]; // 用于存储接收的数据
    int connfd = events[n].data.fd; // 获取当前处理的连接fd

    // 接收客户端数据(从connfd读取数据到buffer)
    // 参数3:最大读取长度(1024字节);参数4:0(默认标志)
    int bytes_read = recv(connfd, buffer, 1024, 0);

    // 处理接收结果:读取失败或客户端关闭连接
    if (bytes_read <= 0)
    {
    perror("recv failed"); // 打印错误(可能是客户端主动关闭,此时bytes_read=0)
    close(connfd); // 关闭连接
    // 从epoll实例中移除该连接(停止监控)
    epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
    }
    // 客户端正常发送数据
    else
    {
    printf("Received: %s\\n", buffer); // 打印接收的内容
    // 将接收到的数据原样发送回客户端(回显)
    // 参数3:发送长度(实际接收的字节数)
    send(connfd, buffer, bytes_read, 0);
    }
    }
    }
    }

    // 程序退出时关闭文件描述符(实际循环break后会执行)
    close(socketfd);
    close(epollfd);
    return 0;
    }

    运行代码

    然后用网络助手工具发起一个连接(A)

    再发起一个连接(B)

    断掉A连接后立马发起一个连接(C):

    发现clientfd为7

    断掉连接C后过一段时间再发起连接

    发现clientfd为5。

    总结:

    1. sockfd是一个文件描述符,是整数 (数量有限制)。系统默认占(0,1,2),所以网络连接时 生成的fd是从3开始的。

    2.情况:现有clientfd:5、6,现在断开5,系统会对该文件描述符进行回收(需要时间timewait)。再次连接:

    情况1:新的fd:7(在timewait内)

    情况2:新的fd:5(timewait之后再连接)

    赞(0)
    未经允许不得转载:171主机测评 » Linux C/C++ 学习日记(18):实现一个简易的百万并发服务器(一):Linux中万物皆文件
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址