【Linux】UDP

【Linux】UDP

一、UDP核心特性与编程基础

1.1 UDP协议关键特性

  • 无连接:通信前无需建立连接,直接发送数据,减少连接开销。
  • 不可靠传输:不保证数据的到达顺序、完整性,可能丢失或重复。
  • 面向数据报:数据以"数据包"为单位传输,每个数据包有明确边界,不会粘包。
  • 全双工:单个Socket可同时读写,支持双向通信。

1.2 UDP核心编程API

UDP编程依赖4个核心系统调用,与TCP不同,无需listenaccept等连接相关接口:

接口 功能描述
socket() 创建UDP Socket,类型指定为SOCK_DGRAM
bind() 绑定IP和端口(服务器必须显式绑定,客户端通常由系统自动绑定随机端口)。
recvfrom() 接收数据,同时获取发送方的IP和端口。
sendto() 发送数据,需指定接收方的IP和端口。

1.3 关键问题:客户端需要显式bind吗?

  • 结论 :不需要。客户端首次调用sendto()时,操作系统会自动分配一个随机端口并绑定到Socket,避免端口冲突(多客户端同时连接时,随机端口保证唯一性)。
  • 服务器必须显式bind:服务器端口是"众所周知"的(如8080),客户端需通过固定端口找到服务器,因此必须手动绑定。

二、V1版本:基础Echo服务器(数据回显)

Echo服务器是最基础的UDP应用,核心功能是"接收客户端数据并原样返回",适合入门UDP编程流程。

2.1 核心代码结构

2.1.1 工具类:禁止拷贝(nocopy.hpp)

避免Socket对象被拷贝导致的资源泄漏:

cpp 复制代码
#pragma once
class nocopy {
public:
    nocopy() = default;
    nocopy(const nocopy&) = delete;  // 禁用拷贝构造
    nocopy& operator=(const nocopy&) = delete;  // 禁用赋值运算符
    ~nocopy() = default;
};
2.1.2 地址封装类(InetAddr.hpp)

封装struct sockaddr_in,简化IP和端口的转换与打印:

cpp 复制代码
#pragma once
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr {
public:
    InetAddr(struct sockaddr_in& addr) : _addr(addr) {
        _port = ntohs(addr.sin_port);  // 网络字节序→主机字节序
        _ip = inet_ntoa(addr.sin_addr);  // in_addr→点分十进制字符串
    }

    std::string Ip() const { return _ip; }
    uint16_t Port() const { return _port; }
    // 格式化输出:IP:端口(如127.0.0.1:8888)
    std::string PrintDebug() const {
        return _ip + ":" + std::to_string(_port);
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
2.1.3 UDP服务器类(UdpServer.hpp)
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"

const static uint16_t default_port = 8888;
const static int default_fd = -1;
const static int buffer_size = 1024;

class UdpServer : public nocopy {
public:
    UdpServer(uint16_t port = default_port) : _port(port), _sockfd(default_fd) {}

    // 初始化:创建Socket + 绑定端口
    void Init() {
        // 1. 创建UDP Socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            std::cerr << "socket error: " << strerror(errno) << std::endl;
            exit(1);
        }
        std::cout << "socket created, fd: " << _sockfd << std::endl;

        // 2. 绑定IP和端口
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 主机字节序→网络字节序
        local.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡IP

        int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (ret != 0) {
            std::cerr << "bind error: " << strerror(errno) << std::endl;
            exit(2);
        }
        std::cout << "bind success, port: " << _port << std::endl;
    }

    // 启动服务器:循环接收并回显数据
    void Start() {
        char buffer[buffer_size];
        while (true) {
            struct sockaddr_in peer;  // 存储发送方地址
            socklen_t peer_len = sizeof(peer);

            // 3. 接收数据(阻塞等待)
            ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
                                (struct sockaddr*)&peer, &peer_len);
            if (n > 0) {
                buffer[n] = '\0';  // 手动添加字符串结束符
                InetAddr client_addr(peer);
                std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;

                // 4. 回显数据(原样返回)
                sendto(_sockfd, buffer, strlen(buffer), 0,
                       (struct sockaddr*)&peer, peer_len);
            }
        }
    }

private:
    uint16_t _port;  // 服务器端口
    int _sockfd;     // Socket文件描述符
};
2.1.4 UDP客户端(UdpClient.cpp)
cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string& process) {
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 2. 填充服务器地址信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());  // 字符串IP→网络字节序

    // 3. 循环发送并接收数据
    std::string input;
    while (true) {
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 发送数据到服务器
        sendto(sockfd, input.c_str(), input.size(), 0,
               (struct sockaddr*)&server, sizeof(server));

        // 接收服务器回显数据
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                            (struct sockaddr*)&temp, &temp_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

2.2 编译运行与核心要点

编译命令
bash 复制代码
# 编译服务器
g++ -o udp_server UdpServer.cpp -std=c++11
# 编译客户端
g++ -o udp_client UdpClient.cpp -std=c++11
运行步骤
  1. 启动服务器:./udp_server 8888
  2. 启动客户端:./udp_client 127.0.0.1 8888
  3. 客户端输入数据,服务器会原样返回。
关键要点
  • INADDR_ANY:绑定所有网卡IP,服务器可接收来自任意网卡的请求(如本地回环127.0.0.1、局域网IP 192.168.0.100)。
  • 字节序转换:端口和IP需通过htons()inet_addr()等函数转换为网络字节序(大端)。
  • recvfrom()peer_len参数:必须传入有效长度,否则可能导致内存错误。

三、V2版本:字典服务器(业务解耦)

在Echo服务器基础上,扩展为"英译汉字典"功能,核心是实现"网络层"与"业务层"的解耦,提高代码可维护性。

3.1 核心设计思路

  • 网络层:负责数据收发、地址解析,不关心业务逻辑。
  • 业务层:负责字典查询,通过回调函数与网络层交互。
  • 配置文件:字典数据存储在dict.txt,启动时加载到内存。

3.2 核心代码实现

3.2.1 字典类(Dict.hpp)

加载配置文件并提供翻译接口:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>

const std::string sep = ": ";  // 字典分隔符(如"apple: 苹果")

class Dict {
public:
    Dict(const std::string& conf_path) : _conf_path(conf_path) {
        LoadDict();  // 初始化时加载字典
    }

    // 翻译接口:输入英文单词,返回中文释义
    std::string Translate(const std::string& word) {
        auto iter = _dict.find(word);
        return iter != _dict.end() ? iter->second : "Unknown word";
    }

private:
    // 加载字典文件到哈希表
    void LoadDict() {
        std::ifstream in(_conf_path);
        if (!in.is_open()) {
            std::cerr << "open dict file failed: " << _conf_path << std::endl;
            return;
        }

        std::string line;
        while (std::getline(in, line)) {
            if (line.empty()) continue;
            size_t pos = line.find(sep);
            if (pos == std::string::npos) continue;  // 跳过格式错误行
            std::string key = line.substr(0, pos);
            std::string value = line.substr(pos + sep.size());
            _dict.emplace(key, value);
        }

        in.close();
        std::cout << "dict loaded, size: " << _dict.size() << std::endl;
    }

private:
    std::string _conf_path;  // 字典文件路径
    std::unordered_map<std::string, std::string> _dict;  // 存储单词-释义映射
};
3.2.2 解耦后的UDP服务器(UdpServer.hpp)

通过函数对象(std::function)实现业务回调:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"

// 定义回调函数类型:输入请求,输出响应
using FuncType = std::function<void(const std::string& req, std::string* resp)>;

const static uint16_t default_port = 8888;
const static int default_fd = -1;
const static int buffer_size = 1024;

class UdpServer : public nocopy {
public:
    // 构造时传入业务回调函数
    UdpServer(FuncType func, uint16_t port = default_port)
        : _func(func), _port(port), _sockfd(default_fd) {}

    void Init() {
        // 与V1版本一致:创建Socket + 绑定端口
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            std::cerr << "socket error: " << strerror(errno) << std::endl;
            exit(1);
        }

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0) {
            std::cerr << "bind error: " << strerror(errno) << std::endl;
            exit(2);
        }
    }

    void Start() {
        char buffer[buffer_size];
        while (true) {
            struct sockaddr_in peer;
            socklen_t peer_len = sizeof(peer);

            // 接收客户端请求
            ssize_t n = recvfrom(_sockfd, buffer, buffer_size - 1, 0,
                                (struct sockaddr*)&peer, &peer_len);
            if (n > 0) {
                buffer[n] = '\0';
                InetAddr client_addr(peer);
                std::cout << "[" << client_addr.PrintDebug() << "]# " << buffer << std::endl;

                // 调用业务回调函数处理请求
                std::string resp;
                _func(buffer, &resp);

                // 发送响应给客户端
                sendto(_sockfd, resp.c_str(), resp.size(), 0,
                       (struct sockaddr*)&peer, peer_len);
            }
        }
    }

