Netty的启动过程

Bootstrap

Bootstrap是Netty中负责引导服务端和客户端启动的,它将ChannelPipeline、ChannelHandler和EventLoop组织起来,让它成为一个可以实际运行的程序,引导一个应用程序,简单来说,是先对它进行配置,然后让它运行起来的过程

Netty有两个引导启动的类:

1:Bootstrap引导客户端运行

2:ServerBootstrap引导服务端运行

下面根据源码看下启动过程是怎样的:

服务端的启动

首先看下服务端的代码示例:

static final int PORT = Integer.parseInt(System.getProperty("port", "8099"));

public static void main(String[] args) {
//1:创建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler handler = new EchoServerHandler();
try {
//2:创建启动器
ServerBootstrap bootstrap = new ServerBootstrap();
//3:配置启动器
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast(handler);
}
});
//4:启动启动器
ChannelFuture f = bootstrap.bind(PORT).sync();
//5:等待服务端channel关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//6:释放资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

写一个Netty服务端,整体来说就是上面六个步骤,前面对EventLoop、ChannelPipeline、ByteBuf相关知识进行了介绍,有不清楚的可以去之前的文章查看,下面根据源码看下Netty服务端的启动过程:

服务端通过bind方法进行启动,整个启动过程做了很多事情,下面会逐一介绍

ChannelFuture f = bootstrap.bind(PORT).sync();

首先进入的是AbstractBootstrap类下面的bind方法:

 public ChannelFuture bind(int inetPort) {
return bind(new InetSocketAddress(inetPort));
} public ChannelFuture bind(SocketAddress localAddress) {
validate();
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
return doBind(localAddress);
}

然后重点看下doBind方法:

//将ServerSocketChannel注册到selector并绑定端口,启动服务
private ChannelFuture doBind(final SocketAddress localAddress) {
//初始化和注册服务端channel
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
} if (regFuture.isDone()) {
//register注册成功后,调用doBind0
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
//register没有注册成功,listener监听,然后回调里调用doBind0
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
//register注册成功后,调用doBind0
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

下面先看下服务端channel的初始化和注册操作:

final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//通过channelFactory创建一个channel
channel = channelFactory.newChannel();
//初始化channel
init(channel);
} catch (Throwable t) {
//...
}
//这里的group就是bootstrap.group()方法传入的parentGroup
//将channel注册到selector上,注册的具体解析可以看之前的文章
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}

ReflectiveChannelFactory类下的newChannel方法:

通过反射的方式创建,即是上面代码示例中的channel(NioServerSocketChannel.class)里的NioServerSocketChannel

 @Override
public T newChannel() {
try {
return constructor.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
}
}

由于这里介绍的是服务端的启动,所以init()方法会进入ServerBootstrap里面对应的方法:

 @Override
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
} final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
//创建channel的时候,会创建一个ChannelPipeline
//具体的ChannelPipeline创建过程,请查看之前的文章
ChannelPipeline p = channel.pipeline();
//获取childGroup和childHandler,传给ServerBootstrapAcceptor
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
} p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
} ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
//添加一个Acceptor处理器
//也就是把客户端的连接分配给一个EventLoop线程
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

上面源码部分很长,主要关注的点是加了注释的地方,然后看下ServerBootstrapAcceptor:

 @Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
//把childHandler添加到客户端的pipeline,每个客户端都有相同的Handler
child.pipeline().addLast(childHandler); setChannelOptions(child, childOptions, logger); 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);
}
}

上面完成的服务端channel的初始化和注册,下面就是绑定端口:

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.
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());
}
}
});
}

下面会进入AbstractChannel类下的bind方法:

@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return pipeline.bind(localAddress, promise);
}

然后进入DefaultChannelPipeline下的bind方法:

@Override
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return tail.bind(localAddress, promise);
}

之前在说pipeline的时候说过bind是出站事件,这里根据源码就可以清楚的看到了

追踪代码路径:



总结

1:初始化和注册channel,其中还初始化了pipeline

2:绑定端口

