2023年4月

1. 说说gulp和webpack的区别

开放式题目

Gulp强调的是前端开发的工作流程。我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让Gulp执行这些task,从而构建项目的整个前端开发流程。通俗一点来说,“Gulp就像是一个产品的流水线,整个产品从无到有,都要受流水线的控制,在流水线上我们可以对产品进行管理。”

Webpack是一个前端模块化方案,更侧重模块打包。我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。 Webpack就是需要通过其配置文件(Webpack.config.js)中 entry 配置的一个入口文件(JS文件),然后在解析过程中,发现其他的模块,如scss等文件,再调用配置的loader或者插件对相关文件进行解析处理。

虽然Gulp 和 Webpack都是前端自动化构建工具,但看2者的定位就知道不是对等的。Gulp严格上讲,模块化不是他强调的东西,旨在规范前端开发流程。Webpack更明显的强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。

2. 小程序路由跳转

  1. 通过组件navigator跳转,设置url属性指定跳转的路径,设置open-type属性指定跳转的类型(可选),open-type的属性有 redirect, switchTab, navigateBack
// redirect 对应 API 中的 wx.redirect 方法
<navigator url="/page/redirect/redirect?title=redirect" open-type="redirect">在当前页打开</navigator>

// navigator 组件默认的 open-type 为 navigate 
<navigator url="/page/navigate/navigate?title=navigate">跳转到新页面</navigator>

// switchTab 对应 API 中的 wx.switchTab 方法
<navigator url="/page/index/index" open-type="switchTab">切换 Tab</navigator>

// reLanch 对应 API 中的 wx.reLanch 方法
<navigator url="/page/redirect/redirect?title=redirect" open-type="redirect">//关闭所有页面,打开到应用内的某个页面

// navigateBack 对应 API 中的 wx.navigateBack 方法
<navigator url="/page/index/index" open-type="navigateBack">关闭当前页面,返回上一级页面或多级页面</navigator>
  1. 通过api跳转,wx.navigateTo() , wx.navigateBack(), wx.redirectTo() , wx.switchTab(), wx.reLanch()
wx.navigateTo({
  url: 'page/home/home?user_id=1'  // 页面 A
})
wx.navigateTo({
  url: 'page/detail/detail?product_id=2'  // 页面 B
})
// 跳转到页面 A
wx.navigateBack({
  delta: 2  //返回指定页面
})

// 关闭当前页面,跳转到应用内的某个页面。
wx.redirectTo({
url: 'page/home/home?user_id=666666'
})

// 跳转到tabBar页面(在app.json中注册过的tabBar页面),同时关闭其他非tabBar页面。
wx.switchTab({
url: 'page/index/index'
})

// 关闭所有页面,打开到应用内的某个页面。
wx.reLanch({
url: 'page/home/home?user_id=666666'
})

3. 阐述一下http1.0与http2.0的区别,及http和https区别

1、HTTP1.0和HTTP1.1的一些区别

缓存处理,HTTP1.0中主要使用Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control…

带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(Partial Content)
错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除…

Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)

长连接,HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点

2、HTTP2.0和HTTP1.X相比的新特性

新的二进制格式(Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮

header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小

服务端推送(server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了

4. 谈谈宏任务与微任务的理解,举一个宏任务与微任务的api

要理解宏任务(macrotask)与微任务(microtask),就必须了解javascript中的事件循环机制(event loop)以及js代码的运行方式

要了解js代码的运行方式,得先搞懂以下几个概念:

JS是单线程执行

单线程指的是JS引擎线程

宿主环境

JS运行的环境,一般为浏览器或者Node

执行栈

是一个存储函数调用的栈结构,遵循先进后出的原则

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它执行,如何传递,这就是
事件循环(event loop)
所做的事情:当js执行栈空闲时,事件循环机制会从任务队列中提取第一个任务进入到执行栈执行,优先提取微任务(microtask),待微任务队列清空后,再提取宏任务(macrotask),并不断重复该过程

在实际应用中,宏任务(macrotask)与微任务(microtask)的API分别如下:

宏任务
setTimeout/setInterval
ajax
setImmediate (Node 独有)
requestAnimationFrame (浏览器独有)
I/O
UI rendering (浏览器独有)
微任务
process.nextTick (Node 独有)
Promise
Object.observe
MutationObserver

5. 如何在TS中对函数的返回值进行类型约束

ts中函数参数的类型定义

函数的参数可能是一个,也可能是多个,有可能是一个变量,一个对象,一个函数,一个数组等等。

1.函数的参数为单个或多个单一变量的类型定义

function fntA(one, two, three) {
    // 参数 "two" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
    return one + two + three
}
const aResult = fntA(1, '3', true)

修改后:

function fntA(one: number, two: string, three: boolean) {
    return one + two + three
}
const aResult1 = fntA(1, '3', true)
// 如果函数的参数为单个或者多个变量的时候,只需要为这些参数进行静态类型下的基础类型定义就行

2. 函数的参数为数组的类型定义

function fntB(arr) {
    //参数 "arr" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
    return arr[0]
}
const bResult = fntB([1, 3, 5])

修改后:

function fntB(arr: number[]) {
    return arr[0]
}
const bResult1 = fntB([1, 3, 5])
// 如果参数是数组时,只需要为这些变量进行对象类型下的数组类型定义

3.函数的参数为对象的类型定义

function fntC({ one, two }) {
    return one + two
}
const cResult = fntC({ one: 6, two: 10 })

修改后:

function fntC({ one, two }: { one: number, two: number }) {
    return one + two
}
const cResult1 = fntC({ one: 6, two: 10 })
// 如果参数是对象,只需要为这些变量进行对象类型下的对象类型定义

4.函数的参数为函数的类型定义

function fntD(callback) {
    //参数 "callback" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型
    callback(true)
}
function callback(bl: boolean): boolean {
    console.log(bl)
    return bl
}
const dResult = fntD(callback)

修改后:

function fntD(callback: (bl: boolean) => boolean) {
    callback(true)
}
function callback(bl: boolean): boolean {
    console.log(bl)
    return bl
}
const dResult = fntD(callback)
// 如果参数是函数,只需要为参数进行对象类型下的函数类型定义即可

ts中函数返回值的类型定义

当函数有返回值时,根据返回值的类型在相应的函数位置进行静态类型定义即可

返回数字:

function getTotal2(one: number, two: number): number {
    return one + two;
}
const total2 = getTotal(1, 2);
// 返回值为数字类型

返回布尔值

function getTotal2(one: number, two: number): boolean {
    return Boolean(one + two);
}
const total2 = getTotal(1, 2);
// 返回值为布尔类型

返回字符串

function getTotal2(one: string, two: string): string{
    return Bone + two;
}
const total2 = getTotal('1', '2');
// 返回值为字符串

返回对象

function getObj(name: string, age: number): { name: string, age: number } {
    return {name,age}
}
getObj('小红',16)
// 返回值为对象

返回数组

function getArr(arr: number[]) :number[]{
    let newArr = [...arr]
    return newArr
}
getArr([1,2,3,4])
// 返回值为数组

函数返回值为underfinde,仅仅时为了在内部实现某个功能,我们就可以给他一个类型注解void,代表没有任何返回值,

function sayName() {
    console.log('hello,world')
}

修改后:

function sayName1(): void {
    console.log('无返回值')
}

当函数没有返回值时

// 因为总是抛出异常,所以 error 将不会有返回值
// never 类型表示永远不会有值的一种类型
function error(message: string): never {
    throw new Error(message);
}

6. 平时工作中有是否有接触linux系统?说说常用到linux命令?

1、常用的目录操作命令

创建目录 mkdir <目录名称>
删除目录 rm <目录名称>
定位目录 cd <目录名称>
查看目录文件 ls ll
修改目录名 mv <目录名称> <新目录名称>
拷贝目录 cp <目录名称> <新目录名称>

2、常用的文件操作命令

创建文件 touch <文件名称> vi <文件名称>
删除文件 rm <文件名称>
修改文件名 mv <文件名称> <新文件名称>
拷贝文件 cp <文件名称> <新文件名称>

3、常用的文件内容操作命令

查看文件 cat <文件名称> head <文件名称> tail <文件名称>
编辑文件内容 vi <文件名称>
查找文件内容 grep '关键字' <文件名称>

7. 常见的 HTTP Method 有哪些? GET/POST 区别?

1、常见的HTTP方法

GET:获取资源
POST:传输资源
PUT:更新资源
DELETE:删除资源
HEAD:获得报文首部

2、GET/POST的区别

GET在浏览器回退时是无害的,而POST会再次提交请求
GET请求会被浏览器主动缓存,而POST不会,除非手动设置
GET请求参数会被完整保留在浏览器的历史记录里,而POST中的参数不会被保留
GET请求在URL中传送的参数是有长度限制的,而POST没有限制
GET参数通过URL传递,POST放在Request body中
GET请求只能进行 url 编码,而POST支持多种编码方式
GET产生的URL地址可以被收藏,而POST不可以
对参数的数据类型,GET只接受ASCII字符,而POST没有限制
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息

8. vue打包内存过大,怎么使用webpack来进行优化

开放式题目

打包优化的目的

1、优化项目启动速度,和性能

2、必要的清理数据

3、性能优化的主要方向

cdn加载

-压缩js

减少项目在首次加载的时长(首屏加载优化)

4、目前的解决方向

cdn加载不比多说,就是改为引入外部js路径

首屏加载优化方面主要其实就两点

第一:
尽可能的减少首次加载的文件体积,和进行分布加载

第二:
首屏加载最好的解决方案就是ssr(服务端渲染),还利于seo
但是一般情况下没太多人选择ssr,因为只要不需要seo,ssr更多的是增加了项目开销和技术难度的。

1、路由懒加载

在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 (split point): import(’./Fee.vue’) // 返回 Promise如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

const Fee = () => import('./Fee.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

const router = new VueRouter({
  routes: [
    { path: '/fee', component: Fee }
  ]
})

2、服务器和webpack打包同时配置Gzip

Gzip是GNU zip的缩写,顾名思义是一种压缩技术。它将浏览器请求的文件先在服务器端进行压缩,然后传递给浏览器,浏览器解压之后再进行页面的解析工作。在服务端开启Gzip支持后,我们前端需要提供资源压缩包,通过Compression-Webpack-Plugin插件build提供压缩

需要后端配置,这里提供nginx方式:

http:{ 
      gzip on; #开启或关闭gzip on off
      gzip_disable "msie6"; #不使用gzip IE6
      gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)
      gzip_buffers 4 16k; #buffer 不用修改
      gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长
      gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #  压缩文件类型 
}

// 安装插件

  $ cnpm i --save-dev compression-webpack-plugin

// 在vue-config.js 中加入

  const CompressionWebpackPlugin = require('compression-webpack-plugin');
  const productionGzipExtensions = [
    "js",
    "css",
    "svg",
    "woff",
    "ttf",
    "json",
    "html"
  ];
  const isProduction = process.env.NODE_ENV === 'production';
  .....
  module.exports = {
  ....
   // 配置webpack
   configureWebpack: config => {
    if (isProduction) {
     // 开启gzip压缩
     config.plugins.push(new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.js$|\.html$|\.json$|\.css/,
      threshold: 10240,
      minRatio: 0.8
     }))
    }
   }
  }

