mokeyWie

mokeyWie 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 monkeywie.cn 编辑
编辑

全干工程师~

个人动态

mokeyWie 赞了文章 · 11月25日

68篇干货,手把手教你通关 Spring Security!

Spring Security 系列前前后后整了 68 篇文章了,是时候告一个段落了。

这两天松哥抽空把该系列的文章整理了一下,做成了一个索引,方便小伙伴们查找。

教程地址如下:

对应的 Demo 地址如下:

前面 javaboy.org 是国外服务器,如果响应慢小伙伴只需要 xxx 即可,不需要我多说了吧。

后面的 itboyhub.com 是国内服务器,虽然松哥已经买了 CDN 加速服务了,但是响应速度好像还是一般般,所以文末松哥也给出了微信公众号的文章索引。

小伙伴们按照松哥已经排列好的顺序,一篇一篇练级通关吧!

文章索引:

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登录用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登录用户,一个配置搞定!
  14. Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
  15. Spring Security 自带防火墙!你都不知道自己的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!
  19. 要学就学透彻!Spring Security 中 CSRF 防御源码解析
  20. Spring Boot 中密码加密的两种姿势!
  21. Spring Security 要怎么学?为什么一定要成体系的学习?
  22. Spring Security 两种资源放行策略,千万别用错了!
  23. 松哥手把手教你入门 Spring Boot + CAS 单点登录
  24. Spring Boot 实现单点登录的第三种方案!
  25. Spring Boot+CAS 单点登录,如何对接数据库?
  26. Spring Boot+CAS 默认登录页面太丑了,怎么办?
  27. 用 Swagger 测试接口,怎么在请求头中携带 Token?
  28. Spring Boot 中三种跨域场景总结
  29. Spring Boot 中如何实现 HTTP 认证?
  30. Spring Security 中的四种权限控制方式
  31. Spring Security 多种加密方案共存,老破旧系统整合利器!
  32. 神奇!自己 new 出来的对象一样也可以被 Spring 容器管理!
  33. Spring Security 配置中的 and 到底该怎么理解?
  34. 一文搞定 Spring Security 异常处理机制!
  35. 写了这么多年代码,这样的登录方式还是头一回见!
  36. Spring Security 竟然可以同时存在多个过滤器链?
  37. Spring Security 可以同时对接多个用户表?
  38. 在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
  39. 深入理解 FilterChainProxy【源码篇】
  40. 深入理解 SecurityConfigurer 【源码篇】
  41. 深入理解 HttpSecurity【源码篇】
  42. 深入理解 AuthenticationManagerBuilder 【源码篇】
  43. 花式玩 Spring Security ,这样的用户定义方式你可能没见过!
  44. 深入理解 WebSecurityConfigurerAdapter【源码篇】
  45. 盘点 Spring Security 框架中的八大经典设计模式
  46. Spring Security 初始化流程梳理
  47. 为什么你使用的 Spring Security OAuth 过期了?松哥来和大家捋一捋!
  48. 一个诡异的登录问题
  49. 什么是计时攻击?Spring Boot 中该如何防御?
  50. Spring Security 中如何让上级拥有下级的所有权限?
  51. Spring Security 权限管理的投票器与表决机制
  52. Spring Security 中的 hasRole 和 hasAuthority 有区别吗?
  53. Spring Security 中如何细化权限粒度?
  54. 一个案例演示 Spring Security 中粒度超细的权限控制!
  55. Spring Security 中最流行的权限管理模型!
  56. 我又发现 Spring Security 中一个小秘密!
  57. 聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
  58. RBAC 案例解读【2】

下面是 OAuth2 相关的技能点:

  1. 做微服务绕不过的 OAuth2,松哥也来和大家扯一扯
  2. 这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程?
  3. 死磕 OAuth2,教练我要学全套的!
  4. OAuth2 令牌还能存入 Redis ?越玩越溜!
  5. 想让 OAuth2 和 JWT 在一起愉快玩耍?请看松哥的表演
  6. 最近在做 Spring Cloud 项目,松哥和大家分享一点微服务架构中的安全管理思路
  7. Spring Boot+OAuth2,一个注解搞定单点登录!
  8. 分分钟让自己的网站接入 GitHub 第三方登录功能
  9. Spring Boot+OAuth2,如何自定义返回的 Token 信息?
  10. OAuth2,想说懂你不容易

从三月份到十月份,大半年的业余时间都耗在这个上面了,现在整理索引这个过程真的很享受!感觉这一年的业余时间没浪费。

最后,松哥还搜集了 50+ 个项目需求文档,想做个项目练练手的小伙伴不妨看看哦~



需求文档地址:https://github.com/lenve/javadoc

查看原文

赞 7 收藏 6 评论 0

mokeyWie 赞了文章 · 11月16日

这些高阶的函数技术,你掌握了么

在 JavaScript 中,函数为一等公民(First Class),所谓的 “一等公民”,指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或作为其它函数的返回值

接下来阿宝哥将介绍与函数相关的一些技术,阅读完本文,你将了解高阶函数、函数组合、柯里化、偏函数、惰性函数和缓存函数的相关知识。

一、高阶函数

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入;
  • 输出一个函数。

接收一个或多个函数作为输入,即函数作为参数传递。这种应用场景,相信很多人都不会陌生。比如常用的 Array.prototype.map()Array.prototype.filter() 高阶函数:

// Array.prototype.map 高阶函数
const array = [1, 2, 3, 4];
const map = array.map(x => x * 2); // [2, 4, 6, 8]

// Array.prototype.filter 高阶函数
const words = ['semlinker', 'kakuqo', 'lolo', 'abao'];
const result = words.filter(word => word.length > 5); // ["semlinker", "kakuqo"]

而输出一个函数,即调用高阶函数之后,会返回一个新的函数。我们日常工作中,常见的 debouncethrottle 函数就满足这个条件,因此它们也可以被称为高阶函数。

关注「全栈修仙之路」阅读阿宝哥原创的 3 本免费电子书及 50 几篇 “重学TS” 教程。

二、函数组合

函数组合就是将两个或两个以上的函数组合生成一个新函数的过程:

const compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
};

在以上代码中,fg 都是函数,而 x 是组合生成新函数的参数。

2.1 函数组合的作用

在项目开发过程中,为了实现函数的复用,我们通常会尽量保证函数的职责单一,比如我们定义了以下功能函数:

在拥有以上功能函数的基础上,我们就可以自由地对函数进行组合,来实现特定的功能:

function lowerCase(input) {
  return input && typeof input === "string" ? input.toLowerCase() : input;
}

function upperCase(input) {
  return input && typeof input === "string" ? input.toUpperCase() : input;
}

function trim(input) {
  return typeof input === "string" ? input.trim() : input;
}

function split(input, delimiter = ",") {
  return typeof input === "string" ? input.split(delimiter) : input;
}

const trimLowerCaseAndSplit = compose(trim, lowerCase, split);
trimLowerCaseAndSplit(" a,B,C "); // ["a", "b", "c"]

在以上的代码中,我们通过 compose 函数实现了一个 trimLowerCaseAndSplit 函数,该函数会对输入的字符串,先执行去空格处理,然后在把字符串中包含的字母统一转换为小写,最后在使用 , 分号对字符串进行拆分。利用函数组合的技术,我们就可以很方便的实现一个 trimUpperCaseAndSplit 函数。

2.2 组合函数的实现

function compose(...funcs) {
  return function (x) {
    return funcs.reduce(function (arg, fn) {
      return fn(arg);
    }, x);
  };
}

在以上的代码中,我们通过 Array.prototype.reduce 方法来实现组合函数的调度,对应的执行顺序是从左到右。这个执行顺序与 Linux 管道或过滤器的执行顺序是一致的。

不过如果你想从右往左开始执行的话,这时你就可以使用 Array.prototype.reduceRight 方法来实现。

其实每当看到 compose 函数,阿宝哥就情不自禁想到 “如何更好地理解中间件和洋葱模型” 这篇文章中介绍的 compose 函数:

function compose(middleware) {
  // 省略部分代码
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

利用上述的 compose 函数,我们就可以实现以下通用的任务处理流程:

三、柯里化

柯里化(Currying)是一种处理函数中含有多个参数的方法,并在只允许单一参数的框架中使用这些函数。这种转变是现在被称为 “柯里化” 的过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。当接收足够的参数后,会自动执行原函数。

在理论计算机科学中,柯里化提供了简单的理论模型,比如:在只接受一个单一参数的 lambda 演算中,研究带有多个参数的函数的方式。与柯里化相反的是 Uncurrying,一种使用匿名单参数函数来实现多参数函数的方法。比如:

const func = function(a) {
  return function(b) {
    return a * a + b * b;
  }
}

func(3)(4); // 25

Uncurrying 不是本文的重点,接下来我们使用 Lodash 提供的 curry 函数来直观感受一下,对函数进行 “柯里化” 处理之后产生的变化:

const abc = function(a, b, c) {
  return [a, b, c];
};
 
const curried = _.curry(abc);
 
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
_.curry(func, [arity=func.length])

创建一个函数,该函数接收 func 的参数,要么调用func返回的结果,如果 func 所需参数已经提供,则直接返回 func 所执行的结果。或返回一个函数,接受余下的func 参数的函数,可以使用 func.length 设置需要累积的参数个数。

来源:https://www.lodashjs.com/docs...

这里需要特别注意的是,在数学和理论计算机科学中的柯里化函数,一次只能传递一个参数。而对于 JavaScript 语言来说,在实际应用中的柯里化函数,可以传递一个或多个参数。好的,介绍完柯里化的相关知识,接下来我们来介绍柯里化的作用。

3.1 柯里化的作用

3.1.1 参数复用
function buildUri(scheme, domain, path) {
  return `${scheme}://${domain}/${path}`;
}

const profilePath = buildUri("https", "github.com", "semlinker/semlinker");
const awesomeTsPath = buildUri("https", "github.com", "semlinker/awesome-typescript");

在以上代码中,首先我们定义了一个 buildUri 函数,该函数可用于构建 uri 地址。接着我们使用 buildUri 函数构建了阿宝哥 Github 个人主页awesome-typescript 项目的地址。对于上述的 uri 地址,我们发现 httpsgithub.com 这两个参数值是一样的。

假如我们需要继续构建阿宝哥其他项目的地址,我们就需要重复设置相同的参数值。那么有没有办法简化这个流程呢?答案是有的,就是对 buildUri 函数执行柯里化处理,具体处理方式如下:

const _ = require("lodash");

const buildUriCurry = _.curry(buildUri);
const myGithubPath = buildUriCurry("https", "github.com");
const profilePath = myGithubPath("semlinker/semlinker");
const awesomeTsPath = myGithubPath("semlinker/awesome-typescript");
3.1.2 延迟计算/运行
const add = function (a, b) {
  return a + b;
};

const curried = _.curry(add);
const plusOne = curried(1);

在以上代码中,通过对 add 函数执行 “柯里化” 处理,我们可以实现延迟计算。好的,简单介绍完柯里化的作用,我们来动手实现一个柯里化函数。

3.2 柯里化的实现

现在我们已经知道了,当柯里化后的函数接收到足够的参数后,就会开始执行原函数。而如果接收到的参数不足的话,就会返回一个新的函数,用来接收余下的参数。基于上述的特点,我们就可以自己实现一个 curry 函数:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) { // 通过函数的length属性,来获取函数的形参个数
      return func.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  }
}

四、偏函数应用

在计算机科学中,偏函数应用(Partial Application)是指固定一个函数的某些参数,然后产生另一个更小元的函数。而所谓的元是指函数参数的个数,比如含有一个参数的函数被称为一元函数。

偏函数应用(Partial Application)很容易与函数柯里化混淆,它们之间的区别是:

  • 偏函数应用是固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数;
  • 柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接收一个参数。

了解完偏函数与柯里化的区别之后,我们来使用 Lodash 提供的 partial 函数来了解一下它如何使用。

4.1 偏函数的使用

function buildUri(scheme, domain, path) {
  return `${scheme}://${domain}/${path}`;
}

const myGithubPath = _.partial(buildUri, "https", "github.com");
const profilePath = myGithubPath("semlinker/semlinker");
const awesomeTsPath = myGithubPath("semlinker/awesome-typescript");
_.partial(func, [partials])

创建一个函数。 该函数调用 func,并传入预设的 partials 参数。

来源:https://www.lodashjs.com/docs...

4.2 偏函数的实现

偏函数用于固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数。基于上述的特点,我们就可以自己实现一个 partial 函数:

function partial(fn) {
  let args = [].slice.call(arguments, 1);
  return function () {
    const newArgs = args.concat([].slice.call(arguments));
    return fn.apply(this, newArgs);
  };
}

4.3 偏函数实现 vs 柯里化实现

五、惰性函数

由于不同浏览器之间存在一些兼容性问题,这导致了我们在使用一些 Web API 时,需要进行判断,比如:

function addHandler(element, type, handler) {
  if (element.addEventListener) {
    element.addEventListener(type, handler, false);
  } else if (element.attachEvent) {
    element.attachEvent("on" + type, handler);
  } else {
    element["on" + type] = handler;
  }
}

在以上代码中,我们实现了不同浏览器 添加事件监听 的处理。代码实现起来也很简单,但存在一个问题,即每次调用的时候都需要进行判断,很明显这是不合理的。对于上述这个问题,我们可以通过惰性载入函数来解决。

5.1 惰性载入函数

所谓的惰性载入就是当第 1 次根据条件执行函数后,在第 2 次调用函数时,就不再检测条件,直接执行函数。要实现这个功能,我们可以在第 1 次条件判断的时候,在满足判断条件的分支中覆盖掉所调用的函数,具体的实现方式如下所示:

function addHandler(element, type, handler) {
  if (element.addEventListener) {
    addHandler = function (element, type, handler) {
      element.addEventListener(type, handler, false);
    };
  } else if (element.attachEvent) {
    addHandler = function (element, type, handler) {
      element.attachEvent("on" + type, handler);
    };
  } else {
    addHandler = function (element, type, handler) {
      element["on" + type] = handler;
    };
  }
  // 保证首次调用能正常执行监听
  return addHandler(element, type, handler);
}

除了使用以上的方式,我们也可以利用自执行函数来实现惰性载入:

const addHandler = (function () {
  if (document.addEventListener) {
    return function (element, type, handler) {
      element.addEventListener(type, handler, false);
    };
  } else if (document.attachEvent) {
    return function (element, type, handler) {
      element.attachEvent("on" + type, handler);
    };
  } else {
    return function (element, type, handler) {
      element["on" + type] = handler;
    };
  }
})();

通过自执行函数,在代码加载阶段就会执行一次条件判断,然后在对应的条件分支中返回一个新的函数,用来实现对应的处理逻辑。

六、缓存函数

缓存函数是将函数的计算结果缓存起来,当下次以同样的参数调用该函数时,直接返回已缓存的结果,而无需再次执行函数。这是一种常见的以空间换时间的性能优化手段。

要实现缓存函数的功能,我们可以把经过序列化的参数作为 key,在把第 1 次调用后的结果作为 value 存储到对象中。在每次执行函数调用前,都先判断缓存中是否含有对应的 key,如果有的话,直接返回该 key 对应的值。分析完缓存函数的实现思路之后,接下来我们来看一下具体如何实现:

function memorize(fn) {
  const cache = Object.create(null); // 存储缓存数据的对象
  return function (...args) {
    const _args = JSON.stringify(args);
    return cache[_args] || (cache[_args] = fn.apply(fn, args));
  };
};

定义完 memorize 缓存函数之后,我们就可以这样来使用它:

let complexCalc = (a, b) => {
  // 执行复杂的计算
};

let memoCalc = memorize(complexCalc);
memoCalc(666, 888);
memoCalc(666, 888); // 从缓存中获取
关注「全栈修仙之路」阅读阿宝哥原创的 3 本免费电子书及 50 几篇 “重学TS” 教程。

七、参考资源

查看原文

赞 19 收藏 11 评论 0

mokeyWie 赞了文章 · 11月13日

写给 35 岁的自己!

这篇文章,我足足写了2个月,写了删,删了又写,很是纠结。本来文笔就不好,所以写来写去,又删来删去,终于纠结了许久,今天,写完了。分享给大家,希望对大家有所帮助与借鉴。大家凑合看吧。。

说实话,在这之前,我从来没有想过我的35岁是个什么样子?

也从来没有想过,35岁这么尴尬的年纪这么快的到来。特别是这几年互联网行业的兴起、没落,一波又一波的预示着很多人的中年危机。

我一直喜欢说一句话:危机、焦虑都是自己给的,别人给不了你,别人也贩卖不了给你。准备好,放平心态,所有的危机、焦虑那都不事儿。

即来之,则安之,这应该是我们唯一能够应对的方法。

写这篇文章,一来记录一下自己这么些年的经历,二来给大家分享一下一些人生经验,同时也希望能够帮助一些朋友、读者们,能够更早的去归避掉一些坑,或者说能够将自己的人生路走的更加的顺畅一些。仅此而已!!!

1、山里孩子往外走

很多熟悉我的读者,大概都知道,我出生在一个小山里,那个年代交通不便,记得应该是刚刚通上电,时不时的还会过上煤油灯的日子。

在上世纪80年代,农村的经济情况都不太好,家家户户都随时有口粮断缺的情况,我的家也不例外。

我的父亲是家里的老大,因此他从小就担起了作为大哥的责任:干农活以及养家糊口。由于常年劳累,我记事的时候,父亲的身体已经累垮了,因此家庭的重担便由瘦弱的母亲独自扛了起来。那时,我印象最深刻的就是父亲咳嗽的声音和母亲扛起重物的样子。

为了让家里的生活不那么拮据,母亲不辞辛苦,将所有苦痛都一个人吞下。我心疼母亲,很小就开始帮母亲干农活,那时我就明白了:只有走出这座大山,我才能让家人过上更好的生活。

所以,那时,走出大山唯一的方法就是读书。但是,我的学习成绩真的是如一湖清水,毫无波澜。成绩一如既往的一般般,所以,学生时代的记忆也就那么回事,没什么可说的。

但学习,仍然是我们这些山里孩子往外走的唯一可行的方法

2、杭漂,梦想在那里启航

民工哥的十年故事:杭漂十年,今撤霸都!,这是我曾经写过的一篇旧文,感兴趣的也可以读一读。

现在回想起来,这10年的杭漂生涯,深深影响了我一生。很多时候回想起来,那段过往的岁月,也会不时有所思考与感叹。

杭漂开始,我的第一份工作是一家企业的网络管理员,那个时候,一个初入社会的毛头小子,连个系统都不会装,楞是在这个行业坚持到现在,连我都无法相像我是怎么坚持下去的。

