Giter Site home page Giter Site logo

blog's People

Contributors

z-950 avatar

Watchers

 avatar

blog's Issues

windows 10 禁用更新方法(可恢复)

禁用

  1. 在服务中,禁用以下三个服务:

    1. Windows Update
    2. Windows Update Medic Service
    3. 更新Orchestrator服务
      提示无权限禁用:使用注册表进行禁用。定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc,右侧找到Start键,右键点击修改,将数值2改为4。找到FailureActions键,右键点击修改,修改该键的二进制数据,分别将00100018行的左起第5个数值选中,由原来的01改为00
  2. C:\Windows\System32中找到WaaSMedicAgent.exe将其重命名为WaaSMedicAgent-fuck.exe,然后在桌面创建一个.txt文件,修改名字为WaaSMedicAgent.exe,并复制进C:\Windows\System32
    重命名时提示无权限:右键该文件,点击属性->安全->高级,在“所有者”一行点击更改,然后点击高级,再选择立刻查找。在搜索结果里选择符合本机用户的对象,点击所有确定。 然后再次右键该文件,点击属性->安全->编辑,选择刚才添加的用户,勾选完全控制,点击所有确定即可。

恢复更新

逆向操作:恢复服务,恢复注册表,恢复文件。然后重启。

为什么不用hexo做博客

常见的hexo建站,使用两个branch,分别放原始md文件和生成的文件。常用vscode进行文章编写。

hexo作为blog的缺点

文章管理困难

本地

  1. 发布的文章只能放在同一个目录下,无法利用目录进行分类
  2. 在1的前提下,使用vscode的资源管理器无法很好的按标题查找文章(目前只有关键字高亮,但更方便的应该是过滤,应该能在两种模式间切换)
  3. 在2的前提下,可以使用本地资源管理器或者everything进行按标题查找,但是切换窗口也算麻烦之一
  4. 无法提供更高级的查找方式,比如按标签查找

其他方式

  1. hexo样式自带按目录、标签搜索(我使用的就有),但还是需要到本地再进行查找
  2. 使用hexo-admin插件,但一样无法按标题、标签查找

原始文件丢失

在只使用一个branch放生成的文件时会发送。因为大部分教程实际上没有提及使用两个branch。

配置繁琐

对于喜欢折腾的人来说不算什么,起码比自己搭建后端甚至写博客界面简单多了。但相对于现成的issue,还是比较麻烦。

文章更新问题

如果文章中有更新,只能手动写明,甚至更改文章日期,没有其他信息。issue虽然也只保留了edited的时间戳,但勉强能用。

使用issue的动机

首先是整理自己所学的技术和知识。此前由于觉得博客毕竟是给人看的,总不能写的太随意。现在想来,大抵只有自己会看,所以无需顾虑太多,该写清楚的还是需要写清楚。

此前记录笔记都是通过网页收藏。近来复习时发现有的文章失效了,大致也算理解了csdn那种抄来抄去的情况了。

对于自己的技术总结,也基本上是在闲聊时完成。聊天式的口述总归不严谨,还是写文章来的通达,并且还能回顾与改正。

优点

自带标签与按标签搜索、无需考虑配置问题、无需考虑存储问题、文章管理更为方便。

利用github写文章其他方式

我见过有人说写md文档直接存进repo里的,实际上这种做法和hexo类似,并且由于没有别人提供的样式,具有的功能反而更少一些(文件没有办法添加标签,自然也没有按标签搜索)。

附上其他更常见的方式:利用GitHub写博客的几种方式

日历组件

这本是一道笔试题。最后差两个判断没有修改。
其中比较少见的是,使用new Date(2020, 1, 0).getDate()可以获取2020年1月的总天数
代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    table.calendar {
      font-size: 14px;
      border-collapse: collapse;
      width: 100%;
      table-layout: fixed;
    }

    table.calendar th {
      background: #f5f5f5;
      color: #999;
    }

    table.calendar th,
    table.calendar td {
      border: 1px solid #e1e1e1;
      padding: 0;
      height: 32px;
      line-height: 32px;
      text-align: center;
    }

    table.calendar td.current {
      background: #1890ff;
      color: #fff;
    }

    table.calendar th.pre,
    table.calendar th.next {
      color: #1890ff;
      cursor: pointer;
    }

    table.calendar th.date {
      color: #000;
    }
  </style>
