(转)开源项目t-io
石墨文档:https://shimo.im/docs/tHwJJcvKl2AIiCZD/
【课程18】BIO、...AIO.xmind0.4MB
【课程18】t-io简介.xmind0.2MB
【课程18】两个官方例子.xmind0.3MB
【课程18】同步异...阻塞.xmind0.3MB
【课程18预习】百万...t-io.xmind0.3MB
宣传:不仅仅是百万级网络通信框架,让天下没有难开发的网络通信
git地址:https://gitee.com/tywo45/t-io
t-io手册:https://t-io.org/doc/index.html
t-io.pdf3MB
t-io:是一个网络框架,从这一点来说是有点像 netty 的,但 t-io 为常见和网络相关的业务(如 IM、消息推送、RPC、监控)提供了近乎于现成的解决方案,即丰富的编程 API,极大减少业务层的编程难度。
websocket: WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
先来个例子理解一下概念,以银行取款为例:
- 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
- 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
- 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
- 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。
- 一个连接一个线程
在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
优化:弄一个线程池来管理线程。即伪异步阻塞IO
- 一个请求一个线程
NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题:每个客户端请求必须使用一个线程单独来处理。问题在于系统本身对线程总数有一定限制,容易瘫痪。
NIO,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。 也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。
** NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
Java对BIO、NIO、AIO的支持:
- Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
- ChannelContext(通道上下文)
- 每一个 tcp 连接的建立都会产生一个 ChannelContext 对象
- (1)ServerChannelContext
- ChannelContext 的子类,当用 tio 作 tcp 服务器时,业务层接触的是这个类的实例。
- (2)ClientChannelContext
- ChannelContext 的子类,当用 tio 作 tcp 客户端时,业务层接触的是这个类的实例
- GroupContext(服务配置与维护)
- GroupContext 就是用来配置线程池、确定监听端口,维护客户端各种数据等的
- ClientGroupContext
- ServerGroupContext
我们在写 TCP Server 时,都会先选好一个端口以监听客户端连接,再创建 N 组线程池来执行相关的任 务,譬如发送消息、解码数据包、处理数据包等任务,还要维护客户端连接的各种数据,为了和业务互动, 还要把这些客户端连接和各种业务数据绑定起来,譬如把某个客户端绑定到一个群组,绑定到一个 userid, 绑定到一个 token 等。GroupContext 就是用来配置线程池、确定监听端口,维护客户端各种数据等的。
GroupContext 是个抽象类,如果你是用 tio 作 tcp 客户端,那么你需要创建 ClientGroupContext,如 果你是用 tio 作 tcp 服务器,那么你需要创建 ServerGroupContext
- AioHandler(消息处理接口)
- 处理消息的核心接口,它有两个子接口
- ClientAioHandler
- ServerAioHandler
- AioListener(通道监听者)
- 处理事件监听的核心接口,它有两个子接口,
- ClientAioListener
- ServerAioListener
- Packet(应用层数据包)
- TCP 层过来的数据,都会被 tio 要求解码成 Packet 对象,应用都需要继承这个类,从而实现自己的业务 数据包。
用于应用层与传输层的数据传递
传输层在往应用层传递数据时,并不保证每次传递的数据是一个完整的应用层数据包(以 http 协议为 例,就是并不保证应用层收到的数据刚好可以组成一个 http 包),这就是我们经常提到的半包和粘包。传输层只负责传递 byte[]数据,应用层需要自己对 byte[]数据进行解码,以 http 协议为例,就是把 byte[] 解码成 http 协议格式的字符串。
- AioServer(tio 服务端入口类)
- AioClient(tio 客户端入口类)
- ObjWithLock(自带读写锁的对象)
- 是一个自带了一把(读写)锁的普通对象(一般是集合对象),每当要对 这个对象进行同步安全操作(并发下对集合进行遍历或对集合对象进行元素修改删除增加)时,就得用这个 锁。
t-io是基于tcp层协议的一个网络框架,所以在应用层与tcp传输层之间设计到一个数据的编码与解码问题,t-io让我们能自定义数据协议,所以需要我们自己手动去编码解码过程。
- git项目地址
https://gitee.com/tywo45/tio-showcase
git里面有个几个例子,首先我们看helloword的例子:
本例子演示的是一个典型的TCP长连接应用,大体业务简介如下。
- 分为server和client工程,server和client共用common工程
- 服务端和客户端的消息协议比较简单,消息头为4个字节,用以表示消息体的长度,消息体为一个字符串的byte[]
- 服务端先启动,监听6789端口
- 客户端连接到服务端后,会主动向服务器发送一条消息
- 服务器收到消息后会回应一条消息
- 之后,框架层会自动从客户端发心跳到服务器,服务器也会检测心跳有没有超时(这些事都是框架做的,业务层只需要配一个心跳超时参数即可)
- 框架层会在断链后自动重连(这些事都是框架做的,业务层只需要配一个重连配置对象即可)
具体类说明:
- 导入核心包
<dependency>
<groupId>org.t-io</groupId>
<artifactId>tio-core</artifactId>
</dependency>
- HelloServerStarter
构造ServerGroupContext,main方法启动服务
- HelloServerAioHandler
实现ServerAioHandler接口,重写decode,encode,handler方法。
- HelloPacket
继承Packet类。自定义数据包的内容
- Const
常量类,配置ip,端口等信息
- HelloClientStarter
构造clientGroupContext,连接节点,使用信息发送消息
- HelloClientAioHandler
实现ClientAioHandler接口,重写decode,encode,handler方法。
- idea请安装PlantUML intergration插件
客户端与服务端沟通时序图.puml0.8KB
Server端初始化时序图.puml0.6KB
(初始化服务器)
(客户端与服务端通讯流程)
- git项目地址
https://gitee.com/tywo45/tio-showcase
上面讲的是helloword的例子,比较简单,接下来的是showcase的例子,结合实际场景的一个例子。
tio的框架初始化使用过程是一样的。不过因为showcase中涉及到的交互越来越多,因此不能像helloword例子中的HelloPacket只有body这么简单了,我们至少要加上一个type参数(消息类型),这样的话服务器获取到数据包之后再更加type来选择消息处理类,从而拓展系统可用性。
(客户端与服务器沟通流程)
客户端与服务端沟通时序图.puml1.4KB
客户端发起登录操作
- org.tio.examples.showcase.client.ShowcaseClientStarter#processCommand
LoginReqBody loginReqBody = new LoginReqBody();
loginReqBody.setLoginname(loginname);
loginReqBody.setPassword(password);
ShowcasePacket reqPacket = new ShowcasePacket();
#这里指定消息类型
reqPacket.setType(Type.LOGIN_REQ);
reqPacket.setBody(Json.toJson(loginReqBody).getBytes(ShowcasePacket.CHARSET));
Tio.send(clientChannelContext, reqPacket);
- LoginReqBody:登录请求参数封装类,继承BaseBody
- ShowcasePacket:数据包,继承Packet,和ByteBuffer相互转换
- clientChannelContext:连接上下文通道
服务端接受请求操作
- org.tio.examples.showcase.server.ShowcaseServerAioHandler#handler
ShowcasePacket showcasePacket = (ShowcasePacket) packet;
#获取消息类型
Byte type = showcasePacket.getType();
#根据消息类型找到对应的消息处理类
AbsShowcaseBsHandler<?> showcaseBsHandler = handlerMap.get(type);
if (showcaseBsHandler == null) {
log.error("{}, 找不到处理类,type:{}", channelContext, type);
return;
}
#执行消息处理。消息处理类必须继承AbsShowcaseBsHandler
showcaseBsHandler.handler(showcasePacket, channelContext);
- handlerMap:存有当前所有消息处理类的map。数据包中包含消息类型,会根据消息类型获取对应的消息处理类,而这个消息处理类会调用handler()方法处理数据。
- AbsShowcaseBsHandler:消息处理抽象类,继承这个类的处理类会对一种消息类型进行处理,并且一般专门处理一种消息封装类(继承BaseBody的封装类)。
消息类型对应消息处理类的初始化
private static Map<Byte, AbsShowcaseBsHandler<?>> handlerMap = new HashMap<>();
static {
#把消息类型与消息处理类映射起来
handlerMap.put(Type.GROUP_MSG_REQ, new GroupMsgReqHandler());
handlerMap.put(Type.HEART_BEAT_REQ, new HeartbeatReqHandler());
handlerMap.put(Type.JOIN_GROUP_REQ, new JoinGroupReqHandler());
handlerMap.put(Type.LOGIN_REQ, new LoginReqHandler());
handlerMap.put(Type.P2P_REQ, new P2PReqHandler());
}
如果接收到的消息类型是P2P_REQ,那么处理类就是P2PReqHandler:
- org.tio.examples.showcase.server.handler.P2PReqHandler#handler
log.info("收到点对点请求消息:{}", Json.toJson(bsBody));
ShowcaseSessionContext showcaseSessionContext = (ShowcaseSessionContext) channelContext.getAttribute();
P2PRespBody p2pRespBody = new P2PRespBody();
p2pRespBody.setFromUserid(showcaseSessionContext.getUserid());
p2pRespBody.setText(bsBody.getText());
ShowcasePacket respPacket = new ShowcasePacket();
respPacket.setType(Type.P2P_RESP);
respPacket.setBody(Json.toJson(p2pRespBody).getBytes(ShowcasePacket.CHARSET));
Tio.sendToUser(channelContext.groupContext, bsBody.getToUserid(), respPacket);
项目运行:https://github.com/fanpan26/SpringBootLayIM.git
由于layim是付费产品,所以在网络上找了一个。(仅供学习哈)
需要放到项目目录下面
static/js/layui/lay/modules/layim.js
layim.js33.3KB
项目集成:
因为这里需要和浏览器之间进行通讯,所以需要用到websocket机制。tio有集成websocket的框架,所以直接导入即可。
<dependency>
<groupId>org.t-io</groupId>
<artifactId>tio-websocket-server</artifactId>
<version>0.0.5-tio-websocket</version>
</dependency>
代码结构
项目使用Guava的EventBus替代了Spring的ApplicationEvent事件机制。
Guava的EventBus使用介绍如下:
事件定义
EventBus为我们提供了register方法来订阅事件,不需要实现任何的额外接口或者base类,只需要在订阅方法上标注上@Subscribe和保证只有一个输入参数的方法就可以搞定。
new Object() {
@Subscribe
public void lister(Integer integer) {
System.out.printf("%d from int%n", integer);
}
}
事件发布
对于事件源,则可以通过post方法发布事件。 正在这里对于Guava对于事件的发布,是依据上例中订阅方法的方法参数类型决定的,换而言之就是post传入的类型和其基类类型可以收到此事件。
//定义事件
final EventBus eventBus = new EventBus();
//注册事件
eventBus.register(new Object() {
//使用@Subscribe说明订阅事件处理方法
@Subscribe
public void lister(Integer integer) {
System.out.printf("%s from int%n", integer);
}
@Subscribe
public void lister(Number integer) {
System.out.printf("%s from Number%n", integer);
}
@Subscribe
public void lister(Long integer) {
System.out.printf("%s from long%n", integer);
}
});
//发布事件
eventBus.post(1);
eventBus.post(1L);
项目的而运用
主要处理事件:包含了申请通知,添加好友成功通知
关键类:
- com.fyp.layim.common.event.bus.EventUtil:封装了事件的监听注册,以及发布动作
- com.fyp.layim.common.event.bus.body.EventBody:发布的内容封装类,包含消息类型和消息内容字段
- com.fyp.layim.common.event.bus.handler.AbsEventHandler:事件处理抽象类,具体处理器需要继承这个重写handler()方法
- com.fyp.layim.common.event.bus.handler.AddFriendEventHandler:添加好友成功通知处理类。
- com.fyp.layim.common.event.bus.LayimEventType:消息类型常量
调用:
- com.fyp.layim.web.biz.UserController#handleFriendApply:好友同意好友请求之后发布事件
逻辑:
事件的处理其实是给申请人发起好友同意通知
- com.fyp.layim.im.common.util.PushUtil#pushAddFriendMessage
/**
* 添加好友成功之后向对方推送消息
* */
public static void pushAddFriendMessage(long applyid){
if(applyid==0){
return;
}
Apply apply = applyService.getApply(applyid);
ChannelContext channelContext = getChannelContext(""+apply.getUid());
//先判断是否在线,再去查询数据库,减少查询次数
if (channelContext != null && !channelContext.isClosed()) {
LayimToClientAddFriendMsgBody body = new LayimToClientAddFriendMsgBody();
User user = getUserService().getUser(apply.getToid());
if (user==null){return;}
//对方分组ID
body.setGroupid(apply.getGroup());
//当前用户的基本信息,用于调用layim.addList
body.setAvatar(user.getAvatar());
body.setId(user.getId());
body.setSign(user.getSign());
body.setType("friend");
body.setUsername(user.getUserName());
push(channelContext, body);
}
}
最后是通过Aio.send发送消息。
- com.fyp.layim.im.common.util.PushUtil#push
/**
* 服务端主动推送消息
* */
private static void push(ChannelContext channelContext,Object msg) {
try {
WsResponse response = BodyConvert.getInstance().convertToTextResponse(msg);
Aio.send(channelContext, response);
}catch (IOException ex){
}
}
- 登录功能
- 单聊功能
- 群聊功能
- 其他自定义消息提醒功能
- 等等。。。。
登录的目的是过滤非法请求,如果有一个非法用户请求websocket服务,直接返回403或者401即可。
单聊,群聊这个就不用解释了
其他自定义消息提醒,比如:时时加好友消息,广播消息,审核消息等。
前端的框架用到是layim,layim已经帮我们做好了界面和封装了接口,所以我们的工作只需要按照layim来写好接口提供结果就可以了。
首先看初始化:
- templates/index.html
layim.config({
//初始化接口
init: {
url: '/layim/base'
}
//查看群员接口
,members: {
url: '/layim/members'
}
//上传图片接口
,uploadImage: {url: '/upload/file'}
//上传文件接口
,uploadFile: {url: '/upload/file'}
,isAudio: true //开启聊天工具栏音频
,isVideo: true //开启聊天工具栏视频
,initSkin: '5.jpg' //1-5 设置初始背景
,notice: true //是否开启桌面消息提醒,默认false
,msgbox: '/layim/msgbox'
,find: layui.cache.dir + 'css/modules/layim/html/find.html' //发现页面地址,若不开启,剔除该项即可
,chatLog: layui.cache.dir + 'css/modules/layim/html/chatLog.html' //聊天记录页面地址,若不开启,剔除该项即可
});
因为需要进行通讯,所以websocket的信息也需要初始化。都是在layim里面一起初始化的。
socket.config({
log:true,
token:'/layim/token',
server:'ws://127.0.0.1:8888'
});
- templates/layim/msgbox.html
控制器:
com.fyp.layim.web.api.GroupController:群组相关
com.fyp.layim.web.biz.AccountController:账户相关,登录注册
com.fyp.layim.web.biz.UploadController:上传文件、图片
com.fyp.layim.web.biz.UserController:用户基础数据
拦截器:
com.fyp.layim.web.filter.TokenVerifyInterceptor:校验token,与userid转换
com.fyp.layim.web.filter.LayimAspect:给所有请求设置当前用户的值,放session当中
全局异常处理:
com.fyp.layim.web.filter.GlobalExceptionHandler:异常处理
com.fyp.layim.im.common.*:定义消息类型,消息处理类的公共包。
com.fyp.layim.im.packet.*:这是数据包,因为协议是websocket,所以返回的不是packet,最后要返回的其实是符合websocket定义的org.tio.websocket.common.WsResponse。而这些数据是放在body中。
com.fyp.layim.im.server.*:t-io的服务配置相关,包括t-io服务的启动,启动之前需要ServerAioHandler、ServerAioListener、ServerGroupContext等参数。
所以总体逻辑是这样的:
步骤一、配置了springboot的模块自动加载机制。
- META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.fyp.layim.im.server.LayimServerAutoConfig
LayimServerAutoConfig配置将会被自动装载。
- com.fyp.layim.im.server.LayimServerAutoConfig:自动装载的配置类
- com.fyp.layim.im.server.config.LayimServerConfig:服务的ip、端口、心跳时间等基本配置
- com.fyp.layim.im.server.LayimWebsocketStarter:初始化配置,启动t-io服务,其中配置初始化和启动都是委托给com.fyp.layim.im.server.LayimServerStarter完成。
- com.fyp.layim.im.server.LayimServerStarter:根据配置初始化serverGroupContext、初始化消息处理器
//初始化t-io的serverGroupContext
//还有消息处理器与消息类型的映射关系
public LayimServerStarter(LayimServerConfig wsServerConfig, IWsMsgHandler wsMsgHandler, TioUuid tioUuid, SynThreadPoolExecutor tioExecutor, ThreadPoolExecutor groupExecutor) throws Exception {
this.layimServerConfig = wsServerConfig;
this.wsMsgHandler = wsMsgHandler;
layimServerAioHandler = new LayimServerAioHandler(wsServerConfig, wsMsgHandler);
layimServerAioListener = new LayimServerAioListener();
serverGroupContext = new ServerGroupContext(layimServerAioHandler, layimServerAioListener, tioExecutor, groupExecutor);
//心跳时间,暂时设置为0
serverGroupContext.setHeartbeatTimeout(wsServerConfig.getHeartBeatTimeout());
serverGroupContext.setName("Tio Websocket Server for LayIM");
aioServer = new AioServer(serverGroupContext);
serverGroupContext.setTioUuid(tioUuid);
//initSsl(serverGroupContext);
//初始化消息处理器
LayimMsgProcessorManager.init();
}
接下来看下与serverGroupContext相关的消息处理咧、事件监听类
- com.fyp.layim.im.server.handler.LayimServerAioHandler:继承ServerAioHandler,完成消息的解码、编码、消息处理过程
- com.fyp.layim.im.server.listener.LayimServerAioListener:继承ServerAioListener,完成事件监听
绑定用户信息
- com.fyp.layim.im.common.processor.ClientCheckOnlineMsgProcessor#process
SetWithLock<ChannelContext> checkChannelContexts =
Aio.getChannelContextsByUserid(channelContext.getGroupContext(),body.getId());
绑定用户上线信息。
- com.fyp.layim.im.server.handler.LayimMsgHandler#handleHandshakeUserInfo
private HttpResponse handleHandshakeUserInfo(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
UserService userService = getUserService();
//增加token验证方法
String path = httpRequest.getRequestLine().getPath();
String token = URLDecoder.decode(path.substring(1),"utf-8");
String userId = TokenVerify.IsValid(token);
if (userId == null) {
//没有token 未授权
httpResponse.setStatus(HttpResponseStatus.C401);
} else {
long uid = Long.parseLong(userId);
//解析token
LayimContextUserInfo userInfo = userService.getContextUserInfo(uid);
if (userInfo == null) {
//没有找到用户
httpResponse.setStatus(HttpResponseStatus.C404);
} else {
channelContext.setAttribute(userId, userInfo.getContextUser());
//绑定用户ID
Aio.bindUser(channelContext, userId);
//绑定用户群组
List<String> groupIds = userInfo.getGroupIds();
//绑定用户群信息
if (groupIds != null) {
groupIds.forEach(groupId -> Aio.bindGroup(channelContext, groupId));
}
//通知所有好友本人上线了
notify(channelContext,true);
}
}
return httpResponse;
}
Aio.java的api:
实现 IWsMsgHandler接口:用于websocket握手,响应,关闭通道等过程的业务处理。
握手逻辑首先是在:com.fyp.layim.im.server.handler.LayimServerAioHandler中。
wsMsgHandler.handshake 方法,这里一般直接返回默认的 httpReponse即可,代表(框架层)握手成功。但是我们可以在接口中自定义一些业务逻辑,比如用户判断之类的逻辑,然后决定是否同意握手流程。
(转)开源项目t-io的更多相关文章
- 瓣呀,一个基于豆瓣api仿网易云音乐的开源项目
整体采用material design 风格,本人是网易云音乐的粉丝,所以界面模仿了网页云音乐,另外,项目中尽量使用了5.0之后的新控件. 项目整体采用mvp+rxjava+retrofit 框架,使 ...
- 《云阅》一个仿网易云音乐UI,使用Gank.Io及豆瓣Api开发的开源项目
CloudReader 一款基于网易云音乐UI,使用GankIo及豆瓣api开发的符合Google Material Desgin阅读类的开源项目.项目采取的是Retrofit + RxJava + ...
- 微软CMS项目 Orchard 所用到的开源项目
研发了Orchard一年左右了,时常遇到瓶颈,总觉得力不从心,其实并不是基础不够,关键还是概念性的东西太多,一会儿这个概念名词,一会那个,关于Orchard的技术文档也的确很少,每次看起来总是焦头烂额 ...
- [转] Android优秀开源项目
Android经典的开源项目其实非常多,但是国内的博客总是拿着N年前的一篇复制来复制去,实在是不利于新手学习.今天爬爬把自己熟悉的一些开源项目整理起来,希望能对Android开发同学们有所帮助.另外, ...
- .Net 开源项目资源大全
伯乐在线已在 GitHub 上发起「DotNet 资源大全中文版」的整理.欢迎扩散.欢迎加入. https://github.com/jobbole/awesome-dotnet-cn (注:下面用 ...
- 分享我的开源项目-springmore
之前有在博客园分享过springmore,不知道是什么原因,被管理员移除首页 在此郑重声明,这是我个人的开源项目,东西不多,也不存在打广告,也没有什么利益可图 完全是出于分享的目的,望博客园管理员予以 ...
- [转]Android开源项目第二篇——工具库篇
本文为那些不错的Android开源项目第二篇--开发工具库篇,主要介绍常用的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容.多 ...
- 2015-2016最火的Android开源项目--github开源项目集锦(不看你就out了)
标签: Android开发开源项目最火Android项目github 2015-2016最火的Android开源项目 本文整理与集结了近期github上使用最广泛最火热与最流行的开源项目,想要充电与提 ...
- swift开源项目精选
Swift 开源项目精选-v1.0 2016-03-07 22:11 542人阅读 评论(0) 收藏 举报 分类: iOS(55) Swift(4) 目录(?)[+] 转自 http: ...
- 开源项目大全 >> ...
http://www.isenhao.com/xueke/jisuanji/kaiyuan.php 监控系统-Nagios 网络流量监测图形分析工具-Cacti 分布式系统监视-zabbix 系统 ...
随机推荐
- HTTP方法之GET与POST对比
超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信.最常用的是GET与POST 1.GET方法: 查询字符串(键/值对)是在GET请求的URL中发送的. /test.php?a=val ...
- aic bic mdl
https://blog.csdn.net/xianlingmao/article/details/7891277 https://blog.csdn.net/lfdanding/article/de ...
- jdk和jre区别
- 仿照admin实现一个自定义的增删改查的组件
1.首先,创建三个项目,app01,app02,stark,在settings里边记得配置.然后举例:在app01的model里边写表,用的db.sqlite3,所以数据库不用再settings里边配 ...
- python seek()方法报错:“io.UnsupportedOperation: can't do nonzero cur-relative seeks”
今天使用seek()时报错了, 看下图 然后就百度了一下,找到了解决方法 这篇博客https://www.cnblogs.com/xisheng/p/7636736.html 帮忙解决了问题, 照理说 ...
- python单下划线与双下划线的区别
Python 用下划线作为变量前缀和后缀指定特殊变量. _xxx 不能用'from moduleimport *'导入 __xxx__ 系统定义名字 __xxx 类中的私有变量名 核心风格:避免用下划 ...
- 前端规范--eslint standard
https://github.com/standard/standard/blob/master/docs/RULES-zhcn.md
- Chrome浏览器相关细节整理
一.上传文件卡死 可能时由于输入法的原因导致上传文件浏览器卡死.将输入法改为英文模式再操作上传文件就不会卡死了.
- [转载]oracle树形查询 start with connect by
一.简介 在oracle中start with connect by (prior) 用来对树形结构的数据进行查询.其中start with conditon 给出的是数据搜索范围, connect ...
- Prometheus监控学习笔记之prometheus的federation机制
0x00 概述 有时候对于一个公司,k8s集群或是所谓的caas只是整个技术体系的一部分,往往这个时候监控系统不仅仅要k8s集群以及k8s中部署的应用,而且要监控传统部署的项目.也就是说整个监控系统不 ...