枚举和注解 枚举 基础知识 枚举是一组常量的集合。枚举属于一种特殊的类,里面只包含一组有限的特定的对象。 其实枚举类是可以通过传统写法自定义的,写法为: 构造器私有化 不提供set方法 在类内部预先初始化好静态的实例,并且对外暴露 代码略,直接学习如何创建真正的枚举。 使用enum关键字来代替class 直接写FALL(“秋天”,“凉爽”),效果上等价于 public static final Season FALL = new Season(“秋天”,“凉爽”); 如果有多个常量对象,使用逗号间隔即可 使用 enum 实现枚举,必须把定义的常量对象写在枚举类的最前面 使用无参构造器时,可以把括号也省略,直接写FALL,举例 FALL, SPRING, SUMMER, WINTER; public class Enum { public static void main(String[] args) { System.out.println(Season.SPRING); } } enum Season{ SPRING("春天","温暖"), SUMMER("夏天","炎热"), FALL("秋天","凉爽"), WINTER("冬天","寒冷"); private String name; private String desc; Season(String name, String desc) { this.name = name; this.desc = desc; } @Override public String toString() { return "Season{" + "name='" + name + '\'' + ", desc='" + desc + '\'' + '}'; } } .java文件可以用 Javac 编译成.class文件,.class文件也可以用 Javap 反编译成字节码文件,通过观察先编译再反编译的结果,可以看到很多隐藏的细节。 对于 Seanon 类,反编译得到的代码如下 Compiled from "Enum.java" final class hspedu.inner.enumer.Season extends java.lang.Enum<hspedu.inner.enumer.Season> { public static final hspedu.inner.enumer.Season SPRING; public static final hspedu.inner.enumer.Season SUMMER; public static final hspedu.inner.enumer.Season FALL; public static final hspedu.inner.enumer.Season WINTER; public static hspedu.inner.enumer.Season[] values(); public static hspedu.inner.enumer.Season valueOf(java.lang.String); public java.lang.String toString(); static {}; } 值得关注的细节有: 枚举类是 final 类型的,因此不可被继承 枚举类默认继承 java.lang.Enum 类,因此不可继承其他类 每一个常量都默认是 public static final 类型的 简单练习 第一题 判断语法正误 enum Gender { BOY, GIRL; } 答案: 语法没有错,相当于一个“没有属性,只含有无参构造器”的枚举类 第二题 判断输出什么 enum Gender2 { BOY, GIRL; } Gender2 boy = Gender2.BOY; Gender2 boy2 = Gender2.BOY; System.out.println(boy); System.out.println(boy2 == boy); 答案: BOY true 分析: 首先,枚举类本质也是类,所以Gender2 boy = Gender2.BOY;这种写法肯定是对的 boy相当于拿到了枚举类里的public static final常量,因此boy和boy2肯定是一样的 System.out.println(boy)相当于调用Gender2的toString方法,但是显然它没有,就去找父类的toString方法,父类是java.lang.Enum(前面有提到) Enum类的成员方法 分别是 name、ordinary、values、valueOf、compareTo方法,建议自己写一写用一用,方法的效果都写在代码里了: public class Enum { public static void main(String[] args) { Season spring = Season.SPRING; // name方法,建议优先使用toString,效果类似 System.out.println(spring.name()); System.out.println(spring); // ordinary方法,输出该枚举对象的序号,从0开始 System.out.println(spring.ordinal()); // values方法,返回所有的枚举对象 Season[] values = Season.values(); for (Season value : values) { System.out.println(value); } // valueOf方法,返回指定名称的枚举对象 Season valueOf = Season.valueOf("FALL"); System.out.println(valueOf); // compareTo方法,比较两个枚举对象,返回它们的序号之差,在这里是spring的序号 - valueOf的序号 System.out.println(spring.compareTo(valueOf)); } } enum Season{ SPRING("春天","温暖"), SUMMER("夏天","炎热"), FALL("秋天","凉爽"), WINTER("冬天","寒冷"); private String name; private String desc; Season(String name, String desc) { this.name = name; this.desc = desc; } } 简单练习2 声明 Week 枚举类,其中包含星期一至星期日的定义; MONDAY, TUESDAY, WEDNESDAY, THURSDAY,FRIDAY, SATURDAY, SUNDAY; 使用 values 返回所有的枚举数组,并遍历,要求打印值为“星期一”而不是“MONDAY” public class Enum { public static void main(String[] args) { Week[] values = Week.values(); for (Week value : values) { System.out.println(value); } } } enum Week{ MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日"); private String name; Week(String name) { this.name = name; } @Override public String toString() { return name; } Enum类的接口 Enum类本身已经有了继承关系,因此不能继承其他类 但作为一个类,它仍然可以实现接口 interface Playing { void play(); } enum Music implements Playing { HARD_ROCK, POP, CLASSIC, ROCK, JAZZ; @Override public void play() { } } 注解 最基本的修饰符 最基本的三个修饰符分别是: Override:用来限定某个方法必须重写父类的方法,只能用于方法 SuppressWarnings:抑制编译器的警告 Deprecated:用来表示某个程序元素(比如类或者方法)已经过时 Override 其实对于正确的方法重写来说,加不加这个修饰符都可以。 但如果加了的话,编译器会检查你是否有正确地重写这个方法。如果不正确的话会报错,产生编译错误。 Override的源码如下,从 @Target (ElementType.METHOD) 上可以看出,这个修饰符只能用在方法上。 @interface 修饰的类都是注解类。 @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } 顺带一提, @Target 是修饰注解的注解,也称为元注解。 Deprecated 用 @Deprecated 修饰符修饰的元素,暗示其已经过时了,不推荐再继续使用,但其实可以使用。 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}) public @interface Deprecated { String since() default ""; boolean forRemoval() default false; } 从源码可以看出,该修饰符可以修饰方法、字段、包、参数等。 该关键字一般用于 JDK 版本更迭时,给过时的方法打上标注。 SuppressWarnings 用来抑制编译器警告,在 ( ) 中可以填抑制的警告类型。 因为懒得打字,我直接把文档粘在这: 参数名称 作用描述 all 抑制所有警告。 unchecked 抑制与泛型相关的“未经检查的操作”警告,例如在使用原始类型时。 deprecation 抑制使用了 @Deprecated 标记的过时类或方法的警告。 rawtypes 抑制使用了泛型但未指定具体类型的“原始类型”警告。 unused 抑制代码中存在但未被使用的变量、方法或类的警告。 serial 抑制可序列化的类未定义 serialVersionUID 的警告。 null 抑制与空值分析相关的警告(如潜在的空指针)。 cast 抑制与强制类型转换操作相关的警告。 fallthrough 抑制在 switch 语句中缺少 break 而导致“直通”的警告。 finally 抑制 finally 块无法正常返回的警告。 boxing 抑制与自动装箱(boxing)和拆箱(unboxing)操作相关的警告。 static-access 抑制不正确的静态成员访问方式的警告。 dep-ann 抑制使用过时注解的警告。 incomplete-switch 抑制 switch 语句中未覆盖所有枚举常量的警告。 javadoc 抑制与 Javadoc 相关的警告。 synthetic-access 抑制内部类访问未优化的警告。 resource 抑制与资源(如 Closeable )使用相关的警告。 restriction 抑制使用了不建议或禁止引用的警告。 使用示例,代码如下: @SuppressWarnings({"all"}) enum Music implements Playing { HARD_ROCK, POP, CLASSIC, ROCK, JAZZ; @Override public void play() { } } @SuppressWarnings是没有对使用位置限制的,从源码中也可以看出,它没有 @Target 去限制,源码如下: @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } 元注解 注解的注解,看源码时可能遇到,没那么重要,快速过一下。 四种元注解: Retention:指定注解的作用范围,三种值SOURCE,CLASS,RUNTIME Target:指定注解可以在哪些地方使用 Documented:指定该注解是否会在javadoc体现,即在生成文档的时候,可以看到该注解 Inherited:子类会继承父类注解 这部分我战略性跳过了,稍微不太好理解,也有点深入了。 1 个帖子 - 1 位参与者 阅读完整话题
最近回到了学校,忙完了毕业的事,就一直在兜兜转转地学基础知识。之前在家待的都长毛了。突然忙起来,感觉也不错。 最近感觉自己在生活习惯上和心态上又有了一些转变,所以打算写点什么来分享一下。 想分享一下自己总结的的几个习惯。 1.输出和输入同等重要 很多人在学习时都容易犯一个错误,就是过度的追求输入,而很少输出。比如学习课程时,经常有弹幕刷“我今天学了三十课。”但你学了三十课,有没有积累三十课的笔记呢?有没有去进行三十课的实践呢?实际上二者同等重要,甚至可以说输出比输入还重要,因为输出可以倒逼输入,而长时间的输入容易给自己制造已经学会了的假象。 2.学习/输入、思考/实践/内化、输出 只有输入和输出还不够,中间的思考实践内化这一步可以防止你变成“把学到的知识逐字背诵”,而是真正带着一点理解(哪怕很少),去把知识真情实感地表达出来。 对于学技术,我建议就直接敲敲代码去实践。 有一些人生道理,不容易在生活中随时都能实践,就可以多多思考消化。 3.大量分享 把自己的想法写出来,只是输出。那既然都已经输出了,不如找合适的论坛或者社区,把自己的想法分享出来,和大家讨论,能获得一些正反馈。 2 个帖子 - 2 位参与者 阅读完整话题
我的其他笔记可以查看 JAVA学习记录总贴 内部类 基础知识 内部类的类的第五大成员。 五大成员分别是:属性、方法、构造器、代码块、内部类。 定义:一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为“内部类”,在外面的类称为“外部类”,内部类最大的特点就是可以访问私有属性,并且可以体现类与类之间的包含关系。 语法 class Outer{//外部类 class Inter{//内部类 } } class OtherOuter{//外部其他类 } PS:内部类是OOP的重难点,底层源码有大量的内部类,必须要要下来这一块。 分类 定义在外部类局部位置(比如方法内) 局部内部类(有类名) 匿名内部类(无类名,重点!!!!!!!) 定义在外部类的成员位置上 成员内部类(没用static) 静态内部类(使用static) class OuterClass { //成员内部类 class MemberInnerClass {} //静态内部类 static class StaticInnerClass{} //外部方法 public void outerMethod() { //局部内部类 class LocalInnerClass {} //匿名内部类 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("匿名内部类"); } }; runnable.run(); } } 局部内部类 细节 局部内部类定义在外部类的局部位置,比如在方法中,并且有类名。(代码块中也行,但罕见) 可以直接访问外部类的所有成员,包含私有的 不能添加访问修饰符,因为局部内部类本质上就是一个局部变量,局部变量不能使用修饰符,同理它也不能,但它可以使用final,因为局部变量也可以用final,顺带一提abstract也可以。 局部内部类是可以被继承的。 作用域:仅仅作用在定义它的方法或代码块中 局部内部类想访问外部类的成员,直接访问即可 外部类想访问局部内部类的成员,可以在作用域内实例化局部内部类,但是注意,必须在定义它之后再new 外部其他类不可能访问局部内部类,这个挺好理解,因为局部内部类本质局部变量,不在它作用域内。 如果外部类和局部内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名 ,ps:顺带一提,如果你不嫌脱裤子放屁,其实 外部类类名.this.成员名 这种语法,可以在类的所有地方精确调用到本类成员 再顺带一提,其实所有内部类都可以在内部继续写内部类,这也是它复杂的原因 class OuterClass { private int n1 = 10; private void m2() {} public void m1() { class InnerClass { public void show() { // 可以直接访问外部类的所有成员,包括私有类型,包括方法 System.out.println("n1 = " + n1); m2(); } } class A extends InnerClass{ } InnerClass innerClass = new InnerClass(); } } 建议对着上面的代码,每一条细节都去自己实践一遍,看看违反了会报什么错。 匿名内部类 重点中的重点,这部分所有代码建议自己真的看完书去手敲一下,不要指望看一遍能懂,更不要单纯相信遇到的时候让AI来解释就好,没那么轻松。 特点 本质是类,底层会有独立的class字节码文件 属于内部类,定义在外部类/代码块,这类局部位置中 没有类名,其实底层是有的,但是程序员不关心,因而匿名 同时也是一个对象,类定义好的同时就已经被创建了 基本语法 new 父类/接口名(构造参数列表){ // 类体:重写父类/实现接口的抽象方法,也可以新增自定义成员 }; 基于接口的匿名内部类 class Outer04 { private int n1 = 10; public void method() { // 基于接口的匿名内部类 // 1. 需求:使用IA接口,并创建对象 // 2. 传统方法是写一个类,实现接口,创建对象,并调用方法 // 3. 但如果我们的需求是,这个类只用一次,那么定义出来就有些浪费了,所以我们可以使用匿名内部类来简化开发 // inner的编译类型是IA,而inner的运行类型是匿名内部类 /*其实这里的底层含义是 class Outer04$1 implements IA { @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } } */ // 4. jdk底层创建了匿名内部类Outer04$1,然后创建了实例,并且把地址返回给inner // 5. 匿名内部类只能使用一次,不能再次使用 IA inner = new IA(){ @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } }; inner.cry(); System.out.println(inner.getClass()); IA inner1 = new IA(){ @Override public void cry() { System.out.println("匿名内部类实现了cry方法"); } }; System.out.println(inner1.getClass()); } } interface IA { public void cry(); } 值得说的细节在注释里都已经写明了,我在此补充一点 Outer04$1 就是 JVM 自动为第一个匿名内部类分配的名字,数字表示该类在外部类中出现的顺序。 基于类的匿名内部类 class Outer04 { private int n1 = 10; public void method() { Father fa = new Father("张三"); Father fa1 = new Father("张三"){}; // 证明匿名类创建的不是Father类,而是匿名内部类 System.out.println(fa.getClass()); System.out.println(fa1.getClass()); Father fa2 = new Father("张三"){ @Override public void show() { System.out.println("匿名内部类重写show()"); } }; fa2.show(); Animal animal = new Animal() { @Override public void eat() { System.out.println("匿名内部类重写eat()"); } }; animal.eat(); } } class Father { private String name; public Father(String name) { this.name = name; } public void show() { System.out.println("show()"); } } abstract class Animal{ public abstract void eat(); } 简单总结一些要点 匿名内部类可以重写方法 父类的构造器如果有参数,则你也要提供对应参数(如果有多个构造器,那你就提供符合其中一个的就行) 基于抽象类(接口)的匿名内部类,必须实现其中所有抽象方法 一些细节 匿名内部类,既有定义一个类的特性,也有创建对象的特性,是一个凉面派 可以直接访问外部类的所有成员,包括外部私有成员 不能添加访问修饰符(直觉看上去也没办法提交orz),因为它本质是局部变量 作用域仅在定义它的方法or代码块中 外部其他类也不能访问匿名内部类,因为其本质局部变量 如果外部类和匿名内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名 匿名内部类练习 题目1:写一个基于接口的匿名内部类,并把它作为方法参数传入 public class AnonymousClass { public static void main(String[] args) { // 方式1:直接在参数位置编写匿名内部类 f1(new AA() { @Override public void show() { System.out.println("匿名类实现接口"); } }); // 方式2:先创建匿名内部类对象,再传入参数 AA aa = new AA() { @Override public void show() { System.out.println("匿名类实现接口"); } }; f1(aa); } public static void f1(AA aa){ aa.show(); } } interface AA{ void show(); } 题目2: 定义一个铃声接口 Bell,接口中包含 ring() 方法。 定义一个手机类 Cellphone,类中包含闹钟功能方法 alarmclock(Bell bell),方法的参数类型为 Bell。 测试手机类的闹钟功能:通过匿名内部类创建 Bell 接口的实现对象,作为参数传入 alarmclock 方法,调用 ring() 方法时打印:懒猪起床了。 再传入另一个匿名内部类对象,调用 ring() 方法时打印:小伙伴上课了。 public class AnonymousClass { public static void main(String[] args) { new Cellphone().alarmclock(new Bell() { @Override public void ring() { System.out.println("懒猪起床了"); } }); new Cellphone().alarmclock(new Bell() { @Override public void ring() { System.out.println("小伙伴上课了"); } }); } } interface Bell { void ring(); } class Cellphone { public void alarmclock(Bell bell){ bell.ring(); } } 重点:一定要写一写感受一下,这部分涉及:多态、继承、动态绑定、内部类,多个知识点混杂在一起,需要认真练习 成员内部类 成员内部类定义在外部类的成员位置,并且没有static修饰 可以访问外部类的所有成员 可以添加任意的修饰符,因为其地位相当于一个成员 成员内部类的作用域和外部类的其他成员一样,都是整个类体。 成员内部类可以调用外部类的所有成员,包括私有。 外部类可以访问成员内部类的所有成员,包括私有成员,不过,必须要创建实例才能访问。 外部其他类想要访问成员内部类,有两种方法,分别标注在代码里了 如果外部类和内部类成员同名,则内部类访问时采取就近原则,如果一定要访问外部类的同名成员,则采用 外部类.this.成员名 的方式 public class AnonymousClass { public static void main(String[] args) { // 外部其他类访问成员内部类的两种方式 // 方式一 通过外部类对象访问成员内部类对象,下面的两种写法本质上是等价的 Outer.Inner inner = new Outer().new Inner(); // 写法1 Outer outer = new Outer(); // 写法2 Outer.Inner inner1 = outer.new Inner(); // 方式二 在外部类中编写一个写法,返回成员内部类对象 Outer.Inner inner2 = outer.getInner(); } } class Outer{ private int n1 = 10; private String name = "张三"; public Inner getInner(){ return new Inner(); } // 成员内部类 // 成员内部类是定义在外部类的成员位置上 public class Inner{ private int n2 = 20; public void say(){ System.out.println("n1 = " + n1 + " name = " + name); } } public void show(){ Inner inner = new Inner(); // 即使是类内部的私有成员也可以被访问,因为本质上它也是类的一部分。 System.out.println(inner.n2); } } 静态内部类 静态内部类定义在外部类的成员位置,并且有 static 修饰符。 静态内部类可以直接访问外部类的所有静态成员(包括私有的),但是不能直接访问非静态成员;可以访问本类的所有成员,不管是静态还是非静态 解释: 静态内部类是外部类的一部分,因而可以访问外部类的所有静态成员。 对于外部类的非静态成员,可以在静态内部类中实例化,然后访问。 可以添加任意访问修饰符,因为它的地位就是一个成员 作用域和其他的成员一样,是整个类体内部。 静态内部类可以在不依赖外部类的前提下被实例化 外部其他类访问静态内部类、静态内部类访问外部类、外部类访问静态内部类的逻辑都写在了代码里,如下所示 外部类和静态内部类成员重名时,静态内部类如果想要访问,则默认就近原则,如果想要访问外部类的同名成员,则需要 外部类名.成员 P.S. 内部类可以是静态的,但是顶层类不能是静态的。 public class AnonymousClass { public static void main(String[] args) { // 展示外部其它类访问静态内部类的方式 // 方式1,直接new Outer.Inner inner = new Outer.Inner(); inner.sayHello(); // 方式2,编写一个方法,返回静态内部类的实例。 Outer.Inner inner1 = new Outer().getInnerInstance("java"); inner1.sayHello(); // 方式2的补充,可以在外部类中写静态方法,返回静态内部类的实例,没有本质区别 Outer.Inner inner2 = Outer.getInnerInstance_("HSP"); inner2.sayHello(); } } class Outer { private int n1 = 10; private static int n2 = 20; // 静态内部类 public static class Inner { private String name; private static int count = 0; private static int n2 = 30; // 静态内部类可以有构造器 Inner(String name) { this.name = name; } Inner() { } // 静态类里可以有普通方法 public void sayHello() { // 展示静态内部类访问外部类的方式。 // System.out.println(n1); 不能直接访问外部类非静态成员变量 System.out.println(new Outer().n1); // 但是可以通过创建外部类实例来访问外部类非静态变量 System.out.println(Outer.n2); // 可以直接访问外部类静态变量 System.out.println(n2); // 直接访问遵循就近原则 System.out.println("Hello, I'm " + this.name); // 也可以访问本类非静态变量 System.out.println("Total count: " + count); // 也可以访问本类静态变量 } } // 接下来展示外部类如何访问静态内部类 public void accessInner() { Inner inner = new Inner("HSP"); // 创建静态内部类对象 System.out.println(inner.name); // 访问静态内部类成员变量 System.out.println(Inner.count); // 访问静态内部类静态成员变量 } public Inner getInnerInstance(String name) { return new Inner(name); // 创建静态内部类对象并返回给调用方 } public static Inner getInnerInstance_(String name) { return new Inner(name); // 创建静态内部类对象并返回给调用方 } } 综合练习 练习1 当前代码会不会报错?为什么? public class Test { class Inner { public int a = 5; } public static void main(String[] args) { Inner r = new Inner(); } } 答案: 会报错,因为成员内部类在创建时依赖外部类的实例而存在,需要一个外部类作为容器,传统初始化方法为 “外部类实例.new 成员内部类名” main方法是静态的,静态方法中没有this关键字,因而报错,下面这样做就可以 public class Outer { public class Inner { } // 实例方法(非静态方法) public void show() { Inner inner = new Inner(); // ✅ 可以! // 等价于:this.new Inner(); // 因为实例方法中,this 指向当前 Outer 对象 } } 练习2 下面这段代码会输出什么? public class Test { public Test() { Inner s1 = new Inner(); s1.a = 10; Inner s2 = new Inner(); System.out.println(s2.a); } class Inner { public int a = 5; } public static void main(String[] args) { Test t = new Test(); Inner r = t.new Inner(); System.out.println(r.a); } } 答案: 5 5 分析: Test构造器里初始化的 S1 和 S2 分别是两个不同的成员内部类实例,而 main 方法里初始化的 r 也是一个不同的成员内部类实例。三个方法中的 a 是独立的 1 个帖子 - 1 位参与者 阅读完整话题
前情提要: 小白学Epoll网络编程1 基础概念的理解 基础知识-Epoll 最近在学习Linux网络编程,目前进展到和epoll相关的部分了 ,我的学习路线是 第一阶段:能写阻塞式 socket 程序 目标:写出最普通的 TCP echo server / client。 第二阶段:理解 TCP 是“字节流”,不是“消息流” 目标:写一个带协议的服务,例如:4 字节长度 + body 当前阶段 :进入 non-blocking + epoll 写一个单线程并发 TCP server。 第四阶段:简单的 Reactor 网络库 按 muduo 的概念拆 实战 封装函数 我把创建一个listenFD放到了一个方法里,这样调起来比较方便 #include "sys/socket.h" #include <netinet/in.h> #include <arpa/inet.h> #include "unistd.h" #include <iostream> #include <fcntl.h> int listenFD(); // 设置fd为非阻塞 int set_nonblocking(int fd); 实现 #include "common.h" int listenFD() { // 进入准备连接的状态 int listenFD = socket(AF_INET, SOCK_STREAM, 0); if (listenFD == -1) { std::cerr << std::system_category().message(errno) << std::endl; return -1; } sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(8080); int ret = inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); if (ret == 0) { std::cerr << "src does not contain a character string\ representing a valid network address in the specified address family "; return -1; } else if (ret == -1) { std::cerr << "af does not contain a valid address family"; return -1; } ret = bind(listenFD, (sockaddr *)&addr, sizeof(addr)); if (ret == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listenFD); return -1; } ret = listen(listenFD, 3); if (ret == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listenFD); return -1; } return listenFD; } int set_nonblocking(int fd) { // 1. 获取原来的 file status flags int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } // 2. 在原有的 flags 基础上,按位或(|)加上 O_NONBLOCK 标志 flags |= O_NONBLOCK; // 3. 将新的 flags 设置回去 if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } 先搭骨架 写好主线程的函数void worker_main()和void worker() worker_main 在上一部分里我们讲到,worker的任务是 等待到来的连接 accept他们 把读数据的ReadTask放到全局队列里 开一个监听fd 既然是“等待到来的连接”,肯定需要一个监听fd,所以在worker_main进入循环之前,我们要新建一个listen fd int epfd = -1; void worker(){ int listener = listenFD(); // 错误处理? } 这里设计到错误处理了,因为是socket()返回的结果,我们可以直接在linux中运行 man socket 或者网络搜索 manpage socket ,跳到其中的 return value 段落,对于其他函数的错误处理,我们都是如法炮制的。 这里可以看到错误时返回-1并设置errno(errno number),这里就直接判断-1并将errno转为str输出: iostream提供 std::error system_error 提供std::system_category().message(errno),将错误码转为str输出 int listener = listenFD(); if (listener == -1){ std::cout<< std::system_category().message(errno)<<std::endl; // listener == -1 不是有效 fd,不需要 close return; } 之后将listener加入到epoll中,记得设置非阻塞(原因上一篇说了) int epfd = -1; void worker() { int listener = listenFD(); if (listener == -1) { std::cout << std::system_category().message(errno) << std::endl; close(listener); return; } set_nonblocking(listener); epfd = epoll_create1(0); // 返回值同样用man命令查询 if (epfd == -1) { std::cout << std::system_category().message(errno) << std::endl; // listener == -1 不是有效 fd,不需要 close return; } // 把listener加到epoll ctl的方法 // 同样通过manpage查询到 // man epoll_ctl或者网络搜索manpage epoll_ctl // 一共就三个重要的函数,epoll_create, epoll_ctl和epoll_wait 遇到挨个查就行 // 一定不要偷懒,要学会自己查的方法 epoll_event e; // man epoll_event e.data.fd = listener; // type的写法在man epoll_ctl的“The available event types are:”中 e.events = EPOLLIN; // 连接到来会发出EPOLLIN epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &e); while (true) { /* code */ } } 真正的循环等待 在上一部分里我们讲到,worker的任务是 等待到来的连接 accept他们 把读数据的ReadTask放到全局队列里 这里就要真正编写循环等待逻辑了,从epoll中获取函数的事件是epoll_wait, 原型是 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 这里的epfd当然是上面得到的epoll的fd,events是一个数组,需要你提供,当事件到来时,内核会把event一个个拷到你这个数组里,maxevents表示你一次想要多少个,比如一次最多要1024个,就新建一个event[1024],然后maxevents传1024;timeout这里没特殊要求,-1即可。因为我们监听的event_type是EPOLLIN,listener fd来新连接和其他fd有数据到来都会触发,所以我们要在循环中判断 while (true) { epoll_event event[1024]; int n = epoll_wait(epfd, event, 1024, 0); for (int i = 0; i < n; i++) { if (event[i].data.fd == listener){ // 监听fd来事件了 要accept新连接 并把新连接加到epoll } else{ // 其他fd来事件 要读数据 } } } 接下来写accept连接,epoll给出了连接到来的event,但并没有告诉有几个连接,所以你需要一直accept到EAGAIN\EWOULDBLOCK,剩下的按上面的listener如法炮制就可以了; 记得设置非阻塞。 while (true) { epoll_event event[1024]; int n = epoll_wait(epfd, event, 1024, -1); for (int i = 0; i < n; i++) { if (event[i].data.fd == listener) { // 有连接需要accept,但是不知道具体来了几个连接 while (true) { // man accept int fd = accept(listener, nullptr, nullptr); // 这里暂时不获取peer的地址,传个空指针 if (fd == -1) { if (errno == EWOULDBLOCK || errno==EAGAIN) { // 全部accept完了 break; } if (errno=EINTR){ // 再试 continue; } std::cerr << "Falied to accept new connect"; std::cerr << std::system_category().message(errno) << std::endl; // 其实可以通过errno判断出失败原因,但这里就不分更细了,只要失败了就忽略了 break; } // 设置非阻塞 set_nonblocking(fd); // 加到epoll中 epoll_event e; // man epoll_event e.data.fd = fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // oneshot别掉了 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e); } } else { // 其他fd来事件 要读数据 ReadTask task{}; task.fd = event[i].data.fd; task.event_type = event[i].events; { // 这也是一个技巧 即尽可能短的持有锁 // RAII的unique_lock在离开这个花括号作用域就会直接解锁 // 确保不会长时间持有,在有锁编程里你会经常看到用花括号控制锁的技巧 std::unique_lock l(mutex); queue.push(task); } cv.notify_one(); // 唤醒一个去读 // 思考:如果资源紧张,没有可用的线程让你去唤醒了怎么办? } } } 而其他读的事件,我们要新建一个ReadTask放到队列中,这里我们补一个全局的队列和对应的锁。注意条件变量要是全局的,之前就看到小白犯这样的错误:每次要用的时候新建一个条件变量,这么做是局部的,不能做到全局约束读写,只有全局共享一个才能做到全局只有一个线程允许读写。 #include <mutex> #include <condition_variable> struct ReadTask { int fd; uint32_t event_type; }; std::queue<ReadTask> queue; std::mutex mutex; std::condition_variable cv; 然后我们回去编写while中的else分支(数据到来),在这里我们要生成一个ReadTask,给队列上锁,把task放进去,解锁,然后唤醒一个worker线程去读 加锁解锁我们可以通过RAII的包装来解决,即unique_lock,唤醒一个线程就是cv.notify_one(),具体操作就是这样 else { // 其他fd来事件 要读数据 ReadTask task{}; task.fd = event[i].data.fd; task.event_type = event[i].events; { // 这也是一个技巧 即尽可能短的持有锁 // RAII的unique_lock在离开这个花括号作用域就会直接解锁 // 确保不会长时间持有,在有锁编程里你会经常看到用花括号控制锁的技巧 std::unique_lock l(mutex); queue.push(task); } cv.notify_one(); //唤醒一个 } 到这里我们的 worker_main 就写完了,完整函数如下 void worker_main() { int listener = listenFD(); if (listener == -1) { std::cerr << std::system_category().message(errno) << std::endl; // listener == -1 不是有效 fd,不需要 close return; } set_nonblocking(listener); epfd = epoll_create1(0); // 返回值同样用man命令查询 if (epfd == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listener); return; } // 把listener加到epoll ctl的方法 // 同样通过manpage查询到 // man epoll_ctl或者网络搜索manpage epoll_ctl // 一共就三个重要的函数,epoll_create, epoll_ctl和epoll_wait 遇到挨个查就行 // 一定不要偷懒,要学会自己查的方法 epoll_event e; // man epoll_event e.data.fd = listener; // type的写法在man epoll_ctl的“The available event types are:”中 e.events = EPOLLIN | EPOLLET; // 连接到来会发出EPOLLIN epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &e); while (true) { epoll_event event[1024]; int n = epoll_wait(epfd, event, 1024, -1); for (int i = 0; i < n; i++) { if (event[i].data.fd == listener) { // 有连接需要accept,但是不知道具体来了几个连接 while (true) { // man accept int fd = accept(listener, nullptr, nullptr); // 这里暂时不获取peer的地址,传个空指针 if (fd == -1) { if (errno == EWOULDBLOCK || errno==EAGAIN) { // 全部accept完了 break; } if (errno=EINTR){ // 再试 continue; } std::cerr << "Falied to accept new connect"; std::cerr << std::system_category().message(errno) << std::endl; // 其实可以通过errno判断出失败原因,但这里就不分更细了,只要失败了就忽略了 break; } // 设置非阻塞 set_nonblocking(fd); // 加到epoll中 epoll_event e; // man epoll_event e.data.fd = fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // oneshot别掉了 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e); } } else { // 其他fd来事件 要读数据 ReadTask task{}; task.fd = event[i].data.fd; task.event_type = event[i].events; { // 这也是一个技巧 即尽可能短的持有锁 // RAII的unique_lock在离开这个花括号作用域就会直接解锁 // 确保不会长时间持有,在有锁编程里你会经常看到用花括号控制锁的技巧 std::unique_lock l(mutex); queue.push(task); } cv.notify_one(); // 唤醒一个去读 // 思考:如果资源紧张,没有可用的线程让你去唤醒了怎么办? } } } } worker worker的工作就相对简单,先阻塞自己然后,等待被唤醒读数据就行了 while (true){ ReadTask task; { // 先获取锁 std::unique_lock l(mutex); // 这里等价于while(queue.empty()) { 解锁并阻塞 } cv.wait(l, []() -> bool { return !queue.empty(); }); // 运行到这里时线程已被阻塞且不再持有锁 // .. 等待中 .. // 被唤醒 // 已获取锁 task= queue.front(); queue.pop(); // 出了作用域 释放锁 } // 可以读数据了 } 接下来就可以读数据了,ET触发必须把所有数据都读完 // 读数据,ET触发必须保证读完 // c选手直接用char*数组也可以; 反正也不会有多大的开销,我习惯用vector std::vector<char> buf(1024); // 这是多大的缓冲区?(1kb 即1024字节) while (true){ // 读到没数据才能停 // 恢复epoll对当前fd的监听 } 这里我们使用EAGAIN和EWOULDBLOCK作为没有数据的条件,内核在没有数据时会给出EAGAIN,而EWOULDBLOCK(再读会阻塞)也是内核在委婉地告诉你没数据了。 代码大概是这样 // 读数据,ET触发必须保证读完 std::vector<char> buf(1024); // 这是多大的缓冲区?(1kb 即1024字节) std::string str; while (true) { ssize_t n = recv(task.fd, buf.data(), 1024, 0); if (n == 0) { // 对端关闭 // recv 返回 0 没数据了,而且以后也不会再有了,因为对方关闭了连接。 close(task.fd); break; } else if (n == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { // 读空了 现在没数据了,但以后可能还有,连接还在 // 这里必须加n,否则他会一直读到\n结尾,显然我们这里没有\n结尾 // 不指定的话会读取越界 // 需要恢复监听 epoll_event e{}; e.data.fd = task.fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_MOD, task.fd, &e); // 退出但不关闭连接 break; } else { // 其他错误 不读了直接撤 // 退出 把连接关了 std::cerr << std::system_category().message(errno) << std::endl; close(task.fd); break; } } else { str.append(buf.data(), n); continue; } } 因为之前我们给加入epoll的fd加了oneshot标志位,这会导致我们收到信号的时候,epoll对fd的监听已被禁用(防止再产生事件被其他线程处理),我们读完数据后必须重新调用epoll_ctl把监听再打开,代码如下 epoll_event e{}; e.data.fd = task.fd; e.events = EPOLLIN | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_MOD, task.fd, &e); 完整函数如下 void worker(int threadNum) { std::cout << "Thread: " << threadNum << " started"; // 标识线程 while (true) { ReadTask task; { // 先获取锁 std::unique_lock l(mutex); // 这里等价于while(queue.empty()) { 解锁并阻塞 } cv.wait(l, []() -> bool { return !queue.empty(); }); // 运行到这里时线程已被阻塞且不再持有锁 // .. 等待中 .. // 被唤醒 // 已获取锁 task = queue.front(); queue.pop(); // 出了作用域 释放锁 } // 读数据,ET触发必须保证读完 std::vector<char> buf(1024); // 这是多大的缓冲区?(1kb 即1024字节) std::string str; while (true) { ssize_t n = recv(task.fd, buf.data(), 1024, 0); if (n == 0) { // 对端关闭 // recv 返回 0 没数据了,而且以后也不会再有了,因为对方关闭了连接。 close(task.fd); break; } else if (n == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { // 读空了 现在没数据了,但以后可能还有,连接还在 // 这里必须加n,否则他会一直读到\n结尾,显然我们这里没有\n结尾 // 不指定的话会读取越界 // 需要恢复监听 epoll_event e{}; e.data.fd = task.fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_MOD, task.fd, &e); // 退出但不关闭连接 break; } else { // 其他错误 不读了直接撤 // 退出 把连接关了 std::cerr << std::system_category().message(errno) << std::endl; close(task.fd); break; } } else { str.append(buf.data(), n); continue; } } std::cout << "Received: " << str << std::endl; } } 恭喜你,读到这里你已经完成了大部分的工作 接下来我们只需要在main函数里起几个线程就可以完成这个简单的程序了; 主程序 int main() { // 先起读数据的线程,让他们自己阻塞自己然后等待 std::vector<std::thread> threads; for (int i = 0; i < 10; i++) { auto func = std::bind(worker, i); threads.push_back(std::thread(func)); } // 再起主线程,来唤醒子线程读数据 std::thread mainThread(worker_main); mainThread.join(); for (int i = 0; i < 10; i++) { threads[i].join(); } } 大功告成,完整程序如下 #include "common/common.h" #include <sys/epoll.h> #include <condition_variable> #include <functional> #include <iostream> #include <mutex> #include <queue> #include <system_error> #include <thread> #include <vector> int epfd = -1; struct ReadTask { int fd; uint32_t event_type; }; std::queue<ReadTask> queue; std::mutex mutex; std::condition_variable cv; void worker_main() { int listener = listenFD(); if (listener == -1) { std::cerr << std::system_category().message(errno) << std::endl; // listener == -1 不是有效 fd,不需要 close return; } set_nonblocking(listener); epfd = epoll_create1(0); // 返回值同样用man命令查询 if (epfd == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listener); return; } // 把listener加到epoll ctl的方法 // 同样通过manpage查询到 // man epoll_ctl或者网络搜索manpage epoll_ctl // 一共就三个重要的函数,epoll_create, epoll_ctl和epoll_wait 遇到挨个查就行 // 一定不要偷懒,要学会自己查的方法 epoll_event e; // man epoll_event e.data.fd = listener; // type的写法在man epoll_ctl的“The available event types are:”中 e.events = EPOLLIN | EPOLLET; // 连接到来会发出EPOLLIN epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &e); while (true) { epoll_event event[1024]; int n = epoll_wait(epfd, event, 1024, -1); for (int i = 0; i < n; i++) { if (event[i].data.fd == listener) { // 有连接需要accept,但是不知道具体来了几个连接 while (true) { // man accept int fd = accept(listener, nullptr, nullptr); // 这里暂时不获取peer的地址,传个空指针 if (fd == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { // 全部accept完了 break; } if (errno = EINTR) { // 再试 continue; } std::cerr << "Falied to accept new connect"; std::cerr << std::system_category().message(errno) << std::endl; // 其实可以通过errno判断出失败原因,但这里就不分更细了,只要失败了就忽略了 break; } // 设置非阻塞 set_nonblocking(fd); // 加到epoll中 epoll_event e; // man epoll_event e.data.fd = fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // oneshot别掉了 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &e); } } else { // 其他fd来事件 要读数据 ReadTask task{}; task.fd = event[i].data.fd; task.event_type = event[i].events; { // 这也是一个技巧 即尽可能短的持有锁 // RAII的unique_lock在离开这个花括号作用域就会直接解锁 // 确保不会长时间持有,在有锁编程里你会经常看到用花括号控制锁的技巧 std::unique_lock l(mutex); queue.push(task); } cv.notify_one(); // 唤醒一个去读 // 思考:如果资源紧张,没有可用的线程让你去唤醒了怎么办? } } } } void worker(int threadNum) { std::cout << "Thread: " << threadNum << " started"; // 标识线程 while (true) { ReadTask task; { // 先获取锁 std::unique_lock l(mutex); // 这里等价于while(queue.empty()) { 解锁并阻塞 } cv.wait(l, []() -> bool { return !queue.empty(); }); // 运行到这里时线程已被阻塞且不再持有锁 // .. 等待中 .. // 被唤醒 // 已获取锁 task = queue.front(); queue.pop(); // 出了作用域 释放锁 } // 读数据,ET触发必须保证读完 std::vector<char> buf(1024); // 这是多大的缓冲区?(1kb 即1024字节) std::string str; while (true) { ssize_t n = recv(task.fd, buf.data(), 1024, 0); if (n == 0) { // 对端关闭 // recv 返回 0 没数据了,而且以后也不会再有了,因为对方关闭了连接。 close(task.fd); break; } else if (n == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { // 读空了 现在没数据了,但以后可能还有,连接还在 // 这里必须加n,否则他会一直读到\n结尾,显然我们这里没有\n结尾 // 不指定的话会读取越界 // 需要恢复监听 epoll_event e{}; e.data.fd = task.fd; e.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_MOD, task.fd, &e); // 退出但不关闭连接 break; } else { // 其他错误 不读了直接撤 // 退出 把连接关了 std::cerr << std::system_category().message(errno) << std::endl; close(task.fd); break; } } else { str.append(buf.data(), n); continue; } } std::cout << "Worker: " << threadNum << "Received: " << str << std::endl; } } int main() { // 先起读数据的线程,让他们自己阻塞自己然后等待 std::vector<std::thread> threads; for (int i = 0; i < 10; i++) { auto func = std::bind(worker, i); threads.push_back(std::thread(func)); } // 再起主线程,来唤醒子线程读数据 std::thread mainThread(worker_main); mainThread.join(); for (int i = 0; i < 10; i++) { threads[i].join(); } } 总结 这个程序其实还存在着很多问题,如setNoBlocking的返回值没检查,epoll_ctl的返回值没检查,当你和内核打交道时,必须谨慎处理内核返回值,因为内核可能吐出各种各样的错误,每个影响都很大。我们在这里用到了很多系统调用如 accept() 、 recv() 、 epoll_ctl() 、 epoll_wait() 、 set_nonblocking() 、 close() ,严谨的编程应该检查每一个函数的返回值和错误。 Linux上运行 common.h #include "sys/socket.h" #include <netinet/in.h> #include <arpa/inet.h> #include "unistd.h" #include <iostream> #include <fcntl.h> int listenFD(); // 这是一个非常经典的封装函数 int set_nonblocking(int fd); common.cpp #include "common.h" int listenFD() { // 进入准备连接的状态 int listenFD = socket(AF_INET, SOCK_STREAM, 0); if (listenFD == -1) { std::cerr << std::system_category().message(errno) << std::endl; return -1; } sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(8080); int ret = inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); if (ret == 0) { std::cerr << "src does not contain a character string\ representing a valid network address in the specified address family "; return -1; } else if (ret == -1) { std::cerr << "af does not contain a valid address family"; return -1; } ret = bind(listenFD, (sockaddr *)&addr, sizeof(addr)); if (ret == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listenFD); return -1; } ret = listen(listenFD, 3); if (ret == -1) { std::cerr << std::system_category().message(errno) << std::endl; close(listenFD); return -1; } return listenFD; } int set_nonblocking(int fd) { // 1. 获取原来的 file status flags int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } // 2. 在原有的 flags 基础上,按位或(|)加上 O_NONBLOCK 标志 flags |= O_NONBLOCK; // 3. 将新的 flags 设置回去 if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } 我们这里使用一台Ubuntu24虚拟机,vscode使用ssh连接到这台机器,然后就可以开始调试了 命令 g++ -o server ./server2.cpp ./common/common.cpp -I . g++ client.cpp -o client 然后两个终端一个起server 一个起client 就能看到server在跑数据了 2 个帖子 - 2 位参与者 阅读完整话题
基础知识-Epoll 最近在学习Linux网络编程,目前进展到和epoll相关的部分了 ,我的学习路线是 第一阶段:能写阻塞式 socket 程序 目标:写出最普通的 TCP echo server / client。 第二阶段:理解 TCP 是“字节流”,不是“消息流” 目标:写一个带协议的服务,例如:4 字节长度 + body 当前阶段 :进入 non-blocking + epoll 写一个单线程并发 TCP server。 第四阶段:简单的 Reactor 网络库 按 muduo 的概念拆 按照之前的计划,用输出倒逼自己深入学习,目前的打算学一点就写一点 编程的部分已经写好了,不过我不懂linux下的调试,还要学习一段时间 正好做个拆分吧 其实感觉每一部分都比较长了 应该没人看完吧 Epoll是什么 Epoll是linux中的一种通知机制,即让内核通知你某些文件描述符上你感兴趣的事件,如你对几个fd上的读写事件感兴趣,你就可以用epoll_create新建一个epoll,用epoll_ctl将自己感兴趣的fd和想监听的事件类型传进去,同时给一个epoll_event类型的数组,数组的大小就是你让内核一次最多通知你的数量。linux内核会帮你监控这组fd上的io事件,如果你感兴趣的事件到来,把数据填到这个epoll_event数组里。 #include <sys/epoll.h> struct epoll_event { uint32_t events; // 事件类型 epoll_data_t data; // 用户数据,常用 data.fd 存 fd }; epoll_create1(int flag); // flag一般传0 epoll_ctl(int epoll_fd, int op, int fd, epoll_event* event); // event里写你感兴趣的事件 int epoll_wait( int epfd, struct epoll_event *events, //返回的事件 int maxevents, int timeout ); // 会阻塞 等待事件到来 这里epoll_ctl的event是你告诉内核,你想要监控什么fd上的什么事件,而epoll_wait在等待到事件后会返给你一个event,里面有fd和实际发生的事件。 非阻塞 IO有几种模型,其中两种就是阻塞式和非阻塞式,他们的区别就一句话: 当你尝试读数据而数据未就绪的时候,是否会立刻返回 在尝试使用recv从一个fd上读数据,而这个fd上没有数据到来时: 阻塞IO: 阻塞在这等待数据到来 非阻塞IO:马上返回 显然想要最大利用资源,非阻塞IO是必须用的。记得不要只设置被accept的fd为非阻塞,监听fd自身也要设置为非阻塞,否则如果队列里没有要accept的连接而你调了accept,listener fd就会被阻塞,直接前功尽弃; 设置一个fd为非阻塞的方法是使用 int fcntl(int fd, int op, ...); 中的 F_GETFL 和 F_SETFL , 先获取到flag然后|上```O_NONBLOCK ``,一般封装成一个bool setNoBlock(int fd)函数 int set_nonblocking(int fd) { // 1. 获取原来的 file status flags int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } // 2. 在原有的 flags 基础上,按位或(|)加上 O_NONBLOCK 标志 flags |= O_NONBLOCK; // 3. 将新的 flags 设置回去 if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl F_SETFL"); return -1; } return 0; } Epoll的水平触发和边沿触发 假设你关注的是数据到来的事件 水平触发LT:只要你没读完,缓冲区还有剩数据,就会一直收到event的通知 边沿触发ET:只有数据到来时会发一下,即无数据->有数据会触发,后面不发,你没读完也不发,所以你必须保证在一次ET中读完所有数据 Epoll+非阻塞 单线程模型 只有一个主线程,同时负责accept到来的连接和接收已经连接上的fd发出的事件。如果事件很多会导致主线程忙,不能即时accept到来的连接。 // 主线程 void work_main(){ ... epoll_event events[MAX_EVENTS]; // event是接受到来的事件的数组 int n = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms); for (int i = 0; i < n; ++i) { int fd = events[i].data.fd; uint32_t ev = events[i].events; if(fd == listenerFD){ // 是监听FD,需要accept连接 } else{ // 数据来了 需要读数据 } } } Epoll+非阻塞+线程池 开一个主线程和线程池,主线程只负责accept到来的连接,然后将fd添加到epoll。当事件到来时,就唤醒线程池的一个线程去读(使用条件变量机制),大大提升效率。 重要处理,边缘/水平触发都要小心一个fd被多个worker处理的情况 水平触发就不用讲了,只要A一次没读完,他就会继续给出event,就可能被错误地分给B线程读。但出乎一些人的意料,边缘触发也会产生这种情况: fd=10 第一次可读 epoll_wait 返回 fd=10 worker A 正在处理 fd=10 这时第二批数据又到来 epoll 可能再次返回 fd=10 主线程又投递给 worker B 所以我们要使用EPOLLONESHOT阻止这种情况(具体代码放下面了),给epoll里的fd设置这个flag后,当epoll产生了数据到来的事件后会直接禁用epoll对这个fd的监听,但fd没被禁用,数据依然可以到来并进入内核缓冲区,不过epoll对他不再感兴趣,需要你重新将fd加入到epoll监听;这样就可以避免多次通知导致几个线程同时操作一个fd。 t1: fd=10 注册 EPOLLIN | EPOLLET | EPOLLONESHOT t2: 第一批数据到来 t3: epoll_wait 返回 fd=10 t4: fd=10 在 epoll 中被自动 disabled t5: 主线程把 fd=10 投递给 worker A t6: worker A 正在读 / 解析 t7: 第二批数据又到来 t8: 不会再因为 fd=10 产生新的 epoll 通知给主线程 简而言之,最好使用EPOLLONESHOT,worker 持有 fd 期间要一直读到 EAGAIN 或者 EWOULDBLOCK (内核另一种委婉地告诉你没数据了的方法,即: 你再读会阻塞),再重启监听。 重启后如果fd就绪可读且还有没读完的数据,就又会收到一个event,然后周而复始。也有一种可能是新来的数据已经被第一个worker读完了,虽然他不知道新来数据了,但他的目的是读到EAGAIN,可能碰巧完成了任务。 被禁用后启用监听的方法,其实和添加监听一模一样: void add_fd(int epfd, int fd) { epoll_event ev{}; ev.data.fd = fd; ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // 被禁用后重新启动就改成EPOLL_CTL_MOD,其他一样 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); } 基础知识-多线程 毕竟我们要使用多线程开发,一些多线程知识还是必不可少 Linux下现代C++开启一个线程的方法 #include <thread> void function(){ while(true) print("Hello world"); } std::thread t = thread(&function); // 此时已经在运行function,也就是在疯狂打印hello world t.join() //等待线程执行完成,必须调用join(某些情况下detach),否则thread在析构时会直接terminate导致程序退出 // 但是因为function里写的是死循环,所以其实永远也等不到t主动关闭 // 这么写是为了规范 经典的生产者消费者模型 使用epoll+多线程我们用的是生产者-消费者模型 我们会有1个主线程,专门负责accept到来的连接和接收数据到来的事件,注意他只是接收事件,他自己不读,它让其他线程读 很多的worker线程,专门负责读数据,不自己处理连接和数据到来事件 那么如何让这些线程联动呢?答案就是生产者-消费者模型中的队列;我们会放个全局的std::queue队列,然后让全部的worker线程都去等待这个队列,他们会全部阻塞;然后再启主线程,不断地将到需要读数据的fd的相关信息放到queue中,放一个唤醒一个worker线程,worker线程会自己取走queue中的信息,然后自己开始读; 这种设计能大大提高效率, 锁的使用 我们用锁的方式来保护queue队列,不管是主线程还是子线程,操作queue时必须先获得锁,为了方便,我们直接使用 std::unique_lock 来获得锁,他是一个RAII的锁,也就是初始化时上锁,析构时解锁 void func(){ { std::unique_lock(mutex); //已上锁 queue.pop(); } // unique_lock析构了,此时已解锁 } 那么初始化时如何让所有的worker线程都因等待queue而阻塞呢?这样我们才能在数据到来时一个个唤醒。答案是使用 std::conditional_variable 条件变量,使用方式是这样的 // 全局的 std::mutex mutex; std::condition_variable cv; std::queue<event> queue; void workder(){ // 假设这是子线程的锁 std::unique_lock l(mutex); // 条件变量必须和锁配合使用,且必须是已经上锁的变量 cv.wait(l, [](){ return !event.isEmpty();}); // !event.isEmpty()是一个pred谓词,这里等价于 // while(!pred){ 继续等待 } 即如果队列为空就等待,而wait会解锁传给他的lock,等他被唤醒时再尝试重新上锁 // 导致的结果是,所有worker经历了:获取到了锁,再主动解锁然后阻塞 // 然后我们就可以启主线程,获取锁,往队列里放东西,解锁,唤醒一个线程让他去获取锁并拿走数据 } 参考资料 epoll(7) - Linux manual page accept(2) - Linux manual page fcntl(2) - Linux manual page https://en.cppreference.com/w/cpp/thread/condition_variable/wait https://en.cppreference.com/w/cpp/thread/thread/~thread https://en.cppreference.com/w/cpp/thread/unique_lock 8 个帖子 - 6 位参与者 阅读完整话题
主要目的是给自己投资用, 既然大家都量化, 打不过就加入, 希望能找到一些深入浅出的教程, 而不是那些用一堆别人看不懂的词语黑话来表示自己很专业的那种. 当然很专业的也能用大白话说出每个专有词语的含义.
主要目的是给自己投资用, 既然大家都量化, 打不过就加入, 希望能找到一些深入浅出的教程, 而不是那些用一堆别人看不懂的词语黑话来表示自己很专业的那种. 当然很专业的也能用大白话说出每个专有词语的含义.
主要目的是给自己投资用, 既然大家都量化, 打不过就加入, 希望能找到一些深入浅出的教程, 而不是那些用一堆别人看不懂的词语黑话来表示自己很专业的那种. 当然很专业的也能用大白话说出每个专有词语的含义.
提示词 模仿哆啦A梦黑白漫画,泛黄的底色,垂直大图,讲解rust编程开发的基础知识第一章: 你好世界和变量讲解。 (简体中文,代码可适当使用彩色) 8 个帖子 - 5 位参与者 阅读完整话题