Python实现搭建-简单服务器教程

Posted Python学习

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python实现搭建-简单服务器教程相关的知识,希望对你有一定的参考价值。

Python动态服务器网页(需要使用WSGI接口),基本实现步骤如下:
1.等待客户端的链接,服务器会收到一个http协议的请求数据报
2.利用正则表达式对这个请求数据报进行解析(请求方式、提取出文件的环境)
3.提取出文件的环境之后,利用截断取片的方法将文件名转化为模块名称
4.使用m = __import__(),就可以得到返回值为m的模块
5.创建一个env字典:其中包含的是请求方式及文件环境等各种的键值对
6.创建一个新的动态脚本,其中定义了application这个函数,必须包含env和start_response的参数(也是服务器里的调用方法)
7.在这个动态脚本中定义状态码status和响应头headers(注意是字典形式,如Content-Type)
8.然后再调用start_response(status,headers),但是要注意,这个函数在服务器被定义
9.在动态脚本中编写动态执行程序
10.m.appliction的返回值就是回应数据包的body,它的数据头在start_response被整合
11.将数据头与数据body拼接起来,然后发送给客户端,就可显示动态网页

MyWebServer

import socket
import re
import sys
 
from multiprocessing import Process
from MyWebFramework import Application
 
# 设置静态文件根目录
HTML_ROOT_DIR = "./html"
WSGI_PYTHON_DIR = "./wsgipython"
 
class HTTPServer(object):
    """"""
    def __init__(self, application):
        """构造函数, application指的是框架的app"""
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.app = application
 
    def start(self):
        self.server_socket.listen(128)
        while True:
            client_socket, client_address = self.server_socket.accept()
            #print("[%s,%s]用户连接上了" % (client_address[0],client_address[1]))
            print("[%s, %s]用户连接上了" % client_address)
            handle_client_process = Process(target=self.handle_client, args=(client_socket,))
            handle_client_process.start()
            client_socket.close()
 
    def start_response(self, status, headers):
        """
         status = "200 OK"
    headers = [
        ("Content-Type", "text/plain")
    ]
    star
        """
        response_headers = "HTTP/1.1 " + status + "\\r\\n"
        for header in headers:
            response_headers += "%s: %s\\r\\n" % header
 
        self.response_headers = response_headers
 
    def handle_client(self, client_socket):
        """处理客户端请求"""
        # 获取客户端请求数据
        request_data = client_socket.recv(1024)
        print("request data:", request_data)
        request_lines = request_data.splitlines()
        for line in request_lines:
            print(line)
 
        # 解析请求报文
        # \'GET / HTTP/1.1\'
        request_start_line = request_lines[0]
        # 提取用户请求的文件名
        print("*" * 10)
        print(request_start_line.decode("utf-8"))
        file_name = re.match(r"\\w+ +(/[^ ]*) ", request_start_line.decode("utf-8")).group(1)
        method = re.match(r"(\\w+) +/[^ ]* ", request_start_line.decode("utf-8")).group(1)
 
        env = 
            "PATH_INFO": file_name,
            "METHOD": method
        
        response_body = self.app(env, self.start_response)
        response = self.response_headers + "\\r\\n" + response_body
 
        # 向客户端返回响应数据
        client_socket.send(bytes(response, "utf-8"))
        # 关闭客户端连接
        client_socket.close()
 
    def bind(self, port):
        self.server_socket.bind(("", port))
 
def main():
    sys.path.insert(1, WSGI_PYTHON_DIR)
    if len(sys.argv) < 2:
        sys.exit("python MyWebServer.py Module:app")
    # python MyWebServer.py  MyWebFrameWork:app
    module_name, app_name = sys.argv[1].split(":")
    # module_name = "MyWebFrameWork"
    # app_name = "app"
    m = __import__(module_name)
    app = getattr(m, app_name)
    http_server = HTTPServer(app)
    # http_server.set_port
    http_server.bind(8000)
    http_server.start()
 
if __name__ == "__main__":
    main()

MyWebFrameWork

import time
# from MyWebServer import HTTPServer
 
# 设置静态文件根目录
HTML_ROOT_DIR = "./html"
 
