当前位置: 首页 > news >正文

CPP网络编程基础知识

CPP网络编程基础知识-理论、结构体与方法、TCP socket封装

写在前面:此系列文章是CPP网络编程课的学习笔记,课程来源:bilibili.com/video/BV11Z4y157RY/?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click

文章目录

  • CPP网络编程基础知识-理论、结构体与方法、TCP socket封装
    • 文件操作符
    • 网络字节序
    • 结构体与方法
      • 结构体
      • 方法
        • socket
        • send
        • recv
        • close
        • bind
        • listen
        • accept
    • socket封装
      • client
      • server
      • 测试类
    • 多进程TCP Server
      • 前置知识
        • fork函数
        • signal函数
      • 多进程Server

文件操作符

  • /proc/进程id/fd目录中,存放了每个进程打开的fd;
  • Linux进程默认打开了三个文件描述符:
    • 0-标准输入(键盘) cin
    • 1-标准输出(显示器) cout
    • 2-标准错误(显示器) cerr
void test1(){int i=0;cin >> i;cout << "cout: " << i << endl;cerr << "cerr: " << i << endl; // 用于错误输出的标准错误流对象/*
2
cout: 2
cerr: 2
*/
}void test2(){int i=0;close(0); // 关闭标准输入cin >> i;cout << "cout: " << i << endl;cerr << "cerr: " << i << endl; // 用于错误输出的标准错误流对象/*
cout: 0
cerr: 0
*/
}void test3(){int i=0;close(0); // 关闭标准输入close(1); // 关闭标准输出cin >> i;cout << "cout: " << i << endl;cerr << "cerr: " << i << endl; // 用于错误输出的标准错误流对象/*
cerr: 0
*/
}void test4(){int i=0;close(0); // 关闭标准输入close(1); // 关闭标准输出close(2); // 关闭标准错误cin >> i;cout << "cout: " << i << endl;cerr << "cerr: " << i << endl; // 用于错误输出的标准错误流对象// 不打印内容
}
  • 文件描述符的分配规则是:找到最小的,没有被占用的文件描述符
void test5(){int fd;    // 定义一个文件描述符/文件句柄。fd=open("data.txt",O_RDONLY); // 打开文件。cout << fd << endl; // 3sleep(100);
/*
ls /proc/113613/fd 
显示:0  1  2  3
*/
}void test6(){close(0); // 关闭标准输入close(1); // 关闭标准输出close(2); // 关闭标准错误int fd;    // 定义一个文件描述符/文件句柄。fd=open("data.txt",O_RDONLY); // 打开文件。cout << fd << endl; // 不打印sleep(100);
/*
ls /proc/114491/fd 
显示:0
*/
}void test7(){close(0); // 关闭标准输入int fd;    // 定义一个文件描述符/文件句柄。fd=open("data.txt",O_RDONLY); // 打开文件。cout << fd << endl; // 0sleep(100);
/*
ls /proc/115082/fd 
显示:0  1  2
*/
}void test8(){close(2); // 关闭标准输入int fd;    // 定义一个文件描述符/文件句柄。fd=open("data.txt",O_RDONLY); // 打开文件。cout << fd << endl; // 2sleep(100);
/*
ls /proc/115718/fd 
显示:0  1  2
*/
}
  • 对Linux来说,socket操作与文件操作一样;
void test9(){int fd =open("data.txt",O_RDONLY); // 打开文件。int sockfd = socket(AF_INET,SOCK_STREAM,0);cout << "fd: " << fd << endl; // 3cout << "sockfd: " << sockfd << endl; // 4sleep(100);
/*
ps -ef |grep test1
liulh     117842   70768  0 00:09 pts/1    00:00:00 ./test1
liulh     117873   87572  0 00:09 pts/2    00:00:00 grep --color=auto test1
ls /proc/117842/fd
显示:0  1  2  3  4
*/
}
  • 在网络传输数据的过程中,可以使用文件的I/O函数;
