Linux网络——套接字编程

1. 网络通信基本脉络

基本脉络图如上,其中数据在不同层的叫法不一样,比如在传输层时称为数据段,而在网络层时称为数据报。我们可以在 Linux 中使用 ifconfig 查看网络的配置,如图

其中,inet 表示的是 IPv4,inet6 表示的是 IPv6,ehther(以太)表示的是 mac 地址。

2. 端口号

在进行网络通信时,是不是两台机器直接在进行通信呢?——当然不是

1. 网络协议中的下三层,主要解决的是数据能安全可靠的传输到另一台主机上

2. 这之后,用户使用应用软件完成数据的发送和接收

而一个应用软件会被操作系统解释成进程,也就是说网络通信的本质就是进程间通信!那么一个数据被 A 主机传输到 B 主机上后,怎么交给应用层呢?——端口号!端口号对于主机 A 和主机 B 都能唯一标识该主机上的一个网络应用程序的进程。

① 什么是套接字编程?

在公网上, ip 地址能标识唯一一台主机,端口号 port 能标识该主机上的唯一一个进程,因此

 我们可以使用 ip:port 来表示全网唯一的一个进程

而我们将 client_ip:client_port 与 server_ip:server_port 间的通信称为套接字编程!

② 端口号 port && 进程 PID

既然 PID 已经能够标识一台主机上的唯一性了,那为什么我们还需要端口号这个概念呢?

1. 并非所有的进程都需要进行网络通信,但是所有的进程都有 PID

2. 使系统和网络的功能解耦

我们举个例子

假如现在你正在手机上使用抖音,想浏览一个视频,你的手机(客户端)就会将“想浏览一个视频”这个行为发送到服务端, 在发送的时候其会在自己的数据中附带上自己的端口号与服务端的端口号(每一个服务端的端口号必须是众所周知,精心设计,被用户端熟知的),而服务端在接收到这个消息后会按照 IP + port 的形式返回应答!

根据我们对端口号的了解

一个进程是可以绑定多个端口号的!但是一个端口号不能被多个进程绑定! 

3. 网络字节序

我们知道在计算机中是存在大端与小端的,我们将低地址放在低位称为小端,而在 TCP/IP 协议中规定了采用大端字节序,我们可以使用 htonl 接口来转换(h: host 主机,n: net 网络,l: long 4字节),与其类似的还有 ntohl, htons(s: short), ntohs等。

4. 套接字编程

我们先来看看 socket 的 API

// 创建 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。但是,各种网络协议的地址格式并不是相同的。套接字编程也分为几种

1. 域间套接字编程 -> 同一机器内

2. 原始套接字编程 -> 网络工具

3. 网络套接字编程 -> 用户间的网络通信

我们想将网络接口统一抽象化,那就表示着参数类型必须是统一的,比如对于这个 struct sockaddr* address 来说,其设计如下

我们在设计接口时,将其设计为基类 struct sockaddr* address ,在使用时我们根据需要传入其对应的子类,这实际上使用到了面向对象中的多态思想!

① UDP版

接下来我们就简单完成一个 UDP 版本的套接字,其模板如下

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>#include "Log.hpp"
extern Log lg;enum 
{SOCKET_ERR=1;
};class UdpServer
{
public:UdpServer(){}void Init(){sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d" , sockfd_);exit(SOCKET_ERR);}}void Run(){}~UdpServer(){}
private:int sockfd_; // 网路文件描述符
};

其调用逻辑如下

#include "Udpserver.hpp"
#include <memory>int main()
{std::unique_ptr<UdpServer> svr(new UdpServer());svr->Init(/**/);svr->Run();return 0;
}

 我们来看看 socket  这个接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

int 表示返回一个文件描述符,int domain 表示要在什么域进行传输(这里我们选择的是 AF_INET-> IPv4),int type 表示创建什么类型的套接字(这里我们选择的是 SOCK_DGRAM ,即 Supports datagrams 面向数据报,也就是 udp 使用的类型),而 int protocol 表示使用什么协议类型。 

接下来我们来完成这个套接字

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>#include "Log.hpp"
extern Log lg;typedef std::function<std::string(const std::string&)> func_t;// 枚举错误信息
enum 
{SOCKET_ERR=1,BIND_ERR
};// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";// 数据缓冲区大小
const int size = 1024;class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):port_(port), ip_(ip), isrunning_(false){}void Init(){// 1. 创建 udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d" , sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d" , sockfd_);// 2. bind socket// 初始化 localstruct sockaddr_in local;bzero(&local, sizeof(local)); // 清零 locallocal.sin_family = AF_INET; // 设置为 IPv4local.sin_port = htons(port_); // 需要保证这里的端口号是网络字节序列,因为该端口号是要给对方发送的// 1. string -> uint32_t // 2. uint32_t必须是网络序列的local.sin_addr.s_addr = inet_addr(ip_.c_str()); 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 socket success!");}void Run(func_t func){// 设置服务器运行状态为 运行中isrunning_ = true;// 设置缓冲区char inbuffer[size];while(isrunning_){// 输出型参数 clientstruct sockaddr_in client;socklen_t len = sizeof(client);// 从 inbuffer 中读取数据// sizeof(inbuffer)-1 意思是将 inbufffer 视为字符串// n 表示实际接收到了多少个字符// 同时获取 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;//充当了一次数据的处理std::string info = inbuffer;std::string echo_string = func(info);// server 向 client 发送应答信息sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);}}~UdpServer(){if (sockfd_ > 0) close(sockfd_);}
private:int sockfd_;       // 网路文件描述符std::string ip_;   // 服务器ipuint16_t port_;    // 服务器进程的端口号bool isrunning_;   // 服务器运行状态
};