class Application(object):
    """框架的核心部分,也就是框架的主题程序,框架是通用的"""
    def __init__(self, urls):
        # 设置路由信息
        self.urls = urls
 
    def __call__(self, env, start_response):
        path = env.get("PATH_INFO", "/")
        # /static/index.html
        if path.startswith("/static"):
            # 要访问静态文件
            file_name = path[7:]
            # 打开文件,读取内容
            try:
                file = open(HTML_ROOT_DIR + file_name, "rb")
            except IOError:
                # 代表未找到路由信息,404错误
                status = "404 Not Found"
                headers = []
                start_response(status, headers)
                return "not found"
            else:
                file_data = file.read()
                file.close()
 
                status = "200 OK"
                headers = []
                start_response(status, headers)
                return file_data.decode("utf-8")
 
        for url, handler in self.urls:
            #("/ctime", show_ctime)
            if path == url:
                return handler(env, start_response)
 
        # 代表未找到路由信息,404错误
        status = "404 Not Found"
        headers = []
        start_response(status, headers)
        return "not found"
 
def show_ctime(env, start_response):
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain")
    ]
    start_response(status, headers)
    return time.ctime()
 
def say_hello(env, start_response):
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain")
    ]
    start_response(status, headers)
    return "hello itcast"
 
def say_haha(env, start_response):
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain")
    ]
    start_response(status, headers)
    return "hello haha"
 
urls = [
            ("/", show_ctime),
            ("/ctime", show_ctime),
            ("/sayhello", say_hello),
            ("/sayhaha", say_haha),
        ]
app = Application(urls)
# if __name__ == "__main__":
#     urls = [
#             ("/", show_ctime),
#             ("/ctime", show_ctime),
#             ("/sayhello", say_hello),
#             ("/sayhaha", say_haha),
#         ]
#     app = Application(urls)
#     http_server = HTTPServer(app)
#     http_server.bind(8000)
#     http_server.start()

CentOS7下安装RabbitMQ,并使用Spring Boot实现一个简单的延迟队列(小白教程,附源码)

笔者在安装RabbitMQ时发现大部分博客记录的安装复杂且版本固定且过时,或复制粘贴缺胳膊少腿;故写下此博客,希望有需要的同学可以少走些弯路。
笔者设备: win10+Linux CentOS7。
笔者RabbitMQ版本3.8.16,Java版本11,Spring Boot版本2.4.5。
使用win10系统去连接服务器不需要安装其它任何软件。
服务器安装使用root用户,使用rpm包。
本文宗旨操作简单化,描述详细化 。
项目源码:https://github.com/MuweiLaw/RabbitMQ.git
项目源码分为两个服务,一个生产者一个消费者,适用于分布式架构。

一.环境搭建

1-安装包和插件准备

1.1在本地新建空文件夹存放rpm安装包

在PC上新建一个文件夹用来存放安装包
例如我在E盘上新建一个空文件夹,路径为E:\\upload
新建空文件夹

1.2下载RabbitMQ

进入 RabbitMQ下载地址,下载RabbitMQ的包。
笔者的Linux版本均为CentOS7,故下载图中蓝框版本
蓝色框上方为CentOS8版本
linux7需要下载的图片截图
点击下载放入到我们刚才建好的文件夹中

1.3下载RabbitMQ的延迟插件

进入延迟插件下载地址,在页面使用Ctrl+F直接搜索rabbitmq_delayed_message_exchange即可找到我们需要下载的插件
插件下载入口
下载和RabbitMQ配套的版本
下载v3.8延迟插件

1.4下载ErLang

RabbitMQ是基于Erlang语言开发,so在安装RabbitMQ之前,需要先安装Erlang,这里使用RabbitM
Q团队提供的Erlang依赖包。
进入Erlang下载页
Erlang下载
点击下载放入到我们刚才建好的文件夹中

2-上传安装包和插件到服务器

2.1上传准备

此时我们已经下载好了3个包并放在了E:\\upload文件夹下
在这里插入图片描述

2.2上传本地文件到服务器

