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);
}