【计算机网络】Linux环境中的网络套接字编程

简介: 【计算机网络】Linux环境中的网络套接字编程

前言

本编文章是博主学习了网络套接字编程后对相关知识的总结,阅读本文可对网络编程有基本的了解。文章内容包括认识IP地址和端口号的作用、了解网络字节序等网络编程中的基本概念、掌握Socket API的基本用法,从而能够实现简单的UDP客户端/服务器和TCP客户端/服务器,并且理解TCP服务器建立连接、发送数据、断开连接的基本流程。

一、预备知识

理解源IP地址和目的IP地址

IP地址被用来给Internet上的电脑一个编号。大家日常见到的情况是每台联网的PC上都需要有IP地址,才能正常通信。如果一台电脑上的数据要传输给另一台电脑,那么该电脑对应的IP地址就是源IP地址,而对于的目的电脑的IP地址就是目的IP地址。如果一台电脑与目的电脑进行通信,就必须知道目的IP地址,才能将信息发送给正确的对象。

认识端口号

知道了目的IP并不能建立双方之间的通信,两台主机通信的目的不是为了将数据发送给对方,而是为了访问目的主机的某一个访问。而目的主机上通常不仅仅只有一个服务,因此就引入了端口号进行区分。


所谓的端口,就好像是门牌号一样,客户端可以通过IP地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。

端口号的作用

端口号的主要作用是表示一台计算机中的特定进程所提供的服务。网络中的计算机是通过IP地址来代表其身份的,它只能表示某台特定的计算机,但是一台计算机上可以同时提供很多个服务,如数据库服务、FTP服务、Web服务等,我们就通过端口号来区别相同计算机所提供的这些不同的服务,如常见的端口号21表示的是FTP服务,端口号23表示的是Telnet服务端口号25指的是SMTP服务等。端口号一般习惯为4位整数,在同一台计算机上端口号不能重复,否则,就会产生端口号冲突这样的例外。

端口号的使用规则

TCP与UDP段结构中端口地址都是16比特,可以有在 0 ~ 65535 范围内的端口号。对于这65536个端口号有以下的使用规定:

  1. 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1 ~ 1023之间的端口号,是由ICANN来管理的;端口号从1024—49151是被注册的端口,也成为“用户端口”,被IANA指定为特殊服务使用;
  2. 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号;
  3. 大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。

认识UDP协议和TCP协议

在TCP/IP网络体系结构中,TCP(传输控制协议,Transport Control Protocol)、UDP(用户数据报协议,User Data Protocol)是传输层最重要的两种协议,为上层用户提供级别的通信可靠性。

传输控制协议(TCP)


TCP(传输控制协议)定义了两台计算机之间进行可靠的传输而交换的数据和确认信息的格式,以及计算机为了确保数据的正确到达而采取的措施。协议规定了TCP软件怎样识别给定计算机上的多个目的进程如何对分组重复这类差错进行恢复。协议还规定了两台计算机如何初始化一个TCP数据流传输以及如何结束这一传输。TCP最大的特点就是提供的是面向连接、可靠的字节流服务。

用户数据报协议(UDP):

UDP(用户数据报协议)是一个简单的面向数据报的传输层协议。不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。因此报文可能会丢失、重复以及乱序等。但由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。UDP最大的特点就是提供的是非面向连接的、不可靠的数据流传输。

了解网络字节序

通过以前对C语言的学习,我们知道了内存中的多字节数据相对于内存地址有大端和小端之分(大端和小端),磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。当然,网络数据流中也同样有大小端之分。

以下是对网络数据流的地址定义

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机从网络中接收到的数据依次保存到缓冲区中,也同样按照内存地址从低到高的顺序保存;
  • 由此可以保证网络数据流的地址都是:先发出的数据在低地址处,后发出的数据在高地址处;
  • TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节;
  • 不管当前主机是大端机还是小端机,都会按照TCP/IP协议所规定的网络字节序进行接收就发送数据;
  • 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

