Netty自娱自乐之协议栈设计
---恢复内容开始---
俺工作已经一年又6个月了,想想过的真快,每天写业务,写业务,写业务......。然后就是祈祷着,这次上线不要出现线上bug。继续这每天无聊的增删改查,学习学习一下自己感兴趣的事,就把自己当作小学生。然后学学习,打发打发时间,如果以后自己能用到呢?这又有谁说的清楚。
好了,最近在学习Netty,主要看了这2本书的一些内容,第一本就是《Netty实战》,第二本就是《Netty权威指南》。然后在看到Netty权威指南上有一章比较感兴趣,用了整整一章用来描写如何取自己定义一个协议。接着阅读完后,我就按照书本上的相关内容,去实现了一下。纠正了一下书本上的错误代码。工作都是在开发电商项目,基本上对底层传输这一块接触甚少。如果有机会想去一个游戏公司,这样看看能不能接触更多的网络传输相关内容。哎,不知道这样的去转有木有要,纠结。。。。。。。。。
好了,现在开始看书和事件的经历吧。
现在,我们设计一个传输协议如下
2字节:协议固定值 1字节:主版本号 |
消息长度 :消息头 和消息体 |
回话ID, 全局唯一 |
业务请求消息 |
优先级别 |
附件 |
code |
length |
sessionId |
type |
primary |
attachment |
上面的定义,是来着Netty的权威指南。这个是协议的头。然后接下来是一个协议体。而协议体在编码上就是一个Object.
协议头 | 协议体 |
customHeader |
bodyMessage |
根据上面的定义,直接写出协议定义model.直接上代码:
@Data
@ToString
public class NettyCustomHeader {
/**
* code 2字节:netty协议消息, 1字节:主版本号 1字节:副版本号 4
*/
private int code = 0xABCD0101; /**
* 消息长度 :消息头 和消息题 32
*/
private int length; /**
* 回话ID, 全局唯一 64
*/
private long sessionId; /**
* 业务请求消息 1:业务请求消息 2:业务响应消息 3:握手请求消息 4:握手应答消息 5:心跳请求消息 6:心跳应答消息
*/
private byte type; /**
* 优先级别
*/
private byte primary; /**
* 附件
*/
Map<String, Object> attachment; }
@Data
@ToString
public class NettyCustomMessage { /**
* 消息头
*/
private NettyCustomHeader customHeader; /**
* 消息体
*/
private Object bodyMessage; }
学过Netty的同学或者了解的同学知道,Netty是通过ChannelHandler来处理IO消息的。我编码的Netty版本是4。那么处理消息首先第一步就是解码,LengthFieldBasedFrameDecoder这个解码器是基于长度的解码器,并且能解决TCP/IP包的粘包和拆包问题。代码如下。
public class ByteBuf2NettyMessageDecoder extends LengthFieldBasedFrameDecoder { // private NettyMarshallingDecoder marshallingDecoder = NettyMarshallingFactory.buildNettyMarshallingDecoder(); public ByteBuf2NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
} public ByteBuf2NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
} public ByteBuf2NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
} public ByteBuf2NettyMessageDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
super(byteOrder, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
} @Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//调用父类decode ,得到整包消息
ByteBuf readBuf = (ByteBuf) super.decode(ctx, in);
if (readBuf == null) {
return null;
}
NettyCustomMessage customMessage = new NettyCustomMessage();
NettyCustomHeader customHeader = new NettyCustomHeader();
customHeader.setCode(readBuf.readInt());
customHeader.setLength(readBuf.readInt());
customHeader.setSessionId(readBuf.readLong());
customHeader.setType(readBuf.readByte());
customHeader.setPrimary(readBuf.readByte()); int attachmentSize = readBuf.readByte();
if (attachmentSize > 0) {
Map<String, Object> attachment = new HashMap<String, Object>();
for (int i = 0; i < attachmentSize; i++) {
int keySize = readBuf.readInt();
byte[] keyByte = new byte[keySize];
readBuf.readBytes(keyByte);
String key = new String(keyByte, CharsetUtil.UTF_8.name()); Object value = JavaByteFactory.decode(readBuf);
//Object value = marshallingDecoder.decode(ctx, readBuf);
attachment.put(key, value);
}
customHeader.setAttachment(attachment);
} customMessage.setCustomHeader(customHeader);
if (readBuf.readableBytes() > 0) {
Object body = JavaByteFactory.decode(readBuf);
//Object body = marshallingDecoder.decode(ctx, readBuf);
customMessage.setBodyMessage(body);
} return customMessage;
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
cause.getStackTrace();
super.exceptionCaught(ctx, cause);
}
}
上面注释的原因,marshallingDecoder不支持java7,所以我自己写了一个编码/解码帮助类,就是前4个字节代表长度,后面是就是时间内容。从上面的代码我们知道,就是把ByteBuf转化为自己定义的协议对象。从上面的解码上,可能有点模糊,但是从下面的如何编码上,就可以知道为啥是这么解码的。
public class NettyMessage2ByteBufEncoder extends MessageToMessageEncoder<NettyCustomMessage> { private NettyMarshallingEncoder nettyMarshallingEncoder; public NettyMessage2ByteBufEncoder() {
// this.nettyMarshallingEncoder = NettyMarshallingFactory.buildNettyMarshallingEncoder(); } protected void encode(ChannelHandlerContext ctx, NettyCustomMessage msg, List<Object> out) throws Exception { if (msg == null || msg.getCustomHeader() == null) {
throw new Exception("the encode message is null");
} ByteBuf sendBuf = Unpooled.buffer();
sendBuf.writeInt(msg.getCustomHeader().getCode());
sendBuf.writeInt(msg.getCustomHeader().getLength());
sendBuf.writeLong(msg.getCustomHeader().getSessionId());
sendBuf.writeByte(msg.getCustomHeader().getType());
sendBuf.writeByte(msg.getCustomHeader().getPrimary()); //attachment , if (msg.getCustomHeader().getAttachment() != null) {
sendBuf.writeByte(msg.getCustomHeader().getAttachment().size());
String key = null;
byte[] keyArray = null;
for (Map.Entry<String, Object> entryKey : msg.getCustomHeader().getAttachment().entrySet()) {
key = entryKey.getKey();
keyArray = key.getBytes(CharsetUtil.UTF_8.name());
sendBuf.writeInt(keyArray.length);
sendBuf.writeBytes(keyArray);
ByteBuf value = JavaByteFactory.encode(entryKey.getValue());
sendBuf.writeBytes(value);
// nettyMarshallingEncoder.encode(ctx, entryKey.getValue(), sendBuf);
}
} else {
sendBuf.writeByte(0);
} if (msg.getBodyMessage() != null) {
ByteBuf value = JavaByteFactory.encode(msg.getBodyMessage());
sendBuf.writeBytes(value);
//nettyMarshallingEncoder.encode(ctx, msg.getBodyMessage(), sendBuf);
} //在第5个字节开始的int 是长度,重新设置
sendBuf.setInt(4, sendBuf.readableBytes()); out.add(sendBuf);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
cause.getStackTrace();
super.exceptionCaught(ctx, cause);
}
}
从上面可以知道解码,就是把自定义协议对象 NettyCustomMessage 通过自己的规则放到ByteBuf上。代码比较简单,不解释。JavaByteFactory的代码如下:
public class JavaByteFactory { public static Object decode(ByteBuf byteBuf) {
if (byteBuf == null || byteBuf.readableBytes() <= 0) {
return null;
}
int valueSize = byteBuf.readInt();
byte[] value = new byte[valueSize];
byteBuf.readBytes(value); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(value);
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(byteArrayInputStream);
return inputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null; } public static ByteBuf encode(Object object) {
if (object == null) {
return null;
}
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutput);
objectOutputStream.writeObject(object);
byte[] bytes = byteOutput.toByteArray(); ByteBuf buffer = Unpooled.buffer(bytes.length + 4);
buffer.writeInt(bytes.length);
buffer.writeBytes(bytes);
return buffer; } catch (IOException e) {
e.printStackTrace();
}
return null;
}
编码就是首选把Object 对象转换了byte []数组,然后写入4个字节为byte[]数组的长度,接着是数组的内容到ByteBuf对象上。相应的解码就是先获取4个字节,得到后面字节长度,接着读取指定长度即可。
接着心跳和权限检测都是在解码器之后进行业务的处理。直接上代码。
下面是权限认证的请求handler和响应handler.
public class AuthorityCertificationRequestHanlder extends ChannelInboundHandlerAdapter { @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buildAuthorityCertificationMsg());
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyCustomMessage message = (NettyCustomMessage) msg;
if (message != null && message.getCustomHeader() != null && message.getCustomHeader().getType() == NettyMessageConstant.CUSTOMER_AUTH_CERTI_TYPE) {
byte authResult = (Byte) message.getBodyMessage();
if (authResult != (byte) 0) { //握手失败。关闭链接
ctx.close();
return;
}
System.out.println("authority certification is success .....");
ctx.fireChannelRead(msg);
} else {
ctx.fireChannelRead(msg);
} } @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.getStackTrace();
ctx.channel().close();
System.out.println(cause.getStackTrace());
ctx.fireExceptionCaught(cause);
} protected NettyCustomMessage buildAuthorityCertificationMsg() {
NettyCustomMessage message = new NettyCustomMessage();
NettyCustomHeader customHeader = new NettyCustomHeader();
customHeader.setType(NettyMessageConstant.CUSTOMER_AUTH_CERTI_TYPE);
message.setCustomHeader(customHeader);
return message;
} }
public class AuthorityCertificationResponseHanlder extends ChannelInboundHandlerAdapter { private Map<String, Boolean> authority = new ConcurrentHashMap<String, Boolean>(); private String[] ipList = new String[]{"127.0.0.1"}; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { NettyCustomMessage customMessage = (NettyCustomMessage) msg;
NettyCustomMessage response;
if (customMessage.getCustomHeader() != null && customMessage.getCustomHeader().getType() == NettyMessageConstant.CUSTOMER_AUTH_CERTI_TYPE) {
String remoteAddress = ctx.channel().remoteAddress().toString();
if (authority.containsKey(remoteAddress)) { //重复登陆
response = buildAuthorCertiResponseMessage((byte) -1);
} else {
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
boolean isAuth = false;
for (String ip : ipList) {
if (ip.equals(inetSocketAddress.getAddress().getHostAddress())) {
isAuth = true;
break;
}
}
if (isAuth) {
response = buildAuthorCertiResponseMessage((byte) 0);
authority.put(remoteAddress, true);
} else {
response = buildAuthorCertiResponseMessage((byte) -1);
}
}
System.out.println("the client [" + remoteAddress + "] is connecting ,status:" + response);
ctx.writeAndFlush(response);
return;
}
ctx.fireChannelRead(msg);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
cause.getStackTrace();
String remoteAddress = ctx.channel().remoteAddress().toString();
authority.remove(remoteAddress);
ctx.channel().close();
ctx.fireExceptionCaught(cause);
} private NettyCustomMessage buildAuthorCertiResponseMessage(byte body) {
NettyCustomMessage message = new NettyCustomMessage();
NettyCustomHeader customHeader = new NettyCustomHeader();
customHeader.setType(NettyMessageConstant.SERVER_AUTH_CERTI_TYPE);
message.setCustomHeader(customHeader);
message.setBodyMessage(body);
return message;
} }
下面是心跳检测handler
public class HeartBeatCheckRequestHandler extends ChannelInboundHandlerAdapter { private volatile ScheduledFuture<?> scheduledFuture; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyCustomMessage customMessage = (NettyCustomMessage) msg;
if (customMessage.getCustomHeader() != null && customMessage.getCustomHeader().getType() == NettyMessageConstant.SERVER_AUTH_CERTI_TYPE) {
scheduledFuture = ctx.executor().scheduleAtFixedRate(new HeartBeatCheckTask(ctx), 0, 5000, TimeUnit.MILLISECONDS);
System.out.println("the client [ " + ctx.channel().localAddress().toString() + " ] send heart beat ...........");
} else if (customMessage.getCustomHeader() != null && customMessage.getCustomHeader().getType() == NettyMessageConstant.HEART_BEAT_CHECK_PONG_TYPE) {
System.out.println("the client [ " + ctx.channel().localAddress().toString() + " ] recieve heart beat .............");
} else {
ctx.fireChannelRead(msg);
} } @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
cause.getStackTrace();
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledFuture = null;
}
ctx.fireExceptionCaught(cause);
} class HeartBeatCheckTask implements Runnable { private ChannelHandlerContext context; public HeartBeatCheckTask(ChannelHandlerContext context) {
this.context = context;
} @Override
public void run() {
NettyCustomMessage customMessage = new NettyCustomMessage();
NettyCustomHeader customHeader = new NettyCustomHeader();
customHeader.setType(NettyMessageConstant.HEART_BEAT_CHECK_PING_TYPE);
customMessage.setCustomHeader(customHeader);
context.writeAndFlush(customMessage);
System.out.println("the client [ " + context.channel().localAddress().toString() + " ] send heart beat to server ...."); }
}
}
public class HeartBeatCheckResponseHandler extends ChannelInboundHandlerAdapter { @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyCustomMessage customMessage = (NettyCustomMessage) msg;
if (customMessage.getCustomHeader() != null && customMessage.getCustomHeader().getType() == NettyMessageConstant.HEART_BEAT_CHECK_PING_TYPE) {
System.out.println("the server recieve the client [ " + ctx.channel().remoteAddress().toString() + " ] heart beat check package,"); NettyCustomMessage sendPongMessage = new NettyCustomMessage();
NettyCustomHeader customHeader = new NettyCustomHeader();
customHeader.setType(NettyMessageConstant.HEART_BEAT_CHECK_PONG_TYPE);
sendPongMessage.setCustomHeader(customHeader);
ctx.writeAndFlush(customMessage);
return;
}
ctx.fireChannelRead(msg);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getStackTrace());
cause.getStackTrace();
super.exceptionCaught(ctx, cause);
} @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("the client [ " + ctx.channel().remoteAddress().toString() + " ] is close ....,then close channel");
ctx.channel().close();
} }
最后是我们的客户端和服务端代码,如下:
public class NettyProtocalClient {
private ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1); private Bootstrap bootstrap; private EventLoopGroup eventLoopGroup; private String host; private int port; private int localPort; public NettyProtocalClient(String host, int port) {
this(7777, host, port);
} public NettyProtocalClient(int localPort, String host, int port) {
this.host = host;
this.port = port;
this.localPort = localPort;
} public void connect() throws InterruptedException {
try {
bootstrap = new Bootstrap();
eventLoopGroup = new NioEventLoopGroup();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<io.netty.channel.Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast("log", new LoggingHandler(LogLevel.INFO))
.addLast("decoder", new ByteBuf2NettyMessageDecoder(6 * 1024, 4, 4, -8, 0, true))
.addLast("encoder", new NettyMessage2ByteBufEncoder())
.addLast("timeout", new ReadTimeoutHandler(50))
.addLast("authority", new AuthorityCertificationRequestHanlder())
.addLast("hearbeat", new HeartBeatCheckRequestHandler()); }
});
ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port), new InetSocketAddress("127.0.0.1", localPort)).sync();
future.channel().closeFuture().sync();
} finally {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully().sync();
}
executorService.execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
connect();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}); }
}
}
public class NettyProtocalServer {
private ServerBootstrap serverBootstrap; private EventLoopGroup boss; private EventLoopGroup worker; private String host; private int port; public NettyProtocalServer(String host, int port) {
this.host = host;
this.port = port;
} public void start() throws InterruptedException {
try {
serverBootstrap = new ServerBootstrap();
boss = new NioEventLoopGroup(1);
worker = new NioEventLoopGroup(); serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast("log",new LoggingHandler(LogLevel.INFO))
.addLast("decoder", new ByteBuf2NettyMessageDecoder(6 * 1024, 4, 4, -8, 0, true))
.addLast("encoder", new NettyMessage2ByteBufEncoder())
.addLast("timeout", new ReadTimeoutHandler(50))
.addLast("authority", new AuthorityCertificationResponseHanlder())
.addLast("hearbeat", new HeartBeatCheckResponseHandler()); }
});
ChannelFuture future = serverBootstrap.bind(new InetSocketAddress(host, port)).sync();
future.channel().closeFuture().sync();
} finally {
if (boss != null) {
boss.shutdownGracefully();
}
if (worker != null) {
worker.shutdownGracefully();
}
}
}
}
最后看一看运行结果吧:
服务端显示内容:
客户端显示内容:
---恢复内容结束---
Netty自娱自乐之协议栈设计的更多相关文章
- Netty自娱自乐之类Dubbo RPC 框架设计构想 【上篇】
之前在前一篇的<Netty自娱自乐之协议栈设计>,菜鸟我已经自娱自乐了设计协议栈,gitHub地址为https://github.com/vOoT/ncustomer-protocal.先 ...
- [自娱自乐] 3、超声波测距模块DIY笔记(三)
前言 上一节我们已经研究了超声波接收模块并自己设计了一个超声波接收模块,在此基础上又尝试用单片机加反相器构成生成40KHz的超声波发射电路,可是发现采用这种设计的发射电路存在严重的发射功率太低问题,对 ...
- [自娱自乐] 4、超声波测距模块DIY笔记(四)——终结篇·基于C#上位机软件开发
前言 上一节我们已经基本上把超声波硬件的发射和接收模块全部做好了,接下来我们着手开发一个软硬结合的基于C#的平面定位软件! 目录 一.整体思路 二.效果提前展示 2-1.软件部分展示 2-2.硬件部分 ...
- [转]为何TCP/IP协议栈设计成沙漏型的
http://m.blog.csdn.net/blog/dog250/18959371 前几天有人回复我的一篇文章问,为何TCP/IP协议栈设计成沙漏型的.这个问题问得好!我先不谈为何它如此设计,我一 ...
- 用C++ 自娱自乐
最无聊的时光当属 考试前的复习时段了,在一些论坛上看到一些用字符组成的图像,觉得有点意思,于是,自己 用C++ 参考一些论坛的图像,写了下面这个东西,来表达此时的心情. #include<ios ...
- [置顶] 自娱自乐7之Linux UDC驱动2(自编udc驱动,现完成枚举过程,从驱动代码分析枚举过程)
花了半个月,才搞定驱动中的枚举部分,现在说linux的枚举,windows可能有差别. 代码我会贴在后面,现在只是实现枚举,你可能对代码不感兴趣,我就不分析代码了,你可以看看 在<自娱自乐1&g ...
- 利用Python编写Windows恶意代码!自娱自乐!勿用于非法用途!
本文主要展示的是通过使用python和PyInstaller来构建恶意软件的一些poc. 利用Python编写Windows恶意代码!自娱自乐!勿用于非法用途!众所周知的,恶意软件如果影响到了他人的生 ...
- [置顶] 自娱自乐1之Linux UDC驱动(形式模板)
首先,我不是做驱动的开发人员.所以只能用自娱自乐来表示我的行为. 我不知道udc和gadget驱动是不是冷门的驱动,资料真是不多.我之前买了一本书,上面说到这些,就教你如何调试已写好的驱动.这样也可以 ...
- Netty服务器连接池管理设计思路
应用场景: 在RPC框架中,使用Netty作为高性能的网络通信框架时,每一次服务调用,都需要与Netty服务端建立连接的话,很容易导致Netty服务器资源耗尽.所以,想到连接池技术,将与同一个Nett ...
随机推荐
- linux c++如何学习
最近有人问我,linux c++工资这么高,怎么学习才能入门,只要有个项目经验能进入一个公司即可. 然后我就说了linux c++开发的整个流程,然后用项目作为目标进行学习,这其实是一种以目的为主导的 ...
- java亦或(^)
在java程序里面的异或用法: 相同输出0,不同输出1,例如: System.out.println(1^1); 输出0 System.out.println(1^2):输出3,因为最后2个低位都不一 ...
- java中链接数据库的具体操作以及pstmt.setObject(i+1, objects[i])这行代码的意思
package dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStat ...
- kickstart文件详解
kickstart自动应答文件选项非常多,以下只说明CentOS 6下几个常用的可能用到的选项.另外,CentOS 6和CentOS 7的选项有不小区别,所以请注意使用,可以查看官方安装文档. Cen ...
- 解决js中post提交数据并且跳转到指定页面的问题总结
今天在开发中过程中遇到了这个问题,js中利用JQuery中的 $.post("url", id, function(){}); 这个方法是数据提交正常,但是后台处理完成之后跳转无法 ...
- 总结jq的一些特效
Jquery代码总结 1.轮播图: <script src="jquery-1.8.3.min.js"></script><script> ...
- 前端到后台ThinkPHP开发整站(4)
今晚继续我的这个项目的开发,今晚也是写的不多,主要写了一个菜单管理功能的CURD方法,前端界面还没有进行编写. 菜单管理Model层的代码: <?php namespace Common\Mod ...
- IOS开发之UITabBarController与UINavigationController混合使用
ios开发中UITabBarController与UINavigationController混合使用是很多app的基础页面结构,下面是简单的的页面初始化的方法,在AppDelegate.m的 - ( ...
- JavaScript入门(一)
一.什么叫JS? 1.JS的概念JS,是JavaScript的缩写形式,而JavaScript是一种基于对象和事件驱动并且具有相对安全性的客户端脚本语言.它在Web开发中JS占据着举足轻重的地位,所有 ...
- Matlab学习笔记(1)
在帮助文档中查看学习视频的时候.出现以下对话框,不能查看 解决办法: 在系统环境变量中找到Path.添加“%SystemRoot%\system32;%SystemRoot%;%SystemRoo ...