首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
69 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
51 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
使用 C 语言实现期末考试成绩的总分和平均分
题目 # 有如下表所示的成绩单: 请建立合适的结构,并从键盘输入表中的数据,求出每名同学的平均分; 按照平均分成绩由高到低对表中的数据进行排序。 计算并输出每一门课程的平均分。 Name sex English Math Programming Zhaolin M 80 70 50 Qianming F 90 80 70 Sunwen F 70 70 80 Liyuan M 60 50 60 Liuli F 70 90 70 解决方案 # 这里我提示一下,当你结束输入的时候按两次 ctrl + z下面是我的程序和运行的结果#include struct ln { char name[20];//姓名 char sex[2];//性别 float enscore;//英语分 float mascore;//数学分 float prscore;//程序设计分 float avg;//平均分 }student[1000];//假设上限为1000名学生 int create()//返回输入学生的人数{ int i=0; char name[20]; printf("请输入学生的姓名,性别,英语分,数学分,程序设计分\n"); printf(" 按ctrl + z 结束输入\n"); while(scanf("%s", name)!=EOF) { strcpy(student[i].name, name); scanf("%s %f %f %f", student[i].sex, &student[i].enscore, &student[i].mascore, &student[i].prscore); student[i].avg = (student[i].enscore + student[i].mascore + student[i].prscore)/3.0; i++; } printf("录入完成\n\n"); return i; } int cmp(const void *p, const void *q){ return (*(struct ln *)q).avg - (*(struct ln *)p).avg; } void avgstudent(int n)//按学生的平均分进行排序,,参数为学生的人数{ int i; qsort(student, n, sizeof(struct ln), cmp);//用快排进行排序 printf("\n 按平均分排序后\n\n"); printf("name sex English Math Programming avg\n\n"); for(i=0; i
2024年10月20日
1 阅读
0 评论
0 点赞
2024-10-20
NodeJs:腾讯新闻构建高性能的 react 同构直出方案
在腾讯新闻抢金达人活动 node 同构直出渲染方案的总结文章中我们整体了解了下同构直出渲染方案在我们项目中的使用。正如我在上篇文章结尾所说的: 应用型技术的难点不是在克服技术问题,而是在于能够不断的结合自身的产品体验,发现其中存在的体验问题,不断使用更好的技术方案去优化用户的体验,为整个产品发展添砖加瓦。 我们在根据产品的体验效果选择了 react 同构直出渲染方案,必然也要保证当前方案的可用性和可靠性。例如我们的服务能同时支撑多少人访问,当用户量增大时是否可以依然保证用户的正常访问,如何保证 CPU、内存等正常运作,而不被一直占用无法释放等。因此,这里我们应当下我们项目的几项数据: 项目一天的访问量是多少,高峰期的访问量是多少,即并发的用户量有多少; 我们的单机服务最大能支持多少 QPS; 高并发时的服务响应时间如何,页面,接口的失败率有多少; CPU 和内存的使用情况,是否存在 CPU 使用不充分或者内存泄露等问题; 这些数据,都是我们上线前要知道的。压力测试的重要性就提现出来了,我们在上线前进行充分的测试,能够让我们掌握程序和服务器的运行性能,大致申请多少台机器等等。 1. 初次压力测试 # 我们这里使用autocannon来对项目进行压测。注意,我们现在还没有进行任何的优化措施,就是要先暴露出问题来,然后针对性的进行优化。每秒钟 60 的并发,并持续 100 秒:autocannon -c 60 -d 100 压测后的数据:从图片中可以看到,每秒 60 的并发请求量时,QPS 平均有 266 左右,不过还有 23 个请求超时了,响应时间还可以,99%的请求在 1817ms 毫秒内完成。就目前这几项数据来看,数据处理能力并不理想,我们还有很大的提升空间。 2. 解决方案 # 针对上面压测出来的数据不理想,我们这里需要采取一些措施了。 2.1 内存管理 # 我们现在写纯前端时,几乎已经很少关注内存的使用了,毕竟在前端发展的过程中,内存的垃圾回收机制相对来说比较完善,而且前端页面的生存周期比较短。如果真是要特别注意的话,也是早期在 IE 浏览器中,js 与 dom 的交互过程中可能会产生内存的泄露。而且如果真会真要是泄露的话,也只会影响当前终端的用户,其他的用户暂时不会受到影响。而服务端则不同,所有用户都会访问当前运行的代码,只要程序有一丁点的内存泄露,在成千上万的访问量下,都会造成内存的堆积,垃圾无法回收,最终造成严重的内存泄露,并导致程序崩溃。为了预防内存泄露,我们在内存管理方面,主要三方面的内容: V8 引擎的垃圾回收机制; 造成内存泄露的原因; 如何检测内存泄露; Node 将 JavaScript 的主要应用场景扩展到了服务器端,相应要考虑的细节也与浏览器端不同, 需要更严谨地为每一份资源作出安排。总的来说,内存在 Node 中不能随心所欲地使用,但也不是完全不擅长。 2.1.1 V8 引擎的垃圾回收机制 # 在 V8 中,主要将内存分为新生代和老生代两代。新生代的对象为存活时间比较短的对象,老生代中的对象为存活时间较长的或常驻内存的对象。默认情况下,新生代的内存最大值在 64 位系统和 32 位系统上分别为 32 MB 和 16 MB。V8 对内存的最大值在 64 位系统和 32 位系统上分别为 1464 MB 和 732 MB。为什么这样分两代呢?是为了最优的 GC 算法。新生代的 GC 算法 Scavenge 速度快,但是不合适大数据量;老生代针使用 Mark-Sweep(标记清除) & Mark-Compact(标记整理) 算法,合适大数据量,但是速度较慢。分别对新旧两代使用更适合他们的算法来优化 GC 速度。 2.1.2 内存泄露的原因 # 内存泄露的情况有很多,例如内存当缓存、队列、重复的事件监听等。内存当缓存这种情况中,通常有用一个变量来缓存数据,然后没有过期时间,一直填充数据,例如下面一个简单的例子:let cached = new Map(); server.get('*', (req, res) => { if (cached.has(req.url)) { return cached.get(req.url); } const html = app.render(req, res); cached.set(req.url, html); res.send(html); }); 除此之外,还有闭包也是其中的一种情况。这种使用内存的不好的地方是,它没有可用的过期策略,只会让数据越来越多,最终造成内存泄露。更好的方式使用第三方的缓存机制,例如 redis、memcached 等,这些都有良好的过期和淘汰策略。同时,也有一些队列方面的处理,例如有些日志的写入操作,当海量的数据需要写入时,就会造成队列的堆积。这时,我们设置队列的超时策略和拒绝策略,让一些操作尽快地释放掉。再一个就是事件的重复监听。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告: Warning: Possible EventEmitter memory leak detected. 11 /question listeners added。Use emitter。setMaxListeners() to increase limit 2.1.3 排查的手段 # 我们从内存的监控图中可以看到,在用户量基本保持不变的情况下,内存是一直在缓慢上涨,说明我们产生了内存泄露,使用的内存并没有被释放掉。这里我们可以通过node-heapdump等工具来进行判断,或者稍微简单点,使用--inspect命令实现:node --inspect server.js 然后打开 chrome 链接chrome://inspect来查看内存的使用情况。通过两次的内存抓取对比发现,handleRequestTimeout()方法一直在产生,且每个 handle 方法中有无数个回调,资源无法被释放。通过定位查看使用的 axios 代码是:if (config.timeout) { timer = setTimeout(function handleRequestTimeout() { req.abort(); reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); } } 这里代码看起来是没任何问题的,这是在前端处理中一个很典型的超时处理解决方式。由于 Nodejs 中,io 的链接会阻塞 timer 处理,因此这个 setTimeout 并不会按时触发,也就有了 10s 以上才返回的情况。貌似问题解决了,巨大的流量和阻塞的 connection 导致请求堆积,服务器处理不过来,CPU 也就下不来了。通过定位并查看axios 的源码:if (config.timeout) { // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. // And then these socket which be hang up will devoring CPU little by little. // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. req.setTimeout(config.timeout, function handleRequestTimeout() { req.abort(); reject( createError( 'timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req ) ); }); } 额,我之前使用的版本比较早,跟我本地使用的代码不一样,说明是更新过了,再查看这个文件的9 月 16 日的改动历史:这里我们就需要把 axios 更新到最新的版本了。而且经过本地大量测试,发现在高负载下 CPU 和内存都在正常范围内了。 2.2 缓存 # 缓存真是性能优化的一把好手,服务不够,缓存来凑。不过缓存的类型有很多种,我们应当根据项目的实际情况,合理地选择使用缓存的策略。这里我们使用了 3 层的缓存策略。在 nginx 中,可以使用 proxy_cache 设置要缓存的路径和缓存的时间,同时可以启用proxy_cache_lock。 当 proxy_cache_lock 被启用时,当多个客户端请求一个缓存中不存在的文件(或称之为一个 MISS),只有这些请求中的第一个被允许发送至服务器。其他请求在第一个请求得到满意结果之后在缓存中得到文件。如果不启用 proxy_cache_lock,则所有在缓存中找不到文件的请求都会直接与服务器通信。 不过这个字段的启用也要非常慎重,当访问量过大时,会造成请求的堆积,必须等待第一个请求返回完成后,才能处理后面的请求。proxy_cache_path /data/cached keys_zone=answer:16m levels=1:2 inactive=60m; server { location / { proxy_cache answer; proxy_cache_valid 1m; } } 在业务层面,我们可以启用 redis 缓存,来缓存整个页面、页面的某个部分或者接口等等,当穿透 nginx 缓存时,可以启用 redis 缓存。使用第三方缓存的特点我们在之前的文章也说了:多个进程之间可以共享,同时减少项目本身对缓存淘汰算法的处理。当前面的两层缓存失效时,进入到我们的 node 服务层。二层的缓存机制,能实现不同的缓存策略和缓存粒度,业务需要根据自身场景, 选用适合自己业务的缓存即可。 3. 效果 # 这时我们项目的性能怎样了呢?autocanon -c 1000 -d 100 从图片里可以看到,99%的请求在182ms内完成,每秒平均处理的请求有15707左右,相比我们最开始只能处理200多个请求,性能足足提升了60倍多。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
如何构建自己的 react hooks
我们组的前端妹子在组内分享时谈到了 react 的钩子,趁此机会我也对我所理解的内容进行下总结,方便更多的同学了解。在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API,我们非常有必要了解下他的使用方法,并能够结合我们的业务编写几个自定义的 hooks。 1. 常用的一个 hooks # 官方中提供了几个内置的钩子,我们简单了解下他们的用法。 1.1 useState: 状态钩子 # 需要更新页面状态的数据,我们可以把他放到 useState 的钩子里。例如点击按钮一下,数据加 1 的操作:const [count, setCount] = useState(0); return ( {count} setCount(count + 1)}> add 1 ); 在 typescript 的体系中,count 的类型,默认就是当前初始值的类型,例如上面例子中的变量就是 number 类型。如果我们想自定义这个变量的类型,可以在 useState 后面进行定义:const [count, setCount] = (useState < number) | (null > null); // 变量count为number类型或者null类型 同时,使用 useState 改变状态时,是整个把 state 替换掉的,因此,若状态变量是个 object 类型的数据,我只想修改其中的某个字段,在之前 class 组件内调用 setState 时,他内部会自动合并数据。class Home extends React.Component { state = { name: "wenzi", age: 20, score: 89, }; update() { this.setState({ score: 98, }); // 内部自动合并 } } 但在 function 组件内使用 useState 时,需要自己先合并数据,然后再调用方法,否则会造成字段的丢失。const [person, setPerson] = useState({ name: 'wenzi', age: 20, score: 89 }); setPerson({ ...person, { score: 98 } }); // 先合并数据 { name: 'wenzi', age: 20, score: 98 } setPerson({ score: 98 }); // 仅传入要修改的字段,后name和age字段丢失 1.2 useEffect: 副作用钩子 # useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。useEffect 钩子在组件初始化完毕时,一定会执行一次,在组件重新渲染的过程中,是否还要 update,还要看传入的第 2 个参数。 当只有回调函数这一个参数时,组件的每次更新,回调都会执行; 当有 2 个参数时,只有第 2 参数里的数据发生变化时,回调才执行; 只想在组件初始化完毕时只执行一次,第 2 个参数可以传入一个空的数组; 我们可以看下这个例子,无论点击 add按钮 还是 settime按钮 ,useEffect 的回调都会执行:const Home = () => { const [count, setCount] = useState(0); const [nowtime, setNowtime] = useState(0); useEffect(() => { console.log("count", count); console.log("nowtime", nowtime); }); return ( count: {count} nowtime: {nowtime} setCount(count + 1)}> add 1 setNowtime(Date.now())}> set now time ); }; 若改成下面的这样,回调仅会在 count 发生变化时才会在控制台输出,仅修改 nowtime 的值时没有输出:useEffect(() => { console.log("count", count); console.log("nowtime", nowtime); }, [count]); useEffect 的回调函数还可以返回一个函数,这个函数在 effect 生命周期结束之前调用。为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。基于上面的代码,我们稍微修改一下:useEffect(() => { console.log("count", count); console.log("nowtime", nowtime); return () => console.log("effect callback will be cleared"); }, [count]); 基于这个机制,在一些存在添加绑定和取消绑定的案例上特别合适,例如监听页面的窗口大小变化、设置定时器、与后端的 websocket 接口建立连接和断开连接等,都可以预计 useEffect 进行二次的封装,形成自定义的 hook。关于自定义 hook,下面我们会讲到。 1.3 useMemo 和 useCallback # function 组件中定义的变量和方法,在组件重新渲染时,都会重新重新进行计算,例如下面的这个例子:const Home = () => { const [count, setCount] = useState(0); const [nowtime, setNowtime] = useState(0); const getSum = () => { const sum = ((1 + count) * count) / 2; return sum + " , " + Math.random(); // 这个random是为了看到区别 }; return ( count: {count} sum: {getSum()} nowtime: {nowtime} setCount(count + 1)}> add 1 setNowtime(Date.now())}> set now time ); }; 这里有 2 个按钮,一个是 count+1,一个设置当前的时间戳, getSun() 方法是计算从 1 到 count 的和,我们每次点击 add 按钮后,sum 方法都会重新计算和。可是当我们点击 settime 按钮时,getSum 方法也会重新计算,这是没有必要的。这里我们可以使用 useMemo 来修改下:const sum = useMemo( () => ((1 + count) * count) / 2 + " , " + Math.random(), [count] ); {sum} ; 点击查看样例,修改后就可以看到,sum 的值只有在 count 发生变化的时候才重新计算,当点击 settime 按钮的时候,sum 并没有重新计算。这要得益于 useMemo 钩子的特性:const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); useMemo 返回回调里 return 的值,而且 memoizedValue 它仅会在某个依赖项改变时才重新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。在上面的例子里,只有 count 变量发生变化时,才重新计算 sum,否则 sum 的值保持不变。useCallback 与 useMemo 类型,只不过 useCallback 返回的是一个函数,例如:const fn = useCallback(() => { return ((1 + count) * count) / 2 + " , " + nowtime; }, [count]); 2. 实现几个自定义的 hook # 在官方文档里,实现了好友的在线与离线功能。这里我们自己也学着实现几个 hook。 2.1 获取窗口变化的宽高 # 我们通过监听resize事件来获取实时获取 window 窗口的宽高,对这个方法进行封装后可以在生命周期结束前能自动解绑 resize 事件:const useWinResize = () => { const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, }); const resize = useCallback(() => { setSize({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, }); }, []); useEffect(() => { window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []); return size; }; 使用起来也非常方便:const Home = () => { const { width, height } = useWinResize(); return ( width: {width} height: {height} ); }; 点击链接useWinResize 的使用可以查看 demo 演示。 2.2 定时器 useInterval # 在前端中使用定时器时,通常要在组件生命周期结束前清除定时器,如果定时器的周期发生变化了,还要先清除定时器再重新按照新的周期来启动。这种最常用的场景就是九宫格抽奖,用户点击开始抽奖后,先缓慢启动,然后逐渐变快,接口返回中奖结果后,再开始减速,最后停止。我们很容易想到用 useEffect 来实现这样的一个 hook(举一个错误的例子):const useInterval = (callback, delay) => { useEffect(() => { if (delay !== null) { let id = setInterval(callback, delay); return () => clearInterval(id); } }, [delay]); }; 我们把这段代码用到项目中试试:const Home = () => { const [count, setCount] = useState(0); useInterval(() => { console.log(count); setCount(count + 1); }, 500); return {count} ; }; 可是这段运行后很奇怪,页面从 0 到 1 后,就再也不变了, console.log(count) 的输出表明代码并没有卡死,那么问题出在哪儿了?React 组件中的 props 和 state 是可以改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果,它们之间不再有相关性。useEffect() Hook 也「丢弃」上一次渲染结果,它会清除上一次 effect 再建立下一个 effect,下一个 effect 锁住新的 props 和 state,这也是我们第一次尝试简单示例可以正确工作的原因。但 setInterval 不会「丢弃」。 它会一直引用老的 props 和 state 直到你把它换掉 —— 不重置时间你是无法做到的。这里就要用到useRef这个 hook 了,我们把 callback 存储到 ref 中,当 callback 更新时去更新 ref.current 的值:const useInterval = (callback, delay) => { const saveCallback = useRef(); useEffect(() => { // 每次渲染后,保存新的回调到我们的 ref 里 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } if (delay !== null) { const id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); }; 当我们使用新的 useInterval 时,发现就可以自增了,点击查看样例useInterval 的简单使用。这里我们使用一个变量来控制增加的速度:const [count, setCount] = useState(0); const [diff, setDiff] = useState(500); useInterval(() => { setCount(count + 1); }, diff); return ( count: {count} diff: {diff}ms setDiff(diff - 50)}> 加快50ms setDiff(diff + 50)}> 减慢50ms ); 分别点击两个按钮,可以调整 count 增加的速度。当想要停止定时器时,将 diff 设置为 null 即可(setDiff(null))。重新设置为数字时,定时器则重新启动。 3. 总结 # 使用 react hook 可以做很多有意思的事情,这里我们也仅仅是举几个简单的例子,后续我们也会更加深入了解 hook 的原理。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
node多进程的创建与守护
本篇文章主要分为4部分讲解: node的单线程 node多进程的创建 多进程间的通信 多进程的维护 1. node的单线程 # 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。线程是程序执行中一个单一的顺序控制流,它存在于进程之中,是比进程更小的能独立运行的基本单位。早期在单核 CPU 的系统中,为了实现多任务的运行,引入了进程的概念,不同的程序运行在数据与指令相互隔离的进程中,通过时间片轮转调度执行,由于 CPU 时间片切换与执行很快,所以看上去像是在同一时间运行了多个程序。由于进程切换时需要保存相关硬件现场、进程控制块等信息,所以系统开销较大。为了进一步提高系统吞吐率,在同一进程执行时更充分的利用 CPU 资源,引入了线程的概念。线程是操作系统调度执行的最小单位,它们依附于进程中,共享同一进程中的资源,基本不拥有或者只拥有少量系统资源,切换开销极小。Node是基于V8引擎之上构建的,决定了他与浏览器的机制很类似。一个node进程只能利用一个核,而且node只能运行在单线程中,严格意义上,node并非真正的单线程架构,即一个进程内可以有多个线程,因为node自己还有一定的i/o线程存在,这些I/O线程由底层的libuv处理,但这些线程对node开发者而言是完成透明的,只有在C++扩展时才会用到,这里我们就屏蔽底层的细节,专门讨论我们所要关注的。单线程的好处是:程序状态单一,在没有多线程的情况下,没有锁、线程同步问题,操作系统在调度时,也因为较少的上下文的切换,可以很好地提高CPU的使用率。然而单核单线程也有相应的缺点: 这个线程挂掉后整个程序就会挂掉; 无法充分利用多核资源 2. node多进程的创建 # node中有提供child_process模块,这个模块中,提供了多个方法来创建子进程。const?{?spawn,?exec,?execFile,?fork?}?=?require('child_process'); 这4个方法都可以创建子进程,不过使用方法还是稍微有点区别。我们以创建一个子进程计算斐波那契数列数列为例,子进程的文件(worker.js):// worker.js const fib = (num) => { if (num === 1 || num === 2) { return num; } let a = 1, b = 2, sum = 0; for (let i = 3; i {}) 启动一个子进程来执行命令,有回调 execFile exexFile('worker.js') 启动一个子进程来执行可执行的文件(头部要添加#!/usr/bin/env node) fork fork('worker.js') 与spawn类似,不过这里只需要自定js文件模块即可 以fork命令为例:const { fork } = require('child_process'); const cpus = require('os').cpus(); for(let i=0, len=cpus.length; i { if (num===1 || num===2) { return num; } let a=1, b=2, sum=0; for(let i=3; i { const result = fib(num); process.send(JSON.stringify({ num, result, pid: process.pid })) }) 输出进程:process.on('message', data => { console.log(process.pid, data); }) 当我们运行master时,就能看到各个子进程计算的结果:第1个数字表示当前输出子进程的编号,后面表示在各个子进程计算的数据。同理,我们在进行http服务日志记录时,也可以采用类似的思路,多个子进程承担http服务,剩下的子进程来进行日志记录等操作。当我想用子进程创建服务器时,采用上面类似斐波那契数列的思路,将fib.js改为httpServer.js:// httpServer.js const http = require('http'); http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(Math.random()+''); }).listen(8080); console.log('http server has started at 8080, pid: '+process.pid); 结果却出现错误了,提示8080端口已经被占用了:Error: listen EADDRINUSE: address already in use :::8080 这是因为:在TCP端socket套接字监听端口有一个文件描述符,每个进程的文件描述符都不相同,监听相同端口时就会失败。解决方案有两种:首先最简单的就是每个子进程都使用不同的端口,主进程将循环的标识给子进程,子进程通过这个标识来使用相关的端口(例如从8080+传入的标识作为当前进程的端口号)。第二种方案是,在主进程进行端口的监听,然后将监听的套接字传给子进程。主进程:// master.js const fork = require('child_process').fork; const net = require('net'); const server = net.createServer(); const child1 = fork('./httpServer1.js'); // random const child2 = fork('./httpServer2.js'); // now server.listen(8080, () => { child1.send('server', server); child2.send('server', server); server.close(); }) httpServer1.js:const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(Math.random()+', at pid: ' + process.pid); }); process.on('message', (type, tcp) => { if (type==='server') { tcp.on('connection', socket => { server.emit('connection', socket) }) } }) httpServer2.js:const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(Date.now()+', at pid: ' + process.pid); }); process.on('message', (type, tcp) => { if (type==='server') { tcp.on('connection', socket => { server.emit('connection', socket) }) } }) 我们的2个server,一个是输出随机数,一个是输出当前的时间戳,可以发现这两个server都可以正常的运行。同时,因为这些进程服务是抢占式的,哪个进程抢到连接,就哪个进程处理请求。我们也应当知道的是: 每个进程之间的内存数据是不互通的,若我们在某一进程中使用变量缓存了数据,另一个进程是读取不到的。 4. 多进程的守护 # 刚才我们在第3部分创建的多进程,解决了多核CPU利用率的问题,接下来要解决进程稳定的问题。每个子进程退出时,都会触发exit事件,因此我们通过监听exit事件来获知有进程退出了,这时,我们就可以创建一个新的进程来替代。const fork = require('child_process').fork; const cpus = require('os').cpus(); const net = require('net'); const server = net.createServer(); const createServer = () => { const worker = fork('./httpServer.js'); worker.on('exit', () => { // 当有进程退出时,则创建一个新的进程 console.log('worker exit: ' + worker.pid); createServer(); }); worker.send('server', server); console.log('create worker: ' + worker.pid); } server.listen(8080, () => { for(let i=0, len=cpus.length; i { console.log(`工作进程 ${worker.process.pid} 已退出`); cluster.fork(); }); } else { http.createServer((req, res) => { res.writeHead(200); res.end(Math.random()+ ', at pid: ' + process.pid); }).listen(8080); console.log(`工作进程 ${process.pid} 已启动`); } 5. 总结 # node虽然是单线程运行的,但我们可以通过创建多个子进程,来充分利用多核CPU资源,通过可以监听进程的一些事件,来感知每个进程的运行状态,来提高我们项目整体的稳定性。
2024年10月20日
1 阅读
0 评论
0 点赞
2024-10-20
腾讯抢金达人项目中的前后端协作
在前后端的协作过程中,通常都是并行开发的状态,那么在后端接口还没有开发完毕时,前端的业务逻辑工作就很难展开。因此也就有很多模拟接口数据的方式,这些方式各有个的优缺点: 直接在代码中模拟接口数据:侵入业务逻辑,在后期需要删除这些模拟数据; fiddler 替换文件:页面接口比较多时,需要替换的文件比较多; fs 模块读取 json 文件:若是长列表的话,需要造的数据很多; mockjs:避免上述方式的缺点,但无法校验参数是否缺失; service worker:基于 service worker 可以拦截前端的请求,并构建假数据返回,但无法拦截 node 端发起的请求; 方式 校验参数合法性 切换环境方便 前后端请求均可 不修改业务代码 模拟数据方便 直接在业务代码中写接口数据 fiddler 替换文件 yes fs 读取 json 文件 yes yes mockjs yes yes yes sw yes 我们理想的状态是: 提前校验请求接口中参数的合法性,是否缺失某些参数等; 切换环境方便,既可以使用模拟数据,也可以使用测试环境中的数据,同时也可以用正式环境中的数据进行检验; 可以拦截前后端均发起的请求,并尽量少的修改业务代码; 生成的模拟数据方便,假如接口中要返回前 1000 名用户的数据,总不能在 json 文件中写 1000 条数据; 上面的这几种方式,在我们抢金达人项目中,均不适用,或者对原有逻辑改动太大,或者使用起来不方便。这里我根据我们项目的需要,基于 mockjs 并与 express 的结合,实现了一套模拟数据的方法。 1. 模拟数据并校验参数的合法性 # 把接口的数据全部写在 json 文件,然后通过 fs 模块进行读取的这种方式,在构造大量数据时非常不方便。因此我们基于 mockjs 来实现模拟的数据,几行代码就能实现排行榜等大量的模拟数据,同时,也可以模拟一些稍微极端的情况,例如用户的昵称长度过长等,这些数据在测试环境一般很少能遇到,或者在后端接口模拟的成本也会比较高。// rank-person.js Mock.mock({ "rank|1000": [ { "no|+1": 1, uin: () => Mock.Random.string(32), nick: () => Mock.Random.string(1, 20), face: () => faces[Mock.Random.integer(0, 3)], region: "INVALID", title: "初露锋芒", level: () => Mock.Random.integer(1, 20), score: () => Mock.Random.integer(1, 2000), winPercent: 86 } ] }); 但是,纯基于 mockjs 数据的方式,我们无法提现获知接口参数的异常。当我们在匹配到接口请求后就返回数据,会降低对参数的敏感度。这里,我对配置文件进行改造,当前接口中需要的参数提前设定好,类似于 jQuery.validate 中的设定。这里我们的排行榜接口里有个last参数,0 表示是本周的数据,1 表示是上周的数据:module.exports = { params: { last: { required: true, // 是否必须 type: "number", // 参数的类型 defaults: 0, // 默认值 min: 0, // 最小值 max: 1 // 最大值等 } } }; 当 mock server 接收到请求后,会先校验参数的合法性,若参数不合法直接返回。其实我们排行榜的 last 参数不是必传项,不传时即默认是 0,但我们在这里测试时改为必传,只要不传 last 参数即为参数不合法:当参数校验通过后,才会返回后面模拟的数据: 2. 数据环境的切换 # 我们在上面的图中可以看到,当 mock 字段为"mock"时,读取模拟的数据,是不是一定要加一个 mock="mock"的参数才能去读模拟的数据呢?这个要看咱们项目到什么状态了,当项目还在前提开发阶段时,大部分接口都还没有完成,这里我们可以将接口默认指向到模拟数据,mock="testing"时就指向到测试环境的接口;当项目已稳定上线了,在迭代更新的阶段时,大部分接口已经存在和完善了,只有部分的接口需要进行模拟调试,这时我们用mock="mock"参数来指向到模拟数据。这里我使用 mock 参数控制,还有一个原因是,前端项目会根据当前是哪个环境,自动请求对应环境的接口,所有的接口均是统一控制的:如上图所示,如果我们要通过环境变量控制 api 字段,最终强行修改某个接口变相请求其他环境的数据,会造成其他接口数据的混乱,最终可能的结果是页面整体会挂掉。我们在使用mock字段作为参数时,侵入的业务逻辑会维持到最小的程度,同时,也能把前后端的协作,缩小到单个接口的粒度。而且,mock 参数也只会在 local 环境生效,即使忘了去掉这个参数,也不会对线上环境造成影响。再有,当我们前端逻辑发生变化后,除了使用模拟数据来检验,如果线上有接口,我们也想用线上的数据检验一下。可以,当传入mock="production"的参数时,mockServer 会读取线上接口的数据并返回。 3. mock 与 json # 有的同学会创建一个 json 文件,把接口需要的数据都放到这个 json 文件里,然后使用fs.readFile来读取。这样倒是也可以,但是当接口数据多的时候怎么办,例如有一个排行榜是要输出前 100 名、前 1000 名用户的数据,总不能复制出 1000 份用户的数据吧。在当前最好的方式,就是使用 mockjs 的工具来生成数据了,可以任意的随机,也可以生成任意个数的数组数据。我们这里把对参数的校验和 mock 生成的模拟数据放到一起:module.exports = { params: { last: { required: true, // 是否必须 type: "number", // 参数的类型 defaults: 0, // 默认值 min: 0, // 最小值 max: 1 // 最大值等 } }, result: { code: 0, msg: "success", "x-from": "mock", data: Mock.mock({ "rank|100": [ { "no|+1": 1, uin: () => Mock.Random.string(32), nick: () => Mock.Random.string(1, 20), face: () => faces[Mock.Random.integer(0, 3)], region: "INVALID", title: "初露锋芒", level: () => Mock.Random.integer(1, 20), score: () => Mock.Random.integer(1, 2000), winPercent: 86 } ] }) } }; params 字段表示必须的参数,result 表示要返回的数据,data 中即为我们伪造的数据。 4. 实时修改模拟数据 # 我们可以在页面中直接实时修改数据,然后进行保存,自动刷新页面后,就会得到刚才想要的结果。我们在页面中 mock 的接口都会在调试面板中展出来,默认调试面板时关闭的,开发者在点击页面右下角的灰色按钮后,可以呼起调试面板。修改里面的数据保存后,就实时看到刚才的效果。 5. 总结 # 在开发和后期维护的过程中,前端会经常遇到模拟接口数据的情况,要么是接口还没有开发完成,要么是接口不太好返回我们想要的状态。我也是在对比多种方案后,选择了适合当前项目的一种前后端协作方案。
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
36
37
38
...
213