</head>

<body>
  <button type="button" onclick="calendar(document.getElementById('jsContainer'), 2020, 1)">button</button>
  <div id="jsContainer">
  </div>
  <script>
    function calendar(container, year, month) {
      this.year = year;
      this.month = month;
      this.html = html;
      this.el = document.createElement("table"); //TODO: 创建分页组件根节点
      if (!el) return;
      this.el.className = 'calendar';
      this.el.innerHTML = this.html();
      container.appendChild(this.el);

      this.el.addEventListener('click', event => {
        var el = event.target;
        var isPre = el.classList.contains('pre');
        var isNext = el.classList.contains('next');
        if (!isPre && !isNext) return;
        if (isPre) {
          //TODO: 更新this.month和this.year
          if (this.month !== 1) {
            this.month--
          } else {
            this.year--
            this.month = 12
          }
        } else {
          //TODO: 更新this.month和this.year
          if (this.month !== 12) {
            this.month++
          } else {
            this.year++
            this.month = 1
          }
        }
        this.el.innerHTML = this.html();
      });

      function html() {
        var date = new Date();
        var year = +this.year || 0;
        var month = (+this.month || 0) - 1;
        var day = date.getDate();
        //TODO: 生成组件的内部html字符串
        let days = new Date(year, month + 1, 0).getDate()
        let startAt = new Date(year, month).getDay() - 1
        if (startAt === -1) startAt = 6
        const lines = Math.ceil((days + startAt) / 7)
        let endAt = (days + startAt) % 7
        if (endAt === 0) endAt = 7

        const couldBeCurrent = month === date.getMonth() && year === date.getFullYear()

        let i = 1
        const getDateTd = () => {
          const current = i
          ++i
          if (couldBeCurrent && current == day) {
            return `<td class="current">${current}</td>`
          } else {
            return `<td>${current}</td>`
          }
        }
        const getDateTdList = (n) => {
          let tdList = ''
          while (n--) {
            tdList += getDateTd()
          }
          return tdList
        }

        let html = `<thead>
                        <tr><th class="pre"><</th><th colspan="5" class="date">${this.year}.${this.month < 10 ? '0' : ''}${this.month}</th><th class="next">></th></tr>
                        <tr><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th><th>日</th></tr>
                    </thead>
                    <tbody>`
        html += `<tr>${'<td></td>'.repeat(startAt)}${getDateTdList(7 - startAt)}</tr>`
        let count = lines - 2
        while (count--) {
          html += `<tr>${getDateTdList(7)}</tr>`
        }
        html += `<tr>${getDateTdList(endAt)}${'<td></td>'.repeat(7 - endAt)}</tr>`
        html += '</tbody>'
        return html;
      }
    }
  </script>
</body>
</html>

性能相关

环境:Windows 10 64bit,16G

浏览器单个tag最大内存占用

结果

  • Chrome(v85):4G
  • Edge(v44):5G

测试代码

使用js增加内存占用

点击展开
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      height: 30vh;
    }
  </style>
</head>

<body>
  <div>div1</div>
  <div>div2</div>
  <div>div3</div>
  <div>div4</div>
  <div>div5</div>
  <div>div6</div>
  <div>div7</div>
  <div>div8</div>
  <div>div9</div>
  <div>div10</div>
  <script>
    const arr = []
    const n = 1 * 1000 * 1000
    let count = 0

    function increace() {
      setTimeout(() => {
        arr.push(new Array(n).fill("x"))

        count += n

        increace()
      }, 100);
    }

    window.onload = () => {
      debugger
      increace()
    }
  </script>
