一、什么是多路复用
多路复用是指在单一的通信信道上,同时传输多个信号或数据流的技术。举个例子,我们可以想象一条高速公路,车辆在不同的车道上行驶,但它们共用同一条路。
二、Redis的多路复用
Redis是一个基于内存的key-value存储系统,常用于缓存,消息队列和会话管理等场景。Redis Server在启动时会监听一定端口,接收来自客户端的连接请求。在客户端与服务端之间的通信中,多路复用起到了至关重要的作用。
Redis服务端采用了单线程模型,避免了多线程并发带来的竞争、锁等问题。但在单线程模型下,如果客户端连接较多,也会导致服务器性能下降。多路复用技术可以同时处理多个连接请求,减少IO的消耗,提升Redis Server的性能。
三、Redis的多路复用实现原理
Redis服务端对于客户端的请求,采取了以下两种方式进行处理:
- 阻塞式
- 非阻塞式
1. 阻塞式
// 阻塞式版本的实现 while(1) { fd = accept(); read(fd); }
在阻塞式的方式中,Redis Server会为每个连接请求创建一个新的线程,然后使用阻塞方式处理IO。当连接请求较多时,会导致服务器创建的线程数太多,系统性能大幅下降。
2. 非阻塞式
// 非阻塞式版本的实现 while(1) { fd_set rfds; FD_ZERO(&rfds); FD_SET(serversock); for (i=0; i在非阻塞式中,Redis Server使用了IO多路复用的方式,将多个连接请求的处理过程合并为一个事件循环中。在每个循环周期中,Redis Server对所有的连接请求进行监听,如果有请求就进行接收处理,如果没有请求则继续等待。
在select、poll和epoll等多路复用IO模型中,Redis Server使用的就是epoll模型。epoll模型的特点是,将连接请求的状态保存在内核中,减少了每次轮询的开销,提高了服务器的效率。
四、Redis多路复用实现代码
1. 服务器端
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #define MAX_EVENTS 10 int main() { struct epoll_event event, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; struct sockaddr_in addr; char buf[256]; int i; listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock == -1) { perror("socket failed"); exit(EXIT_FAILURE); } memset(&addr, 0, sizeof(struct sockaddr_in)); addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = INADDR_ANY; if (bind(listen_sock, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) == -1) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(listen_sock, 5) == -1) { perror("listen failed"); exit(EXIT_FAILURE); } epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1 failed"); exit(EXIT_FAILURE); } event.data.fd = listen_sock; event.events = EPOLLIN | EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &event) == -1) { perror("epoll_ctl failed"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait failed"); exit(EXIT_FAILURE); } for (i = 0; i < nfds; ++i) { if (events[i].data.fd == listen_sock) { conn_sock = accept(listen_sock, NULL, NULL); if (conn_sock == -1) { perror("accept failed"); exit(EXIT_FAILURE); } sprintf(buf, "Hello, client %d", conn_sock); write(conn_sock, buf, strlen(buf)); event.data.fd = conn_sock; event.events = EPOLLIN | EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &event) == -1) { perror("epoll_ctl add conn_sock failed"); } } else { int n = read(events[i].data.fd, buf, sizeof(buf)); if (n <= 0) { if (epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL) == -1) { perror("epoll_ctl delete fd failed"); } close(events[i].data.fd); } else { write(events[i].data.fd, buf, n); } } } } close(listen_sock); exit(EXIT_SUCCESS); }2. 客户端
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { int client_sock; struct sockaddr_in server_addr; char buf[256]; client_sock = socket(AF_INET, SOCK_STREAM, 0); if (client_sock == -1) { perror("socket failed"); exit(EXIT_FAILURE); } memset(&server_addr, 0, sizeof(struct sockaddr_in)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(client_sock, (struct sockaddr *) &server_addr, sizeof(struct sockaddr_in)) == -1) { perror("connect failed"); exit(EXIT_FAILURE); } int n = read(client_sock, buf, sizeof(buf)); if (n > 0) { buf[n] = '\0'; printf("%s\n", buf); } close(client_sock); exit(EXIT_SUCCESS); }五、结论
Redis的多路复用技术可以在单线程模型下提升服务器的性能,避免了多线程并发带来的竞争、锁等问题。Redis服务端采用的是epoll模型,将连接请求的状态保存在内核中,减少了每次轮询的开销。在实际应用中,我们可以参考该示例代码,进行自己的开发和部署。