Redis学习总结(四)

套接字(Socket)

套接字(Socket)是计算机网络中应用层和传输层之间的接口,它是一种通信机制,用于实现不同计算机之间的进程之间的通信。通过套接字,进程可以向另一个进程发送数据,也可以接收来自另一个进程的数据。

在Java中,通过java.net包中的Socket类和ServerSocket类实现套接字的通信。以下是一个简单的示例代码,实现了客户端向服务器发送数据的功能:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
    
    
    private static final int PORT = 8080;
    private static final String HOST = "localhost";
    public static void main(String[] args) throws IOException {
    
    
        Socket socket = new Socket(HOST, PORT);
        OutputStream outputStream = socket.getOutputStream();
        String message = "Hello, server!";
        outputStream.write(message.getBytes());
        outputStream.flush();
        InputStream inputStream = socket.getInputStream();
        byte[] buffer = new byte[1024];
        int length = inputStream.read(buffer);
        String response = new String(buffer, 0, length);
        System.out.println("Server response: " + response);
        socket.close();
    }
}

在这个示例代码中,客户端通过创建一个Socket对象来连接服务器。通过Socket对象获取到输出流,将要发送的数据写入输出流中,然后调用flush方法将数据发送出去。接着,客户端通过Socket对象获取到输入流,读取来自服务器的响应,并将响应打印出来。最后,客户端关闭Socket对象。

以下是服务器端的示例代码,它实现了接收客户端发送的数据并将数据返回给客户端的功能:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
    
    
    private static final int PORT = 8080;
    public static void main(String[] args) throws IOException {
    
    
        ServerSocket serverSocket = new ServerSocket(PORT);
        while (true) {
    
    
            Socket socket = serverSocket.accept();
            new Thread(new Handler(socket)).start();
        }
    }
    static class Handler implements Runnable {
    
    
        private final Socket socket;
        Handler(Socket socket) {
    
    
            this.socket = socket;
        }
        @Override
        public void run() {
    
    
            try {
    
    
                InputStream inputStream = socket.getInputStream();
                byte[] buffer = new byte[1024];
                int length = inputStream.read(buffer);
                String message = new String(buffer, 0, length);
                System.out.println("Client message: " + message);
                OutputStream outputStream = socket.getOutputStream();
                String response = "Hello, client!";
                outputStream.write(response.getBytes());
                outputStream.flush();
                socket.close();
            } catch (IOException ex) {
    
    
                ex.printStackTrace();
            }
        }
    }
}

在这个示例代码中,服务器通过创建一个ServerSocket对象来监听客户端的连接请求。当有客户端连接时,服务器将会创建一个Handler对象来处理客户端请求。Handler对象实现了Runnable接口,用于在新的线程中处理客户端请求。在Handler对象的run方法中,它通过Socket对象获取到输入流,读取客户端发送的数据,并将数据打印出来。接着,它又通过Socket对象获取到输出流,将数据发送给客户端。最后,Handler对象关闭Socket对象。

以上是套接字的一个简单示例,实际上套接字还有很多细节需要考虑,例如如何处理并发请求、如何保证数据的可靠性等等。但是通过这个示例代码,你可以了解到套接字的基本思想和实现方式。

Redis 线程模型

Redis 采用单线程模型,即所有 Redis 操作都是在一个线程中完成的,这个线程被称为主线程(Main Thread)或者IO 线程(IO Thread)。

Redis 的线程模型可以分为两部分: IO 多路复用和事件处理。

IO 多路复用

Redis 使用 IO 多路复用技术来实现高效的网络通信,IO 多路复用是指在同一个线程中同时监听多个 I/O 事件,当有事件发生时,系统会通知线程进行处理。

在 Redis 中,使用 select、epoll、kqueue 等系统调用来实现 IO 多路复用。Redis 在启动时会创建一个或多个 TCP 监听器(TCP Listener),每个监听器会监听一个端口,等待客户端连接。当有客户端连接时,监听器会将连接套接字(Socket)注册到事件循环(Event Loop)中,等待 IO 事件发生。

事件处理

当 IO 事件发生时,事件循环会将事件分发给事件处理器(Event Handler)进行处理。Redis 事件处理器主要包括以下三个部分:

  1. 文件事件(File Event)处理器:用于处理套接字的读写事件。当有数据可读或者可写时,文件事件处理器会调用相应的读写函数进行处理。

  2. 时间事件(Time Event)处理器:用于处理定时事件。时间事件处理器会根据时间事件表中的事件到达时间,定时执行相应的事件处理函数。

  3. 异步任务(Async Task)处理器:用于处理异步任务。异步任务处理器会将异步任务加入到任务队列中,并在主线程空闲时调用相应的处理函数进行处理。

这三个事件处理器在事件循环中以先进先出的方式依次执行,保证了 Redis 的单线程模型。