</body>

</html>

统计方式

  • Chrome:按shift+esc查看内存占用,页面崩溃时的值
  • Edge:使用系统的任务管理器查看内存占用,页面自动刷新前的最大值

其他参考资料

  1. https://js9.si.edu/js9/help/memory.html
  2. https://stackoverflow.com/questions/2936782/javascript-memory-limit
  3. https://stackoverflow.com/questions/6044147/ios-memory-allocation-how-much-memory-can-be-used-in-an-application

总结

  1. Chrome可以用window.performance.memory.jsHeapSizeLimit查看最大内存
  2. 如果考虑IOS,则假定最大内存为500MB比较合适

Vert.x中不能使用Kotlin重写Launcher类

环境:openJDK 13、Kotlin 1.3.72、Vert.x 3.9.x

遇到需要重写Launcher进行固定配置的情况,参考官方例子

由于个人主要使用Kotlin进行编写,所以重写Launcher类自然也会用Kotlin。下面介绍遇到的问题。

main函数的编写

对于Kotlin,main可以单独定义,也可以使用伴生对象在类里面定义。

Launcher类是启动类,所以Vert.x的Launcher类中定义了main方法。重写时,也需要定义自己main方法。

自己的Launcher写在MyLauncher.kt中。

使用伴生对象的方式定义main函数

class MyLauncher : io.vertx.core.Launcher() {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            MyLauncher().dispatch(args)
        }
    }
}

上面的写法会报错:Accidental override: The following declarations have the same JVM signature (main([Ljava/lang/String;)V)

经过搜索,了解到可以通过@JvmName注解改变重写的方法的名字来解决该错误,但此为main函数,不可修改名字,故无解。

单独定义main函数

fun main(args: Array<String>) {
    MyLauncher().dispatch(args)
}

class MyLauncher : io.vertx.core.Launcher() {}

不会报错。来看一下能否运行。

配置build.gradle的mainClass为自己的Launcher:vertx { launcher = "com.example.starter.MyLauncher" }

此处使用了io.vertx.vertx-plugin这个插件。只用java插件时,应该配置mainClassName字段

编译并运行。报错:java.lang.ClassNotFoundException: com.example.starter.Launcher

看一下build/classes里面有什么:

+-- com.example.starter
    +-- MyLauncherKt.class
    +-- MyLauncher.class

具体进去看一下,发现main方法放到了MyLauncher.class里。修改一下mainClass的位置:vertx { launcher = "com.example.starter.MyLauncherKt" }。再次编译运行,没有报错。

单独定义main函数可行吗

答案是不可行,标题已经说明了。

至于为什么不可行,还是出于方便角度考虑的。

mainVerticle设置

对于Vert.x,一个jar里还得包括mainVerticle用于启动,mainVerticle中才是应用的启动逻辑。

mainVerticle可以在build.gradle里设置,放到manifest里。这样我们可以直接运行jar,否则还得传入mainVerticle参数。

首先说明,传入mainVerticle的麻烦之处(对于微服务架构):

  1. 使用ide进行调试时,每个微服务启动的task都需要再传入mainVerticle参数
  2. mainVerticle名字修改后,需要手动修改传入的参数

手动配置参数是容易忘记的,毫无意义,不如将其自动化方便。

mainVerticle如何被获取

dispatch所在的类io.vertx.core.impl.launcher.VertxCommandLauncher中,有如下函数:

  private String getFromManifest(String key) {
    try {
      Enumeration<URL> resources = RunCommand.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
      while (resources.hasMoreElements()) {
        try (InputStream stream = resources.nextElement().openStream()) {
          Manifest manifest = new Manifest(stream);
          Attributes attributes = manifest.getMainAttributes();
          String mainClass = attributes.getValue("Main-Class");
          if (main.getClass().getName().equals(mainClass)) {
            String value = attributes.getValue(key);
            if (value != null) {
              return value;
            }
          }
        }
      }
    } catch (IOException e) {
      throw new IllegalStateException(e.getMessage());
    }
    return null;
  }

其中attributes.getValue("Main-Class")获取到的是build.gradle中配置的mainClass,也就是MyLauncherKt

main.getClass().getName()获取到的则是运行时的Launcher的class,也就是MyLauncher

所以,单独定义main函数,并且配置正确的情况下,Vert.x的Launcher是无法获取到manifest中的mainVerticle的。

改变main函数编译后的文件名

目前找不到此做法。

如果使用@file:JvmName("MyLauncher")强制输出为MyLauncher.class,则会报错:Duplicate JVM class name 'com/example/starter/MyLauncher' generated from: package-fragment com.example.starter, MyLauncher

修改为@file:JvmName("Launcher")可以解决此错误,但Launcher.class里只有main函数,并且MyLauncher类依然在MyLauncher.class里,相当于没用。

总结

综上,解决办法是,使用Java重写Launcher和其中的main函数。

看到有资料说main函数无法被重写(override),实践了一下,其实是可以的。

Signal协议 js使用

signal协议是一种棘轮式前向保密协议,适用于同步和异步消息传递环境。

signal协议的js实现:libsignal-protocol-javascript

注意

此为例子。该js库的实现不完整,缺少了原始版本的部分方法,见issue

概念

  • preKeys:ECPublicKey,唯一id
  • session:通信方式,两种:
    1. PreKeyBundles. 希望向接收者发送消息的客户端可以通过从服务器检索该接收者的PreKeyBundle来建立会话。
    2. PreKeySignalMessages. 客户端可以从收件人接收PreKeySignalMessage并使用它来建立会话

    一旦建立会话,就没有必要解除会话。

  • state:session期间client保存了大量state(持久化保存):
    • Identity State
    • PreKey State
    • Signed PreKey States
    • Session State

使用

引入dist/libsignal-protocol.js

  1. 装载:生成所需的identity keys, registration id, prekeys
const KeyHelper = libsignal.KeyHelper;

const registrationId = KeyHelper.generateRegistrationId();
// Store registrationId somewhere durable and safe.
// store需要自行实现,可参考最后的例子
// 可以储存在浏览器的localStorage或者indexDB。如果浏览器不安全则毫无办法
// 为2.建立会话中同一个store

KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) {
    // keyPair -> { pubKey: ArrayBuffer, privKey: ArrayBuffer }
    // keyPair格式
    // Store identityKeyPair somewhere durable and safe.
});