private:
    FuncType _func;    // 业务回调函数
    uint16_t _port;   // 服务器端口
    int _sockfd;      // Socket文件描述符
};
3.2.3 服务器主函数(Main.cpp)

初始化字典和服务器,绑定业务逻辑:

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"

// 全局字典对象(启动时加载)
Dict g_dict("./dict.txt");

// 业务处理函数:调用字典翻译
void TranslateFunc(const std::string& req, std::string* resp) {
    *resp = g_dict.Translate(req);
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    // 创建服务器,绑定业务回调函数
    std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(TranslateFunc, port);
    server->Init();
    server->Start();

    return 0;
}
3.2.4 字典配置文件(dict.txt)
txt 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
happy: 快乐的
hello: 你好
goodbye: 再见

3.3 核心要点与扩展

运行效果

客户端输入英文单词(如apple),服务器返回中文释义(苹果);输入未知单词,返回Unknown word

关键亮点
  • 业务解耦:网络层与业务层通过回调函数分离,若需替换业务(如改为天气查询),只需修改回调函数,无需改动服务器核心代码。
  • 配置化设计:字典数据存储在文件中,无需修改代码即可更新词汇。
扩展方向
  • 支持中文到英文的反向翻译。
  • 增加单词联想功能(前缀匹配)。
  • 优化字典加载(支持大文件分片加载)。

四、V3版本:多人聊天室(消息转发+多线程)

聊天室是UDP的典型应用,核心功能是"消息转发"------服务器接收客户端消息后,广播给所有在线用户。需解决用户管理、多线程通信、消息路由等问题。

4.1 核心设计架构

  • 用户管理:维护在线用户列表(存储IP和端口)。
  • 消息路由:接收客户端消息,转发给所有在线用户。
  • 多线程客户端:客户端分"读线程"(接收广播消息)和"写线程"(发送消息),支持同时读写。
  • 线程池优化:服务器用线程池处理消息转发,提高并发能力。

4.2 核心代码实现

4.2.1 路由类(Route.hpp)

管理在线用户和消息转发逻辑:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"

class Route {
public:
    // 检查用户是否已在线
    bool IsOnline(const InetAddr& user) {
        for (const auto& u : _online_users) {
            if (u.Ip() == user.Ip() && u.Port() == user.Port()) {
                return true;
            }
        }
        return false;
    }

    // 添加在线用户(首次发消息即登录)
    void AddUser(const InetAddr& user) {
        if (!IsOnline(user)) {
            _online_users.push_back(user);
            std::cout << "user login: " << user.PrintDebug() << std::endl;
        }
    }

