TCP并发服务器多线程和多进程方式以及几种IO模型

avatar
作者
筋斗云
阅读量:0

1. 阻塞 I/O(Blocking I/O)

在阻塞 I/O 模型中,当应用程序发起 I/O 操作时,整个进程会被阻塞,直到操作完成。在这个过程中,应用程序无法执行其他任务,必须等待 I/O 操作的完成。

特点

  • 简单性:编程简单,逻辑清晰,容易理解和实现。
  • 低效性:在高并发场景下,由于每个 I/O 操作都会阻塞整个进程,资源利用率较低。

2. 非阻塞 I/O(Non-blocking I/O)

非阻塞 I/O 模型允许应用程序在发起 I/O 操作时立即返回,即使数据尚未准备好。应用程序可以在等待 I/O 完成的同时执行其他任务,需通过轮询(多次尝试)来检查 I/O 是否完成。

特点

  • 并发性:在等待 I/O 完成时,应用程序可以继续处理其他任务。
  • 轮询开销:需要频繁检查 I/O 状态,增加了 CPU 的负担。

3. I/O 多路复用(I/O Multiplexing)

I/O 多路复用(如 selectpollepoll)允许应用程序同时监听多个 I/O 事件,并在任何一个 I/O 操作准备好时被通知。应用程序可以集中处理多个 I/O 操作,从而避免轮询带来的开销。

特点

  • 高效性:适用于需要同时处理多个 I/O 连接的场景,尤其是高并发服务器。
  • 复杂性:编程复杂度高,需要仔细管理多个 I/O 描述符和事件。

4. 信号驱动 I/O(Signal-driven I/O)

信号驱动 I/O 模型中,应用程序发起 I/O 操作并继续执行其他任务,当数据准备好时,内核会通过信号通知应用程序。这种方式允许应用程序避免轮询和阻塞,且能够异步处理 I/O 事件。

特点

  • 异步性:内核通过信号通知应用程序,无需轮询。
  • 复杂性:信号处理逻辑复杂,容易出错。

5. 异步 I/O(Asynchronous I/O)

在异步 I/O 模型中,应用程序发起 I/O 操作并立即返回,I/O 操作由内核完成,操作完成后内核通过回调机制通知应用程序。应用程序无需等待 I/O 完成,也无需轮询或处理信号。

特点

  • 最高效:真正的异步模型,应用程序可以充分利用 CPU 时间。
  • 复杂性:编程难度较大,需要处理异步回调和并发问题。

1. 多线程并发服务器

在多线程模型中,服务器为每个客户端连接创建一个独立的线程。每个线程处理客户端的请求,并将处理结果返回给客户端。由于线程是在同一进程内执行的,因此它们共享内存空间和其他资源。

工作流程:
  1. 主线程监听:服务器在指定端口上监听客户端连接请求。
  2. 接受连接:当有新的客户端连接时,服务器接受该连接,并为其创建一个新的线程。
  3. 线程处理:新线程负责处理该客户端的所有请求,直到客户端断开连接。线程可以读取客户端发送的数据、进行处理,并将结果发送回客户端。
  4. 线程终止:在处理完毕后,线程可以选择继续等待新的请求(长连接)或终止(短连接)。
优点:
  • 资源共享:线程间共享同一进程的资源(如内存、文件描述符),使得在不同线程之间共享数据变得容易。
  • 响应速度快:创建线程的开销相对较低,线程切换也比进程切换快,适合需要快速响应的场景。
缺点:
  • 同步问题:由于线程共享同一地址空间,因此在访问共享资源时,容易出现数据竞争问题,需要使用同步机制(如互斥锁)来避免竞争条件,这会增加代码复杂度。
  • 稳定性:一个线程崩溃可能会影响整个进程,因为所有线程共享同一进程空间。
使用场景:
  • 高并发应用:适合需要处理大量并发连接的场景,如聊天室、实时通信系统。
  • 轻量级任务:当每个请求的处理时间较短时,多线程模型能够有效提高处理效率。