看下ServerBootstrap API的一些实现:

名称 描述
group 设置ServerBootstrap要用的EventLoopGroup。这个EventLoopGroup将用于ServerChannel和被接受的子Channel的I/O处理
channel 设置将要被实例化的ServerChannel类
channelFactory 如果不能通过默认的构造函数创建Channel,那么可以提供一个ChannelFactory
localAddress 指定ServerChannel应该绑定到本地地址。如果没有指定,则将有操作系统使用一个随机地址。或者,可以通过bind方法来指定该localAddress
option 指定要应用到创建的ServerChannel的配置项,只能在调用bind方法前设置。具体支持的配 置项取决于所使用的Channel类型。参见ChannelConfig的API文档
childOption 指定客户端的Channel被接受时,应用到客户端Channel的配置项
attr 指定ServerChannel上的属性,只能在调用bind方法前设置
childAttr 指定接收的客户端Channel上的属性
handler 设置ServerChannel的事件处理器
childHandler 设置客户端Channel上的事件处理器
clone 克隆一个设置和原始ServerBootstrap相同的ServerBootstrap
bind 绑定ServerChannel并返回一个

客户端的启动

同样的,看下客户端Netty示例,和服务端区别不大:

public static void main(String[] args) {
//1:创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
//2:创建启动器
Bootstrap bootstrap = new Bootstrap();
//3:配置启动器
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast(new EchoClientHandler());
}
});
//4:启动启动器
ChannelFuture f = bootstrap.connect(HOST, PORT).sync();
//:5:等待客户端channel关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//6:释放资源
group.shutdownGracefully();
}
}

启动客户端是通过connect来操作的:

ChannelFuture f = bootstrap.connect(HOST, PORT).sync();

首先进入的是Bootstrap类中的connect方法:

//连接服务端,参数服务端IP和端口
public ChannelFuture connect(String inetHost, int inetPort) {
return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
} public ChannelFuture connect(SocketAddress remoteAddress) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
} validate();
return doResolveAndConnect(remoteAddress, config.localAddress());
} private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
//初始化和注册客户端channel
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel(); if (regFuture.isDone()) {
if (!regFuture.isSuccess()) {
return regFuture;
}
//注册成功后解析地址和连接服务端
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) { } else {
promise.registered();
//解析地址和连接服务端
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
}
}
});
return promise;
}
}

其中初始化和注册与服务端是差不多的,这里不做赘述,下面看下doResolveAndConnect0方法:

 private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
final SocketAddress localAddress, final ChannelPromise promise) {
try {
final EventLoop eventLoop = channel.eventLoop();
//服务端地址解析,因为服务端地址可能是域名而不是IP
final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop); if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
//解析成功后连接服务端
doConnect(remoteAddress, localAddress, promise);
return promise;
} final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress); if (resolveFuture.isDone()) {
final Throwable resolveFailureCause = resolveFuture.cause(); if (resolveFailureCause != null) {
// Failed to resolve immediately
channel.close();
promise.setFailure(resolveFailureCause);
} else {
// Succeeded to resolve immediately; cached? (or did a blocking lookup)
doConnect(resolveFuture.getNow(), localAddress, promise);
}
return promise;
} // Wait until the name resolution is finished.
resolveFuture.addListener(new FutureListener<SocketAddress>() {
@Override
public void operationComplete(Future<SocketAddress> future) throws Exception {
if (future.cause() != null) {
channel.close();
promise.setFailure(future.cause());
} else {
doConnect(future.getNow(), localAddress, promise);
}
}
});
} catch (Throwable cause) {
promise.tryFailure(cause);
}
return promise;
}

然后是doConnect方法:

private static void doConnect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
final Channel channel = connectPromise.channel();
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (localAddress == null) {
//调用connect方法
channel.connect(remoteAddress, connectPromise);
} else {
channel.connect(remoteAddress, localAddress, connectPromise);
}
connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
});
}

追踪代码路径:

Bootstrap API:

TCP粘包、拆包

