Java NIO实现高性能HTTP代理

news2024/11/14 14:30:06
NIO采用多路复用IO模型,相比传统BIO(阻塞IO),通过轮询机制检测注册的Channel是否有事件发生,可以实现一个线程处理客户端的多个连接,极大提升了并发性能。
在5年前,本人出于对HTTP正向代理的好奇新,那时候也在学习JAVA,了解到了NIO,就想用NIO写一个正向代理软件,当时虽然实现了正向代理,但是代码逻辑及其混乱,而且没有经过测试也许有不少的bug
近期因为找工作,又复习起了以往的一些JAVA知识,包括JVM内存模型、GC垃圾回收机制等等,其中也包括NIO。现在回头再看NIO,理解也更深刻了一点。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
本HTTP代理软件只能代理HTTP和HTTPS协议,分享出来共广大网友参考和学习
1.Bootstrap类
此类用于创建和启动一个HTTP代理服务
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Bootstrap {
    private final Logger logger = LogManager.getLogger(Bootstrap.class);
    private AbstractEventLoop serverEventLoop;
    private int port;

    public Bootstrap() {
        port = 8888;
        serverEventLoop = new ServerEventLoop(this);
    }

    public Bootstrap bindPort(int port) {
        try {
            this.port = port;
            this.serverEventLoop.bind(port);
        } catch (Exception e) {
            logger.error("open server socket channel error.", e);
        }
        return this;
    }

    public void start() {
        serverEventLoop.getSelector().wakeup();
        logger.info("Proxy server started at port {}.", port);
    }

    public AbstractEventLoop getServerEventLoop() {
        return serverEventLoop;
    }
}


