Linux 网络套接字解析:实现网络通信

目录
- 一.网络基础
- 1.协议
- 2.OSI与TCP/IP模型
- 3.网络通信流程
- 4.IP与Mac地址
- 二.网络编程套接字
- 1.端口号
- 2.网络字节序
- 3.tcp、udp协议
- 4.socket编程
- 5.sockaddr结构解析
- 6.实现Udp_socket
- 7.实现Windows与Linux通信
- 8.Linux下远程执行指令
- 9.实现tcp_socket
- 10.守护进程
一.网络基础
1.协议
计算机相关的硬件软件很多,这时就需要制定一个标准保障数据的传输,所以网络协议就是是计算机网络中通信双方遵循的约定。本质上来说:协议是通过结构体表达出来的特定的双方都认识的结构体对象。
2.OSI与TCP/IP模型
为了更好的将网络维护起来,OSI将网络模型分成了七层的理论模型,TCP/IP为互联网的实际模型通常为五层(四层)。
TCP/IP模型:
- 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念
- 数据链路层:负责处理硬件层面的通信细节,包括如何在本地网络上发送和接收数据。它涵盖了物理层和数据链路层的功能,管理数据帧的传输、错误检测、以及链路控制。
- 网络层:主要负责数据包的路由和转发,确保数据能够在不同网络之间传输。这一层定义了IP地址的结构和数据包的传输路径。IP协议(IPv4和IPv6)是这一层的核心协议。
- 传输层:提供端到端的数据传输服务,确保数据的完整性和顺序性。传输层负责数据的分段、传输、重组,并根据需要提供错误检测和流量控制。TCP和UDP协议在这一层运行,分别提供可靠的连接型和高效的无连接型数据传输服务。
- 应用层:直接为用户提供网络服务,负责数据的格式化、加密以及应用程序之间的通信。这一层涵盖了OSI模型的应用层、表示层和会话层功能,是用户与网络交互的接口。
网络协议栈示意图:

3.网络通信流程
两主机通过TCP/IP通信过程所示