白天不会问题,用笔记下来,晚上回去查资料,将解决问题的方法一步一步的写下来。第二天上班,再去一步步试,将遇到的问题逐一解决。因为,那个时候,找一份工作多难,特别是对于我们这些无经验、无学历、无背影的三无人员。

也是由于工作的原因,我一直都随带着一个小本子,不管走到哪看到什么,我感兴趣的,我都会记在小本子上面,可惜的是在杭漂的这10年里,搬家无数次,最终不知道丢哪里了。但是,这个习惯我一直坚持到现在(现在不用小本本了,智能手机这么发达,随时可以记录)。

慢慢的,工作也越来越顺手了,遇到的问题也不再是难以解决,基本上都能就会的过来,这时我也尝到了学习的甜头。

所以,一直利用业余的时间,不断的从网络找一些资料、视频,自已在家看、学习,比如:像windows AD域、邮件系统、路由、交换等一些IT技术。

杭漂,是我梦想开始的地方。

3、折腾,亦是转折

工作得心应手了,空闲的时间也多了。但是,慢慢的人也变的懒了,散漫的日子总是那么逍遥快活,到点上班,准时下班,这种生活太舒服了。

下班回家也忘记了学习,喜欢看看剧,电影,打打斗地主。

还玩过一段时间的传奇私服。

人啊,一旦过上这种日子,很容易迷失自己。

不过,还好,我在这种迷失自己的岁月中沉沦了没有多久,突然有一天,我萌生了一个想法:我要去考CCIE,折腾的开始,也是我人生中的一次转折点。

人啊,拼命起来干一件事,你连自己都有点害怕。

白天上班,晚上在家看视频、看资料,和天南海北的一群人讨论问题,很多时候,一干能干到夜里1、2点,白天继续上班。

苦是苦了一点,但是,很多时候你想起来,不管结果如何,你为之曾经奋斗过,努力过,认真过,至少能对的起那段无法回去的过往岁月。

就为了考这个CCIE ,我基本上长达6-7个月都是这样的状态,白天上班,晚上学习。

为些,我也写出了一份好几百页的CCIE学习笔记,纯手工一个字一个字敲上去的。

其实,那个时候,我也没有多想,后面是不会从事相关的工作,当时只有一个想法就是我要干这件事,不管结果如何,折腾呗,大不了从头再来

确实,这种折腾,也开启了我人生的转折,另一个新的起点。

4、考试失利,重新开始

最终,虽然经过了几个月的奋战,不过还是没有能一击拿下CCIE RS。

考试的前一个月,我裸辞在家备考,既然,考试失利,那么也预示着我将要踏上求职大军的路上。

那时候,也正是各类应届生的求职高峰期。7月的杭州,骄阳似火,沥青的路面都能把鸡蛋烤熟了,这个时段找工作真的是要命啊。

一过找工作,一边学习,也是我正式与Linux结缘。

一边学习,一边找工作,也是从那时候起,我对Linux系统产生了很大的、浓厚的兴趣,不停折腾,不断的总结,越来越感觉,这系统很好玩,很有意思。

都说兴趣是最好的老师,这句话在我的身上印证了效果,确实,这对于我后面的学习、工作生涯产生了深远的影响与意义。

慢慢的,从最基础的小白(虚拟机装个操作系统都能搞几天搞不成功的),逐渐的也能胜任中小型企业的运维工作了。

所以说,有时候,一件事情的成与失,不见得是一件坏事,或许可能是一个转折,也或许是另一扇即将为你打开的大门

5、是幸运的,也是“不幸”的

为什么这样说呢?

幸运的是,前面的我付出的努力与平时积累打下的基础,在后面的工作中发挥出了作用,幸运的是,有幸赶上了那几年互联网在发展的东风,借势而行,当然会省力不少。而且,也是有幸遇上一些特别好的朋友,给予很多的帮助与支持。

很多读者经常问我,学运维好不好找工作?学运维在北京或XX地方,能不能月薪一万+啊?说实话,每当我看到这些问题时,非常的苦恼,不知道如何回答这些读者朋友们,还有就是我不能很多的给出一些因人而宜的建议。

这也是我常和一些读者(如今是非常好的朋友)常说的,学习始终靠的是自己,谁都帮不了你。

首先,你得摆正你的思维,比如,不是学了某个技术,你就能拿到多少多少的薪水,而是你得为了拿到这个薪水去努力学习某些技术,当然,光会技术是远远不够的。

其次,技术是更新迭代的,没有一劳永逸的学习,都是不断积累的过程,任何时候,任何岗位,都需要保持不断自我学习的心态,这才是前进的动力之源。

为什么这么说,因为,这些年我一直是这样做的,不能保证适合于所有人,但应该说是经过一定的市场检验出来的干货吧,也分享给大家。

“不幸”为什么我加上引号,当然,它是一种婉转的说法,在这里应该说是中意词。

由于多种原因,以及一些其它的因素,我不得不离开这样我曾经漂泊十年的城市,一个我这个山村出来的孩子,见证过那十年飞速发展的地方,还是有点不舍的。

很多地方,原来一片荒芜,现在是人头攒动,到处高楼林立。一片繁华,杭州这十年的发展真的太快了,太快了。

2017年4月15日,收拾完所有在杭的物品,向我曾经漂泊、工作、学习过十年的城市说再见了,记得那时我还调侃自己,下次再来,我是以游客的身份了。

所以,我也一直很感谢,上天给我安排了这一段十年的杭漂岁月。

这也是很多时候,我为什么推荐刚毕业、或希望去一线城市打拼的人们去杭州看一看,或许杭州也会是你们梦想启航的地方。

6、杭漂结束,又一新起点

回来之后,很长一段时间内,真的不太能适应。所以,这里也给那些在外漂泊,可能有想法回家乡发展的朋友一个提醒,做好一切的心里准备,接受一切可能出现的,你可能不太能接受的事实。

城市的不同,环境的不同,所有的一切都会大不相同。所有的一切都是新的开始,你的工作,你的生活,你的人脉等等。

再比如:你找工作时薪资的落差、工作环境、公司氛围等等,肯定和你之前是有所区别的。每个城市都有自己的特色。

但是,生活可是不会给你调整、适应的时间,所以,自我调节是很重要的。

成年人的世界,没有容易二字!!!

新的起点,新的开始,对于我这样一个城市陌生人来说,所有的一切都是挑战,都是需要克服的困难。

7、写公众号

写公众号,也是源于我一直写总结,记笔记的习惯。

从最开始的单纯的记录一些学习笔记,一些最基础的、零散的知识点,到后来写的一些成系列的的文章,比如:《运维工程师打怪升级进阶之路 V2.0》,《吐血总结|史上最全的MySQL学习资料!!》,《史上最全、最详细的Docker学习资料》,《史上最全的大厂Mysql面试题在这里》等等。

随着读者的日益增长,需求也逐渐的多样化,所以,我也会时而转载一些其它技术爱好者的好文章,我认为只要对读者有帮助的文章,就没有白发。

当然,肯定有很多人会比较关心一个问题,也是很多读者私下问我的一个问题,写公众号赚钱吗?答案是肯定的。

付出总有回报的,这个不变的事实与真理。但是,到底能赚多少钱,到底有多赚钱?这个问题,因人而宜,没办法有一个统一的答案。

在很久之前我也分享过一个技术人赚钱的文章:聊几个与赚钱相关的小事情

大家也都知道,我的公众号也会接一些培训类的课程推广的广告,这就是公众号的收入之一,对于IT技术号来说是这样的,其它类的公众号我不是太了解,所以,没办法告诉大家。

在这个过程中,也遭遇了一些读者的爆粗口、谩骂、有的甚至是人生攻击。其实,我想说,真的没有必要,如果这个课程对于你来说没有你想要的,你可以略过。或许对其它人有帮助的呢,真的也有很多读者私下、后台给我留言,让我推荐一些课程、书籍的。

不管如何,还是感谢无论是曾经关注过、现在正关注的、或是未来即将关注的读者们,正是有你们的关注、支持,公众号得以写到今天。同时,我也会一直写下去,也是为了给众多的技术爱好者提供哪怕一丁点的帮助,都是有益的。

因为,你我都是开源技术的受益者,所以,也希望大家都是开源技术的传播者、建设者。

8、写书

今年4月,我的第一本书也出版上线了:折腾 2 年多!我们终于见面了!也算是自己给自己35岁一个最好的礼物,也很庆幸有这样的一个机会能够分享我的一个经验,也希望帮助到众多想要得到帮助的技术同行们。

真的,人生没有彩排,完全是现场直播,我也没有想到,我自己会写书,真的没有。

这也完全是一个机缘巧合的事情,在人邮张老师联系我之前,有很多出版社的编辑联系过、沟通过,我都一再的回绝了,毕竟写书是一个很慎重的事,对知识的敬畏之心一定要有的,不是随随便便写出来的。再加上个人能力、水平有限,更加的不敢动笔了。

不得不为张老师的专业、专注点赞,在写书的这两年多的时间里,张老师不停的指导、帮助我,不断为我解答一些专业问题,不断给我打气。所以,一本书的出版,编辑老师背后的辛苦付出是常人不能看到的。在这也一并感谢下张老师,和一直支持关注,给予我意见、建议的所有好朋友们。

可能这本书并不适合所有人,或者说并不是很完美,如有不正或错误的地方,还请各位同行们给予指正,谢谢大家。因为,一个知识点,每个人理解的角度不同,可能得到的结果也会不同,所以,并不能说谁对谁错,只能说各有千秋,理解不同。