2.ServerEventLoop
事件循环,单线程处理事件循环。包括客户端的连接和读写请求,目标服务器的连接和读写事件,在同一个事件循环中处理。
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.example.common.HttpRequestParser;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class ServerEventLoop extends AbstractEventLoop {
    private final Logger logger = LogManager.getLogger(ServerEventLoop.class);

    public ServerEventLoop(Bootstrap bootstrap) {
        super(bootstrap);
    }

    @Override
    protected void processSelectedKey(SelectionKey key) {
        if (key.isValid() && key.isAcceptable()) {
            if (key.attachment() instanceof Acceptor acceptor) {
                acceptor.accept();
            }
        }
        if (key.isValid() && key.isReadable()) {
            if (key.attachment() instanceof ChannelHandler channelHandler) {
                channelHandler.handleRead();
            }
        }
        if (key.isValid() && key.isConnectable()) {
            key.interestOpsAnd(~SelectionKey.OP_CONNECT);
            if (key.attachment() instanceof ChannelHandler channelHandler) {
                channelHandler.handleConnect();
            }
        }
        if (key.isValid() && key.isWritable()) {
            key.interestOpsAnd(~SelectionKey.OP_WRITE);
            if (key.attachment() instanceof ChannelHandler channelHandler) {
                channelHandler.handleWrite();
            }
        }
    }

    @Override
    public void bind(int port) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        SelectionKey key = serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        key.attach(new Acceptor(serverSocketChannel));
        serverSocketChannel.bind(new InetSocketAddress(port));
    }

    class Acceptor {
        ServerSocketChannel ssc;

        public Acceptor(ServerSocketChannel ssc) {
            this.ssc = ssc;
        }

        public void accept() {
            try {
                SocketChannel socketChannel = ssc.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ, new ClientChannelHandler(socketChannel));
                logger.info("accept client connection");
            } catch (IOException e) {
                logger.error("accept error");
            }
        }
    }

    abstract class ChannelHandler {
        Logger logger;
        SocketChannel channel;
        ByteBuffer writeBuffer;

        public ChannelHandler(SocketChannel channel) {
            this.logger = LogManager.getLogger(this.getClass());
            this.channel = channel;
            this.writeBuffer = null;
        }

        abstract void handleRead();

        public void handleWrite() {
            doWrite();
        }

        public abstract void onChannelClose();

        public ByteBuffer doRead() {
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            try {
                int len = channel.read(buffer);
                if (len == -1) {
                    logger.info("read end-of-stream, close channel {}", channel);
                    channel.close();
                    onChannelClose();
                }
                if (len > 0) {
                    buffer.flip();
                }
            } catch (IOException e) {
                logger.error("read channel error");
                try {
                    channel.close();
                    onChannelClose();
                } catch (IOException ex) {
                    logger.error("close channel error.");
                }
            }
            return buffer;
        }

        public void doWrite() {
            if (writeBuffer != null) {
                try {
                    while (writeBuffer.hasRemaining()) {
                        channel.write(writeBuffer);
                    }
                } catch (IOException e) {
                    logger.error("write channel error.");
                    try {
                        channel.close();
                        onChannelClose();
                    } catch (IOException ex) {
                        logger.error("close channel error");
                    }
                }
                writeBuffer = null;
            }
        }

        public void handleConnect() {
        }
    }

    class ClientChannelHandler extends ChannelHandler {
        HttpRequestParser requestParser;
        private SelectableChannel proxyChannel;

        public ClientChannelHandler(SocketChannel sc) {
            super(sc);
            this.channel = sc;
            this.requestParser = new HttpRequestParser();
            this.proxyChannel = null;
        }

        @Override
        public void handleRead() {
            if (requestParser.isParsed()) {
                if (proxyChannel != null) {
                    SelectionKey proxyKey = proxyChannel.keyFor(selector);
                    if (proxyKey != null && proxyKey.isValid() && proxyKey.attachment() instanceof ProxyChannelHandler proxyHandler) {
                        //需要等待ProxyHandler的写入缓存为空后才可读取客户端的数据
                        if (proxyHandler.writeBuffer == null) {
                            ByteBuffer buffer = doRead();
                            if (buffer.hasRemaining() && proxyKey.isValid()) {
                                proxyHandler.writeBuffer = buffer;
                                proxyKey.interestOpsOr(SelectionKey.OP_WRITE);
                            }
                        }
                    }
                }
            } else {
                ByteBuffer buffer = doRead();
                requestParser.putFromByteBuffer(buffer);
                if (requestParser.isParsed()) {
                    //连接到目标服务器
                    ByteBuffer buf = null;
                    if (requestParser.getMethod().equals(HttpRequestParser.HTTP_METHOD_CONNECT)) {
                        //回写客户端连接成功
                        SelectionKey clientKey = channel.keyFor(selector);
                        if (clientKey != null && clientKey.isValid() && clientKey.attachment() instanceof ClientChannelHandler clientHandler) {
                            clientHandler.writeBuffer = ByteBuffer.wrap((requestParser.getProtocol() + " 200 Connection Established\r\n\r\n").getBytes());
                            clientKey.interestOpsOr(SelectionKey.OP_WRITE);
                        }
                    } else {
                        //将缓存的客户端的数据通过代理转发
                        byte[] allBytes = requestParser.getAllBytes();
                        buf = ByteBuffer.wrap(allBytes);
                    }
                    this.proxyChannel = connect(requestParser.getAddress(), buf);
                }
            }

        }

        @Override
        public void onChannelClose() {
            try {
                if (proxyChannel != null) {
                    proxyChannel.close();
                }
            } catch (IOException e) {
                logger.error("close channel error");
            }
        }

        private SocketChannel connect(String address, ByteBuffer buffer) {
            String host = address;
            int port = 80;
            if (address.contains(":")) {
                host = address.split(":")[0].trim();
                port = Integer.parseInt(address.split(":")[1].trim());
            }
            SocketAddress target = new InetSocketAddress(host, port);
            SocketChannel socketChannel = null;
            SelectionKey proxyKey = null;
            int step = 0;
            try {
                socketChannel = SocketChannel.open();
                socketChannel.configureBlocking(false);
                step = 1;
                ProxyChannelHandler proxyHandler = new ProxyChannelHandler(socketChannel);
                proxyHandler.setClientChannel(channel);
                proxyHandler.writeBuffer = buffer;
                proxyKey = socketChannel.register(selector, SelectionKey.OP_CONNECT, proxyHandler);
                proxyKey.interestOpsOr(SelectionKey.OP_WRITE);
                step = 2;
                socketChannel.connect(target);
            } catch (IOException e) {
                logger.error("connect error.");
                switch (step) {
                    case 2:
                        proxyKey.cancel();
                    case 1:
                        try {
                            socketChannel.close();
                        } catch (IOException ex) {
                            logger.error("close channel error.");
                        }
                        socketChannel = null;
                        break;
                }
            }
            return socketChannel;
        }
    }

    class ProxyChannelHandler extends ChannelHandler {
        private SelectableChannel clientChannel;

        public ProxyChannelHandler(SocketChannel sc) {
            super(sc);
            clientChannel = null;
        }

        @Override
        public void handleConnect() {
            try {
                if (channel.isConnectionPending() && channel.finishConnect()) {
                    SelectionKey proxyKey = channel.keyFor(selector);
                    proxyKey.interestOpsOr(SelectionKey.OP_READ);
                }
            } catch (IOException e) {
                try {
                    channel.close();
                    onChannelClose();
                } catch (IOException ex) {
                    logger.error("close channel error.");
                }
                logger.error("finish connection error.");
            }
        }

        @Override
        public void handleRead() {
            if (clientChannel != null) {
                SelectionKey clientKey = clientChannel.keyFor(selector);
                if (clientKey != null && clientKey.isValid() && clientKey.attachment() instanceof ClientChannelHandler clientHandler) {
                    if (clientHandler.writeBuffer == null) {
                        ByteBuffer buffer = doRead();
                        if (buffer.hasRemaining() && clientKey.isValid()) {
                            clientHandler.writeBuffer = buffer;
                            clientKey.interestOpsOr(SelectionKey.OP_WRITE);
                        }
                    }
                }
            }
        }

        @Override
        public void onChannelClose() {
            try {
                if (clientChannel != null) {
                    clientChannel.close();
                }
            } catch (IOException e) {
                logger.error("close channel error");
            }
        }

        public void setClientChannel(SocketChannel client) {
            this.clientChannel = client;
        }
    }
}
3.AbstractEventLoop
事件循环的抽象类
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Executors;