// 以下两行等同
iret=send(sockfd,buffer,strlen(buffer),0);
iret=write(sockfd,buffer,strlen(buffer));// 以下两行等同
iret=recv(sockfd,buffer,sizeof(buffer),0);
iret=read(sockfd,buffer,sizeof(buffer));
  • 文件描述符是Linux分配给文件或socket的整数。

网络字节序

如果数据类型占用的内存空间大于1,CPU把数据存放在内存中的方式有两种:

  • 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。

  • 小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。

假设从内存地址0x00000001处开始存储十六进制数0x12345678,那么:
Bit-endian(按原来顺序存储)
0x00000001           0x12 
0x00000002           0x34
0x00000003           0x56
0x00000004           0x78
Little-endian(颠倒顺序储存)
0x00000001           0x78
0x00000002           0x56
0x00000003           0x34
0x00000004           0x12

Intel系列的CPU以小端序方式保存数据,其它型号的CPU不一定。

为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。

C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

uint16_t htons(uint16_t hostshort);   // 主机序转网络序 uint16_t  2字节的整数 unsigned short
uint32_t htonl(uint32_t hostlong);    // 主机序转网络序 uint32_t  4字节的整数 unsigned int
uint16_t ntohs(uint16_t netshort);    // 网络序转主机序
uint32_t ntohl(uint32_t netlong);     // 网络序转主机序

可使用如下方法理解上述库函数:

  • h: host(主机);

  • to: 转换;

  • n: network(网络);

  • s: short(2字节,16位的整数);

  • l: long(4字节,32位的整数);

在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换,只有向sockaddr_in成员变量填充数据时,需要考虑字节序的问题

IP大端序格式与字符串格式转换:

typedef unsigned int in_addr_t;    // 32位大端序的IP地址。// 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp); // 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。
int inet_aton(const char *cp, struct in_addr *inp);	// 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。
char *inet_ntoa(struct in_addr in);

结构体与方法

结构体

struct sockaddr {unsigned short sa_family;	// 协议族,与socket()函数的第一个参数相同,填AF_INET。unsigned char sa_data[14];	// 14字节的端口和地址。端口2字节,地址12字节支持IPv4和IPv6
};// sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。
struct sockaddr_in {  unsigned short sin_family;	// 协议族,与socket()函数的第一个参数相同,填AF_INET。unsigned short sin_port;		// 16位(2字节)端口号,大端序。用htons(整数的端口)转换。struct in_addr sin_addr;		// IP地址的结构体。192.168.101.138unsigned char sin_zero[8];	// 未使用,为了保持与struct sockaddr一样的长度而添加。
};
struct in_addr {				// IP地址的结构体。unsigned int s_addr;		// 32位的IP地址,大端序。4字节,只支持IPv4
};// 此外,为了方便对sockaddr_in中的in_addr(IP地址)传参,定义下述方法和结构体
// 方法
struct hostent *gethostbyname(const char *name);// 传参使用stringIp.c_str()
// 结构体
struct hostent { char *h_name;     	// 主机名。char **h_aliases;    	// 主机所有别名构成的字符串数组,同一IP可绑定多个域名。 short h_addrtype; 	// 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。short h_length;     	// 主机IP地址长度,IPV4地址为4,IPV6地址则为16。char **h_addr_list; 	// 主机的ip地址,以网络字节序存储。 
};
#define h_addr h_addr_list[0] 	// for backward compatibility
// 转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员中。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

方法

socket
// 创建一个socket,成功返回socketId,失败返回-1
int socket(int __domain, int __type, int __protocol);// demo
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 创建tcp的sock
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);    // 创建udp的sock

domain:指定通信协议族(IPv4常用,IPv6未普及,其他不常用)

  • PF_INET IPv4互联网协议族。

  • PF_INET6 IPv6互联网协议族。

  • PF_LOCAL 本地通信的协议族。

  • PF_PACKET 内核底层的协议族。

  • PF_IPX IPX Novell协议族。