回到电脑桌面
使用快捷键win+R
在弹出的窗口输入cmd
回车后打开windows运行命令行窗口,笔者的字体颜色是绿色(人生在世总要带点绿哈哈哈),大部分同学的字体颜色是白色命令行窗口
输入命令sftp user@host建立sftp连接,user是你的Linux账户名,host是你的Linux系统对外的IP地址。注意,密码不会显示,带有小键盘的同学尽量不要用小键盘。输入密码后回车显示
Connected to user@host.
则表示连接成功。如下:
在这里插入图片描述
连接成功后输入命令lcd E:\\upload
再输入命令lls
就能看到我们之前放在 E:\\upload 里的三个文件
查看本地文件
好了,现在我们开始本地上传这三个文件,put 加上对应的文件名称
put erlang-23.3.2-1.el7.x86_64.rpm
put rabbitmq_delayed_message_exchange-3.8.0.ez
put rabbitmq-server-3.8.16-1.el7.noarch.rpm
在这里插入图片描述
再接着,我们把这三个文件保存到服务器,get 加上对应的文件名称
get erlang-23.3.2-1.el7.x86_64.rpm
get rabbitmq_delayed_message_exchange-3.8.0.ez
get rabbitmq-server-3.8.16-1.el7.noarch.rpm
服务器报存文件
我们使用命令ls查看服务器是否保存这三个文件
使用命令pwd知道服务器当前保存的文件夹为 /root
如下:查看服务器保存文件
最后,我们使用Ctrl+C关闭sftp连接

2.3注意事项

IP查看方式:
1.虚拟机: 使用ifconfig命令
在这里插入图片描述
2.云服务器:在云服务器控制台上找到服务器的公网IP,笔者是一台最低配阿里云ECS服务器,年费用不超过100软妹币,满足日常学习开发使用,其它高配云服务器享受同等折扣,链接戳我直达

查看云服务器IP

第一次使用sftp传输文件回有如下提示
第一次使用sftp

3-安装

3.1使用win10自带ssh连接服务器

使用Xshell,PuTTY,CRT等ssh工具的可以跳过这一步

打开windows运行命令行
输入ssh user@host这里user依旧是你的Linux账户名,host依旧是你的Linux系统对外的IP地址。密码不显示,输入密码时尽量不要使用小键盘。
shh连接图示我们已经通过ssh用root账号连接上了我们的服务器,接下来我们将正式在服务器搭建环境

3.2正式搭建

登录到服务器成功之后
使用 ls 即可查看我们之前通过sftp传输的三个文件。ssh登录后查看文件

依次执行如下命令:

rpm -ivh erlang-23.3.2-1.el7.x86_64.rpm 安装erlang依赖
安装erlang依赖
yum -y install socat 安装socat
安装socat
rpm -ivh rabbitmq-server-3.8.16-1.el7.noarch.rpm 安装RabbitMQ 安装RabbitMQ

接下来我们启动RabbitMQ

使用命令 systemctl start rabbitmq-server 稍等几秒钟,正常启动如下,没有任何提示启动
systemctl enable rabbitmq-server 设置RabbitMQ开机自启
rabbitmq-plugins enable rabbitmq_management 启用web控制台
设置自启和web控制台

开放端口(防火墙已关闭的同学可以跳过这一小步)

firewall-cmd --zone=public --add-port=5672/tcp --permanent 开放客户端端口
firewall-cmd --zone=public --add-port=15672/tcp --permanent 开放web控制台端口
firewall-cmd --reload 重启防火墙
开放端口
需要注意一点的是,使用阿里云服务器的同学还需要配置安全组云服务器配置安全组

尝试登录web控制台

RabbitMQ默认的账号用户名和密码都是guest
在浏览器输入 http://host:15672/ 即可访问web控制台,这里的 host 依旧是你的服务器对外IP。
例如笔者的web控制台的Url为 http://10.1.2.4:15672/
默认情况下,RabbitMQ的默认的guest用户只允许本机访问,当我们在web控制台输入账号密码后会发现如下图所示登录web控制台

所以我们还需要让guest用户能够远程访问

cd /etc/rabbitmq 进入到 etc/rabbitmq 目录下
touch rabbitmq.config 创建名为 rabbitmq.config 的配置文件
创建配置文件
vi rabbitmq.config 编辑配置文件编辑配置文件
按下回车键之后效果如下:注意红框内描述变化编辑中1
此时我们按下键盘 “ i ” 键,进行编辑,注意左下角变化,表示已进入编辑状态
编辑2
输入 [{rabbit, [{loopback_users, []}]}].
编辑3
再按下键盘“ Esc ”键退出编辑模式编辑4键盘依次按下 :wq
再按下回车键后保存并退出,注意此处冒号是英文冒号
编辑5
systemctl restart rabbitmq-server重启RabbitMQ,等待数秒
重启

再次访问web控制台

重新输入guest账号密码即可实现登录在这里插入图片描述

安装延迟插件

