Chemmy's Blog

chengming0916@outlook.com

简单工厂模式(Simple Factory Pattern)是设计模式中创建型模式的基础,它不属于GoF(Gang of Four)23种经典设计模式,但因其简洁易用的特性,成为实际开发中最常用的“准设计模式”。核心思想是通过一个工厂类封装对象的创建逻辑,客户端无需直接实例化具体类,只需通过工厂类传入参数即可获取对应实例,降低代码耦合度,提升扩展性。

本文将从简单工厂模式的核心原理出发,分别基于C#、Python、Golang、C++和纯C语言实现该模式,覆盖面向对象(OOP)和过程式编程场景,帮助开发者理解不同语言下的落地方式。

一、简单工厂模式核心结构

简单工厂模式通常包含三个核心角色:

  1. 产品抽象层:定义产品的公共接口/抽象类(OOP语言)或统一的数据结构与函数指针(过程式语言如C);

  2. 具体产品类:实现/继承抽象层,是实际被创建的对象;

  3. 工厂类:提供静态/普通方法,根据输入参数创建并返回具体产品实例。

二、多语言实现示例

场景说明

以“计算器”为例:定义四则运算(加法、减法)的产品抽象,通过工厂类根据“+”“-”参数创建对应运算实例,最终执行计算逻辑。

1. C# 实现(纯OOP)

C#作为典型的面向对象语言,通过接口定义产品抽象,工厂类封装创建逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;

// 1. 产品抽象层:运算接口
public interface IOperation
{
double Calculate(double num1, double num2);
}

// 2. 具体产品类:加法运算
public class AddOperation : IOperation
{
public double Calculate(double num1, double num2)
{
return num1 + num2;
}
}

// 2. 具体产品类:减法运算
public class SubtractOperation : IOperation
{
public double Calculate(double num1, double num2)
{
return num1 - num2;
}
}

// 3. 工厂类:运算工厂
public class OperationFactory
{
public static IOperation CreateOperation(string oper)
{
return oper switch
{
"+" => new AddOperation(),
"-" => new SubtractOperation(),
_ => throw new ArgumentException("不支持的运算类型")
};
}
}

// 客户端调用
class Program
{
static void Main()
{
IOperation addOp = OperationFactory.CreateOperation("+");
Console.WriteLine("10 + 5 = " + addOp.Calculate(10, 5)); // 输出:15

IOperation subOp = OperationFactory.CreateOperation("-");
Console.WriteLine("10 - 5 = " + subOp.Calculate(10, 5)); // 输出:5
}
}

2. Python 实现(动态OOP)

Python无需显式定义接口,通过抽象基类(ABC)模拟产品抽象,工厂函数实现创建逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from abc import ABC, abstractmethod

# 1. 产品抽象层:运算抽象类
class Operation(ABC):
@abstractmethod
def calculate(self, num1, num2):
pass

# 2. 具体产品类:加法运算
class AddOperation(Operation):
def calculate(self, num1, num2):
return num1 + num2

# 2. 具体产品类:减法运算
class SubtractOperation(Operation):
def calculate(self, num1, num2):
return num1 - num2

# 3. 工厂函数:运算工厂
def create_operation(oper):
if oper == "+":
return AddOperation()
elif oper == "-":
return SubtractOperation()
else:
raise ValueError("不支持的运算类型")

# 客户端调用
if __name__ == "__main__":
add_op = create_operation("+")
print(f"10 + 5 = {add_op.calculate(10, 5)}") # 输出:15

sub_op = create_operation("-")
print(f"10 - 5 = {sub_op.calculate(10, 5)}") # 输出:5

3. Golang 实现(基于接口的OOP)

Go语言无“类”和“继承”,通过接口实现产品抽象,工厂函数封装创建逻辑(Go推荐使用函数而非结构体作为工厂):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"errors"
"fmt"
)

// 1. 产品抽象层:运算接口
type Operation interface {
Calculate(num1, num2 float64) float64
}

// 2. 具体产品类:加法运算(Go中用结构体模拟类)
type AddOperation struct{}

func (a *AddOperation) Calculate(num1, num2 float64) float64 {
return num1 + num2
}

// 2. 具体产品类:减法运算
type SubtractOperation struct{}

func (s *SubtractOperation) Calculate(num1, num2 float64) float64 {
return num1 - num2
}

// 3. 工厂函数:运算工厂
func CreateOperation(oper string) (Operation, error) {
switch oper {
case "+":
return &AddOperation{}, nil
case "-":
return &SubtractOperation{}, nil
default:
return nil, errors.New("不支持的运算类型")
}
}

// 客户端调用
func main() {
addOp, _ := CreateOperation("+")
fmt.Printf("10 + 5 = %.1f\n", addOp.Calculate(10, 5)) // 输出:15.0

subOp, _ := CreateOperation("-")
fmt.Printf("10 - 5 = %.1f\n", subOp.Calculate(10, 5)) // 输出:5.0
}

4. C++ 实现(经典OOP)

C++通过纯虚函数定义产品抽象类,工厂类提供静态方法创建具体产品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <stdexcept>
using namespace std;

// 1. 产品抽象层:运算抽象类
class Operation {
public:
virtual double calculate(double num1, double num2) = 0; // 纯虚函数
virtual ~Operation() = default; // 虚析构函数
};

// 2. 具体产品类:加法运算
class AddOperation : public Operation {
public:
double calculate(double num1, double num2) override {
return num1 + num2;
}
};

// 2. 具体产品类:减法运算
class SubtractOperation : public Operation {
public:
double calculate(double num1, double num2) override {
return num1 - num2;
}
};

// 3. 工厂类:运算工厂
class OperationFactory {
public:
static Operation* createOperation(string oper) {
if (oper == "+") {
return new AddOperation();
} else if (oper == "-") {
return new SubtractOperation();
} else {
throw invalid_argument("不支持的运算类型");
}
}
};

// 客户端调用
int main() {
Operation* addOp = OperationFactory::createOperation("+");
cout << "10 + 5 = " << addOp->calculate(10, 5) << endl; // 输出:15
delete addOp;

Operation* subOp = OperationFactory::createOperation("-");
cout << "10 - 5 = " << subOp->calculate(10, 5) << endl; // 输出:5
delete subOp;

return 0;
}

5. 纯C语言实现(过程式模拟)