type:指定数据传输的类型

  • SOCK_STREAM 面向连接的socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。

  • SOCK_DGRAM 无连接的socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。

protocol:指定最终使用的协议

  • IPPROTO_TCP
  • IPPROTO_UDP
send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

入参:

  • sockfd:套接字文件描述符
  • buf:指向待发送数据的缓冲区的指针
  • len:缓冲区中待发送数据的长度(以字节为单位)
  • flags:用于修改发送行为的标识,常用0,表示标准模式。

返回值:

  • 成功: 返回实际发送出去的字节数。这个值可能小于指定的 len 参数!这种现象称为“部分写(Partial Write)”。
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型。
recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

入参:

  • sockfd:套接字文件描述符
  • buf:指向应用程序中用于存放接收到的数据的缓冲区的指针
  • len:缓冲区最大容量(以字节为单位)
  • flags:用于修改接收行为的标识,常用0,表示标准模式,阻塞式接收。

返回值:

  • 成功: 返回实际接收到的字节数(> 0)。
  • 返回 0: 对方已优雅地关闭了连接(发送了 FIN 包)。这是一个非常重要的信号,意味着不会再从这个套接字收到任何数据。
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型。
close
int close(int sockfd)

入参:

  • sockfd:套接字文件描述符

返回值:

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误原因。
bind
int bind(int sockfd, const sockaddr *addr, socklen_t len)

入参:

  • sockfd:套接字文件描述符

  • addr: 指向一个包含要绑定的IP地址和端口号的结构体的指针

  • len: addr 指针所指向结构体的实际长度(以字节为单位)

返回值:

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误原因。

特别注意:服务端的bind()函数,普通用户只能使用1024以上的端口,root用户可以使用任意端口。

./tcpServer 300
报错:bind: Perminssion deniedsu -
./tcpServer 300
成功
listen
int listen(int sockfd, int __n)

入参:

  • sockfd:套接字文件描述符

  • __n: TCP队列的最大长度,当多个客户端同时向服务器发起连接请求时,TCP 协议栈会将这些到来的连接请求放入一个队列中

返回值:

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误原因。
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

入参:

  • sockfd:套接字文件描述符
  • addr:一个指向缓冲区的指针,该缓冲区用于接收客户端的地址信息(IP地址和端口号)
  • addrlen:指向一个整数,该整数表示 addr 缓冲区的大小

返回值:

  • 成功: 返回一个新的、已连接的套接字的文件描述符。这个新套接字用于与刚刚接受的客户端进行通信。
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误原因。

accept默认为阻塞模式(也可通过修改socketfd将其修改为非阻塞模式):

  • 如果已完成连接队列不为空: accept() 立即返回队列中的第一个连接,创建一个新的已连接套接字。
  • 如果已完成连接队列为空: accept() 调用会阻塞(线程暂停),直到一个新的连接完成三次握手并进入已完成连接队列。

socket封装

client

头文件:tcpClient.h

#ifndef TCP_CLIENT_H
#define TCP_CLIENT_H#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;class TcpClient // TCP通讯的客户端类。
{
private:int m_clientfd;        // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket。string m_ip;           // 服务端的IP/域名。unsigned short m_port; // 通讯端口。public:// 构造函数,socket初始化为-1TcpClient();// 析构函数,释放对象时关闭处于建立状态的tcp连接~TcpClient();// 向服务端发起连接请求,成功返回true,失败返回false。bool connect(const string &in_ip, const unsigned short in_port);// 向服务端发送报文,成功返回true,失败返回false。bool send(const string &buffer);// 接收服务端的报文,成功返回true,失败返回false// 未收到信息时阻塞// 入参:buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度bool recv(string &buffer, const size_t maxlen);// 断开与服务端的连接bool close();
};#endif

实现类:tcpClient.cpp