cd /usr/lib/rabbitmq/lib 先进入lib目录,
再使用 ls 查看RabbitMQ安装目录名称,因为版本因素,各位同学此处的目录名称可能也存在不同
查看文件夹
好了,知道黄色框内该版本的安装目录名称之后
cd rabbitmq_server-3.8.16/plugins 我们直接进入插件库文件夹内
ls 查看,后缀 .ez 的文件都是RabbitMQ自带的插件查看自带插件
cp /root/rabbitmq_delayed_message_exchange-3.8.0.ez ./ 我们将先前保存在 /root 目录下的延迟插件 rabbitmq_delayed_message_exchange-3.8.0.ez 拷贝到当前目录。
ls 可以查看到,我们的延迟插件已经拷贝到当前文件夹了
查看插件是否拷贝到插件库

现在我们要启用插件了

cd …/sbin 进入sbin目录(注意注意注意!!CSDN的bug,导致此处命令高亮区域显示了三个“ . ”,下图是两个“ . ”,直接复制粘贴时注意)
rabbitmq-plugins enable rabbitmq_delayed_message_exchange 启用插件成功后就可以看到如下信息启用插件信息
再进入到我们的web控制台,哎哟新增了一个交换机类型可选项 x-delayed-messageweb控制台新增交换机类型

环境搭建大功告成!!!

二.代码实现一个简单的订单超时自动取消功能

项目源码:https://github.com/MuweiLaw/RabbitMQ.git

1-代码编辑

笔者用的IDE是intellij IDEA,新建两个Spring Initializr项目
项目1:mq_producer
项目2:mq_consumer

两个项目的结构类似,如下:
项目结构

首先我们需要在两个项目中的pom.xml文件中都添加这3个依赖;

		<!--消息队列相关依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--    JSON支持,消息序列化用到    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
        <!--    测试    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

之后在application.properties添加RabbitMQ的相关配置;

mq_producer:

server.port=52320

spring.application.name=Spring-boot-rabbitmq
spring.rabbitmq.host=10.1.2.4
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#开启confirm 确认机制,如果对异步消息需要回调必须设置为correlated
#spring.rabbitmq.publisher-confirms=true   已过时,下行替代
spring.rabbitmq.publisher-confirm-type=correlated

mq_consumer:

server.port=52321

spring.rabbitmq.host=10.1.2.4
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirm-type=correlated

接下来创建RabbitMQ的Java配置,主要用于配置交换机、队列和绑定关系,两个项目的配置相同,如下。
枚举类

/**
 * 消息队列枚举配置
 *
 * @author Murray
 * @date 2021/5/6 14:30
 */
public enum QueueEnum {
    /**
     * 插件实现延迟队列
     */
    QUEUE_ORDER_PLUGIN_CANCEL("mall.order.direct.plugin", "mall.order.cancel.plugin", "mall.order.cancel.plugin");

    /**
     * 交换名称
     */
    private String exchange;
    /**
     * 队列名称
     */
    private String name;
    /**
     * 路由键
     */
    private String routeKey;

    QueueEnum(String exchange, String name, String routeKey) {
        this.exchange = exchange;
        this.name = name;
        this.routeKey = routeKey;
    }

    public String getExchange() {
        return exchange;
    }

    public String getName() {
        return name;
    }

    public String getRouteKey() {
        return routeKey;
    }
}

RabbitMQ配置类

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ配置类
 *
 * @author Murray
 * @date 2021/5/6 14:24
 */
@Configuration
public class RabbitConfig {

    /**
     * 订单延迟插件消息队列所绑定的交换机
     */
    @Bean
    CustomExchange orderPluginDirect() {
        //创建一个自定义交换机,可以发送延迟消息
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(QueueEnum.QUEUE_ORDER_PLUGIN_CANCEL.getExchange(), "x-delayed-message", true, false, args);
    }

    /**
     * 订单延迟插件队列
     */
    @Bean
    public Queue orderPluginQueue() {
        return new Queue(QueueEnum.QUEUE_ORDER_PLUGIN_CANCEL.getName());
    }

    /**
     * 将订单延迟插件队列绑定到交换机
     */
    @Bean
    public Binding orderPluginBinding(CustomExchange orderPluginDirect, Queue orderPluginQueue) {
        return BindingBuilder
                .bind(orderPluginQueue)
                .to(orderPluginDirect)
                .with(QueueEnum.QUEUE_ORDER_PLUGIN_CANCEL.getRouteKey())
                .noargs();
    }
 	/**
     * 将默认的消息转换器替换成json消息转换器
     */
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

两个项目都需要订单实体类,且包名也得一样

import java.io.Serializable;
import java.util.UUID;

/**
 * 订单实体类
 *
 * @author Murray Law
 * @date 2021/5/8 14:30
 */
public class Order implements Serializable {
    private static final long serialVersionUID = -8432910728496351007L;
    String orderNumber;
    String userName;
    Integer age;
    String remark;

