码云分布式之 Brzo 服务器
摘要: 码云是国内最大的代码托管平台,为了支持更大的用户规模,开发团队也在对一些组件进行大规模的重构.
前言
码云是国内最大的代码托管平台。码云基于 Gitlab 5.5 开发,经过几年的开发已经和官方的 Gitlab 有了很大的不同。 为了支撑更大的用户规模,码云也在不断的改进,而本文也主要分享码云分布式 Brzo GIT HTTP 服务器的开发经验。
码云分布式概述
自码云研发分布式以来,其分布式方案也发生了几次演。在 2014 年,码云(当时的 GIT@OSC ) 出现了高速的增长, 用户和项目越来越多,在旧的方案中,多个机器通过 NFS 挂载在前端服务器上,用户对仓库的读写和网页浏览最终都是在前端服务器上被处理, 这样的机制容易带来严重的性能问题,第一是 IO 与计算 过于集中,第二是 NFS 带来了巨大的内网流量。
团队决定使用 Ceph FS,当服务最终迁移到 Ceph 上时,迁移成功一天之后就出现了严重的宕机事故,经过研究发现, Git 存储库具有海量的小文件,海量小文件一直是分布式文件系统的难题,并且当时 Ceph 也并未完善,我们不得不回退到旧的 NFS 方案。 码云的分布式由此被提出到议程。
码云分布式研发之初,最先被提出的方案是也就是直接使用分布式 RPC, 然而开发团队并未对 Git 版本控制软件的基础特性有深入的研究, 并且团队也缺乏基础服务开发人员,基于 RPC 的分布式方案也就只限于我的 demo 之中。
后来有团队成员提出了使用 NGINX 动态代理,通过解析请求的 URL,将与存储相关的请求代理到存储服务器上,然后, 存储服务器上的 Gitlab 对请求进行解析。这样一来, 浏览器访问,以及 Git 的 HTTP 协议 clone 都能够分发到各个存储服务器上。 这个时候剩下的就是如何实现 NGINX 动态代理了,由于我实现 git 的 svn 接入时有过 NGINX 模块开发经验,所以就被安排到 NGINX 动态代理模块的开发, 以及路由模块的开发。路由策略一开始直接使用Gitlab 的 Magic Path 策略,即取用户的前两个字符 (A~Z|a~z|0~9|-_) 等, 不同的 Magic Path 对应不同的内网 IP。后来改为在 MySQL 中存储用户的仓库所在的机器内网 IP,并将 IP 缓存到 Redis 中,独立存储。 路由模块自主的向 Redis~MySQL 读取路由,当从 MySQL 中也找不到存储机器时,才返回错误。对于存储库无关的 URL 请求, 随机分发到不同的存储服务器上。先后有 zouqilin 和 lowkey2046 参与开发。
NGINX 的动态代理方案中,其模块开发也经历了基于 NDK 旧版路由,NDK 新版路由,以及 Upstream 新版路由的演进。 目前已经稳定运行,其中不乏企业用户的私有化部署。
对于 SSH 协议方案的分布式支持,最初采用的是 zouqilin 的意见: 使用端口转发。 gitlab-shell 只需要少量修改就能支持了 SSH 端口转发。 这个时候,唯一没有分布式支持的就是 svn 协议,作为 svn 兼容实现的开发者,我在接受 svn 分布式任务后,使用 Boost.Asio 开发实现了 svnsrv 动态代理服务器,经过一些波折,svnsrv 服务器也逐渐稳定下来。
在今年初,我研究 Git 协议后,开发了 git-srv 服务器,这个服务器接受一些参数,然后启动 git 传输命令 (这些命令有 git-upload-pack git-receive-pack git-upload-archive) ,将接收的网络数据写到命令的标准输入,将命令的标准输出, 标准错误通过网络发送给客户端。基于 git-srv ,实现了 hook 的 git-upload-pack,git-receive-pack,git-upload-archive。 在这些命令启动时, 会加载 CratosMini 路由库,自动连接到对应的存储服务器上的 git-srv, Git 的 Git 协议和 HTTP 协议以及 SSH 协议 操作都可以通过这些命令支持分布式。 然而这个方案需要频繁的启动进程,并不是非常高效。后来便开始开发 Miracle(SSHD),Mixture,Hover 这些项目。当然 SSH 方案也有新的 SSHD 取代。
Sshd (ssh://) 基于 libssh 开发,减少了 ssh 连接过程的进程创建次数,直接与 git-srv 通信。 而 Github 实际上也是使用 libssh 开发的服务器。 Mixture (git-daemon git://) 基于 Boost.Asio 开发,是 git 协议分布式动态服务器,直接与 git-srv 通信。 Hover (Brzo http://) 基于 Boost.Asio 开发,是 HTTP 协议服务器,直接与 git-srv 通信。 Aton 基于 Crow 开发,是监听服务器,将机器上的服务信息以及机器信息输出成 JSON 格式,返回给管理员。
这些服务的实现,使得码云整个架构变得清晰起来。也能够支撑更大的用户规模,更好的横向扩展。
Brzo 架构与实现
Brzo 是码云分布式架构的重要组件,它实现了 Git HTTP 协议的分布式,在 Git 的网络协议中,HTTPS 流量占据了很大一部分。 在码云团队实现了 SSH, GIT 协议的分布式,以及存储机器上的分布式基础服务 (git-srv) 后, Git HTTP 分布式的改造也提上日程。
Git 的 HTTP 协议可以分为哑协议和智能协议,哑协议就是通过 GET 获取到存储库中的引用和包文件。这个不需要在服务器上安装 git 就可以访问, 目前,包括码云在内的代码托管平台基本上都不支持哑协议。 另一类协议是智能协议,使用 HTTP 请求,方式描述如下:
Git clone 或者 fetch 操作:
- GET /pathto/repo.git/info/refs?service=git-upload-pack
- POST /pathto/repo.git/git-upload-pack
Git push 操作:
- GET /pathto/repo.git/info/refs/service=git-receive-pack
- POST /pathto/repo.git/git-receive-pack
GET 拿到的是服务器的引用列表和支持的操作, POST 在 clone 时 推送需要的引用,返回远程库打包的 pack 文件,POST 在 push 时, 推送本地仓库与服务器引用差异的打包,返回服务器解包的结果,这些都是动态生成的。 了解到 GIT 的 HTTP 协议原理, 才能更好的实现 GIT 的 HTTP 协议分布式服务器。
在项目初期,我曾经使用 .Net Core 实现过 Brzo 同等功能的服务器,在 Linux 上正常运行,由于团队没有 C# 使用经验, 项目可能无法维护,于是 C# 版也就没有继续开发了,仅仅是个实验性项目。
码云的基础服务主要是使用 C++开发,在使用 C++ 的过程中,虽然 C++ 标准没有添加网络库,但是有许多第三网络库可以被开发者使用, 操作系统提供的 API 也能直接被 C++ 项目使用( 比如在 Windows 系统,如果开发 HTTP 服务器,可以直接使用HTTP.sys 提供的 API, 这个是经过内核优化,再使用 RIO 优化 ,效率一骑绝尘)。
在选择第三方库时,却苦于这些第三方 HTTP 库并不一定适合服务场景,比如 Microsoft 开源的 cpprestsdk,基于 HTTP.sys ( Linux 是 Boost.Asio ), 支持 Linux,还专门实现了 Parallel Patterns Library,使用体验和 C# await 类似, 而 Brzo 需要动态代理到存储服务器, 并且需要针对 Git 的特殊场景进行优化。 Brzo 需要支持 Git 的 智能协议,并且与 git-srv 通讯,cpprestsdk 在实现这些功能时显得麻烦并且低效, 后来我还使用过 Boost。HTTP 库,发现并不是很合适,鉴于 HTTP 1.1协议比较简单,我在使用 CURL(WinHTTP) 实现 HttpRequest 时, 曾经做过一些简单解析,于是我干脆直接基于 Boost.Asio 实现 HTTP 协议服务器 Brzo。 Brzo 被设计为一个针对 GIT HTTP 协议优化的服务器, 需要支持 HTTP 1.1, 支持 Chunked Encoding,支持 GZip 解析(可以不支持 GZip 响应); 由于 Brzo 可能与 NGINX 一同运行在同一前端机器, 支持 Unix domain socket 能够优化反向代理效率,故而 Brzo 添加了 Unix domain socket 支持。
HTTP 协议解析
要获取 HTTP 协议全文,可以访问: RFC7230, RFC7231 ,RFC7232 RFC7233, RFC7234, RFC7235, RFC7236,RFC7237。
除此之外,还可以阅读 《HTTP 权威指南》。
Git 的 HTTP 协议是 HTTP 协议的真子集,当 GIT 使用哑协议访问远程仓库时,就是纯粹的 GET 请求,请求的资源都是静态的存在在远程服务器上, 面对这种请求, NGINX 开启 sendfile 就能很好的支持。 当 GIT 使用智能协议访问远程仓库时,情况变得稍微复杂,请求分为 GET 和 POST, 然后头部的一些字段的属性需要符合 GIT 的规范,比如 Content-Type。并且请求体也可能是动态生成的,这个时候就是 chunked 编码了。
了解了 GIT 的 HTTP 协议,如果要针对 GIT 实现 HTTP 服务器,首先要解析头部,然后请求体解析需要支持解析 gzip,以及 chunked 编码, 由于 git 不会同时使用chunked+gzip 编码,所以这一点可以忽略。然后就是生成 chunked 编码。 HTTP 1.1协议要支持 KeepAlive, 所以 Brzo 还要支持 KeepAlive。
HTTP KeepAlive 策略
KeepAlive 的实现简单来说就是打开 socket 后,处理完流程后,服务端 socket 并不主动断开,而是设置超时,超时时间内,有新的连接就继续处理, 重设定时器。如果超时时间过后仍然没有新的连接,就关闭 socket。
如果使用 Session 来描述整个 HTTP 处理流程, 处理完成后重置 Session,继续等待请求即可。如图:
如果是客户端如下图:
图片来自于 HTTP Keepalive Connections and Web Performance
Chunked 编码
在网络的世界里,有些资源是静态的,大小可期的,使用 HTTP 请求获取文件时,Content-Length 就能够拿到大小,从而按照大小将数据全部读取, 然而,还有很多资源是动态生成的,而 GIT 的 HTTP 协议,Push 的 POST 操作的请求体是 send-pack 的标准输出, 这个大小只能边读取边计算。 所以这个时候的请求体就是 chunked 编码。在服务器上,无论是 fetch 还是 push 操作,都是 git-upload-pack (git-receive-pack) 的标准输出, 这个时候响应体也是 chunked 编码。
Chunked 编码的 BNF 格式描述如下:
Chunked-Body = *chunk
last-chunk
trailer
CRLF
chunk = chunk-size [ chunk-extension ] CRLF
chunk-data CRLF
chunk-size = 1*HEX
last-chunk = 1*("0") [ chunk-extension ] CRLF
chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)
trailer = *(entity-header CRLF)
在使用 Boost.Asio 实现 HTTP 协议时,遇到 Chunked 编码的第一选择是使用 boost::asio::streambuf 配合 boost::asio::async_read_until 先读取 chunk-size 然后读取 chunk-data,cpprestsdk 正是使用 streambuf 解析 chunked-encoding, async_read_until 先读取一定长度的数据, 如果存在 CRLF 就返回,不存在就继续度, async_read_until 内部使用 boost::regex 实现。 出于内存分配和读取效率上的考量, Brzo 使用固定长度缓冲区,并且封装了一个 chunked 解析状态机:
static const int8_t unhex[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8,
9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
class ChunkedsizeImpl {
public:
enum State {
StatusClear, ////
RequireInput,
ChunkedLengthOK
};
void Reset() {
state_ = StatusClear;
offset_ = 0;
chklen_ = 0;
}
int ChunkedsizeEx(const char *data, size_t datalen){
switch (state_) {
case StatusClear:
break;
case RequireInput:
offset_ = 0;
break;
case ChunkedLengthOK:
offset_ = 0;
chklen_ = 0;
break;
}
state_ = RequireInput;
for (size_t i = 0; i < datalen; i++) {
uint8_t c = static_cast<uint8_t>(data[i]);
switch (c) {
case '\r':
break;
case '\n': {
offset_ = offset_ + i + 1;
state_ = ChunkedLengthOK;
return 1;
}
default:
int8_t c2 = unhex[c];
if (c2 == -1) {
return -1;
}
chklen_ *= 16;
chklen_ += c2;
break;
}
}
return 0;
}
std::size_t Chklen() const { return chklen_; }
std::size_t Offset() const { return offset_; }
private:
State state_{StatusClear};
std::size_t offset_{0};
std::size_t chklen_{0}; //// FM
};
ChunkedsizeEx 负责解析,根据返回值的不同,Session 将决定写 chunk 到后端还是继续读取。这样虽然使编码变得复杂,当减少内存分配, 降低拷贝。这对于长期运行的服务器来说,是非常重要的。
生成 chunked 编码时,使用 async_read_some 读取一定大小的数据,准备的缓冲区预留前8个字节,后2字节,共计10字节,读取到数据后, 将数据长度按照 chunk 规定格式写入前8个字节,8字节与缓冲区大小有关,尾部写入 "\r\n"。与存储服务器连接断开时(或者进程关闭输出), 写入 chunk-end 即 "0\r\n\r\n"
这种 chunk 策略,能够减少内存拷贝,降低服务器资源占用。Brzo 的 clone 速度和 GIT 协议服务器 Mixture (git-daemon) 接近。
GZip 编码
Git 客户端在 fetch 的 POST 请求时,发送给客户端的需要的引用列表一般使用 gzip 编码,此时 Content-Encoding 对应的编码是 gzip。
gzip 使用的是 Deflate 算法,要解析 gzip,通常的办法是使用 zlib 来实现。在 我开发 Exile 时, 就使用过 zlib 解析 gzipGZipStream.cpp 当然, GZipStream.cpp 解析的是已知 gzip 编码大小,一次性解析完毕就行,Brzo 解析则是读取多少解析多少, 所以这里使用了类似与 ChunkedsizeImpl 的策略。
class GZipExchange {
public:
GZipExchange();
~GZipExchange();
bool Initialize(char *refbuf, size_t bufsize); ////
bool IsEmpty() const { return stream_.avail_out != 0; }
bool AvailableJoin(const char *buf, size_t len);
ssize_t Decompress();
private:
z_stream stream_;
uint8_t *out_;
std::size_t chunksize_;
bool initialized_{false};
};
其中 Initialize 将缓冲区绑定到 GitZipExchange, 然后 AvailableJoin 就是待解析的 gzip 缓存区, Decompress 就是不断解析,直至 IsEmpty 为真。 在 Session 中, GitZipExchange 变量使用 shared_ptr 包装, KeepAlive 重置时, reset 即可。
Unix domain socket 支持
Brzo 核心版运行在 Linux 上,用户在使用 NGINX, Apache 之类的 HTTP 服务器作为负载均衡服务器时, 可以通过 Unix domain socket 与 Brzo 通信, Brzo 基于 Boost.Asio 开发,在实现对 Unix domain socket 的支持时也就考虑到使用 Boost.Asio 的方案。
Boost.Asio 支持 Unix domain socket 可以使用: boost::asio::local::stream_protocol::socket
而普通的 TCP 使用 boost::asio::ip::tcp::socket, 两个类都拥有相同的读写函数,可以直接使用模板包装一下,就可以支持两种 socket 了。 更多的细节可以查看开源版本源码。
Brzo 开源版本
Brzo 开源版本移除了验证和分布式功能,而 ProcessAsync 取代了后端的 socket。
进程的包装
ProcessAsync 就是进程的包装,将输入输出与对于平台的流 绑定到一起。然后实现 async_read_some async_write_some 这样的函数。
#ifndef PROCESS_HPP
#define PROCESS_HPP
#include <boost/asio.hpp>
#ifdef _WIN32
#include <boost/asio/windows/stream_handle.hpp>
typedef boost::asio::windows::stream_handle stdiostream;
typedef DWORD ProcessId;
#else
#include <boost/asio/posix/stream_descriptor.hpp>
typedef boost::asio::posix::stream_descriptor stdiostream;
typedef pid_t ProcessId;
#endif
class ProcessAsync {
public:
ProcessAsync(boost::asio::io_service &ios) : input_(ios), output_(ios) {}
~ProcessAsync() { ProcessClean(); }
int Execute(int Argc, char **Argv);
void ProcessClean();
boost::asio::io_service &get_io_service() { return input_。get_io_service(); }
/// Write
template <typename ConstBufferSequence, typename WriteHandler>
void async_write_some(const ConstBufferSequence &buffers,
WriteHandler &&handler) {
input_.async_write_some(buffers, handler);
}
//// Read
template <typename MutableBufferSequence, typename ReadHandler>
void async_read_some(const MutableBufferSequence &buffers,
ReadHandler &&handler) {
output_.async_read_some(buffers, handler);
}
////
template <typename ConstBufferSequence, typename WriteHandler>
void async_write(const ConstBufferSequence &buffers, WriteHandler &&handler) {
boost::asio::async_write(input_, buffers, handler);
}
/////
template <typename MutableBufferSequence, typename ReadHandler>
void async_read(const MutableBufferSequence &buffers, ReadHandler &&handler) {
boost::asio::async_read(output_, buffers, handler);
}
void cancel() {
input_.cancel();
output_.cancel();
}
void cancel(boost::system::error_code &ec) {
input_.cancel(ec);
output_.cancel(ec);
}
private:
stdiostream input_;
stdiostream output_;
ProcessId id_{0};
};
#endif
对于不同平台,Execute 函数是重中之重,启动进程,修改输入输出等等。
Windows 命名管道
在 Windows 中,匿名管道不支持端口完成,而对于 boost stream_handle 而言,不支持端口完成意味着不能异步读写, 所以我们需要改造新的管道,实际上匿名管道是命名管道的一种特殊实现,同样的,我们也可以使用命名管道实现支持端口完成的等价匿名管道。
BOOL WINAPI MzCreatePipeEx(OUT LPHANDLE lpReadPipe, OUT LPHANDLE lpWritePipe,
IN LPSECURITY_ATTRIBUTES lpPipeAttributes) {
HANDLE ReadPipeHandle, WritePipeHandle;
DWORD dwError;
WCHAR PipeNameBuffer[MAX_PATH];
//
// Only one valid OpenMode flag - FILE_FLAG_OVERLAPPED
//
auto PipeId = InterlockedIncrement(&ProcessPipeId_);
StringCchPrintfW(PipeNameBuffer, MAX_PATH, L"\\\\.\\Pipe\\Brzo.%08x.%08x",
GetCurrentProcessId(), PipeId);
ReadPipeHandle = CreateNamedPipeW(
PipeNameBuffer, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, // Number of pipes
65536, // Out buffer size
65536, // In buffer size
0, //
lpPipeAttributes);
if (!ReadPipeHandle) {
return FALSE;
}
WritePipeHandle = CreateFileW(
PipeNameBuffer, GENERIC_WRITE,
0, // No sharing
lpPipeAttributes, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED, /// child process input output is wait
NULL // Template file
);
if (INVALID_HANDLE_VALUE == WritePipeHandle) {
dwError = GetLastError();
CloseHandle(ReadPipeHandle);
SetLastError(dwError);
return FALSE;
}
*lpReadPipe = ReadPipeHandle;
*lpWritePipe = WritePipeHandle;
return TRUE;
}
最后
在实现 Brzo 的过程中,遇到诸多难题,在解决这些难题的过程中也收获很多经验。关于 Brzo 的开源版,我们也将适时发布。
备注
- 此处 NDK 是 ngx_devel_kit, nginx development kit
- RIO - Registered Input/Output (RIO) API Extensions
https://my.oschina.net/GIIoOS/blog/784598
码云分布式之 Brzo 服务器的更多相关文章
- Git从码云Clone代码到本地
Git从码云或者Github 克隆代码到本地 1.下载安装Git,傻瓜式下一步下一步即可... 2.配置Git: 2.1.选择你要clone到本地的路径:右键--->$ Git Bash Her ...
- 码云git使用二(从码云git服务器上下载到本地)
假如我们现在已经把项目添加到码云git服务器了. 我们现在需要通过studio工具把码云git服务器上的某个项目下载到本,并且运行. 1.打开码云网页,找到对应项目的git路径. 2.打开studio ...
- 码云git使用一(上传本地项目到码云git服务器上)
主要讲下如果将项目部署到码云git服务器上,然后使用studio导入git项目,修改本地代码后,并同步到码云git上面. 首先:我们在码云上注册账号并登陆.官网(https://git.oschina ...
- vdscode连接git服务器(以码云为例)
准备工作:先下载并安装git客户端 1.在码云或者github上新建项目,获得新建项目的地址,得到一个类似:https://gitee.com/zhangshitongsky/vueTest.git ...
- 宝塔webhook配合码云,本地git push 服务器自动pull
emmmm,这其实是一个很简单的一件事情,但是有很多坑,记录一下 先大概讲一下原理吧,就是每次您 push 代码后,都会给远程 HTTP URL 发送一个 POST 请求 更多说明 » 然后在宝塔这边 ...
- 如何使用Git和码云Git@OSC
1.Git简介 关于Git是什么,阅读博客Git简介 2.Git 基础 Git命令很多,常用命令如下图 Workspace:工作区 Index/Stage :暂存区 Local Repository: ...
- 2017 年度码云新增项目排行榜 TOP 50,为它们打“call”
2017 年度码云新增项目排行榜 TOP 50 正式出炉 !2017 结束了,我们来关注一下这一年里码云上新增的最热门的开源项目吧.此榜单根据 2017 年在码云上新增开源项目的 Watch.Star ...
- git与svn与github与码云的区别
1.git与github(https://www.oschina.net/)的区别 Git(https://git-scm.com/)是一个版本控制工具 github是一个用git做版本控制的项目托管 ...
- 码云 Gitee 云端软件平台学习--GitHub
码云 Gitee http://git.oschina.net/jackjiang/MobileIMSDK http://www.blogjava.net/jb2011/archive/2018/11 ...
随机推荐
- js表格的输出
<html> <head> <title>隔行变色</title> <script type="text/javascript" ...
- java List交集 并集 差集 去重复并集
首先定义两个list List list1 =new ArrayList(); list1.add("); list1.add("); list1.add("); Lis ...
- Mysql 的一些异常解决
一.关于大文件存储 1.利用mysql存储大文件时,异常截图 在配置文件中加上如下一行 2.改完后重启mysql,但是又报如下错误: 解决方案: 我的mysql 是5.6版本,查到网上说要修改配置文件 ...
- Android应用不随手机屏幕旋转的方法
在主配置文件里面.在需要设置的activity项后面加上 android:screenOrientation="portrait",这个activity就保持竖屏显示了:在每个ac ...
- 常用JS验证和函数
下面是我常用一些JS验证和函数,有一些验证我直接写到了对象的属性里面了,可以直接通过对象.方法来调用 //浮点数除法运算 function fdiv(a, b, n) { if (n == undef ...
- Java中多线程的使用!!
简介: 1.要了解多线程,首先我们得先了解进程和线程.那么什么是进程?进程就是一个正在运行的程序分配内存让应用程序能够运行的叫做进程.那么什么又是线程呢?线程:在一个程序中,负责代码的执行 ...
- 基于SSM框架的简易的分页功能——包含maven项目的搭建
新人第一次发帖,有什么不对的地方请多多指教~~ 分页这个功能经常会被使用到,我之前学习的时候找了很多资源,可都看不懂(笨死算了),最后还是在朋友帮助下做出了这个分页.我现在把我所能想到的知识 做了一个 ...
- 10.21_Nutz批量插入顺序,POI,wiki持续关注,POI,SSH,数据库优先
(1)Nutz,dao的批量插入会关注顺序吗? http://www.douban.com/group/topic/64322582/ (2)工作需要优先!!! POI,SSH,数据库管理及plsq ...
- 模板:优先队列(priority_queue)
#include <iostream> #include <cstdio> #include <queue> #include <vector> usi ...
- 【原创】Android开发使用华为手机调试logcat没有应用输出信息
输入 *#*#2846579#*#* 点击project Menu点击后台 1.设置logcat 2. Dump & Log",打开开关"打开Dump & Log& ...