一、什么是多路复用
多路复用是指在单一的通信信道上,同时传输多个信号或数据流的技术。举个例子,我们可以想象一条高速公路,车辆在不同的车道上行驶,但它们共用同一条路。
二、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<numclients; i++)
FD_SET(clients[i]);
select(n, &rfds, NULL, NULL, NULL);
for (i=0; i<numclients; i++) {
if (FD_ISSET(clients[i])) {
read(clients[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模型,将连接请求的状态保存在内核中,减少了每次轮询的开销。在实际应用中,我们可以参考该示例代码,进行自己的开发和部署。
原创文章,作者:FOYAF,如若转载,请注明出处:https://www.506064.com/n/333490.html
微信扫一扫
支付宝扫一扫