KeyHelper.generatePreKey(keyId).then(function(preKey) {
    store.storePreKey(preKey.keyId, preKey.keyPair);
});

KeyHelper.generateSignedPreKey(identityKeyPair, keyId).then(function(signedPreKey) {
    store.storeSignedPreKey(signedPreKey.keyId, signedPreKey.keyPair);
});

// Register preKeys and signedPreKey with the server
// 向服务器注册,服务器有signal相关接口.
// 应为2.建立会话
  1. 建立会话
// store需要自行实现,可参考最后的
const store   = new MySignalProtocolStore(); // 储存identity, prekeys, signed prekeys, and session state
const address = new libsignal.SignalProtocolAddress(recipientId, deviceId); // 目标地址

// Instantiate a SessionBuilder for a remote recipientId + deviceId tuple.
const sessionBuilder = new libsignal.SessionBuilder(store, address);

// Process a prekey fetched from the server. Returns a promise that resolves
// once a session is created and saved in the store, or rejects if the
// identityKey differs from a previously seen identity for this address.
// 将载入时生成的id和key传入以建立链接
const promise = sessionBuilder.processPreKey({
    registrationId: <Number>,
    identityKey: <ArrayBuffer>,
    signedPreKey: {
        keyId     : <Number>,
        publicKey : <ArrayBuffer>,
        signature : <ArrayBuffer>
    },
    preKey: {
        keyId     : <Number>,
        publicKey : <ArrayBuffer>
    }
});

promise.then(function onsuccess() {
  // encrypt messages
});