2. 多进程并发服务器

在多进程模型中,服务器为每个客户端连接创建一个独立的进程。每个进程在自己的内存空间中运行,处理来自客户端的请求并返回结果。由于进程是独立的,数据不会在进程之间共享。

工作流程:
  1. 主进程监听:服务器在指定端口上监听客户端连接请求。
  2. 接受连接:当有新的客户端连接时,服务器接受该连接,并为其派生一个新的子进程。
  3. 进程处理:子进程独立运行,处理客户端的请求,直到客户端断开连接。子进程在处理过程中可以读取数据、进行处理,并返回结果。
  4. 进程终止:子进程处理完毕后终止,释放相关资源。
优点:
  • 独立性强:每个进程都有独立的内存空间和资源,因此一个进程崩溃不会影响其他进程的运行,服务器整体的稳定性较高。
  • 安全性高:由于进程间的数据不共享,因此天然避免了线程间的竞争条件和同步问题。
缺点:
  • 资源开销大:创建和销毁进程的开销比线程大得多,尤其是在高并发场景下,进程的频繁创建和销毁可能会耗尽系统资源。
  • 进程通信复杂:如果进程之间需要通信,必须使用 IPC 机制(如管道、消息队列、共享内存等),这增加了开发的复杂度。
使用场景:
  • 高安全性应用:适合对安全性要求较高的场景,如需要严格隔离不同客户端的应用。
  • 长时间任务:适合处理时间较长、复杂度较高的任务,因为进程之间相互独立,不会因为某个进程的长时间运行影响到其他任务的处理。

  • 多线程并发服务器:适合轻量级、高并发的应用场景,能够快速响应请求,但需要注意线程同步问题和稳定性。
  • 多进程并发服务器:适合高安全性、高稳定性的场景,尤其是需要隔离不同任务的应用,但进程开销较大,进程间通信较为复杂。

 fcntl()

  • 原型int fcntl(int fd, int cmd, ... /* arg */ );

  • 用法

    1. 首先,使用 fcntl() 函数获取文件描述符的当前标志。
      • 可以通过传递 F_GETFL 作为 cmd 参数来实现。
    2. 然后,将非阻塞标志 O_NONBLOCK 添加到当前标志中。
    3. 最后,再次使用 fcntl() 函数将新的标志设置回文件描述符。
      • 可以通过传递 F_SETFL 作为 cmd 参数来实现

实现信号驱动 I/O 主要依赖以下函数:

1. 设置文件描述符为信号驱动模式

要将文件描述符设置为信号驱动模式,可以使用 fcntl() 函数。

  • fcntl()
    • 原型int fcntl(int fd, int cmd, ... /* arg */ );

    • 用法

      1. 首先,使用 fcntl() 函数获取文件描述符的当前标志。
        • 通过传递 F_GETFL 作为 cmd 参数来获取当前标志。
      2. 然后,将 O_ASYNC 标志添加到文件描述符的当前标志中。
        • 通过传递 F_SETFL 作为 cmd 参数,并将 O_ASYNC 与现有标志结合来实现。
      3. 最后,使用 fcntl() 设置信号接收进程或进程组:
        • 通过传递 F_SETOWN 作为 cmd 参数,并传递要接收信号的进程 ID 或进程组 ID。

      这样,当文件描述符有 I/O 事件发生时,系统将向指定进程发送 SIGIO 信号。

2. 处理信号

在信号驱动 I/O 中,当文件描述符准备好时,会触发 SIGIO 信号。应用程序需要设置信号处理程序来处理该信号。

  • sigaction()
    • 原型int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

    • 用法

      • 使用 sigaction() 函数为 SIGIO 信号设置一个处理函数。
      • 在信号处理函数中,应用程序可以执行相应的 I/O 操作(如读取或写入数据)。

      通过 sigaction() 配置 SIGIO 信号的处理程序后,应用程序在文件描述符准备好进行 I/O 操作时会自动收到通知并调用处理程序。

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!