数据采用大小端写入内存的区别:

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转

换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host;n表示network;l表示32位长整数;s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

二、socket 套接字

socket 常见API

#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket等。然而,各种网络协议的地址格式并不相同。

sockaddr 和 sockaddr_in

struct sockaddrstruct sockaddr_in 这两个结构体用来处理网络通信的地址。

1、sockaddr

sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了。其结构如下:

/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */
#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family
/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);  /* Common data: address family and length.  */
    char sa_data[14];   /* Address data.  */
  };
struct sockaddr {  
     sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
   }; 

2、sockadd_in

sockaddr_in在头文件#include<netinet/in.h>#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把portaddr 分开储存在两个变量中,其结构定义如下:


/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;     /* Port number.  */
    struct in_addr sin_addr;    /* Internet address.  */
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
         - __SOCKADDR_COMMON_SIZE
         - sizeof (in_port_t)
         - sizeof (struct in_addr)];
  };

sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。


虽然socket API中的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP地址。


3、sockaddr 和 sockaddr_in的结构

  • IPv4和IPv6的地址格式定义在头文件netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6, 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API 可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in, 这样的好处是程序的通用性,既可以接收IPv4、IPv6、也可以接收UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

4、总结

二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。


sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。

sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数。即sockaddr_in用于socket定义和赋值;而sockaddr用于函数参数。

用法举例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char **argv)
{
    int sockfd = 0;
    struct sockaddr_in addr_in;
    struct sockaddr * addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);  //获得fd
    bzero(&addr_in,sizeof(addr_in));  // 初始化结构体
    /*
     8008的主机字节序  小端字节序 0001 1111 0100 1000 = 8008
     8008的网络字节序  大端字节序 0100 1000 0001 1111 = 18463
    */
    addr_in.sin_port = htons(8008);
    addr_in.sin_family = AF_INET;  // 设置地址家族
    addr_in.sin_addr.s_addr = inet_addr("192.168.3.30");  //设置地址
    printf("sockaddr_in.sin_addr.s_addr = %d \n", addr_in.sin_addr.s_addr);
    printf("addr = %s \n", inet_ntoa(addr_in.sin_addr));
//    addr_in.sin_addr.s_addr = htonl(INADDR_ANY);  //设置地址
    printf("struct sockaddr size = %ld \n", sizeof (addr));
    printf("struct sockaddr_in size = %ld \n", sizeof (addr_in));
    addr = (struct sockaddr *)&addr_in;
//    bind(sockfd, (struct sockaddr *)&addr_in, sizeof(struct sockaddr));  /* bind的时候进行转化 */
    bind(sockfd, addr, sizeof(struct sockaddr));
    ... ...
    return 0;
}

其中inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr的初始化。inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。

三、UDP Socket编程

这里我们使用UDP套接字编程实现一个简单的英译汉服务器。

封装UdpSocket