我想开源的意义可能也在此,也是为了让所有的技术爱好者,都能参与到这项技术的建设当中来,也是为了更好的完善它,热爱开源,分享开源,拥抱开源。

9、人生第一次得奖

人生第一次领奖!这也是我完全没有想到事,出乎意料!!!

人生第一次得奖,又让我人生第一次去北京,真的感谢人邮出版社,和广大读者的支持,让我在这一年有了很多个人生第一次的经历。

正好,趁着这个机会,也在北京顺便转了转,去了北大、清华,北海公园,鸟巢、水立方,老北京的胡同里转了转,天安门,还有一些特色的街区,像王府井、鲜鱼口等,可惜的是由于时间关系,没有去长城。

10、结语

在这个后浪如此出彩的年代,我没有因此而感到比别人差,因为,我有我的人生目标、计划、以及如何实现这些目标的步骤。

我也会毫无避讳的承认,新的时代肯定是属于年轻人的天下,因为,很多后浪们比我们这些人更努力、更有创造力、更具有竞争力。也是我学习的对象,在他们身上我看到了很多自己都无法预知的东西,也学习了很多自己未知的领域。

时代的发展,这是必然的结果。但是不能成为我们这些前浪们不努力的借口。

2020年注定是不平凡的一年,肯定也是大家所有人都记忆深刻的一年,因为,这一年发生了太多太多的事,有好的,有坏的,但是这些串起来都是人生的记忆,都是难已回去的往事。

对于我们,唯有向前看,不要停下我们不断前行的脚步,好好学习,努力赚钱,好好生活,努力前行。

努力只有起点,没有终点,我们一直在路上,做一个努力为梦想奔跑的人。

最后,给大家附上我人生中第一个手工作品。

祝我自己、大家、以及所有的朋友们:

未来的工作、学习、生活都能一帆风顺!!

           2020年11月12日合肥

如果你也爱折腾、爱开源技术,可以关注我的微信公众号:民工哥技术之路,我们一同学习、一起交流、共同成长。

image

查看原文

赞 33 收藏 5 评论 15

mokeyWie 回答了问题 · 11月12日

解决Node.js 的 http.ClientRequest 类为什么没有用于描述请求体的属性?

很正常,因为body可能会非常大,比如说下载或上传一个大文件,如果全部帮你扔到内存里,那就直接内存溢出了。
这个不止是node,java、golang等等其它的语言标准库自带的http模块都是这样通过流实现的,因为底层库是不知道你用http具体要干嘛,需要业务层自己去做定制化。

关注 3 回答 2

mokeyWie 发布了文章 · 11月11日

通过浏览器连接docker容器

前言

在公司内部使用 Jenkins 做 CI/CD 时,经常会碰到项目构建失败的情况,一般情况下通过 Jenkins 的构建控制台输出都可以了解到大概发生的问题,但是有些特殊情况开发需要在 Jenkins 服务器上排查问题,这个时候就只能找运维去调试了,为了开发人员的体验就调研了下 web terminal,能够在构建失败时提供容器终端给开发进行问题的排查。

效果展示

支持颜色高亮,支持tab键补全,支持复制粘贴,体验基本上与平常的 terminal 一致。

基于 docker 的 web terminal 实现

docker exec 调用

首先想到的就是通过docker exec -it ubuntu /bin/bash命令来开启一个终端,然后将标准输入和输出通过 websocket 与前端进行交互。

然后发现 docker 有提供 API 和 SDK 进行开发的,通过 Go SDK可以很方便的在 docker 里创建一个终端进程:

  • 安装 sdk
go get -u github.com/docker/docker/client@8c8457b0f2f8

这个项目新打的 tag 没有遵循 go mod server 语义,所以如果直接go get -u github.com/docker/docker/client默认安装的是 2017 年的打的一个 tag 版本,这里我直接在 master 分支上找了一个 commit ID,具体原因参考issue

  • 调用 exec
package main

import (
    "bufio"
    "context"
    "fmt"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/client"
)