发送数据在不断的对数据加上报头,发送数据即封装的过程,接受数据是在不断的解包和分用的过程。分用:决定将自己的有效在和交付给上层的哪个协议的能力(就是分用)。传输层的叫数据段,网络层的叫叫数据报,数据链路层叫数据帧,通信的的过程本质就是不断封装和解包的过程,报文 = 报头 + 有效载荷。
局域网通信原理是基于碰撞域和碰撞检测的,交换机会划分碰撞域,局域网可以看成多台主机所共享的临界资源,所以是要保证互斥的。
4.IP与Mac地址
在计算机网络中,IP地址和MAC地址是两种用于标识设备的重要地址类型。
IP地址:
IP地址(是分配给网络中每个设备的逻辑地址,用于在网络层进行通信。IP地址有两种版本:IPv4和IPv6。
- IPv4地址: 由32位二进制数构成,通常表示为四个十进制数(例如,192.168.1.1),每个数值代表8位(一个字节)。
- IPv6地址: 由于IPv4地址枯竭问题,IPv6应运而生,它使用128位地址空间,表示为8组16进制数(例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334)。
Mac地址:
MAC地址网络接口卡(网卡)的唯一标识符,存在于数据链路层。每个网络设备出厂时都会分配一个唯一的MAC地址,通常由硬件制造商确定。MAC地址由48位二进制数构成,6字节
MAC地址用于在同一局域网(LAN)内的设备之间进行通信。因为它是硬件地址,所以在设备移动到不同网络时保持不变。交换机等网络设备利用MAC地址来学习和识别局域网中设备的位置,并相应地转发数据帧。
在网络通信中,当一个设备需要与另一个设备通信时,通常需要知道对方的IP地址。然而,数据帧(数据链路层)的实际传输依赖于MAC地址。这时,ARP(地址解析协议)发挥作用。
二.网络编程套接字
1.端口号
端口号为2字节16位的整数,范围为在0到65535之间,它与IP地址一起构成一个套接字(socket),唯一标识网络中的某个服务。用来标识唯一进程。
那么端口号和进程pid有什么区别呢?,端口号是用来标识网络服务需要用到的进程,不是所有进程都要通信,但是所有进程都必须有pid标识自己。一个端口号可以被多个进程绑定,一个端口号不能绑定多个进程,在公网上:IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程 ,所以用IP和端口号可以标识全网唯一的一个进程。
2.网络字节序
在计算机网络中,数据传输可能会在不同计算机系统上。由于不同系统可能使用不同的字节序),为了确保数据在网络中传输时的正确性,需要统一使用一种标准的字节序。所以诞生了网络字节序:TCP/IP协议规定网络数据流应该采用大端字节序。所以如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
接着主机序列转网络序列的库函数应运而生:
#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代表主机序列转网络序列,n代表网络字节序列转主机序列,其中如果机器是小端字节序会做相应的转换。
3.tcp、udp协议
在Linux网络编程中,TCP(传输控制协议)和UDP(用户数据报协议)是两种最常用的传输层协议。
UDP协议(用户数据报协议):
- 无连接:UDP是无连接的协议。数据传输之前不需要建立连接,因此可以更快地发送数据,但不保证数据的可靠性。
- 不可靠传输:UDP不保证数据包的到达顺序,也不提供数据包的确认或重传机制。如果数据包丢失,UDP不会重发。
- 面向数据报:在计算机网络中,**数据报(Datagram)**是指独立传输的、具有独立完整意义的数据单元。
- 传输层协议
TCP协议(传输控制协议):
- 有连接:TCP是一种面向连接的协议。在数据传输之前,客户端和服务器需要通过三次握手建立连接,确保双方准备好进行通信。
- 面向字节流:TCP中,数据被看作是一个连续的字节流。发送方可以将任意数量的字节发送到接收方,而接收方会以流的形式接收到这些字节。这意味着发送方发送的每一段数据不会被当作一个独立的数据单元,TCP不会将数据拆分成数据报。接收方接收到的数据流是发送方数据流的一个完整视图。
- 可靠传输:TCP提供可靠的数据传输,通过数据包的确认和重传机制来确保数据完整无误。数据包的顺序也会被正确维护。
- 传输层协议
4.socket编程
socket是一种通信机制socket 是应用层与传输层之间的一个接口,通过它,应用程序可以利用底层网络协议来发送和接收数据。网络协议栈中的Socket通常被用来实现TCP/IP协议通信。
socket编程系列函数:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
socket用来创建一个套接字:
- domain:指定协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信)。
- type:指定Socket类型,如 SOCK_STREAM(流式套接字,TCP)、SOCK_DGRAM(数据报套接字,UDP)。
- protocol:指定协议,一般设置为 0,让系统选择合适的协议。
- 成功时返回一个Socket描述符(非负整数),失败时返回 -1 并设置 errno。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind函数在网络编程中用于将一个套接字(socket)绑定到一个特定的地址和端口(struct sockaddr结构体)。
- sockfd:socket函数返回的Socket描述符。
- addr:指向struct sockaddr结构的指针,其中包含要绑定的地址信息。
- addrlen:addr结构的大小。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
listen 函数在网络编程中用于将一个已经绑定到地址和端口的套接字转换为监听状态,以便接受客户端的连接请求。这个函数主要用于服务器端。
- sockfd:socket函数返回的Socket描述符。
- backlog:指定连接请求队列的最大长度。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept 函数在网络编程中用于接受客户端的连接请求。它主要用于服务器端,在一个已经处于监听状态的套接字上调用,用于从等待队列中提取一个连接请求,并返回一个新的套接字描述符用于与客户端进行数据交换。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数在网络编程中用于发起与远程主机的连接。它主要用于客户端程序,试图连接到指定的服务器地址和端口。
- addr:说明:指向一个 struct sockaddr 结构体的指针,包含了远程主机的地址和端口信息。
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom 函数在网络编程中用于从一个套接字接收数据,它特别适用于面向数据报的协议(如 UDP),但也可以用于其他协议。
- sockfd:由 socket 函数创建的套接字描述符。这个套接字应该是用来接收数据的,并且可以是任何支持数据接收的协议(如 UDP)。
- buf:指向一个缓冲区的指针,用于存储接收到的数据。这个缓冲区应该足够大,以容纳预期的数据量。
- len:缓冲区 buf 的大小(以字节为单位)。接收到的数据量不能超过这个大小
- flags:接收操作的标志。通常使用 0,但也可以使用以下标志之一:MSG_OOB:接收带外数据(如果有)。MSG_PEEK:窥视数据,不从队列中移除。MSG_DONTWAIT:非阻塞模式(适用于非阻塞套接字)。
- src_addr:指向一个 struct sockaddr 结构体的指针,用于存储数据源的地址信息。对于 IPv4 通信,通常使用 struct sockaddr_in;对于 IPv6 通信,通常使用 struct sockaddr_in6。这个参数可以为 NULL,如果不需要源地址信息。
- addrlen:指向 socklen_t 类型变量的指针,表示 src_addr 结构体的大小。在调用前应设置为 src_addr 的大小;调用后,它将包含实际的地址长度。可以为 NULL,如果不需要源地址信息。
5.sockaddr结构解析
在网络编程中,sockaddr 结构体是用于表示网络地址的基础结构体。常见变体有sockaddr_in 和 sockaddr_un 的详细解析:

- sockaddr 结构:是一个通用的地址结构体,用于表示网络地址。具体地址信息存储在其派生结构体中。
- sockaddr_in:用于表示 IPv4 地址,包含地址族、端口号和 IPv4 地址。适用于使用 TCP/UDP 协议的通信。
- sockaddr_un:用于表示UNIX 域套接字的本地通信地址,包含地址族和路径名。适用于本地进程间通信。
套接字分为三种:域间套接字(同一机器内)、原始套接字(网络工具)、网络套接字(用户间网络通信)
6.实现Udp_socket
首先我们分别要实现客户端和服务器逻辑,让服务器接受客户端的数据,并且返回给客户端。
首先我们创建服务器类:
class UdpServer
{
private:int _socketid;//网络文件描述符string _ip;//字符串形式ip地址uint16_t _port;//服务器进程的端口号
};
构造函数:
UdpServer(uint16_t port = default_port, string ip = default_ip):_socketid(0),_ip(ip),_port(port){}
绑定ip缺省为0.0.0.0代表允许接收来自任何IP地址的连接请求
创建套接字和绑定sockaddr_in结构体:
void Init(){ //1.创建udp socket _socketid = socket(AF_INET,SOCK_DGRAM,0);if(_socketid <0){ exit(Socket_err);}//2.bind //int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);struct sockaddr_in _obj;bzero(&_obj,sizeof(_obj));_obj.sin_family = AF_INET;_obj.sin_port = htons(_port);_obj.sin_addr.s_addr = inet_addr(_ip.c_str());int ret = bind(_socketid,(struct sockaddr*)&_obj,sizeof(_obj));if(ret<0){exit(Bind_err);}}
其中AF_INET代表IPV4协议,SOCK_DGRAM面向数据报的套接字,htons用于将主机字节序列的端口号转为网络序列,inet_addr将char*类型转换成in_addr_t类型,bind将套接字与存储好协议和ip与端口号的struct sockaddr_in绑定起来。bzero将指定内存块清空,常用于使用之前初始化结构体
接着进行客户端数据收发逻辑:
void run(){char Buffer[1024];string temp;while(true){//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,//struct sockaddr *src_addr, socklen_t *addrlen);struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t Re_ret = recvfrom(_socketid,Buffer,sizeof(Buffer)-1,0,//接收(struct sockaddr*)&client,&len);if(Re_ret<0){lg(Fatal,"bind create is error : %d",Re_ret);continue;}Buffer[Re_ret] = 0;//看成字符串temp = Buffer;cout<<temp<<endl;//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,//const struct sockaddr *dest_addr, socklen_t addrlen);sendto(_socketid,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送}}
客户端逻辑:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(string proc)
{cout << "Usage: " << proc << " serverip serverport"<< endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string Ser_ip = argv[1];uint16_t Ser_proc = stoi(argv[2]);//1.创建struct sockaddr struct sockaddr_in Server;bzero(&Server,sizeof(Server));Server.sin_family=AF_INET;Server.sin_port = htons(Ser_proc);Server.sin_addr.s_addr= inet_addr(Ser_ip.c_str());int _sockeid = socket(AF_INET,SOCK_DGRAM,0);if(_sockeid <0){cout<<"Client is error "<<endl;exit(1);}char Buffer[1024];string message;socklen_t len = sizeof(Server);while(true){cout<<"Please enter :";getline(cin,message);sendto(_sockeid,message.c_str(),sizeof(message)-1,0,(struct sockaddr*)&Server,len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(_sockeid, Buffer, sizeof(Buffer)-1, 0, (struct sockaddr*)&temp, &len);//接受&if(s > 0){Buffer[s] = 0;cout << Buffer << endl;}}close(_sockeid);return 0;
}
客户端也是需要bind的但是是由OS在客户端发送数据的时候自动做。不需要显示调用bind首先客户端想要与服务器通信,要知道服务器的ip和端口号,这里用到了可变参数,让用户的运行的时候告诉进程服务器的ip和端口号。之后的逻辑乏善可陈,即是发送和接受数据。
我们再设置服务器端口号时,要知道0到1023:为系统内定的端口号,一般都要有固定的。

如此以来我们就封装出了udpsocket实现简单的cs通信。
7.实现Windows与Linux通信
将Linux机器作为服务器,代码逻辑与上相同:
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"using namespace std;
Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void Run() // 对代码进行分层{isrunning_ = true;char inbuffer[size];string temp;while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;//看成字符串temp = inbuffer;cout<<temp<<endl;//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,//const struct sockaddr *dest_addr, socklen_t addrlen);sendto(sockfd_,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_; // 网路文件描述符string ip_; // 任意地址bind 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;};
Windows下vs2022代码:
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <stdio.h>
#include <winsock2.h>
#include <Windows.h>#include<iostream>
#include<string>#pragma comment(lib,"ws2_32.lib") //引入库文件int main()
{//初始化网络环境WSADATA wsa;if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0){printf("WSAStartup failed\n");return -1;}//建立一个udp的socketSOCKET socked = socket(AF_INET, SOCK_DGRAM, 0);if (socked == INVALID_SOCKET){printf("create socket failed\n");return -1;}int port = 8080;std::string ip = "";//服务器ip地址//创建结构体sockaddr_in addr = { 0 };addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.S_un.S_addr = inet_addr(ip.c_str());std::string info;char buffer[1024];memset(buffer, 0, sizeof(buffer));//收发数据while (true) {std::cout << "Please enter:";std::getline(std::cin, info);//发送数据int n = sendto(socked, info.c_str(), info.size(), 0, (SOCKADDR*)&addr, sizeof(SOCKADDR));if (n == 0){printf("send failed\n");return -1;}sockaddr_in t = { 0 };int len = sizeof(sockaddr_in);// 接收数据n = recvfrom(socked, buffer, sizeof(buffer) - 1, 0, (SOCKADDR*)&t, &len);buffer[n] = 0;std::cout << buffer << std::endl;memset(buffer, 0, sizeof(buffer));}//关闭SOCKET连接closesocket(socked);//清理网络环境WSACleanup();return 0;
}

即可完成通信
8.Linux下远程执行指令
popen 函数允许程序通过一个文件流与外部命令进行交互:
FILE *popen(const char *command, const char *type);
- command:一个指向字符串的指针,表示要执行的命令。这可以是任何可以在命令行执行的命令,包括路径、可执行文件名以及命令的参数。
- type:一个指向字符串的指针,用于指定文件流的类型,可以是 “r” 或 “w”:“r”:表示读取(即从命令的标准输出读取数据)。“w”:表示写入(即将数据写入到命令的标准输入)。
- 成功时,返回一个指向 FILE 的指针,该指针可以用于读取或写入外部命令的输入输出。
也就是客户端向服务器发送数据后,加一层处理,将数据转换成指令并将结果读取并打印出来:
string ExcuteCommand(const string &cmd)
{cout << "get a request cmd: " << cmd << endl;FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}string ret;char buffer[4096];while(true){char *tmp = fgets(buffer, sizeof(buffer), fp);if(tmp == nullptr) break;ret += buffer;}pclose(fp);return ret;
}

当然用Windows也是可以的:

其实我们的xshell原理就类似:

我们是客户端访问远端服务器后输入数据被解析成指令,服务器再将结果返回给我们。
9.实现tcp_socket
系列函数解析:
listen 函数在网络编程中用于将一个套接字从“主动”状态切换到“监听”状态,,通常在调用 socket 函数创建套接字并用 bind 绑定到地址和端口之后使用。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
- backlog:指定套接字的最大连接队列长度,即在服务器处理新连接之前,内核中允许排队的未决连接数。这个数值决定了在连接请求被接受之前,内核可以缓存多少个连接请求。
accept 函数是网络编程中的一个重要函数,用于从已经处于监听状态的套接字中接受一个连接请求,并返回一个新的套接字描述符,该描述符用于与客户端进行通信
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 成功时,返回一个新的套接字描述符,该描述符用于与客户端进行通信。
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
connect 函数用于将一个套接字连接到一个远程主机上的指定地址。用于客户端连接服务器,连接成功后,客户端可以通过该套接字与服务器进行数据交换。
tcp通信也是全双工的,因为他的发送缓冲区和接收缓冲区是分开的,所以接收和发送是可以同时进行的。
我们先来实现简单的tcpsocket的cs通信:
服务器代码逻辑:
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
//#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;const string defaultip = "0.0.0.0";
const int backlog = 5;
extern Log lg;enum
{UsageError = 1,SocketError,BindError,ListenError,
};class TcpServer
{
public:TcpServer(const uint16_t &port, const string &ip = defaultip): _port(port), _ip(ip){}void InitServer(){_listensock = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in _obj;bzero(&_obj, sizeof(_obj));_obj.sin_family = AF_INET;_obj.sin_port = htons(_port);_obj.sin_addr.s_addr = INADDR_ANY;socklen_t len = sizeof(_obj);if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0){lg(Fatal, "Server bind is error %s", strerror(errno));exit(BindError);}lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);// 监听if (listen(_listensock, backlog) < 0){lg(Fatal, "Server listen is error %s", strerror(errno));exit(ListenError);}lg(Info, "Server listen is succes ");}void _test(int _sockefd, uint16_t &port, string &clientip){char buffer[4096];while (true){ssize_t n = read(_sockefd, buffer, sizeof(buffer) - 1); // 留一个位置给 '\0'if (n > 0){buffer[n] = 0; // 添加终止符,防止溢出cout << "client say# " << buffer << endl;string echo_string = "tcpserver echo# ";echo_string += buffer;write(_sockefd, echo_string.c_str(), echo_string.size());}else if (n == 0){lg(Info, "%s:%d quit, server close _sockefd: %d", clientip.c_str(), port, _sockefd);break;}else{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", _sockefd,clientip.c_str(), port);break;}}}void Start(){lg(Info, "tcpServer is running....");for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);if (_sockefd < 0){lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));continue;}char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));uint16_t clientport = ntohs(client.sin_port);lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);_test(_sockefd, client.sin_port, _ip);close(_sockefd);}}~TcpServer() {}private:int _listensock;uint16_t _port;string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){int sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;return 1;}int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){lg(Fatal, "Client connect is error %s", strerror(errno));}while (true){string message;cout << "Please Enter# ";getline(cin, message);int n = write(sockfd, message.c_str(), message.size());if (n < 0){cerr << "write error..." << endl;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;cout << inbuffer << endl;}}close(sockfd);}return 0;
}
运行起来既实现

可是当前的代码是单进程的,如果有两台主机同时向服务器发送数据,服务器只能处理先连接上的,那我们服务器能同时处理多个客户端的数据该怎么办呢?
我们可以将代码逻辑改成多进程版本的:
核心代码:
pid_t id=fork();
if(id==0)
{close(_listensock);if(fork()>0)exit(0);_test(_sockefd, client.sin_port, _ip);close(_sockefd);exit(0);
}
close(_sockefd);
pid_t w_id=waitpid(id,nullptr,0);
这时就完成了多进程的代码逻辑。

其中:
if(fork()>0)exit(0);
目的是在子进程中创建孙子进程让孙子进程执行_test代码,这样在执行_test的时候,子进程已经被父进程回收,所以可以继续创建子进程完成逻辑,实现多主机可以同时访问服务器的功能。使用多线程的话也是类似的逻辑,但是创建线程的成本比进程低的多,这里就不演示了。
接下来我们编写一个基于线程池处理汉译英功能的服务器。首先直接将我们之前编写的单例模式的线程池拿过来:
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 10;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}void Start(){int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
服务器代码逻辑:
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;const string defaultip = "0.0.0.0";
const int backlog = 6;
extern Log lg;enum
{UsageError = 1,SocketError,BindError,ListenError,
};class TcpServer;class ThreadData
{
public:ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;string clientip;uint16_t clientport;TcpServer *tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t &port, const string &ip = defaultip): _port(port), _ip(ip){}void InitServer(){_listensock = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in _obj;bzero(&_obj, sizeof(_obj));_obj.sin_family = AF_INET;_obj.sin_port = htons(_port);_obj.sin_addr.s_addr = INADDR_ANY;socklen_t len = sizeof(_obj);if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0){lg(Fatal, "Server bind is error %s", strerror(errno));exit(BindError);}lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);// 监听if (listen(_listensock, backlog) < 0){lg(Fatal, "Server listen is error %s", strerror(errno));exit(ListenError);}lg(Info, "Server listen is succes ");void Start(){ThreadPool<Task>::GetInstance()->Start();lg(Info, "tcpServer is running....");for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);if (_sockefd < 0){lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));continue;}char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));uint16_t clientport = ntohs(client.sin_port);lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);Task t(_sockefd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);}}~TcpServer() {}private:int _listensock;uint16_t _port;string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){int sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;return 1;}// 客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){lg(Fatal, "Client connect is error %s", strerror(errno));return 1;}while (true){string message;cout << "Please Enter# ";getline(cin, message);int n = write(sockfd, message.c_str(), message.size());if (n < 0){cerr << "write error..." << endl;break;}else if (n == 0) {break;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;cout << inbuffer << endl;}else if(n == 0) {break;}}close(sockfd);}return 0;
}
英译汉搜索逻辑:
#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
Log lg;const string dictname = "./dict.txt";
const string sep = ":";//yellow:黄色...
static bool Split(string &s, string *part1, string *part2)
{auto pos = s.find(sep);if(pos == string::npos) return false;*part1 = s.substr(0, pos);*part2 = s.substr(pos+1);return true;
}class Init
{
public:Init(){ifstream in(dictname);if(!in.is_open()){lg(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}string line;while(getline(in, line)){string part1, part2;Split(line, &part1, &part2);dict.insert({part1, part2});}in.close();}string translation(const string &key){auto iter = dict.find(key);if(iter == dict.end()) return "Unknow";else return iter->second;}
private:unordered_map<string, string> dict;
};
任务代码逻辑:
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"extern Log lg;
Init init;class Task
{
public:Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}Task(){}void run(){char buffer[4096];while(1) {ssize_t n = read(sockfd_, buffer, sizeof(buffer)); if (n > 0){buffer[n] = 0;std::cout << "client key# " << buffer << std::endl;std::string echo_string = init.translation(buffer);n = write(sockfd_, echo_string.c_str(), echo_string.size()); if(n < 0){lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));}}else if (n == 0){lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);break;}else{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);break;}}close(sockfd_);}~Task(){}private:int sockfd_;std::string clientip_;uint16_t clientport_;
};

