Netty实战 IM即时通讯系统(六)实战: 客户端和服务端双向通信
##
Netty实战 IM即时通讯系统(六)实战: 客户端和服务端双向通信
零、 目录
- IM系统简介
- Netty 简介
- Netty 环境配置
- 服务端启动流程
- 实战: 客户端和服务端双向通信
- 数据传输载体ByteBuf介绍
- 客户端与服务端通信协议编解码
- 实现客户端登录
- 实现客户端与服务端收发消息
- pipeline与channelHandler
- 构建客户端与服务端pipeline
- 拆包粘包理论与解决方案
- channelHandler的生命周期
- 使用channelHandler的热插拔实现客户端身份校验
- 客户端互聊原理与实现
- 群聊的发起与通知
- 群聊的成员管理(加入与退出,获取成员列表)
- 群聊消息的收发及Netty性能优化
- 心跳与空闲检测
- 总结
- 扩展
###六、 实战: 客户端和服务端双向通信
本节我们要实现的功能是客户端连接成功后,向服务端写出一段数据 , 服务端收到数据后打印 , 并向客户端回复一段数据 。
我们先做一个代码框架 , 然后在框架上面做修改
public class Test_07_客户端和服务端双向通信 {public static void main(String[] args) {Test_07_Server.start(8000);Test_07_Client.start("127.0.0.1" , 8000 ,5);}}class Test_07_Client{public static void start(String IP , int port ,int maxRetry){NioEventLoopGroup workerGroup = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {}});connect(bootstrap , IP , port , maxRetry);}private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry , int... retryIndex) {bootstrap.connect(IP , port).addListener(future ->{int[] finalRetryIndex;if(future.isSuccess()) {System.out.println("连接成功");}else if(maxRetry ==0) {System.out.println("达到最大重试此时,放弃重试");}else {// 初始化 重试计数if(retryIndex.length == 0) {finalRetryIndex = new int[]{0};}else {finalRetryIndex = retryIndex;}// 计算时间间隔int delay = 1 << finalRetryIndex[0];// 执行重试System.out.println(new Date() +" 连接失败,剩余重试次数:"+ maxRetry + ","+delay+"秒后执行重试");bootstrap.config().group().schedule(()->{connect(bootstrap , IP, port , maxRetry -1 , finalRetryIndex[0]+1);}, delay, TimeUnit.SECONDS);}});}}class Test_07_Server{public static void start(int port){NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new Test_07_ServerHandler());}});bind(serverBootstrap, port);}private static void bind(ServerBootstrap serverBootstrap, int port) {serverBootstrap.bind(port).addListener(future -> {if(future.isSuccess()) {System.out.println("服务端:端口【"+port+"】绑定成功!");}else {System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");bind(serverBootstrap, port+1);}});}}
客户端发送数据到服务端
在《客户端启动流程》这一小节 , 我们提到 客户端相关的数据读写逻辑是通过BootStrap的handler()方法指定
bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {}});
现在我们在initChannel()中给客户端添加一个逻辑处理器 , 这个处理器的作用就是负责向服务端写数据
bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑ch.pipeline().addLast(new Test_07_ClientHandler());}});
ch.pipeline()方法返回的是和这条连接相关的逻辑处理链 , 采用了责任链处理模式 , 这里不理解没关系 , 后面会讲到。
然后再调用addLast()方法添加一个逻辑处理器 , 这个逻辑处理器为的就是在客户端建立连接成功之后向服务端写数据 , 下面是这个逻辑处理器的代码:
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {System.out.println(new Date() + " 客户端写出数据...");// 1. 获取数据ByteBuf buffer = getByteBuf(ctx);// 2. 写数据ctx.channel().writeAndFlush(buffer);}private ByteBuf getByteBuf(ChannelHandlerContext ctx) {// 获取二进制抽象 ByteBufferByteBuf buf = ctx.alloc().buffer();// 准备数据byte[] bs = "你好,奥特曼!".getBytes(Charset.forName("UTF-8"));// 把数据填充到 bufbuf.writeBytes(bs);return buf;}}
- 这个逻辑处理器继承自ChannelInboundHandlerAdapter ,然后覆盖了channelActive()方法 , 这个方法会在客户端连接建立成功之后被调用
- 客户端连接建立成功之后 , 调用channelActive() , 在这个方法里面 , 我们编写向服务端写数据的逻辑
- 向服务端写数据分为两步 , 首先我们要获取一个netty对二进制数据抽象的二进制ByteBuf , 上面代码中ctx.alloc() 获取一个ByteBuf的内存管理器 , 这个内存管理器的作用就是分配一个ByteBuf , 然后我们把字符串的二进制数据填充到ByteBuf , 这样我们就获取到了Netty需要的一个数据格式, 最后我们调用ctx.channel().writeAndFlush()把数据写到服务端。
- 以上就是 向服务端写数据的逻辑 , 和传统的socket 编程不同的是 , Netty 里面的数据是以ByteBuf为单位的 , 所有需要写出的数据必须塞到一个ByteBuf里 , 需要读取的数据也是如此。
服务端读取客户端数据
服务端的数据处理逻辑 是通过ServerBootStrap 的childHandler()方法指定
serverBootStrtap.childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {// TODO Auto-generated method stub}})
现在 , 我们在initChannel() 中 给服务端添加一个逻辑处理器 , 这个处理器 的作用就是负责客户端读数据
serverBootStrtap.childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new Test_07_ServerHandler());}})
这个方法里的逻辑和客户端类似 , 获取服务端关于这条连接的逻辑处理链pipeline , 然后添加一个逻辑处理器 , 负责读取客户端发来的数据
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));}}
- 服务端的逻辑处理器同样是继承自ChannelInboundHandlerAdapter , 与客户端不同的是 , 这里覆盖的方法是 channelRead() ,这个方法在接收到数据之后会被回调
- 这里的msg 值的是Netty里面数据读写的载体 , 为什么不直接是ByteBuf , 而需要我们强转一下 , 我们后面会分析道 , 这里我们强转之后 , 然后调用buteBuf.toString() 就能够拿到我们客户端发过来的字符串数据。
运行测试
完整代码
import java.nio.charset.Charset;import java.util.Date;import java.util.concurrent.TimeUnit;import io.netty.bootstrap.Bootstrap;import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.channel.ChannelInitializer;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;public class Test_07_客户端和服务端双向通信 {public static void main(String[] args) throws Exception {Test_07_Server.start(8000);Test_07_Client.start("127.0.0.1", 8000, 5);}}class Test_07_Client {public static void start(String IP, int port, int maxRetry) {NioEventLoopGroup workerGroup = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑ch.pipeline().addLast(new Test_07_ClientHandler());}});connect(bootstrap, IP, port, maxRetry);}private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry, int... retryIndex) {bootstrap.connect(IP, port).addListener(future -> {int[] finalRetryIndex;if (future.isSuccess()) {System.out.println("客户端连接【"+IP+":"+port+"】成功");} else if (maxRetry == 0) {System.out.println("达到最大重试此时,放弃重试");} else {// 初始化 重试计数if (retryIndex.length == 0) {finalRetryIndex = new int[] { 0 };} else {finalRetryIndex = retryIndex;}// 计算时间间隔int delay = 1 << finalRetryIndex[0];// 执行重试System.out.println(new Date() + " 连接失败,剩余重试次数:" + maxRetry + "," + delay + "秒后执行重试");bootstrap.config().group().schedule(() -> {connect(bootstrap, IP, port, maxRetry - 1, finalRetryIndex[0] + 1);}, delay, TimeUnit.SECONDS);}});}}class Test_07_Server {public static void start(int port) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new Test_07_ServerHandler());}});bind(serverBootstrap, port);}private static void bind(ServerBootstrap serverBootstrap, int port) {serverBootstrap.bind(port).addListener(future -> {if(future.isSuccess()) {System.out.println("服务端:端口【"+port+"】绑定成功!");}else {System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");bind(serverBootstrap, port+1);}});}}class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {String content = "你好,奥特曼!";System.out.println(new Date() + " 客户端写出数据:"+content);// 1. 获取数据ByteBuf buffer = getByteBuf(ctx , content);// 2. 写数据ctx.channel().writeAndFlush(buffer);}private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {// 获取二进制抽象 ByteBufferByteBuf buf = ctx.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到 bufbuf.writeBytes(bs);return buf;}}class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));}}
运行结果:
服务端回复数据给客户端
服务端向客户端写数据的逻辑与客户端向服务端写数据的逻辑一样 , 先创建一个ByteBuf , 然后填充二进制数据 , 最后调用writeAndFlush()方法写出去
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));// 向客户端回复数据String content = "你好,田先森!";System.out.println(new Date() +":服务端写出数据-> "+content);ByteBuf byteBuf = getByteBuf(ctx , content);ctx.channel().writeAndFlush(byteBuf);}private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {// 获取 二进制抽象 ByteBufByteBuf byteBuf = cxt.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到buf中byteBuf.writeBytes(bs);return byteBuf;}}
现在轮到客户端了 , 客户端读取数据的逻辑和服务端读数据的逻辑一样 , 同样是覆盖channelRead() 方法
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf = (ByteBuf) msg;System.out.println(new Date()+": 客户端读到数据 ->"+ byteBuf.toString(Charset.forName("UTF-8")));}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {String content = "你好,奥特曼!";System.out.println(new Date() + " 客户端写出数据:"+content);// 1. 获取数据ByteBuf buffer = getByteBuf(ctx , content);// 2. 写数据ctx.channel().writeAndFlush(buffer);}private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {// 获取二进制抽象 ByteBufferByteBuf buf = ctx.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到 bufbuf.writeBytes(bs);return buf;}}
现在 客户端和服务端就实现了双向通信
完整代码:
import java.nio.charset.Charset;import java.util.Date;import java.util.concurrent.TimeUnit;import io.netty.bootstrap.Bootstrap;import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.channel.ChannelInitializer;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;public class Test_07_客户端和服务端双向通信 {public static void main(String[] args) throws Exception {Test_07_Server.start(8000);Test_07_Client.start("127.0.0.1", 8000, 5);}}class Test_07_Client {public static void start(String IP, int port, int maxRetry) {NioEventLoopGroup workerGroup = new NioEventLoopGroup();Bootstrap bootstrap = new Bootstrap();bootstrap.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑ch.pipeline().addLast(new Test_07_ClientHandler());}});connect(bootstrap, IP, port, maxRetry);}private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry, int... retryIndex) {bootstrap.connect(IP, port).addListener(future -> {int[] finalRetryIndex;if (future.isSuccess()) {System.out.println("客户端连接【"+IP+":"+port+"】成功");} else if (maxRetry == 0) {System.out.println("达到最大重试此时,放弃重试");} else {// 初始化 重试计数if (retryIndex.length == 0) {finalRetryIndex = new int[] { 0 };} else {finalRetryIndex = retryIndex;}// 计算时间间隔int delay = 1 << finalRetryIndex[0];// 执行重试System.out.println(new Date() + " 连接失败,剩余重试次数:" + maxRetry + "," + delay + "秒后执行重试");bootstrap.config().group().schedule(() -> {connect(bootstrap, IP, port, maxRetry - 1, finalRetryIndex[0] + 1);}, delay, TimeUnit.SECONDS);}});}}class Test_07_Server {public static void start(int port) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new Test_07_ServerHandler());}});bind(serverBootstrap, port);}private static void bind(ServerBootstrap serverBootstrap, int port) {serverBootstrap.bind(port).addListener(future -> {if(future.isSuccess()) {System.out.println("服务端:端口【"+port+"】绑定成功!");}else {System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");bind(serverBootstrap, port+1);}});}}class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf = (ByteBuf) msg;System.out.println(new Date()+": 客户端读到数据 ->"+ byteBuf.toString(Charset.forName("UTF-8")));}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {String content = "你好,奥特曼!";System.out.println(new Date() + " 客户端写出数据:"+content);// 1. 获取数据ByteBuf buffer = getByteBuf(ctx , content);// 2. 写数据ctx.channel().writeAndFlush(buffer);}private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {// 获取二进制抽象 ByteBufferByteBuf buf = ctx.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到 bufbuf.writeBytes(bs);return buf;}}class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));// 向客户端回复数据String content = "你好,田先森!";System.out.println(new Date() +":服务端写出数据-> "+content);ByteBuf byteBuf = getByteBuf(ctx , content);ctx.channel().writeAndFlush(byteBuf);}private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {// 获取 二进制抽象 ByteBufByteBuf byteBuf = cxt.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到buf中byteBuf.writeBytes(bs);return byteBuf;}}
执行结果
总结
- 本小节中 , 我们了解到客户端和服务端的逻辑处理均是在启动的时候 , 通过给逻辑处理链pipeline添加逻辑处理器 , 来编写数据的处理逻辑 , pipeline的逻辑我们会在后面分析
- 接下来我们学到了 在客户端连接成功之后会回调逻辑处理器的channelActive()方法 , 而不管是服务端还是客户端 , 收到数据之后都会调用channelRead方法
- 写数据用writeAndFlush() 方法 客户端与服务端交互的二进制数据载体为ByteBuf , ByteBuf 通过连接的内存管理器创建 , 字节数据填充到ByteBuf 之后才能写到对端 , 接下来一小节 , 我们就重点来分析ByteBuf
思考: 如何实现在新连接介入的时候 , 服务端主动向客户端推送消息 , 客户端回复服务端消息?
解答: 在服务器端的逻辑处理其中也实现 channelActive() 在有新的连接接入时 会回调此方法
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {String content = "是不是你连我了?";System.out.println(new Date() +":服务端写出数据-> "+content);ByteBuf byteBuf = getByteBuf(ctx , content);ctx.channel().writeAndFlush(byteBuf);}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));// 向客户端回复数据String content = "你好,田先森!";System.out.println(new Date() +":服务端写出数据-> "+content);ByteBuf byteBuf = getByteBuf(ctx , content);ctx.channel().writeAndFlush(byteBuf);}private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {// 获取 二进制抽象 ByteBufByteBuf byteBuf = cxt.alloc().buffer();// 准备数据byte[] bs = content.getBytes(Charset.forName("UTF-8"));// 把数据填充到buf中byteBuf.writeBytes(bs);return byteBuf;}}
Netty实战 IM即时通讯系统(六)实战: 客户端和服务端双向通信相关推荐
- Netty实战 IM即时通讯系统(十二)构建客户端与服务端pipeline
Netty实战 IM即时通讯系统(十二)构建客户端与服务端pipeline 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端和服务端双向 ...
- Netty实战 IM即时通讯系统(十一)pipeline与channelHandler
Netty实战 IM即时通讯系统(十一)pipeline与channelHandler 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端 ...
- Netty实战 IM即时通讯系统(十)实现客户端和服务端收发消息
Netty实战 IM即时通讯系统(十)实现客户端和服务端收发消息 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端和服务端双向通信 数据 ...
- Netty实战 IM即时通讯系统(九)实现客户端登录
## Netty实战 IM即时通讯系统(九)实现客户端登录 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端和服务端双向通信 数据传输载 ...
- Netty实战 IM即时通讯系统(八)服务端和客户端通信协议编解码
Netty实战 IM即时通讯系统(八)服务端和客户端通信协议编解码 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端和服务端双向通信 数 ...
- Netty实战 IM即时通讯系统(七)数据传输载体ByteBuf介绍
## Netty实战 IM即时通讯系统(七)数据传输载体ByteBuf介绍 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 客户端启动流程 实战: 客户端和服务端双向 ...
- Netty实战 IM即时通讯系统(五)客户端启动流程
## Netty实战 IM即时通讯系统(五)客户端启动流程 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 实战: 客户端和服务端双向通信 数据传输载体ByteBuf ...
- Netty实战 IM即时通讯系统(四)服务端启动流程
## Netty实战 IM即时通讯系统(四)服务端启动流程 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 实战: 客户端和服务端双向通信 数据传输载体ByteBuf ...
- Netty实战 IM即时通讯系统(三)Netty环境配置
## Netty实战 IM即时通讯系统(三)Netty环境配置 零. 目录 IM系统简介 Netty 简介 Netty 环境配置 服务端启动流程 实战: 客户端和服务端双向通信 数据传输载体ByteB ...
最新文章
- VMware vSphere四种迁移类型的区别与适应场景
- mlc tlc slc qlc_QLC颗粒怎么样?固态硬盘QLC、SLC、MLC、TLC颗粒区别对比知识
- 设置图片格式为php,php 将bmp图片转为jpg等其他任意格式的图片
- 如何新建分支上传_Git分支策略及操作演示1|IDCF FDCC认证学员作品
- NumPy 数学函数
- ubuntu之ufw防火墙
- 系统架构师学习笔记-系统开发基础知识(一)
- openssl req
- 注册登录时本地图片验证码
- 独角兽影视APP系统源码/双端影视APP源码
- JPA语法大全 特别是JPA的不等于
- 打开和切换文件的基础方法(DOS)命令
- 中国第一大忽悠终于倒下了
- 工业级交换机级联介绍
- F-Pairwise Modulo
- 服务器芯片市场容量,未来内存接口芯片市场规模 使用内存接口芯片的服务器内存模组主要有寄存内存模组(RDIMM)和减载内存模组(LRDIMM)两种。RDIMM需要一颗寄... - 雪球...
- rails respon_to
- 《AngularJS深度剖析与最佳实践》一1.1 环境准备
- tableview概述
- Tensorflow2.x: TensorFlow Addons介绍
热门文章
- linux信号量超过系统限制
- 深度实现session【包括session入库、session机制和session和cookie的使用方法,完善会话机制(在分布式机器中也能使用)】、无限分类的实现...
- 原SUN网站:java.sun.com,developers.sun.com,bigadmin将合并到OTN
- python简单定义_python定义类的简单用法
- php 输入内容类型,实例解析php的数据类型
- logrotate测试_使用 logrotate 对 apache/nginx 日志切割
- C语言学习之猴子吃桃问题。猴子第1天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第2天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。
- 网页html好学嘛,javascript好学么?
- 一道网易游戏笔试题的不同解法
- C# 文件流 导入 导出