public abstract class AbstractEventLoop implements Runnable {
    private final Logger logger = LogManager.getLogger(AbstractEventLoop.class);
    protected Selector selector;
    protected Bootstrap bootstrap;

    public AbstractEventLoop(Bootstrap bootstrap) {
        this.bootstrap = bootstrap;
        openSelector();
        Executors.newSingleThreadExecutor().submit(this);
    }

    public void bind(int port) throws Exception {
        throw new Exception("not support");
    }

    @Override
    public void run() {
        while (true) {
            try {
                if (selector.select() > 0) {
                    processSelectedKeys();
                }
            } catch (Exception e) {
                logger.error("select error.", e);
            }
        }
    }

    private void processSelectedKeys() {
        Set<SelectionKey> keys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = keys.iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            iterator.remove();
            processSelectedKey(key);
        }
    }
    protected abstract void processSelectedKey(SelectionKey key);
    public Selector openSelector() {
        try {
            this.selector = Selector.open();
            return this.selector;
        } catch (IOException e) {
            logger.error("open selector error.", e);
        }
        return null;
    }

    public Selector getSelector() {
        return selector;
    }
}

4.HttpRequestParser
用于解析HTTP请求报文中的请求头,可以获取主机和端口号
package org.example.common;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class HttpRequestParser {
    private final Logger logger = LogManager.getLogger(HttpRequestParser.class);
    public static final String COLON = ":";
    public static final String REQUEST_HEADER_HOST_PREFIX = "host:";
    private UnboundedByteBuffer requestBytes = new UnboundedByteBuffer();
    private List<String> headers = new ArrayList<>();
    public static final String HTTP_METHOD_GET = "GET";
    public static final String HTTP_METHOD_POST = "POST";
    public static final String HTTP_METHOD_PUT = "PUT";
    public static final String HTTP_METHOD_DELETE = "DELETE";
    public static final String HTTP_METHOD_TRACE = "TRACE";
    public static final String HTTP_METHOD_OPTIONS = "OPTIONS";
    public static final String HTTP_METHOD_HEAD = "HEAD";
    public static final String HTTP_METHOD_CONNECT = "CONNECT";

    private String address;

    private String protocol;
    private String method;

    private boolean parsed = false;

    private StringBuffer reqHeaderBuffer = new StringBuffer();

    public void putFromByteBuffer(ByteBuffer buffer) {
        for (; buffer.hasRemaining(); ) {
            byte b = buffer.get();
            requestBytes.addByte(b);
            reqHeaderBuffer.append((char) b);
            if (b == '\n' && reqHeaderBuffer.charAt(reqHeaderBuffer.length() - 2) == '\r') {
                if (reqHeaderBuffer.length() == 2) {
                    parsed = true;
                    logger.debug("Request header line end.");
                    break;
                }
                String headerLine = reqHeaderBuffer.substring(0, reqHeaderBuffer.length() - 2);
                logger.debug("Request header line parsed {}", headerLine);
                headers.add(headerLine);
                if (headerLine.startsWith(HTTP_METHOD_GET)
                        || headerLine.startsWith(HTTP_METHOD_POST)
                        || headerLine.startsWith(HTTP_METHOD_PUT)
                        || headerLine.startsWith(HTTP_METHOD_DELETE)
                        || headerLine.startsWith(HTTP_METHOD_TRACE)
                        || headerLine.startsWith(HTTP_METHOD_OPTIONS)
                        || headerLine.startsWith(HTTP_METHOD_HEAD)
                        || headerLine.startsWith(HTTP_METHOD_CONNECT)) {
                    this.protocol = headerLine.split(" ")[2].trim();
                    this.method = headerLine.split(" ")[0].trim();
                } else if (headerLine.toLowerCase().startsWith(REQUEST_HEADER_HOST_PREFIX)) {
                    this.address = headerLine.toLowerCase().replace(REQUEST_HEADER_HOST_PREFIX, "").trim();
                }
                reqHeaderBuffer.delete(0, reqHeaderBuffer.length());
            }
        }
    }

    public boolean isParsed() {
        return parsed;
    }

    public String getAddress() {
        return address;
    }

    public String getProtocol() {
        return protocol;
    }

    public String getMethod() {
        return method;
    }

    public byte[] getAllBytes() {
        return requestBytes.toByteArray();
    }
}

