Redis连接
-
客户端和服务器端正常连接后才能实现彼此的交互、通信。
-
redis通过RESP实现C/S之间的连接通信,该协议包含两个部分:
网络模型:负责数据交互的组织方式;
序列化协议:实现了数据的序列化; -
C以序列号后的协议数据向S发出请求,S也以RESP后的数据返给C
-
命令:redis-cli//开启c-s连接
Redis安全策略
- C要连接S,为保证数据安全,客户端连接S时要验证密码;
- 指令安全:如flushdb、flushall会让redis的所有数据清空,所以redist提供了rename-command用于将某些危险的指令修改成特别的名称。
- 端口安全:redis的配置文件中要绑定外网的ip,目的为了监听,防止redis的服务地址被外网直接访问,其内部的数据失去了安全性,黑客就可以通过redis执行Lua脚本拿到服务器权限,为所欲为。
- SSL代理:redis不支持ssl连接,因此C/S交互的数据不能在公网上传输,若想在公网上传输,可以使用ssl代理---->常见的有ssh
IO(补充)
-
同步、异步描述的是用户线程-----内核(操作系统资源)的交互方式。
同步:用户线程发起io请求后需要等待、或者轮询 等内核操作完成后才能继续执行;
异步:用户发起io请求后,可以继续执行,内核操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 -
阻塞、非阻塞指的是用户线程调用内核io操作的方式;
阻塞:io操作彻底完成后,才能返回给用户线程;
非阻塞:内核io操作后会返给用户线程一个状态值,无须等到io操作彻底完成。 -
同步阻塞io
用户线程系统调用read(socket,buffer)发起read服务器端的数据请求,(用户线程通过LWP连接到内核线程,然后由内核控制内核线程,对资源操作)在内核等待服务器端的数据包到达的这段时间+内核将接收到的数据拷贝到用户空间,完成read操作,用户线程是处于阻塞的状态。 -
同步非阻塞io
用户线程系统调用read(socket,buffer)发起read服务器端的数据请求,(用户线程通过LWP连接到内核线程,然后由内核控制内核线程,对资源操作),用户线程发起read请求后若服务器端的数据还未到达,可以立即返回,接着不断地发起请求,尝试读取socket里面的数据,直到数据到达,内核将数据拷贝到用户空间,用户线程读取数据到自个的缓存区成功,再继续处理接收到的数据。
缺点:轮询期间,占有CPU,耗能 -
io多路复用
前提:建立在由内核(或者同步事件多路分离器----->Synchronous event Demutiplexer)提供的多路分离函数select基础上,使用该函数可以避免同步非阻塞io中用户线程不断轮询等待的问题。
优点:用户可以注册多个socket,不断调用select获取被激活了的socket们,实现了在一个线程内处理多个socket的io请求,而在同步阻塞io中,要使用多线程才能做到(一个用户线程只能轮询一个指定的socket,不能解决处理多个io请求)。。
实现原理:用户线程将进行io操作的socket添加到select函数中,目的用于监视被激活的socket(因为当来(C or S的)请求来了或者当用户线程等待的数据到了,接收请求方的socket才会被唤醒),接着循环调用select来获取被唤醒的socket,(注意:selecth函数被调用期间,用户线程一直是阻塞的),一旦发现socket被唤醒------>表明数据可以读取了,select函数返回结果给用户线程,用户线程正式发起read请求、读取socket里的数据并继续往下执行。
用户线程使用select函数的伪代码描述:
{
select(socket);
while(1){
sockets=selerct();//select返回结果
for(socket in sockets){
if(can_read(socket)){
read(socket,buffer);
process(buffer);
}
}
}
}
select缺点:
注意:1)如果任何一个socket被唤醒,select仅仅会返回,但不会告诉你是哪个socket上有数据,你需要循环去寻找,开销颇大。
2)select只能监听1024个socket连接;
3)select不是线程安全的。
4) 需要进行两次遍历文件描述符集合—>存放的是已连接的Socket,目的时查找发生了网络事件的socket;一次:是select函数将文件描述符拷贝到内核里,由内核检查是否有网络事件,若有,则将socket标记为可读/可写;再将整个文件描述符拷贝到用户态;下一次:由用户态(用户线程在用户空间)遍历整个文件描述符,找到可读、可写的socket;
5)线性结构存储文件描述符集合,O(n)
poll
1)修复了select的很多问题
2)socket连接没限制;
3)不再修改传入的参数数组;
4)依然不是线程安全的,只可以在一个线程里面处理一组I/O流;
5)遍历文件描述符同上述。
epoll
1)线程安全的;
2)会告诉用户线程哪个socket里面有数据;
3)只有linux支持
4)内核里使用红黑树跟踪所有待检测的文件描述符,CID为O(n),每次只传入一个需要监控的Socket,减少了内核和用户空间大量的拷贝。
5)经过红黑树检测,当某个socket发生事件,通过回调函数内核将其加入一个由内核维护的就绪事件列表,当用户调用epoll_wait(),时,只返回有事件发生了的socket,而不是像那两个一顿夸夸复制过去轮询整个文件描述符集合。
通过epoll监听,及时要监听的socket越来越多,效率也不会降低。其被称为解决C10K问题的利器。
以上这三个都是IO多路复用的具体实现。
结论:在不断执行selecth函数为了读取可唤醒的socket,知道返回期间,用户线程一直是阻塞的,也就是说,在每个io请求的过程中是阻塞的。
但是,如果用户只注册自己感兴趣的socket或者io请求,然后不必阻塞,仍然可以去做自己的其他事情,等到数据来了再去处理,提高了cpu的利用率。
如何解决:引入一个中间体Reactor,去执行本该用户线程调用select函数的职责。
|> -
采取了reactor设计模式的io多路复用模型(又叫异步阻塞io模型)
用户线程注册io事件处理器(EventHandle----->拥有io文件句柄是通过get_handle()获取的)(该事件处理器交由Reactor管理),Reactor线程受理该事件处理器,接着,该线程会调用内核的select函数检查socket状态,并处于阻塞状态,当有socket被唤醒时(即io文件句柄被激活),(数据来了)select会返回给Reactor线程,Reactor线程就会去通知相应的用户线程(即Reacrotl类的handle_events()会调用与那个io文件句柄相关联的io事件处理器的handle_event()进行读写),由用户线程执行注册了的io事件处理器的handle_event进行数据的读取、处理工作,(还是由内核将数据拷贝到用户空间)。
用户线程注册自己的io事件处理器到Reactor,由其来对事件处理器进行注册、删除伪代码:
{
Reactor.register(new UserEventHandler(socket));
}
用户线程使用IO多路复用模型的伪代码描述为:
void UserEventHandler::handle_event() {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
Reactor类的handel_events事件循环的代码如下:
Reactor::handle_events(){
while(1){
sockets=select();
for(socket in sockets){
get_event_handle(socket).hanle_event();
}
}
}
优点:即上面"但是"描述的;
由于使用了会阻塞线程的select系统调用(系统调用由内核线程可直接调用,凡不是内核线程调用系统调用的话,都相当于用户级线程,是通过LWP连接到内核线程,和内核通信,都会阻塞用户级线程),所以该模型只能被称为异步阻塞io,并非真正的异步IO。 -
异步io
“真正”的异步IO需要操作系统更强的支持。
与io多路复用模型不同的是,handle_events事件循环将将socket的激活状态通知给用户线程,由用户线程的io事件处理器自行读取数据、处理数据(不明白:处理数据指什么?)
而异步io模型中,当用户线程收到通知时,内核已经将数据读取完毕,并放在了用户线程指定的缓冲区内,用户线程只需要直接使用即可。
以上实现是使用了Proactor设计模式实现的。
过程:用户线程要先将AsynchonousOperation、proctor(持有handle_events())、CompletionHandler(持有handle_event())(注意:用户线程需要先重写CompetionHandle的handle_event函数进行处理数据的工作,具体就是process(buffer)------->原来是用户线程自己读取数据到buffer,现在,这个buffer表示是Proactor已经准备好的数据)注册到Asynchronous Operation Processor(相当于内核)中,其使用Facade模式提供了一组异步操作API(主要是读写等)供用户线程使用,接着,用户线程直接使用这组由内核提供的异步IO API发起read请求,发起后立即返回,可继续执行用户线程的代码;Asynchronous Operation Processor会开启独立的内核线程去处理异步io操作------->即:当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区里。接着,Asynchronous Operation Processor(或者说是内核)将read到的数据和用户线程注册到的CompletionHandle一起转发给内部Proactor,接着,proactor的handle_events()负责调用用户线程注册的完成处理事件----即CompletionHandel的handle_event(),用来将IO操作完成的信息通知给用户线程,就此,read完成。
伪代码:
用户线程使用异步IO模型的伪代码描述为:
void UserCompletionHandler::handle_event(buffer) {
process(buffer);}
{//用户线程使用内核提供的异步ioAPI发起read请求;
aio_read(socket, new UserCompletionHandler);
}
用户需要重写CompletionHandler的handle_event函数进行处理数据的工作,参数buffer表示Proactor已经准备好的数据,用户线程直接调用内核提供的异步IO API,并将重写的CompletionHandler注册即可。
总结:相比io多路复用模型,异步io并不十分常用,取决于os对异步io的支持度;许多高性能并发服务程序使用 io多路复用模型+多线程任务处理的架构基本可以满足需求。 -
线程和io
一个线程的执行,通常需要3个资源:cpu—>负责计算、执行指令;内存------>负责存放即时数据;IO负责和磁盘、数据库、网络等做数据交换。
cpu:一个物理cpu只有一个核,即单核cpu----->在单位时间内,cpu只能处理一个线程,由于采用了时间片划分,实现了多线程。多核cpu下,每个核的处理情况和单核cpu一样。
逻辑cpu:即一个物理cpu抽象为多个cpu核心,一个物理cpu可以支持多线程,但并不是前面所说的时间片实现的,它是真正意义上的单位时间的多线程,即逻辑cpu的个数和系统单位时间内可执行的线程数量一样。
I/O:常见的io如:文件流、数据库连接、网络连接;注意:单位时间内只能为一个线程服务,它并不能像单核cpu那样,通过切片,执行多线程;另外,当一个线程在使用I/O时,在使用的这段时间内,线程不能进行计算、读写内存。即:一个线程在使用I/O时,可以把cpu执行权交给别的线程,可以充分的利用系统资源。
socket
-
实现客户端和服务端在网络中通信,特别:可以跨主机间通信;
-
双方在网络通信前,都要各自创建一个socket,创建时要指定网络层使用的是iPV4/iPV6,传输层用的是TCP/UDP;
服务端:要先跑起来,等待客户端的连接请求和数据。
1)服务端的socket要bind一个IP地址和端口,当内核收到tcp报文,要通过tcp里面的端口来找到应用程序,然后将数据传给我们;一台机器有多个网卡,每个网卡都有对应的ip地址,内核在收到该ip对应的网卡上的数据包时,才会发给我们。
2)接下来,服务器端进入了监听状态,之后调用accept(),来从内核获取客户端的连接,一直到客户端的连接来,是处于阻塞的状态。
客户端:创建的socket,通过connect()发起连接,通过该函数的参数,要指明服务端的ip、端口。接着,三次握手就开始了。 -
在tcp连接中,服务端的内核实际上为每个socket维护了两个队列,一个队列里是没有完成三次握手连接的socket,此时,服务端处于syn_rcvd状态。一个队列里都是完成了三次握手连接的socket,此时的服务端状态是established状态。
服务端的accept函数就会从这个全连接的队列中拿出一个已连接的socket返回应用程序(或者用户线程),后续的数据传输都用这个Socket.
注意:上述用于监听的socket和真正用来传输数据的socket是两个东西。 -
连接建立后,就可以传输数据了,双方都可以通过read()、write()来读写数据。
多进程模型
上述的C---S通信基本是一对一,采用的是同步阻塞,S在没有处理完一个客户端的网络IO时,或者读写发生阻塞时,是没办法和其他的C连接的。
服务器单机理论上可以连接的最大客户端tcpl连接数是客户端IP数*客户端端口数。但服务器肯定承载不了这么大的连接数,主要受两方面的限制:1)单个进程可以连接的socket是有限制的 ;2)系统内存:每个tcpl连接在内核中都有对应的数据结构,占有一定的内存。
为了使服务端单机并发处理过万的客户端请求,先是提出了**多进程模型**------>
1)为每个客户端请求分配一个进程来处理
2)服务器端的主进程负责监听客户的连接,一旦连接建立,accept()会返回一个socket,接着主进程通过fork()创建一个子进程(继承了父进程的文件描述符----->每个socket都有一个文件描述符、内存地址空间、程序计数器、执行的代码等),直接使用这个socket和对应的客户端通信(进行read()、write()).
缺点:客户端数量很多时,扛不住,进程的上下文切换包袱很重。
多线程模型
- 服务端将与客户端建立了连接的socket们统一放在一个队列里,通过phtrread_creat()创建线程,线程从这个队列里取出一个socket和客户端通信。
注意:这里采用线程池的方式管理线程。
但对与上万个线程,维护起来对os还是颇有压力。
Redis—IO多路复用
-
redis通过监听tcp端口的方式来接受客户端的连接,客户端socket设置属性tcp_nodelay,目的是禁用nagle算法----->如果redis以命令的形式在客户端输入数据,由于数据量小,希望可以快速得到服务端的应答,即低延时,nagel算法并不适用,其适合传输较大的数据在广域网,减少分组的报文数。
-
redis在网络事件处理上采用了io多路复用模型。
1) 其底层是一个单线程模型,即使用一个线程来处理所有的网络事件,避免了线程切换导致的cpu开销,也不用考虑各种锁问题。
2)redis采用的是epoll的方式监听多个socket,原理同上面讲述的一样; -
命令使用:
1)client kll 127.0.0.1:49502//关闭当前客户端连接
2)client list//以列表的形式返回所有连接到redis服务器的客户端
输出:id=2556 addr=127.0.0.1:64684 fd=30 name=age=2422 idle=339 flags=N db=0
id=2557 addr=127.0.0.1:64684 …