3、优化打包chunk-vendor.js文件体积过大

当我们运行项目并且打包的时候,会发现chunk-vendors.js这个文件非常大,那是因为webpack将所有的依赖全都压缩到了这个文件里面,这时我们可以将其拆分,将所有的依赖都打包成单独的js。

  // 在vue-config.js 中加入

  .....
  module.exports = {
  ....
   // 配置webpack
   configureWebpack: config => {
    if (isProduction) {
      // 开启分离js
      config.optimization = {
        runtimeChunk: 'single',
        splitChunks: {
          chunks: 'all',
          maxInitialRequests: Infinity,
          minSize: 20000,
          cacheGroups: {
            vendor: {
              test: /[\\/]node_modules[\\/]/,
              name (module) {
                // get the name. E.g. node_modules/packageName/not/this/part.js
                // or node_modules/packageName
                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
                // npm package names are URL-safe, but some servers don't like @ symbols
                return `npm.${packageName.replace('@', '')}`
              }
            }
          }
        }
      };
    }
   }
  }

// 至此,你会发现原先的vender文件没有了,同时多了好几个依赖的js文件

4、启用CDN加速

用Gzip已把文件的大小减少了三分之二了,但这个还是得不到满足。那我们就把那些不太可能改动的代码或者库分离出来,继续减小单个chunk-vendors,然后通过CDN加载进行加速加载资源。

  // 修改vue.config.js 分离不常用代码库
  // 如果不配置webpack也可直接在index.html引入

  module.exports = {
   configureWebpack: config => {
    if (isProduction) {
     config.externals = {
      'vue': 'Vue',
      'vue-router': 'VueRouter',
      'moment': 'moment'
     }
    }
   }
  }

// 在public文件夹的index.html 加载

  <script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.runtime.min.js"></script>
  <script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>

5、完整vue.config.js代码

  const path = require('path')

  // 在vue-config.js 中加入
  // 开启gzip压缩
  const CompressionWebpackPlugin = require('compression-webpack-plugin');
  // 判断开发环境
  const isProduction = process.env.NODE_ENV === 'production';

  const resolve = dir => {
    return path.join(__dirname, dir)
  }

  // 项目部署基础
  // 默认情况下,我们假设你的应用将被部署在域的根目录下,
  // 例如:https://www.my-app.com/
  // 默认:'/'
  // 如果您的应用程序部署在子路径中,则需要在这指定子路径
  // 例如:https://www.foobar.com/my-app/
  // 需要将它改为'/my-app/'
  // iview-admin线上演示打包路径: https://file.iviewui.com/admin-dist/
  const BASE_URL = process.env.NODE_ENV === 'production'
    ? '/'
    : '/'

  module.exports = {
    //webpack配置
    configureWebpack:config => {
      // 开启gzip压缩
      if (isProduction) {
        config.plugins.push(new CompressionWebpackPlugin({
          algorithm: 'gzip',
          test: /\.js$|\.html$|\.json$|\.css/,
          threshold: 10240,
          minRatio: 0.8
        }));
        // 开启分离js
        config.optimization = {
          runtimeChunk: 'single',
          splitChunks: {
            chunks: 'all',
            maxInitialRequests: Infinity,
            minSize: 20000,
            cacheGroups: {
              vendor: {
                test: /[\\/]node_modules[\\/]/,
                name (module) {
                  // get the name. E.g. node_modules/packageName/not/this/part.js
                  // or node_modules/packageName
                  const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
                  // npm package names are URL-safe, but some servers don't like @ symbols
                  return `npm.${packageName.replace('@', '')}`
                }
              }
            }
          }
        };
        // 取消webpack警告的性能提示
        config.performance = {
          hints:'warning',
              //入口起点的最大体积
              maxEntrypointSize: 50000000,
              //生成文件的最大体积
              maxAssetSize: 30000000,
              //只给出 js 文件的性能提示
              assetFilter: function(assetFilename) {
            return assetFilename.endsWith('.js');
          }
        }
      }
    },
    // Project deployment base
    // By default we assume your app will be deployed at the root of a domain,
    // e.g. https://www.my-app.com/
    // If your app is deployed at a sub-path, you will need to specify that
    // sub-path here. For example, if your app is deployed at
    // https://www.foobar.com/my-app/
    // then change this to '/my-app/'
    publicPath: BASE_URL,
    // tweak internal webpack configuration.
    // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
    devServer: {
      host: 'localhost',
      port: 8080, // 端口号
      hotOnly: false,
      https: false, // https:{type:Boolean}
      open: true, //配置自动启动浏览器
      proxy:null // 配置跨域处理,只有一个代理

    },
    // 如果你不需要使用eslint,把lintOnSave设为false即可
    lintOnSave: true,
    css:{
      loaderOptions:{
        less:{
          javascriptEnabled:true
        }
      },
      extract: true,// 是否使用css分离插件 ExtractTextPlugin
      sourceMap: false,// 开启 CSS source maps
      modules: false// 启用 CSS modules for all css / pre-processor files.
    },
    chainWebpack: config => {
      config.resolve.alias
        .set('@', resolve('src')) // key,value自行定义,比如.set('@@', resolve('src/components'))
        .set('@c', resolve('src/components'))
    },
    // 打包时不生成.map文件
    productionSourceMap: false
    // 这里写你调用接口的基础路径,来解决跨域,如果设置了代理,那你本地开发环境的axios的baseUrl要写为 '' ,即空字符串
    // devServer: {
    //   proxy: 'localhost:3000'
    // }
  }