udpSocket.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class udpSocket
{
public:
    udpSocket() : _fd(-1) {}
    ~udpSocket(){}
public:
    // 创建套接字
    bool Socket()
    {
        _fd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET表示采用IPv4, SOCK_DGRAM表示采用udp协议
        if (_fd < 0)
        {
            std::cerr << "create socket error!" << std::endl;
            return false;
        }
        return true;
    }
    // 关闭套接字
    bool Close()
    {
        close(_fd);
        return true;
    }
    // Bind套接字相关信息
    bool Bind(const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;
        // 填充协议家族
        addr.sin_family = AF_INET;
        // 填写端口号,port会通过网络发送给客户端
        addr.sin_port = htons(port);
        // 服务器必须有IP地址,“xx.yy.zz.aaa”,字符串风格的点分十进制 -》4字节IP -> uint32_t IP
        // INADDR_ANY(0): 不关心会bind到哪个IP,bind任意IP,服务器一般的做法
        // inet_addr指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
        addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str());
        // bind 网络信息
        // struct sockaddr_in填充信息, struct sockaddr充当函数参数
        if (bind(_fd, (const sockaddr *)&addr, sizeof addr) == -1)
        {
            std::cerr << "bind error!" << std::endl;
            return false;
        }
        return true;
    }
    // 接收数据
    bool RecvFrom(std::string *buf, std::string *ip = nullptr, uint16_t *port = nullptr)
    {
        char inbuf[1024 * 10];
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 将接收的数据读入到inbuf缓冲区中,并且获取客户端的sockaddr_in信息
        ssize_t read_size = recvfrom(_fd, inbuf, sizeof(inbuf) - 1, 0, (sockaddr *)&peer, &len);
        if (read_size < 0)
        {
            std::cerr << "recvfrom error!" << std::endl;
            return false;
        }
        // 将缓冲区中的内容放到输出型参数中
        buf->assign(inbuf, read_size);
        if (ip != nullptr)
        {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != nullptr)
        {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    // 发送数据
    bool SendTo(const std::string &buf, const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str());
        //发送数据给客户端
        ssize_t write_size = sendto(_fd, buf.c_str(), buf.size(), 0, (const sockaddr *)&addr, sizeof(addr));
        if (write_size < 0)
        {
            std::cerr << "sendto error!" << std::endl;
            return false;
        }
        return true;
    }
private:
    int _fd;
};

实现UDP通用服务器

udpServer.hpp

#pragma once
#include "udpSocket.hpp"
// C++11 能够兼容函数指针、仿函数和Lambda表达式
#include <functional>
typedef std::function<void (const std::string &, std::string* resp)> Handler;
class udpServer
{
public:
    udpServer()
    {
        assert(_sock.Socket());
    }
    ~udpServer()
    {
        assert(_sock.Close());
    }
public:
    bool Start(const std::string &ip, uint16_t port, Handler handler)
    {
        // 绑定IP 和 端口号
        if (!_sock.Bind(ip, port))
        {
            return false;
        }
        while (true)
        {
            // 尝试读取请求
            std::string req;
            std::string client_ip;
            uint16_t client_port;
            if (!_sock.RecvFrom(&req, &client_ip, &client_port))
            {
                continue;
            }
            std::string resq;
            //根据请求获得响应
            handler(req, &resq);
            //返回相应给客户端
            _sock.SendTo(resq, client_ip, client_port);
            //debug
            printf("[%s:%d] req: %s, resq: %s\n", client_ip.c_str(), client_port, req.c_str(), resq.c_str());
        }
        _sock.Close();
        return true;
    }
private:
    udpSocket _sock;
};

实现英译汉服务器

dict_server.cpp

#include <iostream>
#include <string>
#include <unordered_map>
#include "udpServer.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
    auto it = g_dict.find(req);
    if (it == g_dict.end())
    {
        std::cout << "未查找到!" << std::endl;
        return;
    }
    *resp = it->second;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage: ./dict_server [ip] [port]\n");
        return 1;
    }
    // 数据初始化
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "最好的编程语言"));
    g_dict.insert(std::make_pair("byte", "字节"));
    // 启动服务器
    udpServer().Start(argv[1], atoi(argv[2]), Translate);
    return 0;
}

实现UDP通用客户端

udpClient.hpp

#pragma once
#include "udpSocket.hpp"
class udpClient
{
public:
    udpClient(const std::string &ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        assert(_sock.Socket());
    }
    ~udpClient() { _sock.Close(); }
public:
    bool RecvFrom(std::string* buf)
    {
        return _sock.RecvFrom(buf);
    }
    bool SendTo(const std::string& buf)
    {
        return _sock.SendTo(buf, _ip, _port);
    }
private:
    udpSocket _sock;
    // 服务器的IP和端口号
    std::string _ip;
    uint16_t _port;
};

实现英译汉客户端

client.cpp