promise.catch(function onerror(error) {
  // handle identity key conflict
});
  1. 加密
const plaintext = "Hello world"; // 要发送的原信息
const sessionCipher = new libsignal.SessionCipher(store, address); // 加密解密接口
sessionCipher.encrypt(plaintext).then(function(ciphertext) {
    // ciphertext -> { type: <Number>, body: <string> }
    handle(ciphertext.type, ciphertext.body);
});
  1. 解密
// 新建会话解密PreKeyWhisperMessage
const sessionCipher = new SessionCipher(store, address);
// Decrypt a PreKeyWhisperMessage by first establishing a new session.
// Returns a promise that resolves when the message is decrypted or
// rejects if the identityKey differs from a previously seen identity for this
// address.
sessionCipher.decryptPreKeyWhisperMessage(ciphertext).then(function(plaintext) {
    // handle plaintext ArrayBuffer
}).catch(function(error) {
    // handle identity key conflict
});

// 使用现有会话解密WhisperMessage
// Decrypt a normal message using an existing session
const sessionCipher = new SessionCipher(store, address);
sessionCipher.decryptWhisperMessage(ciphertext).then(function(plaintext) {
    // handle plaintext ArrayBuffer
});

stroe示例,储存在memory中。但规范要求持久化(durable)储存。

此示例来自该js库的test

function SignalProtocolStore() {
  this.store = {};
}