接下来完成 udpserver 的编写

#include "UdpServer.hpp"
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}std::string ExcuteCommand(const std::string &cmd)
{FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(ExcuteCommand);return 0;
}

 接下来我们编写一个 udpclient 来与其进行通信

#include "UdpServer.hpp"
#include <memory>
#include <iostream>using namespace std;extern Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 确认发送服务端struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){cout << "socker error" << endl;return 1;}lg(Info, "socket create success, sockfd: %d", sockfd);// client要bind吗?——要,只不过不需要用户显示的bind!一般由 OS 自由随机选择!//一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!//其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!//系统什么时候bind呢?——首次发送数据的时候string message;char buffer[1024];while(true){// 1. 获取数据cout << "Please Enter: ";getline(cin, message);// 2. 给服务端发送信息sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);struct sockaddr_in tmp;socklen_t t_len = sizeof(tmp);ssize_t n = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &t_len);if (n > 0){buffer[n] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}

运行效果如下

② TCP版

我们先来看看 TCP 方案的模板

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>#include "Log.hpp"
extern Log lg;// 枚举错误信息
enum 
{SOCKET_ERR=1,BIND_ERR
};// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";class TcpServer
{
public:TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) :port_(port), ip_(ip){}void InitServer(){// 创建套接字为 IPv4, 字节流(TCP)sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if(sockfd_ < 0){lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "create socket success, sockfd: %d" , sockfd_);// 初始化 localstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;   // 设置为 IPv4local.sin_port = htons(port_);// 保证端口号是网络字节序列// 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址// 返回的字符串存储在静态区(多次调用只保存最后一次调用结果) inet_aton(ip_.c_str(), &(local.sin_addr));// 绑定套接字if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind socket success!" , sockfd_);}void Start(){}~TcpServer(){}
private:int sockfd_;       // 网路文件描述符std::string ip_;   // 服务器ipuint16_t port_;    // 服务器进程的端口号bool isrunning_;   // 服务器运行状态
};

