关于select:一文说透IO多路复用selectpollepoll

概述如果咱们要开发一个高并发的TCP程序。惯例的做法是:多过程或者多线程。即:应用其中一个线程或者过程去监听有没有客户端连贯上来,一旦有新客户端连贯,就新开一个线程,将其扔到线程(或过程)中去解决具体的读写操作等业务逻辑,主线程(过程)持续期待,监听其余的客户端。 这样操作往往存在很大的弊病。首先是浪费资源,要晓得,单个过程的最大虚拟内存是4G,单个线程的虚拟内存也有将近8M,那么,如果上万个客户端连贯上来,服务器将会承受不住。 其次是浪费时间,因为你必须始终等在accept那个中央,非常被动。 上述的网络模型,其实说白了,就是一个线程一路IO,在单个线程里只能解决一个IO。因而,也可称之为单路IO。而一路IO,就是一个并发。有多少个并发,就必须要开启多少个线程,因而,对资源的节约是显而易见的。 那么,有没有一种形式,能够在一个线程里,解决多路IO呢? 咱们回顾一下多线程模型 ,它最大的技术难点是accept和recv函数都是阻塞的。只有没有新连贯上来,accept就阻塞住了,无奈解决后续的业务逻辑;没有数据过去,recv又阻塞住了 ,无奈解决新的accept申请。因而,只有可能搞定在同一个线程里同时accept和recv的问题,仿佛所有问题就迎刃而解了。 有人说,这怎么可能嘛?必定要两个线程的 。 还真有可能,而这所谓的可能 ,就是IO多路复用技术。 IO多路复用所谓的IO多路复用,它的核心思想就是,把监听新客户端连贯的操作转包进来,让零碎内核来做这件事件。即由内核来负责监听有没有连贯建设、有没有读写申请 ,作为服务端,只须要注册相应的事件,当事件触发时,由内核告诉服务端程序去解决就行了。 这样做的益处不言而喻:只须要在一个主线程里,就能够实现所有的工作,既不会阻塞,也不会节约太多资源。 说得通俗易懂一些,就是原来须要由主线程干的活,当初都交给内核去干了。咱们不必阻塞在accept和recv那里,而是由内核通知程序,有新客户端连贯上来了 ,或者有数据发送过去了,咱们再去调用accept和recv就行了,其余工夫,咱们能够解决其余的业务逻辑。 那么有人问了,你不还是要调用accept和recv吗?为什么当初就不会阻塞了呢 ? 这就要深刻说一下listen和accept的关系了。 如果服务器是海底捞火锅店的话,listen就是门口迎宾的小姐,当来了一个客人(客户端),就将其迎进店内。而accept则是店内的大堂经理 ,当没人来的时候,就始终闲在那里没事做,listen将客人 迎进来之后,accept就会调配一个服务员(fd)专门 服务于这个客人 。 所以说,只有listen失常工作,就能源源不断地将客人迎进饭店(客户端能 失常连贯上服务器),即便此时并没有accept。那么,有人必定有疑难,总不能始终 往里迎吧,酒店也是有大小的,全副挤在大堂也装不下 那么多人啊。还记得 listen函数的第二个参数backlog吗?它就示意在没有accept之前,最多能够迎多少个客人进来。 因而,对于多线程模型来说,accept作为大堂经理,在 没客人来的时候 ,就眼巴巴地盯着门口 ,啥也不干,当listen把人迎进来了,才开始干活。只能说,摸鱼,还是accpet会啊。 而IO多路复用,则相当于请了一个秘书。accept作为大堂经理,必定有很多其余事件能够忙,他就不必 始终盯着门口,当listen把人迎进来之后,秘书就会把客人(们)带到经理身边,让经理安顿服务员(fd)。 只是这个秘书是内核提供的,因而不仅收费,而且勤快。收费的劳动力 ,何乐而不为呢? 它的流程图大略是上面这样子的: 咱们通常所说的IO多路复用技术,在Linux环境下,次要有三种实现,别离为select、poll 和 epoll,以及io_uring。在darwin平台 ,则有kqueue,Windows 下则是 iocp。从性能上来说,iocp要优于epoll,与io_uring并驾齐驱。但select、poll、epoll的演变是一个继续迭代的过程,虽说从效率以及应用普及率上来说,epoll堪称经典,但并不是另外两种实现就毫无用处,也是有其存在的意义的,尤其是select。 本文不会花太多笔墨来介绍kqueue,笔者始终认为,拿MacOS作为服务器开发,要么脑子瓦特了,要么就是钱烧的。基本上除了本人写写 demo外,极少能在生产环境真正用起来。而iocp自成一派,将来有暇,将专门开拓专题细说。io_uring作为较新的内核才引入的个性,本文也不宜大肆开展。 唯有select、poll 以及epoll,久经工夫考验,已被宽泛使用于各大出名网络应用,并由此诞生出许多经典的网络模型,切实是值得好好细说。 select原型select函数原型: /* According to POSIX.1-2001, POSIX.1-2008 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);参数阐明: ...

April 14, 2023 · 6 min · jiezi

关于select:彻底搞懂IO多路复用

上一篇文章以近乎啰嗦的形式详细描述了BIO与非阻塞IO的各种细节。如果各位还没有读过这篇文章,强烈建议先浏览一下,而后再来看本篇,因为逻辑关系是层层递进的。 1. 多路复用的诞生非阻塞IO应用一个线程就能够解决所有socket,然而付出的代价是必须频繁调用零碎调用来轮询每一个socket的数据,这种轮询太消耗性能,而且大部分轮询都是空轮询。 咱们心愿有个组件能同时监控多个socket,并在socket把数据筹备好的时候通知过程哪些socket已“就绪”,而后过程只对就绪的socket进行数据读写。 Java在JDK1.4的时候引入了NIO,并提供了Selector这个组件来实现这个性能。 2. NIO在引入NIO代码之前,有点事件须要解释一下。 “就绪”这个词用得有点暧昧,因为不同的socket对就绪有不同的表白。比方对于监听socket而言,如果有客户端对其进行了连贯,就阐明处于就绪状态,它并不像连贯socket一样,须要对数据的收发进行解决;相同,连贯socket的就绪状态就至多蕴含了数据筹备好读(is ready for reading)与数据筹备好写(is ready for writing)这两种。 因而,能够设想,咱们让Selector对多个socket进行监听时,必然须要通知Selector,咱们对哪些socket的哪些事件感兴趣。这个动作叫注册。 接下来看代码。 public class NIOServer { static Selector selector; public static void main(String[] args) { try { // 取得selector多路复用器 selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 监听socket的accept将不会阻塞 serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8099)); // 须要把监听socket注册到多路复用器上,并通知selector,须要关注监听socket的OP_ACCEPT事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 该办法会阻塞 selector.select(); // 失去所有就绪的事件,事件被封装成了SelectionKey Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()) { handleAccept(key); } else if (key.isReadable()) { handleRead(key); } else if (key.isWritable()) { //发送数据 } } } } catch (IOException e) { e.printStackTrace(); } } // 解决「读」事件的业务逻辑 private static void handleRead(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer allocate = ByteBuffer.allocate(1024); try { socketChannel.read(allocate); System.out.println("From Client:" + new String(allocate.array())); } catch (IOException e) { e.printStackTrace(); } } // 解决「连贯」事件的业务逻辑 private static void handleAccept(SelectionKey key) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); try { // socketChannel肯定是非空,并且这里不会阻塞 SocketChannel socketChannel = serverSocketChannel.accept(); // 将连贯socket的读写设置为非阻塞 socketChannel.configureBlocking(false); socketChannel.write(ByteBuffer.wrap("Hello Client, I am Server!".getBytes())); // 注册连贯socket的「读事件」 socketChannel.register(selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } }}咱们首先应用Selector.open();失去了selector这个多路复用对象;而后在服务端创立了监听socket,并将其设置为非阻塞,最初将监听socket注册到selector多路复用器上,并通知selector,如果监听socket有OP_ACCEPT事件产生的话就要通知咱们。 ...

January 31, 2023 · 8 min · jiezi

关于select:React中select设置defaultValue不生效

问题这样一种写法下,defaultValue是不会失效的。 function Test() { const [dv, setDv] = useState(0) // 在某个元素的点击回调中setDv() // foo.onClick = () => {setDv(bar)} return( <select defaultValue={dv} > <option value='1' >1</option> <option value='2' >2</option> <option value='3' >3</option> </select> )}解决给<select/>加个动静key强制执行渲染。 function Test() { const [dv, setDv] = useState(0) // 在某个元素的点击回调中setDv() // foo.onClick = () => {setDv(bar)} return( <select key={Date.now()} defaultValue={dv} > <option value='1' >1</option> <option value='2' >2</option> <option value='3' >3</option> </select> )}

January 21, 2023 · 1 min · jiezi

关于select:GrowingIO-Design-组件库搭建之Select组件

前言Select 是最频繁应用的UI组件之一,它能够使用在很多场景。大多数状况下,原生HTML的<Select>标签无奈满足业务的性能过需要,以及原生HTML的<Select>标签在各个浏览器版本里款式体现不太一样。在这样的状况下,少数人都会抉择实现一个合乎UI要求以及产品性能需要的Select组件,或者抉择应用一些开源组件库提供的 Select 组件。本文次要梳理了 gio-design 中的Select组件在实现过程中遇到的一些妨碍及须要留神的中央,心愿能对大家在设计和实现select组件时提供一些帮忙。 数据源(dataSource)Select 组件的两种应用办法: // 第一种写法const options = [{label:'a',value:'a'},{label:'b',value:''b}];<Select options={options} />// 第二种<Select> <Select.Option value={'a'} >a</Select.Option> <Select.Option value={'b'} >b</Select.Option></Select>应用 Select 组件的时候,个别状况下,有两种形式来设定 dataSource。 通过 options 参数,传入纯数据格式。JSX,通过拦挡子组件 <Select.Option/> 的参数,转化为 nodeOptions。相比拟 JSX 而言,options 参数模式领有更好的性能( JSX 形式最终也会转为相似 options 参数的模式)。该转换形式借鉴了rc-select 中的写法。export function convertChildrenToData(nodes: React.ReactNode, group = {}): Option[] { let nodeOptions: Option[] = []; React.Children.forEach(nodes, (node: React.ReactElement & { type: { isSelectOptGroup?: boolean }; props: OptionProps }) => { if (!React.isValidElement(node)) return; const { type: { isSelectOptGroup }, props: { children, label, value }, } = node; if (!isSelectOptGroup) { // option nodeOptions.push(convertNodeToOption(node, group)); } else { // Group nodeOptions = concat(nodeOptions, convertChildrenToData(children, { groupLabel: label, groupValue: value })); } }); return nodeOptions;}// ReactNode To Optionsexport function convertNodeToOption(node: React.ReactElement, group: group): Option { const { props: { value, children, ...restProps }, } = node as React.ReactElement & { props: OptionProps }; const { groupValue, groupLabel } = group; if (groupLabel && groupLabel) { return { value, label: children !== undefined ? children : value, groupValue, groupLabel, ...restProps }; } return { value, label: children !== undefined ? children : value, ...restProps };}Group 和 Option 的定义: ...

July 5, 2021 · 5 min · jiezi

elementui设置下拉选择切换必填和非必填

➢ 需求默认都是必选 下拉选择的时候 选择必填,活动名称为必填,需要校验和显示* 选择非必填,活动名称不做校验,隐藏* ➢ 初始校验规则经测试,网上其他的方式都没有实现需求,动态切换rules中的required没有作用 因为按照以下的写法的话,element-ui在组件初始化后校验规则就定型了,切换也没用 rules: { name: [ { required: true, message: "请输入名称", trigger: "blur" } ], region: [ { required: true, message: "请选择类型", trigger: "blur" } ]}➢ 解决方案第一步: 去除rules中需要动态校验的字段规则 去除name rules: { region: [ { required: true, message: "请选择类型", trigger: "blur" } ]}第二步: 在字段为name的form-item上,添加required属性 下面代码isHaveTo为新字段,根据下拉框选择的值来决定是为true还是false <el-form-item label="活动名称" prop="name" :required="isHaveTo"> <el-input v-model="ruleForm.name"></el-input></el-form-item>第三步: 计算属性,新增字段isHaveTo 下拉选择框非必须是为1,其他都是必填,包括默认 computed: { isHaveTo: function() { return this.ruleForm.region === `0`; }},效果如图: ...

June 16, 2019 · 2 min · jiezi

动态增加select框elementUI-框架

<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>演示动态增加select框(elementUI 框架)</title> <script src="//unpkg.com/vue/dist/vue.js"></script> <script src="//unpkg.com/element-ui@2.8.2/lib/index.js"></script> <style type="text/css"> @import url("//unpkg.com/element-ui@2.8.2/lib/theme-chalk/index.css"); </style> </head> <body> <div id="app"> <!-- 带多选的 select --> <el-select v-model="value5" multiple placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <!-- 带清除的 select --> <el-select v-model="value5" clearable placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <!-- 带计数的 select --> <el-select v-model="value11" multiple collapse-tags style="margin-left: 20px;" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <br/> <hr style="height: 20px; color: aqua; background-color: aqua;" /> <el-form> <el-form-item v-for="(it, index) in list" :key="index"> <el-select v-model="oneId[index]" placeholder="请选择" @change="saveList($event, index)"> <el-option v-for="item in array" :key="item.id" :label="item.name" :value="item.id" > </el-option> </el-select> <el-button @click="addItem" type="primary">增加</el-button> <el-button @click="removeItem(it, index)" type="danger">删除</el-button> </el-form-item> <el-button type="success" @click="submit">提交</el-button> </el-form> </div> </body> <script> var vue = new Vue({ el: '#app', data() { return { options: [ {value: '选项1',label: '黄金糕'}, {value: '选项2',label: '双皮奶'}, {value: '选项3',label: '蚵仔煎'}, {value: '选项4',label: '龙须面'}, {value: '选项5',label: '北京烤鸭'}, ], value5: [], value11: [], oneId: [], array:[ {id: '1',name: '黄金糕'}, {id: '2',name: '双皮奶'}, {id: '3',name: '蚵仔煎'}, {id: '4',name: '龙须面'}, {id: '5',name: '北京烤鸭'} ], list:[{"oneId":''}], selectedList:[], // 存储每次 option 选中的 集合 } }, methods:{ addItem(){ // 1。这里为什么改变list的大小就能实现动态增加呢?因为 el-form-item 遍历的是 list,list 中的每一项都是一个 el-form-item // 也就是说因为刚开始 list:[{"oneId":''}] 中,只有一个对象,所以才会只出现一个 el-form-item // 不信可以自己在初始化时 list 中多加入几个对象进行尝试(一定要理解,这里 list 集合的大小与 el-form-item 之间的关系) // 2、第二个问题:el-form-item 是动态增加了,但是如果 el-select 那里写的是 v-model="oneId" 呢?会发生什么?结果你会发现,只要增加一项 el-form-item ,每一项绑定的值都是你所选中的那一个值.为什么呢?因为每一项的 el-option的 :value 值都绑定在 el-select 的 v-model 上,但这是一个全局唯一值,当下一个 el-form-item 产生后,它里面的 el-select 中绑定的 v-model 还是那个 oneId 的值,因此才会出现这样的问题.好了,我们既然找到了原因,那就要来解决一下了,怎么解决呢?很简单:因为我前面说了,每一个 list 的遍历对象,都是一项 el-form-item,即 el-form-item 项数是和 list 的下标(里面存的对象的索引下标)相关联的,而这个下标,在每一个 el-form-item 中肯定是不一样的,因此我们只需要将 oneId 与这个 下标(即此处的 index) 发生关系即可,因此我们这里将 oneId 声明为了一个数组,当你每选中一个 option 时,都将这个 option 的value放入 oneId[当前el-form-item项数下标] 数组中 this.list.push({"oneId": ''}); }, removeItem(it, index){ // 删除时,我们带两个参数,这个 it 可用可不用,因为我当时只是想看到删除的这个对象的信息,故而带上了; index 是 list 中该对象对应的下标,也是 el-form-item 的项数 // 根据这个 index 下标删除 list 中 的该对象 if(index != 0){ this.list.splice(index, 1); } }, saveList(event, index){ // 当每选一个 option 时,我们需要将这个 选中的 oneId 放入 对应的 list 中即可,最后都选中完后,我们只要获取这个 list,即可拿到所有的数据 this.list[index].oneId = event; }, submit(){ // 这里我们打印一下 最后的 list,确保我们都拿到数据了 alert(`最终的数据: ${JSON.stringify(this.list)}`); }, }, }); </script></html>声明我只写自己的原创内容,所有内容都是自己手敲,实践过得,这个也是我在实际项目中要用,所以自己就敲了一遍。如需转载请注明出处,谢谢

May 24, 2019 · 2 min · jiezi

react写一个select组件

之前一直用的antd的Select组件,但在有些端并不适用,而原生的select样式修改不灵活,遂产生自己写一个组件的想法。观察select组件:<select onChange={(value) => {this.value=value}} <option value=‘1’>man</option> <option value=‘0’>woman</option></select>可以看出数据都是在option中,有值value和显示出来的数据一一对应。如果我们写一个select组件,那么应该有onChange方法,应该要访问到子元素,而且div是没有value这个属性的,所以option应该也是一个组件,有value属性。下面是我写的组件的用法:import {MobileSelect, MobileOption} from ‘../../components/MobileSelect’; <MobileSelect disabled={isDisabled} value={data.clarity || ringResponse.clarity || ‘Flawless’} style={{ width: ‘132px’ }} onChange={(v) => this.changeDataValue(‘clarity’, v)} > { (clarity || []).map((item, i) => { return ( <MobileOption key={i + ‘’} value={item.code}>{item.title}</MobileOption> ); }) } </MobileSelect>可以看出其和一般的select组件用法差不多。效果如下:下面是组件import {observable} from ‘mobx’;import {observer} from ‘mobx-react’;import React from ‘react’;import {Icon} from ‘antd’;import ‘./index.less’;interface IProps { disabled?: boolean; onChange?: (value) => void; value?: string | number; style?: React.CSSProperties; className?: string;}@observerexport class MobileSelect extends React.Component<IProps> { @observable showOption = false; // 是否弹出下拉框 @observable value: any = ‘’; // 当前选中的value值 @observable text: any = ‘’; // 选中的value值对应的文本 @observable cell: any; // 组件的dom节点 componentDidMount(): void { // 获取选择框的ref,当在组件外点击时的时候收起下拉框 document.addEventListener(‘click’, (e) => { if (this.cell && this.cell !== e.target && !this.cell.contains(e.target)) { this.showOption = false; } }, true); } componentWillReceiveProps(nextProps: Readonly<IProps>, nextContext: any): void { // 根据传入的value值,遍历children,找到对应值的展示文本 if (nextProps.value !== this.props.value || nextProps.children !== this.props.children) { React.Children.map(this.props.children, (child, index) => { if (nextProps.value === child.props.value) { this.text = child.props.children; } }); } } render(): React.ReactNode { const {children, value} = this.props; console.log(value); return ( <div className={‘Mobile-Select ’ + this.props.className} style={this.props.style} ref={(node) => this.cell = node} > <div className={‘select-wrap’} onClick={() => { // 禁用不能弹出下拉框 if (!this.props.disabled) { this.showOption = !this.showOption; } }} > <Icon type=‘down’ style={this.showOption ? {transform: ‘rotate(180deg)’} : {transform: ‘rotate(0deg)’}} className={‘select-icon’}/> {this.text} </div> <div className={‘option-wrap’} style={this.showOption ? {position: ‘absolute’} : {display: ’none’}}> { React.Children.map(children, (child, index) => { // 设置选中option和未选中option的样式 let optionClassName = ‘’; if (this.props.value === child.props.value) { optionClassName = child.props.className ? child.props.className + ’ option-item selected’ : ‘option-item selected’; } else { optionClassName = child.props.className + ’ option-item’; } return ( <div onClick={() => { // 为了在父组件给子组件添加onClick事件,包裹了一层div // 有无onChange事件都能改变值 if (this.props.value && this.props.onChange) { this.props.onChange(child.props.value); } else { this.text = child.props.children; this.value = child.props.value; } console.log(this.value); this.showOption = !this.showOption; }} style={this.props.style} className={optionClassName} >{child}</div> ); }) } </div> </div> ); }}interface OptionProps { value?: string | number; className?: string; style?: React.CSSProperties;}export class MobileOption extends React.Component<OptionProps> { render(): React.ReactNode { const {children} = this.props; return ( <div style={this.props.style}> {children} </div> ); }}下面是组件的样式.Mobile-Select { display: inline-block; min-width: 100px; margin: 0 6px; .select-wrap { border: 1px solid #e0c0a2; border-radius: 4px; padding: 5px 11px; display: flex; flex-direction: row-reverse; justify-content: space-between; align-items: center; .select-icon { transition: .3s; float: right; } } .option-wrap { box-shadow: 0 0 5px #333; z-index: 1000; border-radius: 5px; .option-item { background-color: #fff; padding: 2px 11px; min-width: 100px; &.selected { background-color: #fbe6d0; } } }}总的来说只实现了select的基本功能。有改进的地方请指点一二。 ...

April 3, 2019 · 2 min · jiezi

Ant Design UI组件之Select踩坑

Ant Design UI组件之Select踩坑前言在使用Ant design UI组件时总会遇到一些奇奇怪怪的问题,在本篇中将总结中在使用Select中几种容易常见的问题,持续更新遇到的问题在初始化Select值,如何根据value显示对应文本实现代码如下…this.props.form.setFieldsValue({ latticeId, latticeNo, goodsId, remaining});…<FormItem {…formItemLayout} label=“商品”> {getFieldDecorator(‘goodsId’, { })( <Select style={{ width: ‘150px’ }}> {this.state.goodsData.map((item,index) => <Option key={item.goodsId} >{item.goodsId +’-’ + item.goodsName}</Option>)} </Select> )} </FormItem> 此时,代码实现的效果并不如预期效果,显示出了商品的id在尝试加上value属性的时候出现了一个问题查阅相关文档是支持number的,百思不得其解。了解到项目使用版本是2.13.10版本的,怀疑是版本问题。查阅对应版本的文档,问题就出现在这里,在2.13.11版本中value是还不支持number类型的,只支持string。在了解到问题的根源后,修改相应代码。…this.props.form.setFieldsValue({ goodsId: goodsId && goodsId.toString(),});…<FormItem {…formItemLayout} label=“商品”> {getFieldDecorator(‘goodsId’, { })( <Select style={{ width: ‘150px’ }}> {this.state.goodsData.map((item,index) => <Option key={item.goodsId} value={item.goodsId.toString()}>{item.goodsId +’-’ + item.goodsName}</Option>)} </Select> )} </FormItem>Antd select如何设置能够实现输入筛选在使用select实现输入筛选时只需要在Select中加上showSearch即可,不过需要注意的是默认是根据value值筛选,需要使用内容实现输入筛选的话可以使用设置optionFilterProp属性为"children"。 总结在使用Ant Design UI组件时遇到一些不符合预期的错误时,可以查看是否是因版本问题导致的,才能对症下药第二个问题出现是因为一开始有人告知筛选属性只能根据value去筛选而忽略了去查看官方文档,而采用蹩脚的方式去实现,花费了较长时间。再去查看相应官方文档由于英文不好,没有理解到官方文档的意思。还是需要加强对英文官方文档的理解。在使用组件时最好能帮该组件的属性都熟悉理解一遍,不要偷懒只听从他人的,很多问题的出现都可以从官方文档中找到想要的答案。

February 27, 2019 · 1 min · jiezi

总结了才知道,原来channel有这么多用法!

这篇文章总结了channel的10种常用操作,以一个更高的视角看待channel,会给大家带来对channel更全面的认识。在介绍10种操作前,先简要介绍下channel的使用场景、基本操作和注意事项。channel的使用场景把channel用在数据流动的地方:消息传递、消息过滤信号广播事件订阅与广播请求、响应转发任务分发结果汇总并发控制同步与异步…channel的基本操作和注意事项channel存在3种状态:nil,未初始化的状态,只进行了声明,或者手动赋值为nilactive,正常的channel,可读或者可写closed,已关闭,千万不要误认为关闭channel后,channel的值是nilchannel可进行3种操作:读写关闭把这3种操作和3种channel状态可以组合出9种情况:对于nil通道的情况,也并非完全遵循上表,有1个特殊场景:当nil的通道在select的某个case中时,这个case会阻塞,但不会造成死锁。参考代码请看:https://dave.cheney.net/2014/…下面介绍使用channel的10种常用操作。1. 使用for range读channel场景:当需要不断从channel读取数据时原理:使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。用法:for x := range ch{ fmt.Println(x)}2. 使用_,ok判断channel是否关闭场景:读channel,但不确定channel是否关闭时原理:读已关闭的channel会造成panic,如果不确定channel,需要使用ok进行检测。ok的结果和含义:true:读到数据,并且通道没有关闭。false:通道关闭,无数据读到。用法:if v, ok := <- ch; ok { fmt.Println(v)}3. 使用select处理多个channel场景:需要对多个通道进行同时处理,但只处理最先发生的channel时原理:select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的。用法:// 分配job时,如果收到关闭的通知则退出,不分配jobfunc (h *Handler) handle(job *Job) { select { case h.jobCh<-job: return case <-h.stopCh: return }}4. 使用channel的声明控制读写权限场景:协程对某个通道只读或只写时目的:A. 使代码更易读、更易维护,B. 防止只读协程对通道进行写数据,但通道已关闭,造成panic。用法:如果协程对某个channel只有写操作,则这个channel声明为只写。如果协程对某个channel只有读操作,则这个channe声明为只读。// 只有generator进行对outCh进行写操作,返回声明// <-chan int,可以防止其他协程乱用此通道,造成隐藏bugfunc generator(int n) <-chan int { outCh := make(chan int) go func(){ for i:=0;i<n;i++{ outCh<-i } }() return outCh}// consumer只读inCh的数据,声明为<-chan int// 可以防止它向inCh写数据func consumer(inCh <-chan int) { for x := range inCh { fmt.Println(x) }}5. 使用缓冲channel增强并发和异步场景:异步和并发原理:A. 有缓冲通道是异步的,无缓冲通道是同步的,B. 有缓冲通道可供多个协程同时处理,在一定程度可提高并发性。用法:// 无缓冲,同步ch1 := make(chan int)ch2 := make(chan int, 0)// 有缓冲,异步ch3 := make(chan int, 1)// 使用5个do协程同时处理输入数据func test() { inCh := generator(100) outCh := make(chan int, 10) for i := 0; i < 5; i++ { go do(inCh, outCh) } for r := range outCh { fmt.Println(r) }}func do(inCh <-chan int, outCh chan<- int) { for v := range inCh { outCh <- v * v }}6. 为操作加上超时场景:需要超时控制的操作原理:使用select和time.After,看操作和定时器哪个先返回,处理先完成的,就达到了超时控制的效果用法:func doWithTimeOut(timeout time.Duration) (int, error) { select { case ret := <-do(): return ret, nil case <-time.After(timeout): return 0, errors.New(“timeout”) }}func do() <-chan int { outCh := make(chan int) go func() { // do work }() return outCh}7. 使用time实现channel无阻塞读写场景:并不希望在channel的读写上浪费时间原理:是为操作加上超时的扩展,这里的操作是channel的读或写用法:func unBlockRead(ch chan int) (x int, err error) { select { case x = <-ch: return x, nil case <-time.After(time.Microsecond): return 0, errors.New(“read time out”) }}func unBlockWrite(ch chan int, x int) (err error) { select { case ch <- x: return nil case <-time.After(time.Microsecond): return errors.New(“read time out”) }}注:time.After等待可以替换为default,则是channel阻塞时,立即返回的效果8. 使用close(ch)关闭所有下游协程场景:退出时,显示通知所有协程退出原理:所有读ch的协程都会收到close(ch)的信号用法:func (h *Handler) Stop() { close(h.stopCh) // 可以使用WaitGroup等待所有协程退出}// 收到停止后,不再处理请求func (h *Handler) loop() error { for { select { case req := <-h.reqCh: go handle(req) case <-h.stopCh: return } }}9. 使用chan struct{}作为信号channel场景:使用channel传递信号,而不是传递数据时原理:没数据需要传递时,传递空struct用法:// 上例中的Handler.stopCh就是一个例子,stopCh并不需要传递任何数据// 只是要给所有协程发送退出的信号type Handler struct { stopCh chan struct{} reqCh chan *Request}10. 使用channel传递结构体的指针而非结构体场景:使用channel传递结构体数据时原理:channel本质上传递的是数据的拷贝,拷贝的数据越小传输效率越高,传递结构体指针,比传递结构体更高效用法:reqCh chan *Request// 好过reqCh chan Request你有哪些channel的奇淫巧技,说来看看?如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/01/20/golang-channel-all-usage/ ...

January 21, 2019 · 2 min · jiezi

Golang并发模型:select进阶

最近公司工作有点多,Golang的select进阶就这样被拖沓啦,今天坚持把时间挤一挤,把吹的牛皮补上。前一篇文章《Golang并发模型:轻松入门select》介绍了select的作用和它的基本用法,这次介绍它的3个进阶特性。nil的通道永远阻塞如何跳出for-selectselect{}阻塞nil的通道永远阻塞当case上读一个通道时,如果这个通道是nil,则该case永远阻塞。这个功能有1个妙用,select通常处理的是多个通道,当某个读通道关闭了,但不想select再继续关注此case,继续处理其他case,把该通道设置为nil即可。下面是一个合并程序等待两个输入通道都关闭后才退出的例子,就使用了这个特性。func combine(inCh1, inCh2 <-chan int) <-chan int { // 输出通道 out := make(chan int) // 启动协程合并数据 go func() { defer close(out) for { select { case x, open := <-inCh1: if !open { inCh1 = nil continue } out<-x case x, open := <-inCh2: if !open { inCh2 = nil continue } out<-x } // 当ch1和ch2都关闭是才退出 if inCh1 == nil && inCh2 == nil { break } } }() return out}如何跳出for-selectbreak在select内的并不能跳出for-select循环。看下面的例子,consume函数从通道inCh不停读数据,期待在inCh关闭后退出for-select循环,但结果是永远没有退出。func consume(inCh <-chan int) { i := 0 for { fmt.Printf(“for: %d\n”, i) select { case x, open := <-inCh: if !open { break } fmt.Printf(“read: %d\n”, x) } i++ } fmt.Println(“combine-routine exit”)}运行结果:➜ go run x.gofor: 0read: 0for: 1read: 1for: 2read: 2for: 3gen exitfor: 4for: 5for: 6for: 7for: 8… // never stop既然break不能跳出for-select,那怎么办呢?给你3个锦囊:在满足条件的case内,使用return,如果有结尾工作,尝试交给defer。在select外for内使用break挑出循环,如combine函数。使用goto。select{}永远阻塞select{}的效果等价于创建了1个通道,直接从通道读数据:ch := make(chan int)<-ch但是,这个写起来多麻烦啊!没select{}简洁啊。但是,永远阻塞能有什么用呢!?当你开发一个并发程序的时候,main函数千万不能在子协程干完活前退出啊,不然所有的协程都被迫退出了,还怎么提供服务呢?比如,写了个Web服务程序,端口监听、后端处理等等都在子协程跑起来了,main函数这时候能退出吗?select应用场景最后,介绍下我常用的select场景:无阻塞的读、写通道。即使通道是带缓存的,也是存在阻塞的情况,使用select可以完美的解决阻塞读写,这篇文章我之前发在了个人博客,后面给大家介绍下。给某个请求/处理/操作,设置超时时间,一旦超时时间内无法完成,则停止处理。select本色:多通道处理并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 18, 2018 · 1 min · jiezi

Golang并发模型:轻松入门select

之前的文章都提到过,Golang的并发模型都来自生活,select也不例外。举个例子:我们都知道一句话,“吃饭睡觉打豆豆”,这一句话里包含了3件事:妈妈喊你吃饭,你去吃饭。时间到了,要睡觉。没事做,打豆豆。在Golang里,select就是干这个事的:到吃饭了去吃饭,该睡觉了就睡觉,没事干就打豆豆。结束发散,我们看下select的功能,以及它能做啥。select功能在多个通道上进行读或写操作,让函数可以处理多个事情,但1次只处理1个。以下特性也都必须熟记于心:每次执行select,都会只执行其中1个case或者执行default语句。当没有case或者default可以执行时,select则阻塞,等待直到有1个case可以执行。当有多个case可以执行时,则随机选择1个case执行。case后面跟的必须是读或者写通道的操作,否则编译出错。select长下面这个样子,由select和case组成,default不是必须的,如果没其他事可做,可以省略default。func main() { readCh := make(chan int, 1) writeCh := make(chan int, 1) y := 1 select { case x := <-readCh: fmt.Printf(“Read %d\n”, x) case writeCh <- y: fmt.Printf(“Write %d\n”, y) default: fmt.Println(“Do what you want”) }}我们创建了readCh和writeCh2个通道:readCh中没有数据,所以case x := <-readCh读不到数据,所以这个case不能执行。writeCh是带缓冲区的通道,它里面是空的,可以写入1个数据,所以case writeCh <- y可以执行。有case可以执行,所以default不会执行。这个测试的结果是$ go run example.goWrite 1用打豆豆实践select来,我们看看select怎么实现打豆豆:eat()函数会启动1个协程,该协程先睡几秒,事件不定,然后喊你吃饭,main()函数中的sleep是个定时器,每3秒喊你吃1次饭,select则处理3种情况:从eatCh中读到数据,代表有人喊我吃饭,我要吃饭了。从sleep.C中读到数据,代表闹钟时间到了,我要睡觉。default是,没人喊我吃饭,也不到时间睡觉,我就打豆豆。import ( “fmt” “time” “math/rand”)func eat() chan string { out := make(chan string) go func (){ rand.Seed(time.Now().UnixNano()) time.Sleep(time.Duration(rand.Intn(5)) * time.Second) out <- “Mom call you eating” close(out) }() return out}func main() { eatCh := eat() sleep := time.NewTimer(time.Second * 3) select { case s := <-eatCh: fmt.Println(s) case <- sleep.C: fmt.Println(“Time to sleep”) default: fmt.Println(“Beat DouDou”) }}由于前2个case都要等待一会,所以都不能执行,所以执行default,运行结果一直是打豆豆:$ go run x.goBeat DouDou现在我们不打豆豆了,你把default和下面的打印注释掉,多运行几次,有时候会吃饭,有时候会睡觉,比如这样:$ go run x.goMom call you eating$ go run x.goTime to sleep$ go run x.goTime to sleepselect很简单但功能很强大,它让golang的并发功能变的更强大。这篇文章写的啰嗦了点,重点是为下一篇文章做铺垫,下一篇我们将介绍下select的高级用法。select的应用场景很多,让我总结一下,放在下一篇文章中吧。并发系列文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门select如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。我的文章列表,点此可查看如果喜欢本文,随意转载,但请保留此原文链接。 ...

December 12, 2018 · 1 min · jiezi

美化select下拉框

在写示例的时候,用到了下拉框,但是原生的下拉框是在是有点难看,然后模仿着写了点,一个是直接在写好的Dom上进行美化,一个是用js生成,然后定义类名及相应的事件来处理1.效果图2.直接是在Dom上美化html文件<div class=“root”> <div id=“selectedItem”> <div id=“promptText”><span id=“spanText”>请选择你喜欢的文字</span><img src="../images/arrowup.png" id=“arrows” /></div> <ul class=“choiceDescription”> <li class=“item”>万水千山,陪你一起看</li> <li class=“item”>万水千山,陪你一起看1</li> <li class=“item”>万水千山,陪你一起看2</li> <li class=“item”>万水千山,陪你一起看3</li> <li class=“item”>万水千山,陪你一起看4</li> </ul> </div></div>css文件ul{ margin: 0; padding: 0; list-style: none;}/* 下拉框包含层 /#selectedItem{ width: 240px; cursor: pointer;}/ 已选中的选项 /#promptText{ position: relative; padding-left: 10px; width: 230px; height: 30px; line-height: 30px; border: 1px solid #d3d3d3; border-radius: 4px; background: #fff; color: #999; font-size: 14px;}/ 图标 /#arrows{ position: absolute; top: 0; right: 0; width: 30px; height: 30px; vertical-align: middle;}#arrows:focus{ outline: none;}/ 下拉可选项包含层 /.choiceDescription{ position: absolute; display: none; /overflow: hidden;/ margin-top: 2px; width: 240px; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 1px 6px rgba(0, 0, 0, .1); background: #fff;}.show{ display: block;}/ 下拉可选项 /.item{ height: 30px; line-height: 30px; padding-left: 10px; font-size: 15px; color: #666;}.item:hover{ color: #fff; background: rgba(49, 255, 195, 0.67);}js文件(function() { let choiceDescription = document.getElementsByClassName(‘choiceDescription’)[0]; let arrows = document.getElementById(‘arrows’); / 用于判断是否是下拉 / let isDown = false; let selectedItem = document.getElementById(‘selectedItem’); / 对点击下拉进行监听 / selectedItem.addEventListener(‘click’, function() { isDown = !isDown; if(isDown) { / 如果是下拉状态,则显示下拉的选项,并把图标显示为向下的图标 / choiceDescription.className += ’ show’; arrows.src = ‘../images/arrowdown.png’; } else { choiceDescription.className = ‘choiceDescription’; arrows.src = ‘../images/arrowup.png’; } }); choiceDescription.addEventListener(‘click’, function(e) { let promptText = document.getElementById(‘spanText’); let selectElement = e.target; / 判断是否点击的是li标签,防止点击了li标签以外的空白位置 / while(selectElement.tagName !== ‘LI’) { / 如果点中的是当前容器层 / if(selectElement == choiceDescription) { selectElement = null; break; } / 若果不是,则再找父级容器 / selectElement = selectElement.parentNode; } / innerText、innerHTML、value * innerText 是指html标签里的文字信息,单纯的文本,不会有html标签,存在兼容性 * innerHTML 是指包含在html标签里的所有子元素,包括空格、html标签 * value 表单里的元素属性值 * / if(selectElement) { promptText.innerHTML = e.target.innerHTML; } });})()在已有的Dom节点上对Dom绑定事件,我这里的宽度是固定死的,相对来说不是很友好3.js自动生成进行美化html文件<div id=“select” class=“select”></div><script src=“autoGenerateSelect.js”></script><script> (function() { / 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了 * 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash / document.addEventListener(‘DOMContentLoaded’,function(){ new $Selector({ elementSelector:’#select’, options:[ {name:‘选项1’,value:‘0’}, {name:‘选项2’,value:‘1’}, {name:‘选项3’,value:‘2’} ], defaultText:‘选项2’ }); }) })()</script>css文件html, body, ul{ margin: 0; padding: 0;}ul{ list-style: none;}#select{ padding: 30px 40px 0;}/ 下拉框 /.dropDown{ position: relative; display: inline-block; min-width: 120px; box-sizing: border-box; color: #515a6e; font-size: 14px;}/ 已选中的值包含层 /.selectedOption{ position: relative; box-sizing: border-box; outline: none; user-select: none; cursor: pointer; background: #fff; border-radius: 4px; border: 1px solid #dcdee2; transition: all .2s ease-in-out;}.selectedValue{ display: block; overflow: hidden; height: 28px; line-height: 28px; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; padding-left: 8px; padding-right: 24px;}/ 图标 /.arrowDown{ position: absolute; display: inline-block; top: 50%; right: 8px; margin-top: -7px; font-size: 14px; color: #808695; transition: all .2s ease-in-out; / 字体抗锯齿渲染 / -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.arrowDown:before{ content: “”; display: block; width: 6px; height: 6px; background-color: transparent; border-left: 2px solid #808695; border-bottom: 2px solid #808695; transform: rotate(-45deg);}/ 所有选项的包含层 /.optionsContainer{ position: absolute; top: 30px; left: 0; min-width: 120px; max-height: 200px; margin: 5px 0; padding: 5px 0; background: #fff; box-sizing: border-box; border-radius: 4px; box-shadow: 0 1px 6px rgba(0, 0, 0, .2); z-index: 2; transform-origin: center top 0px; transition: all 0.3s; will-change: top, left; transform: scale(1, 0); opacity: 0;}/ 每个选项 /.optionsItem{ line-height: normal; padding: 7px 16px; color: #515a6e; font-size: 12px; white-space: nowrap; cursor: pointer; transition: background .2s ease-in-out;}.itemSelected, .optionsItem:hover{ color: #2d8cf0; background-color: #f3f3f3;}对下拉框初始化/ 私有方法:初始化下拉框 /_initSelector({ / 传入id,class,tag,用于挂载下拉框 / elementSelector = ‘’, / 传入的下拉框选项 / options = [{ name: ‘请选择你喜欢的颜色’, value: ‘0’ }], defaultText = ‘请选择你喜欢的颜色’}) { / 找到要挂载的Dom节点 / this.parentElement = document.querySelector(elementSelector) || document.body; this.options = options; this.defaultText = defaultText; / 下拉框的显示与隐藏状态 / this.downStatus = false; / 下拉框默认选中的值 / this.defaultValue = ‘’; this._createElement();},创建元素节点 / 创建Dom节点 /_createElement() { / 创建下拉框最外层 / let dropDown = document.createElement(‘div’); dropDown.className = ‘dropDown’; / 已选中的选项值 / let selectedOption = document.createElement(‘div’); selectedOption.className = ‘selectedOption’; / 选中的值 / let selectedValue = document.createElement(‘span’); selectedValue.className = ‘selectedValue’; / 先赋值为默认值 / selectedValue.innerText = this.defaultText; / 向下的图标 / let downIcon = document.createElement(‘i’); downIcon.className = ‘arrowDown’; / 将已选中的值的层添加到Dom节点中 / selectedOption.appendChild(selectedValue); selectedOption.appendChild(downIcon); / 创建选项的外层容器 / let optionsContainer = document.createElement(‘div’); optionsContainer.className = ‘optionsContainer’; / 用ul来包含选项层 / let ulOptionsList = document.createElement(‘ul’); ulOptionsList.className = ‘ulOptionsList’; / 循环创建每个选项 / this.options.forEach((item) => { let optionsItem = document.createElement(’li’); / 是否是选中状态 / if(item.name == this.defaultText) { optionsItem.className = ‘optionsItem itemSelected’; } else { optionsItem.className = ‘optionsItem’; } optionsItem.innerText = item.name; ulOptionsList.appendChild(optionsItem); }); / 添加到每个对应的元素里面 / optionsContainer.appendChild(ulOptionsList); dropDown.appendChild(selectedOption); dropDown.appendChild(optionsContainer); this.parentElement.appendChild(dropDown); / 设置Dom元素,挂载、绑定事件 / / 已选中的选项的包含层 / this.selectedOption = selectedOption; / 选中的值 / this.selectedValue = selectedValue; / 下拉框选项包含层 / this.optionsContainer = optionsContainer; this._handleShowOptions(this.parentElement); this._unifyWidth(selectedOption);},显示与隐藏相关事件/ 显示与隐藏事件 /_handleShowOptions(element) { element.addEventListener(‘click’, (e) => { let clickNode = e.target; this._unifyWidth(this.selectedOption); / 点击的是否是下拉框 / if(this._isOptionNode(clickNode, this.selectedOption)) { if(this.downStatus) { this._hiddenDropDown(); } else { this._showDropDown(); } } else if(clickNode.className == ‘optionsItem’) { this._handleSelected(clickNode); } else { this._hiddenDropDown(); } })},/ 判断是否是下拉框选项 /_isOptionNode(clickNode, target) { if (!clickNode || clickNode === document) return false; return clickNode === target ? true : this._isOptionNode(clickNode.parentNode, target);},/ 显示下拉框选项 /_showDropDown() { this.optionsContainer.style.transform = ‘scale(1, 1)’; this.optionsContainer.style.opacity = ‘1’; this.selectedOption.className = ‘selectedOption’; this.downStatus = true;},/ 隐藏下拉框选项 /_hiddenDropDown() { this.optionsContainer.style.transform = ‘scale(1, 0)’; this.optionsContainer.style.opacity = ‘0’; this.selectedOption.className = ‘selectedOption’; this.downStatus = false;},定义点击事件 / 对每个选项的点击事件 /_handleSelected(clickNode) { this.selectedValue.innerText = clickNode.innerText; clickNode.className = ‘optionsItem itemSelected’; this._siblingsDom(clickNode, function(clickNode) { if(clickNode) { clickNode.className = ‘optionsItem’; } }); this._hiddenDropDown();},/ 兄弟节点处理函数 /_siblingsDom(clickNode, callback) { / arguments 是一个对应于传递给函数的参数的类数组对象 * arguments对象是所有(非箭头)函数中都可用的局部变量 * 包含传递给函数的每个参数,第一个参数在索引0处 * arguments对象不是一个 Array,它类似于Array, * 但除了length属性和索引元素之外没有任何Array属性 * / (function (ele) { / arguments.callee * 指向当前执行的函数 * / callback(ele); if (ele && ele.previousSibling) { arguments.callee(ele.previousSibling); } })(clickNode.previousSibling); (function (ele) { callback(ele); if (ele && ele.nextSibling) { arguments.callee(ele.nextSibling); } })(clickNode.nextSibling);},判断宽度/ 判断宽度 /_unifyWidth(selectedOption) { / 找到所有的li标签 / let optionsItem = document.querySelectorAll(’.optionsItem’); let standardWidth = selectedOption.offsetWidth; / 对每个li标签设置宽度 / optionsItem.forEach((item) => { standardWidth = item.offsetWidth > standardWidth ? item.offsetWidth : standardWidth; item.style.width = standardWidth - 32 + ‘px’; selectedOption.style.width = standardWidth + ‘px’; });}(function() { / 定义selector下拉框 / let Selector = function(params) { / 初始化 / this._initSelector(params); }; Selector.prototype = { / 将上面的方法全部放在Selector原型上 / }; / 挂载到window上/ window.$Selector = Selector;})();关于原型与原型链,可以查看我记录的js面试使用js生成的话,相对来说,代码长点,我这里的话,对宽度有做判断,但是不完美关于DOMContentLoaded可以查看这篇文章DOMContentLoaded正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现单行及多行文字超出后加省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

December 10, 2018 · 5 min · jiezi

PHP socket初探 --- select系统调用

[原文地址:https://blog.ti-node.com/blog…]在<PHP socket初探 — 先从一个简单的socket服务器开始>中依次讲解了三个逐渐进步的服务器:只能服务于一个客户端的服务器利用fork可以服务于多个客户端的额服务器利用预fork派生进程服务于多个客户端的服务器最后一种服务器的进程模型基本上的大概原理其实跟我们常用的apache是非常相似的.其实这种模型最大的问题在于需要根据实际业务预估进程数量,依旧是需要大量进程来解决问题,可能会出现CPU浪费在进程间切换上,还有可能会出现惊群现象(简单理解就是100个进程在等带客户端连接,来了一个客户端但是所有进程都被唤醒了,但最终只有一个进程为这个客户端服务,其余99个白白折腾),那么,有没有一种解决方案可以使得少量进程服务于多个客户端呢?答案就是在<PHP socket初探 — 关于IO的一些枯燥理论>中提到的"IO多路复用".多路是指多个客户端连接socket,复用就是指复用少数几个进程,多路复用本身依然隶属于同步通信方式,只是表现出的结果看起来像异步,这点值得注意.目前多路复用有三种常用的方案,依次是:select,最早的解决方案poll,算是select的升级版epoll,目前的最终解决版,解决c10k问题的功臣今天说的是select,这个东西本身是个Linux系统调用.在Linux中一切皆为文件,socket也不例外,每当Linux打开一个文件系统都会返回一个对应该文件的标记叫做文件描述符.文件描述符是一个非负整数,当文件描述数达到最大的时候,会重新回到小数重新开始(题外话:按照传统,一般情况下标准输入是0,标准输出是1,标准错误是2).对文件的读写操作就是利用对文件描述符的读写操作.一个进程可以操作的文件描述符的数量是有限制的,不同系统有不同的数量,在linux中,可以通过调整ulimit来调整控制.先通过一个简单的例子说明下select的作用和功能.双11到了,你给少林足球队买了很多很多球鞋,分别有10个快递给你运送,然后你就不断地电话询问这10个快递员,你觉得有点儿累.阿梅很心疼你,于是阿梅就说:“这事儿你不用管了,你去专心练大力金刚腿吧,等任何一个快递到了,我告诉你”.当其中一个快递来了后,阿梅就喊你:"下来啦,有快递!",但是,这个阿梅比较缺心眼,她不告诉你是具体哪双鞋子的快递,只告诉你有快递到了.所以,你只能依次查询一遍所有快递单的状态才能确认是哪个签收了.上面这个例子通过结合术语演绎一遍就是,你就是服务器软件,阿梅就是select,10个快递就是10个客户端(也就是10个连接socket fd).阿梅负责替你管理着这10个连接socket fd,当其中任何一个fd有反应了也就是可以读数据或可以发送数据了,阿梅(select)就会告诉你有可以读写的fd了,但是阿梅(select)不会告诉你是哪个fd可读写,所以你必须轮循所有fd来看看是哪个fd,是可读还是可写.是时候机械记忆一波儿了:当你启动select后,需要将三组不同的socket fd加入到作为select的参数,传统意义上这种fd的集合就叫做fd_set,三组fd_set依次是可读集合,可写集合,异常集合.三组fd_set由系统内核来维护,每当select监控管理的三个fd_set中有可读或者可写或者异常出现的时候,就会通知调用方.调用方调用select后,调用方就会被select阻塞,等待可读可写等事件的发生.一旦有了可读可写或者异常发生,需要将三个fd_set从内核态全部copy到用户态中,然后调用方通过轮询的方式遍历所有fd,从中取出可读可写或者异常的fd并作出相应操作.如果某次调用方没有理会某个可操作的fd,那么下一次其余fd可操作时,也会再次将上次调用方未处理的fd继续返回给调用方,也就是说去遍历fd的时候,未理会的fd依然是可读可写等状态,一直到调用方理会.上面都是我个人的理解和汇总,有错误可以指出,希望不会误人子弟.下面通过php代码实例来操作一波儿select系统调用.在php中,你可以通过stream_select或者socket_select来操作select系统调用,下面演示socket_select进行代码演示:<?php// BEGIN 创建一个tcp socket服务器$host = ‘0.0.0.0’;$port = 9999;$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen_socket, $host, $port );socket_listen( $listen_socket );// END 创建服务器完毕 // 也将监听socket放入到read fd set中去,因为select也要监听listen_socket上发生事件$client = [ $listen_socket ];// 先暂时只引入读事件,避免有同学晕头$write = [];$exp = [];// 开始进入循环while( true ){ $read = $client; // 当select监听到了fd变化,注意第四个参数为null // 如果写成大于0的整数那么表示将在规定时间内超时 // 如果写成等于0的整数那么表示不断调用select,执行后立马返回,然后继续 // 如果写成null,那么表示select会阻塞一直到监听发生变化 if( socket_select( $read, $write, $exp, null ) > 0 ){ // 判断listen_socket有没有发生变化,如果有就是有客户端发生连接操作了 if( in_array( $listen_socket, $read ) ){ // 将客户端socket加入到client数组中 $client_socket = socket_accept( $listen_socket ); $client[] = $client_socket; // 然后将listen_socket从read中去除掉 $key = array_search( $listen_socket, $read ); unset( $read[ $key ] ); } // 查看去除listen_socket中是否还有client_socket if( count( $read ) > 0 ){ $msg = ‘hello world’; foreach( $read as $socket_item ){ // 从可读取的fd中读取出来数据内容,然后发送给其他客户端 $content = socket_read( $socket_item, 2048 ); // 循环client数组,将内容发送给其余所有客户端 foreach( $client as $client_socket ){ // 因为client数组中包含了 listen_socket 以及当前发送者自己socket,所以需要排除二者 if( $client_socket != $listen_socket && $client_socket != $socket_item ){ socket_write( $client_socket, $content, strlen( $content ) ); } } } } } // 当select没有监听到可操作fd的时候,直接continue进入下一次循环 else { continue; } }将文件保存为server.php,然后执行php server.php运行服务,同时再打开三个终端,执行telnet 127.0.0.1 9999,然后在任何一个telnet终端中输入"I am DOG!",再看其他两个telnet窗口,是不是感觉很屌?不完全截图图下:还没意识到问题吗?如果我们看到有三个telnet客户端连接服务器并且可以彼此之间发送消息,但是我们只用了一个进程就可以服务三个客户端,如果你愿意,可以开更多的telnet,但是服务器只需要一个进程就可以搞定,这就是IO多路复用diao的地方!最后,我们重点解析一些socket_select函数,我们看下这个函数的原型:int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )值得注意的是$read,$write,$except三个参数前面都有一个&,也就是说这三个参数是引用类型的,是可以被改写内容的.在上面代码案例中,服务器代码第一次执行的时候,我们要把需要监听的所有fd全部放到了read数组中,然而在当系统经历了select后,这个数组的内容就会发生改变,由原来的全部read fds变成了只包含可读的read fds,这也就是为什么声明了一个client数组,然后又声明了一个read数组,然后read = client.如果我们直接将client当作socket_select的参数,那么client数组内容就被修改.假如有5个用户保存在client数组中,只有1个可读,在经过socket_select后client中就只剩下那个可读的fd了,其余4个客户端将会丢失,此时客户端的表现就是连接莫名其妙发生丢失了.[原文地址:https://blog.ti-node.com/blog…] ...

September 2, 2018 · 1 min · jiezi