C语言无面向对象特性,通过函数指针+结构体模拟产品抽象,工厂函数返回结构体指针(封装创建逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 1. 产品抽象层:运算结构体(包含函数指针)
typedef struct {
double (*calculate)(double num1, double num2); // 运算函数指针
} Operation;

// 2. 具体产品实现:加法运算函数
double add_calculate(double num1, double num2) {
return num1 + num2;
}

// 2. 具体产品实现:减法运算函数
double subtract_calculate(double num1, double num2) {
return num1 - num2;
}

// 3. 工厂函数:创建运算实例
Operation* create_operation(const char* oper) {
Operation* op = (Operation*)malloc(sizeof(Operation));
if (op == NULL) {
perror("malloc failed");
exit(EXIT_FAILURE);
}

if (strcmp(oper, "+") == 0) {
op->calculate = add_calculate;
} else if (strcmp(oper, "-") == 0) {
op->calculate = subtract_calculate;
} else {
free(op);
fprintf(stderr, "不支持的运算类型\n");
exit(EXIT_FAILURE);
}
return op;
}

// 客户端调用
int main() {
Operation* add_op = create_operation("+");
printf("10 + 5 = %.1f\n", add_op->calculate(10, 5)); // 输出:15.0
free(add_op);

Operation* sub_op = create_operation("-");
printf("10 - 5 = %.1f\n", sub_op->calculate(10, 5)); // 输出:5.0
free(sub_op);

return 0;
}

三、简单工厂模式的优缺点

优点

  1. 解耦:客户端无需关注产品创建细节,仅需通过工厂调用,符合“开闭原则”(扩展新产品时仅需新增具体产品类+修改工厂逻辑);

  2. 复用性:创建逻辑集中在工厂,避免重复代码;

  3. 易维护:产品实例的创建逻辑统一管理,修改时仅需调整工厂类。

缺点

  1. 工厂类职责过重:所有产品创建逻辑集中在工厂,新增产品需修改工厂代码,违反“开闭原则”(可通过工厂方法模式优化);

  2. 产品类型扩展受限:若产品类型过多,工厂类会变得臃肿,可读性和维护性下降;

  3. 难以支持复杂产品创建:无法处理依赖关系复杂的产品实例化。

四、适用场景

  1. 产品类型较少且相对固定,如工具类、简单业务组件;

  2. 客户端无需知道产品创建细节,仅需通过参数获取实例;

  3. 希望统一管理产品创建逻辑,降低客户端与具体产品的耦合。

五、总结

简单工厂模式是创建型模式的入门级实现,不同语言的落地方式虽有差异(OOP语言基于接口/抽象类,过程式语言基于函数指针+结构体),但核心思想一致:封装创建逻辑,简化客户端调用

在实际开发中,需根据语言特性和业务复杂度选择:OOP语言(C#/Python/Go/C++)可直接通过接口/抽象类实现,过程式语言(C)可通过函数指针模拟;若产品类型频繁扩展,建议升级为工厂方法模式或抽象工厂模式,进一步解耦创建逻辑。

(注:文档部分内容可能由 AI 生成)

工厂方法模式是GoF 23种经典创建型设计模式之一,核心价值在于解耦对象的创建与使用,通过抽象化设计满足“开闭原则”,让系统在不修改原有代码的前提下灵活扩展产品类型。本文将从模式核心结构入手,详解其工作原理,提供C#、Python、Golang、C++及纯C语言的落地实现,并分析优缺点与适用场景,帮助开发者快速掌握该模式的实际应用。

一、工厂方法模式的核心结构

工厂方法模式通过分层设计,将产品创建逻辑抽象化,核心包含4个角色,各角色职责清晰、分工明确,共同实现“创建与使用分离”的目标,具体结构如下:

1.1 核心角色定义

  • 抽象产品(Abstract Product):定义产品的统一规范,描述产品的核心行为与属性,是所有具体产品的父类(或接口)。例如“电子设备”抽象类,定义“开机”“关机”等通用方法。

  • 具体产品(Concrete Product):实现抽象产品的接口,是工厂方法模式的最终创建目标,与具体工厂一一对应。例如“手机”“电脑”,均继承自“电子设备”,并实现各自的开机、关机逻辑。

  • 抽象工厂(Abstract Factory):定义创建产品的抽象接口(仅声明创建方法,不实现具体逻辑),是所有具体工厂的父类(或接口),负责约束具体工厂的行为。

  • 具体工厂(Concrete Factory):继承或实现抽象工厂,重写创建产品的方法,负责具体产品的实例化,是连接抽象产品与具体产品的桥梁。

1.2 核心工作流程

  1. 客户端无需直接创建产品,仅需依赖抽象工厂;2. 客户端根据需求选择具体工厂;3. 具体工厂调用自身的创建方法,生成对应的具体产品;4. 客户端通过抽象产品接口使用产品,无需关注产品的具体实现细节。

这种流程设计的核心优势的是“扩展无侵入”——新增产品时,只需新增具体产品类和对应具体工厂类,无需修改抽象工厂、已有具体工厂及客户端代码,完美契合开闭原则。

二、多语言实现(统一场景:电子设备创建)

以下以“电子设备创建”为统一场景(抽象产品:电子设备;具体产品:手机、电脑;抽象工厂:电子设备工厂;具体工厂:手机工厂、电脑工厂),分别实现工厂方法模式,覆盖5种主流语言,兼顾语法特性与模式核心。

2.1 C# 实现(抽象类+继承)

C#作为强类型面向对象语言,通过抽象类+继承天然适配工厂方法模式,语法简洁且类型安全,适合企业级开发场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
using System;

// 1. 抽象产品:电子设备
public abstract class ElectronicDevice
{
// 抽象方法:开机
public abstract void PowerOn();
// 抽象方法:关机
public abstract void PowerOff();
}

// 2. 具体产品1:手机
public class MobilePhone : ElectronicDevice
{
public override void PowerOn()
{
Console.WriteLine("手机开机,启动操作系统...");
}

public override void PowerOff()
{
Console.WriteLine("手机关机,保存数据...");
}
}

// 2. 具体产品2:电脑
public class Computer : ElectronicDevice
{
public override void PowerOn()
{
Console.WriteLine("电脑开机,加载驱动程序...");
}

public override void PowerOff()
{
Console.WriteLine("电脑关机,关闭后台进程...");
}
}

// 3. 抽象工厂:电子设备工厂
public abstract class ElectronicDeviceFactory
{
// 抽象创建方法
public abstract ElectronicDevice CreateDevice();
}

// 4. 具体工厂1:手机工厂
public class MobilePhoneFactory : ElectronicDeviceFactory
{
public override ElectronicDevice CreateDevice()
{
Console.WriteLine("手机工厂:创建手机实例");
return new MobilePhone();
}
}

// 4. 具体工厂2:电脑工厂
public class ComputerFactory : ElectronicDeviceFactory
{
public override ElectronicDevice CreateDevice()
{
Console.WriteLine("电脑工厂:创建电脑实例");
return new Computer();
}
}

// 客户端调用
class Client
{
static void Main(string[] args)
{
// 1. 创建手机工厂,获取手机产品
ElectronicDeviceFactory mobileFactory = new MobilePhoneFactory();
ElectronicDevice mobile = mobileFactory.CreateDevice();
mobile.PowerOn();
mobile.PowerOff();

// 2. 创建电脑工厂,获取电脑产品
ElectronicDeviceFactory computerFactory = new ComputerFactory();
ElectronicDevice computer = computerFactory.CreateDevice();
computer.PowerOn();
computer.PowerOff();
}
}

2.2 Python 实现(abc模块+动态特性)

Python无严格的抽象类语法,通过abc模块实现抽象接口,结合动态语言的灵活性,简化工厂与产品的定义,代码简洁易读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from abc import ABC, abstractmethod

# 1. 抽象产品:电子设备
class ElectronicDevice(ABC):
@abstractmethod
def power_on(self):
"""抽象方法:开机"""
pass

@abstractmethod
def power_off(self):
"""抽象方法:关机"""
pass

# 2. 具体产品1:手机
class MobilePhone(ElectronicDevice):
def power_on(self):
print("手机开机,启动操作系统...")

def power_off(self):
print("手机关机,保存数据...")

# 2. 具体产品2:电脑
class Computer(ElectronicDevice):
def power_on(self):
print("电脑开机,加载驱动程序...")

def power_off(self):
print("电脑关机,关闭后台进程...")

# 3. 抽象工厂:电子设备工厂
class ElectronicDeviceFactory(ABC):
@abstractmethod
def create_device(self):
"""抽象创建方法"""
pass

# 4. 具体工厂1:手机工厂
class MobilePhoneFactory(ElectronicDeviceFactory):
def create_device(self):
print("手机工厂:创建手机实例")
return MobilePhone()

# 4. 具体工厂2:电脑工厂
class ComputerFactory(ElectronicDeviceFactory):
def create_device(self):
print("电脑工厂:创建电脑实例")
return Computer()

# 客户端调用
if __name__ == "__main__":
# 获取手机产品
mobile_factory = MobilePhoneFactory()
mobile = mobile_factory.create_device()
mobile.power_on()
mobile.power_off()

# 获取电脑产品
computer_factory = ComputerFactory()
computer = computer_factory.create_device()
computer.power_on()
computer.power_off()

2.3 Golang 实现(接口+结构体)

Golang无类继承特性,通过“接口+结构体”实现多态,核心是定义产品接口和工厂接口,结构体实现接口方法,贴合Go语言“面向接口编程”的设计理念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import "fmt"

// 1. 抽象产品:电子设备接口
type ElectronicDevice interface {
PowerOn() // 开机方法
PowerOff() // 关机方法
}

// 2. 具体产品1:手机
type MobilePhone struct{}

func (m *MobilePhone) PowerOn() {
fmt.Println("手机开机,启动操作系统...")
}

func (m *MobilePhone) PowerOff() {
fmt.Println("手机关机,保存数据...")
}

// 2. 具体产品2:电脑
type Computer struct{}

func (c *Computer) PowerOn() {
fmt.Println("电脑开机,加载驱动程序...")
}

func (c *Computer) PowerOff() {
fmt.Println("电脑关机,关闭后台进程...")
}

// 3. 抽象工厂:电子设备工厂接口
type ElectronicDeviceFactory interface {
CreateDevice() ElectronicDevice // 创建产品方法
}

// 4. 具体工厂1:手机工厂
type MobilePhoneFactory struct{}

func (m *MobilePhoneFactory) CreateDevice() ElectronicDevice {
fmt.Println("手机工厂:创建手机实例")
return &MobilePhone{}
}

// 4. 具体工厂2:电脑工厂
type ComputerFactory struct{}

func (c *ComputerFactory) CreateDevice() ElectronicDevice {
fmt.Println("电脑工厂:创建电脑实例")
return &Computer{}
}

// 客户端调用
func main() {
// 获取手机产品
var mobileFactory ElectronicDeviceFactory = &MobilePhoneFactory{}
mobile := mobileFactory.CreateDevice()
mobile.PowerOn()
mobile.PowerOff()

// 获取电脑产品
var computerFactory ElectronicDeviceFactory = &ComputerFactory{}
computer := computerFactory.CreateDevice()
computer.PowerOn()
computer.PowerOff()
}

2.4 C++ 实现(纯虚函数+继承)

C++通过纯虚函数定义抽象接口(抽象产品、抽象工厂),结合继承实现具体产品与具体工厂,需注意虚析构函数的定义,避免内存泄漏,适合高性能、底层开发场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>
using namespace std;

// 1. 抽象产品:电子设备
class ElectronicDevice {
public:
// 纯虚函数:开机
virtual void PowerOn() = 0;
// 纯虚函数:关机
virtual void PowerOff() = 0;
// 虚析构函数,避免内存泄漏
virtual ~ElectronicDevice() {}
};

// 2. 具体产品1:手机
class MobilePhone : public ElectronicDevice {
public:
void PowerOn() override {
cout << "手机开机,启动操作系统..." << endl;
}

void PowerOff() override {
cout << "手机关机,保存数据..." << endl;
}
};

// 2. 具体产品2:电脑
class Computer : public ElectronicDevice {
public:
void PowerOn() override {
cout << "电脑开机,加载驱动程序..." << endl;
}

void PowerOff() override {
cout << "电脑关机,关闭后台进程..." << endl;
}
};

// 3. 抽象工厂:电子设备工厂
class ElectronicDeviceFactory {
public:
// 纯虚创建方法
virtual ElectronicDevice* CreateDevice() = 0;
// 虚析构函数
virtual ~ElectronicDeviceFactory() {}
};

// 4. 具体工厂1:手机工厂
class MobilePhoneFactory : public ElectronicDeviceFactory {
public:
ElectronicDevice* CreateDevice() override {
cout << "手机工厂:创建手机实例" << endl;
return new MobilePhone();
}
};

// 4. 具体工厂2:电脑工厂
class ComputerFactory : public ElectronicDeviceFactory {
public:
ElectronicDevice* CreateDevice() override {
cout << "电脑工厂:创建电脑实例" << endl;
return new Computer();
}
};

// 客户端调用
int main() {
// 获取手机产品
ElectronicDeviceFactory* mobileFactory = new MobilePhoneFactory();
ElectronicDevice* mobile = mobileFactory->CreateDevice();
mobile->PowerOn();
mobile->PowerOff();

// 获取电脑产品
ElectronicDeviceFactory* computerFactory = new ComputerFactory();
ElectronicDevice* computer = computerFactory->CreateDevice();
computer->PowerOn();
computer->PowerOff();

// 释放内存,避免泄漏
delete mobile;
delete mobileFactory;
delete computer;
delete computerFactory;

return 0;
}

2.5 纯C语言实现(结构体+函数指针)

纯C语言无面向对象特性,通过“结构体嵌套+函数指针”模拟抽象接口与继承,核心是将产品行为、工厂创建逻辑封装为函数指针,实现类似多态的效果,适合底层嵌入式开发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdio.h>
#include <stdlib.h>

// 1. 抽象产品:电子设备(结构体+函数指针模拟抽象方法)
typedef struct _ElectronicDevice {
void (*PowerOn)(struct _ElectronicDevice*); // 开机函数指针
void (*PowerOff)(struct _ElectronicDevice*); // 关机函数指针
} ElectronicDevice;

// 2. 具体产品1:手机
typedef struct _MobilePhone {
ElectronicDevice base; // 嵌套基结构体,模拟继承
} MobilePhone;

// 手机的开机实现
void MobilePhone_PowerOn(ElectronicDevice* device) {
printf("手机开机,启动操作系统...\n");
}

// 手机的关机实现
void MobilePhone_PowerOff(ElectronicDevice* device) {
printf("手机关机,保存数据...\n");
}

// 2. 具体产品2:电脑
typedef struct _Computer {
ElectronicDevice base; // 嵌套基结构体,模拟继承
} Computer;

// 电脑的开机实现
void Computer_PowerOn(ElectronicDevice* device) {
printf("电脑开机,加载驱动程序...\n");
}

// 电脑的关机实现
void Computer_PowerOff(ElectronicDevice* device) {
printf("电脑关机,关闭后台进程...\n");
}

// 3. 抽象工厂:电子设备工厂(结构体+函数指针模拟创建方法)
typedef struct _ElectronicDeviceFactory {
ElectronicDevice* (*CreateDevice)(struct _ElectronicDeviceFactory*);
} ElectronicDeviceFactory;

// 4. 具体工厂1:手机工厂
typedef struct _MobilePhoneFactory {
ElectronicDeviceFactory base; // 嵌套基结构体,模拟继承
} MobilePhoneFactory;

// 手机工厂创建手机实例
ElectronicDevice* MobilePhoneFactory_CreateDevice(ElectronicDeviceFactory* factory) {
printf("手机工厂:创建手机实例\n");
MobilePhone* mobile = (MobilePhone*)malloc(sizeof(MobilePhone));
// 绑定函数指针(实现抽象方法)
mobile->base.PowerOn = MobilePhone_PowerOn;
mobile->base.PowerOff = MobilePhone_PowerOff;
return (ElectronicDevice*)mobile;
}

// 4. 具体工厂2:电脑工厂
typedef struct _ComputerFactory {
ElectronicDeviceFactory base; // 嵌套基结构体,模拟继承
} ComputerFactory;

// 电脑工厂创建电脑实例
ElectronicDevice* ComputerFactory_CreateDevice(ElectronicDeviceFactory* factory) {
printf("电脑工厂:创建电脑实例\n");
Computer* computer = (Computer*)malloc(sizeof(Computer));
// 绑定函数指针(实现抽象方法)
computer->base.PowerOn = Computer_PowerOn;
computer->base.PowerOff = Computer_PowerOff;
return (ElectronicDevice*)computer;
}

// 客户端调用
int main() {
// 1. 获取手机产品
MobilePhoneFactory mobileFactory;
mobileFactory.base.CreateDevice = MobilePhoneFactory_CreateDevice;
ElectronicDevice* mobile = mobileFactory.base.CreateDevice((ElectronicDeviceFactory*)&mobileFactory);
mobile->PowerOn(mobile);
mobile->PowerOff(mobile);

// 2. 获取电脑产品
ComputerFactory computerFactory;
computerFactory.base.CreateDevice = ComputerFactory_CreateDevice;
ElectronicDevice* computer = computerFactory.base.CreateDevice((ElectronicDeviceFactory*)&computerFactory);
computer->PowerOn(computer);
computer->PowerOff(computer);

// 释放内存
free(mobile);
free(computer);

return 0;
}

三、工厂方法模式的优缺点

工厂方法模式是在简单工厂模式基础上的优化,解决了简单工厂“扩展需修改代码”的核心问题,但同时也带来了一定的复杂度,需结合场景合理选择。

3.1 优点

  • 符合开闭原则:新增产品时,仅需新增具体产品类和对应具体工厂类,无需修改原有抽象工厂、已有工厂及客户端代码,降低扩展成本。

  • 解耦创建与使用:客户端仅依赖抽象工厂和抽象产品,无需了解产品的具体创建逻辑(如实例化细节、初始化步骤),降低代码耦合度。

  • 支持多态扩展:通过抽象接口实现多态,具体工厂可灵活返回不同的具体产品,客户端无需修改代码即可使用新的产品。

  • 责任单一:每个具体工厂仅负责创建一种具体产品,符合单一职责原则,便于代码维护和调试。

3.2 缺点

  • 增加系统复杂度:每新增一个产品,需同时新增具体产品类和具体工厂类,导致系统类数量增多,增加代码量和维护成本。

  • 层级冗余:相比简单工厂模式,多了抽象工厂和具体工厂的分层,对于简单场景(产品类型极少、无需扩展),显得过于繁琐。

  • 理解成本提升:需理解抽象接口、多态等概念,对于新手而言,上手难度高于简单工厂模式。

四、工厂方法模式的使用场景

工厂方法模式适合“产品类型需灵活扩展、需隐藏创建细节”的场景,具体应用如下:

  • 产品类型稳定且需扩展:如电商系统的支付方式(支付宝、微信支付、银行卡支付),新增支付方式时,仅需新增支付产品类和支付工厂类,无需修改原有支付逻辑。

  • 隐藏产品创建细节:如数据库连接池、日志管理器,客户端无需关心连接/日志实例的创建、初始化、销毁细节,仅通过工厂获取实例即可。

  • 框架/库设计:框架开发中,为了让用户能扩展自定义产品,通常会提供抽象工厂接口,用户通过实现具体工厂和产品,集成到框架中(如.NET的HandlerFactory、Spring的BeanFactory)。

  • 多环境适配:如不同环境(开发、测试、生产)的配置实例创建,通过不同的具体工厂,返回适配对应环境的配置产品,客户端无需修改代码即可切换环境。

注意:若产品类型极少(如仅1-2种)且无需扩展,建议使用简单工厂模式;若需支持多系列产品(如手机+电脑+平板,每种产品又有不同品牌),建议使用抽象工厂模式。

五、总结

工厂方法模式的核心是“抽象化创建逻辑,通过多态实现扩展”,其本质是将对象创建的责任委托给具体工厂,实现“创建与使用分离”,核心价值在于满足开闭原则,提升系统的灵活性和可维护性。

从多语言实现来看,尽管不同语言的语法差异显著,但核心思想完全一致:

  • 面向对象语言(C#、Python、Golang、C++):通过接口/抽象类定义规范,结合继承/结构体实现具体逻辑,利用多态特性实现扩展,贴合模式原生设计。

  • 纯C语言:通过结构体嵌套+函数指针模拟面向对象的继承和多态,核心是封装行为和创建逻辑,满足底层开发的需求。

在实际开发中,选择工厂方法模式的关键的是“判断产品是否需要扩展”:若产品固定不变,简单工厂模式更简洁;若产品需灵活扩展,工厂方法模式是更优选择。同时,需权衡系统复杂度与扩展性,避免过度设计——无需为了使用设计模式而强行引入分层,适合的场景才是最好的。

引言

在软件开发中,单例模式(Singleton Pattern) 是一种常见的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点。它适用于需要严格控制资源访问的场景,例如数据库连接池、配置管理器或任务调度器等。本文将详细介绍单例模式的核心思想,并展示其在 **C#、Python、Golang 中的实现方式。

单例模式的主要特点包括:

  • 唯一性:类只有一个实例对象
  • 自创建:类自行创建自己的实例
  • 全局访问:提供一个全局访问点来获取该实例

特点

  • 唯一性:类自身负责创建和管理实例。
  • 延迟加载:实例通常在第一次使用时创建(懒汉式)。
  • 线程安全:在多线程环境中需确保实例的唯一性。
  • 不可克隆/序列化:避免通过克隆或反序列化创建新实例。

单例模式的实现方式

C# 实现

C# 中的单例模式通常通过 双重检查锁定(Double-Check Locking) 实现,以确保线程安全和延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public sealed class Singleton
{
// 使用 volatile 保证多线程下的可见性
private static volatile Singleton _instance;
private static readonly object _lock = new object();

// 私有构造函数
private Singleton() { }


public static Singleton GetInstance()
{
// 第一次检查,避免不必要的锁定
if (_instance == null)
{
// 锁定操作
lock (_lock)
{
// 第二次检查,确保多线程安全
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}

饿汉式(立即加载)

1
2
3
4
5
6
7
8
9
10
11
12
13
public sealed class Singleton
{
// 静态初始化,CLR保证线程安全
private static readonly Singleton _instance = new Singleton();

// 私有构造函数
private Singleton() { }

public static Singleton GetInstance()
{
return _instance;
}
}

Python 实现

Python 的模块天然支持单例,但也可以通过类实现。以下是一个线程安全的懒汉式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import threading

class Singleton:
_instance_lock = threading.Lock() # 线程锁

def __init__(self):
# 初始化逻辑
pass

def __new__(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with cls._instance_lock: # 确保线程安全
if not hasattr(Singleton, "_instance"):
Singleton._instance = super().__new__(cls)
return Singleton._instance

# 使用示例
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # 输出: True

饿汉式(模块级单例)

1
2
3
4
5
6
7
8
9
# singleton.py
class Singleton:
def __init__(self):
pass

instance = Singleton()

# 使用示例
from singleton import instance

装饰器实现

1
2
3
4
5
6
7
8
9
10
11
def singleton(cls): 
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper

@singleton
class MySingleton:
pass

Golang 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"sync"
)

type Singleton struct{}

var (
instance *Singleton
once sync.Once
)

func GetInstance() *Singleton {
// sync.Once 确保代码只执行一次,线程安全
once.Do(func() {
instance = &Singleton{}
})
return instance
}

// 使用示例
func main() {
s1 := GetInstance()
s2 := GetInstance()
println(s1 == s2) // 输出: true
}

饿汉式

1
2
3
4
5
6
7
8
9
package main

type Singleton struct{}

var instance = &Singleton{}

func GetInstance() *Singleton {
return instance
}

单例模式的优缺点

优点

  • 控制实例数量:确保全局唯一性,避免资源浪费。
  • 灵活扩展:可通过子类化或组合模式扩展功能。
  • 全局访问:简化了对共享资源的访问。

缺点

  • 违反单一职责原则:类负责管理自己的实例,增加了耦合。
  • 测试困难:全局状态可能导致单元测试难以隔离。
  • 生命周期管理:实例与程序生命周期一致,可能占用过多内存。.

应用场景

  • 资源管理器:如文件系统、数据库连接池。
  • 配置中心:全局配置对象,避免重复加载配置。
  • 缓存服务:单点缓存,减少内存开销。
  • 日志记录器:统一日志输出,避免多线程冲突。

总结

单例模式是一种简单但强大的设计模式,适用于需要严格控制实例数量的场景。不同编程语言的实现方式各有特色:

  • C# 通过 lock 和 volatile 保证线程安全。
  • Python 可利用模块的天然单例特性。
  • Golang 使用 sync.Once 实现原子初始化。
  • C/C++ 通过静态局部变量或互斥锁实现线程安全。

实现要点总结

  1. 私有构造函数:防止外部直接实例化
  2. 静态实例变量:保存唯一的实例
  3. 全局访问点:提供获取实例的静态方法
  4. 线程安全:在多线程环境下需要考虑线程安全问题

选择建议

  • 懒汉式:适用于实例创建开销较大,且可能不被使用的场景
  • 饿汉式:适用于实例创建开销小,且一定会被使用的场景
  • 双重检查锁定:适用于需要兼顾性能和线程安全的场景

在实际开发中,需根据语言特性和具体需求选择合适的实现方式,同时注意避免过度使用单例模式,以免引入全局状态带来的复杂性。

面向对象的设计原则

写代码也是有原则的,我们之所以使用设计模式,主要是为了适应变化,提高代码复用率,使软件更具有可维护性和可扩展性。如果我们能更好的理解这些设计原则,对我们理解面向对象的设计模式也是有帮助的,因为这些模式的产生是基于这些原则的。这些规则是:单一职责原则(SRP)、开放封闭原则(OCP)、里氏代替原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、合成复用原则(CRP)和迪米特原则(LoD)。下面我们就分别介绍这几种设计原则。

  1. 单一职责原则(SRP):

    1. SRP(Single Responsibilities Principle)的定义:就一个类而言,应该仅有一个引起它变化的原因。简而言之,就是功能要单一。
    2. 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其它职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。(敏捷软件开发)
    3. 软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。

    小结:单一职责原则(SRP)可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,引起它变化的原因就越多,这样就会导致职责依赖,大大损伤其内聚性和耦合度。

  2. 开放关闭原则(OCP)

    1. OCP(Open-Close Principle)的定义:就是说软件实体(类,方法等等)应该可以扩展(扩展可以理解为增加),但是不能在原来的方法或者类上修改,也可以这样说,对增加代码开放,对修改代码关闭。
    2. OCP的两个特征: 对于扩展(增加)是开放的,因为它不影响原来的,这是新增加的。对于修改是封闭的,如果总是修改,逻辑会越来越复杂。

    小结:开放封闭原则(OCP)是面向对象设计的核心思想。遵循这个原则可以为我们面向对象的设计带来巨大的好处:可维护(维护成本小,做管理简单,影响最小)、可扩展(有新需求,增加就好)、可复用(不耦合,可以使用以前代码)、灵活性好(维护方便、简单)。开发人员应该仅对程序中出现频繁变化的那些部分做出抽象,但是不能过激,对应用程序中的每个部分都刻意地进行抽象同样也不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。

  3. 里氏代替原则(LSP)

    1. LSP(Liskov Substitution Principle)的定义:子类型必须能够替换掉它们的父类型。更直白的说,LSP是实现面向接口编程的基础。

    小结:任何基类可以出现的地方,子类一定可以出现,所以我们可以实现面向接口编程。 LSP是继承复用的基石,只有当子类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而子类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  4. 依赖倒置原则(DIP)

    1. DIP(Dependence Inversion Principle)的定义:抽象不应该依赖细节,细节应该依赖于抽象。简单说就是,我们要针对接口编程,而不要针对实现编程。
    2. 高层模块不应该依赖低层模块,两个都应该依赖抽象,因为抽象是稳定的。抽象不应该依赖具体(细节),具体(细节)应该依赖抽象。

    小结:依赖倒置原则其实可以说是面向对象设计的标志,如果在我们编码的时候考虑的是面向接口编程,而不是简单的功能实现,体现了抽象的稳定性,只有这样才符合面向对象的设计。

  5. 接口隔离原则(ISP)

    1. 接口隔离原则(Interface Segregation Principle, ISP)指的是使用多个专门的接口比使用单一的总接口要好。也就是说不要让一个单一的接口承担过多的职责,而应把每个职责分离到多个专门的接口中,进行接口分离。过于臃肿的接口是对接口的一种污染。
    2. 使用多个专门的接口比使用单一的总接口要好。
    3. 一个类对另外一个类的依赖性应当是建立在最小的接口上的。
    4. 一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
    5. “不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

    小结:接口隔离原则(ISP)告诉我们,在做接口设计的时候,要尽量设计的接口功能单一,功能单一,使它变化的因素就少,这样就更稳定,其实这体现了高内聚,低耦合的原则,这样做也避免接口的污染。

  6. 组合复用原则(CRP)

    1. 组合复用原则(Composite Reuse Principle, CRP)就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分。新对象通过向这些对象的委派达到复用已用功能的目的。简单地说,就是要尽量使用合成/聚合,尽量不要使用继承。
    2. 要使用好组合复用原则,首先需要区分”Has—A”和“Is—A”的关系。 “Is—A”是指一个类是另一个类的“一种”,是属于的关系,而“Has—A”则不同,它表示某一个角色具有某一项责任。导致错误的使用继承而不是聚合的常见的原因是错误地把“Has—A”当成“Is—A”.例如:鸡是动物,这就是“Is-A”的表现,某人有一个手枪,People类型里面包含一个Gun类型,这就是“Has-A”的表现。

    小结:组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏替换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

  7. 迪米特法则(Law of Demeter)

    1. 迪米特法则(Law of Demeter,LoD)又叫最少知识原则(Least Knowledge Principle,LKP),指的是一个对象应当对其他对象有尽可能少的了解。也就是说,一个模块或对象应尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立,这样当一个模块修改时,影响的模块就会越少,扩展起来更加容易。
    2. 关于迪米特法则其他的一些表述有:只与你直接的朋友们通信;不要跟“陌生人”说话。
    3. 外观模式(Facade Pattern)和中介者模式(Mediator Pattern)就使用了迪米特法则。

    小结:迪米特法则的初衷是降低类之间的耦合,实现类型之间的高内聚,低耦合,这样可以解耦。但是凡事都有度,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

DotNetty完全教程(一)

Excerpt

写本系列文章的目的我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。我想,.NET程序…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

写本系列文章的目的

我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。

我想,.NET程序员为什么不能使用这么好的应用程序框架。好在,Azure团队写出了DotNetty,使得.NET程序员也可以迅速的,便捷的搭建一个高性能的网络应用程序,但是,DotNetty并没有多少资料,项目代码中也没有多少注释,这对我们的学习以及使用带来了极大的障碍。

我通过对于Netty的研究,一步步的使用DotNetty来创建应用程序,分析DotNetty实现了哪些,没有实现哪些,实现的有何不同,希望通过最简单的描述,让读者能够了解DotNetty,无论是在工作学习中快速搭建网络应用程序还是通过分析Netty的思想,为自己写的网络库添砖加瓦都是十分有意义的。

本系列文章参考了《Netty实战》,感兴趣的同学可以去看看这本书。

Netty是什么

Netty 是一款用于创建高性能网络应用程序的高级框架。

Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器
和客户端

DotNetty是什么

DotNetty是微软的Azure团队仿造Netty编写的网络应用程序框架。

优点

  1. 关注点分离——业务和网络逻辑解耦;
  2. 模块化和可复用性;
  3. 可测试性作为首要的要求

历史

  1. 阻塞Socket通信特点:
    1. 建立连接要阻塞线程,读取数据要阻塞线程
    2. 如果要管理多个客户端,就需要为每个客户端建立不同的线程
    3. 会有大量的线程在休眠状态,等待接收数据,资源浪费
    4. 每个线程都要占用系统资源
    5. 线程的切换很耗费系统资源
  2. 非阻塞Socket(NIO)特点:
    1. 如图,每个Socket如果需要读写操作,都通过事件通知的方式通知选择器,这样就实现了一个线程管理多个Socket的目的。
    2. 选择器甚至可以在所有的Socket空闲的时候允许线程先去干别的事情
    3. 减少了线程数量导致的资源占用,减少了线程切换导致的资源消耗
  3. Netty特点
    在这里插入图片描述

Netty设计的关键点

异步和事件驱动是Netty设计的关键

核心组件

  • Channel:一个连接就是一个Channel
  • 回调:通知的基础
1
2
3
4
5
6
7
8
9
10
11
12
public class ConnectHandler : SimpleChannelInboundHandler<string>
{
public override void ChannelActive(IChannelHandlerContext context)
{
// 新的连接建立的时候会触发这个回调
base.ChannelActive(context);
}
protected override void ChannelRead0(IChannelHandlerContext ctx, string msg)
{
throw new NotImplementedException();
}
}
  • Future:通知的另一种方式,可以认为ChannelFuture是包装了一系列Channel事件的对象。回调和Future相互补充,相互结合同时也可以理解Future是一种更加精细的回调。

    但是ChannelFuture在DotNetty中被Task取代

  • 事件和ChannelHandler
    ChannelHandler是事件处理器,负责处理入站事件和出站事件。通常每一个事件都由一系列的Handler处理。

本文参考资料以及截图来自《Netty实战》

DotNetty完全教程(七)

Excerpt

ChannelPipeline和ChannelHandleContext介绍ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

ChannelPipeline和ChannelHandleContext

介绍

ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:

ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅是信息可以交互,甚至可以改变其他Handler在Pipeline中的位置。

特性

  1. 每一个Channel都会被分配到一个ChannelPipeline,这种关联是永久性的。在Netty中是关联,在DotNetty中这种关联被进一步的强绑定,变成了一个Channel中存在一个Pipeline。
  2. 对于Pipeline来说,入站口被当作Pipeline的头部,出站口被当作尾部。虽然我们看到有两条线,但是在Pipeline中其实是线性的,在事件传播的时候,如果Pipeline发现这个事件的属性(入站出站)跟下一个Handler不匹配,就会跳过这个Handler,前进到下一个。
  3. 一个Handler可以既作为入站处理器也作为出站处理器。
  4. 修改Pipeline

  5. 为了保证ChannelHandler处理事件的高效性,在Handler中不能有阻塞代码,但是如果遇到了一些阻塞API,就需要用到DefaultEventExecutorGroup,其功能是把这个事件的处理从原先的EventLoop中移除,送到一个专门的执行事件处理器中进行处理,从而不阻塞Pipeline。

ChanelPipeline的事件


我们可以看到fire方法都是调用下一个Handler中的方法,我们可以在合适的时机调用下一个Handler中的方法以实现数据的流动。

这里我们注意一下,Write方法并不会将消息写入Socket中,而是写入消息队列中,等待Flush将数据冲刷下去。

Context的API支持


Pipeline和Context

我们可以发现,Pipeline上也有fire–的方法,Context也有类似的方法,他们的差别在于,Pipeline或者Channel上的这些方法引发的事件流将从Pipeline的头部开始移动,而Context上的方法会让事件从当前Handler开始移动,所以为了更短的事件流,我们应该尽可能的使用Context的方法。

使用ChannelHandlerContext

  1. 获取当前Channel

    1
    2
    IChannelHandlerContext ctx = ...;
    IChannel channel = ctx.Channel
  2. 获取当前pipeline

    1
    2
    3
    4
    5
    6
    7
    // 注意一下在Netty中可以直接通过context获取pipeline,在DotNetty中需要从Channel中获取
    // Netty
    IChannelHandlerContext ctx = ...;
    IChannel channel = ctx.pipeline
    // DotNetty
    IChannel channel = ctx.Channel;
    IChannelPipeline pipeline = channel.Pipeline;
  3. 写入pipeline让事件从尾端开始移动

    1
    2
    3
    4
    IChannel channel = ctx.Channel;
    IChannelPipeline pipeline = channel.Pipeline;
    channel.WriteAndFlushAsync("Hello World!");
    pipeline.WriteAndFlushAsync("Hello World!");

注意,Write是出站事件,他的流动方向是从末尾到头部,这个一定要注意。在pipeline或者channel中写入事件,都是从最末尾开始流动,在Context中写入是从当前Handler中开始移动,这个我们已经在很多地方都说明了这样的不同。

应用

  1. 协议切换
    因为我们可以通过Context获取Pipeline的引用,获取了pipeline之后又可以动态的加载和删除Handler,利用这个特性我们可以实现协议的切换,
  2. 随时随地使用Context
    这里我们补充一个知识,Context和Handler的关系是一对一的,而不是一个Context对应多个Handler,这就让我们可以缓存下Context的引用,在任何时候进行使用,这里的任何时候可以是不同的线程。举个例子就是我们之前写的回声程序是在收到信息之后发送,但是复杂一点我们需要在按下按钮的时候发送一条数据,这时候我们可以在连接之后缓存Context的引用,在按下按钮的时候使用Ctx.Write();方法来发送一条数据。

线程安全

在Netty中,如果想要将一个Handler用于多个Pipeline中,需要标注Shared,同时需要保证线程安全,因为这里可能有多线程的重入问题。

异常处理

  1. 入站异常无论在何时引发,都会顺着Pipeline继续向下流动,如果最后的Handler没有处理,则会被标记为未处理。所以为了处理所有的入站异常,我们可以在pipeline的尾端通过复写ExceptionCaught来处理所有pipeline上的异常。
  2. 在出站Handler中获取异常在Netty中需要使用ChannelFuture以及ChannelPromise这里先不做叙述

DotNetty完全教程(三)

Excerpt

组件介绍ChannelChannel是Socket的封装,提供绑定,读,写等操作,降低了直接使用Socket的复杂性。EventLoop我们之前就讲过EventLoop这里回顾一下:一个 EventLoopGroup 包含一个或者多个 EventLoop;一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;所有由 EventLoop 处理的 I/O 事件都将在它…

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

组件介绍

Channel

Channel是Socket的封装,提供绑定,读,写等操作,降低了直接使用Socket的复杂性。

EventLoop

我们之前就讲过EventLoop这里回顾一下:

  1. 一个 EventLoopGroup 包含一个或者多个 EventLoop;
  2. 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  3. 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  4. 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  5. 一个 EventLoop 可能会被分配给一个或多个 Channel。

ChannelFuture

本身是Channel中消息的回调,在DotNetty中被Task取代。

ChannelHandler

ChannelHandler是处理数据的逻辑容器

ChannelInboundHandler是接收并处理入站事件的逻辑容器,可以处理入站数据以及给客户端以回复。

ChannelPipeline

ChannelPipeline是将ChannelHandler穿成一串的的容器。

需要说明的是:

  1. ChannelInboundHandler只处理入站事件,ChannelOutboundHandler只处理出站事件
  2. ChannelInboundHandler和ChannelOutboundHandler可以注册在同一个ChannelPipeline中

(尝试一下)在 Netty 中,有两种发送消息的方式。你可以直接写到 Channel 中,也可以 写到和 ChannelHandler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 ChannelHandler 开始流动。

编码器和解码器

Netty中内置了一些编码器和解码器,用来进行处理字节流数据,编码器用来将消息编码为字节流,解码器用来将字节流解码为另一种格式(字符串或一个对象)。

需要注意的是,编码器和解码器都实现了ChannelInboundHandler和 ChannelOutboundHandler接口用于处理入站或出站数据。

Bootstrap引导类

  1. Bootstrap用于引导客户端,ServerBootstrap用于引导服务器
  2. 客户端引导类只需要一个EventLoopGroup服务器引导类需要两个EventLoopGroup。但是在简单使用中,也可以公用一个EventLoopGroup。为什么服务器需要两个EventLoopGroup呢?是因为服务器的第一个EventLoopGroup只有一个EventLoop,只含有一个SeverChannel用于监听本地端口,一旦连接建立,这个EventLoop就将Channel控制权移交给另一个EventLoopGroup,这个EventLoopGroup分配一个EventLoop给Channel用于管理这个Channel。

DotNetty完全教程(九)

Excerpt

引导Bootstrap引导一个应用程序是指对他进行配置并且使他运行的过程。体系结构注意,DotNetty没有实现Cloneable的接口,而是直接实现了一个Clone方法。Netty实现这个接口是为了创建两个有着相同配置的应用程序,可以把一个配置整体应用到另一个上面,需要注意的是EventLoopGroup是一个浅拷贝,这就导致了拷贝的Bootstrap都会使用同一个EventLoopGr…


引导Bootstrap

引导一个应用程序是指对他进行配置并且使他运行的过程。

体系结构


注意,DotNetty没有实现Cloneable的接口,而是直接实现了一个Clone方法。Netty实现这个接口是为了创建两个有着相同配置的应用程序,可以把一个配置整体应用到另一个上面,需要注意的是EventLoopGroup是一个浅拷贝,这就导致了拷贝的Bootstrap都会使用同一个EventLoopGroup,这在每个Channel生命周期很短的时候是没有太大影响的。

服务器引导和普通引导有什么区别呢?区别在于,服务器接收到客户端的连接请求,会用一个Channel接受连接,然后用另一个Channel与客户端进行交流,但是客户端只需要一个Channel就可以与服务器进行交互。

关于链式调用

我们发现Bootstrap类可以通过流式语法进行链式调用,这要归功于Bootstrap类的特殊定义。下面我们来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义
public abstract class AbstractBootstrap<TBootstrap, TChannel>
where TBootstrap : AbstractBootstrap<TBootstrap, TChannel>
where TChannel : IChannel
// 定义子类
public class Bootstrap : AbstractBootstrap<Bootstrap, IChannel>
// 方法实现
public virtual TBootstrap Group(IEventLoopGroup group)
{
Contract.Requires(group != null);

if (this.group != null)
{
throw new InvalidOperationException("group has already been set.");
}
this.group = group;
return (TBootstrap)this;
}
// 使用
var bootstrap = new Bootstrap();
bootstrap
.Group(group)
.Channel<TcpSocketChannel>()
.Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
pipeline.AddLast(new EchoClientHandler());
}));

API


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KF8h1wnu-1572421399290)(https://ws1.sinaimg.cn/large/007hF5Quly1g1o4mltliij30jr05mabe.jpg)]

客户端引导

1
2
3
4
5
6
7
8
9
10
11
12
13
var group = new MultithreadEventLoopGroup();
var bootstrap = new Bootstrap();
bootstrap
.Group(group)
.Channel<TcpSocketChannel>()
.Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
pipeline.AddLast(new EchoClientHandler());
}));
IChannel clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Parse("10.10.10.158"), 3000));
Console.ReadLine();
await clientChannel.CloseAsync();

