Netty工作模型——网络IO模型的演进,从BIO到NIO到Reactor模型与Netty工作模型

news/2024/11/3 5:32:21/

文章目录

  • 一、IO模型
    • 1、BIO(同步阻塞)
    • 2、NIO(同步非阻塞)
    • 3、AIO(异步非阻塞)
  • 二、Reactor模型
    • 1、单Reactor单线程
    • 2、单Reactor多线程
    • 3、主从Reactor多线程
  • 三、Netty工作模型
    • 1、Netty工作模型
    • 2、Netty入门案例
      • (1)server
      • (2)client
    • 3、Netty任务队列
  • 四、异步模型
    • 1、Future-Listener机制
  • 参考资料

一、IO模型

I/O 模型简单的理解:就是 用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO。

1、BIO(同步阻塞)

服务器实现模式为 一个连接对应一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

在JDK1.4之前,BIO是我们实现网络编程的唯一选择,虽然服务端对并发量支撑较差,但好在程序简单易理解。

在这里插入图片描述


import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class BIOServer {public static void main(String[] args) throws Exception {//线程池机制//思路//1. 创建一个线程池//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();//创建ServerSocketServerSocket serverSocket = new ServerSocket(6666);System.out.println("服务器启动了");while (true) {System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());//监听,等待客户端连接System.out.println("等待连接....");final Socket socket = serverSocket.accept();System.out.println("连接到一个客户端");//就创建一个线程,与之通讯(单独写一个方法)newCachedThreadPool.execute(new Runnable() {public void run() { //我们重写//可以和客户端通讯handler(socket);}});}}//编写一个handler方法,和客户端通讯public static void handler(Socket socket) {try {System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());byte[] bytes = new byte[1024];//通过socket 获取输入流InputStream inputStream = socket.getInputStream();//循环的读取客户端发送的数据while (true) {System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());System.out.println("read....");int read =  inputStream.read(bytes);if(read != -1) {System.out.println(new String(bytes, 0, read)); //输出客户端发送的数据} else {break;}}}catch (Exception e) {e.printStackTrace();}finally {System.out.println("关闭和client的连接");try {socket.close();}catch (Exception e) {e.printStackTrace();}}}
}

BIO的问题也很明显:
1.每个请求都需要创建独立的线程,与对应的客户端进行数据Read、业务处理、数据Write。
2.当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3.连接建立后,如果当前线程暂时没有数据可读,则线程会阻塞在Read操作上,造成线程资源浪费。

2、NIO(同步非阻塞)

Java NIO全称java non-blocking IO,是指JDK从1.4开始提供新的API,是同步非阻塞的,NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。

NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。

在这里插入图片描述
在这里插入图片描述
由于NIO的编程复杂、API复杂等等原因,Netty应运而生。

3、AIO(异步非阻塞)

JDK7 引入了 Asynchronous I/0,即 AIO。在进行IO编程中,常用到两种模式:Reactor 和 Proactor。Java 的NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。

AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

在这里插入图片描述

二、Reactor模型

AIO目前并不是主流的IO模型,linux目前并不支持AIO的异步非阻塞模式,即使是epoll也是在NIO的基础上进行的优化,所以当前主流的IO模型还是NIO模型。

1、单Reactor单线程

在这里插入图片描述
在这里插入图片描述

  • Select 是Java NIO多路复用模型的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求。
  • Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发。
  • 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接3完成后的后续业务处理。
  • 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应。
  • Handler 会完成 Read一业务处理一Send 的完整业务流程。

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;public class NIOServer {public static void main(String[] args) throws Exception{//创建ServerSocketChannel -> ServerSocketServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//得到一个Selecor对象Selector selector = Selector.open();//绑定一个端口6666, 在服务器端监听serverSocketChannel.socket().bind(new InetSocketAddress(6666));//设置为非阻塞serverSocketChannel.configureBlocking(false);//把 serverSocketChannel 注册到  selector 关心 事件为 OP_ACCEPTserverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1//循环等待客户端连接while (true) {//这里我们等待1秒,如果没有事件发生, 返回if(selector.select(1000) == 0) { //没有事件发生System.out.println("服务器等待了1秒,无连接");continue;}//如果返回的>0, 就获取到相关的 selectionKey集合//1.如果返回的>0, 表示已经获取到关注的事件//2. selector.selectedKeys() 返回关注事件的集合//   通过 selectionKeys 反向获取通道Set<SelectionKey> selectionKeys = selector.selectedKeys();System.out.println("selectionKeys 数量 = " + selectionKeys.size());//遍历 Set<SelectionKey>, 使用迭代器遍历Iterator<SelectionKey> keyIterator = selectionKeys.iterator();while (keyIterator.hasNext()) {//获取到SelectionKeySelectionKey key = keyIterator.next();//根据key 对应的通道发生的事件做相应处理if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接//该该客户端生成一个 SocketChannelSocketChannel socketChannel = serverSocketChannel.accept();System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());//将  SocketChannel 设置为非阻塞socketChannel.configureBlocking(false);//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel//关联一个BuffersocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..}if(key.isReadable()) {  //发生 OP_READ//通过key 反向获取到对应channelSocketChannel channel = (SocketChannel)key.channel();//获取到该channel关联的bufferByteBuffer buffer = (ByteBuffer)key.attachment();channel.read(buffer);System.out.println("form 客户端 " + new String(buffer.array()));}//手动从集合中移动当前的selectionKey, 防止重复操作keyIterator.remove();}}}
}

这种模型比较简单,没有多线程问题,所有的连接、业务处理都在一个线程中完成。

这种模型显而易见有个致命缺点,就是当业务处理比较耗时时,会造成消息积压。

2、单Reactor多线程

在这里插入图片描述
在这里插入图片描述

Reactor 对象通过 select 监控客户端请求事件,收到事件后,通过 dispatch 进行分发。

  • 如果建立连接请求,则右 Acceptor 通过accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件
  • 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理。
  • handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务。
  • worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler。
  • handler 收到响应后,通过 send 将结果返回给 client。

这种模式充分利用了多核CPU的处理能力,使用多线程进行业务处理。但是单reactor处理所有的事件的监听和响应,在单reactor下,高并发场景很容易出现性能瓶颈。

3、主从Reactor多线程

在这里插入图片描述
在这里插入图片描述

在这种模式下,对Reactor进一步拆分。

  • Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过Acceptor 处理连接事件。
  • 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor。
  • subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理。
  • 当有新事件发生时,subreactor 就会调用对应的 handler 处理。
  • handler 通过 read 读取数据,分发给后面的 worker 线程处理。
  • worker 线程池分配独立的 worker 线程进行业务处理,并返回结果。
  • handler 收到响应的结果后,再通过 send 将结果返回给 client。
  • Reactor 主线程可以对应多个 Reactor 子线程、即 MainRecator 可以关联多个 SubReactor。

三、Netty工作模型

1、Netty工作模型

Netty就是在主从Reactor多线程的模型下,进行了优化产生的。

在这里插入图片描述

  • Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写。
  • BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup(在Epoll支持下也可以是EpollEventLoopGroup)
  • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop。
  • NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 的网络通讯。
  • NioEventLoopGroup 可以有多个线程、即可以含有多个 NioEventLoop。
  • 每个 Boss NioEventLoop 循环执行的步骤有 3 步:
    (1)轮询 accept 事件(2)处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NIOEventLoop 上的 selector(3)处理任务队列的任务 ,即 runAllTasks
  • 每个 Worker NIOEventLoop 循环执行的步骤:
    (1)轮询 read,write 事件(2)处理 i/o 事件,即 read ,write 事件,在对应 NioScocketChannel 处理(3)处理任务队列的任务,即 runAlITasks
  • 每个 WorkerNIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel,即通过pipeline可以获取到对应通道。管道中维护了很多的 处理器

2、Netty入门案例

(1)server


public class NettyServer {public static void main(String[] args) throws Exception {//创建BossGroup 和 WorkerGroup//说明//1. 创建两个线程组 bossGroup 和 workerGroup//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成//3. 两个都是无限循环//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数//   默认实际 cpu核数 * 2 (NettyRuntime.availableProcessors() * 2)EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup(); //8try {//创建服务器端的启动对象,配置参数ServerBootstrap bootstrap = new ServerBootstrap();//使用链式编程来进行设置bootstrap.group(bossGroup, workerGroup) //设置两个线程组.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
//                    .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)//给pipeline 设置处理器@Overrideprotected void initChannel(SocketChannel ch) throws Exception {System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueuech.pipeline().addLast(new NettyServerHandler());}}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器System.out.println(".....服务器 is ready...");//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象//启动服务器(并绑定端口)ChannelFuture cf = bootstrap.bind(6668).sync();//给cf 注册监听器,监控我们关心的事件cf.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {if (cf.isSuccess()) {System.out.println("监听端口 6668 成功");} else {System.out.println("监听端口 6668 失败");}}});//对关闭通道进行监听cf.channel().closeFuture().sync();}finally {// 优雅关闭bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}/*
说明
1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
2. 这时我们自定义一个Handler , 才能称为一个handler*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {//读取数据事件(这里我们可以读取客户端发送的消息)/*1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址2. Object msg: 就是客户端发送的数据 默认Object*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());System.out.println("server ctx =" + ctx);System.out.println("看看channel 和 pipeline的关系");Channel channel = ctx.channel(); // channel和pipeline是互相包含的关系ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链表, 涉及到出栈入栈问题//将 msg 转成一个 ByteBuf//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.ByteBuf buf = (ByteBuf) msg;System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));System.out.println("客户端地址:" + channel.remoteAddress());}//数据读取完毕@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {//writeAndFlush 是 write + flush//将数据写入到缓存,并刷新//一般讲,我们对这个发送的数据进行编码ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));}//处理异常, 一般是需要关闭通道@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();}
}

(2)client


public class NettyClient {public static void main(String[] args) throws Exception {//客户端需要一个事件循环组EventLoopGroup group = new NioEventLoopGroup();try {//创建客户端启动对象//注意客户端使用的不是 ServerBootstrap 而是 BootstrapBootstrap bootstrap = new Bootstrap();//设置相关参数bootstrap.group(group) //设置线程组.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器}});System.out.println("客户端 ok..");//启动客户端去连接服务器端//关于 ChannelFuture 要分析,涉及到netty的异步模型ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();//给关闭通道进行监听channelFuture.channel().closeFuture().sync();}finally {// 优雅关闭group.shutdownGracefully();}}
}public class NettyClientHandler extends ChannelInboundHandlerAdapter {//当通道就绪就会触发该方法@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {System.out.println("client " + ctx);ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));}//当通道有读取事件时,会触发@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}

3、Netty任务队列

(1)用户程序自定义的普通任务
将任务推送至普通任务队列异步执行,不阻塞主线程。

(2)定时任务
将任务推送至定时任务队列中异步执行,不阻塞主线程。

//读取数据事件(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交在该channel 对应的NIOEventLoop 的 taskQueue中//解决方案1 用户程序自定义的普通任务ctx.channel().eventLoop().execute(new Runnable() {@Overridepublic void run() {try {Thread.sleep(5 * 1000);ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));System.out.println("channel code=" + ctx.channel().hashCode());} catch (Exception ex) {System.out.println("发生异常" + ex.getMessage());}}});// 上面的任务执行完毕之后,才会执行该任务!taskQueue中的任务是同一个线程ctx.channel().eventLoop().execute(new Runnable() {@Overridepublic void run() {try {Thread.sleep(5 * 1000);ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));System.out.println("channel code=" + ctx.channel().hashCode());} catch (Exception ex) {System.out.println("发生异常" + ex.getMessage());}}});//解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中ctx.channel().eventLoop().schedule(new Runnable() {@Overridepublic void run() {try {Thread.sleep(5 * 1000);ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));System.out.println("channel code=" + ctx.channel().hashCode());} catch (Exception ex) {System.out.println("发生异常" + ex.getMessage());}}}, 5, TimeUnit.SECONDS);System.out.println("go on ...");System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());System.out.println("server ctx =" + ctx);System.out.println("看看channel 和 pipeline的关系");Channel channel = ctx.channel(); // channel和pipeline是互相包含的关系ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链表, 涉及到出栈入栈问题//将 msg 转成一个 ByteBuf//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.ByteBuf buf = (ByteBuf) msg;System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));System.out.println("客户端地址:" + channel.remoteAddress());
}

(3)非当前Reactor线程调用Channel的各种方法
将channel列表维护为一个List,可以取得该List中的数据在非当前用户下推送给其他用户。

//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
//                    .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)//给pipeline 设置处理器@Overrideprotected void initChannel(SocketChannel ch) throws Exception {System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueuech.pipeline().addLast(new NettyServerHandler());}}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器

Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道。NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop负责。

NioEventLoopGroup 下包含多个 NioEventLoop

  • 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
  • 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
  • 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
  • 每个 NioChannel 都绑定有一个自己的 ChannelPipeline

四、异步模型

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。

Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得IO 操作结果。

Netty 的异步模型是建立在 fuure 和 callback 的之上的。callback 就是回调。 Future是重点,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun 返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future 去监控方法 fun 的处理过程(即 : Future-Listener 机制)

1、Future-Listener机制

当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态注册监听函数来执行完成后的操作。

常见有如下操作:

  • 通过 isDone 方法来判断当前操作是否完成;
  • 通过 isSuccess 方法来判断已完成的当前操作是否成功;
  • 通过 getCause 方法来获取已完成的当前操作失败的原因;
  • 通过 isCancelled 方法来判断已完成的当前操作是否被取消;通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果Future 对象已完成,则通知指定的监听器
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();//给cf 注册监听器,监控我们关心的事件cf.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {if (cf.isSuccess()) {System.out.println("监听端口 6668 成功");} else {System.out.println("监听端口 6668 失败");}}
});

参考资料

《Scalable IO in Java》
https://blog.csdn.net/qq_36389060/article/details/124232377
https://www.bilibili.com/video/BV1DJ411m7NR


http://www.ppmy.cn/news/49520.html

相关文章

Mysql简介、DCL、规范、表关联关系、版本和工具、自带数据库作用

目录 1. Mysql简介2. SQL的DCL3. Mysql的规范4. 表的关联关系5. Mysql版本和工具6. Mysql自带数据库的作用 1. Mysql简介 MySQL是可以定制的&#xff0c;采用了GPL(GNU General Public License协议&#xff0c;可以修改源码来开发自己的MySQL系统MySQL支持大型数据库&#xff…

使用tun虚拟网络接口建立IP隧道的实例

通常的socket编程,面对的都是物理网卡,Linux下其实很容易创建虚拟网卡;本文简单介绍一下Linux虚拟网卡的概念,并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡,最终实现一个从客户端到服务器的简单的IP隧道,希望本文能对理解虚拟网卡和IP隧道有所帮助,本文…

【Linux命令行与Shell脚本编程】第五章 理解 Shell

Linux命令行与Shell脚本编程 第五章 理解 Shell 文章目录 Linux命令行与Shell脚本编程五,理解 Shell5.1,shell的类型5.2,shell的父子关系5.2.1,进程列表 ()5.2.2 子shell的用法1,后台模式2,后台进程列表3,协程 5.3,外部命令和内建命令5.3.1,外部命令5.3.2,内建命令1,history 命…

redis入门必会知识

Redis基础知识目录 5、sortedSet 文章目录 系列文章目录前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 一、redis是什么&#xff1f; Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务 ! 是一个开源的使用ANSI C语言编写…

Nginx的优化-安全与防盗链

1.Nginx的网页优化-网页压缩 在Nginx的ngx_http_gzip_module压缩模块提供对文件内容压缩的功能。进行相关的配置修改&#xff0c;就能实现Nginx页面的压缩&#xff0c;达到节约带宽&#xff0c;提升用户访问速度 重启服务进行访问测试 2.配置Nginx的图片缓存 当Nginx将网页数据…

操作系统(四)——文件管理

文章目录 第四章 文件管理[4.1.1] 初识文件管理&#xff08;一&#xff09;文件的属性&#xff08;二&#xff09;文件内部的数据应该怎样组织起来&#xff08;三&#xff09;文件之间应该怎样组织起来&#xff08;四&#xff09;操作系统应该向上提供哪些功能&#xff08;五&a…

nodejs+vue宠物商城健康医院挂号服务管理系统python+java+php

在前台&#xff0c;首先提供一个界面清晰、导航明确的首页&#xff0c;无论是会员还是游客都可以访问。游客通过首页查看该网站所要具备的功能&#xff0c;以及对应的周边商城信息&#xff0c;特别在周边商城模块&#xff0c;需要明确的进行介绍&#xff0c;突出周边商城特色和…

Java语法理论和面经杂疑篇《十二. JDK8 - 17新特性》

第18章_JDK8-17新特性&#xff08;下&#xff09; 6. 新语法结构 新的语法结构&#xff0c;为我们勾勒出了 Java 语法进化的一个趋势&#xff0c;将开发者从复杂、繁琐的低层次抽象中逐渐解放出来&#xff0c;以更高层次、更优雅的抽象&#xff0c;既降低代码量&#xff0c;又…