TCP底层并不了解上层应用的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上会认为,一个完整的包可能被TCP拆分成多个包进行发送,也可能把多个小的包封装成一个大的包进行发送。这就是TCP的粘包和拆包

图示

1:粘包



上述图示,假设上述每两行的数据包大小为1024字节,那么TCP在进行发送的时候,就会出现图中的粘包问题

2:拆包



上述图示,数据传输的大小大于1024字节了,就会进行拆包

简单的例子

服务端:

public static void main(String[] args) throws Exception {
// 1、 线程定义
// accept 处理连接的线程池
EventLoopGroup acceptGroup = new NioEventLoopGroup();
// read io 处理数据的线程池
EventLoopGroup readGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(acceptGroup, readGroup);
// 2、 选择TCP协议,NIO的实现方式
b.channel(NioServerSocketChannel.class);
b.handler(new LoggingHandler(LogLevel.INFO));
b.childHandler(new ChannelInitializer<SocketChannel>() { @Override
protected void initChannel(SocketChannel ch) throws Exception {
// 3、 职责链定义(请求收到后怎么处理)
ChannelPipeline pipeline = ch.pipeline();
// // TODO 3.1 增加解码器
// pipeline.addLast(new XDecoder());
// TODO 3.2 打印出内容 handdler
pipeline.addLast(new XHandller());
}
});
// 4、 绑定端口
System.out.println("启动成功,端口 9999");
Channel channel = b.bind(new InetSocketAddress(9999)).sync().channel();
System.out.println(channel.localAddress());
channel.closeFuture().sync(); } finally {
acceptGroup.shutdownGracefully();
readGroup.shutdownGracefully();
}
}

客户端:

public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 9999);
socket.setTcpNoDelay(true);
OutputStream outputStream = socket.getOutputStream(); // 消息长度固定为 220字节,包含有
// 1. 目标用户ID长度为10, 10 000 000 000 ~ 19 999 999 999
// 2. 消息内容字符串长度最多70。 按一个汉字3字节,内容的最大长度为210字节
byte[] request = new byte[220];
byte[] userId = "10000000000".getBytes();
byte[] content = "我爱你baby你爱我吗我爱你baby你爱我吗我爱你baby你爱我吗我爱你baby你爱我吗".getBytes();
System.arraycopy(userId, 0, request, 0, 10);
System.arraycopy(content, 0, request, 10, content.length); for (int i = 0; i < 10; i++) { outputStream.write(request);
}
Thread.sleep(2000L); // 两秒后退出
socket.close();
}

Handller:

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 输出 bytebuf
ByteBuf buf = (ByteBuf) msg;
byte[] content = new byte[buf.readableBytes()];
buf.readBytes(content);
System.out.println(new String(content));
} // 异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}

服务端输出结果:



可以看到结果中并不是一行行打印的,就是出现了粘包问题,Netty怎么解决粘包和拆包呢,接着往下:

Netty编解码框架

TCP的粘包、拆包问题,可以通过自定义的通讯协议解决,通讯的双方约定好数据格式,发送方按照格式发送,接收方按照格式解析即可

1:编码

发送方将发送的二进制数据转换为协议规定的二进制数据流称为编码(encode),编码的功能是由编码器完成的

2:解码

接收方根据协议的格式,对二进制数据流进行解析,称为解码(decoder)解码的功能由解码器完成

3:编解码

既能编码,又能解码,称为编码解码器(codec)

Netty解码器

Netty中主要提供了抽象基类ByteToMessageDecoder和MessageToMessageDecoder。实现了ChannelInboundHandler接口。

ByteToMessageDecoder:用于将接收到的二进制数据(byte)解码,得到完整的请求报文(Message)。

MessageToMessageDecoder:将一个本身就包含完整报文信息的对象转换成另一个Java对象。

ByteToMessageDecoder实现类

1:FixedLengthFrameDecoder:定长协议解码器,可以指定固定的字节数算一个完整的报文

2:LineBasedFrameDecoder:行分隔符解码器,遇到\n或者\r\n,则认为是一个完整的报文