我们完成它的代码,如下

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <functional>#include "Log.hpp"
extern Log lg;// 枚举错误信息
enum 
{SOCKET_ERR=1,BIND_ERR,LISTEN_ERR
};// 服务器默认端口号
uint16_t defaultport = 8081;// 服务器默认ip
std::string defaultip = "0.0.0.0";// 监听接口 listen 的第二个参数
// listen 函数的第二个参数 backlog 表示等待队列的最大长度。这个等待队列是用于存放那些已经到达但还没有被 accept 函数接受的连接请求。
// 当一个新的连接请求到达时,如果服务器的等待队列还没有满,那么这个连接请求就会被添加到队列中,等待服务器的 accept 函数来处理。
// 如果等待队列已经满了,那么新的连接请求可能就会被拒绝,客户端可能会收到一个 ECONNREFUSED 错误。
const int backlog = 10;class TcpServer
{
public:TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) :port_(port), ip_(ip){}void InitServer(){// 创建套接字为 IPv4, 字节流(TCP)listensock_ = socket(AF_INET, SOCK_STREAM, 0);if(listensock_ < 0){lg(Fatal, "create listensock error, errno: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "create listensock success, listensock: %d" , listensock_);// 初始化 localstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;   // 设置为 IPv4local.sin_port = htons(port_);// 保证端口号是网络字节序列// 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址// 返回的字符串存储在静态区(多次调用只保存最后一次调用结果) inet_aton(ip_.c_str(), &(local.sin_addr));// 当 local.sin_addr.s_addr 被设置为 INADDR_ANY 时// 意思是告诉操作系统,我们希望绑定的套接字监听所有可用的网络接口上的指定端口。// 这样设置后,当有数据包到达端口时,无论它们来自哪个网络接口,套接字都能接收到。// 在服务器编程中,这通常用于监听所有网络接口上的连接请求,而不是只监听某个特定的 IP 地址。// 这样,服务器可以接受来自任何网络接口的连接,而不仅仅是一个特定的接口。local.sin_addr.s_addr = INADDR_ANY;// 绑定套接字if(bind(listensock_, (struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind socket success, sockfd: %d" , listensock_);// tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态// 监听套接字if (listen(listensock_, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s ", errno, strerror(errno));exit(LISTEN_ERR);}lg(Info, "listen socket success, sockfd: %d" , listensock_);}// 启动服务器void Start(){lg(Info, "TCP server is running...");for (;;){//1.获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);// accept 类似于 recvfrom // 其返回一个文件描述符,后两个参数表示获取哪个用户的信息int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);// sockfd && listensock_// 举个简单的例子,对于一个农家乐来说会存在两种人// 一种是去拉客到农家乐内,另一种是在农家乐内进行服务的// listensock_ -> 拉客的人; sockfd -> 进行服务的人// listensock_ 只负责监听,如果监听失败会等待监听下一个主机// sockfd 只负责通信,其可能会变得越来越多if (sockfd < 0){lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];// inet_ntop 将网络地址转换成文本表示形式// 它是 inet_aton 的逆函数,它将一个网络地址(通常是 IP 地址)从二进制形式转换为人类可读的字符串形式。// AF_INET 表示 IPv4 地址; &(client.sin_addr) 指向要转换的网络地址; clientip 是存储转换结果的字符串缓冲区; sizeof(clientip) 是 ipstr 缓冲区的大小// 确保 inet_ntop 不会写入超出缓冲区范围的内存。inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));// 2.根据新连接进行通信lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);Service(sockfd, clientip, clientport);}}void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){//测试代码char buffer[4096];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;std::cout << "client say: " << buffer << std::endl;std::string echo_string = "tcpserver echo: ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}}}~TcpServer(){if (listensock_ > 0) close(listensock_);}
private:int listensock_;   // 监听套接字std::string ip_;   // 服务器ipuint16_t port_;    // 服务器进程的端口号bool isrunning_;   // 服务器运行状态
};

其调用逻辑如下

#include "TcpServer.hpp"
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// ./tcpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(port));svr->InitServer();svr->Start();return 0;
}

 我们可以使用 telnet 来对其进行测试,如图

接下来我们编写一个 client 客户端进行测试,代码如下

#include "TcpServer.hpp"
#include <memory>
#include <iostream>using namespace std;extern Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){cout << "socker error" << endl;return 1;}lg(Info, "socket create success, sockfd: %d", sockfd);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));//tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind//客户端发起connect的时候,进行自动随机bind// connect 类似于sendtoint n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));if(n < 0){std::cerr << "connect error. . ." << std::endl;return 2;}std::string message;while(true){std::cout << "Please Enter: ";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());char inbuffer[4096];int n = read(sockfd, inbuffer, sizeof(inbuffer));if(n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}}close(sockfd);return 0;
}

测试效果如下

5. 改进方案与拓展

①多进程版

我们修改 Start 函数,即

void Start()
{lg(Info, "TCP server is running...");for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);if (sockfd < 0){lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);// 多进程服务pid_t id = fork();if(id == 0){// childclose(listensock_);if (fork() > 0) exit(0);// 使用孙子进程服务,由 system 领养// 从而使孙子进程与父进程并发执行Service(sockfd, clientip, clientport);close(sockfd);exit(0);}// fatherpid_t rid = waitpid(id, nullptr, 0);(void)rid;}
}

此外,我们也可以在最开始设置

signal(SIGCHID, IGN);

来提升并发度,但是这种方案的成本太高了,所以我们一般不推荐这种做法。 

②多线程版

我们稍作修改,有

