Netty源码分析 (六)----- 客户端连接接入accept过程
通读本文,你会了解到
1.netty如何接受新的请求
2.netty如何给新请求分配reactor线程
3.netty如何给每个新连接增加ChannelHandler
netty中的reactor线程
netty中最核心的东西莫过于两种类型的reactor线程,可以看作netty中两种类型的发动机,驱动着netty整个框架的运转
一种类型的reactor线程是boos线程组,专门用来接受新的连接,然后封装成channel对象扔给worker线程组;还有一种类型的reactor线程是worker线程组,专门用来处理连接的读写
不管是boos线程还是worker线程,所做的事情均分为以下三个步骤
- 轮询注册在selector上的IO事件
- 处理IO事件
- 执行异步task
对于boos线程来说,第一步轮询出来的基本都是 accept 事件,表示有新的连接,而worker线程轮询出来的基本都是read/write事件,表示网络的读写事件
新连接的建立
简单来说,新连接的建立可以分为三个步骤
1.检测到有新的连接
2.将新的连接注册到worker线程组
3.注册新连接的读事件
检测到有新连接进入
我们已经知道,当服务端绑启动之后,服务端的channel已经注册到boos reactor线程中,reactor不断检测有新的事件,直到检测出有accept事件发生
NioEventLoop.java
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
//检查该SelectionKey是否有效,如果无效,则关闭channel
if (!k.isValid()) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
} try {
int readyOps = k.readyOps();
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
// 如果准备好READ或ACCEPT则触发unsafe.read() ,检查是否为0,如上面的源码英文注释所说:解决JDK可能会产生死循环的一个bug。
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {//如果已经关闭,则直接返回即可,不需要再处理该channel的其他事件
// Connection already closed - no need to handle write.
return;
}
}
// 如果准备好了WRITE则将缓冲区中的数据发送出去,如果缓冲区中数据都发送完成,则清除之前关注的OP_WRITE标记
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// 如果是OP_CONNECT,则需要移除OP_CONNECT否则Selector.select(timeout)将立即返回不会有任何阻塞,这样可能会出现cpu 100%
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops); unsafe.finishConnect();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
该方法主要是对SelectionKey k进行了检查,有如下几种不同的情况
1)OP_ACCEPT,接受客户端连接
2)OP_READ, 可读事件, 即 Channel 中收到了新数据可供上层读取。
3)OP_WRITE, 可写事件, 即上层可以向 Channel 写入数据。
4)OP_CONNECT, 连接建立事件, 即 TCP 连接已经建立, Channel 处于 active 状态。
本篇博文主要来看下当boss线程 selector检测到OP_ACCEPT事件时,内部干了些什么。
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {//如果已经关闭,则直接返回即可,不需要再处理该channel的其他事件
// Connection already closed - no need to handle write.
return;
}
}
boos reactor线程已经轮询到 SelectionKey.OP_ACCEPT
事件,说明有新的连接进入,此时将调用channel的 unsafe
来进行实际的操作,此时的channel为 NioServerSocketChannel,则unsafe为NioServerSocketChannel的属性NioMessageUnsafe
那么,我们进入到它的read
方法,进入新连接处理的第二步
注册到reactor线程
NioMessageUnsafe.java
private final List<Object> readBuf = new ArrayList<Object>(); public void read() {
assert eventLoop().inEventLoop();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
do {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
} while (allocHandle.continueReading());
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
pipeline.fireChannelReadComplete();
}
调用 doReadMessages
方法不断地读取消息,用 readBuf
作为容器,这里,其实可以猜到读取的是一个个连接,然后调用 pipeline.fireChannelRead()
,将每条新连接经过一层服务端channel的洗礼,之后清理容器,触发 pipeline.fireChannelReadComplete()
下面我们具体看下这两个方法
1.doReadMessages(List)
2.pipeline.fireChannelRead(NioSocketChannel)
doReadMessages()
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = javaChannel().accept(); try {
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t); try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
} return 0;
}
我们终于窥探到netty调用jdk底层nio的边界 javaChannel().accept();
,由于netty中reactor线程第一步就扫描到有accept事件发生,因此,这里的accept
方法是立即返回的,返回jdk底层nio创建的一条channel
ServerSocketChannel有阻塞和非阻塞两种模式:
a、阻塞模式:ServerSocketChannel.accept() 方法监听新进来的连接,当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。阻塞模式下, accept()方法会一直阻塞到有新连接到达。
b、非阻塞模式:,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.
在NioServerSocketChannel的构造函数分析中,我们知道,其通过ch.configureBlocking(false);语句设置当前的ServerSocketChannel为非阻塞的。
netty将jdk的 SocketChannel
封装成自定义的 NioSocketChannel
,加入到list里面,这样外层就可以遍历该list,做后续处理
从上一篇文章中,我们已经知道服务端的创建过程中会创建netty中一系列的核心组件,包括pipeline,unsafe等等,那么,接受一条新连接的时候是否也会创建这一系列的组件呢?
带着这个疑问,我们跟进去
NioSocketChannel.java
public NioSocketChannel(Channel parent, SocketChannel socket) {
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
我们重点分析 super(parent, socket),
NioSocketChannel
的父类为 AbstractNioByteChannel
AbstractNioByteChannel.java
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
这里,我们看到jdk nio里面熟悉的影子—— SelectionKey.OP_READ
,一般在原生的jdk nio编程中,也会注册这样一个事件,表示对channel的读感兴趣
我们继续往上,追踪到AbstractNioByteChannel
的父类 AbstractNioChannel
, 这里,我相信读了上一篇文章你对于这部分代码肯定是有印象的
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
if (logger.isWarnEnabled()) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
在创建服务端channel的时候,最终也会进入到这个方法,super(parent)
, 便是在AbstractChannel
中创建一系列和该channel绑定的组件,如下
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
而这里的 readInterestOp
表示该channel关心的事件是 SelectionKey.OP_READ
,后续会将该事件注册到selector,之后设置该通道为非阻塞模式,在channel中创建 unsafe 和一条 pipeline
pipeline.fireChannelRead(NioSocketChannel)
对于 pipeline
我们前面已经了解过,在netty的各种类型的channel中,都会包含一个pipeline,字面意思是管道,我们可以理解为一条流水线工艺,流水线工艺有起点,有结束,中间还有各种各样的流水线关卡,一件物品,在流水线起点开始处理,经过各个流水线关卡的加工,最终到流水线结束
对应到netty里面,流水线的开始就是HeadContxt
,流水线的结束就是TailConext
,HeadContxt
中调用Unsafe
做具体的操作,TailConext
中用于向用户抛出pipeline中未处理异常以及对未处理消息的警告
通过前面的文章中,我们已经知道在服务端的channel初始化时,在pipeline中,已经自动添加了一个pipeline处理器 ServerBootstrapAcceptor
, 并已经将用户代码中设置的一系列的参数传入了构造函数,接下来,我们就来看下ServerBootstrapAcceptor
ServerBootstrapAcceptor.java
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs; ServerBootstrapAcceptor(
EventLoopGroup childGroup, ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;
} public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg; child.pipeline().addLast(childHandler); for (Entry<ChannelOption<?>, Object> e: childOptions) {
try {
if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + child, t);
}
} for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
} try {
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
}
前面的 pipeline.fireChannelRead(NioSocketChannel);
最终通过head->unsafe->ServerBootstrapAcceptor的调用链,调用到这里的 ServerBootstrapAcceptor
的channelRead
方法,而 channelRead
一上来就把这里的msg强制转换为 Channel
然后,拿到该channel,也就是我们之前new出来的 NioSocketChannel中
对应的pipeline,将用户代码中的 childHandler
,添加到pipeline,这里的 childHandler
在用户代码中的体现为
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
其实对应的是 ChannelInitializer
,到了这里,NioSocketChannel
中pipeline对应的处理器为 head->ChannelInitializer->tail,牢记,后面会再次提到!
接着,设置 NioSocketChannel
对应的 attr和option,然后进入到 childGroup.register(child)
,这里的childGroup就是我们在启动代码中new出来的NioEventLoopGroup
我们进入到NioEventLoopGroup
的register
方法,代理到其父类MultithreadEventLoopGroup
MultithreadEventLoopGroup.java
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
这里又扯出来一个 next()方法,我们跟进去
MultithreadEventLoopGroup.java
@Override
public EventLoop next() {
return (EventLoop) super.next();
}
回到其父类
MultithreadEventExecutorGroup.java
@Override
public EventExecutor next() {
return chooser.next();
}
这里的chooser对应的类为 EventExecutorChooser
,字面意思为事件执行器选择器,放到我们这里的上下文中的作用就是从worker reactor线程组中选择一个reactor线程
public interface EventExecutorChooserFactory { /**
* Returns a new {@link EventExecutorChooser}.
*/
EventExecutorChooser newChooser(EventExecutor[] executors); /**
* Chooses the next {@link EventExecutor} to use.
*/
@UnstableApi
interface EventExecutorChooser { /**
* Returns the new {@link EventExecutor} to use.
*/
EventExecutor next();
}
}
chooser的实现有两种
public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory { public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory(); private DefaultEventExecutorChooserFactory() { } @SuppressWarnings("unchecked")
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
return new PowerOfTowEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
} private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
} private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors; PowerOfTowEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
} @Override
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
} private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors; GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
} @Override
public EventExecutor next() {
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
}
默认情况下,chooser通过 DefaultEventExecutorChooserFactory
被创建,在创建reactor线程选择器的时候,会判断reactor线程的个数,如果是2的幂,就创建PowerOfTowEventExecutorChooser
,否则,创建GenericEventExecutorChooser
两种类型的选择器在选择reactor线程的时候,都是通过Round-Robin的方式选择reactor线程,唯一不同的是,PowerOfTowEventExecutorChooser
是通过与运算,而GenericEventExecutorChooser
是通过取余运算,与运算的效率要高于求余运算
选择完一个reactor线程,即 NioEventLoop
之后,我们回到注册的地方
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
SingleThreadEventLoop.java
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
其实,这里已经和服务端启动的过程一样了,可以参考我前面的文章
AbstractNioChannel.java
private void register0(ChannelPromise promise) {
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true; pipeline.invokeHandlerAddedIfNeeded(); safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
}
和服务端启动过程一样,先是调用 doRegister();
做真正的注册过程,如下
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
eventLoop().selectNow();
selected = true;
} else {
throw e;
}
}
}
}
将该条channel绑定到一个selector
上去,一个selector被一个reactor线程使用,后续该channel的事件轮询,以及事件处理,异步task执行都是由此reactor线程来负责
绑定完reactor线程之后,调用 pipeline.invokeHandlerAddedIfNeeded()
前面我们说到,到目前为止NioSocketChannel
的pipeline中有三个处理器,head->ChannelInitializer->tail,最终会调用到 ChannelInitializer
的 handlerAdded
方法
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isRegistered()) {
initChannel(ctx);
}
}
handlerAdded
方法调用 initChannel
方法之后,调用remove(ctx);
将自身删除,如下
AbstractNioChannel.java
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) {
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
exceptionCaught(ctx, cause);
} finally {
remove(ctx);
}
return true;
}
return false;
}
而这里的 initChannel
方法又是神马玩意?让我们回到用户方法,比如下面这段用户代码
用户代码
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
原来最终跑到我们自己的代码里去了啊!完了之后,NioSocketChannel
绑定的pipeline的处理器就包括 head->LoggingHandler->EchoServerHandler->tail
注册读事件
接下来,我们还剩下这些代码没有分析完
AbstractNioChannel.java
private void register0(ChannelPromise promise) {
// ..
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
}
pipeline.fireChannelRegistered();
,其实没有干啥有意义的事情,最终无非是再调用一下业务pipeline中每个处理器的 ChannelHandlerAdded
方法处理下回调
isActive()
在连接已经建立的情况下返回true,所以进入方法块,进入到 pipeline.fireChannelActive();
在这里我详细步骤先省略,直接进入到关键环节
AbstractNioChannel.java
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
} readPending = true; final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
这里其实就是将 SelectionKey.OP_READ
事件注册到selector中去,表示这条通道已经可以开始处理read事件了
总结
至此,netty中关于新连接的处理已经向你展示完了,我们做下总结
1.boos reactor线程轮询到有新的连接进入
2.通过封装jdk底层的channel创建 NioSocketChannel
以及一系列的netty核心组件
3.将该条连接通过chooser,选择一条worker reactor线程绑定上去
4.注册读事件,开始新连接的读写
Netty源码分析 (六)----- 客户端连接接入accept过程的更多相关文章
- 【Netty源码分析】客户端connect服务端过程
上一篇博客[Netty源码分析]Netty服务端bind端口过程 我们介绍了服务端绑定端口的过程,这一篇博客我们介绍一下客户端连接服务端的过程. ChannelFuture future = boos ...
- 【Netty源码分析】发送数据过程
前面两篇博客[Netty源码分析]Netty服务端bind端口过程和[Netty源码分析]客户端connect服务端过程中我们分别介绍了服务端绑定端口和客户端连接到服务端的过程,接下来我们分析一下数据 ...
- Netty源码分析-- 处理客户端接入请求(八)
这一节我们来一起看下,一个客户端接入进来是什么情况.首先我们根据之前的分析,先启动服务端,然后打一个断点. 这个断点打在哪里呢?就是NioEventLoop上的select方法上. 然后我们启动一个客 ...
- Netty源码分析第3章(客户端接入流程)---->第2节: 处理接入事件之handle的创建
Netty源码分析第三章: 客户端接入流程 第二节: 处理接入事件之handle的创建 上一小节我们剖析完成了与channel绑定的ChannelConfig初始化相关的流程, 这一小节继续剖析客户端 ...
- Netty源码分析第3章(客户端接入流程)---->第3节: NioSocketChannel的创建
Netty源码分析第三章: 客户端接入流程 第三节: NioSocketChannel的创建 回到上一小节的read()方法: public void read() { //必须是NioEventLo ...
- Netty源码分析第3章(客户端接入流程)---->第4节: NioSocketChannel注册到selector
Netty源码分析第三章: 客户端接入流程 第四节: NioSocketChannel注册到selector 我们回到最初的NioMessageUnsafe的read()方法: public void ...
- Netty源码分析第3章(客户端接入流程)---->第1节: 初始化NioSockectChannelConfig
Netty源码分析第三章: 客户端接入流程 概述: 之前的章节学习了server启动以及eventLoop相关的逻辑, eventLoop轮询到客户端接入事件之后是如何处理的?这一章我们循序渐进, 带 ...
- Netty源码分析第3章(客户端接入流程)---->第5节: 监听读事件
Netty源码分析第三章: 客户端接入流程 第五节: 监听读事件 我们回到AbstractUnsafe的register0()方法: private void register0(ChannelPro ...
- Netty源码分析第4章(pipeline)---->第7节: 前章节内容回顾
Netty源码分析第四章: pipeline 第七节: 前章节内容回顾 我们在第一章和第三章中, 遗留了很多有关事件传输的相关逻辑, 这里带大家一一回顾 首先看两个问题: 1.在客户端接入的时候, N ...
随机推荐
- Cobbler 自动安装CentOS7
1. Cobbler介绍 Cobbler是一个Linux服务器安装的服务,可以通过网络启动(PXE)的方式来快速安装.重装物理服务器和虚拟机,同时还可以管理DHCP,DNS等.Cobbler可以使用命 ...
- idea 新建不了servlet文件 方法(1)
在pem.xml中添加较新版本的servletapi包 <dependency> <groupId>javax.servlet</groupId> <arti ...
- Appium+python自动化(二十九)- 模拟手指在手机上多线多点作战 - 多点触控(超详解)
简介 在网页中我们经常使用缩放操作来便利的查看具体的信息,在appium中使用MultiAction多点触控的类来实现.MultiAction是多点触控的类,可以模拟用户多点操作.主要包含加载add( ...
- 小X的逆袭
[问题描述]毕业于普通本科的小x 一直自称是资深屌丝.谁又能想到,如此不起眼的小x 在历经重重面试环节后,竟然如愿以偿加入了心仪已久的腾讯公司!正所谓野百合也有春天,屌丝也有逆袭的那一天!一段时间以后 ...
- Python 四大主流 Web 编程框架
Python 四大主流 Web 编程框架 目前Python的网络编程框架已经多达几十个,逐个学习它们显然不现实.但这些框架在系统架构和运行环境中有很多共通之处,本文带领读者学习基于Python网络框架 ...
- JavaWeb——Servlet开发2
1.HttpServletRequest的使用 获取Request的参数的方法. 方法getParameter将返回参数的单个值 方法getParameterValues将返回参数的值的数组 方法ge ...
- Knative 基本功能深入剖析:Knative Serving 之服务路由管理
导读:本文主要围绕 Knative Service 域名展开,介绍了 Knative Service 的路由管理.文章首先介绍了如何修改默认主域名,紧接着深入一层介绍了如何添加自定义域名以及如何根据 ...
- AutoCAD二次开发(.Net)之创建图层Layer
//https://blog.csdn.net/qq_21489689?t=1[CommandMethod("CREATELY")] public void CreateLayer ...
- mysql数据库的水平拆分与垂直拆分
近端时间在面试,发现很多面试官或者面试都把数据的水平拆分合垂直拆分给搞混了,今天特意写了一篇博客来说说水平拆分和垂直拆分希望对程序猿们有所帮助. 数据库水平与垂直拆分: 垂直(纵向)拆分:是指按功能模 ...
- Jmeter发送post请求报错Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
常识普及: Content-type,在Request Headers里面,告诉服务器,我们发送的请求信息格式,在JMeter中,信息头存储在信息头管理器中,所以在做接口测试的时候,我们维护Conte ...