3:DelimiterBasedFrameDecoder:分隔符解码器,与 LineBasedFrameDecoder类似,只不过分隔符可以自己指定

4:LengthFieldBasedFrameDecoder:长度编码解码器,将报文划分为报文头/报文体,根据报文头中的Length字段确定报文体的长度,因此报文提的长度是可变的

5:JsonObjectDecoder:json格式解码器,当检测到匹配数量的"{" 、”}”或”[””]”时,则认为是一个完整的json对象或者json数组

MessageToMessageDecoder实现类

1:StringDecoder:用于将包含完整的报文信息的ByteBuf转换成字符串

2:Base64Decoder:用于Base64编码

Netty编码器

与ByteToMessageDecoder和MessageToMessageDecoder相对应,Netty提供了对应的编码器实现MessageToByteEncoder和MessageToMessageEncoder,二者都实现ChannelOutboundHandler接口。

相对来说,编码器比解码器的实现要更加简单,原因在于解码器除了要按照协议解析数据,还要处理粘包、拆包问题;而编码器只要将数据转换成协议规定的二进制格式发送即可

1:MessageToByteEncoder:是一个泛型类,泛型参数I表示将需要编码的对象的类型,编码的结果是将信息转换成二进制流放入ByteBuf中。子类通过覆写其抽象方法encode,来实现编码

2:MessageToMessageEncoder:同样是一个泛型类,泛型参数I表示将需要编码的对象的类型,编码的结果是将信息放到一个List中。子类通过覆写其抽象方法encode,来实现编码

MessageToMessageEncoder实现类

1:LineEncoder:按行编码,给定一个CharSequence(如String),在其之后添加换行符\n或者\r\n,并封装到ByteBuf进行输出,与LineBasedFrameDecoder相对应

2:Base64Encoder:给定一个ByteBuf,得到对其包含的二进制数据进行Base64编码后的新的ByteBuf进行输出,与Base64Decoder相对应

3:LengthFieldPrepender:给定一个ByteBuf,为其添加报文头Length字段,得到一个新的ByteBuf进行输出。Length字段表示报文长度,与LengthFieldBasedFrameDecoder相对应

4:StringEncoder:给定一个CharSequence(如:StringBuilder、StringBuffer、String等),将其转换成ByteBuf进行输出,与StringDecoder对应

Netty编码解码器

编码解码器同时具有编码与解码功能,特点同时实现了ChannelInboundHandler和ChannelOutboundHandler接口,因此在数据输入和输出时都能进行处理。Netty提供提供了一个ChannelDuplexHandler适配器类,编码解码器的抽象基类 ByteToMessageCodec 、MessageToMessageCodec都继承与此类