#include "tcpClient.h"// 构造函数,socket初始化为-1
TcpClient::TcpClient() : m_clientfd(-1) {}// 析构函数,释放对象时关闭处于建立状态的tcp连接
TcpClient::~TcpClient() { close(); }// 向服务端发起连接请求,成功返回true,失败返回false。
bool TcpClient::connect(const string &in_ip, const unsigned short in_port)
{if (m_clientfd != -1)return false; // 如果socket已连接,直接返回失败。m_ip = in_ip;m_port = in_port; // 把服务端的IP和端口保存到成员变量中。// 第1步:创建客户端的socket。if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)return false;// 第2步:向服务器发起连接请求。struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。memset(&servaddr, 0, sizeof(servaddr));// 设置协议和端口号servaddr.sin_family = AF_INET;     // ①协议族,固定填AF_INET。servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。// 设置IP地址struct hostent *h;                                // 用于存放服务端IP地址(大端序)的结构体的指针。if ((h = gethostbyname(m_ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体。{::close(m_clientfd);m_clientfd = -1;return false;}memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序)。// 向服务端发起连接清求if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){::close(m_clientfd);m_clientfd = -1;return false;}return true;
}// 向服务端发送报文,成功返回true,失败返回false。
bool TcpClient::send(const string &buffer)
{// 如果socket的状态是未连接,直接返回失败if (m_clientfd == -1)return false;// 调用系统函数if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)return false;return true;
}// 接收服务端的报文,成功返回true,失败返回false
// 未收到信息时阻塞
// 入参:buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度
bool TcpClient::recv(string &buffer, const size_t maxlen)
{// 如果直接操作string对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小// 清空buffer并重置大小buffer.clear();        // 清空容器。buffer.resize(maxlen); // 设置容器的大小为maxlen。// 调用系统函数int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存  // 0表示阻塞接收if (readn <= 0){buffer.clear();return false;}// 重置buffer的实际大小buffer.resize(readn);return true;
}// 断开与服务端的连接
bool TcpClient::close()
{// 如果socket的状态是未连接,直接返回失败if (m_clientfd == -1)return false;// 调用系统函数::close(m_clientfd);m_clientfd = -1;return true;
}

server

头文件:tcpServer.h

#ifndef TCP_SERVER_H
#define TCP_SERVER_H#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;class TcpServer // TCP通讯的服务端类。
{
private:int m_listenfd;        // 监听的socket,-1表示未初始化。int m_clientfd;        // 客户端连上来的socket,-1表示客户端未连接。string m_clientip;     // 客户端字符串格式的IP。unsigned short m_port; // 服务端用于通讯的端口。
public:// 构造函数,将listenfd和clientfd都初始化为-1TcpServer();// 析构函数,关闭listenfd和clientfd~TcpServer();// 初始化服务端用于监听的socketbool initserver(const unsigned short in_port);// 受理客户端的连接(从已连接的客户端中取出一个客户端),如果没有已连接的客户端,accept()函数将阻塞等待bool accept();// 获取客户端的IP(字符串格式)。const string &getClientIp() const;// 向对端发送报文,成功返回true,失败返回falsebool send(const string &buffer);// 接收客户端的报文,成功返回true,失败返回false// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度bool recv(string &buffer, const size_t maxlen);// 关闭监听的socketbool closelisten();// 关闭客户端连上来的socketbool closeclient();
};#endif

实现类:tcpServer.cpp