服务器引导

API:

注意上面箭头指示的是与Bootstrap不一样的方法。
为什么会有子Channel的概念呢,我们看下面这个图:

因为服务器是一对多的,所以有子Channel的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
IEventLoopGroup eventLoop;
eventLoop = new MultithreadEventLoopGroup();
try
{
// 服务器引导程序
var bootstrap = new ServerBootstrap();
bootstrap.Group(eventLoop);
bootstrap.Channel<TcpServerSocketChannel>();
bootstrap.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
pipeline.AddLast(new EchoServerHandler());
}));
IChannel boundChannel = await bootstrap.BindAsync(3000);
Console.ReadLine();
await boundChannel.CloseAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
await eventLoop.ShutdownGracefullyAsync();
}

从Channel中引导客户端

  • 场景

    如果我们的服务器需要去第三方获取数据,这时候服务器就需要充当客户端去第三方取数据,这时候就需要在Channel中再开一个客户端获取数据。

  • 方式

    我们最好是从Channel中获取当前EventLoop,这样新开的客户端就跟当前Channel在一个线程中,减少了线程切换带来的开销,尽可能的重用了EventLoop

  • 实现

    1
    2
    3
    // 从Context创建客户端引导
    var bootstrap = new Bootstrap();
    bootstrap.Group(ctx.Channel.EventLoop);