5.UnboundedByteBuffer
无界的字节缓冲区,每次会以两倍的容量扩容,可以用于追加存入客户端的请求数据,实现粘包
package org.example.common;

public class UnboundedByteBuffer {
    private byte[] bytes;
    private int size;
    private int cap;
    private final int DEFAULT_CAP = 4096;
    private final int MAX_CAP = 1 << 30;

    public UnboundedByteBuffer() {
        this.cap = DEFAULT_CAP;
        this.bytes = new byte[this.cap];
        this.size = 0;
    }

    public void addBytes(byte[] data) {
        ensureCapacity(data.length);
        System.arraycopy(data, 0, bytes, size, data.length);
        this.size += data.length;
    }

    private void ensureCapacity(int scale) {
        if (scale + this.size > this.cap) {
            int tmpCap = this.cap;
            while (scale + this.size > tmpCap) {
                tmpCap = tmpCap << 1;
            }
            if (tmpCap > MAX_CAP) {
                return;
            }
            byte[] newBytes = new byte[tmpCap];
            System.arraycopy(this.bytes, 0, newBytes, 0, this.size);
            this.bytes = newBytes;
        }
    }

    public byte[] toByteArray() {
        byte[] ret = new byte[this.size];
        System.arraycopy(this.bytes, 0, ret, 0, this.size);
        return ret;
    }