SignalProtocolStore.prototype = {
  Direction: {
    SENDING: 1,
    RECEIVING: 2,
  },

  getIdentityKeyPair: function() {
    return Promise.resolve(this.get('identityKey'));
  },
  getLocalRegistrationId: function() {
    return Promise.resolve(this.get('registrationId'));
  },
  put: function(key, value) {
    if (key === undefined || value === undefined || key === null || value === null)
      throw new Error("Tried to store undefined/null");
    this.store[key] = value;
  },
  get: function(key, defaultValue) {
    if (key === null || key === undefined)
      throw new Error("Tried to get value for undefined/null key");
    if (key in this.store) {
      return this.store[key];
    } else {
      return defaultValue;
    }
  },
  remove: function(key) {
    if (key === null || key === undefined)
      throw new Error("Tried to remove value for undefined/null key");
    delete this.store[key];
  },

  isTrustedIdentity: function(identifier, identityKey, direction) {
    if (identifier === null || identifier === undefined) {
      throw new Error("tried to check identity key for undefined/null key");
    }
    if (!(identityKey instanceof ArrayBuffer)) {
      throw new Error("Expected identityKey to be an ArrayBuffer");
    }
    var trusted = this.get('identityKey' + identifier);
    if (trusted === undefined) {
      return Promise.resolve(true);
    }
    return Promise.resolve(util.toString(identityKey) === util.toString(trusted));
  },
  loadIdentityKey: function(identifier) {
    if (identifier === null || identifier === undefined)
      throw new Error("Tried to get identity key for undefined/null key");
    return Promise.resolve(this.get('identityKey' + identifier));
  },
  saveIdentity: function(identifier, identityKey) {
    if (identifier === null || identifier === undefined)
      throw new Error("Tried to put identity key for undefined/null key");

    var address = new libsignal.SignalProtocolAddress.fromString(identifier);

    var existing = this.get('identityKey' + address.getName());
    this.put('identityKey' + address.getName(), identityKey)

    if (existing && util.toString(identityKey) !== util.toString(existing)) {
      return Promise.resolve(true);
    } else {
      return Promise.resolve(false);
    }

  },

  /* Returns a prekeypair object or undefined */
  loadPreKey: function(keyId) {
    var res = this.get('25519KeypreKey' + keyId);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storePreKey: function(keyId, keyPair) {
    return Promise.resolve(this.put('25519KeypreKey' + keyId, keyPair));
  },
  removePreKey: function(keyId) {
    return Promise.resolve(this.remove('25519KeypreKey' + keyId));
  },

  /* Returns a signed keypair object or undefined */
  // 25519.Curve25519是一个椭圆曲线,在加密中使用
  loadSignedPreKey: function(keyId) {
    var res = this.get('25519KeysignedKey' + keyId);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storeSignedPreKey: function(keyId, keyPair) {
    return Promise.resolve(this.put('25519KeysignedKey' + keyId, keyPair));
  },
  removeSignedPreKey: function(keyId) {
    return Promise.resolve(this.remove('25519KeysignedKey' + keyId));
  },

  loadSession: function(identifier) {
    return Promise.resolve(this.get('session' + identifier));
  },
  storeSession: function(identifier, record) {
    return Promise.resolve(this.put('session' + identifier, record));
  },
  removeSession: function(identifier) {
    return Promise.resolve(this.remove('session' + identifier));
  },
  removeAllSessions: function(identifier) {
    for (var id in this.store) {
      if (id.startsWith('session' + identifier)) {
        delete this.store[id];
      }
    }
    return Promise.resolve();
  }
};

web api风格 浅谈

由于是web api,主要考虑使用http、websocket的交互方式,也即http和websocket的api设计风格。

从http的语义角度入手

调用api,实际上就是信息传递。这里信息传递包括了请求和响应。

简单起见,先只考虑使用http协议调用api。

从纯post到RESTful

最初接触到前后端交互并自己进行开发,很理所当然的按最“简单”的方式设计api:所有请求数据都使用json并塞进http的body里,同时所有返回数据同样使用json塞进body里。

此处的“简单”并非真的简单,自行解析和处理数据,实际上不比利用http字段并使用相关函数处理简单。

后面了解到更多关于http的知识,并且了解到RESTful api规范,自然而然会使用RESTful风格的api。

迁移确实有成本,但下文会给出迁移成本低的理由。

纯post和RESTful的语义

前面提到了调用api实际上是信息传递。我们来看一下纯post和RESTful有哪里不一样。

首先可以肯定的是,经过设计,纯post能表达的信息量与RESTful是一样甚至更多的。

具体他们的区别:纯post是语义的聚合,RESTful是语义的分散。聚合,指的是大部分的信息都集中在http的body中。分散,指的是信息分散在http的各个字段中。

也即他们的区别是,对http使用方式的不同。纯post往往需要将http已有的字段重新定义一次,对http的利用率是较低的。纯post与RESTful风格的api比较,它们对path的设计也不同:纯post往往会把method放入path;而对于RESTful来说,method就是http的method。

也就是说,如果不了解http,那么设计纯post api会再设计出类似http的一套东西。那么为什么不学习http并直接转向RESTful风格呢?自行设计需要踩坑,效率通常比学习现有的东西低。即便是需求复杂,http的字段无法满足,http和RESTful的知识也能为设计提供思路。

纯post和JSON-RPC

JSON-RPC实际上就是比较完整的纯post信息交换设计。实际需要时可供参考。

对于websocket

由于websocket在正式通信过程中仅有消息内容,与http丰富的字段不同。协议本身限制了通信过程中只能使用类似http的纯post的方式。(socket.io有命名空间和事件实际上也是对消息内容的设计)

所以JSON-RPC同样适合websocket。

关于RPC

虽然RPC主要是在服务间进行,但浏览器也可以当作一个服务(渲染服务),并且通常只作为调用者。

RPC和RESTful

RPC面向方法,RESTful面向资源,但无论怎么样,调用时都需要知道相关的地址(RPC的注册中心,RESTful的资源解析)和对应的参数,对于调用者来说,两者区别不大。

上面是经典的误解。RPC调用强调的是 函数调用,如何实现网络通信是函数调用的底层,也就是说,纯post(JSON-RPC)和RESTful都能作为RPC的网络交互设计风格。具体如何选择,看需求、接口的复杂程度。

这两者其实不应该相提并论,具体区别可以参考:如何理解RPC和REST (注:RESTful是REST使用http的一种实现)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.