9. git经常用哪些指令

产生代码库

新建一个git代码库

git init

下载远程项目和它的整个代码历史

git clone 远程仓库地址

配置

显示配置

git config --list [--global]

编辑配置

git config -e [--global]

设置用户信息

git config [--global] user.name "名"
git config [--global] user.email "邮箱地址"

暂存区文件操作

增加文件到暂存区

# 1.添加当前目录的所有文件到暂存区
git add .
# 2.添加指定目录到暂存区,包括子目录
git add [dir]
# 3.添加指定文件到暂存区
git add [file1] [file2] ...

在暂存区中删除文件

# 删除工作区文件,并且将这次删除放入暂存区
git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
git rm --cached [file]

重命名暂存区文件

# 改名文件,并且将这个改名放入暂存区
git mv [file-original] [file-renamed]

代码提交

# 提交暂存区到仓库区
git commit -m [message]

分支操作

# 列出所有本地分支
git branch

# 列出所有远程分支
git branch -r

# 列出所有本地分支和远程分支
git branch -a

# 新建一个分支,但依然停留在当前分支
git branch [branch-name]

# 新建一个分支,并切换到该分支
git checkout -b [branch]

# 新建一个分支,指向指定commit
git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
git checkout [branch-name]

# 切换到上一个分支
git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
git merge [branch]

# 选择一个commit,合并进当前分支
git cherry-pick [commit]

# 删除分支
git branch -d [branch-name]

# 删除远程分支
git push origin --delete [branch-name]
git branch -dr [remote/branch]

信息查看

# 显示有变更的文件
git status

# 显示当前分支的版本历史
git log

# 显示commit历史,以及每次commit发生变更的文件
git log --stat

# 搜索提交历史,根据关键词
git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
git log [tag] HEAD --grep feature

# 显示过去5次提交
git log -5 --pretty --oneline

同步操作

# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]

# 上传本地指定分支到远程仓库
git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force

# 推送所有分支到远程仓库
git push [remote] --all

撤销操作

# 恢复暂存区的指定文件到工作区
git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
git revert [commit]

10. 协商缓存和强缓存

浏览器缓存主要分为
强强缓存(也称本地缓存)和协商缓存(也称弱缓存)
。浏览器在第一次请求发生后,再次发送请求时:

  • 浏览器请求某一资源时,会先获取该资源缓存的header信息,然后根据header中的Cache-Control和Expires来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的header的信息,所以此次请求不会与服务器进行通信。这里判断是否过期,则是强缓存相关。后面会讲Cache-Control和Expires相关。
  • 如果显示已过期,浏览器会向服务器端发送请求,这个请求会携带第一次请求返回的有关缓存的header字段信息,比如客户端会通过If-None-Match头将先前服务器端发送过来的Etag发送给服务器,服务会对比这个客户端发过来的Etag是否与服务器的相同,若相同,就将If-None-Match的值设为false,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就将If-None-Match的值设为true,返回状态为200,客户端重新机械服务器端返回的数据;客户端还会通过If-Modified-Since头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是最新的,则返回304,客户端继续使用本地缓存。

强缓存

强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。强缓存中,普通刷新会忽略它,但不会清除它,需要强制刷新。浏览器强制刷新,请求会带上Cache-Control:no-cache和Pragma:no-cache

Expires

Expires是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。如我现在这个网页的Expires值是:expires:Fri, 14 Apr 2017 10:47:02 GMT。这个时间代表这这个资源的失效时间,只要发送请求时间是在Expires之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。如果同时出现Cache-Control:max-age和Expires,那么max-age优先级更高。如我主页的response headers部分如下:

cache-control:max-age=691200
expires:Fri, 14 Apr 2017 10:47:02 GMT

Cache-Control

Cache-Control是在http1.1中出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面几个比较常用的设置值:

  • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

  • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

  • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

  • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

  • Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

协商缓存

协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。

普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存,
这也是为什么有时候我们更新一张图片、一个js文件,页面内容依然是旧的,但是直接浏览器访问那个图片或文件,看到的内容却是新的。

这个主要涉及到两组header字段:Etag和If-None-Match、Last-Modified和If-Modified-Since。上面以及说得很清楚这两组怎么使用啦~复习一下:

Etag和If-None-Match

Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。

与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,
由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。

当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。

为什么要有Etag

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

11. 对Event loop的了解?

Javascript是单线程的,那么各个任务进程是按照什么样的规范来执行的呢?这就涉及到Event Loop的概念了,EventLoop是在html5规范中明确定义的;

何为eventloop,javascript中的一种运行机制,用来解决浏览器单线程的问题

Event Loop是一个程序结构,用于等待和发送消息和事件。同步任务、异步任务、微任务、宏任务

javascript单线程任务从时间上分为同步任务和异步任务,而异步任务又分为宏任务(macroTask)和微任务(microTask)

宏任务:主代码块、setTimeOut、setInterval、script、I/O操作、UI渲染

微任务:promise、async/await(返回的也是一个promise)、process.nextTick

在执行代码前,任务队列为空,所以会优先执行主代码块,再执行主代码块过程中,遇到同步任务则立即将任务放到调用栈执行任务,遇到宏任务则将任务放入宏任务队列中,遇到微任务则将任务放到微任务队列中。

主线程任务执行完之后,先检查微任务队列是否有任务,有的话则将按照先入先出的顺序将先放进来的微任务放入调用栈中执行,并将该任务从微任务队列中移除,执行完该任务后继续查看微任务队列中是否还有任务,有的话继续执行微任务,微任务队列null时,查看宏任务队列,有的话则按先进先出的顺序执行,将任务放入调用栈并将该任务从宏任务队列中移除,该任务执行完之后继续查看微任务队列,有的话则执行微任务队列,没有则继续执行宏任务队列中的任务。

需要注意的是:每次调用栈执行完一个宏任务之后,都会去查看微任务队列,如果microtask queue不为空,则会执行微任务队列中的任务,直到微任务队列为空再返回去查看执行宏任务队列中的任务

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')