10.守护进程
我们之前学习过前台进程和后台进程,在命令行中前台进程是必须要一直存在的,能接受键盘信号的就是前台进程。所以说谁拥有键盘文件谁就是前台进程,一个会话可以有多个后台进程但只能有一个前台进程。&符号就是用来将程序设置到后台运行的:

jobs指令可以查看后台任务:

fg加任务号可以将后台任务提到前台:

ctrl Z可将将其暂停并放回后台:

bg加任务号可将后台任务在跑起来:


每次登录xshell就会建立一个会话,所以有会话id的概念,TTY为控制终端,一个进程组只有一个进程的pid和pgid相同,组长就是进程组的第一个。


我们可以观察到终端是 ?这表明它没有关联到任何终端。
守护进程的父进程 ID(PPID)是 1,这表明它们可能是由 init启动的。守护进程的会话 ID 通常与其进程组 ID 相同,表示它们属于同一会话
函数setsid:
#include <unistd.h>pid_t setsid(void);
setsid 函数是用于创建新的会话的系统调用。它的主要作用是将当前进程创建为新的会话的领导者,从而实现与当前会话的断开,通常用于守护进程的创建。也就是说守护进程是自成会话的进程,需要注意的是setsid的调用进程不能是进程组组长,所以我们需要fork子进程执行这个函数。守护进程也是孤儿进程
代码逻辑实现:
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "")
{// 1. 忽略其他异常信号signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2. 将自己变成独立的会话if (fork() > 0)exit(0);setsid();// 3. 更改当前调用进程的工作目录if (!cwd.empty())chdir(cwd.c_str());// 4. 标准输入,标准输出,标准错误重定向至/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}
逻辑解析:
- 定义了一个常量 nullfile,指定了 /dev/null,任何写入到它的数据都会被丢弃,读取则返回 EOF。
- 忽略异常信号这些信号处理设置确保了守护进程不会因为这些信号而被意外终止。
- fork():创建一个子进程。父进程退出,子进程继续执行。这让子进程脱离原有的会话。
- setsid():创建一个新的会话,并将调用进程设置为该会话的会话组长。这样,进程就不再与终端相关联,可以在后台运行。
- chdir(cwd.c_str());:如果提供了工作目录路径,则更改进程的工作目录。通常,守护进程会将工作目录更改为根目录。
- open(nullfile.c_str(), O_RDWR);:打开 /dev/null 设备文件。dup2(fd, 0);、dup2(fd, 1);、dup2(fd, 2);:将标准输入(0)、标准输出(1)、标准错误(2)重定向到 /dev/null。这样,守护进程的输入输出不会干扰终端,也不会产生任何不必要的输出。
相关文章:
Linux 网络套接字解析:实现网络通信
目录 一.网络基础1.协议2.OSI与TCP/IP模型3.网络通信流程4.IP与Mac地址 二.网络编程套接字1.端口号2.网络字节序3.tcp、udp协议4.socket编程5.sockaddr结构解析6.实现Udp_socket7.实现Windows与Linux通信8.Linux下远程执行指令9.实现tcp_socket10.守护进程 一.网络基础 1.协议…...
vue3 组合式API
<!-- 深度监听 deep 点击按钮控制台,才输出count变化了: 1, 老值: 0;否则控制台不输出 --> <script setup>import { ref,watch } from vueconst state ref({count:0})const setCount () > {state.count.value}watch(state, () > {console.log(…...
二、什么是Vue中的响应式?Vue的响应式原理
什么是Vue中的响应式 Vue中的响应式,简而言之就是当数据发生变化时,页面跟随变化。使用过Vue的v-model都有比较深刻的感受,我们在代码中修改双向绑定的数据后,页面上的数据也会自动更新,页面跟随变化 我们看个例子&am…...
快9月了才开始强化,跟张宇还是武忠祥?
快9月了才开始强化,跟张宇还是武忠祥! 说真的,我也替这位同学着急,但是考研数学越是进度慢,就越不能急!急着赶进度,容易出事!遇到这个问题的朋友肯定不止一位,那我就帮大…...
SSM好易学学习平台---附源码92142
摘 要 随着互联网趋势的到来,各行各业都在考虑利用互联网将自己推广出去,最好方式就是建立自己的互联网系统,并对其进行维护和管理。在现实运用中,应用软件的工作规则和开发步骤,采用Java技术建设好易学学习平台。本文…...
对于mp4 ios和mac safari不能播放问题处理
直接对原mp4文件进行重新转码就可以了 ffmpeg -i origin.mp4 -vcodec h264 -profile:v high -level 4.1 orgin_hl.mp4 原因源文件不符合苹果基本规则 苹果官网文档...
开发同城交友找搭子系统app前景分析
开发同城交友系统APP的背景 社交需求多样化: 随着城市化的加速和人们生活节奏的加快,现代人的社交圈子往往较为狭窄,难以结识新朋友。传统的线下交友方式受限于时间、地点等因素,难以满足现代人对于交友的多样化需求。互联网和智…...
faiss向量数据库测试《三体》全集,这家国产AI加速卡,把性能提了7倍!
在人工智能和机器学习技术的飞速发展中,向量数据库在处理高维数据方面扮演着日益重要的角色。近年来,随着大型模型的流行,向量数据库技术也得到了进一步的发展和完善。 向量数据库为大型模型提供了一个高效的数据管理和检索平台,…...
负载均衡---相关概念介绍(一)
负载均衡(Load Balance)是集群技术的一种重要应用,旨在将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,从而提高系统的并发处理能力、增加吞吐量、加强网络处理能力,并提供故障转移以…...
计算机基础知识复习8.14
子线程抛异常主线程能否catch 在不做任何处理的情况下,主线程不能catch 解决方式: 子线程使用try catch来捕获异常 为线程设置未捕获异常处理器UncaughtExceptionHandler 通过future的get方法捕获异常 JVM相关参数 显示指定堆内存-Xms和-Xmx指定最…...
【io深层理解】
io深层理解 1.内核态2.用户态3. select IO多路复用执行原理4. select io多路复用限制和不足 1.内核态 一个进程会涉及多文件的修改,比如说。那么在内核态就会维护一个表,这个表叫文件描述符bitmap,这个表会传递给内核态,当然肯定传…...
【懒人工具】指定新文件,替换全盘旧文件
没辙,就是懒 最近在调整.clang-format,这个format文件要跟着项目走,只换本地默认的还不够。调整好以后一个项目一个项目的换,有时候会漏掉,索性全盘一次性换完。 基于自己操作的流程,写了个脚本࿰…...
React+Vis.js(02):设置节点样式
文章目录 1、修改vis.js的节点和关系颜色2、修改vis.js节点的字体颜色2.1 统一设置节点字体颜色2.2 自定义某个节点的字体颜色3、设置vis.js节点的边框颜色和宽度3.1 设置单个节点3.2 统一设置1、修改vis.js的节点和关系颜色 在vis.js中,可以通过color属性,来给node节点添加…...
3G网络要彻底没了
2月21日,三大运营商公布了最新的用户数据,移动联通电信三家5G套餐用户数合计超过了7.5亿。信通院早前公布的数据显示,一月份,国内市场5G手机出货量2632.4万部,占同期手机出货量的79.7%。 这两项数据,说明我们已经进入到了5G时代,5G的普及速度远超很多人的想象。就在5G逐…...
如何配置ESXI主机的IP地址管理
🏡作者主页:点击! 🐧Linux基础知识(初学):点击! 🐧Linux高级管理防护和群集专栏:点击! 🔐Linux中firewalld防火墙:点击! ⏰️创作…...
软件测试学习笔记丨测试用例设计方法
本文转自测试人社区,原文链接:https://ceshiren.com/t/topic/31921 一,黑盒测试方法论 1,等价类 1.1 定义 等价类划分是一种重要的、常用的黑盒测试方法不需要考虑程序的内部结构,只需要考虑程序的输入规格即可它将…...
MinIO基本用法
在现代云计算和大数据领域,对象存储因其可扩展性、可靠性和低成本成为数据存储的重要选择。MinIO作为一个高性能、分布式的对象存储系统,凭借其开源、简单易用以及与Amazon S3兼容的特性,在业界得到了广泛的应用。本文将带您了解MinIO的基本用…...
MySQL windows版本安装
一、下载MySQL安装包 访问MySQL官网:首先,访问MySQL的官方网站(MySQL),或者更具体地,访问MySQL的下载页面(MySQL :: Download MySQL Community Server)。 选择适合的版本࿱…...
Python实现人脸轮廓提取
目录 一、背景知识1.1 人脸检测和轮廓提取的意义1.2 人脸检测方法概述1.3 轮廓提取方法概述二、常用的人脸轮廓提取方法2.1 基于边缘检测的轮廓提取2.2 基于形态学操作的轮廓提取2.3 基于特征点检测的轮廓提取三、Python实现人脸轮廓提取3.1 安装依赖库3.2 使用Dlib进行人脸检测…...
Prettier+Vscode setting提高前端开发效率
文章目录 前言Prettier第一步:下载依赖(团队合作)或下载插件(独立开发)第二步:添加.prettierrc.json文件**以下是我使用的****配置规则** 第三步:添加.prettierignore文件**以下是我常用的****配…...
九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...
C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...
处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的
修改bug思路: 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑:async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
elementUI点击浏览table所选行数据查看文档
项目场景: table按照要求特定的数据变成按钮可以点击 解决方案: <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
spring Security对RBAC及其ABAC的支持使用
RBAC (基于角色的访问控制) RBAC (Role-Based Access Control) 是 Spring Security 中最常用的权限模型,它将权限分配给角色,再将角色分配给用户。 RBAC 核心实现 1. 数据库设计 users roles permissions ------- ------…...
02.运算符
目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&:逻辑与 ||:逻辑或 !:逻辑非 短路求值 位运算符 按位与&: 按位或 | 按位取反~ …...
何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡
何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡 背景 我们以建设星云智控官网来做AI编程实践,很多人以为AI已经强大到不需要程序员了,其实不是,AI更加需要程序员,普通人…...
数据分析六部曲?
引言 上一章我们说到了数据分析六部曲,何谓六部曲呢? 其实啊,数据分析没那么难,只要掌握了下面这六个步骤,也就是数据分析六部曲,就算你是个啥都不懂的小白,也能慢慢上手做数据分析啦。 第一…...
