【Linux】网络编程套接字Socket:TCP网络编程
目录
一、TCP网络编程概述
TCP(传输控制协议)和UDP(用户数据报协议)是计算机网络中最常用的两个传输层协议,它们各自具有不同的特性和优势。以下是TCP的主要特性以及与UDP的区别。
TCP的特性
-
面向连接:TCP在数据传输之前需要先建立连接(通过三次握手),在传输完成后需要进行连接的关闭(四次挥手)。
-
可靠性:TCP提供可靠的数据传输服务,确保数据包在传输过程中不会丢失、重复或错序。它通过序列号、确认应答、重传机制和差错检测来保证数据的完整性。
-
流量控制:通过滑动窗口机制,TCP可以控制数据的发送速率,以避免接收方的缓存溢出。
-
拥塞控制:TCP具备拥塞控制机制,根据网络当前的拥塞情况动态调整传输速率,避免网络崩溃。
-
数据流:TCP是面向字节流的协议,把数据看作一个顺序的字节流,应用程序无需关心具体的数据分包或重组。
-
顺序性:TCP保证数据包的顺序,接收方在接收数据时,严格按照发送方的顺序进行处理。
UDP的特性
-
无连接:UDP是无连接的协议,发送数据之前无需建立连接,直接发送数据包即可。
-
不保证可靠性:UDP不保证数据传输的可靠性,数据包可能会丢失、重复或乱序。UDP不提供序列号、确认应答和重传机制。
-
无流量控制与拥塞控制:UDP不进行流量控制和拥塞控制,这意味着发送方可以以任意速度发送数据,可能导致网络拥塞。
-
数据报:UDP将数据视为一个个独立的数据报(datagram),每个数据报都是独立的,不能保证顺序。
-
开销小:由于UDP没有连接管理、错误检查和重传机制,相较于TCP,UDP的头部开销较小,因此效率更高。
TCP与UDP的主要区别
特性 | TCP | UDP |
---|---|---|
连接 | 面向连接 | 无连接 |
可靠性 | 提供可靠传输及错误恢复 | 不提供保障,可能丢失或重复 |
数据传输 | 字节流,顺序传输 | 数据报,包独立,顺序不保 |
流量控制 | 提供流量控制 | 不提供流量控制 |
拥塞控制 | 提供拥塞控制 | 不提供拥塞控制 |
速度 | 较慢(由于连接建立和流量控制等) | 较快(无连接和机制开销较小) |
适用场景 | 文件传输、电子邮件、Web等 | 视频会议、在线游戏、实时通信等 |
总结
TCP和UDP各有优缺点,选择使用哪种协议取决于应用场景的需求。如果需要可靠性和顺序保证,TCP是更合适的选择;而如果应用对速度和效率要求较高,可以接受数据丢失,UDP则更为适用。
二、TCP网络编程服务端与客户端的编程逻辑
1、TCP服务端代码逻辑
创建套接字 (
使用socket
):socket()
函数创建一个 TCP 套接字。绑定地址 (
使用bind
):bind()
函数将套接字与一个地址(IP 和端口)绑定。这个地址是客户机将连接到的地址。监听请求 (
使用listen
):listen()
函数使套接字进入监听状态,准备接受来自客户端的连接请求。接受连接 (
使用accept
):accept()
函数阻塞并等待客户端的连接请求。一旦接受连接,将返回一个新的套接字用于与该客户端通信。处理客户端请求:
通过新的套接字与客户端进行数据通信,通常会用recv()
和send()
或read()
和write()
函数。关闭套接字 (
当所有通信完成后,使用close
):close()
函数关闭与客户端的套接字以及监听套接字。
2、TCP客户端编程逻辑
创建套接字 (
使用socket
):socket()
函数创建一个 TCP 套接字。连接到服务器 (
使用connect
):connect()
函数将客户端套接字连接到服务端的 IP 地址和端口。发送和接收数据:
一旦连接成功,可以使用send()
和recv()
或write()
和read()
函数进行数据通信。关闭套接字 (
通信完成后,使用close
):close()
函数关闭客户端的套接字。
三、TCP网络编程相关函数
socket函数
函数功能:用于创建一个新的套接字,套接字(socket)是计算机网络中用于进行通信的一个端点。
1. 函数原型
在不同的编程语言和平台中,
socket
函数的原型可能有所不同,但在 C 语言和 POSIX 标准下,它的原型通常是:int socket(int domain, int type, int protocol);
2. 参数说明
domain:指定套接字的域或协议族,常见的有:
AF_INET
:IPv4 网络协议。AF_INET6
:IPv6 网络协议。AF_UNIX
:用于本地进程间通信的 Unix 域套接字。type:指定套接字的类型,常见的有:
SOCK_STREAM
:面向连接的流式套接字,通常用于 TCP 协议。SOCK_DGRAM
:数据报套接字,通常用于 UDP 协议。SOCK_RAW
:原始套接字,通常用于访问底层网络协议。protocol:指定协议类型。一般情况下,你可以设置为 0,这样系统会根据
domain
和type
自动选择合适的协议。例如,对于SOCK_STREAM
类型的套接字,系统会默认使用 TCP 协议;对于SOCK_DGRAM
类型的套接字,系统会默认使用 UDP 协议。3. 返回值
- 成功时,
socket
函数返回一个非负整数,这个整数是套接字的描述符。- 失败时,返回
-1
,并设置errno
以指示错误类型。4. 错误处理
当
socket
函数失败时,你可以通过errno
获取错误代码。常见的错误代码有:
EAFNOSUPPORT
:不支持指定的地址族。EINVAL
:提供了无效的参数。PROTONOSUPPORT
:不支持指定的协议。
bind函数
函数功能:
bind
函数在网络编程中用于将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。1. 函数原型
在 C 语言和 POSIX 标准下,
bind
函数的原型如下:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2. 参数说明
sockfd:套接字描述符,这是由
socket
函数创建并返回的套接字。addr:指向
sockaddr
结构体的指针,用于指定要绑定的本地地址。通常会使用具体的结构体如sockaddr_in
(用于 IPv4 地址)或sockaddr_in6
(用于 IPv6 地址)。addrlen:
addr
指向的地址结构体的长度。对于sockaddr_in
,通常是sizeof(struct sockaddr_in)
。3.
sockaddr
结构体
sockaddr_in
(用于 IPv4):struct sockaddr_in { sa_family_t sin_family; // 地址族,通常为 AF_INET uint16_t sin_port; // 端口号(网络字节顺序) struct in_addr sin_addr; // IP 地址 }; struct in_addr { uint32_t s_addr; // IP 地址(网络字节顺序) };
sockaddr_in6
(用于 IPv6):struct sockaddr_in6 { sa_family_t sin6_family; // 地址族,通常为 AF_INET6 uint16_t sin6_port; // 端口号(网络字节顺序) uint32_t sin6_flowinfo; // 流量信息 struct in6_addr sin6_addr; // IPv6 地址 uint32_t sin6_scope_id; // 范围 ID }; struct in6_addr { unsigned char s6_addr[16]; // IPv6 地址 };
4. 返回值
- 成功时,返回
0
。- 失败时,返回
-1
,并设置errno
以指示错误类型。5. 错误处理
常见的错误代码包括:
EADDRINUSE
:地址已经在使用中,通常是端口被占用。EADDRNOTAVAIL
:提供的地址在本地不可用。EINVAL
:提供了无效的参数。ENOTSOCK
:描述符不是一个套接字。6. 示例代码
以下是一个简单的 C 语言示例,演示如何将一个套接字绑定到一个特定的本地地址和端口:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> int main() { int sockfd; struct sockaddr_in server_addr; // 创建 UDP 套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket"); exit(EXIT_FAILURE); } // 配置服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口 server_addr.sin_port = htons(12345); // 设置端口号(转换为网络字节顺序) // 绑定套接字到本地地址 if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); close(sockfd); exit(EXIT_FAILURE); } printf("UDP socket successfully bound to port 12345.\n"); // 关闭套接字 close(sockfd); return 0; }
7. 使用场景
bind
函数通常在服务器端使用,绑定套接字到特定的 IP 地址和端口,以便监听来自客户端的连接请求。在客户端,通常不需要显式地调用bind
,除非你需要绑定到特定的本地地址和端口。
listen函数
listen()
函数是用于将一个套接字(socket)设置为被动模式,等待来自客户端的连接请求。通常,listen()
函数用在服务器端程序中,用来监听某个特定的端口,等待客户端发起连接请求。函数定义
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog);
参数说明
sockfd
:
- 这是一个套接字描述符(socket descriptor),是通过
socket()
函数返回的文件描述符。这个套接字应该是通过bind()
绑定到某个特定的地址和端口的。
backlog
:
- 这是一个整数,表示允许的最大未决连接数(pending connections)。当一个客户端尝试连接但还没有被
accept()
函数接受时,连接会被放入队列中。backlog
参数定义了这个队列的最大长度。返回值
- 成功时,
listen()
返回 0。- 失败时,返回 -1,并设置
errno
以指示错误类型。详细说明
套接字类型:
listen()
函数只能用于面向连接的套接字类型,比如SOCK_STREAM
(TCP)。它不能用于无连接的套接字类型,比如SOCK_DGRAM
(UDP)。被动模式:
- 调用
listen()
函数后,套接字进入被动模式,意味着它将等待来自客户端的连接请求,而不是主动去连接其他主机。backlog 参数:
backlog
参数定义了内核应该为这个套接字排队的最大连接数。包括以下两种连接:
- 未完成连接: 三次握手尚未完成的连接。
- 已完成连接: 已经完成三次握手但尚未被
accept()
函数接受的连接。- 当队列满时,新的连接请求将被拒绝。
accept函数
accept()
函数在 Linux 网络编程中用于从已经监听的套接字中接受一个连接请求。它是 TCP 服务器实现的核心部分,通常在调用了listen()
函数后使用。接下来,我们将详细介绍accept()
函数的定义、用法及其关键点。函数定义
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
sockfd
:
- 这是一个套接字描述符,指定了要接受连接的监听套接字。该套接字必须是在调用
listen()
函数后创建的。
addr
:
- 这是一个指向
sockaddr
结构的指针,用于存储连接客户端的地址信息。可以传入NULL
,表示不需要获取客户端的地址。
addrlen
:
- 这是一个指向
socklen_t
变量的指针,表示addr
所指向的结构体的长度。在调用accept()
前,需要将其设定为sizeof(struct sockaddr)
。接受连接后,addrlen
将被设置为实际存储的地址长度。返回值
- 成功时,
accept()
返回一个新的套接字描述符,用于与已连接的客户端进行通信。- 失败时,返回 -1,并设置
errno
以指示错误类型。详细说明
阻塞行为:
accept()
是一个阻塞调用,意味着如果没有连接请求到来,它将一直等待直到有新的连接。因此,在设计服务器时,通常会把accept()
放在一个循环中,以便处理多个连接。新套接字描述符:
- 每次调用
accept()
成功后,都会返回一个新的套接字描述符,用于与客户端进行通信,而原始的监听套接字仍然可以继续接受其他的连接。安全性:
- 由于
accept()
是阻塞的,您可能需要采用多线程机制或者非阻塞 I/O,以便允许服务器同时处理多个连接。
connect函数
connect()
函数在 Linux 网络编程中用于客户端程序中,旨在请求与服务器建立连接。该函数通常在创建了套接字之后调用,通过指定服务器的 IP 地址和端口来发起连接请求。函数定义
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
sockfd
:
- 这是一个套接字描述符,指定了客户端用于连接的套接字。该套接字必须是通过
socket()
函数创建的。
addr
:
- 这是一个指向
sockaddr
结构的指针,包含了服务器的 IP 地址和端口信息。一般使用struct sockaddr_in
来进行 IPv4 地址的配置。
addrlen
:
- 这是
addr
结构的长度,通常设置为sizeof(struct sockaddr)
。返回值
- 成功时,
connect()
返回 0。- 失败时,返回 -1,并设置
errno
以指示错误类型。详细说明
建立连接:
connect()
用于在 TCP 套接字上发起连接请求。对于 TCP 协议,客户端通过调用connect()
函数发起三次握手过程,以建立与服务器的连接。- 对于 UDP 套接字,
connect()
只是设置默认的对端地址,并不会真正发起连接请求,因为 UDP 是无连接的协议。阻塞行为:
connect()
默认是阻塞的,这意味着在发起连接请求后,它会一直等待直到连接成功或失败。如果连接超时,它将返回错误。错误处理:
- 常见的错误包括:
ECONNREFUSED
: 连接被拒绝,通常是因为服务器没有监听指定端口。ETIMEDOUT
: 连接超时。ENETUNREACH
: 网络不可达。
recv()
和send()
函数在 Linux 网络编程中用于在已连接的套接字上进行数据的发送和接收。这两个函数是 TCP/IP 通信中数据传输的核心接口。
recv()
函数
recv()
函数用于从已连接的套接字接收数据。函数定义
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明
sockfd
:
- 这是一个套接字描述符,指定了要从中接收数据的套接字。
buf
:
- 这是一个指向接收数据缓冲区的指针。
recv()
函数将接收的数据存储在这个缓冲区中。
len
:
- 这是缓冲区的长度,单位为字节。
recv()
函数最多将接收len
字节的数据。
flags
:
- 这是一个位掩码,用于指定接收操作的各种选项。常用选项包括:
MSG_OOB
: 接收带外数据。MSG_PEEK
: 窥视输入数据,但不从输入队列中移除它。MSG_WAITALL
: 等待接收所有请求的数据量。返回值
- 成功时,
recv()
返回接收到的字节数。- 如果连接被正常关闭,返回 0。
- 失败时,返回 -1,并设置
errno
以指示错误类型。--------------------------------------------------------------------------------------------------------------------------
send()
函数
send()
函数用于向已连接的套接字发送数据。函数定义
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明
sockfd
:
- 这是一个套接字描述符,指定了要发送数据的套接字。
buf
:
- 这是一个指向发送数据缓冲区的指针。
send()
函数将从这个缓冲区发送数据。
len
:
- 这是要发送的数据的长度,单位为字节。
flags
:
- 这是一个位掩码,用于指定发送操作的各种选项。常用选项包括:
MSG_OOB
: 发送带外数据。MSG_DONTROUTE
: 绕过路由表查找直接发送数据。MSG_NOSIGNAL
: 发送数据时忽略SIGPIPE
信号。返回值
- 成功时,
send()
返回发送的字节数。- 失败时,返回 -1,并设置
errno
以指示错误类型。详细说明
阻塞与非阻塞:
- 默认情况下,
recv()
和send()
是阻塞的,这意味着在数据传输完成之前,调用不会返回。- 如果套接字被设置为非阻塞模式(通过
fcntl()
设置O_NONBLOCK
标志),则recv()
和send()
会立即返回,即使操作尚未完成。数据完整性:
recv()
可能不会一次接收完所有请求的数据量。如果需要确保接收完所有数据,可以使用MSG_WAITALL
标志,或者在循环中重复调用recv()
直到接收完所有数据。send()
也可能不会一次发送完所有数据。如果需要确保发送完所有数据,可以在循环中重复调用send()
直到发送完所有数据。错误处理:
- 常见的
recv()
错误包括:
EAGAIN
或EWOULDBLOCK
: 套接字为非阻塞模式,且没有数据可读。ECONNRESET
: 连接被对端重置。ENOTCONN
: 套接字未连接。- 常见的
send()
错误包括:
EAGAIN
或EWOULDBLOCK
: 套接字为非阻塞模式,且缓冲区已满。EPIPE
: 对端已经关闭连接,且本地发送的数据将被丢弃。ENOTCONN
: 套接字未连接。注意事项
缓冲区管理:
- 在实际应用中,重要的是管理缓冲区的大小和内容,确保不会产生缓冲区溢出或数据截断问题。
数据完整性:
- 对于大数据传输,可能需要多次调用
recv()
或send()
以确保数据的完整性。错误处理:
- 在实际应用中,需要对
recv()
和send()
函数的返回值进行详细检查,以处理各种网络错误和异常情况。
【除了 recv()
和 send()
函数之外,在 Linux 网络编程中还可以使用 read()
和 write()
函数来进行数据的接收和发送。这两个函数更为通用,不仅适用于网络套接字,还可以用于文件、管道等其他类型的文件描述符。 】
四、TCP服务端代码详解
服务端代码思路:
通过服务端的代码逻辑,我们了解到服务端所要做的是:1、建立套接字;2、将套接字设置为监听状态监听客户端的连接请求;3、与客户端建立连接;4、接收并处理来自客户端的请求;5、将处理的结果发送给客户端。
我们发现:第1、2、3步是服务端的固定工作,不会因为服务端工作的不同而改变。而第4、5步会随着服务端处理任务的不同而进行相应的改变。
因此,我们可以将处理任务的功能与服务端解耦,在服务端代码内部只管执行上层传递给服务端的任务函数,其他一概不问。而任务处理方式的具体实现则交给上层代码实现。因此,我们可以在构建服务端时将需要处理的任务函数作为参数参与服务端的构造初始化。需要注意的是,由于任务函数的处理必然涉及到套接字通信,所以我们在设计接口时要将IO套接字和客户端的IP地址+端口号作为任务函数的参数。
1、初始化服务端
using Tcp_Server_FuncType = std::function<void(int, InetAddr)>; // 注:InetAddr 为类,该对象中包含目标端的IP地址、端口号
class Tcp_Server
{
private:
int _listen_sockfd; // 监听套接字,使用listen函数设置为监听态,负责监听来自客户端的连接请求
bool _is_running; // 运行状态
uint16_t _port; // 服务端端口号
Tcp_Server_FuncType _tcp_service; // 服务端需要执行的任务对象
public:
// 构造函数,提供端口号
Tcp_Server(Tcp_Server_FuncType tcp_service, uint16_t port = gport)
: _port(port), _listen_sockfd(-1), _is_running(false), _tcp_service(tcp_service)
{
_listen_sockfd = -1;
}
// 初始化服务端
void InitServer()
{
// 1、 创建监听套接字
_listen_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sockfd < 0)
{
LOG(FATAL, "Sockfd Create False!\n");
exit(-1);
}
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(_port);
local_addr.sin_family = AF_INET;
// 2、绑定本地ip地址和port端口号
if (::bind(_listen_sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0)
{
LOG(FATAL, "Sockfd Bind False!\n");
exit(-1);
}
LOG(DEBUG, "Sockfd Bind Success!\n");
// 3、将套接字设置为【监听状态】, 以监听来自客户端的连接请求
if (::listen(_listen_sockfd, MAX_LEN) < 0)
{
LOG(FATAL, "Sockfd Listen False!\n");
exit(-1);
}
LOG(DEBUG, "Sockfd Listen Success!\n");
}
};
2、启动服务端
我们知道,服务端的工作就是不断接收信息、处理任务、返回结果。以上操作不仅会受到IO操作的阻塞、还可能让服务端一直处于长任务的处理过程中。在多个客户端连接同一个服务端时,如果服务端是单执行流的,这势必会大大降低服务端的吞吐量。因此,我们将服务端的IO操作和处理任务的动作与主执行流分离。让服务端一直处于接收客户端请求的动作中。当有新的客户端连接服务端后,我们就再创建一个执行流,让该执行流去处理客户端的请求。如此,既能保证服务端可以并发连接多个客户端且不造成阻塞,还能提高服务端的效率。
对于以上操作的实现,我们可以采取多进程、多线程的方式。
我们已经知道,无论是IO阻塞还是长服务的执行都会使得单执行流的服务端处于阻塞状态,以至于在有新的客户端对服务端进行连接请求时不能及时响应,因为此时服务端处于阻塞/处理任务的状态中,无法再使用accept函数响应新的连接请求。因此我们需要采用多进程/多线程的方式去创建新的执行流来执行任务。
但是!!!重点来了:无论是多进程还是多线程在退出时都需要主执行流进行进程/线程等待以进行资源回收,而这个过程大概率是阻塞的!!!因此,我们绝对不能使用等待函数来回收进程/线程资源。因为势必会造成主执行流的阻塞!
那有什么办法可以解决呢?把需要回收的资源交给系统来自行回收!!!主执行流不参与回收操作!如此,进程/线程的资源既可以被回收,主执行流也不会被阻塞。
需要注意的是,子进程会继承父进程的文件描述符表。而套接字描述符也在文件描述符表中。所以,在父子进程中,为防止意外和系统资源浪费,我们需要关闭掉该进程中不需要使用的文件描述符。如:父进程需要监听请求,但不需要进行通信IO,所以关闭掉IO套接字;子进程只需要进行IO操作,所以关闭掉监听套接字。
【1】、多进程版本1:主执行流创建儿子进程,儿子进程创建孙子进程。使用孙子进程执行任务函数,儿子进程直接退出,由此孙子进程变成孤儿进程由系统领养回收,以解决waitpid函数阻塞造成的串行通信问题和可能造成的僵尸进程的问题。
// 运行服务端
void Loop()
{
_is_running = true;
while (_is_running)
{
// 1、获取来自客户端的连接请求,并获得I/O专用套接字
struct sockaddr_in from_client;
socklen_t addr_len = sizeof(from_client);
memset(&from_client, 0, addr_len);
int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
if (_io_sockfd < 0)
{
LOG(FATAL, "Sockfd Accept False!");
exit(-1);
}
LOG(DEBUG, "Sockfd Accept Success!");
// 1、多进程版本
int _pid = fork();
if (_pid < 0)
{
LOG(FATAL, "Fork False!");
exit(-1);
}
else if (_pid > 0)
{
close(_listen_sockfd); // 关闭不需要的文件描述符
int child_pid = fork(); // 子进程再创建一个子进程
if (_pid < 0)
{
LOG(FATAL, "Child Fork False!");
exit(-1);
}
else if (child_pid > 0)
{
exit(0);
}
else // 孙子进程执行任务函数,儿子进程退出,孙子进程称为孤儿进程,由系统进行回收
{
_tcp_service(_io_sockfd, from_client); // 处理任务
close(_io_sockfd);
}
}
else
{
close(_io_sockfd);
// 父进程等待退出的儿子进程
int res_pid = waitpid(_pid, nullptr, 0); // 虽然是阻塞等待,但是任务函数由孙子进程去执行,儿子进程在创建完孙子进程后直接退出,因此可以直接进行回收
if (res_pid < 0)
{
LOG(FATAL, "Waitpid False!");
exit(-1);
}
LOG(DEBUG, "Waitpid Success!");
}
}
_is_running = false;
}
【2】、多进程版本2:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
// 运行服务端
void Loop()
{
// 多进程版本 version 2
// 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
// 系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
signal(SIGCHLD, SIG_IGN);
// 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
// 既保证了程序的并发性,也保证了对僵尸进程的清理
_is_running = true;
while (_is_running)
{
// 1、获取来自客户端的连接请求,并获得I/O专用套接字
struct sockaddr_in from_client;
socklen_t addr_len = sizeof(from_client);
memset(&from_client, 0, addr_len);
int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
if (_io_sockfd < 0)
{
LOG(FATAL, "Sockfd Accept False!");
exit(-1);
}
LOG(DEBUG, "Sockfd Accept Success!");
// 1、多进程版本
int _pid = fork();
if (_pid < 0)
{
LOG(FATAL, "Fork False!");
exit(-1);
}
else if (_pid > 0)
{
close(_listen_sockfd); // 关闭不需要的文件描述符
_tcp_service(_io_sockfd, from_client); // 处理任务
close(_io_sockfd);
}
else
{
close(_io_sockfd);
}
}
_is_running = false;
}
【3】、多线程版本1:使用线程分离函数将线程设置为“分离状态”。在分离状态下,线程的资源会在其终止时自动释放,而无需其他线程调用 pthread_join 来显式回收这些资源。在进入线程函数时,使用pthread_detach(pthread_self())将线程自身分离即可。
class Tcp_Server
{
// 内部类:线程数据
// 内部类(嵌套类)和所属外部类(包含类)之间的关系是可以相互访问对方的私有成员的。
class ThreadData
{
public:
int _io_sockfd; // 进行io通信的套接字描述符
Tcp_Server *_self; // Tcp_Server类指针,用于调取该类中的函数方法
InetAddr _net_addr; // 包含:ip + port
public:
ThreadData(int io_sockfd, Tcp_Server *self, InetAddr net_addr)
: _io_sockfd(io_sockfd), _self(self), _net_addr(net_addr)
{
}
};
static void *ThreadRoute(void *thread_data)
{
// 1、将该线程设置为分离态,该线程运行结束后系统自动回收资源
pthread_detach(pthread_self());
// 2、运行任务函数
ThreadData *thread_self_data = static_cast<ThreadData *>(thread_data);
thread_self_data->_self->_tcp_service(thread_self_data->_io_sockfd, thread_self_data->_net_addr);
delete thread_self_data;
return nullptr;
}
// 运行服务端
void Loop()
{
// 多线程版本
// 使用线程分离函数将线程设置为“分离状态”。在分离状态下,线程的资源会在其终止时自动释放,而无需其他线程调用 pthread_join 来显式回收这些资源。
_is_running = true;
while (_is_running)
{
// 1、获取来自客户端的连接请求,并获得I/O专用套接字
struct sockaddr_in from_client;
socklen_t addr_len = sizeof(from_client);
memset(&from_client, 0, addr_len);
int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
if (_io_sockfd < 0)
{
LOG(FATAL, "Sockfd Accept False!");
exit(-1);
}
LOG(DEBUG, "Sockfd Accept Success!");
pthread_t tid = 0;
ThreadData *thread_data = new ThreadData(_io_sockfd, this, from_client);
// 线程需要执行类中的Service函数,同时主线程不能对该线程进行等待回收,所以需要该线程进行线程分离,让线程退出后自动由系统回收
if (pthread_create(&tid, nullptr, ThreadRoute, thread_data) < 0)
{
LOG(FATAL, "Thread Create False!");
exit(-1);
}
}
_is_running = false;
}
};
【4】、多线程版本2:接入线程池,将任务交给线程池处理
// 运行服务端
void Loop()
{
// 线程池版本
using service_task_t = std::function<void()>;
_is_running = true;
while (_is_running)
{
// 1、获取来自客户端的连接请求,并获得I/O专用套接字
struct sockaddr_in from_client;
socklen_t addr_len = sizeof(from_client);
memset(&from_client, 0, addr_len);
int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
if (_io_sockfd < 0)
{
LOG(FATAL, "Sockfd Accept False!");
exit(-1);
}
LOG(DEBUG, "Sockfd Accept Success!");
service_task_t excute_task = std::bind(_tcp_service, _io_sockfd, from_client); // 绑定参数
ThreadPool<service_task_t>::GetInstance()->Push(excute_task); // 创建并启动线程池,向线程池中推送任务
}
_is_running = false;
ThreadPool<service_task_t>::GetInstance()->Stop(); // 终止线程池
}
服务端完整代码:
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include "ThreadPool.hpp"
#include <functional>
static uint16_t gport = 8888;
static const int MAX_LEN = 5;
static const int BUFFER_SIZE = 256;
using Tcp_Server_FuncType = std::function<void(int, InetAddr)>; // 注:InetAddr 为类,该对象中包含目标端的IP地址、端口号
class Tcp_Server
{
private:
int _listen_sockfd; // 监听套接字,使用listen函数设置为监听态,负责监听来自客户端的连接请求
bool _is_running;
uint16_t _port;
Tcp_Server_FuncType _tcp_service; // 服务端需要执行的任务对象
public:
// 构造函数,提供端口号
Tcp_Server(Tcp_Server_FuncType tcp_service, uint16_t port = gport)
: _port(port), _listen_sockfd(-1), _is_running(false), _tcp_service(tcp_service)
{
_listen_sockfd = -1;
}
void InitServer()
{
// 1、 创建监听套接字
_listen_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sockfd < 0)
{
LOG(FATAL, "Sockfd Create False!\n");
exit(-1);
}
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(_port);
local_addr.sin_family = AF_INET;
// 2、绑定本地ip地址和port端口号
if (::bind(_listen_sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0)
{
LOG(FATAL, "Sockfd Bind False!\n");
exit(-1);
}
LOG(DEBUG, "Sockfd Bind Success!\n");
// 3、将套接字设置为【监听状态】, 以监听来自客户端的连接请求
if (::listen(_listen_sockfd, MAX_LEN) < 0)
{
LOG(FATAL, "Sockfd Listen False!\n");
exit(-1);
}
LOG(DEBUG, "Sockfd Listen Success!\n");
}
// 运行服务端
void Loop()
{
// // 多进程版本1
// _is_running = true;
// while (_is_running)
// {
// // 1、获取来自客户端的连接请求,并获得I/O专用套接字
// struct sockaddr_in from_client;
// socklen_t addr_len = sizeof(from_client);
// memset(&from_client, 0, addr_len);
// int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
// if (_io_sockfd < 0)
// {
// LOG(FATAL, "Sockfd Accept False!");
// exit(-1);
// }
// LOG(DEBUG, "Sockfd Accept Success!");
// // 1、多进程版本
// int _pid = fork();
// if (_pid < 0)
// {
// LOG(FATAL, "Fork False!");
// exit(-1);
// }
// else if (_pid > 0)
// {
// close(_listen_sockfd); // 关闭不需要的文件描述符
// int child_pid = fork(); // 子进程再创建一个子进程
// if (_pid < 0)
// {
// LOG(FATAL, "Child Fork False!");
// exit(-1);
// }
// else if (child_pid > 0)
// {
// exit(0);
// }
// else // 孙子进程执行任务函数,儿子进程退出,孙子进程称为孤儿进程,由系统进行回收
// {
// _tcp_service(_io_sockfd, from_client); // 处理任务
// close(_io_sockfd);
// }
// }
// else
// {
// close(_io_sockfd);
// // 父进程等待退出的儿子进程
// int res_pid = waitpid(_pid, nullptr, 0); // 虽然是阻塞等待,但是任务函数由孙子进程去执行,儿子进程在创建完孙子进程后直接退出,因此可以直接进行回收
// if (res_pid < 0)
// {
// LOG(FATAL, "Waitpid False!");
// exit(-1);
// }
// LOG(DEBUG, "Waitpid Success!");
// }
// }
// _is_running = false;
// -------------------------------------------------------------------------------------------
// // 多进程版本 version 2
// // 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
// // 系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
// signal(SIGCHLD, SIG_IGN);
// // 父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
// // 既保证了程序的并发性,也保证了对僵尸进程的清理
// _is_running = true;
// while (_is_running)
// {
// // 1、获取来自客户端的连接请求,并获得I/O专用套接字
// struct sockaddr_in from_client;
// socklen_t addr_len = sizeof(from_client);
// memset(&from_client, 0, addr_len);
// int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
// if (_io_sockfd < 0)
// {
// LOG(FATAL, "Sockfd Accept False!");
// exit(-1);
// }
// LOG(DEBUG, "Sockfd Accept Success!");
// // 1、多进程版本
// int _pid = fork();
// if (_pid < 0)
// {
// LOG(FATAL, "Fork False!");
// exit(-1);
// }
// else if (_pid > 0)
// {
// close(_listen_sockfd); // 关闭不需要的文件描述符
// _tcp_service(_io_sockfd, from_client); // 处理任务
// close(_io_sockfd);
// }
// else
// {
// close(_io_sockfd);
// }
// }
// _is_running = false;
// -------------------------------------------------------------------------------------------
// 多线程版本
// 使用线程分离函数将线程设置为“分离状态”。在分离状态下,线程的资源会在其终止时自动释放,而无需其他线程调用 pthread_join 来显式回收这些资源。
_is_running = true;
while (_is_running)
{
// 1、获取来自客户端的连接请求,并获得I/O专用套接字
struct sockaddr_in from_client;
socklen_t addr_len = sizeof(from_client);
memset(&from_client, 0, addr_len);
int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
if (_io_sockfd < 0)
{
LOG(FATAL, "Sockfd Accept False!");
exit(-1);
}
LOG(DEBUG, "Sockfd Accept Success!");
pthread_t tid = 0;
ThreadData *thread_data = new ThreadData(_io_sockfd, this, from_client);
// 线程需要执行类中的Service函数,同时主线程不能对该线程进行等待回收,所以需要该线程进行线程分离,让线程退出后自动由系统回收
if (pthread_create(&tid, nullptr, ThreadRoute, thread_data) < 0)
{
LOG(FATAL, "Thread Create False!");
exit(-1);
}
}
_is_running = false;
// -------------------------------------------------------------------------------------------
// // 线程池版本
// using service_task_t = std::function<void()>;
// _is_running = true;
// while (_is_running)
// {
// // 1、获取来自客户端的连接请求,并获得I/O专用套接字
// struct sockaddr_in from_client;
// socklen_t addr_len = sizeof(from_client);
// memset(&from_client, 0, addr_len);
// int _io_sockfd = accept(_listen_sockfd, (struct sockaddr *)&from_client, &addr_len);
// if (_io_sockfd < 0)
// {
// LOG(FATAL, "Sockfd Accept False!");
// exit(-1);
// }
// LOG(DEBUG, "Sockfd Accept Success!");
// service_task_t excute_task = std::bind(_tcp_service, _io_sockfd, from_client); // 绑定参数
// ThreadPool<service_task_t>::GetInstance()->Push(excute_task); // 创建并启动线程池,向线程池中推送任务
// }
// _is_running = false;
// ThreadPool<service_task_t>::GetInstance()->Stop(); // 终止线程池
}
// 内部类:线程数据
// 内部类(嵌套类)和所属外部类(包含类)之间的关系是可以相互访问对方的私有成员的。
class ThreadData
{
public:
int _io_sockfd; // 进行io通信的套接字描述符
Tcp_Server *_self; // Tcp_Server类指针,用于调取该类中的函数方法
InetAddr _net_addr; // ip + port
public:
ThreadData(int io_sockfd, Tcp_Server *self, InetAddr net_addr)
: _io_sockfd(io_sockfd), _self(self), _net_addr(net_addr)
{
}
};
static void *ThreadRoute(void *thread_data)
{
// 1、将该线程设置为分离态,该线程运行结束后系统自动回收资源
pthread_detach(pthread_self());
// 2、运行任务函数
ThreadData *thread_self_data = static_cast<ThreadData *>(thread_data);
thread_self_data->_self->_tcp_service(thread_self_data->_io_sockfd, thread_self_data->_net_addr);
delete thread_self_data;
return nullptr;
}
};
五、客户端代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include <cstring>
#include <signal.h>
#include <sys/wait.h>
#include "Log.hpp"
static const int BUFFER_SIZE = 256;
class Tcp_Client
{
private:
int _sockfd;
std::string _to_server_ip;
uint16_t _to_server_port;
bool _is_running;
public:
Tcp_Client(const std::string &ip, const uint16_t port)
: _to_server_ip(ip), _to_server_port(port)
{
_sockfd = -1;
}
void InitClient()
{
// 1、创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "Client Sockfd Create False!\n");
exit(-1);
}
// 客户端无需手动绑定bind,在使用connect函数连接时,自动绑定IP地址和端口号
// 2、连接服务端
struct sockaddr_in to_server;
memset(&to_server, 0, sizeof(to_server));
to_server.sin_addr.s_addr = inet_addr(_to_server_ip.c_str());
to_server.sin_family = AF_INET;
to_server.sin_port = htons(_to_server_port);
if (connect(_sockfd, (struct sockaddr *)&to_server, sizeof(to_server)) < 0)
{
LOG(FATAL, "Client Connect False!\n");
exit(-1);
}
LOG(DEBUG, "Client Connect Success!\n");
}
// 启动客户端
void Start()
{
_is_running = true;
while(_is_running)
{
std::cout << "Please Enter # ";
std::string message;
std::getline(std::cin, message);
// 将信息发送给服务端
int r_num = 0, w_num = 0;
if((w_num = write(_sockfd, message.c_str(), message.size())) < 0)
{
LOG(FATAL, "Client Write To Server False!\n");
exit(-1);
}
LOG(DEBUG, "Client Write To Server Success!\n");
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
if((r_num = read(_sockfd, buffer, BUFFER_SIZE - 1)) < 0)
{
LOG(FATAL, "Client Write To Server False!\n");
exit(-1);
}
buffer[r_num] = '\0';
std::cout << buffer << std::endl;
}
_is_running = false;
}
};
六、使用TCP通信实现远程命令执行
程序功能:
popen函数
popen
函数是 C 语言标准库中用于创建进程的一种方法,它能够启动一个子进程并与该进程进行管道通信。popen
定义在<stdio.h>
头文件中,其基本语法如下:FILE *popen(const char *command, const char *mode);
参数说明
- command: 要执行的命令字符串,通常是一个可以在 shell 中直接执行的命令。
- mode: 指定打开管道的方式,通常是
"r"
(读取) 或"w"
(写入)。如果选择"r"
,则可以从子进程中读取数据;如果选择"w"
,则可以向子进程写入数据。返回值
- 成功时,
popen
返回一个指向FILE
对象的指针,该对象可以用于对进程进行读写操作。- 失败时,返回
NULL
,并且可以通过errno
获取错误信息。使用步骤
- 调用
popen
启动进程并获得 FILE 指针。- 使用标准的 I/O 函数(如
fgets
、fprintf
等)与子进程进行读写操作。- 调用
pclose
关闭管道并回收资源。示例代码
下面是一个使用
popen
读取ls
命令输出的简单示例:#include <stdio.h> #include <stdlib.h> int main() { FILE *fp; char path[1035]; // 开启一个进程读取 ls 命令的输出 fp = popen("ls -l", "r"); if (fp == NULL) { perror("popen failed"); return EXIT_FAILURE; } // 读取输出内容 while (fgets(path, sizeof(path), fp) != NULL) { printf("%s", path); } // 关闭进程 if (pclose(fp) == -1) { perror("pclose failed"); return EXIT_FAILURE; } return EXIT_SUCCESS; }
注意事项
- 安全性: 使用
popen
执行命令时,应避免将用户输入直接传递给命令,以防止命令注入攻击。- 跨平台兼容性:
popen
在不同操作系统中的实现可能存在差异,特别是在 Windows 和类 Unix 系统中。- Buffering: 子进程的输出可能会被缓冲,可以根据需要使用
setvbuf
或其他方法来控制输出。
命令类的封装 :
#pragma once
#include <cstdio>
#include <string>
#include "InternetAddr.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include <set>
// 命令类
class Command
{
private:
std::set<std::string> _check_table; // 哈希表,用于存储允许执行的命令
public:
Command()
{
Init_CheckTabnle();
}
// 初始化允许被执行的命令表
void Init_CheckTabnle()
{
_check_table.insert("ls");
_check_table.insert("touch");
_check_table.insert("pwd");
}
// 判断输入的命令是否存在于命令表中
bool IsExistInCheckTable(const std::string &target)
{
for (auto &str : _check_table)
{
if (strncmp(str.c_str(), target.c_str(), str.size()) == 0)
{
return true;
}
}
return false;
}
// 执行命令实体函数,并返回命令执行后的打印结果
std::string Excute(const std::string &cmd_line)
{
// 1、先判断命令是否存在于命令表中
if (IsExistInCheckTable(cmd_line))
{
// 2、获取popen函数返回的文件流指针
FILE *fp = popen(cmd_line.c_str(), "r");
char cmd_info[1024];
std::string res;
// 3、从文件流中读取内容存储在字符串中
// fgets 会在读取的字符串末尾自动添加空字符(\0),以保证字符串的正确结束。这意味着 fgets 最多读取 n-1 个字符,并保证以空字符结束字符串。
while (fgets(cmd_info, sizeof cmd_info, fp))
{
res += cmd_info;
}
// 部分命令,如touch没有返回值,但执行流走到这个位置就代表已经执行成功,返回success或执行结果
return res.empty() ? "Success!" : res;
}
else
{
return "This command is not allowed!";
}
}
// 处理命令
void HandleCommand(int io_sockfd, InetAddr net_addr)
{
// 长服务,死循环
while (true)
{
char buffer[1024];
int r_num = 0;
// 1、读取来自客户端发送的命令字符串
if ((r_num = recv(io_sockfd, buffer, sizeof(buffer) - 1, 0)) < 0)
{
LOG(FATAL, "Read From Server Sockfd False!\n");
exit(-1);
}
else if (r_num == 0)
{
LOG(INFO, "Read Quit!\n");
break;
}
else
{
buffer[r_num] = '\0';
std::string from_message = "From Client # ";
from_message += buffer;
std::cout << from_message << std::endl;
LOG(INFO, "get message from client %s, message: %s\n", net_addr.AddrStr().c_str(), buffer);
// 2、获取命令执行结果
std::string to_message = "From Server # \n";
to_message += Excute(buffer);
int w_num = 0;
// 3、向客户端发送命令执行结果
if ((w_num = send(io_sockfd, to_message.c_str(), to_message.size(), 0)) < 0)
{
LOG(FATAL, "Server Write To Client False!\n");
exit(-1);
}
LOG(DEBUG, "Server Write To Client Success!\n");
}
}
}
};
服务端主函数:
#include "Tcp_Server.hpp"
#include "Command.hpp"
//在命令行需自主输入绑定的端口号
int main(int argc, char* argv[])
{
if(argc < 2){
std::cout << "未输入端口号..." << std::endl;
exit(-1);
}
Command command;
uint16_t port = std::stoi(argv[1]);
// 绑定命令类中的命令处理方法,作为服务端的执行函数构造服务端
Tcp_Server server(std::bind(&Command::HandleCommand, &command, std::placeholders::_1, std::placeholders::_2), port);
server.InitServer(); // 初始化服务端
server.Loop(); // 启动服务端
return 0;
}
客户端主函数:
#include "Tcp_Client.hpp"
//在命令行需自主输入目标客户端的IP地址和需要绑定的端口号
int main(int argc, char* argv[])
{
if(argc < 3){
std::cout << "命令行参数过少..." << std::endl;
exit(-1);
}
// 构造服务端
Tcp_Client client(argv[1], std::stoi(argv[2]));
client.InitClient(); // 构造客户端
client.Start(); // 启动客户端
return 0;
}
对 struct sockaddr_in 结构体对象的封装:
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
// 将网络字节序转换为主机字节序
void ToHost(const struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
char ip_buf[32];
::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
_ip = ip_buf;
}
public:
InetAddr(const struct sockaddr_in &addr):_addr(addr)
{
ToHost(addr);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip && this->_port == addr._port);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr_in Addr()
{
return _addr;
}
std::string AddrStr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{
}
private:
std::string _ip; // IP地址
uint16_t _port; // 端口号
struct sockaddr_in _addr; // 需要进行字节序转换的结构体对象
};
原文地址:https://blog.csdn.net/2301_76606232/article/details/142885299
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!