最终输出

script start
VM70:8 async2 end
VM70:17 Promise
VM70:27 script end
VM70:5 async1 end
VM70:21 promise1
VM70:24 promise2
undefined
VM70:13 setTimeout

解析:

在说返回结果之前,先说一下async/await,async 返回的是一个promise,而await则等待一个promise对象执行,await之后的代码则相当于promise.then()

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}

//等价于
new Promise(resolve => 
resolve(console.log('async2 end'))
).then(res => {
console.log('async1 end')
})

整体分析:先执行同步任务 script start -> async2 end -> await之后的console被放入微任务队列中 -> setTimeOut被放入宏任务队列中 ->promise -> promise.then被放入微任务队列中 -> script end -> 同步任务执行完毕,查看微任务队列 --> async1 end -> promise1 -> promise2 --> 微任务队列执行完毕,执行宏任务队列打印setTimeout

12. node.js如何导出页面数据形成报表

node.js如何导出页面数据形成报表

生成报表并下载是作为web应用中的一个传统功能,在实际项目中有广范的应用,实现原理也很简单,以NodeJS导出数据的方法为例,就是拿到页面数据对应id,然后根据id查询数据库,拿到所需数据后格式化为特点格式的数据,最后导出文件。

在nodejs中,也提供了很多的第三方库来实现这一功能,以node-xlsx导出excel文件为例,实现步骤如下:

  1. 下载node-xlsx
    npm install node-xlsx
  1. 编写接口

以下代码使用express编写,调用接口实现下载功能

    const fs = require('fs');
    const path = require('path');
    const xlsx = require('node-xlsx');
    const express = require('express');
    const router = express.Router();
    const mongo = require('../db');

    router.post('/export', async (req, res) => {
        const { ids } = req.body;
        const query = {
            _id: { $in: ids }
        }
        // 查询数据库获取数据
        let result = await mongo.find(colName, query);
     // 设置表头
     const keys = Object.keys(Object.assign({}, ...result));
     rows[0] = keys;
 
     // 设置表格数据
     const rows = []
     result.map(item => {
         const values = []
         keys.forEach((key, idx) => {
             if (item[key] === undefined) {
                 values[idx] = null;
             } else {
                 values[idx] = item[key];
             }
         })
         rows.push(values);
     });
 
     let data = xlsx.build([{ name: "商品列表", data: rows }]);
     const downloadPath = path.join(__dirname, '../../public/download');
     const filePath = `${downloadPath}/goodslist.xlsx`;
     fs.writeFileSync(filePath, data);
     res.download(filePath, `商品列表.xlsx`, (err) => {
         console.log('download err', err);
     });
 })

13. 说说你对nodejs的了解

我们从以下几方面来看nodejs.

什么是nodejs?

Node.js 是一个开源与跨平台的 JavaScript 运行时环境, 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能.

可以理解为 Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境

非阻塞异步

Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作

例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率

事件驱动

事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数

优缺点

优点:

处理高并发场景性能更佳
适合I/O密集型应用,指的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作

因为Nodejs是单线程

缺点:

不适合CPU密集型应用
只支持单核CPU,不能充分利用CPU
可靠性低,一旦代码某个环节崩溃,整个系统都崩溃

应用场景

借助Nodejs的特点和弊端,其应用场景分类如下:

善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程
大量并发的I/O,应用程序内部并不需要进行非常复杂的处理
与 websocket 配合,开发长连接的实时交互应用程序

具体场景可以表现为如下:

第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程>序
第二大类:基于web、canvas等多人联网游戏
第三大类:基于web的多人实时聊天客户端、聊天室、图文直播
第四大类:单页面浏览器应用程序
第五大类:操作数据库、为前端和移动端提供基于json的API


持续更新ing

学习操作系统原理最好的方法是自己写一个简单的操作系统。


GrapeOS操作系统之前一直运行在模拟器和虚拟机中,今天我们来演示一下GrapeOS在真机上运行的情况。

一、物理机真机

今天演示用的真机是一台ThinkPad笔记本电脑,照片如下:

二、开机桌面

按电脑的电源按钮开机,由于GrapeOS非常小,很快就进入了桌面,照片如下:

三、资源管理器

用鼠标双击桌面上的电脑图标,打开了资源管理器,照片如下:

四、记事本

在资源管理器中双击“NOTEPAD.EXE”打开记事本程序,随便输入一些字符并保存,照片如下:

演示结束。我们可以看到,GrapeOS在真机上的操作和之前在虚拟机上的操作一样。为了更直观的了解,推荐观看视频演示:
https://www.bilibili.com/video/BV16a4y1M7Ja/


配套的代码与资料在:
https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系统交流QQ群:643474045

微服务的概念

微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。
微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的发布时间。

整体式架构 与 微服务架构 的比较

通过整体式架构

所有进程紧密耦合,并可作为单项服务运行。这意味着,如果应用程序的一个进程遇到需求峰值,则必须扩展整个架构。随着代码库的增长,添加或改进整体式应用程序的功能变得更加复杂。这种复杂性限制了试验的可行性,并使实施新概念变得困难。整体式架构增加了应用程序可用性的风险,因为许多依赖且紧密耦合的进程会扩大单个进程故障的影响。

使用微服务架构

将应用程序构建为独立的组件,并将每个应用程序进程作为一项服务运行。这些服务使用轻量级 API 通过明确定义的接口进行通信。这些服务是围绕业务功能构建的,每项服务执行一项功能。由于它们是独立运行的,因此可以针对各项服务进行更新、部署和扩展,以满足对应用程序特定功能的需求。

微服务的特性

自主性

可以对微服务架构中的每个组件服务进行开发、部署、运营和扩展,而不影响其他服务的功能。这些服务不需要与其他服务共享任何代码或实施。各个组件之间的任何通信都是通过明确定义的 API 进行的。

专用性

每项服务都是针对一组功能而设计的,并专注于解决特定的问题。如果开发人员逐渐将更多代码增加到一项服务中并且这项服务变得复杂,那么可以将其拆分成多项更小的服务。

单一职责

每个微服务都需要满足单一职责原则,微服务本身是内聚的,因此微服务通常比较小。每个微服务按业务逻辑划分,每个微服务仅负责自己归属于自己业务领域的功能。

微服务的优势

敏捷性

微服务促进若干小型独立团队形成一个组织,这些团队负责自己的服务。各团队在小型且易于理解的环境中行事,并且可以更独立、更快速地工作。这缩短了开发周期时间。您可以从组织的总吞吐量中显著获益。

灵活扩展

通过微服务,您可以独立扩展各项服务以满足其支持的应用程序功能的需求。这使团队能够适当调整基础设施需求,准确衡量功能成本,并在服务需求激增时保持可用性。

轻松部署

微服务支持持续集成和持续交付,可以轻松尝试新想法,并可以在无法正常运行时回滚。由于故障成本较低,因此可以大胆试验,更轻松地更新代码,并缩短新功能的上市时间。

技术自由

微服务架构不遵循“一刀切”的方法。团队可以自由选择最佳工具来解决他们的具体问题。因此,构建微服务的团队可以为每项作业选择最佳工具。

可重复使用的代码:将软件划分为小型且明确定义的模块,让团队可以将功能用于多种目的。专为某项功能编写的服务可以用作另一项功能的构建块。这样应用程序就可以自行引导,因为开发人员可以创建新功能,而无需从头开始编写代码。

弹性

服务独立性增加了应用程序应对故障的弹性。在整体式架构中,如果一个组件出现故障,可能导致整个应用程序无法运行。通过微服务,应用程序可以通过降低功能而不导致整个应用程序崩溃来处理总体服务故障。