初始化Pipeline

如果要添加的Handler不止一个,我们就需要用到ChannelInitializer,在DotNetty中,我们有十分简单的方法可以初始化一个pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var bootstrap = new Bootstrap();
bootstrap
.Group(group)
.Channel<TcpSocketChannel>()
.Option(ChannelOption.TcpNodelay, true)
.Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;

if (cert != null)
{
pipeline.AddLast("tls", new TlsHandler(stream => new SslStream(stream, true, (sender, certificate, chain, errors) => true), new ClientTlsSettings(targetHost)));
}
pipeline.AddLast(new LoggingHandler());
pipeline.AddLast("framing-enc", new LengthFieldPrepender(2));
pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(ushort.MaxValue, 0, 2, 0, 2));

pipeline.AddLast("echo", new EchoClientHandler());
}));

ChannelOption

ChannelOption可以在引导的时候将设置批量的设置到所有Channel上,而不必要在每一个Channel建立的时候手动的去指定它的配置,应用场景是比如设置KeepAlive或者设置超时时间。

1
2
bootstrap.Option(ChannelOption.SoKeepalive, true)
.Option(ChannelOption.ConnectTimeout, new TimeSpan(5000));

面向无连接的用户数据报文

UDP的全称是“User Datagram Protocol”,在DotNetty中实现了SocketDatagramChannel来创建无连接的引导,需要注意的是无连接的引导不需要Connect只需要bind即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
var bootstrap = new Bootstrap();
bootstrap
.Group(group)
.Channel<SocketDatagramChannel>()
.Option(ChannelOption.SoBroadcast, true)
.Handler(new ActionChannelInitializer<IChannel>(channel =>
{
channel.Pipeline.AddLast("Quote", new QuoteOfTheMomentClientHandler());
}));

