Netty源码解读(二)-服务端源码讲解
简单Echo案例
注释版代码地址:netty
代码是netty的源码,我添加了自己理解的中文注释。
了解了Netty的线程模型和组件之后,我们先看看如何写一个简单的Echo案例,后续的源码讲解都基于此案例。以下是服务端的代码:
public final class MyEchoServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final MyEchoServerHandler serverHandler = new MyEchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
// 说明服务器端通道的实现类(便于 Netty 做反射处理)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
// 对服务端的 NioServerSocketChannel 添加 Handler
// LoggingHandler 是 netty 内置的一种 ChannelDuplexHandler,
// 既可以处理出站事件,又可以处理入站事件,即 LoggingHandler
// 既记录出站日志又记录入站日志。
.handler(new LoggingHandler(LogLevel.INFO))
// 对服务端接收到的、与客户端之间建立的 SocketChannel 添加 Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(serverHandler);
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
EventLoopGroup的创建与初始化
对应代码
EventLoopGroup bossGroup = new NioEventLoopGroup();
默认线程数
跟踪NioEventLoopGroup
的无参构造
NioEventLoopGroup()
-->
NioEventLoopGroup(int nThreads)
-->
NioEventLoopGroup(int nThreads, Executor executor)
-->
NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider)
-->
NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory)
-->
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
这里能看到,如果构造传入的线程数为0,则使用DEFAULT_EVENT_LOOP_THREADS
值为系统变量io.netty.eventLoopThreads
,没有环境变量就取cpu逻辑线程数*2
例如我的电脑为8核16线程,nThreads = 16 * 2
EventLoop的创建
继续跟踪代码,以下代码有部分省略
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 检查线程数量不能小于1
checkPositive(nThreads, "nThreads");
// 这里的 ThreadPerTaskExecutor 实例是下文用于创建 EventExecutor 实例的参数
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建EventLoop(重点)
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
。。。。。。
}
}
// chooser 的作用是为了实现 next()方法,即从 EventLoopGroup 中挑选
// 一个 NioEventLoop 来处理连接上 IO 事件的方法
chooser = chooserFactory.newChooser(children);
。。。。。。
}
ThreadPerTaskExecutor很简单,实现了Executor接口
public final class ThreadPerTaskExecutor implements Executor {
。。。。。。
@Override
public void execute(Runnable command) {
threadFactory.newThread(command).start();
}
}
这意味着每次执行executor.execute方法,都会开启一个线程。
EventLoop的创建是在newChild中
// 类NioEventLoopGroup
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
// selector工厂
SelectorProvider selectorProvider = (SelectorProvider) args[0];
// 选择策略工厂
SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
// 拒绝执行处理器(任务添加到队列中失败时调用)
RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
EventLoopTaskQueueFactory taskQueueFactory = null;
EventLoopTaskQueueFactory tailTaskQueueFactory = null;
int argsLength = args.length;
if (argsLength > 3) {
taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
}
if (argsLength > 4) {
tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
}
// 创建NioEventLoop并返回
return new NioEventLoop(this, executor, selectorProvider,
selectStrategyFactory.newSelectStrategy(),
rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
}
小结
NioEventLoopGroup的创建,初始化了selector工厂,选择策略,拒绝执行处理器等。
并创建了同样线程数的NioEventLoop
服务端引导类ServerBootstrap的创建与设置
对应代码
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)......
group的设置
public ServerBootstrap group(EventLoopGroup group) {
return group(group, group);
}
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
return this;
}
提供了两个设置EventLoopGroup的方法,也就是parentGroup和childGroup可以是同一个group,
而parentGroup对应线程图中的bossGroup,childGroup对应线程图中的workerGroup
channel的设置
public B channel(Class<? extends C> channelClass) {
return channelFactory(new <C>(
ObjectUtil.checkNotNull(channelClass, "channelClass")
));
}
这里设置的是channel反射工厂,该工厂会使用反射生成NioServerSocketChannel
对象。
创建并绑定channel
对应代码
ChannelFuture f = b.bind(PORT).sync();
channel的创建
准确点说,是负责创建连接(ACCEPT)的channel的创建
AbstractBootstrap#bind(int inetPort) -->
AbstractBootstrap#bind(SocketAddress localAddress) -->
private ChannelFuture doBind(final SocketAddress localAddress) {
// 初始化 NioServerSocketChannel 的实例,并且将其注册到
// bossGroup 中的 EvenLoop 中的 Selector 中,initAndRegister()
// 实例的初始化和注册(此方法是异步的):
// (1) 初始化:将handler注册进通道,并执行handler的handlerAdded、channelRegistered方法
// (2) 将channel注册进selector
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
// 若异步过程 initAndRegister()已经执行完毕,则进入该分支
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
// 若异步过程 initAndRegister()还未执行完毕,则进入该分支
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
// 监听 regFuture 的完成事件,完成之后再调用
// doBind0(regFuture, channel, localAddress, promise);
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
initAndRegister方法中主要做了3个操作,channel的创建、初始化以及将channel注册到EventLoop中
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 无参构造会创建pipelile
// NioServerSocketChannel
channel = channelFactory.newChannel();
// 初始化相关属性
// 如果是ServerBoottrap,还会设置bossGroup的handler,
// 其中包括ServerBootstrap.handler设置的handler,以及最后添加ServerBootstrapAcceptor
// ServerBootstrapAcceptor就是将channel注册到workerGroup的类
init(channel);
} catch (Throwable t) {
。。。。。。
}
// 将channel注册进selector(监听ACCEPT事件)
// 依然是通过开启eventLoop线程的方式进行注册
// MultithreadEventLoopGroup
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
创建
请查看前面channel的设置一节,使用ReflectiveChannelFactory反射调用
NioServerSocketChannel
的无参构造器,创建channelNioServerSocketChannel() -->
NioServerSocketChannel(SelectorProvider provider) -->
public NioServerSocketChannel(SelectorProvider provider, InternetProtocolFamily family) {
// newChannel(provider, family)生成Java NIO中的ServerSocketChannel
this(newChannel(provider, family));
}
-->
public NioServerSocketChannel(ServerSocketChannel channel) {
// SelectionKey.OP_ACCEPT表示当前channel监听的是accept事件
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
-->
AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp)
-->
AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)
-->
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
// 创建管道,同时创建头尾的handlerContext
pipeline = newChannelPipeline();
}
我们看看管道的创建做了啥
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true); // 创建尾部HandlerContext
tail = new TailContext(this);
// 创建头部HandlerContext
head = new HeadContext(this); // 初始化链条关系
head.next = tail;
tail.prev = head;
}
Pipeline的addLast实际上是插入,而不是在尾部添加。会将对应的handler封装成HandlerContext,插入到TailContext之前
图片来源:netty源码分析之pipeline(一) - 简书 (jianshu.com)
而在此案例中,pipeline长这样
初始化
初始化调用的是init方法
// ServerBootstrap类
void init(Channel channel) {
// 初始化相关属性
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, newAttributesArray()); ChannelPipeline p = channel.pipeline(); final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs); p.addLast(new ChannelInitializer<Channel>() {
// 这里会在channel被注册进selector后执行
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
// ServerBootstrap.handler(new LoggingHandler(LogLevel.INFO))
// 就是在这里被设置进管道
if (handler != null) {
pipeline.addLast(handler);
} ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// 负责监听READ的channel就是在这个handler中注册的
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
初始化方法就是将
ServerBootstrap
一开始设置的相关属性初始化,以及往管道中添加handler,ServerBootstrapAcceptor
这个handler是重点,我们在下面讲。注册
channel已经创建和初始化了,接下来就是将channel注册到EventLoop中
// ChannelFuture regFuture = config().group().register(channel);
// 类MultithreadEventLoopGroup
public ChannelFuture register(Channel channel) {
// next():从EventLoopGroup中选择一个EventLoop
return next().register(channel);
}
-->
SingleThreadEventLoop#register(Channel channel)
-->
SingleThreadEventLoop#register(final ChannelPromise promise)
-->
// 类AbstractChannel
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
。。。。。。 AbstractChannel.this.eventLoop = eventLoop; // 判断如果当前线程是EventLoop的线程,则直接执行
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
// 否则添加进EventLoop的任务队列,由EventLoop的线程去执行
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
。。。。。。
}
}
}
register0方法就是注册方法了,至于为什么会有eventLoop.execute,这个方法很有意思,等会讲。
先看看register0
private void register0(ChannelPromise promise) {
try {
。。。。。。
boolean firstRegistration = neverRegistered;
// 将当前channel注册到selector中
doRegister();
neverRegistered = false;
registered = true; // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
// 触发channel的handlerAdded方法
// 如果是ChannelInitializer的话,会在handlerAdded中触发initChannel方法
pipeline.invokeHandlerAddedIfNeeded(); safeSetSuccess(promise);
// 触发channel的channelRegistered方法
pipeline.fireChannelRegistered();
// Only fire a channelActive if the channel has never been registered. This prevents firing
// multiple channel actives if the channel is deregistered and re-registered.
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
beginRead();
}
}
} catch (Throwable t) {
。。。。。。
}
}
doRegister():将当前channel注册到selector中,里面会调用到Java NIO的一些API
pipeline.invokeHandlerAddedIfNeeded():触发channel的handlerAdded方法
pipeline.fireChannelRegistered():触发channel的channelRegistered方法从这里可以看到,全部handler中的handlerAdded执行完,才会执行channelRegistered方法
绑定端口
重新回到AbstractBootstrap#doBind方法中
// 如果上面的initAndRegister方法执行完毕(异步执行的),则执行doBind0
if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
// 若异步过程 initAndRegister()已经执行完毕,则进入该分支
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
// 若异步过程 initAndRegister()还未执行完毕,则进入该分支
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
// 监听 regFuture 的完成事件,完成之后再调用
// doBind0(regFuture, channel, localAddress, promise);
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
上面这段if/esle做了同一件是,就是自行doBind0方法,区别在于如果initAndRegister执行完毕,则执行调用doBind0,否则添加监听器,等执行完成触发调用doBind0
继续看doBind0
// 类AbstractBootstrap
private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
// execute方法会将这个Runnable加入到taskQueue中,并开线程执行EventLoop的run方法(死循环)
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}
channel.eventLoop().execute这个后面再说,可以看到,里面的逻辑是调用channel.bind在实现绑定的,继续跟踪
AbstractChannel#bind(SocketAddress localAddress, ChannelPromise promise)
-->
// 类AbstractChannel
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
// tail就是TailContext
return tail.bind(localAddress, promise);
}
-->
// 类AbstractChannel
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
ObjectUtil.checkNotNull(localAddress, "localAddress");
if (isNotValidPromise(promise, false)) {
// cancelled
return promise;
}
// 在管道中从当前handlerContext往前查找实现了bind方法的handlerContext
final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
// 执行handlerContext的bind方法
next.invokeBind(localAddress, promise);
} else {
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeBind(localAddress, promise);
}
}, promise, null, false);
}
return promise;
}
-->
// 类AbstractChannel
private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) {
try {
((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
} else {
bind(localAddress, promise);
}
}
从上面可以看到,最终会执行handler的bind方法,拿LoggingHandler
的bind方法举例
// 类LoggingHandler
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
if (logger.isEnabled(internalLevel)) {
logger.log(internalLevel, format(ctx, "BIND", localAddress));
}
ctx.bind(localAddress, promise);
}
ctx.bind(localAddress, promise)是不是很眼熟,没错,就是AbstractChannel#bind(final SocketAddress localAddress, final ChannelPromise promise)
就像一个循环,每一次都在当前handlerContext往前找有实现了bind方法的handlerContext,执行bind,然后继续往前找。
最终找到管道中的第一个handler,也就是HeadContext
,看看它实现的bind方法
// 类HeadContext
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
unsafe.bind(localAddress, promise);
}
-->
AbstractChannel#bind(final SocketAddress localAddress, final ChannelPromise promise)
-->
NioServerSocketChannel#doBind(SocketAddress localAddress)
最后,还是Java NIO的API来绑定
参考资料:
《Netty in Action》,Norman Maurer
《Scalable IO in Java》,Doug Lea
45 张图深度解析 Netty 架构与原理 (qq.com)
Netty源码解读(二)-服务端源码讲解的更多相关文章
- muduo库源码剖析(二) 服务端
一. TcpServer类: 管理所有的TCP客户连接,TcpServer供用户直接使用,生命期由用户直接控制.用户只需设置好相应的回调函数(如消息处理messageCallback)然后TcpSer ...
- Netty 4源码解析:服务端启动
Netty 4源码解析:服务端启动 1.基础知识 1.1 Netty 4示例 因为Netty 5还处于测试版,所以选择了目前比较稳定的Netty 4作为学习对象.而且5.0的变化也不像4.0这么大,好 ...
- Zookeeper 源码(四)Zookeeper 服务端源码
Zookeeper 源码(四)Zookeeper 服务端源码 Zookeeper 服务端的启动入口为 QuorumPeerMain public static void main(String[] a ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- Spring Cloud系列(三):Eureka源码解析之服务端
一.自动装配 1.根据自动装配原理(详见:Spring Boot系列(二):Spring Boot自动装配原理解析),找到spring-cloud-starter-netflix-eureka-ser ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- vs2008编译FileZilla服务端源码
vs2008编译FileZilla服务端源码 FileZilla服务端下载地址:https://download.filezilla-project.org/server/.FileZilla服务端源 ...
- Netty入门之客户端与服务端通信(二)
Netty入门之客户端与服务端通信(二) 一.简介 在上一篇博文中笔者写了关于Netty入门级的Hello World程序.书接上回,本博文是关于客户端与服务端的通信,感觉也没什么好说的了,直接上代码 ...
- Photon Server 实现注册与登录(二) --- 服务端代码整理
一.有的代码前端和后端都会用到.比如一些请求的Code.使用需要新建项目存放公共代码. 新建项目Common存放公共代码: EventCode :存放服务端自动发送信息给客户端的code Operat ...
- 小D课堂 - 新版本微服务springcloud+Docker教程_4-06 Feign核心源码解读和服务调用方式ribbon和Feign选择
笔记 6.Feign核心源码解读和服务调用方式ribbon和Feign选择 简介: 讲解Feign核心源码解读和 服务间的调用方式ribbon.feign选择 ...
随机推荐
- java.sql和javax.sql的区别
根据 JDBC 规范,javax.sql 包中的类和接口首先作为 JDBC 2.0 可选包提供.此可选程序包以前与 J2SE1.2 中的 java.sql 程序包是分开的.从 J2SE1.4 开始,这 ...
- 微服务生态组件之Spring Cloud OpenFeign详解和源码分析
Spring Cloud OpenFeign 概述 Spring Cloud OpenFeign 官网地址 https://spring.io/projects/spring-cloud-openfe ...
- 解析Java-throw抛出异常详细过程
摘要:Java有3种抛出异常的形式:throw.throws.系统自动抛异常. 本文分享自华为云社区<Java-throw异常详解以及过程>,作者: gentle_zhou . 首先,我们 ...
- Asp.Net Core 7 preview 4 重磅新特性--限流中间件
前言 限流是应对流量暴增或某些用户恶意攻击等场景的重要手段之一,然而微软官方从未支持这一重要特性,AspNetCoreRateLimit这一第三方库限流库一般作为首选使用,然而其配置参数过于繁多,对使 ...
- MAC系统下破解WIFI密码(亲测可用,含wifi密码字典)
出差第二天,住的小区因为疫情被封,宿舍又没有wifi,看着附近满满的WIFI信号列表,wifi万能钥匙却一个都连接不上,心中一万匹CNM...于是电脑连上手机热点,然后各种折腾,终于破解了一个隔壁的w ...
- 206. Reverse Linked List - LeetCode
Question 206. Reverse Linked List Solution 题目大意:对一个链表进行反转 思路: Java实现: public ListNode reverseList(Li ...
- 103_Power Pivot 透视表中空白标签处理及百分比
焦棚子的文章目录 请点击下载附件 1.案例来源于不断变化的需求 事实表:销售表 维度表:城市表 销售表和城市建立多对一的关系 如图1: 图1 2.插入透视表 如图2: 图2 3.问题 1.销售表中,城 ...
- 数仓选型必列入考虑的OLAP列式数据库ClickHouse(中)
实战 案例使用 背景 ELK作为老一代日志分析技术栈非常成熟,可以说是最为流行的大数据日志和搜索解决方案:主要设计组件及架构如下: 而新一代日志监控选型如ClickHouse.StarRocks特别是 ...
- 什么是请求参数、表单参数、url参数、header参数、Cookie参数?一文讲懂
最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下. 回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了. 先分享一个小故 ...
- form表单与css选择器
目录 form表单 action属性 input标签 lable标签 select标签 textarea标签 补充 网络请求方式 CSS简介 CSS基本选择器 组合选择器 属性选择器 分组与嵌套 伪类 ...