class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t):sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;std::string clientip;uint16_t clientport;TcpServer *tsvr; // static routine 无法访问类内成员,因此需要一个 server 指针
};void Start()
{lg(Info, "TCP server is running...");for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);if (sockfd < 0){lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));// 多线程版ThreadData *td = new ThreadData(sockfd, clientip, clientport);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);}
}static void *Routine(void *args)
{pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData*>(args);td->tsvr->Service(td->sockfd, td->clientip, td->clientport);delete td;return nullptr;
}

这种方案已经能大大提升运行效率,我们还可以对其进行优化——使用线程池! 

③线程池版

修改方案如下

#include <iostream>
#include <string>
#include "Log.hpp"
extern Log lg;class Task
{
public:Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd),clientip_(clientip),clientport_(clientport){}Task(){}   void run(){//测试代码while (true){char buffer[4096];ssize_t n = read(sockfd_, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;std::string buff = buffer;if (buff == "Bye") break;std::cout << "client say: " << buffer << std::endl;std::string echo_string = "tcpserver echo: ";echo_string += buffer;write(sockfd_, echo_string.c_str(), echo_string.size());}else if (n == 0){lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);}}lg(Info, "client sockfd is closed, sockfd: %d", sockfd_);close(sockfd_);}void operator()(){run();}~Task(){}
private:int sockfd_;std::string clientip_;uint16_t clientport_;
};void Start()
{lg(Info, "TCP server is running...");ThreadPool<Task>::GetInstance()->Start();for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);if (sockfd < 0){lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);// 线程池版Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);}
}

运行效果如下

④守护进程化

1. 简单的重联

在实际的连接过程中我们可能会出现各种各样的问题,比如网络突然断了,或者服务器在一瞬间突然断开了和客户端的连接,此时我们需要有一种简单的重联方案,比如在游戏中断联就会出现当前正在重新连接,请稍等,接下来我们就简单实现一下这个功能

#include "TcpServer.hpp"
#include <memory>
#include <iostream>using namespace std;extern Log lg;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){// 尝试进行5次重连int cnt = 5;int isreconnect = false;int sockfd = 0;do{sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){cout << "socker error" << endl;return 1;}   //tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind//客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));if(n < 0){isreconnect = true;cnt--;std::cerr << "connect error, reconnecting... times: " << 5 - cnt << std::endl;sleep(1);}else{break;}} while (cnt && isreconnect);if (cnt == 0){cout << "user offline..." << endl;break;}std::string message;std::cout << "Please Enter: ";std::getline(std::cin, message);int n = write(sockfd, message.c_str(), message.size());if (n < 0){cout << "write error" << endl;continue;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if(n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}close(sockfd);}return 0;
}

运行效果如下

2. session && 前台 && 后台

我们通过画图来理解,如图

如图,每当有一个用户登录时,OS 会为其分配一个会话(session),一个 session 只能有前台进程运行,且键盘信号只能发给前台进程。那我们如何分别前台与后台呢?——谁拥有键盘就叫前台!在命令行中,前台会一直存在;前台与后台都能向显示器打印数据,但是后台是不能从标准输入获取数据的。

我们可以在运行程序时在最后带上一个 & 使其在后台运行,举个例子

#include <iostream>using namespace std;int main()
{while (true){cout << "hello world" << endl;}return 0;
}

运行效果如下

对于 [1] 2744 ,[1] 表示后台任务号,我们可以使用一系列操作来操作它们

jobs -> 查看所有后台任务

fg -n -> 将 n 号任务提到前台

ctrl+z -> 将前台进程放到后台(暂停)

bg -n -> 将后台暂停的进程继续执行 

3. Linux 系统进程间关系

我们在后台多运行几个 test 有

可以看到,多个任务(进程组)在同一个 session 内启动。那进程组和任务间有什么关系呢?

任务的完成往往需要多个进程协同工作,而这些进程可以被组织在一个或多个进程组中。例如,一个复杂的任务可能需要多个进程组来共同完成,每个进程组负责任务的不同部分。在这种情况下,进程组作为任务的一个执行单元,可以被看作是任务的一个子集或实现部分。

那当用户退出的时候后台进程会怎样呢?——会被 OS 领养,即成为孤儿进程,也就是说后台进程受到了用户登录和退出的影响!那我们将不想受到任何用户登录和注销影响的行为称为守护进程化! 

4. 进程的守护进程化

那我们如何做到守护进程化呢?我们可以封装一个接口,即