IChannel clientChannel = await bootstrap.BindAsync(IPEndPoint.MinPort);

关闭

Channel的关闭:

1
await clientChannel.CloseAsync();

EventLoopGroup的关闭:

1
await group.ShutdownGracefullyAsync();

DotNetty完全教程(八)

Excerpt

EventLoop介绍我们先回顾一下,EventLoop就是我们在最开始的示意图中的Selector,每个EventLoop和一个线程绑定,用于处理多个Channel。任务调度如果我们想实现延时任务的调度,比如连接成功5s之后发送一包数据,就可以用到EventLoop的计划任务ctx.Channel.EventLoop.Schedule(() =>{ Console.Wr…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

EventLoop

介绍

我们先回顾一下,EventLoop就是我们在最开始的示意图中的Selector,每个EventLoop和一个线程绑定,用于处理多个Channel。

任务调度

  1. 如果我们想实现延时任务的调度,比如连接成功5s之后发送一包数据,就可以用到EventLoop的计划任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ctx.Channel.EventLoop.Schedule(() =>
    {
    Console.WriteLine("delay 1s");
    }, new TimeSpan(1000));
    // 如果需要提前取消,可以调用Cancel方法
    IScheduledTask task = ctx.Channel.EventLoop.Schedule(() =>
    {
    Console.WriteLine("delay 1s");
    }, new TimeSpan(1000));
    tsak.Cancel();
  2. 一个任务引发后,会判断当前是否在需要处理这个任务的EventLoop中(程序知道自己目前在执行哪个线程,线程又跟EventLoop对应),如果在就直接执行该任务,如果不在该任务中,则任务入队稍后处理

  3. 永远不要把一个需要耗费长时间的任务放到EventLoop执行队列来执行,需要使用我们前面介绍的EventExecutor的方法。

Group

许多Channel对应一个EventLoop,但是EventLoop能分配给她的Channel个数是有限的,要处理可以扩展的无数个Channel就需要EventLoopGroup。他们的结构关系如下图:

我们之前讲过,Netty不仅能够完成NIO系统的搭建,也能通过一些简单的配置,变成OIO阻塞IO系统,阻塞IO的话,就不能多个Channel共享一个EventLoop了,就需要一个Channel分配一个EventLoop。总的来说,EventLoop跟线程的关系是不会改变的。

需要注意的是:

  1. 给Channel分配EventLoop的是EventLoopGroup。而他将尽量均衡的将Channel进行分配。

DotNetty完全教程(二)

Excerpt

第一个DotNetty应用程序准备工作NuGet包介绍DotNetty由九个项目构成,在NuGet中都是单独的包,可以按需引用,其中比较重要的几个是以下几个:DotNetty.Common 是公共的类库项目,包装线程池,并行任务和常用帮助类的封装DotNetty.Transport 是DotNetty核心的实现DotNetty.Buffers 是对内存缓冲区管理的封装DotNett…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

第一个DotNetty应用程序

准备工作

NuGet包介绍

DotNetty由九个项目构成,在NuGet中都是单独的包,可以按需引用,其中比较重要的几个是以下几个:

  • DotNetty.Common 是公共的类库项目,包装线程池,并行任务和常用帮助类的封装
  • DotNetty.Transport 是DotNetty核心的实现
  • DotNetty.Buffers 是对内存缓冲区管理的封装
  • DotNetty.Codes 是对编码器解码器的封装,包括一些基础基类的实现,我们在项目中自定义的协议,都要继承该项目的特定基类和实现
  • DotNetty.Handlers 封装了常用的管道处理器,比如Tls编解码,超时机制,心跳检查,日志等,如果项目中没有用到可以不引用,不过一般都会用到

开始一个项目

  1. 新建一个解决方案
  2. 新建一个项目
  3. 到NuGet中引用 DotNetty.Common DotNetty.Transport DotNetty.Buffers
  4. 开始编写实例代码

编写测试程序

回声测试应用程序编写 源码下载

  1. 新建一个解决方案 名字叫NettyTest

  2. 新建一个项目 名字叫EchoServer

  3. 到NuGet中引用 DotNetty.Common DotNetty.Transport DotNetty.Buffers

  4. 新建一个类 EchoServerHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    using DotNetty.Buffers;
    using DotNetty.Transport.Channels;
    using System;
    using System.Text;

    namespace EchoServer
    {
    /// <summary>
    /// 因为服务器只需要响应传入的消息,所以只需要实现ChannelHandlerAdapter就可以了
    /// </summary>
    public class EchoServerHandler : ChannelHandlerAdapter
    {
    /// <summary>
    /// 每个传入消息都会调用
    /// 处理传入的消息需要复写这个方法
    /// </summary>
    /// <param name="ctx"></param>
    /// <param name="msg"></param>
    public override void ChannelRead(IChannelHandlerContext ctx, object msg)
    {
    IByteBuffer message = msg as IByteBuffer;
    Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
    ctx.WriteAsync(message);
    }
    /// <summary>
    /// 批量读取中的最后一条消息已经读取完成
    /// </summary>
    /// <param name="context"></param>
    public override void ChannelReadComplete(IChannelHandlerContext context)
    {
    context.Flush();
    }
    /// <summary>
    /// 发生异常
    /// </summary>
    /// <param name="context"></param>
    /// <param name="exception"></param>
    public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
    {
    Console.WriteLine(exception);
    context.CloseAsync();
    }
    }
    }

    上面的代码注释已经非常详细了,相信看注释你就能明白这个类大致干了些什么,但是突如其来的一个类还是有点难以理解,那么本着认真负责的精神我会再详细解释一下没有学过Netty的同学难以理解的点:

    1. 问:EchoServerHandler 是干什么用的?回答:Netty帮我们封装了底层的通信过程让我们不需要再关心套接字等网络底层的问题,更加专注于处理业务,何为业务?就是数据来了之后我要怎么办,Handler就是一个处理数据的工厂,那么上面的Handler中我们做了什么事情呢?稍加分析就能发现,我们在接到消息之后打印在了控制台上,之后将消息再发送回去。
    2. 问:WriteAsync 是在干什么?Flush 又是在干什么?答:由于是初学,不灌输太多,大家现在只需要知道数据写入之后并不会直接发出去,Flush的时候才会发出去。
  5. 在自动生成的Program.cs中写入服务器引导程序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    using DotNetty.Transport.Bootstrapping;
    using DotNetty.Transport.Channels;
    using DotNetty.Transport.Channels.Sockets;
    using System;
    using System.Threading.Tasks;

    namespace EchoServer
    {
    public class Program
    {
    static async Task RunServerAsync()
    {
    IEventLoopGroup eventLoop;
    eventLoop = new MultithreadEventLoopGroup();
    try
    {
    // 服务器引导程序
    var bootstrap = new ServerBootstrap();
    bootstrap.Group(eventLoop);
    bootstrap.Channel<TcpServerSocketChannel>();
    bootstrap.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
    {
    IChannelPipeline pipeline = channel.Pipeline;
    pipeline.AddLast(new EchoServerHandler());
    }));
    IChannel boundChannel = await bootstrap.BindAsync(3000);
    Console.ReadLine();
    await boundChannel.CloseAsync();
    }
    catch (Exception ex)
    {
    Console.WriteLine(ex);
    }
    finally
    {
    await eventLoop.ShutdownGracefullyAsync();
    }
    }
    static void Main(string[] args) => RunServerAsync().Wait();
    }
    }

    这个程序中同样有很多需要解释的,但是对于初学者来说,先明白这些概念就好了:

    1. bootstrap是启动引导的意思,Netty中的bootstrap的意思就是启动一个网络应用程序,那在启动之前我们肯定需要设置很多参数,bootstrap可以接收参数,引导用户启动Netty应用。
    2. EventLoopGroup 是一系列EventLoop的集合
    3. EventLoop 就对应了一个选择器(选择器看上一节的图)
    4. 一个Channel都需要绑定到一个选择器(EventLoop)上
    5. 每一个选择器(EventLoop)和一个线程绑定
    6. 我们可以把Handler串起来处理数据,这个我们后面再讲,这里的做法是把Handler串到pipeline上。
  6. 再新建一个项目取名叫EchoClient

  7. 新建一个类 EchoClientHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    using DotNetty.Buffers;
    using DotNetty.Transport.Channels;
    using System;
    using System.Text;

    namespace EchoClient
    {
    public class EchoClientHandler : SimpleChannelInboundHandler<IByteBuffer>
    {
    /// <summary>
    /// Read0是DotNetty特有的对于Read方法的封装
    /// 封装实现了:
    /// 1. 返回的message的泛型实现
    /// 2. 丢弃非该指定泛型的信息
    /// </summary>
    /// <param name="ctx"></param>
    /// <param name="msg"></param>
    protected override void ChannelRead0(IChannelHandlerContext ctx, IByteBuffer msg)
    {
    if (msg != null)
    {
    Console.WriteLine("Receive From Server:" + msg.ToString(Encoding.UTF8));
    }
    ctx.WriteAsync(Unpooled.CopiedBuffer(msg));
    }
    public override void ChannelReadComplete(IChannelHandlerContext context)
    {
    context.Flush();
    }
    public override void ChannelActive(IChannelHandlerContext context)
    {
    Console.WriteLine("发送Hello World");
    context.WriteAndFlushAsync(Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes("Hello World!")));
    }

    public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
    {
    Console.WriteLine(exception);
    context.CloseAsync();
    }
    }
    }

    Handler的编写方法于上面服务器的Handler基本一致,这里我们还是需要解释一些问题:

    1. SimpleChannelInboundHandler 继承自 ChannelHandlerAdapter,前者更强大的地方是对于资源的自动释放(这是一个伏笔)
    2. Read0方法在代码的注释中已经解释过了,有兴趣的同学可以看一下源码。这里我就不贴出来了
    3. ctx.WriteAsync(Unpooled.CopiedBuffer(msg));如果这里直接将msg发送出去,大家就会发现,实验失败了,这是为什么呢?简单解释就是因为引用计数器机制,IByteBuffer只能使用一次,而在我们使用Read0方法接收这个消息的时候,这个消息的引用计数就被归零了,这时候我们再次使用就会报出异常,所以这里需要将源消息再复制一份。当然,如果你使用的Read方法则不会有这样的问题。原则上来说,我们不应该存储指向任何消息的引用供未来使用,因为这些引用都会自动失效(意思就是消息收到了处理完就丢掉,消息不应该被长久保存)。
  8. 编写客户端引导程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    using DotNetty.Transport.Bootstrapping;
    using DotNetty.Transport.Channels;
    using DotNetty.Transport.Channels.Sockets;
    using System;
    using System.Net;
    using System.Threading.Tasks;

    namespace EchoClient
    {
    class Program
    {
    static async Task RunClientAsync()
    {
    var group = new MultithreadEventLoopGroup();
    try
    {
    var bootstrap = new Bootstrap();
    bootstrap
    .Group(group)
    .Channel<TcpSocketChannel>()
    .Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
    {
    IChannelPipeline pipeline = channel.Pipeline;
    pipeline.AddLast(new EchoClientHandler());
    }));
    IChannel clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Parse("10.10.10.158"), 3000));
    Console.ReadLine();
    await clientChannel.CloseAsync();
    }
    catch (Exception ex)
    {
    Console.WriteLine(ex);
    }
    finally
    {
    await group.ShutdownGracefullyAsync();
    }
    }
    static void Main(string[] args) => RunClientAsync().Wait();
    }
    }

写在最后

项目的完整代码我放在了码云上,你可以点击这里可以下载。我相信很多完全没有接触过Netty的同学在跟着写完了第一个项目之后还是很懵,虽然解释了很多,但是还是感觉似懂非懂,这很正常。就如同我们写完HelloWorld之后,仍然会纠结一下static void Main(string[] args)为什么要这么写。我要说的是,只要坚持写完了第一个应用程序,你就是好样的,关于Netty我们还有很多很多要讲,相信你学了之后的知识以后,回过头来再看这个实例,会有恍然大悟的感觉。如果你坚持看完了文章并且敲了程序并且试验成功了,恭喜你,晚饭加个鸡腿,我们还有很多东西要学。

0%