#include <iostream>
#include <string>
#include "udpClient.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage: ./dict_server [ip] [port]\n");
        return 1;
    }
    udpClient client(argv[1], atoi(argv[2]));
    while(true)
    {
        std::string word;
        std::cout << "请输入您要查找的单词:";
        if(!(std::cin >> word))
        {
            std::cout << "end..." << std::endl;
            break;
        }
        client.SendTo(word);
        std::string res;
        client.RecvFrom(&res);
        std::cout << word << "的意思是:" << res << std::endl;
    }
    return 0;
}

运行结果:

四、地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。

字符串转in_addr的函数

#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family, const char* strptr, void* addrptr);


in_addr转字符串的函数

char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);


其中inet_ptoninet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* addrptr

代码样例:

#include <iostream>
#include <cstdio>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
    sockaddr_in addr;
    inet_aton("127.0.0.1", &addr.sin_addr);
    uint32_t* ptr = (uint32_t*)(&addr.sin_addr);
    printf("addr: %x\n", *ptr);
    printf("addr_str: %s\n", inet_ntoa(addr.sin_addr));
    return 0;
}

运行结果:

关于inet_ntoa

inet_ntoa这个函数返回了一个char*类型的指针,很显然是这个函数自己在内部为我们申请了一块内存来保存IP的结果。那么是

否需要调用者手动释放呢?答案是不需要。


在man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,所以这个时候不需要我们手动进行释放。

目录
相关文章
|
1月前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
74 2
|
5天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
49 13
|
29天前
|
监控 安全 网络安全
云计算环境下的网络安全防护策略
在云计算的浪潮下,企业和个人用户纷纷将数据和服务迁移到云端。这种转变带来了便利和效率的提升,同时也引入了新的安全挑战。本文将探讨云计算环境中网络安全的关键问题,并介绍一些实用的防护策略,帮助读者构建更为安全的云环境。
|
29天前
|
缓存 Ubuntu Linux
Linux环境下测试服务器的DDR5内存性能
通过使用 `memtester`和 `sysbench`等工具,可以有效地测试Linux环境下服务器的DDR5内存性能。这些工具不仅可以评估内存的读写速度,还可以检测内存中的潜在问题,帮助确保系统的稳定性和性能。通过合理配置和使用这些工具,系统管理员可以深入了解服务器内存的性能状况,为系统优化提供数据支持。
35 4
|
27天前
|
云安全 监控 安全
云计算环境下的网络安全策略与实践
在数字化时代,云计算已成为企业和个人存储、处理数据的重要方式。然而,随着云服务的普及,网络安全问题也日益凸显。本文将探讨如何在云计算环境中实施有效的网络安全措施,包括加密技术、访问控制、安全监控和应急响应计划等方面。我们将通过具体案例分析,展示如何在实际场景中应用这些策略,以保护云中的数据不受威胁。
|
29天前
|
安全 网络协议 网络安全
【Azure 环境】从网络包中分析出TLS加密套件信息
An TLS 1.2 connection request was received from a remote client application, but non of the cipher suites supported by the client application are supported by the server. The connection request has failed. 从远程客户端应用程序收到 TLS 1.2 连接请求,但服务器不支持客户端应用程序支持的任何密码套件。连接请求失败。
|
1月前
|
关系型数据库 MySQL Linux
Linux环境下MySQL数据库自动定时备份实践
数据库备份是确保数据安全的重要措施。在Linux环境下,实现MySQL数据库的自动定时备份可以通过多种方式完成。本文将介绍如何使用`cron`定时任务和`mysqldump`工具来实现MySQL数据库的每日自动备份。
98 3
|
1月前
|
监控 关系型数据库 MySQL
Linux环境下MySQL数据库自动定时备份策略
在Linux环境下,MySQL数据库的自动定时备份是确保数据安全和可靠性的重要措施。通过设置定时任务,我们可以每天自动执行数据库备份,从而减少人为错误和提高数据恢复的效率。本文将详细介绍如何在Linux下实现MySQL数据库的自动定时备份。
45 3
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
119 1
|
1月前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现