func main() {
    // 初始化 go sdk
    ctx := context.Background()
    cli, err := client.NewClientWithOpts(client.FromEnv)
    if err != nil {
        panic(err)
    }

    cli.NegotiateAPIVersion(ctx)

    // 在指定容器中执行/bin/bash命令
    ir, err := cli.ContainerExecCreate(ctx, "test", types.ExecConfig{
        AttachStdin:  true,
        AttachStdout: true,
        AttachStderr: true,
        Cmd:          []string{"/bin/bash"},
        Tty:          true,
    })
    if err != nil {
        panic(err)
    }

    // 附加到上面创建的/bin/bash进程中
    hr, err := cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
    if err != nil {
        panic(err)
    }
    // 关闭I/O
    defer hr.Close()
    // 输入
    hr.Conn.Write([]byte("ls\r"))
    // 输出
    scanner := bufio.NewScanner(hr.Conn)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

这个时候 docker 的终端的输入输出已经可以拿到了,接下来要通过 websocket 来和前端进行交互。

前端页面

当我们在 linux terminal 上敲下ls命令时,看到的是:

root@a09f2e7ded0d:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr

实际上从标准输出里返回的字符串却是:

[0m[01;34mbin[0m   [01;34mdev[0m  [01;34mhome[0m  [01;34mlib64[0m  [01;34mmnt[0m  [01;34mproc[0m  [01;34mrun[0m   [01;34msrv[0m  [30;42mtmp[0m  [01;34mvar[0m
[01;34mboot[0m  [01;34metc[0m  [01;34mlib[0m   [01;34mmedia[0m  [01;34mopt[0m  [01;34mroot[0m  [01;34msbin[0m  [01;34msys[0m  [01;34musr[0m

对于这种情况,已经有了一个叫xterm.js的库,专门用来模拟 Terminal 的,我们需要通过这个库来做终端的显示。

var term = new Terminal();
term.open(document.getElementById("terminal"));
term.write("Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ");

通过官方的例子,可以看到它会将特殊字符做对应的显示:

这样的话只需要在 websocket 连上服务器时,将获取到的终端输出使用term.write()写出来,再把前端的输入作为终端的输入就可以实现我们需要的功能了。

思路是没错的,但是没必要手写,xterm.js已经提供了一个 websocket 插件就是来做这个事的,我们只需要把标准输入和输出的内容通过 websocket 传输就可以了。

  • 安装 xterm.js
npm install xterm
  • 基于 vue 写的前端页面
<template>
  <div ref="terminal"></div>
</template>

<script>
// 引入css
import "xterm/dist/xterm.css";
import "xterm/dist/addons/fullscreen/fullscreen.css";

import { Terminal } from "xterm";
// 自适应插件
import * as fit from "xterm/lib/addons/fit/fit";
// 全屏插件
import * as fullscreen from "xterm/lib/addons/fullscreen/fullscreen";
// web链接插件
import * as webLinks from "xterm/lib/addons/webLinks/webLinks";
// websocket插件
import * as attach from "xterm/lib/addons/attach/attach";

export default {
  name: "Index",
  created() {
    // 安装插件
    Terminal.applyAddon(attach);
    Terminal.applyAddon(fit);
    Terminal.applyAddon(fullscreen);
    Terminal.applyAddon(webLinks);

    // 初始化终端
    const terminal = new Terminal();
    // 打开websocket
    const ws = new WebSocket("ws://127.0.0.1:8000/terminal?container=test");
    // 绑定到dom上
    terminal.open(this.$refs.terminal);
    // 加载插件
    terminal.fit();
    terminal.toggleFullScreen();
    terminal.webLinksInit();
    terminal.attach(ws);
  }
};
</script>

后端 websocket 支持

在 go 的标准库中是没有提供 websocket 模块的,这里我们使用官方钦点的 websocket 库。

go get -u github.com/gorilla/websocket

核心代码如下:

// websocket握手配置,忽略Origin检测
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func terminal(w http.ResponseWriter, r *http.Request) {
    // websocket握手
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Error(err)
        return
    }
    defer conn.Close()

    r.ParseForm()
    // 获取容器ID或name
    container := r.Form.Get("container")
    // 执行exec,获取到容器终端的连接
    hr, err := exec(container)
    if err != nil {
        log.Error(err)
        return
    }
    // 关闭I/O流
    defer hr.Close()
    // 退出进程
    defer func() {
        hr.Conn.Write([]byte("exit\r"))
    }()

    // 转发输入/输出至websocket
    go func() {
        wsWriterCopy(hr.Conn, conn)
    }()
    wsReaderCopy(conn, hr.Conn)
}

func exec(container string) (hr types.HijackedResponse, err error) {
    // 执行/bin/bash命令
    ir, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{
        AttachStdin:  true,
        AttachStdout: true,
        AttachStderr: true,
        Cmd:          []string{"/bin/bash"},
        Tty:          true,
    })
    if err != nil {
        return
    }

    // 附加到上面创建的/bin/bash进程中
    hr, err = cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
    if err != nil {
        return
    }
    return
}

// 将终端的输出转发到前端
func wsWriterCopy(reader io.Reader, writer *websocket.Conn) {
    buf := make([]byte, 8192)
    for {
        nr, err := reader.Read(buf)
        if nr > 0 {
            err := writer.WriteMessage(websocket.BinaryMessage, buf[0:nr])
            if err != nil {
                return
            }
        }
        if err != nil {
            return
        }
    }
}

// 将前端的输入转发到终端
func wsReaderCopy(reader *websocket.Conn, writer io.Writer) {
    for {
        messageType, p, err := reader.ReadMessage()
        if err != nil {
            return
        }
        if messageType == websocket.TextMessage {
            writer.Write(p)
        }
    }
}

总结

以上就完成了一个简单的 docker web terminal 功能,之后只需要通过前端传递container IDcontainer name就可以打开指定的容器进行交互了。

完整代码:https://github.com/monkeyWie/...

我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

wechat

查看原文

赞 1 收藏 1 评论 0

mokeyWie 赞了文章 · 11月10日

编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(修订版)

编译器是一个程序,作用是将一门语言翻译成另一门语言

例如 babel 就是一个编译器,它将 es6 版本的 js 翻译成 es5 版本的 js。从这个角度来看,将英语翻译成中文的翻译软件也属于编译器。

一般的程序,CPU 是无法直接执行的,因为 CPU 只能识别机器指令。所以要想执行一个程序,首先要将高级语言编写的程序翻译为汇编代码(Java 还多了一个步骤,将高级语言翻译成字节码),再将汇编代码翻译为机器指令,这样 CPU 才能识别并执行。

由于汇编语言和机器语言一一对应,并且汇编语言更具有可读性。所以计算机原理的教材在讲解机器指令时一般会用汇编语言来代替机器语言讲解。

本文所要写的四则运算编译器需要将 1 + 1 这样的四则运算表达式翻译成机器指令并执行。具体过程请看示例:

// CPU 无法识别
10 + 5

// 翻译成汇编语言
push 10
push 5
add

// 最后翻译为机器指令,汇编代码和机器指令一一对应
// 机器指令由 1 和 0 组成,以下指令非真实指令,只做演示用
0011101001010101
1101010011100101
0010100111100001

四则运算编译器,虽然说功能很简单,只能编译四则运算表达式。但是编译原理的前端部分几乎都有涉及:词法分析、语法分析。另外还有编译原理后端部分的代码生成。不管是简单的、复杂的编译器,编译步骤是差不多的,只是复杂的编译器实现上会更困难。

可能有人会问,学会编译原理有什么好处

我认为对编译过程内部原理的掌握将会使你成为更好的高级程序员。另外在这引用一下知乎网友-随心所往的回答,更加具体:

  1. 可以更加容易的理解在一个语言种哪些写法是等价的,哪些是有差异的
  2. 可以更加客观的比较不同语言的差异
  3. 更不容易被某个特定语言的宣扬者忽悠
  4. 学习新的语言是效率也会更高
  5. 其实从语言a转换到语言b是一个通用的需求,学好编译原理处理此类需求时会更加游刃有余

好了,下面让我们看一下如何写一个四则运算编译器。

词法分析

程序其实就是保存在文本文件中的一系列字符,词法分析的作用是将这一系列字符按照某种规则分解成一个个字元(token,也称为终结符),忽略空格和注释。

示例:

// 程序代码
10 + 5 + 6

// 词法分析后得到的 token
10
+
5
+
6

终结符

终结符就是语言中用到的基本元素,它不能再被分解。

四则运算中的终结符包括符号和整数常量(暂不支持一元操作符和浮点运算)。

  1. 符号+ - * / ( )
  2. 整数常量:12、1000、111...

词法分析代码实现

function lexicalAnalysis(expression) {
    const symbol = ['(', ')', '+', '-', '*', '/']
    const re = /\d/
    const tokens = []
    const chars = expression.trim().split('')
    let token = ''
    chars.forEach(c => {
        if (re.test(c)) {
            token += c
        } else if (c == ' ' && token) {
            tokens.push(token)
            token = ''
        } else if (symbol.includes(c)) {
            if (token) {
                tokens.push(token)
                token = ''
            } 

            tokens.push(c)
        }
    })

    if (token) {
        tokens.push(token)
    }

    return tokens
}

console.log(lexicalAnalysis('100    +   23   +    34 * 10 / 2')) 
// ["100", "+", "23", "+", "34", "*", "10", "/", "2"]

四则运算的语法规则(语法规则是分层的)

  1. x*, 表示 x 出现零次或多次
  2. x | y, 表示 x 或 y 将出现
  3. ( ) 圆括号,用于语言构词的分组

以下规则从左往右看,表示左边的表达式还能继续往下细分成右边的表达式,一直细分到不可再分为止。

  • expression: addExpression
  • addExpression: mulExpression (op mulExpression)*
  • mulExpression: term (op term)*
  • term: '(' expression ')' | integerConstant
  • op: + - * /

addExpression 对应 +- 表达式,mulExpression 对应 */ 表达式。

如果你看不太懂以上的规则,那就先放下,继续往下看。看看怎么用代码实现语法分析。

语法分析

对输入的文本按照语法规则进行分析并确定其语法结构的一种过程,称为语法分析。

一般语法分析的输出为抽象语法树(AST)或语法分析树(parse tree)。但由于四则运算比较简单,所以这里采取的方案是即时地进行代码生成和错误报告,这样就不需要在内存中保存整个程序结构。

先来看看怎么分析一个四则运算表达式 1 + 2 * 3

首先匹配的是 expression,由于目前 expression 往下分只有一种可能,即 addExpression,所以分解为 addExpression
依次类推,接下来的顺序为 mulExpressionterm1(integerConstant)、+(op)、mulExpressionterm2(integerConstant)、*(op)、mulExpressionterm3(integerConstant)。

如下图所示:

这里可能会有人有疑问,为什么一个表达式搞得这么复杂,expression 下面有 addExpressionaddExpression 下面还有 mulExpression
其实这里是为了考虑运算符优先级而设的,mulExpraddExpr 表达式运算级要高。

1 + 2 * 3
compileExpression
   | compileAddExpr
   |  | compileMultExpr
   |  |  | compileTerm
   |  |  |  |_ matches integerConstant        push 1
   |  |  |_
   |  | matches '+'
   |  | compileMultExpr
   |  |  | compileTerm
   |  |  |  |_ matches integerConstant        push 2
   |  |  | matches '*'
   |  |  | compileTerm
   |  |  |  |_ matches integerConstant        push 3
   |  |  |_ compileOp('*')                      *
   |  |_ compileOp('+')                         +
   |_

有很多算法可用来构建语法分析树,这里只讲两种算法。

递归下降分析法

递归下降分析法,也称为自顶向下分析法。按照语法规则一步步递归地分析 token 流,如果遇到非终结符,则继续往下分析,直到终结符为止。

LL(0)分析法

递归下降分析法是简单高效的算法,LL(0)在此基础上多了一个步骤,当第一个 token 不足以确定元素类型时,对下一个字元采取“提前查看”,有可能会解决这种不确定性。

以上是对这两种算法的简介,具体实现请看下方的代码实现。

表达式代码生成

我们通常用的四则运算表达式是中缀表达式,但是对于计算机来说中缀表达式不便于计算。所以在代码生成阶段,要将中缀表达式转换为后缀表达式。

后缀表达式

后缀表达式,又称逆波兰式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则)。

示例:

中缀表达式: 5 + 5 转换为后缀表达式:5 5 +,然后再根据后缀表达式生成代码。

// 5 + 5 转换为 5 5 + 再生成代码
push 5
push 5
add

代码实现

编译原理的理论知识像天书,经常让人看得云里雾里,但真正动手做起来,你会发现,其实还挺简单的。

如果上面的理论知识看不太懂,没关系,先看代码实现,然后再和理论知识结合起来看。

注意:这里需要引入刚才的词法分析代码。

// 汇编代码生成器
function AssemblyWriter() {
    this.output = ''
}

AssemblyWriter.prototype = {
    writePush(digit) {
        this.output += `push ${digit}\r\n`
    },

    writeOP(op) {
        this.output += op + '\r\n'
    },

    //输出汇编代码
    outputStr() {
        return this.output
    }
}

// 语法分析器
function Parser(tokens, writer) {
    this.writer = writer
    this.tokens = tokens
    // tokens 数组索引
    this.i = -1
    this.opMap1 = {
        '+': 'add',
        '-': 'sub',
    }

    this.opMap2 = {
        '/': 'div',
        '*': 'mul'
    }

    this.init()
}

Parser.prototype = {
    init() {
        this.compileExpression()
    },

    compileExpression() {
        this.compileAddExpr()
    },

    compileAddExpr() {
        this.compileMultExpr()
        while (true) {
            this.getNextToken()
            if (this.opMap1[this.token]) {
                let op = this.opMap1[this.token]
                this.compileMultExpr()
                this.writer.writeOP(op)
            } else {
                // 没有匹配上相应的操作符 这里为没有匹配上 + - 
                // 将 token 索引后退一位
                this.i--
                break
            }
        }
    },

    compileMultExpr() {
        this.compileTerm()
        while (true) {
            this.getNextToken()
            if (this.opMap2[this.token]) {
                let op = this.opMap2[this.token]
                this.compileTerm()
                this.writer.writeOP(op)
            } else {
                // 没有匹配上相应的操作符 这里为没有匹配上 * / 
                // 将 token 索引后退一位
                this.i--
                break
            }
        }
    },

    compileTerm() {
        this.getNextToken()
        if (this.token == '(') {
            this.compileExpression()
            this.getNextToken()
            if (this.token != ')') {
                throw '缺少右括号:)'
            }
        } else if (/^\d+$/.test(this.token)) {
            this.writer.writePush(this.token)
        } else {
            throw '错误的 token:第 ' + (this.i + 1) + ' 个 token (' + this.token + ')'
        }
    },

    getNextToken() {
        this.token = this.tokens[++this.i]
    },

    getInstructions() {
        return this.writer.outputStr()
    }
}

const tokens = lexicalAnalysis('100+10*10')
const writer = new AssemblyWriter()
const parser = new Parser(tokens, writer)
const instructions = parser.getInstructions()
console.log(instructions) // 输出生成的汇编代码
/*
push 100
push 10
push 10
mul
add
*/

模拟执行

现在来模拟一下 CPU 执行机器指令的情况,由于汇编代码和机器指令一一对应,所以我们可以创建一个直接执行汇编代码的模拟器。
在创建模拟器前,先来讲解一下相关指令的操作。

在内存中,栈的特点是只能在同一端进行插入和删除的操作,即只有 push 和 pop 两种操作。

push

push 指令的作用是将一个操作数推入栈中。

pop

pop 指令的作用是将一个操作数弹出栈。

add

add 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a + b,再将结果 push 到栈中。

sub

sub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a - b,再将结果 push 到栈中。

mul

mul 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a * b,再将结果 push 到栈中。

div

sub 指令的作用是执行两次 pop 操作,弹出两个操作数 a 和 b,然后执行 a / b,再将结果 push 到栈中。

四则运算的所有指令已经讲解完毕了,是不是觉得很简单?

代码实现

注意:需要引入词法分析和语法分析的代码

function CpuEmulator(instructions) {
    this.ins = instructions.split('\r\n')
    this.memory = []
    this.re = /^(push)\s\w+/
    this.execute()
}

CpuEmulator.prototype = {
    execute() {
        this.ins.forEach(i => {
            switch (i) {
                case 'add':
                    this.add()
                    break
                case 'sub':
                    this.sub()
                    break
                case 'mul':
                    this.mul()
                    break
                case 'div':
                    this.div()
                    break                
                default:
                    if (this.re.test(i)) {
                        this.push(i.split(' ')[1])
                    }
            }
        })
    },

    add() {
        const b = this.pop()
        const a = this.pop()
        this.memory.push(a + b)
    },

    sub() {
        const b = this.pop()
        const a = this.pop()
        this.memory.push(a - b)
    },

    mul() {
        const b = this.pop()
        const a = this.pop()
        this.memory.push(a * b)
    },

    div() {
        const b = this.pop()
        const a = this.pop()
        // 不支持浮点运算,所以在这要取整
        this.memory.push(Math.floor(a / b))
    },

    push(x) {
        this.memory.push(parseInt(x))
    },

    pop() {
        return this.memory.pop()
    },

    getResult() {
        return this.memory[0]
    }
}

const tokens = lexicalAnalysis('(100+  10)*  10-100/  10      +8*  (4+2)')
const writer = new AssemblyWriter()
const parser = new Parser(tokens, writer)
const instructions = parser.getInstructions()
const emulator = new CpuEmulator(instructions)
console.log(emulator.getResult()) // 1138

一个简单的四则运算编译器已经实现了。我们再来写一个测试函数跑一跑,看看运行结果是否和我们期待的一样:

function assert(expression, result) {
    const tokens = lexicalAnalysis(expression)
    const writer = new AssemblyWriter()
    const parser = new Parser(tokens, writer)
    const instructions = parser.getInstructions()
    const emulator = new CpuEmulator(instructions)
    return emulator.getResult() == result
}

console.log(assert('1 + 2 + 3', 6)) // true
console.log(assert('1 + 2 * 3', 7)) // true
console.log(assert('10 / 2 * 3', 15)) // true
console.log(assert('(10 + 10) / 2', 10)) // true

测试全部正确。另外附上完整的源码,建议没看懂的同学再看多两遍。

更上一层楼

对于工业级编译器来说,这个四则运算编译器属于玩具中的玩具。但是人不可能一口吃成个胖子,所以学习编译原理最好采取循序渐进的方式去学习。下面来介绍一个高级一点的编译器,这个编译器可以编译一个 Jack 语言(类 Java 语言),它的语法大概是这样的:

class Generate {
    field String str;
    static String str1;
    constructor Generate new(String s) {
        let str = s;
        return this;
    }

    method String getString() {
        return str;
    }
}

class Main {
    function void main() {
        var Generate str;
        let str = Generate.new("this is a test");
        do Output.printString(str.getString());
        return;
    }
}

上面代码的输出结果为:this is a test

想不想实现这样的一个编译器?

这个编译器出自一本书《计算机系统要素》,它从第 6 章开始,一直到第 11 章讲解了汇编编译器(将汇编语言转换为机器语言)、VM 编译器(将类似于字节码的 VM 语言翻译成汇编语言)、Jack 语言编译器(将高级语言 Jack 翻译成 VM 语言)。每一章都有详细的知识点讲解和实验,只要你一步一步跟着做实验,就能最终实现这样的一个编译器。

如果编译器写完了,最后机器语言在哪执行呢?

这本书已经为你考虑好了,它从第 1 章到第 5 章,一共五章的内容。教你从逻辑门开始,逐步组建出算术逻辑单元 ALU、CPU、内存,最终搭建出一个现代计算机。然后让你用编译器编译出来的程序运行在这台计算机之上。

另外,这本书的第 12 章会教你写操作系统的各种库函数,例如 Math 库(包含各种数学运算)、Keyboard 库(按下键盘是怎么输出到屏幕上的)、内存管理等等。

想看一看全书共 12 章的实验做完之后是怎么样的吗?我这里提供几张这台模拟计算机运行程序的 DEMO GIF,供大家参考参考。

这几张图中的右上角是“计算机”的屏幕,其他部分是“计算机”的堆栈区和指令区。

这本书的所有实验我都已经做完了(每天花 3 小时,两个月就能做完),答案放在我的 github 上,有兴趣的话可以看看。

参考资料

我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:谭光志,邀请大家一同入驻:https://www.oschina.net/shari...

查看原文

赞 14 收藏 9 评论 2

mokeyWie 赞了文章 · 11月10日

小蝌蚪传记:让接口提速60%的优化技巧

FFCreator是我们团队做的一个轻量、灵活的短视频加工库。您只需要添加几张图片或文字,就可以快速生成一个类似抖音的酷炫短视频。github地址:https://github.com/tnfe/FFCreator 欢迎小伙伴star。

背景

好久没写文章了,沉寂了大半年

持续性萎靡不振,间歇性癫痫发作

天天来大姨爹,在迷茫、焦虑中度过每一天

不得不承认,其实自己就是个废物

作为一名低级前端工程师

最近处理了一个十几年的祖传老接口

它继承了一切至尊级复杂度逻辑

传说中调用一次就能让cpu负载飙升90%的日天服务

专治各种不服与老年痴呆

我们欣赏一下这个接口的耗时

平均调用时间在3s以上

导致页面出现严重的转菊花

经过各种深度剖析与专业人士答疑

最后得出结论是:放弃医疗

鲁迅在《狂人日记》里曾说过:“能打败我的,只有女人和酒精,而不是bug

每当身处黑暗之时

这句话总能让我看到光

所以这次要硬起来

我决定做一个node代理层

用下面三个方法进行优化:

  • 按需加载 -> graphQL
  • 数据缓存 -> redis
  • 轮询更新 -> schedule

代码地址:github

按需加载 -> graphQL

天秀老接口存在一个问题,我们每次请求1000条数据,返回的数组中,每一条数据都有上百个字段,其实我们前端只用到其中的10个字段而已。

如何从一百多个字段中,抽取任意n个字段,这就用到graphQL。

graphQL按需加载数据只需要三步:

  • 定义数据池 root
  • 描述数据池中数据结构 schema
  • 自定义查询数据 query

定义数据池

我们针对屌丝追求女神的场景,定义一个数据池,如下:

// 数据池
var root = {
    girls: [{
        id: 1,
        name: '女神一',
        iphone: 12345678910,
        weixin: 'xixixixi',
        height: 175,
        school: '剑桥大学',
        wheel: [{ name: '备胎1号', money: '24万元' }, { name: '备胎2号', money: '26万元' }]
    },
    {
        id: 2,
        name: '女神二',
        iphone: 12345678910,
        weixin: 'hahahahah',
        height: 168,
        school: '哈佛大学',
        wheel: [{ name: '备胎3号', money: '80万元' }, { name: '备胎4号', money: '200万元' }]
    }]
}

里面有两个女神的所有信息,包括女神的名字、手机、微信、身高、学校、备胎集合等信息。

接下来我们就要对这些数据结构进行描述。

描述数据池中数据结构

const { buildSchema } = require('graphql');

// 描述数据结构 schema
var schema = buildSchema(`
    type Wheel {
        name: String,
        money: String
    }
    type Info {
        id: Int
        name: String
        iphone: Int
        weixin: String
        height: Int
        school: String
        wheel: [Wheel]
    }
    type Query {
        girls: [Info]
    }
`);

上面这段代码就是女神信息的schema。

首先我们用type Query定义了一个对女神信息的查询,里面包含了很多女孩girls的信息Info,这些信息是一堆数组,所以是[Info]

我们在type Info中描述了一个女孩的所有信息的维度,包括了名字(name)、手机(iphone)、微信(weixin)、身高(height)、学校(school)、备胎集合(wheel)

定义查询规则

得到女神的信息描述(schema)后,就可以自定义获取女神的各种信息组合了。

比如我想和女神认识,只需要拿到她的名字(name)和微信号(weixin)。查询规则代码如下:

const { graphql } = require('graphql');

// 定义查询内容
const query = `
    { 
        girls {
            name
            weixin
        }
    }
`;

// 查询数据
const result = await graphql(schema, query, root)

筛选结果如下:

又比如我想进一步和女神发展,我需要拿到她备胎信息,查询一下她备胎们(wheel)的家产(money)分别是多少,分析一下自己能不能获取优先择偶权。查询规则代码如下:

const { graphql } = require('graphql');

// 定义查询内容
const query = `
    { 
        girls {
            name
            wheel {
                money
            }
        }
    }
`;

// 查询数据
const result = await graphql(schema, query, root)

筛选结果如下:

我们通过女神的例子,展现了如何通过graphQL按需加载数据。

映射到我们业务具体场景中,天秀接口返回的每条数据都包含100个字段,我们配置schema,获取其中的10个字段,这样就避免了剩下90个不必要字段的传输。

graphQL还有另一个好处就是可以灵活配置,这个接口需要10个字段,另一个接口要5个字段,第n个接口需要另外x个字段

按照传统的做法我们要做出n个接口才能满足,现在只需要一个接口配置不同schema就能满足所有情况了。

感悟

在生活中,咱们舔狗真的很缺少graphQL按需加载的思维

渣男渣女,各取所需

你的真情在名媛面前不值一提

我们要学会投其所好

上来就亮车钥匙,没有车就秀才艺

今晚我有一条祖传的染色体想与您分享一下

行就行,不行就换下一个

直奔主题,简单粗暴

缓存 -> redis

第二个优化手段,使用redis缓存

天秀老接口内部调用了另外三个老接口,而且是串行调用,极其耗时耗资源,秀到你头皮发麻

我们用redis来缓存天秀接口的聚合数据,下次再调用天秀接口,直接从缓存中获取数据即可,避免高耗时的复杂调用,简化后代码如下:

const redis = require("redis");
const { promisify } = require("util");

// 链接redis服务
const client = redis.createClient(6379, '127.0.0.1');

// promise化redis方法,以便用async/await
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);

async function list() {
    // 先获取缓存中数据,没有缓存就去拉取天秀接口
    let result = await getAsync("缓存");
    if (!result) {
          // 拉接口
          const data = await 天秀接口();
          result = data;
          // 设置缓存数据
          await setAsync("缓存", data)
    }
       return result;
}

list(); 

先通过getAsync来读取redis缓存中的数据,如果有数据,直接返回,绕过接口调用,如果没有数据,就会调用天秀接口,然后setAsync更新到缓存中,以便下次调用。因为redis存储的是字符串,所以在设置缓存的时候,需要加上JSON.stringify(data),为了便于大家理解,我就不加了,会把具体细节代码放在github中。

将数据放在redis缓存里有几个好处

可以实现多接口复用、多机共享缓存

这就是传说中的云备胎

追求一个女神的成功率是1%

同时追求100个女神,那你获取到一个女神的概率就是100%

鲁迅《狂人日记》里曾说过:“舔一个是舔狗,舔一百个你就是战狼

你是想当舔狗还是当战狼?

来吧,缓存用起来,redis用起来

轮询更新 -> schedule

最后一个优化手段:轮询更新 -> schedule

女神的备胎用久了,会定时换一批备胎,让新鲜血液进来,发现新的快乐

缓存也一样,需要定时更新,保持与数据源的一致性,代码如下:

const schedule = require('node-schedule');

// 每个小时更新一次缓存
schedule.scheduleJob('* * 0 * * *', async () => {
    const data = await 天秀接口();
    // 设置redis缓存数据
    await setAsync("缓存", data)
});

我们用node-schedule这个库来轮询更新缓存,* * 0 * * *这个的意思就是设置每个小时的第0分钟就开始执行缓存更新逻辑,将获取到的数据更新到缓存中,这样其他接口和机器在调用缓存的时候,就能获取到最新数据,这就是共享缓存和轮询更新的好处。

早年我在当舔狗的时候,就将轮询机制发挥到淋漓尽致

每天向白名单里的女神,定时轮询发消息

无限循环云跪舔三件套:

  • “啊宝贝,最近有没有想我”
  • “啊宝贝早安安”
  • “宝贝晚安,么么哒”

虽然女神依然看不上我

但仍然时刻准备着为女神服务!

结尾

经过以上三个方法优化后

接口请求耗时从3s降到了860ms

这些代码都是从业务中简化后的逻辑

真实的业务场景远比这要复杂:分段式数据存储、主从同步 读写分离、高并发同步策略等等

每一个模块都晦涩难懂

就好像每一个女神都高不可攀

屌丝战胜了所有bug,唯独战胜不了她的心

受伤了只能在深夜里独自买醉

但每当梦到女神打开我做的页面

被极致流畅的体验惊艳到

在精神高潮中享受灵魂升华

那一刻

我觉得我又行了

(完)

代码地址:github

作者:第一名的小蝌蚪,公众号:前端屌丝
查看原文

赞 17 收藏 6 评论 6

mokeyWie 发布了文章 · 11月9日

通过GitHub Action自动部署Maven项目

前言

要把自己的 JAVA 项目发布到 Maven 中央仓库上,这个过程非常的麻烦,而且由于 Maven 中央仓库的严谨性,每次发布都需要登录到Nexus网站手动进行流程确认,并不支持纯命令行式的部署,导致无法做到真正的CI/CD,为了弥补这一点,我抓包分析了一下Nexus API并且开发了一个Github Action(maven-nexus-release)用于自动的CloseRelease,从而达到真正的全自动部署。

  • 效果图
已经有发布 jar 包到中央仓库的老司机应该都明白发布 jar 包有多麻烦,没有发布过但是想把自己开源项目发布到Maven中央仓库的可以先参考下我之前的一篇文章:发布 jar 包到 maven 中央仓库

使用

首先最好是对 Github Action 有一定的了解,如果不了解也没关系,可以通过我之前的文章快速过一遍:Github Actions 尝鲜

准备

托管在 Github 上的 Maven 项目

需要调整pom.xmlmaven-gpg-plugin插件的配置,示例:

 <plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-gpg-plugin</artifactId>
   <executions>
       <execution>
           <id>sign-artifacts</id>
           <phase>verify</phase>
           <goals>
               <goal>sign</goal>
           </goals>
       </execution>
   </executions>
   <configuration>
       <!-- 这个configuration必须配置,用于gpg非交互式密码输入 -->
       <gpgArguments>
           <arg>--pinentry-mode</arg>
           <arg>loopback</arg>
       </gpgArguments>
   </configuration>
 </plugin>

Nexus 用户名和密码

登录到https://oss.sonatype.org的账号和密码。

gpg private key

Base64编码的 gpg 私钥,通过命令行导出:

  • 列出秘钥
gpg --list-secret-keys --keyid-format LONG
------------------------------------------------
sec   rsa4096/2A6B618785DD7899 2020-11-05 [SC]
      992BB9305698C72B846EF4982A6B618785DD7899
uid                 [ultimate] monkeyWie <liwei-8466@qq.com>
ssb   rsa4096/F8E9F8CBD90028C5 2020-11-05 [E]

找到用于发布 jar 包的 key,这里示例中的是2A6B618785DD7899

  • 导出私钥
gpg --armo --export-secret-keys 2A6B618785DD7899

注意私钥是从-----BEGIN PGP PRIVATE KEY BLOCK-----一直到-----END PGP PRIVATE KEY BLOCK-----,而不是仅仅是中间这一段文本。

gpg passphrase

在生成 gpg 秘钥的时候会需要输入一个短密码,应该还记得吧。

将秘钥配置到 Github Secrets 中

  1. 进入 Github 项目主页,然后找到 Settings 选项。
  2. 进入Secrets菜单
  3. 把刚刚准备好的秘钥一一创建
    在右边有New secret按钮用于创建秘钥,将刚刚的秘钥内容创建并给定对应的名称,示例:

最终 Secrets 如下:

编写 Github Action 配置文件

在项目根目录下新建.github/workflows/deploy.yml文件,内容如下:

name: deploy

on:
  # 支持手动触发构建
  workflow_dispatch:
  release:
    # 创建release的时候触发
    types: [published]
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      # 拉取源码
      - uses: actions/checkout@v2
      # 安装JDK环境
      - name: Set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      # 设置Maven中央仓库配置
      - name: Set up Apache Maven Central
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
          server-id: releases
          # Nexus用户名环境变量
          server-username: MAVEN_USERNAME
          # Nexus密码环境变量
          server-password: MAVEN_CENTRAL_TOKEN
          # gpg短密码环境变量
          gpg-passphrase: MAVEN_GPG_PASSPHRASE
          # gpg私钥
          gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
      # 推送jar包至maven中央仓库
      - name: Publish to Apache Maven Central
        # 执行maven deploy命令
        run: mvn clean deploy
        # 环境变量设置
        env:
          # Nexus用户名,如果觉得不想暴露也可以配置到secrets中
          MAVEN_USERNAME: xxx
          # Nexus密码
          MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
          # gpg短密码
          MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
      # Nexus自动部署
      - name: Release on nexus
        uses: monkeyWie/maven-nexus-release@v1
        with:
          # Nexus用户名
          maven-repo-server-username: xxx
          # Nexus密码
          maven-repo-server-password: ${{ secrets.MAVEN_CENTRAL_TOKEN }}

把代码推送到 Github 上,就可以看到对应的Action了,上面示例中有两种方式来触发构建:

  • 手动触发
    通过 Github 可以手动的触发构建,方便测试,操作如下图:
  • 发布 release 时自动触发
    在 Github 项目中创建 release,会自动的触发构建,适用于项目稳定之后。

后记

以上步骤都在我的项目proxyee中通过验证,另外maven-nexus-release项目还是刚起步,功能可能不够完善,大家如果有什么好的想法和建议欢迎提出 issue 和 pr。

顺便小小的安利下proxyee,它是基于netty编写的 HTTP 代理服务器,支持代理HTTP+HTTPS+WebSocket,并且支持HTTPHTTPS抓包,感兴趣的可以 Star 一下。

我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

wechat

查看原文

赞 2 收藏 2 评论 8

mokeyWie 发布了文章 · 10月29日

使用免费的HTTPS证书

前言

众所周知 HTTPS 是保证 HTTP 通讯安全的协议,网站启用 HTTPS 可以避免很多安全性的问题, 而且 Chrome 浏览器 从 68 版本开始直接将 HTTP 网站标记为不安全了。

所以把网站升级成 HTTPS 自然是大势所趋,不过启用 HTTPS 有个最重要的问题是 HTTPS 证书要花钱!如果每年额外花钱去购买 HTTPS 证书,那也是一笔很大的开销。那么有没有免费的HTTPS证书可以用呢,查了下资料有个叫Let’s Encrypt的项目就提供了免费签发 HTTPS 证书的服务,这里记录下如何使用Let’s Encrypt来签发证书。

certbot 介绍

certbot是用于从 Let's Encrypt 获取证书的命令行工具,代码开源在github上。

使用certbot命令行工具可以轻松的实现HTTPS证书签发,在签发证书之前,需要证明签发的域名是属于你控制的,目前certbot有两种验证方式:

  1. HTTP
    HTTP 方式就是certbot会生成一个特定的文件名和文件内容,要求放在你对应域名下对应路径(/.well-known/acme-challenge/)下,然后certbot再通过 HTTP 请求访问到此文件,并且文件内容与生成时候的一致。

    例如:certbot生成文件名check和内容!@#$%^,你需要申请的域名为baidu.com,则certbot访问http://baidu.com/.well-known/acme-challenge/check来校验是否与生成的内容一致。

  2. DNS
    DNS 则是certbot生成一段特定的文本,要求在你对应域名中配置一条对应子域名(_acme-challenge)的TXT类型解析记录。

    例如:certbot生成内容!@#$%^,你需要申请的域名为baidu.com,则需要添加一条_acme-challenge.baidu.comTXT类型解析记录,值为之前生成的内容。

在域名验证通过之后,certbot就可以签发HTTPS证书了,注意在此验证步骤基础上,certbot提供了很多开箱即用的自动验证方案,但是都不符合我的需求,原因是我需要支持通配符域名的证书,但是这种证书只支持DNS验证方式,而官方提供的DNS插件中并没有支持我用的阿里云DNS,所以只能自己去实现 阿里云的 DNS 自动校验。

使用 certbot 签发 HTTPS 证书

通过官网教程可以选择对应操作系统,并获取安装步骤:

image

这里我选择的Debian 9,根据官网的提示进行安装:

sudo apt-get install certbot -t stretch-backports

注:如果install失败可以先执行下 apt-get update

开始签发证书

certbot certonly --cert-name pdown.org -d *.pdown.org,*.proxyee-down.com --manual --register-unsafely-without-email  --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

这里签发了一个支持*.pdown.org*.proxyee-down.com通配符域名的证书,注意如果是通配符域名证书需要指定--server https://acme-v02.api.letsencrypt.org/directory

示例:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Registering without email!

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for pdown.org
dns-01 challenge for proxyee-down.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.pdown.org with the following value:

Axdqtserd184wvJc86Dxen386UXqbK2wrgb-*******

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

这里会生成一串随机字符并阻塞住,需要去设置一条对应的 TXT 类型的 DNS 解析记录再继续,在设置好之后可以用nslookup进行本地验证:

nslookup -type=txt _acme-challenge.pdown.org
服务器:  UnKnown
Address:  192.168.200.200

非权威应答:
_acme-challenge.pdown.org       text =

        "Tit0SAHaO3MVZ4S-d6CjKLv6Z-********"

本地验证通过之后按回车键继续,接着 Let's Encrypt 就会校验这个 DNS 解析记录是否正确,校验通过后就会进行下一个域名的验证直到全部验证通过。

Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/pdown.org/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/pdown.org/privkey.pem
   Your cert will expire on 2019-12-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

当验证通过的时候会输出证书生成的目录,里面会包含证书和对应的私钥,这里目录是/etc/letsencrypt/live/pdown.org/

证书截图:
image

image

这样证书就生成好了,之后只需要把证书和私钥配置到nginx中就可以用https访问了。

使用 certbot hook 自动续签

上面证书虽然是生成好了,但是证书的有效期只有三个月,意味着每过三个月就得重新签发一个新的证书,一不注意证书就过期了,而且每次手动签发都非常的繁琐需要去手动设置 DNS 解析,所以certbot提供了一种自动续签的方案:hook

在创建证书的时候certbot提供了两个hook参数:

  • manual-auth-hook
    指定用于验证域名的脚本文件
  • manual-cleanup-hook
    指定用于清理的脚本文件,即验证完成之后

通过自定义这两个脚本就可以做到自动续签了,文档参考pre-and-post-validation-hooks

在此基础上,官方已经提供了很多云厂商的自动续签方案,但是我用的阿里云官方并没有提供,于是参照官网文档,写了一个基于阿里云的自动续签脚本,在验证域名的脚本中通过阿里提供的 DNS API 添加一条域名解析记录,在验证完成之后再把刚刚那条域名解析记录删除,命令行调用如下:

certbot certonly --cert-name pdown.org -d *.pdown.org,*.proxyee-down.com --manual --register-unsafely-without-email --manual-auth-hook /path/to/dns/authenticator.sh --manual-cleanup-hook /path/to/dns/cleanup.sh --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

为了方便使用,提供了一个docker镜像,通过环境变量将阿里云 API 调用的 AK 传递就可以生成和续签证书了。

  • 启动容器
docker run \
--name cert \
-itd \
-v /etc/letsencrypt:/etc/letsencrypt \
-e ACCESS_KEY_ID=XXX \
-e ACCESS_KEY_SECRET=XXX \
liwei2633/certbot-aliyun
  • 首次创建证书
docker exec -it cert ./create.sh *.pdown.org

创建过程中会等待一段时间,来确保 dns 记录生效,完成之后在/etc/letsencrypt/live目录下可以找到对应的证书文件

  • 续签证书
docker exec cert ./renew.sh

代码开源在github,欢迎 start。

我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

wechat

查看原文

赞 4 收藏 4 评论 0

mokeyWie 发布了文章 · 10月21日

Go语言HTTP服务生命周期

在 go 语言里启动一个 http 服务非常简单,只需要一行代码http.ListenAndServe()就可以搞定,这个方法会一直阻塞着直到进程关闭,如果这个时候来了些特殊的需求比如:

  • 监听服务启动
  • 手动关闭服务
  • 监听服务关闭

在 go 中应该怎么实现呢?下面来一一举例。

监听服务启动

方法一(推荐)

Listen步骤拆分出来,先监听端口,再绑定到server上,代码示例:

l, _ := net.Listen("tcp", ":8080")
// 服务启动成功,进行初始化
doInit()
// 绑定到server上
http.Serve(l, nil)

方法二

通过一个协程去轮询监听服务启动状态,代码示例:

go func() {
    for {
        if _, err := net.Dial("tcp", "127.0.0.1:8080"); err == nil {
            // 服务启动成功,进行初始化
            doInit()
            //退出协程
            break
        }
        // 每隔一秒检查一次服务是否启动成功
        time.Sleep(time.Second)
    }
}()
http.ListenAndServe(":8080", nil)

手动关闭服务

优雅关闭(推荐)

http包中并没有暴露服务的关闭方法,通过http.ListenAndServe()方法启动的 http 服务默认帮我们创建了一个*http.Server对象,源码如下:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

实际上在*http.Server中是有提供Shutdown方法的,所以我们只需要手动构造一个*http.Server对象,就可以进行优雅关闭了,代码示例:

srv := &http.Server{Addr: ":8080"}
go func(){
    // 10秒之后关闭服务
    time.Sleep(time.Second * 10)
    srv.Shutdown(context.TODO())
}()
// 启动服务
srv.ListenAndServe()

强制关闭

强制关闭和上面步骤是一样的,只是调用的方法换成了srv.Close(),这会导致所有的请求立即中断,所以需要特别注意。

监听服务关闭

当我们手动将服务关闭之后,srv.ListenAndServe()方法就会立即返回,这里需要注意的是该方法会返回一个error,当然这个error是一个特殊的 error http.ErrServerClosed,帮助我们区分是否为正常的服务关闭,所以需要对它特殊处理下,代码示例:

if err := server.ListenAndServe(); err != nil {
    // 服务关闭,进行处理
    doShutdown()
    if err != http.ErrServerClosed{
        // 异常宕机,打印错误信息
        log.Fatal(err)
    }
}

参考资料

我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

wechat

查看原文

赞 2 收藏 1 评论 0

认证与成就

  • 获得 434 次点赞
  • 获得 43 枚徽章 获得 2 枚金徽章, 获得 12 枚银徽章, 获得 29 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-07
个人主页被 4.9k 人浏览