#include "tcpServer.h"// 构造函数,将listenfd和clientfd都初始化为-1
TcpServer::TcpServer() : m_listenfd(-1), m_clientfd(-1) {}// 析构函数,关闭listenfd和clientfd
TcpServer::~TcpServer()
{closelisten();closeclient();
}// 初始化服务端用于监听的socket
bool TcpServer::initserver(const unsigned short in_port)
{// 第1步:创建服务端的socketif ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)return false;m_port = in_port;// 第2步:把服务端用于通信的IP和端口绑定到socket上// 创建存放协议、端口和IP地址的结构体struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;                // ①协议族,固定填AF_INET。servaddr.sin_port = htons(m_port);            // ②指定服务端的通信端口。servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。// 监听socket绑定服务端的IP和端口(为socket分配IP和端口)if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){close(m_listenfd);m_listenfd = -1;return false;}// 第3步:把socket设置为可连接(监听)的状态if (listen(m_listenfd, 5) == -1) // 第二个参数为TCP队列的最大长度,当多个客户端同时向服务器发起连接请求时,TCP 协议栈会将这些到来的连接请求放入一个队列中{close(m_listenfd);m_listenfd = -1;return false;}return true;
}// 受理客户端的连接(从已连接的客户端中取出一个客户端),如果没有已连接的客户端,accept()函数将阻塞等待
bool TcpServer::accept()
{struct sockaddr_in caddr;          // 客户端的地址信息socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小// 调用系统函数if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1)return false;// 赋值客户端ipm_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。return true;
}// 获取客户端的IP(字符串格式)。
const string& TcpServer::getClientIp() const
{return m_clientip;
}// 向对端发送报文,成功返回true,失败返回false
bool TcpServer::send(const string &buffer)
{// 如果clientfd没有初始化,返回falseif (m_clientfd == -1)return false;// 调用系统函数if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)return false;return true;
}// 接收客户端的报文,成功返回true,失败返回false
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度
bool TcpServer::recv(string &buffer, const size_t maxlen)
{buffer.clear();                                               // 清空容器buffer.resize(maxlen);                                        // 设置容器的大小为maxlenint readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存  // 0表示阻塞接收if (readn <= 0){buffer.clear();return false;}buffer.resize(readn); // 重置buffer的实际大小return true;
}// 关闭监听的socket
bool TcpServer::closelisten()
{if (m_listenfd == -1)return false;// 调用系统函数::close(m_listenfd);m_listenfd = -1;return true;
}// 关闭客户端连上来的socket
bool TcpServer::closeclient()
{if (m_clientfd == -1)return false;// 调用系统函数::close(m_clientfd);m_clientfd = -1;return true;
}

测试类

clientTest.cpp

#include "tcpClient.h"int main(int argc, char *argv[])
{if (argc != 3){cout << "Using:./client 服务端的IP 服务端的端口\nExample:./client 192.168.101.138 5005\n\n";return -1;}// 创建tcp client并与服务端建立连接TcpClient tcpclient;if (tcpclient.connect(argv[1], atoi(argv[2])) == false) // 向服务端发起连接请求。{perror("connect()");return -1;}// 与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。string buffer;for (int ii = 0; ii < 10; ii++) // 循环3次,将与服务端进行三次通讯。{buffer = "这是第" + to_string(ii + 1) + "个超级女生,编号" + to_string(ii + 1) + "。";// 向服务端发送请求报文。if (tcpclient.send(buffer) == false){perror("send");break;}cout << "发送:" << buffer << endl;// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。if (tcpclient.recv(buffer, 1024) == false){perror("recv()");break;}cout << "接收:" << buffer << endl;sleep(1);}
}

serverTest.cpp