同时,Redis 还提供了一些命令可以在主线程中执行,例如字符串操作、哈希操作、列表操作等等。这些操作都是非阻塞的,可以在短时间内完成,不会影响到主线程的执行效率。

总之,Redis 的线程模型非常精简,但同时也非常高效。通过 IO 多路复用和事件处理的结合,Redis 在单线程的情况下可以处理海量的并发连接,保证了 Redis 的高吞吐量和低延迟。

这个文件事件处理器是干什么的?为什么叫这个名字?这里的文件是什么意思

文件事件处理器是 Redis 与外部世界交互的接口,主要负责监听文件描述符的可读、可写等事件,响应这些事件并调用相应的事件处理器进行处理。在 Redis 中,文件描述符可以是 socket、文件或者管道等,所以这里的“文件”实际上涵盖了多种不同类型的 IO 资源,而不仅仅是传统意义下的“文件”。

文件事件处理器的设计和实现主要是基于 Reactor 模式来完成的,同样和网络 IO 中的 Reactor 模式类似。在 Redis 中,每个客户端的 socket 都有一个对应的文件事件处理器,它利用操作系统提供的 IO 多路复用技术(如 epoll 或 kqueue)来实现高效处理多个客户端请求的目的。

Redis 中的文件事件处理器主要包括以下两个部分:

  1. 文件事件驱动器(EventLoop):它是基于 Redis 内部实现的一个事件驱动库,用于监听文件事件、生成事件状态等。事件驱动器通过不断地检测注册的文件事件,当事件发生时立即通知相应的事件处理器进行处理。

  2. 文件事件处理器(FileEventHandler):负责具体的事件处理,如接收和发送数据等。当文件事件驱动器检测到一个事件时,它会将事件交给对应的文件事件处理器进行处理。文件事件处理器通过 Redis 事件处理器的接口操作 Redis 的数据库,实现了 Redis 的主要功能,如命令执行、数据读写等。

文件事件处理器被称为“文件”事件处理器是因为所有的 IO 资源在 Linux 内核中都以文件的形式进行管理,因此 Redis 将网络连接也视为一种“文件”,并将其作为文件事件处理器的处理对象。

IO多路复用

IO多路复用(I/O multiplexing)是一种 Linux 系统级别的 IO 模型,可以同时监控多个文件描述符,等待事件发生,从而实现高效的事件驱动 IO 操作。

在大规模的服务中,客户端连接数量庞大,每个客户端都需要进行 IO 操作,如果采用传统的多线程/多进程模型,需要创建大量的线程或进程来处理这些 IO 请求,这样会占用大量的系统资源,导致服务器性能下降。而采用 IO 多路复用模型,只需要使用一个进程或线程就可以监听多个文件描述符,而不需要为每个连接新建一个线程或进程,大大减轻了系统资源的压力。

在 Linux 中,常用的 IO 多路复用技术有 select,poll 和 epoll。它们的本质都是一样的,都利用了内核提供的 select、poll 和 epoll 等系统调用来实现 IO 多路复用。这些系统调用会在程序创建了多个文件描述符后,将这些文件描述符集中起来,由内核统一进行事件监控和事件通知。当某个文件描述符发生事件时,内核会将其对应的事件加入到事件队列中,并通过 select、poll 或 epoll 等系统调用通知应用程序进行处理。

select 和 poll 是早期的 IO 多路复用技术,它们主要用于监控所感兴趣的文件描述符集合是否就绪。由于 select 和 poll 在处理大量文件描述符时,需要遍历所有文件描述符,因此效率并不高。而 epoll 则是更加高效,可扩展的 IO 多路复用技术。它只对所关心的文件描述符进行监控,并利用 epoll_ctl 系统调用来实现动态管理和快速添加、删除文件描述符。因此,epoll 比 select 和 poll 在并发性能上有更大的优势,是当前 Linux 系统下的主流 IO 多路复用技术。

select、poll 或 epoll

在 Linux 中,select、poll、epoll 都是实现 IO 多路复用的系统调用,主要用于高效地处理多个文件描述符的 IO 事件。它们虽然是不同的系统调用,但本质上都是实现一个事件循环,不断地轮询一组文件描述符上的 IO 事件状态,并响应这些事件。以下是对每个系统调用的详细介绍:

  1. select

select 是最早的 IO 多路复用系统调用之一,它在一个 fd_set 数组集合中管理多个文件描述符,该数组的最大大小受限于操作系统内核中 fd_set 宏的大小,一般默认为 1024。select 在每次轮询时,会遍历整个 fd_set 数组,通过 FD_ISSET 宏来检查哪些文件描述符准备好了 IO 操作。select 有以下几个缺点:

  • fd_set 大小的限制会影响到可同时管理的文件描述符数量,在高并发的环境下效率较低。
  • 在实际使用中需要手动维护 fd_set 数组,不够灵活。
  • 轮询过程中,即使只有一个文件描述符准备好了 IO 操作,也会将整个 fd_set 数组遍历一遍,效率较低。
  1. poll