    // 移除在线用户(发送"QUIT"退出)
    void RemoveUser(const InetAddr& user) {
        for (auto iter = _online_users.begin(); iter != _online_users.end(); ++iter) {
            if (iter->Ip() == user.Ip() && iter->Port() == user.Port()) {
                _online_users.erase(iter);
                std::cout << "user logout: " << user.PrintDebug() << std::endl;
                break;
            }
        }
    }

    // 消息转发:向所有在线用户发送消息
    void ForwardMessage(int sockfd, const std::string& msg, const InetAddr& sender) {
        AddUser(sender);  // 未在线则登录

        // 构建消息:发送方IP:端口# 消息内容
        std::string broadcast_msg = sender.PrintDebug() + "# " + msg;

        // 转发给所有在线用户
        for (const auto& user : _online_users) {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(user.Port());
            addr.sin_addr.s_addr = inet_addr(user.Ip().c_str());

            sendto(sockfd, broadcast_msg.c_str(), broadcast_msg.size(), 0,
                   (struct sockaddr*)&addr, sizeof(addr));
        }

        // 发送"QUIT"则退出
        if (msg == "QUIT") {
            RemoveUser(sender);
        }
    }

private:
    std::vector<InetAddr> _online_users;  // 在线用户列表
};
4.2.2 多线程客户端(UdpClient.cpp)

分读、写线程,支持同时接收和发送消息:

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"

// 线程参数:Socket fd + 服务器地址
struct ThreadData {
    int sockfd;
    InetAddr server_addr;
};

// 读线程:接收服务器广播消息
void* RecvThread(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    char buffer[4096];
    while (true) {
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        ssize_t n = recvfrom(data->sockfd, buffer, sizeof(buffer) - 1, 0,
                            (struct sockaddr*)&temp, &temp_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "\n" << buffer << std::endl;  // 打印广播消息
            std::cout << "Please Enter# ";
            std::flush(std::cout);  // 刷新输入提示
        }
    }
    return nullptr;
}

// 写线程:从标准输入发送消息
void* SendThread(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    std::string input;
    while (true) {
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        // 发送消息到服务器
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(data->server_addr.Port());
        server_addr.sin_addr.s_addr = inet_addr(data->server_addr.Ip().c_str());

        sendto(data->sockfd, input.c_str(), input.size(), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));

        // 输入"QUIT"退出
        if (input == "QUIT") {
            break;
        }
    }
    close(data->sockfd);
    return nullptr;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 创建UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }

    // 初始化服务器地址
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    InetAddr server_addr(server);

    // 初始化线程参数
    ThreadData data;
    data.sockfd = sockfd;
    data.server_addr = server_addr;

    // 创建读、写线程
    pthread_t recv_tid, send_tid;
    pthread_create(&recv_tid, nullptr, RecvThread, &data);
    pthread_create(&send_tid, nullptr, SendThread, &data);

    // 等待线程结束
    pthread_join(send_tid, nullptr);
    pthread_cancel(recv_tid);  // 写线程退出后,终止读线程

    return 0;
}
4.2.3 线程池优化的服务器(Main.cpp)

结合之前实现的线程池,处理消息转发任务:

cpp 复制代码
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"  // 复用之前实现的线程池

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    auto route = std::make_unique<Route>();
    auto thread_pool = ThreadPool<std::function<void()>>::GetInstance();

    // 业务回调:将消息转发任务提交到线程池
    auto forward_func = [&](const std::string& msg, std::string* resp) {
        (void)resp;  // 忽略响应(聊天室无需单独回复)
        // 获取发送方地址(需修改UdpServer,传递InetAddr参数)
        // 此处简化:实际需在UdpServer中扩展,传递sender地址
        InetAddr sender;  // 实际应从recvfrom获取
        thread_pool->Enqueue(std::bind(&Route::ForwardMessage, route.get(), _sockfd, msg, sender));
    };

    // 启动服务器
    std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(forward_func, port);
    server->Init();
    server->Start();

    return 0;
}

4.3 核心要点与运行效果