微服务的缺点

当微服务过多时,服务间的通信变得错综复杂,比如:A服务 -> E服务 -> B服务 ... 甚至更多的分支串联,形成一张莫大的蜘蛛网,若要追踪一笔数据... 这对未来的工作变得更加复杂。

作者:[
Sol·wang
] - 博客园,原文出处:
https://www.cnblogs.com/Sol-wang/p/17293829.html

认证授权

参考以往文章:


IdentityServer4 - v4.x 概念理解及运行过程


IdentityServer4 - v4.x .Net中的实践应用

服务限流

为什么要限流。。。削峰,减轻压力,为了确保服务器能够正常持续的平稳运行。
当访问量大于服务器的承载量,我们不希望有服务器的灾难发生;在接收请求的初期,适当的过滤一些请求,或延时处理或忽略掉。
有第三方工具如hystrix、有分布式网关限流如Nginx、未来的.NET7自带限流中间件
AspNetCoreRateLimit
等。
以下按限流算法的理解做一些分享。

限流方式

计数方式、固定窗口方式、滑动窗口方式、令牌桶方式、漏桶方式等。

滑动窗口方式

随着时间的流逝,窗口逐步向前移动;窗口有宽度,也就是时长;窗口内处理的量,也就是量有上限。

数组存放每个请求的时间点;数组首尾时间差不超过定义时长;定义时长可接收的量。

运行示例图

限流滑动窗口示意图

实现过程

  1. 准备一个数组,存储每次请求的时间点;定义时长1s;定义单位时长内可接收请求数量的上限
  2. 本次请求的当前时间点,与数组中最早的请求时间点 比对(数组首尾比对)
  3. 比对差值(秒)在定义的时间内 & 在上限数量的范围内,当前时间点记录到数组,被视为可接收的请求
  4. 比对差值(秒)超过定义时长(1s)或超出上限的请求,被限制/忽略;不加入数组,设置Response后返回
  5. 每次记得移除超出时长的记录,以确保持续接收合规的新请求

限流中间件案例

非完整版 看懂就行

public class RequestLimitingMiddleware
{
    // 单位时间内,可接收的请求数量
    private int _qps = 6;
    // 定义单位时长(秒)
    private readonly int _unit_seconds = 1;
    // 集合存放已接收的请求
    private ConcurrentQueue<DateTime> _backlog_request = new ConcurrentQueue<DateTime>();
        
        
    /// <summary>
    /// 限流方法 - 时间滑动窗口算法,是否限流
    /// </summary>
    /// <returns></returns>
    private bool Limiting()
    {
        // 比对的结果差值
        double _diff_sec = 0;
        // 本次请求时间
        DateTime _curr_req_now = DateTime.Now;


        #region 1、每次先消除已过期的请求(超出时间范围的请求,被定义为系统已处理)
        // 遍历整个集合
        DateTime _disused_req = new DateTime();
        while (_backlog_request.TryPeek(out _disused_req))
        {
            // 超出定义时长的
            if (_curr_req_now.Subtract(_disused_req).TotalSeconds > _unit_seconds)
            {
                // 移除
                _backlog_request.TryDequeue(out _disused_req);
            }
            else
                break;
        }
        #endregion


        #region 2、有积压的请求,取最早的那个请求时间,与本次时间比对,并计算出差值
        DateTime _first_req_now = new DateTime();
        if (_backlog_request.TryPeek(out _first_req_now))
        {
            // 当前请求的时间 与 最早的请求时间 跨度
            _diff_sec = _curr_req_now.Subtract(_first_req_now).TotalSeconds;
        }
        #endregion


        #region 3、是否限制的请求
        // 集合的首尾不能超过单位时长,及数量上限
        if (_diff_sec < _unit_seconds && _backlog_request.Count < _qps)
        {
            // 可接收的新请求 记录到集合
            _backlog_request.Enqueue(_curr_req_now);
            return true;
        }
        // 被视为限制的请求
        return false;
        #endregion
    }


    public Task Invoke(HttpContext context)
    {
        #region 限流方法的应用
        if (!this.Limiting())
        {
            _logger.LogWarning($" ! 被限制的请求,忽略");
            context.Response.StatusCode = (Int16)HttpStatusCode.TooManyRequests;
            context.Response.ContentType = "text/json;charset=utf-8;";
            return context.Response.WriteAsync("抱歉,限流了,请稍后再试。");
        }
        _logger.LogInformation($" + 新增的请求,当前积压 {_backlog_request.Count} req.");
        #endregion


        // 模拟运行消耗时间
        Thread.Sleep(300);
        _next(context);
        return Task.CompletedTask;
    }
}

滑动窗口限流测试

由于设置的1s/6次请求,所以手动可以测试;浏览器快速的敲击F5请求API接口,测试效果如下图:

漏桶方式

看桶内容量,溢出就拒绝;(累加的请求数是否小于上限)

实现逻辑

有上限数量的桶,接收任意请求

随着时间的流逝,上次请求时间到现在,通过速率,计算出桶内应有的量

此量超过上限,拒绝新的请求

直到消耗出空余数量后,再接收新的请求

以上仅通过计算出的剩余的数字,决定是否接收新请求

比如:每秒10个请求上线,还没到下一秒,进来的第11个请求被拒绝

令牌方式

看令牌数量,用完就拒绝;(累减的令牌是否大于0)

假如以秒为单位发放令牌,每秒发10个令牌,当这一秒还没过完,收到了第11个请求,此时令牌干枯了,那就拒绝此请求;

所以每次请求看有没有令牌可用。

实现逻辑

按速率,两次请求的时间差,计算出可生成的令牌数;每个请求减一个令牌

相同时间进来的请求,时间差值为0,所以每次没能生成新的令牌,此请求也消耗一个令牌

直到令牌数等于0,拒绝新请求

跨域

为什么有跨域

源自于浏览器;出于安全的考虑,浏览器默认限制不同站点域名间的通讯,所以 JS/Cookie 只能访问本站点下的内容;叫
同源策略

跨域的原理及策略

浏览器默认是限制跨域的,当然也可以告诉浏览器,怎样的站点间通讯可以取消限制。

Request 或 Response 中追加 Header 的设定:
允许的请求源头

允许的请求动作

允许的Header方式
等。

如:
Access-Control-Allow-Origin:{目标域名Url}

可以用不受限的
*
,允许所有的跨域请求,这样的安全性低;

也可以指定一个二级域名,域名下所有的Url不受限;

也可以仅指定一个固定的Url;

也可以指定请求动作 GET/PUT;

以上设定都称为跨域的策略,按实际情况自定义策略。

.NET跨域的实现

Request / Response 的 Header 设定方式:

Response.Headers["Access-Control-Allow-Origin"] = "{域名地址}";
Response.Headers["Access-Control-Allow-Credentials"] = "true";
Response.Headers["Access-Control-Allow-Headers"] = "x-requested-with,content-type";

中间件定义策略方式:

.NET默认提供了跨域的中间件
UseCors
,同样可以在中间件中设定 源头/动作/Header 等。

全局策略案例:

// 设定跨域策略
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "策略名称1", policy =>
    {
        // 允许的域名
        policy.WithOrigins("http://contoso.com", "http://*.sol.com")
        // 允许的请求动作
        .WithMethods("GET", "POST", "PUT", "DELETE")
        // 允许的 Header
        .AllowAnyHeader();

    });
});
// ... 最后启用跨域中间件
app.UseCors("{策略名称}");

Action单独设定跨域:

启用:
[EnableCors]
指定:
[EnableCors("策略名称")]
详细:
[EnableCors(origins: "http://Sol.com:8013/", headers: "*", methods: "GET,PUT")]
排除:
[DisableCors]

服务间的通信

Remote Procedure Call - RPC

Remote Procedure Call,远程过程调用。通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。传输速度快,效率高的特点,常用于服务间的通信。

整体运行过程:

.NET服务被调方集成 gRPC

1、NuGet 安装 Grpc.AspNetCore

2、编写 Proto 文件(为生成C#代码)

syntax = "proto3";
// 生成代码后的命名空间
option csharp_namespace = "GrpcService";
// 包名(不是必须)
package product;
// 定义一个服务
service Producter{
    // 定义一个方法(请求参数类,返回参数类)
    rpc Add(CreateProductRequest) returns (CreateProductResponse);
    rpc Query(QueryProductRequest) returns (QueryProductResponse);
}

// 为上述服务 定义 请求参数类
message QueryProductRequest{
    // 类型、名称、唯一标识
    string name = 1;
    string code = 2;
}
// 为上述服务 定义 返回参数类
message QueryProductResponse{
    // 定义为集合类型
    repeated Product products = 1;
}

message CreateProductRequest{
    string name = 1;
    string code = 2;
    string color = 3;
    string size = 4;
    string manufacturing = 5;
}

message CreateProductResponse{
    ResultType result = 1;
}
// 定义(以上用到的)枚举
enum ResultType{
    success=0;
    fail=1;
}

message Product{
    int32 id = 1;
    string name = 2;
    string code = 3;
    string color = 4;
    string size = 5;
}

3、项目属性文件配置编译包含项

4、Build 项目;通过 proto 文件自动生成C#代码(于obj目录中)

5、编写对应的Service 继承于自动生成的抽象类,并实现其中抽象方法

public class ProductService : Producter.ProducterBase

6、注册到容器

// 注册
builder.Services.AddGrpc();
// 到容器
app.MapGrpcService<ProductService>();

7、appsettings.json 配置启用RPC所需的HTTP2协议

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http2"
  }
}

8、最终目录效果图

.NET服务调用方集成 gRPC

1、NuGet 安装 Grpc.AspNetCore、Grpc.Net.Client

2、Cope 服务端 Proto 文件于目录

3、项目属性文件配置编译包含项

<ItemGroup>
    <Protobuf Include="Protos\product.proto" GrpcServices="Client" />
</ItemGroup>

4、Build 项目;通过 proto 文件自动生成C#代码(于obj目录中)

5、使用生成的客户端代码请求服务端

// 建立连接
var channel = GrpcChannel.ForAddress("https://localhost:7068");
// 创建客户端对象
var client = new Producter.ProducterClient(channel);
// 调用服务端方法(及参数)
QueryProductResponse resp = client.Query(new QueryProductRequest { Code = "1", Name = "1" });
// 返回的数据集合
foreach (var item in resp.Products)

管理WEB服务器文件的WebDAV协议

WebADV协议

WEBDAV追加方法

WeDAV请求示例

HTTP大跃进--QUIC与HTTP30

QUIC&HTTP3.0

HTTP2.0的问题

队头阻塞

建立连接的握手延迟大

QUIC的特性

0RTT

没有队头阻塞的多路复用

WEB安全攻击概述

Web应用的概念

1.Web应用是由动态脚本,编译过的代码等组合而成

2.它通常架设在Web服务器上,用户在Web浏览器上发送请求

3.这些请求使用HTTP协议,由Web应用和企业后台的数据库及其他动态内容通信

Web应用三层架构

典型的Web应用通常是标准的三层架构模型

WASC的定义

Web Application Security Consortium

是一个由安全专家,行业顾问和诸多住址的代表组成的国际团体

他们负责为WWW指定被广为接受的应用安全标准

WASC将Web应用安全威胁分为六大类

1、Authentication(验证):用来确认某用户、服务或是应用身份的攻击手段

2、Authorization(授权):用来决定是否某用户、服务或是应用具有执行请求动作必要权限的攻击手段

3、Client-Side Attacks(客户侧攻击):用来扰乱或是探测Web站点用户的攻击手段

4、Command Execution(命令执行):在Web站点上执行远程命令的攻击手段

5、lnformation Disclosure(信息暴露):用来获取Web站点具体系统信息的攻击手段

6、Logical Attacks(逻辑性攻击):用来扰乱或是探测Web 应用逻辑流程的攻击手段

OWASP的定义

Open Web Application Security Project

该组织致力于发现和解决不安全Web应用的根本原因

它们最重要的项目之一是" Web应用的十大安全隐患 "

总结了目前Web应用最常受到的十种攻击手段,并且按照攻击发生的概率进行了排序

从OWAS谈漏洞分类

概述

前几天在使用 Terraform + cloud-init 批量初始化我的实验室 Linux 机器。正好发现有一些定时场景需要使用到 cronjob, 进一步了解到 systemd timer 完全可以替换 cronjob, 并且 systemd timer 有一些非常有趣的功能。

回归话题:为什么我推荐你使用 systemd timer 替代 cronjob? 因为相比 cronjob, systemd timer 有这些优势:

  • 可以覆盖 cronjob 的所有功能
  • 统一日志收集到 systemd 日志
  • 针对时间精确度更详细的配置项
  • 除了定时场景,还支持基于 event 的触发
  • 相比 cronjob 更灵活的语法
  • 更丰富的使用/运维命令集

接下来我们一一介绍。

首先我们通过系统自带的 timer 来熟悉这个新玩意。

系统自带的 timer

当 Ubuntu 或任何基于 systemd 的发行版安装在一个新系统上时,它会创建几个 timer,作为任何 Linux 主机后台的系统维护程序的一部分。这些 timer 会触发普通维护任务所需的事件,比如更新系统数据库、清理临时目录、切割日志文件等等。

我们使用
systemctl status *timer
命令列出我的主机上的所有 timer:

casey@casey-Virtual-Machine:~$ systemctl status *timer
● plocate-updatedb.timer - Update the plocate database daily
     Loaded: loaded (/lib/systemd/system/plocate-updatedb.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
    Trigger: Wed 2023-04-05 00:40:16 CST; 7h left
   Triggers: ● plocate-updatedb.service

4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Update the plocate database daily.

● fwupd-refresh.timer - Refresh fwupd metadata regularly
     Loaded: loaded (/lib/systemd/system/fwupd-refresh.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
    Trigger: Wed 2023-04-05 01:54:51 CST; 9h left
   Triggers: ● fwupd-refresh.service

4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Refresh fwupd metadata regularly.

● update-notifier-motd.timer - Check to see whether there is a new version of Ubuntu available
     Loaded: loaded (/lib/systemd/system/update-notifier-motd.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2023-04-04 16:49:50 CST; 19s ago
    Trigger: Sat 2023-04-08 03:19:02 CST; 3 days left
   Triggers: ● update-notifier-motd.service

4 月 04 16:49:50 casey-Virtual-Machine systemd[1]: Started Check to see whether there is a new version of Ubuntu available.

● fstrim.timer - Discard unused blocks once a week
     Loaded: loaded (/lib/systemd/system/fstrim.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
    Trigger: Tue 2023-04-04 17:58:23 CST; 1h 8min left
   Triggers: ● fstrim.service
       Docs: man:fstrim

4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Discard unused blocks once a week.
...

每个 timer 至少有六行信息与之相关:

  • 第一行是 timer 的文件名和对其用途的简短描述。
  • 第二行显示 timer 的状态,它是否被加载,timer unit 文件的完整路径,以及供应商的预设。
  • 第三行显示其活动状态,包括 timer 开始活动的日期和时间。
  • 第四行包含 timer 下次被触发的日期和时间,以及直到触发发生的
    大致
    时间。
  • 第五行显示由 timer 触发的事件或服务的名称。
  • 一些(但不是全部)systemd unit 文件有指向相关文档的指针。如上面的
    Docs: man:fstrim
  • 最后一行是 timer 所触发的服务的最新实例的日志条目。

创建 timer

优势之一:统一日志收集到 systemd 日志

为了更快了解 timer, 我们创建自己的 service unit 和 timer unit 来触发。

具体用途为:每周定期更新 tailscale 的版本。

首先,创建 tailscale update 服务,如下:

[Unit]
Description=Tailscale update
Wants=tailscale-weekly-update.timer

[Service]
Type=oneshot
ExecStart=/usr/bin/tailscale update -yes

[Install]
WantedBy=multi-user.target

然后,创建 tailscale update timer, 如下:

[Unit]
Description=Tailscale update
Requires=tailscale-weekly-update.service

[Timer]
Unit=tailscale-weekly-update.service
OnCalendar=weekly

[Install]
WantedBy=timers.target

最后,启用 timer:

systemctl enable tailscale-weekly-update.timer 

这样就可以了,但是为了演示,执行:
systemctl start tailscale-weekly-update.service
手动运行一次。

输出会直接集成到 systemd 日志里,并可以通过
journalctl
查看:(包含手动执行日志,和后续自动定期执行的日志)

$ sudo journalctl -S "2023-03-29 00:00:00" -u tailscale-weekly-update.service
4 月 02 09:14:28 casey-Virtual-Machine systemd[1]: Starting Tailscale node agent...
4 月 02 09:14:30 casey-Virtual-Machine tailscale[6898]: 获取:1 https://pkgs.tailscale.com/stable/ubuntu jammy InRelease
4 月 02 09:14:30 casey-Virtual-Machine tailscale[6898]: 获取:2 https://pkgs.tailscale.com/stable/ubuntu jammy/main amd64 Packages [7,853 B]
4 月 02 09:14:32 casey-Virtual-Machine tailscale[6898]: 已下载 13.9 kB,耗时 1 秒 (14.4 kB/s)
4 月 02 09:14:32 casey-Virtual-Machine tailscale[6898]: 正在读取软件包列表。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在读取软件包列表。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在分析软件包的依赖关系树。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在读取状态信息。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 下列软件包将被升级:
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]:   tailscale
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 升级了 1 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 4 个软件包未被升级。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 需要下载 23.0 MB 的归档。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 解压缩后将会空出 1,024 B 的空间。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 获取:1 https://pkgs.tailscale.com/stable/ubuntu jammy/main amd64 tailscale amd64 1.38.3 [23.0 MB]
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 无法初始化前端界面:Dialog
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: (系统未设定 TERM 环境变量,所以对话框界面将不可使用。)
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 返回前端界面:Readline
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 无法初始化前端界面:Readline
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: (这个界面要求可控制的 tty。)
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 返回前端界面:Teletype
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: dpkg-preconfigure: 重新开启标准输入失败:
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7101]: 已下载 23.0 MB,耗时 40 秒 (577 kB/s)
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: [729B blob data]
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: 准备解压 .../tailscale_1.38.3_amd64.deb  ...
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: 正在解压 tailscale (1.38.3) 并覆盖 (1.38.2) ...
4 月 02 09:15:15 casey-Virtual-Machine tailscale[7101]: 正在设置 tailscale (1.38.3) ...
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: Running kernel seems to be up-to-date.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: Services to be restarted:
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]:  systemctl restart tailscale-weekly-update.service
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No containers need to be restarted.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No user sessions are running outdated binaries.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No VM guests are running outdated hypervisor (qemu) binaries on this host.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.service: Deactivated successfully.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: Finished Tailscale node agent.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.service: Consumed 6.317s CPU time.

$ sudo journalctl -S "2023-03-29 00:00:00" -u tailscale-weekly-update.timer
4 月 02 09:14:28 casey-Virtual-Machine systemd[1]: Started Tailscale node agent.
4 月 02 20:01:52 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.timer: Deactivated successfully.
4 月 02 20:01:52 casey-Virtual-Machine systemd[1]: Stopped Tailscale node agent.

如上面的日志,可以很方便地检查 timer 和服务的状态。

在日志这方面,你不需要做任何特别的事情,就可以使
tailscale-weekly-update.service
unit 中的
ExecStart
触发器的
STDOUT
存储在日志中。这都是使用 systemd 运行服务的一部分。

Systemd timer 时间精度

优势之一:针对时间精确度更详细的配置项

从上面日志,如果细看,timer 不会在
:00
秒的时候准确触发,甚至不会在上一个实例的一分钟内准确触发。这是故意的,但如果有必要的话,可以覆盖它的默认配置。

这种行为的原因是为了防止多个服务在完全相同的时间被触发。例如,你可以使用时间规格,如每周、每天,等等。这些快捷方式都被定义为在它们被触发的那一天的 00:00:00 时触发。当多个 timer 被这样指定时,它们很有可能会试图同时启动。

systemd timer 被有意设计成在指定时间内随机触发,以防止同时触发。它们在一个时间窗口内半随机地触发。根据
systemd.timer
手册,这个触发时间相对于所有其他定义的 timer 单位来说,保持在一个稳定的位置。

大多数时候,这种概率性的触发时间是没有问题的。当安排备份等任务运行时,只要它们在非工作时间运行,就不会有问题。一个系统管理员可以选择一个确定的开始时间,如典型的 cronjob 规范中的 01:05:00,以不与其他任务冲突,但有很大范围的时间值可以达到这个目的。启动时间中的一分钟随机性通常是不相关的。

然而,对于某些任务,精确的触发时间是一个绝对要求。对于这些任务,你可以通过在 timer unit 文件的
Timer
部分添加这样的配置来指定更高的触发时间跨度精度(如精度在一微秒内):

AccuracySec=1us

时间跨度可用于指定所需的精度,以及为重复性或一次性事件定义时间跨度。它可以识别以下单位:

  • usec, us, µs
  • msec, ms
  • seconds, second, sec, s
  • minutes, minute, min, m
  • hours, hour, hr, h
  • days, day, d
  • weeks, week, w
  • months, month, M (定义为 30.44 天)
  • years, year, y (定义为 365.25 天)

/usr/lib/systemd/system
中的所有默认 timer 都指定了一个更大的精度范围,因为精确的时间并不关键。看看系统创建的 timer 中的一些规格:

$ grep Accur /usr/lib/systemd/system/*timer
/usr/lib/systemd/system/fstrim.timer:AccuracySec=1h
/usr/lib/systemd/system/logrotate.timer:AccuracySec=1h
/usr/lib/systemd/system/plocate-updatedb.timer:AccuracySec=20min
/usr/lib/systemd/system/snapd.snap-repair.timer:AccuracySec=10min

Timer 类型

优势之一:除了定时场景,还支持基于 event 的触发

systemd timer 具有 cron 所不具备的其他功能,cron 只在特定的、重复的、实时的日期和时间触发。但是,一个 timer 可以被配置为在系统启动后,或在启动后,或在某个定义的服务 unit 激活后的特定时间内触发。这些被称为
单调性 timer
。单调指的是一个持续增加的计数或序列。这些 timer 不是持久的,因为它们在每次启动后都会重置。

表 1 列出了单调的 timer 以及每个 timer 的简短定义,还有 "OnCalendar" timer,它不是单调的,用于指定未来的时间,可能是重复的,也可能不是。

Timer 单调性 定义
OnActiveSec= X 这定义了一个相对于 timer 被激活的时刻的 timer。
OnBootSec= X 这定义了一个相对于机器启动时间的 timer。
OnStartupSec= X 这定义了一个相对于服务管理器首次启动时间的计时器。对于系统 timer unit,这与
OnBootSec=
非常相似,因为系统服务管理器通常在启动时很早就启动。当配置在每个用户服务管理器中运行的单元时,它主要是有用的,因为用户服务管理器一般只在第一次登录时启动,而不是在启动时。
OnUnitActiveSec= X 这定义了一个相对于要激活的 timer 最后一次被激活的时间。
OnUnitInactiveSec= X 这定义了一个相对于要激活的 timer 最后被停用的时间的定时器。
OnCalendar= 这就用日历事件表达式定义了实时 timer。更多关于日历事件表达式的语法信息请参见
systemd.time(7)
。否则,其语义与
OnActiveSec=
及相关设置类似。这个 timer 是最像那些与 cron 服务一起使用的 timer。

表 1: systemd timer 定义

单调 timer 的时间跨度可以使用与前面提到的
AccuracySec
语句相同的快捷名称,但 systemd 将这些名称规范化为秒。例如,你可能想指定一个 timer,在系统启动 5 天后触发一次事件,可以这样写:
OnBootSec=5d
。如果主机在
2020-06-15 09:45:27
启动,timer 将在
2020-06-20 09:45:27
或之后一分钟内触发。

Calendar event 定义

优势之一:相比 cronjob 更灵活的语法

Calendar event 定义是在所需的重复时间触发 timer 的关键部分。首先看一下
OnCalendar
设置中使用的一些规格。

systemd 及其 timer 使用的时间和日期规格与 crontab 中使用的格式不同。它比 crontab 更
灵活
,允许以
at
命令的方式模糊日期和时间。

使用
OnCalendar=
的 systemdtimer 的基本格式是
DOW YYYY-MM-DD HH:MM:SS
。DOW(星期)是可选的,其他字段可以使用星号(*)来匹配该位置的任何值。所有日历时间形式都被转换为规范化的形式。如果没有指定时间,则假定其为 00:00:00。如果没有指定日期但指定了时间,那么下一个匹配可能是今天或明天,这取决于当前的时间。名称或数字可用于月份和星期。可以指定每个单位的逗号分隔的列表。单位范围可以在开始和结束值之间用
...
来指定。

有几个有趣的选项用于指定日期。波浪号(~)可以用来指定该月的最后一天或该月最后一天之前的指定天数。"/"可以用来指定一周中的某一天作为修饰语。

下面是一些在
OnCalendar
语句中使用的典型时间规格的例子:

Calendar event 定义 描述
DOW YYYY-MM-DD HH:MM:SS
*-*-* 00:15:30 每年的每个月的每一天,在午夜后的 15 分钟 30 秒。
Weekly 每个星期一的 00:00:00
Mon *-*-* 00:00:00 与每周相同
Mon 与每周相同
Wed 2020-*-* 2020 年的每个星期三,00:00:00
Mon..Fri 2021-*-* 2021 年的每个工作日的 00:00:00
2023-6,7,8-1,15 01:15:00 2023 年 6 月、7 月和 8 月的 1 日和 15 日凌晨 01:15:00
Mon *-05~03 任何一年的 5 月的下一个星期一,也是月末的第三天。
Mon..Fri *-08~04 任何年份的 8 月底前的第 4 天,如果该天也是工作日,则为 8 月底。
*-05~03/2 从五月底开始的第三天,两天后再来一次。每年都会重复。请注意,这个表达式使用了(~)。
*-05-03/2 五月的第三天,然后在五月的其余时间里每隔一天。每年重复一次。注意,这个表达式使用了破折号(-)。

表 2: 示例 OnCalendar event 定义

测试 calendar 定义

优势之一:更丰富的使用/运维命令集

systemd 提供了一个很好的工具来验证和检查 timer 中的日历时间事件规范。
systemd-analyze calendar
工具解析了一个日历时间事件规范,并提供了规范化的形式以及其他有趣的信息,比如下一个 "elapse"(即匹配)的日期和时间,以及达到触发时间前的大致时间。

首先,看一下未来的一个没有时间的日期:

$ systemd-analyze calendar 2030-06-17
  Original form: 2030-06-17
Normalized form: 2030-06-17 00:00:00
    Next elapse: Mon 2030-06-17 00:00:00 CST
       (in UTC): Sun 2030-06-16 16:00:00 UTC
       From now: 7 years 2 months left

现在添加一个时间。在这个例子中,日期和时间作为非相关实体被单独分析:

$ systemd-analyze calendar 2030-06-17 15:21:16
  Original form: 2030-06-17
Normalized form: 2030-06-17 00:00:00
    Next elapse: Mon 2030-06-17 00:00:00 CST
       (in UTC): Sun 2030-06-16 16:00:00 UTC
       From now: 7 years 2 months left

  Original form: 15:21:16
Normalized form: *-*-* 15:21:16
    Next elapse: Wed 2023-04-05 15:21:16 CST
       (in UTC): Wed 2023-04-05 07:21:16 UTC
       From now: 21h left

要把日期和时间作为一个 unit 来分析,需要用引号把它们括起来。

$ systemd-analyze calendar "2030-06-17 15:21:16"
Normalized form: 2030-06-17 15:21:16
    Next elapse: Mon 2030-06-17 15:21:16 CST
       (in UTC): Mon 2030-06-17 07:21:16 UTC
       From now: 7 years 2 months left

现在测试表 2 中的条目。选一个复杂的:

$ systemd-analyze calendar "2023-6,7,8-1,15 01:15:00"
  Original form: 2023-6,7,8-1,15 01:15:00
Normalized form: 2023-06,07,08-01,15 01:15:00
    Next elapse: Thu 2023-06-01 01:15:00 CST
       (in UTC): Wed 2023-05-31 17:15:00 UTC
       From now: 1 month 26 days left

让我们看一个例子,在这个例子中,我们列出了时间戳表达式的下五个执行时间:

$ systemd-analyze calendar --iterations=5 "Mon *-05~3"
  Original form: Mon *-05~3
Normalized form: Mon *-05~03 00:00:00
    Next elapse: Mon 2023-05-29 00:00:00 CST
       (in UTC): Sun 2023-05-28 16:00:00 UTC
       From now: 1 month 23 days left
       Iter. #2: Mon 2028-05-29 00:00:00 CST
       (in UTC): Sun 2028-05-28 16:00:00 UTC
       From now: 5 years 1 month left
       Iter. #3: Mon 2034-05-29 00:00:00 CST
       (in UTC): Sun 2034-05-28 16:00:00 UTC
       From now: 11 years 1 month left
       Iter. #4: Mon 2045-05-29 00:00:00 CST
       (in UTC): Sun 2045-05-28 16:00:00 UTC
       From now: 22 years 1 month left
       Iter. #5: Mon 2051-05-29 00:00:00 CST
       (in UTC): Sun 2051-05-28 16:00:00 UTC
       From now: 28 years 1 month left

这应该给你足够的信息来开始测试你的
OnCalendar
时间规格。

总结

systemd timer 可以用来执行与 cron 工具相同类型的任务,但在触发事件的 calendar 和单调的时间规格方面提供了更多的灵活性。

除此之外,systemd timer 还有的优势包括:

  • 统一日志收集到 systemd 日志
  • 针对时间精确度更详细的配置项
  • 更丰富的使用/运维命令集


快去尝试迁移你的 cronjob 到 systemd timer 吧~