poll 与 select 类似,也是在一个 pollfd 数组集合中管理多个文件描述符,但 pollfd 可以管理的文件描述符数量比 fd_set 多得多,并且没有大小限制,一般默认为65536。poll 在每次轮询时,会遍历整个 pollfd 数组,并通过 revents 字段来检查哪些文件描述符准备好了 IO 操作。相对于 select,poll 的优点是管理更多的文件描述符,API 更加健壮,但轮询过程的效率问题和手动维护 pollfd 数组的问题仍然存在。

  1. epoll

epoll 是最新、最优秀、最常用的 Linux IO 多路复用机制,它的API比 select 和 poll 更加简单,同时能够支持更多的文件描述符,并且在每次 IO 事件的响应上效率更高。epoll 使用的是基于事件驱动的方式,其主要优点在于:

  • 管理的文件描述符数量几乎没有限制。
  • 没有轮询问题,只会处理已就绪的文件描述符。
  • 应用程序监测 fd 的状态时,只需要调用一次 epoll_create 创建一个 epoll 句柄,然后再调用 epoll_ctl 向 epoll 句柄中添加或者删除文件描述符即可。而在每次 epoll_wait 的时候,只需要传入 epoll 句柄,并设置一个超时时间,内核会检查哪些文件描述符已经准备好进行 IO 操作,并将准备好的文件描述符加入到一个可读事件列表中,便于应用程序进行相应的读写操作。

综上可知,虽然 select 和 poll 仍然具有一定的使用价值,但是在高并发场景下,epoll 是IO多路复用机制的最佳选择。

epoll

epoll 对所关心的文件描述符进行监控,而不是对所有的文件描述符进行监控。

在 epoll 的使用过程中,首先需要通过 epoll_create 创建一个 epoll 句柄,然后通过 epoll_ctl 来向该句柄注册需要关心的文件描述符。在注册文件描述符时,可以通过 EPOLL_CTL_ADD 把文件描述符加入监听队列中,也可以通过 EPOLL_CTL_MOD 修改关注的事件,通过 EPOLL_CTL_DEL 从监听队列中删除文件描述符。这样,在应用程序进行 epoll_wait() 操作时,内核只会通知那些已经准备好 IO 事件的文件描述符,并将这些文件描述符加入到一个可读/可写事件列表中,等待应用程序进行相应的读写操作。

因此,相比于 poll 和 select,epoll 具有更高效的事件过滤能力,可以只监视应用程序需要关注的文件描述符,不会浪费资源在不必要的文件描述符上,从而实现更高效的 IO 多路复用。同时,由于只关注所添加的文件描述符,epoll 可以支持非常大的文件描述符集,也不会受到连接数的限制,非常适用于高并发应用程序的开发。

相对于其他 IO 多路复用机制(如 select 和 poll),epoll 有以下几个优点:

  1. 没有连接数限制

select 和 poll 在处理大量文件描述符时,需要遍历整个描述符集合,导致性能低下,而 epoll 则完全没有这个问题。epoll 可以处理超过 10 万个连接,不会受到连接数的限制。

  1. 更快的速度和更少的系统调用

select 和 poll 在遍历文件描述符集合的时候,需要将集合中所有的文件描述符都交给内核,导致内核一遍一遍地扫描描述符,造成很多内存和 CPU 的浪费。而 epoll 可以通过 epoll_ctl 系统调用向内核注册需要监听的文件描述符,只有被激活的文件描述符才会被加入到事件队列,从而在循环过程中只处理真正发生了事件的描述符,并且由于减少了不必要的系统调用而更快。

  1. 内核空间和用户空间的数据拷贝

在 select 和 poll 中,每次事件的导致都需要将文件描述符集合从用户空间移到内核空间,而在 epoll 中,也需要首先将所有感兴趣的文件描述符通过 epoll_ctl 从用户空间移到内核空间中,但发生的事件可以直接从内核空间返回,避免了内核空间和用户空间数据的拷贝。

  1. 更丰富的事件类型

select 和 poll 只能检查文件描述符的 IO 可读或 IO 可写状态,而 epoll 不仅支持文件描述符的读写事件检测,而且还能够检测 EPOLLRDHUP(TCP 连接关闭)和 EPOLLERR(错误)等更多的事件类型,从而更加精细地处理事件。

总的来说,epoll 的优点是:不受连接数限制、更高效的事件通知机制、系统调用次数少,从而减少了内存和 CPU 的浪费,最终提高了服务器性能。

猜你喜欢

转载自blog.csdn.net/m0_51431003/article/details/131021718