运行步骤
  1. 启动聊天室服务器:./chat_server 8888
  2. 启动多个客户端:./chat_client 127.0.0.1 8888
  3. 任意客户端输入消息,所有客户端都会收到广播;输入QUIT退出。
关键技术点
  • UDP全双工:单个Socket同时支持读和写,多线程操作无冲突。
  • 用户管理 :通过IP+端口唯一标识用户,首次发消息自动登录,发送QUIT自动退出。
  • 线程池优化:消息转发任务提交到线程池,避免单线程阻塞,提高并发处理能力。

五、UDP编程关键知识点补充

5.1 地址转换函数(避免线程安全问题)

UDP编程中常用IP地址转换函数,需注意线程安全:

函数 功能描述 线程安全
inet_addr() 点分十进制字符串→in_addr_t(网络字节序)
inet_ntoa() in_addr→点分十进制字符串 否(静态缓冲区覆盖)
inet_pton() 支持IPv4/IPv6,字符串→二进制地址
inet_ntop() 支持IPv4/IPv6,二进制地址→字符串 是(用户提供缓冲区)

推荐使用线程安全函数

cpp 复制代码
// inet_pton + inet_ntop 示例
#include <arpa/inet.h>

int main() {
    const char* ip_str = "192.168.0.1";
    struct in_addr addr;

    // 字符串→二进制地址
    inet_pton(AF_INET, ip_str, &addr);

    // 二进制地址→字符串(线程安全)
    char buf[INET_ADDRSTRLEN];
    const char* res = inet_ntop(AF_INET, &addr, buf, sizeof(buf));
    std::cout << "ip: " << res << std::endl;
    return 0;
}

5.2 UDP常见问题与解决方案

问题 原因 解决方案
数据丢失 UDP不可靠传输,网络拥堵或接收缓冲区满 应用层添加确认机制(如ACK)、重传策略
端口冲突 服务器绑定已被占用的端口 选择未使用的端口,或检测端口占用后自动重试
消息乱序 数据包路由路径不同,到达顺序不一致 应用层添加序列号,接收端按序列号重组
缓冲区溢出 接收方处理速度慢,缓冲区被填满 增大接收缓冲区(setsockopt),优化处理逻辑

六、总结与进阶方向

UDP编程的核心是"简单高效",适合对实时性要求高、可容忍少量数据丢失的场景。

进阶学习方向

  1. 应用层可靠性优化:实现基于UDP的可靠传输(如添加序列号、确认、重传)。
  2. 广播与组播 :学习UDP广播(SO_BROADCAST)和组播(IP_ADD_MEMBERSHIP)。
  3. 性能优化 :调整Socket缓冲区大小、使用IO多路复用(select/poll/epoll)处理高并发。
  4. 跨平台兼容 :适配Windows系统的UDP编程接口(WSASocket等)。
相关推荐
大侠课堂2 小时前
ARM Linux内核异常排查指南
linux·arm开发
DeeplyMind2 小时前
Linux Virtio 子系统核心数据结构解析
linux·驱动开发·virtio-gpu
贝塔实验室2 小时前
Altium Designer 6.0 初学教程-如何生成一个集成库并且实现对库的管理
linux·服务器·前端·fpga开发·硬件架构·基带工程·pcb工艺
阿巴~阿巴~3 小时前
TCP服务器实现全流程解析(简易回声服务端):从套接字创建到请求处理
linux·服务器·网络·c++·tcp·socket网络编程
赖small强3 小时前
【Linux C/C++开发】第20章:进程间通信理论
linux·c语言·c++·进程间通信
赖small强3 小时前
【Linux C/C++开发】第24章:现代C++特性(C++17/20)核心概念
linux·c语言·c++·c++17/20
Robpubking3 小时前
elasticsearch 使用 systemd 启动时卡在 starting 状态 解决过程记录
linux·运维·elasticsearch
hlsd#4 小时前
我把自己的小米ax3000t换成了OpenWRT
linux·iot
不想画图4 小时前
Linux——web服务介绍和nginx编译安装
linux·nginx