    public Order() {
    }

    public Order(String userName, Integer age, String remark) {
        this.orderNumber = UUID.randomUUID().toString().trim().replaceAll("-", "");
        this.userName = userName;
        this.age = age;
        this.remark = remark;
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }

    @Override
    public String toString() {
        return "Order{" +
                "orderNumber='" + orderNumber + '\\'' +
                ", userName='" + userName + '\\'' +
                ", age=" + age +
                ", remark='" + remark + '\\'' +
                '}';
    }
}

mq_producer 里创建一个 延时消息队列发出者,通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间

import com.murray.mq.producer.config.QueueEnum;
import com.murray.mq.commons.entity.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 订单自动取消的消息发出者
 *
 * @author Murray Law
 * @date  2021/5/6 13:24
 */
@Component
public class CancelOrderSender {
    private static Logger LOGGER = LoggerFactory.getLogger(CancelOrderSender.class);
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMessage(Order order, final long delayTimes) {
        //给延迟队列发送消息
        amqpTemplate.convertAndSend(QueueEnum.QUEUE_ORDER_PLUGIN_CANCEL.getExchange(), QueueEnum.QUEUE_ORDER_PLUGIN_CANCEL.getRouteKey(), order, message -> {
            //给消息设置延迟毫秒值
            message.getMessageProperties().setHeader("x-delay", delayTimes);
            return message;
        });
        LOGGER.info("发送出了一个延时取消订单{}", order);
    }
}

mq_producer 里还需要一个订单服务,在订单生成之后调用延时消息发出者的方法

import com.murray.mq.commons.entity.Order;
import com.murray.mq.producer.sender.CancelOrderSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 前台订单管理Service
 *
 * @author Murray Law
 * @date 2021/5/6 13:26
 */

@Service
public class OrderService {

    @Autowired
    private CancelOrderSender cancelOrderSender;

    public void generateOrder(Order order) {
        //TODO 实际项目中会执行一系类下单操作
        //下单完成后开启一个延迟消息,用于当用户没有付款时取消订单
        sendDelayMessageCancelOrder(order);
    }

    private void sendDelayMessageCancelOrder(Order order) {
        //获取订单超时时间,假设为60分钟(测试用的10秒)
        long delayTimes = 10 * 1000;
        //发送延迟消息
        cancelOrderSender.sendMessage(order, delayTimes);
    }
}

既然我们有了消息发出者和订单发出服务,那我们就需要在 mq_consumer 里创建一个延时消息接收者用于处理订单延迟队列中的消息,还有收到订单被取消消息后的订单取消服务

import com.murray.mq.commons.entity.Order;
import com.murray.mq.consumer.service.CancelOrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 订单自动取消的消息接受者
 *
 * @author Murray Law
 * @date 2021/5/6 13:23
 */
@Component
@RabbitListener(queues = "mall.order.cancel.plugin")
public class CancelOrderReceiver {
    private static Logger LOGGER = LoggerFactory.getLogger(CancelOrderReceiver.class);
    @Autowired
    private CancelOrderService portalOrderService;

    @RabbitHandler
    public void handle(Order order) {
        LOGGER.info("接受到一个订单因超时未支付{}", order);
        portalOrderService.cancelOrder(order);
    }
}
import com.murray.mq.commons.entity.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * 前台订单管理Service
 *
 * @author Murray Law
 * @date 2021/5/6 13:26
 */

@Service
public class CancelOrderService {
    private static Logger LOGGER = LoggerFactory.getLogger(CancelOrderService.class);

    public void cancelOrder(Order order) {
        //TODO 实际项目中执行一系类取消订单操作
        LOGGER.info("已取消订单{}", order);
    }
}

好了,我们启动消息消费端项目 mq_consumer,清空控制台上的启动log,方便我们直观看出新的log记录
启动消费端
清空后Appium+Python3环境搭建,其实超简单!软件测试教程

搭建python简单环境与集成开发环境PyCharm安装教程

Python实用环境pyenv搭建教程

最全Python+Selenium环境搭建教程-你绝对想不到有这么简单!

CentOS7下安装RabbitMQ,并使用Spring Boot实现一个简单的延迟队列(小白教程,附源码)

h5牛牛棋牌大厅搭建 图文教程