    public void addByte(byte b) {
        ensureCapacity(1);
        this.bytes[this.size++] = b;
    }
}

以上实现是在单个事件循环线程中处理所有事件,一个更好的方案是将客户端的Channel和代理服务器与目标服务器的Channel区分开,分别在两个事件循环中处理。基本实现也和本文中的代码大体一致,两者在理论上应该存在性能差距,实际经过本人测试可以每秒处理客户端的上千个连接。代码传送门

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2238678.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang | Leetcode Golang题解之第554题砖墙

题目&#xff1a; 题解&#xff1a; func leastBricks(wall [][]int) int {cnt : map[int]int{}for _, widths : range wall {sum : 0for _, width : range widths[:len(widths)-1] {sum widthcnt[sum]}}maxCnt : 0for _, c : range cnt {if c > maxCnt {maxCnt c}}retur…

QT中使用图表之QChart绘制动态折线图

使用QChart绘制一个随着时间的变化而动态显示的折线图 每一秒增加1个点&#xff0c;总共显示10s内的数据 显然x轴我们使用日期时间轴 同时使用1个定时器&#xff0c;每隔1秒往折线系列中添加1个数据进去 步骤如下&#xff1a; 1、创建图表视图 //1、创建图表视图 QChartV…

自然语言处理在客户服务中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 引言 自然语言处理概述 定义…

vs2022搭建opencv开发环境

1 下载OpenCV库 https://opencv.org/ 下载对应版本然后进行安装 将bin目录添加到系统环境变量opencv\build\x64\vc16\bin 复制该路径 打开高级设置添加环境变量 vs2022新建一个空项目 修改属性添加头文件路径和库路径 修改链接器&#xff0c;将OpenCV中lib库里的o…