#include "tcpServer.h"int main(int argc, char *argv[])
{if (argc != 2){cout << "Using:./server 通讯端口\nExample:./server 5005\n\n"; // 端口大于1024,不与其它的重复。cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";cout << "      如果是云服务器,还要开通云平台的访问策略。\n\n";return -1;}// 创建客户端并监听端口TcpServer tcpserver;if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket。{perror("initserver()");return -1;}// 受理客户端的连接(从已连接的客户端中取出一个客户端),// 如果没有已连接的客户端,accept()函数将阻塞等待。if (tcpserver.accept() == false){perror("accept()");return -1;}cout << "客户端已连接(" << tcpserver.getClientIp() << ")。\n";// 客户端连接后收发报文string buffer;while (true){// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待// 如果客户端断开连接,recv函数返回false,跳出while循环if (tcpserver.recv(buffer, 1024) == false){perror("recv()");break;}cout << "接收:" << buffer << endl;buffer = "ok";if (tcpserver.send(buffer) == false) // 向对端发送报文。{perror("send");break;}cout << "发送:" << buffer << endl;}
}

多进程TCP Server

前置知识

fork函数

fork() 是 Unix/Linux 系统编程中一个极其重要的系统调用,用于创建新的进程。这个新创建的进程称为子进程,而调用 fork() 的进程称为父进程

返回值含义
> 0父进程中,返回子进程的PID
= 0子进程中,返回0
< 0调用失败,没有创建子进程

demo:

#include <iostream>
#include <unistd.h>int main() {std::cout << "A: Before fork - PID: " << getpid() << std::endl;  // ✅ 只在父进程执行int x = 10;  // ✅ 只在父进程执行std::cout << "B: x = " << x << std::endl;  // ✅ 只在父进程执行pid_t pid = fork();  // ⚡ 分水岭时刻// 🔽 从这里开始,父子进程都执行if (pid == 0) {std::cout << "C: Child process - x = " << x << std::endl;  // 子进程 输出10} else {std::cout << "D: Parent process - x = " << x << std::endl;  // 父进程 输出10}return 0;
}
signal函数

signal 函数是 C++ 标准库 <csignal>(或 C 风格的 <signal.h>)中提供的一个机制,用于设置对操作系统信号的处理方式。信号是操作系统向运行中的进程发送的异步通知,通常用于报告异常事件(如段错误、用户中断等)。

简单来说,signal 函数允许“捕获”一个信号,并指定一个自定义的函数(称为信号处理程序)来处理它,而不是使用系统的默认行为。

sighandler_t signal(int __sig, sighandler_t __handler)