网络编程Netty入门:Netty的启动过程分析的更多相关文章

  1. 浅谈iOS网络编程之一入门

    计算机网络,基本上可以抽象是端的通信.实际在通讯中会用到不同的设备,不同的硬件中,为了能友好的传输信息,那么建立一套规范就十分必要了.先来了解一些基本概念 了解网络中传输的都是二进制数据流.  2.了 ...

  2. ios网络编程(入门级别)-- 基础知识

    在学习ios的过程中,停留在UI控件很长时间,现在正在逐步的接触当中!!!!!!在这个过程中,小编学到了一些关于网络编程知识,并且有感而发,在此分享一下: 关于网络请求的重要性我想不用多说了吧!!!对 ...

  3. socket 网络编程高速入门(一)教你编写基于UDP/TCP的服务(client)通信

    由于UNIX和Win的socket大同小异,为了方便和大众化,这里先介绍Winsock编程. socket 网络编程的难点在入门的时候就是对基本函数的了解和使用,由于这些函数的结构往往比較复杂,參数大 ...

  4. linux下网络编程学习——入门实例ZZ

    http://www.cppblog.com/cuijixin/archive/2008/03/14/44480.html 是不是还对用c怎么实现网络编程感到神秘莫测阿,我们这里就要撕开它神秘的面纱, ...

  5. 网络编程-Mysql-1、数据库的启动关闭,创建数据库,表等基本操作

    启动服务端:sudo service mysql start 关闭服务端:suodo service mysql stop 重启服务端:suodo service mysql restart 连接数据 ...

  6. Java网络编程从入门到精通(27):关闭服务端连接

    在客户端和服务端的数据交互完成后,一般需要关闭网络连接.对于服务端来说,需要关闭Socket和ServerSocket. 在关闭Socket后,客户端并不会马上感知自已的Socket已经关闭,也就是说 ...

  7. 网络编程udp入门

    老师布置的作业 echo4_server.c #include<stdio.h> #include<stdlib.h> #include<string.h> #in ...

  8. 用Netty开发中间件:网络编程基础

    用Netty开发中间件:网络编程基础 <Netty权威指南>在网上的评价不是很高,尤其是第一版,第二版能稍好些?入手后快速翻看了大半本,不免还是想对<Netty权威指南(第二版)&g ...

  9. Netty入门教程——认识Netty

    什么是Netty? Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架. Netty 是一个广泛使用的 Java 网络编程框架(N ...

  10. Netty 系列(三)Netty 入门

    Netty 系列(三)Netty 入门 Netty 是一个提供异步事件驱动的网络应用框架,用以快速开发高性能.高可靠性的网络服务器和客户端程序.更多请参考:Netty Github 和 Netty中文 ...

随机推荐

  1. JUnit5学习之三:Assertions类

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  2. 1079 Total Sales of Supply Chain ——PAT甲级真题

    1079 Total Sales of Supply Chain A supply chain is a network of retailers(零售商), distributors(经销商), a ...

  3. WPF -- 一种直线识别方案

    本文介绍一种直线的识别方案. 步骤 使用最小二乘法回归直线: 得到直线方程y=kx+b后,计算所有点到直线的距离,若在阈值范围内,认为是直线. 实现 /// <summary> /// 最 ...

  4. python使用requests模块下载文件并获取进度提示

    一.概述 使用python3写了一个获取某网站文件的小脚本,使用了requests模块的get方法得到内容,然后通过文件读写的方式保存到硬盘同时需要实现下载进度的显示 二.代码实现 安装模块 pip3 ...

  5. 基于QT的全自动超声波焊接机上位机追溯系统(已经在设备上应用)

    应用说明: 本上位机程序是我在做锂电池产线项目的时候开发的,用于采集设备数据以及实现设备自动控制,下位机采用基恩士PLC,超声波机采用上海一家的超声波焊接机,实现电芯极耳的自动焊接,上位在设备焊接过程 ...

  6. 大话Spark(5)-三图详述Spark Standalone/Client/Cluster运行模式

    之前在 大话Spark(2)里讲过Spark Yarn-Client的运行模式,有同学反馈与Cluster模式没有对比, 这里我重新整理了三张图分别看下Standalone,Yarn-Client 和 ...

  7. POJ-3660(Floyd算法)

    Cow Contest POJ-3660 1.本题考察的是最短路,用的算法是Floyd算法 2.如果一个结点和剩余的n-1个结点都有关系,那么可以确定其排名 3.需要注意的是,判断是否有关系时,反向关 ...

  8. .net 开源模板引擎jntemplate 教程:基础篇之语法

    一.基本概念 上一篇我们简单的介绍了jntemplate并写了一个hello world(如果没有看过的,点击查看),本文将继续介绍jntemplate的模板语法. 我们在讲解语法前,首先要了解一下标 ...

  9. Python学习笔记 CH1-4:从入门到列表

    Python CH1 环境准备 因为已经有了C/C++.Java的基础,所以上手很快. 参考书:Eric Matthes -<Python编程 从入门到实践> 环境准备:python3.P ...

  10. WPF 基础 - ControlTemplate

    常用 ControlTemplate 的地方:Control 的 Template 属性 运用效果举例:穿着 CheckBox 外衣的 ToggleButton,披着温度计的 ProgressBar. ...