【含文档】基于ssm+jsp的校园疫情管理系统(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: apache tomcat 主要技术: Java,Spring,SpringMvc,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定义了两个…

在Django中安装、配置、使用CKEditor5,并将CKEditor5录入的文章展现出来,实现一个简单博客网站的功能

在Django中可以使用CKEditor4和CKEditor5两个版本&#xff0c;分别对应软件包django-ckeditor和django-ckeditor-5。原来使用的是CKEditor4&#xff0c;python manager.py makemigrations时总是提示CKEditor4有安全风险&#xff0c;建议升级到CKEditor5。故卸载了CKEditor4&…

网络管理之---3种网络模式配置

目标&#xff1a; 了解几个概念&#xff1a; 1.什么是IP&#xff1f;什么是IP地址&#xff1f; 2.什么是桥接、NAT、仅主机模式 3.端口&#xff1f; 4.什么是网络接口命名规则 5.网络管理器 IP&#xff1a;指网络之间互联的协议&#xff0c;是TCP/IP 体系中的网络协议 I…

统信UOS开发环境支持Electron

全面支持Electron开发环境,同时还提供了丰富的开发工具和开发资源,进一步提升工作效率。 文章目录 一、环境部署1. Electron应用开发介绍2. Electron开发环境安装安装Node.js和npm安装electron环境配置二、代码示例Electron开发案例三、常见问题一、环境部署 1. Electron应用…

三级等保安全解决方案,实施方案,整改方案(Word,PPT等相关资料学习)

信息系统进行三级等保的主要原因在于保障信息安全&#xff0c;维护国家安全和公共利益。三级等保是我国根据相关法律法规制定的信息安全等级保护制度中的一部分&#xff0c;旨在确保信息系统的完整性、可用性和保密性。通过三级等保&#xff0c;信息系统可以得到一系列的安全保…

优选算法合集————双指针(专题一)

题目一&#xff1a;移动零 题目描述&#xff1a; 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输…

python基于深度学习的音乐推荐方法研究系统

需求设计 一款好的音乐推荐系统其目的是为用户进行合理的音乐推荐&#xff0c;普通的用户在登录到系统之后&#xff0c;能够通过搜索的方式获取与输入内容相关的音乐推荐&#xff0c;而以管理员登录到系统之后&#xff0c;则可以进行徐昂管的数据管理等内容操作。此次的需求主…

Docker 镜像和容器的导入导出及常用命令

Docker 镜像和容器的导入导出 1.1 镜像的导入导出 1.1.1 镜像的保存 通过镜像ID保存 方式一&#xff1a; docker save image_id > image-save.tar例如&#xff1a; rootUbuntu:/usr/local/docker/nginx# docker imagesREPOSITORY TAG IMAGE ID …

Java集合 List——针对实习面试

目录 Java集合 ListJava List的三种主要实现是什么&#xff1f;它们各自的特点是什么&#xff1f;Java List和Array&#xff08;数组&#xff09;的区别&#xff1f;Java List和Set有什么区别&#xff1f;ArrayList和Vector有什么区别&#xff1f;什么是LinkedList&#xff1f;…

超级干货O2OA数据中心-查询配置开发

O2OA提供的数据管理中心&#xff0c;可以让用户通过配置的形式完成对数据的汇总&#xff0c;统计和数据分组展现&#xff0c;查询和搜索数据形成列表数据展现。也支持用户配置独立的数据表来适应特殊的业务的数据存储需求。本文主要介绍如何在O2OA中开发和配置自定义数据查询语…

Unity中IK动画与布偶死亡动画切换的实现

在Unity游戏开发中&#xff0c;Inverse Kinematics&#xff08;IK&#xff09;是创建逼真角色动画的强大工具。同时&#xff0c;能够在适当的时候切换到布偶物理状态来实现死亡动画等效果&#xff0c;可以极大地增强游戏的视觉体验。本文将详细介绍如何在Unity中利用IK实现常规…

【ArcGISPro】单次将自己建立的工具箱添加至Arcpy中

新建工具箱 添加至Arcpy中 调用刚添加的工具箱

JVM的组成、字节码文件的组成

目录 java虚拟机的组成 字节码文件的组成 基础信息 常量池 字段 方法 属性 字节码相关的常用工具&#xff1a; 总结&#xff1a; 1、如何查看字节码文件&#xff1f; 2、字节码文件的核心组成有哪些&#xff1f; java虚拟机的组成 类加载器 ClassLoader运行时数据区…

新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)

常见的基础服务器配置之Centos命令 正常来说都是安装一个docker基本上很多问题都可以解决了&#xff0c;我基本上都是通过docker去管理一些容器如&#xff1a;mysql、redis、mongoDB等之类的镜像&#xff0c;还有一些中间件如kafka。下面就安装一个 docker 和 nginx 的相关配置…

Mysql COUNT() 函数详解

Mysql COUNT 函数详解 COUNT() 的几种用法COUNT(*)COUNT(1)COUNT(column)COUNT(*) 与 GROUP BYCOUNT(*) 与 GROUP BY 和 HAVING COUNT(expr) 的用法COUNT(DISTINCT expr)COUNT(expr) 带条件查询 写在最后 在使用Mysql的时候&#xff0c;作为开发者&#xff0c;聚合函数是肯定会…

yum下载时出现报错 Couldn‘t read a file:// file for file:///mnt/repodata/repomd.xml

得知说yum源指定的/mnt/没有镜像源 发现可能是镜像没有挂载成功 mount /dev/cdrom /mnt 清理一下缓存重新试一下 yum clean all yum install mod_ssl 解决