__sig:信号编号

  • SIGINT: 中断信号。通常由用户按下 Ctrl+C 产生。
  • SIGSEGV: 段错误信号。通常由无效内存访问(如解引用空指针)引起。
  • SIGFPE: 算术错误信号。例如除以零。
  • SIGTERM: 终止信号。通常由 kill 命令发送。
  • SIGABRT: 中止信号。通常由 abort() 函数调用引起。
  • (更多信号请查阅系统文档,如 man 7 signal

__handler:指向信号处理程序的函数指针。该函数必须接受一个 int 参数(接收到的信号编号)并且返回 void

多进程Server

之前封装的TcpServer类可继续使用。

/** 此程序用于演示多进程的socket服务端*/
#include <signal.h>
#include "tcpServer.h"
using namespace std;TcpServer tcpserver;void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。int main(int argc, char *argv[])
{if (argc != 2){cout << "Using:./mpServer 通讯端口\nExample:./mpServer 5005\n\n";cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";cout << "      如果是云服务器,还要开通云平台的访问策略。\n\n";return -1;}// 先设置忽略全部的信号for (int ii = 1; ii <= 64; ii++)signal(ii, SIG_IGN);// 设置接收终止信号和中断信号// 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程// 但请不要用 "kill -9 +进程号" 强行终止signal(SIGTERM, FathEXIT); // SIGTERM 15 终止信号signal(SIGINT, FathEXIT); // SIGINT 2 中断信号// 初始化TCP服务端,并监听端口if (tcpserver.initserver(atoi(argv[1])) == false){perror("initserver()");return -1;}while (true) // 父进程循环受理客户端{// 受理客户端的连接(从已连接的客户端中取出一个客户端),// 如果没有已连接的客户端,accept()函数将阻塞等待。if (tcpserver.accept() == false){perror("accept()");return -1;}// 每受理一个客户端,就fock一个子进程出来,处理与客户端的通信int pid = fork();if (pid == -1){perror("fork()");return -1;} // 系统资源不足。// 处理父进程逻辑if (pid > 0){tcpserver.closeclient(); // 父进程关闭客户端连接的socket。continue;                // 父进程返回到循环开始的位置,继续受理客户端的连接。}// 以下为子进程处理逻辑tcpserver.closelisten(); // 子进程关闭监听的socket。// 子进程需要重新设置信号。signal(SIGTERM, ChldEXIT); // 子进程只响应父进程发送的SIGTERM信号signal(SIGINT, SIG_IGN);   // 子进程不需要响应SIGINT信号(ctrl+c)// 子进程负责与客户端进行通讯。cout << "客户端已连接(" << tcpserver.getClientIp() << ")。\n";string buffer;while (true){// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。if (tcpserver.recv(buffer, 1024) == false){perror("recv()");break;}cout << "接收:" << buffer << endl;buffer = "ok";if (tcpserver.send(buffer) == false) // 向对端发送报文。{perror("send");break;}cout << "发送:" << buffer << endl;}// 子进程退出,必要,否则又会回到accept()函数的位置。return 0; }
}// 父进程的信号处理函数。
void FathEXIT(int sig)
{// 忽略中断信号和终止信号,防止信号处理函数在执行的过程中再次被信号中断。signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);cout << "父进程退出,sig=" << sig << endl;kill(0, SIGTERM); // 向全部的子进程发送SIGTERM信号,通知它们退出。// 在这里增加释放资源的代码(全局的资源)。tcpserver.closelisten(); // 父进程关闭监听的socket。exit(0);
}// 子进程的信号处理函数。
void ChldEXIT(int sig)
{// 忽略中断信号和终止信号,防止信号处理函数在执行的过程中再次被信号中断。signal(SIGINT, SIG_IGN);signal(SIGTERM, SIG_IGN);cout << "子进程" << getpid() << "退出,sig=" << sig << endl;// 在这里增加释放资源的代码(只释放子进程的资源)。tcpserver.closeclient(); // 子进程关闭客户端连上来的socket。exit(0);
}
http://www.dtcms.com/a/389369.html

相关文章:

  • 临床AI产品化全流程研究:环境聆听、在环校验与可追溯系统的多技术融合实践(上)
  • 【k8s】web服务优雅关闭用户连接
  • 设计模式的七大原则总述
  • C/C++柔性数组
  • 从 LiveData 到 Flow:Android 状态管理的现代化演进
  • 34、模型微调技术实战 - LoRA参数高效微调全流程
  • ASP.NET Core 中基于角色的授权
  • C++ 在 Windows 下实现最基础的 WebSocket 服务端与客户端
  • 并发、分布式和实时设计方法
  • C语言第15讲
  • windows 下使用 bat 批处理运行 Chrome 无头模式刷一波访问量
  • 项目名称:基于Qt框架的跨平台天气预报应用程序​​
  • 王自如重操旧业拆箱iPhone:苹果新机发售旧机发热是惯例……
  • 鸿蒙Next Core File Kit:文件管理的高效安全之道
  • Java-128 深入浅出 MySQL MyCat 分布式数据库中间件详解:架构、功能与应用场景
  • gozero使用gRPC-gateway生成http网关
  • Go语言100个实战案例-项目实战篇:股票行情数据爬虫
  • Python开发最新 PyCharm 2025使用(附详细教程)
  • 【session基础】
  • 客户流失预警中uplift建模案例、入门学习(二)
  • SSH远程管理工具
  • 4644电源芯片的介绍和使用
  • MIPI D-PHY布线规则
  • 《深入理解Java虚拟机》第四章节读书笔记:虚拟机性能监控、故障处理工具
  • ​​[硬件电路-251]:电源相关常见的专业术语
  • 日志中的SQL语句直接转为可执行的SQL
  • Java 大视界 -- Java 大数据在智慧文旅旅游景区游客情感分析与服务改进中的应用实践
  • Nginx-RTMP-Module开源项目全解析:从基础部署到企业级应用实践
  • 新代系统如何输入期限密码
  • 【C++】STL--stack(栈)queue(队列)使用及其重要接口模拟实现