#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "")
{// 1.忽略其他异常信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2.将自己变成独立的会话if (fork() > 0)exit(0);// setsid 组长不能调用,只有组员可以调用setsid();// 3.更改当前调用进程的工作目录if (!cwd.empty())chdir(cwd.c_str());// 4.标准输入,标准输出,标准错误重定向至 /dev/null// 写到 /dev/null 的数据都会被丢弃int fd = open(nullfile.c_str(), O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

接下来我们就可以让 TCP 服务器守护进程化,即

void Start()
{Deamon();lg(Info, "TCP server is running...");ThreadPool<Task>::GetInstance()->Start();for (;;){struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);if (sockfd < 0){lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);// 线程池版Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);}
}

运行效果如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/474582.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习的实践层面

深度学习的实践层面 设计机器学习应用 在训练神经网络时&#xff0c;超参数选择是一个高度迭代的过程。我们通常从一个初步的模型框架开始&#xff0c;进行编码、运行和测试&#xff0c;通过不断调整优化模型。 数据集一般划分为三部分&#xff1a;训练集、验证集和测试集。常…

Jmeter 如何导入证书并调用https请求

Jmeter 如何导入证书并调用https请求 通过SSL管理器添加证书文件 支持添加的文件为.p12&#xff0c;.pfx&#xff0c;.jks 如何将pem文件转换为pfx文件&#xff1f; 在公司内部通常会提供3个pem文件。 ca.pem&#xff1a;可以理解为是根证书&#xff0c;用于验证颁发的证…

LabVIEW 温湿度测试与监控系统

煤炭自燃是煤矿和煤炭储存领域面临的重大安全隐患&#xff0c;尤其是在煤炭堆积和运输过程中&#xff0c;温湿度变化会直接影响煤体的氧化速率和自燃倾向。传统的监测手段通常存在实时性差、数据处理复杂等问题&#xff0c;难以准确评估煤自燃的风险。因此&#xff0c;设计了一…

IDEA 开发工具常用快捷键有哪些?

‌在IDEA中&#xff0c;输出System.out.println()的快捷键是sout&#xff0c;输入后按回车&#xff08;或Tab键&#xff09;即可自动补全为System.out.println()‌‌。 此外&#xff0c;IDEA中还有一些其他常用的快捷键&#xff1a; 创建main方法的快捷键是psvm&#xff0c;代…

KF UKF

我需要Kalman 现在&#xff0c;主要是用来处理检测问题情况里的漏检&#xff0c;因为模拟了一段2D&#xff0c; &#xff08;x&#xff0c;y&#xff09;的数据&#xff0c;为了看效果&#xff0c;画的线尽量简单一点&#xff1a; import numpy as np import matplotlib.pyplo…

多品牌摄像机视频平台EasyCVR视频融合平台+应急布控球:打造城市安全监控新体系

在当今快速发展的智慧城市和数字化转型浪潮中&#xff0c;视频监控技术已成为提升公共安全、优化城市管理、增强应急响应能力的重要工具。EasyCVR视频监控平台以其强大的多协议接入能力和多样化的视频流格式分发功能&#xff0c;为用户提供了一个全面、灵活、高效的视频监控解决…

第8章硬件维护-8.2 可维护性和可靠性验收

8.2 可维护性和可靠性验收 可维护性和可靠性验收非常重要&#xff0c;硬件维护工程师在后端发现问题后&#xff0c;总结成可维护性和可靠性需求&#xff0c;在产品立项的时候与新特性一起进行需求分析&#xff0c;然后经过设计、开发和测试环节&#xff0c;在产品中落地。这些需…

医学图像语义分割:前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室

医学图像语义分割&#xff1a;前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室 语义分割网络FCN&#xff1a;通过将全连接层替换为卷积层并使用反卷积上采样&#xff0c;实现了第一个端到端的像素级分割网络U-Net&#xff1a;采用对称的U形编解码器结构&#xff…

如何解决多系统数据重复与冲突问题?

多系统并行运作已成为现代企业的常态。企业通常同时使用ERP、CRM、HR等多个业务系统来管理不同的功能模块。然而&#xff0c;这种多系统环境也带来了一个常见且棘手的问题&#xff1a;数据重复与矛盾。由于各系统独立运行且缺乏有效的集成机制&#xff0c;不同系统间的数据容易…

麒麟时间同步搭建chrony服务器

搭建chrony服务器 在本例中&#xff0c;kyserver01&#xff08;172.16.200.10&#xff09;作为客户端&#xff0c;同步服务端时间&#xff1b;kyserver02&#xff08;172.16.200.11&#xff09;作为服务端&#xff0c;提供时间同步服务。 配置服务端&#xff0c;修改以下内容…

【GPTs】Ai-Ming:AI命理助手,个人运势与未来发展剖析

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | GPTs应用实例 文章目录 &#x1f4af;GPTs指令&#x1f4af;前言&#x1f4af;Ai-Ming主要功能适用场景优点缺点 &#x1f4af;小结 &#x1f4af;GPTs指令 中文翻译&#xff1a; defcomplete_sexagenary&#xff08;年&a…

Chainlit快速实现AI对话应用将聊天记录的持久化到MySql关系数据库中

概述 默认情况下&#xff0c;Chainlit 应用不会保留其生成的聊天和元素。即网页一刷新&#xff0c;所有的聊天记录&#xff0c;页面上的所有聊天记录都会消失。但是&#xff0c;存储和利用这些数据的能力可能是您的项目或组织的重要组成部分。 之前写过一篇文章《Chainlit快速…

【动手学深度学习Pytorch】6. LeNet实现代码

LeNet&#xff08;LeNet-5&#xff09;由两个部分组成&#xff1a;卷积编码器和全连接层密集块 x.view(): 对tensor进行reshape import torch from torch import nn from d2l import torch as d2lclass Reshape(torch.nn.Module):def forward(self, x):return x.view(-1, 1, 28…

AI工具百宝箱|任意选择与Chatgpt、gemini、Claude等主流模型聊天的Anychat,等你来体验!

文章推荐 AI工具百宝箱&#xff5c;使用Deep Live Cam&#xff0c;上传一张照片就可以实现实时视频换脸...简直太逆天&#xff01; Anychat 这是一款可以与任何模型聊天 &#xff08;chatgpt、gemini、perplexity、claude、metal llama、grok 等&#xff09;的应用。 在页面…

Excel数据动态获取与映射

处理代码 动态映射 动态读取 excel 中的数据&#xff0c;并通过 json 配置 指定对应列的值映射到模板中的什么字段上 private void GetFreightFeeByExcel(string filePath) {// 文件名需要以快递公司命名 便于映射查询string fileName Path.GetFileNameWithoutExtension(fi…

SRP 实现 Cook-Torrance BRDF

写的很乱&#xff01; BRDF&#xff08;Bidirectional Reflectance Distribution Function&#xff09;全称双向反射分布函数。辐射量单位非常多&#xff0c;这里为方便直观理解&#xff0c;会用非常不严谨的光照强度来解释说明。 BRDF光照模型&#xff0c;上反射率公式&#…

[代码随想录Day16打卡] 找树左下角的值 路径总和 从中序与后序遍历序列构造二叉树

找树左下角的值 定义&#xff1a;二叉树中最后一行最靠左侧的值。 前序&#xff0c;中序&#xff0c;后序遍历都是先遍历左然后遍历右。 因为优先遍历左节点&#xff0c;所以递归中因为深度增加更新result的时候&#xff0c;更新的值是当前深度最左侧的值&#xff0c;到最后就…

【第七节】在RadAsm中使用OllyDBG调试器

前言 接着本专栏上一节&#xff0c;我们虽然已经用上RadAsm进行编写x86汇编代码并编译运行&#xff0c;但是想进行断点调试怎么办。RadAsm里面找不到断点调试&#xff0c;下面我们来介绍如何在RadAsm上联合调试器OllyDBG进行调试代码。 OllyDBG的介绍与下载 OllyDBG 是一款功能…

WPF MVVM框架

一、MVVM简介 MVC Model View Control MVP MVVM即Model-View-ViewModel&#xff0c;MVVM模式与MVP&#xff08;Model-View-Presenter&#xff09;模式相似&#xff0c;主要目的是分离视图&#xff08;View&#xff09;和模型&#xff08;Model&#xff09;&#xff0c;具有低…

PH热榜 | 2024-11-19

DevNow 是一个精简的开源技术博客项目模版&#xff0c;支持 Vercel 一键部署&#xff0c;支持评论、搜索等功能&#xff0c;欢迎大家体验。 在线预览 1. Layer 标语&#xff1a;受大脑启发的规划器 介绍&#xff1a;体验一下这款新一代的任务和项目管理系统吧&#xff01;它…