第三部分:网络编程
3.1 套接字编程(TCP/UDP基础)
3.1.1 套接字基础
3.1.1.1 套接字概念与类型
套接字(Socket)是网络编程的基础,它提供了一种进程间通信的机制。根据传输特点,套接字主要分为以下两种类型:
- 流套接字(Stream Socket):适用于TCP协议,提供了可靠、面向连接的字节流服务。数据传输有序、不丢失且无重复。
- 数据报套接字(Datagram Socket):适用于UDP协议,提供了无连接、尽力而为的服务。不保证数据按顺序到达、数据可能丢失或重复。
3.1.1.2 套接字地址结构
套接字地址用于表示网络上的主机和端口,包括以下几种常用结构:
-
sockaddr
:通用套接字地址结构,通常作为其他具体地址结构的基类使用。 -
sockaddr_in
:用于IPv4地址,包括如下成员:cstruct sockaddr_in { short int sin_family; // 地址族(AF_INET) unsigned short int sin_port; // 端口号 struct in_addr sin_addr; // IP地址 unsigned char sin_zero[8]; // 填充字节,保证结构大小 };
-
sockaddr_in6
:用于IPv6地址,包括如下成员:cstruct sockaddr_in6 { u_int16_t sin6_family; // 地址族(AF_INET6) u_int16_t sin6_port; // 端口号 u_int32_t sin6_flowinfo; // 流量信息 struct in6_addr sin6_addr; // IPv6地址 u_int32_t sin6_scope_id; // Scope ID };
3.1.1.3 网络字节序与主机字节序转换
在网络编程中,数据的字节序(即在内存中存储多字节数据的方式)是一个重要概念。网络协议通常使用"大端字节序"(Big-endian),而不同主机可能使用"小端字节序"(Little-endian)。以下函数用于在主机字节序和网络字节序之间转换:
-
htonl
(Host to Network Long):将32位整数从主机字节序转换为网络字节序。cuint32_t htonl(uint32_t hostlong);
-
ntohl
(Network to Host Long):将32位整数从网络字节序转换为主机字节序。cuint32_t ntohl(uint32_t netlong);
-
htons
(Host to Network Short):将16位整数从主机字节序转换为网络字节序。cuint16_t htons(uint16_t hostshort);
-
ntohs
(Network to Host Short):将16位整数从网络字节序转换为主机字节序。cuint16_t ntohs(uint16_t netshort);
这些转换函数在网络编程中至关重要,因为它们确保在不同架构的系统之间的通信中数据能被正确解释。
IPv4示例代码
以下是一个使用套接字连接TCP服务器的简单示例,展示了如何使用上述地址结构和字节序转换函数:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字 [1]
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址 [2]
server_addr.sin_family = AF_INET; // [3]
server_addr.sin_port = htons(8080); // 端口号使用 htons 转换 [4]
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地主机 [5]
// 连接到服务器 [6]
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("连接到服务器成功\n");
// 关闭套接字 [7]
close(sockfd);
return 0;
}
在上述示例中,我们创建了一个TCP种类的套接字,并连接到本地主机的8080端口。连接成功后,会打印一条消息并关闭套接字。
-
[1] 创建套接字 :
socket()
函数用于创建一个套接字,AF_INET
表示使用 IPv4,SOCK_STREAM
表示使用 TCP 协议,0
表示使用默认协议。成功时返回套接字描述符,失败时返回-1
。 -
[2] 设置服务器地址:我们需要指定服务器的 IP 地址和端口号以建立连接。
-
[3] 设置地址族 :
server_addr.sin_family = AF_INET;
设置地址族为 IPv4。 -
[4] 端口号转换 :
htons(8080)
将端口号 8080 转换为网络字节序,htons
表示 "host to network short"(主机到网络短整数)。这一操作确保了字节序的正确性,因为网络的字节序通常不同于计算机的字节序。 -
[5] 本地主机地址转换 :
inet_addr("127.0.0.1")
将 IP 地址字符串 "127.0.0.1" 转换为网络字节序的整数。127.0.0.1 是环回地址,表示本地主机。 -
[6] 连接到服务器 :
connect()
函数用于请求与指定服务器的连接。需要传入套接字描述符、服务器地址结构指针以及该结构的大小。若连接失败,connect
函数返回-1
。 -
[7] 关闭套接字 :
close(sockfd)
关闭套接字套接字描述符,释放系统资源。
IPv6示例代码
以下是一个使用套接字连接到一个支持 IPv6 的 TCP 服务器的简单示例,展示了如何使用 sockaddr_in6
结构体来处理 IPv6 地址:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in6 server_addr;
// 创建套接字 [1]
if ((sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址 [2]
server_addr.sin6_family = AF_INET6; // 设置地址族为 IPv6 [3]
server_addr.sin6_port = htons(8080); // 端口号使用 htons 转换 [4]
inet_pton(AF_INET6, "::1", &server_addr.sin6_addr); // 本地主机 IPv6 地址 [5]
server_addr.sin6_flowinfo = 0; // 流量信息不设置 [6]
server_addr.sin6_scope_id = 0; // Scope ID 不设置 [7]
// 连接到服务器 [8]
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("连接到服务器成功\n");
// 关闭套接字 [9]
close(sockfd);
return 0;
}
在上述示例中,我们创建了一个支持 IPv6 的 TCP 套接字,并连接到支持 IPv6 的本地服务器的8080端口。连接成功后,程序会打印一条消息并关闭套接字。
-
[1] 创建套接字 :
socket()
函数用于创建 IPv6 套接字,AF_INET6
表示使用 IPv6 地址,SOCK_STREAM
表示使用 TCP 协议,0
表示使用默认协议。成功时返回套接字描述符,失败时返回-1
。 -
[2] 设置服务器地址:为 IPv6,我们指定需要使用的 IPv6 地址和端口号。
-
[3] 设置地址族 :
server_addr.sin6_family = AF_INET6;
设置为 IPv6 地址族。 -
[4] 端口号转换 :
htons(8080)
将端口号 8080 转换到网络字节序。 -
[5] 本地主机地址转换 :使用
inet_pton
函数将字符串形式的 IPv6 地址 "::1"(也即 IPv6 的环回地址,类似于 IPv4 的 127.0.0.1)转换到网络字节序。 -
[6] 流量信息 :
sin6_flowinfo
设置成 0,通常用于服务质量 (QoS) 和流标签。 -
[7] Scope ID :对于环回地址,这里为
0
。Scope ID 用于标识链路本地 IPv6 地址的接口。 -
[8] 连接到服务器 :与 IPv4 中的
connect()
类似,用于请求与指定 IPv6 服务器的连接。 -
[9] 关闭套接字:释放系统资源,关闭套接字描述符。
3.1.2 TCP 编程
在进行网络编程时,TCP(传输控制协议)是一种常见的选择,因其提供了可靠的、有序的、基于连接的数据传输服务。以下内容将详细介绍如何使用C语言进行TCP编程。
3.1.2.1 TCP 套接字的创建与配置 (socket
, setsockopt
)
-
套接字创建
-
使用
socket
函数创建TCP套接字。socket
函数通常使用如下形式:cint socket(int domain, int type, int protocol);
domain
:协议族,比如AF_INET
表示IPv4协议。type
:套接字类型,比如SOCK_STREAM
表示流套接字。protocol
:一般设为0
,默认值代表TCP。
-
示例代码:
cint sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); }
-
-
套接字配置
-
使用
setsockopt
函数配置套接字选项以提高通信效率、解决地址复用问题等:cint setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
sockfd
:套接字描述符。 -
level
:选项所在的协议层,比如SOL_SOCKET
表示套接字层。 -
optname
:指定需要设置的选项名,如SO_REUSEADDR
。 -
optval
:选项对应的值。 -
optlen
:optval
的长度。 -
示例代码设置地址复用:
cint opt = 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); exit(EXIT_FAILURE); }
-
-
3.1.2.2 服务器端编程(bind
, listen
, accept
)
-
绑定(bind)
-
绑定套接字到特定的地址与端口上,使服务器可以接收客户端的连接请求:
cint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
:套接字描述符。 -
addr
:服务器地址和端口号,采用struct sockaddr
结构体来存储。 -
addrlen
:addr
的长度。 -
示例代码:
cstruct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用地址 address.sin_port = htons(PORT); // 将端口号转换为网络字节序 if (bind(sockfd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); }
-
-
-
监听(listen)
-
listen
函数将套接字设为被动模式,用于接收客户端连接:cint listen(int sockfd, int backlog);
-
sockfd
:套接字描述符。 -
backlog
:等待连接队列的最大长度。指明了内核为此套接字排队的最大连接数。 -
示例代码:
cif (listen(sockfd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); }
-
-
-
接受连接(accept)
-
accept
函数提取待处理连接请求,为每个连接分配一个新的套接字:cint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
sockfd
:监听套接字的描述符。 -
addr
:指向一个用于存储已连接客户端地址信息的struct sockaddr
结构体。 -
addrlen
:指向一个值的指针,最初这个值指定客户端地址结构体addr
的大小,函数返回时更新为实际客户端地址的大小。 -
示例代码:
cint new_socket; struct sockaddr_in client_address; socklen_t addrlen = sizeof(client_address); new_socket = accept(sockfd, (struct sockaddr *)&client_address, &addrlen); if (new_socket < 0) { perror("accept failed"); exit(EXIT_FAILURE); } printf("Connection accepted.\n");
-
-
3.1.2.3 客户端编程(connect
)
- 连接服务器(connect)
-
connect
函数用于客户端尝试连接服务器:cint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 示例代码连接服务器:
cstruct sockaddr_in server_address; server_address.sin_family = AF_INET; server_address.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr) <= 0) { perror("invalid address/ Address not supported"); exit(EXIT_FAILURE); } if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) { perror("connection failed"); exit(EXIT_FAILURE); } printf("Connected to server.\n");
-
3.1.2.4 数据传输(send
, recv
)
-
发送数据(send)
-
send
函数用于将数据从客户端/服务器发送到另一端:cssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
sockfd
:套接字描述符,标识要发送数据的套接字。 -
buf
:指向包含要发送数据的缓冲区。 -
len
:要发送的数据长度,以字节为单位。 -
flags
:发送数据的标志,可以是 0 或使用位掩码组合的其他标志(如MSG_DONTWAIT
)。 -
示例代码:
cchar *message = "Hello, World!"; send(new_socket, message, strlen(message), 0); printf("Message sent.\n");
-
-
-
接收数据(recv)
-
recv
函数用于从连接中接收数据:cssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd
:套接字描述符,从中接收数据。 -
buf
:指向用于存储接收数据的缓冲区。 -
len
:缓冲区的大小,以字节为单位。 -
flags
:接收操作的标志,操作的修改行为,如MSG_WAITALL
,MSG_PEEK
。 -
示例代码:
cchar buffer[1024] = {0}; int valread = recv(new_socket, buffer, 1024, 0); printf("Received: %s\n", buffer);
-
-
3.1.2.5 连接关闭(close
, shutdown
)
-
连接关闭(close)
-
close
函数用于关闭套接字及其创建的连接:cint close(int fd);
-
fd
:需要关闭的文件描述符,通常表示一个打开的套接字。 -
示例代码:
cclose(new_socket);
-
-
-
关闭连接(shutdown)
-
shutdown
函数用于关闭部分连接,即停止进一步发送或接收数据:cint shutdown(int sockfd, int how);
how
参数:SHUT_RD
:关闭读但继续写。SHUT_WR
:关闭写但继续读。SHUT_RDWR
:关闭读写。
- 示例代码:
cshutdown(sockfd, SHUT_RDWR);
-
3.1.3 UDP 编程
UDP(User Datagram Protocol)是一种无连接的协议,与面向连接的TCP不同,UDP更轻量、无需建立连接,因此常用于对时延要求高但不需要可靠传输的场景。下面是关于UDP编程的详细讲解:
3.1.3.1 UDP 套接字的创建与配置 (socket
, setsockopt
)
套接字的创建:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字 [1]
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("UDP socket created successfully\n");
close(sockfd);
return 0;
}
- [1] 创建UDP套接字 :
socket(AF_INET, SOCK_DGRAM, 0)
创建了一个IPv4的UDP套接字。AF_INET
表示IPv4地址族,SOCK_DGRAM
表示数据报套接字,0
表示默认协议。
套接字选项配置:
c
// 省略必要的#include和main函数启动部分
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { // 配置套接字选项 [2]
perror("setsockopt failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Socket options set successfully\n");
- [2] 配置套接字选项 :
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
设置套接字的选项。SO_REUSEADDR
允许在套接字关闭后立即重新使用该端口。
3.1.3.2 服务器端编程(bind
)
c
// 省略必要的#include和main函数启动部分
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // IPv4
servaddr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
servaddr.sin_port = htons(PORT); // 端口
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { // 绑定 [3]
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Bind to port %d successful\n", PORT);
- [3] 绑定 :
bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr))
将IP地址和端口绑定到套接字上。INADDR_ANY
表示绑定到所有可用接口。
3.1.3.3 数据发送与接收(sendto
, recvfrom
)
数据发送:
c
char *message = "Hello, UDP!";
struct sockaddr_in cliaddr;
memset(&cliaddr, 0, sizeof(cliaddr));
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(CLIENT_PORT);
cliaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int n = sendto(sockfd, message, strlen(message), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr)); // 发送数据 [4]
if (n < 0) {
perror("sendto failed");
} else {
printf("Message sent.\n");
}
- [4] 发送数据 :
sendto(sockfd, message, strlen(message), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr))
将数据发送到指定的地址和端口。sockfd
:套接字描述符,标识要使用的套接字。message
:指向要发送数据的缓冲区,在此例中是一个字符串"Hello, UDP!"
。strlen(message)
:要发送数据的长度,以字节为单位。这一参数确保只发送指定长度的数据。MSG_CONFIRM
:标志参数,用于指定发送操作的特定选项,例如MSG_CONFIRM
表示在某些协议中需要确认(该标志在 UDP 中实际上不常用)。(const struct sockaddr *) &cliaddr
:指向包含目标地址和端口信息的sockaddr
结构的指针。在此例中,cliaddr
包含了目标 IP 地址和端口。sizeof(cliaddr)
:目标地址结构的长度。指明cliaddr
的大小,让函数正确理解地址结构的长度。
数据接收:
c
char buffer[MAXLINE];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len); // 接收数据 [5]
if (n < 0) {
perror("recvfrom failed");
} else {
buffer[n] = '\0'; // 添加字符串终止符
printf("Client : %s\n", buffer);
}
- [5] 接收数据 :
recvfrom(sockfd, buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len)
从指定的地址和端口接收数据。sockfd
:套接字描述符,用于标识接收数据的套接字。buffer
:用于存储接收到的数据的缓冲区。MAXLINE
:接收缓冲区的最大字节数,buffer
的大小。MSG_WAITALL
:接收选项标志。此标志表示在完整接收到请求字节的数据之前,调用不会返回。可以根据需要选择其他标志,如0
。(struct sockaddr *) &cliaddr
:指向存储来源地址信息的指针。cliaddr
是一个用于保存发送端地址信息的结构体。&len
:指向一个变量的指针,存储cliaddr
结构体的大小,并在接收函数返回时,包含发送端地址的实际长度。
3.1.3.4 连接管理(无连接特性分析)
UDP是无连接的,即在发送数据前不需要与对方建立连接。每个数据包(数据报)独立发送,可能会乱序到达或丢失,因此需要应用层实现可靠性保证。
3.1.3.5 数据报的丢失与重传机制
在UDP中,由于无连接特性,数据包可能丢失、重复或乱序到达。实际应用中通常需要在应用层实现:
- 重传机制:超时未收到ACK则重传数据。
- 序列号机制:为每个数据包加上序列号,以便接收方按序重组。
- 校验和检查:用来检查数据完整性。
综上所述,UDP编程的核心在于通过创建和配置套接字、实现数据的发送与接收,并结合应用层机制来应对数据丢失及乱序问题。希望以上讲解对你的项目开发有所帮助。
3.1.4 高级套接字编程技巧
3.1.4.1 非阻塞套接字与多路复用(select
, poll
, epoll
)
在高级网络编程中,非阻塞I/O与多路复用技术是处理高并发连接的关键。非阻塞I/O使套接字在I/O操作时不会阻塞进程,多路复用则允许程序同时监视多个套接字,提升效率。
-
非阻塞套接字
-
设置方法:使用
fcntl
函数将套接字设置为非阻塞模式。c#include <fcntl.h> int set_nonblocking(int sock) { // [1] int flags = fcntl(sock, F_GETFL, 0); // [2][3] if (flags == -1) return -1; return fcntl(sock, F_SETFL, flags | O_NONBLOCK); // [4] }
sock
:要设置为非阻塞模式的套接字描述符。flags
:套接字的当前标志位,通过fcntl
的F_GETFL
命令获取。fcntl(sock, F_GETFL, 0)
:获取sock
当前的文件状态标志。fcntl(sock, F_SETFL, flags | O_NONBLOCK)
:将套接字设为非阻塞模式,在现有标志位的基础上添加O_NONBLOCK
,更新文件状态标志。
-
-
select
-
用于监视一组文件描述符(套接字),在任何一个或多个文件描述符变为可读、可写或有错误时返回。
cfd_set readfds; // [1] FD_ZERO(&readfds); // [2] FD_SET(sock, &readfds); // [3] int result = select(sock + 1, &readfds, NULL, NULL, &timeout); // [4] if (result > 0 && FD_ISSET(sock, &readfds)) { // [5] // sock 变为可读 }
readfds
:一个文件描述符集合,用于存储需要监视的文件描述符,检查它们是否可读。FD_ZERO(&readfds)
:初始化文件描述符集合readfds
,将其清空。FD_SET(sock, &readfds)
:将套接字sock
添加到readfds
集合中,用于监控其可读事件。select(sock + 1, &readfds, NULL, NULL, &timeout)
:sock + 1
:第一个参数指定监视的文件描述符范围,即待监控的最大描述符加一(因数组索引从零开始)。&readfds
:第二个参数指定需要检查可读性的文件描述符集合。NULL
:第三个和第四个参数用于检查可写性和异常情况的文件描述符集合,设置为NULL
表示不检查。&timeout
:第五个参数为select
等待的超时时间。
result
:select
函数的返回值;大于 0 表示有文件描述符变为可读、可写或有错误,小于 0 表示出错,等于 0 表示超时无事件发生。FD_ISSET(sock, &readfds)
:宏用于判断套接字sock
是否在readfds
集合中可读。
-
-
poll
-
类似于
select
, 但处理的文件描述符数量更大,且性能更好。cstruct pollfd fds[1]; fds[0].fd = sock; // [1] fds[0].events = POLLIN; // [2] int result = poll(fds, 1, timeout); // [3][4][5] if (result > 0 && (fds[0].revents & POLLIN)) { // [6] // sock 变为可读 }
-
fds
:pollfd
结构体数组,用于指定要监视的文件描述符和事件。 -
fds[0].fd
:要检测的套接字描述符。 -
fds[0].events
:待检测的事件类型,例如POLLIN
表示等待数据可读。 -
poll(fds, 1, timeout)
:调用poll
函数执行检测。 -
1
:指定fds
数组中需要检测的文件描述符数量。 -
timeout
:指定poll
等待事件的毫秒数。负值表示无限等待。 -
result
:poll
的返回值,表示准备就绪的文件描述符数量。若大于 0,表示有文件描述符满足条件。
-
-
epoll
-
专为Linux设计的更高效的I/O多路复用机制,适用于处理大量并发连接。
cint epoll_fd = epoll_create1(0); // [1] struct epoll_event ev = {.events = EPOLLIN, .data.fd = sock}; // [2] epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev); // [3] struct epoll_event events[MAX_EVENTS]; // [4] int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // [5] for (int i = 0; i < nfds; i++) { if (events[i].events & EPOLLIN) { // sock 变为可读 } }
epoll_create1(0)
:创建一个 epoll 实例,返回一个 epoll 文件描述符,用于后续的 epoll 操作。struct epoll_event ev
:定义一个 epoll 事件结构体ev
,用于描述要监视的事件类型和相关数据。ev.events
:代表事件类型,如EPOLLIN
表示可读事件。ev.data.fd
:事件关联的文件描述符,这里是套接字sock
。
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev)
:将套接字sock
添加到 epoll 监控列表中,通过epoll_fd
进行管理。epoll_fd
:epoll 文件描述符。EPOLL_CTL_ADD
:操作码,表示将新的描述符添加到 epoll 实例中。sock
:要添加的文件描述符。&ev
:指向要添加的事件结构的指针。
struct epoll_event events[MAX_EVENTS]
:定义事件数组events
,用于存储被触发事件的信息,数组大小由MAX_EVENTS
定义。epoll_wait(epoll_fd, events, MAX_EVENTS, -1)
:等待事件发生。epoll_fd
:epoll 文件描述符。events
:指向epoll_event
结构体数组,存储触发的事件。MAX_EVENTS
:可以监听的最大事件数。-1
:超时值,-1
代表无限期等待直到事件发生。
nfds
:epoll_wait
返回值,表示已触发事件的数量。
-
3.1.4.2 套接字选项(SO_REUSEADDR
, SO_KEEPALIVE
, SO_LINGER
等)
套接字选项用于控制套接字的行为,可以通过 setsockopt
函数设置不同选项。
-
SO_REUSEADDR
- 允许在套接字关闭后立即重用地址。
cint opt = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
SO_KEEPALIVE
- 启用保持连接功能,内核会定期发送探测包以检测连接是否活跃。
cint opt = 1; setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
-
SO_LINGER
- 控制套接字关闭时的行为,避免未发送的数据丢失。
cstruct linger so_linger; so_linger.l_onoff = 1; /* 开启linger选项 */ so_linger.l_linger = 30; /* 超时30秒 */ setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
3.1.4.3 多线程与多进程服务器模型(预创建线程/进程池)
为了提升服务器的性能和并发处理能力,常采用多线程或多进程模型,每个线程或进程处理不同的连接。
-
多线程模型
-
使用线程池预创建多个线程,每个线程等待处理新的连接。
cvoid *thread_function(void *arg) { // [1] int sock = *(int*)arg; // [2] // 处理连接 return NULL; } void create_thread_pool(int num_threads) { // [3] pthread_t threads[num_threads]; // [4] for (int i = 0; i < num_threads; i++) { // [5] pthread_create(&threads[i], NULL, thread_function, (void*)&sock); // [6] } }
arg
:传递给线程函数的参数,一般为指向套接字描述符的指针。sock
:从arg
解引用得到的套接字描述符,用于处理连接。num_threads
:要创建的线程数量,也就是线程池中的线程数量。threads
:存储线程标识符的数组,用于跟踪和管理线程。i
:循环变量,用于迭代创建num_threads
个线程。pthread_create(&threads[i], NULL, thread_function, (void*)&sock)
:用于创建线程,将每个线程绑定到thread_function
函数,并传递套接字作为参数。
-
-
多进程模型
-
使用
fork
创建子进程处理新连接,或使用预创建的进程池。cvoid create_process_pool(int num_processes) { for (int i = 0; i < num_processes; i++) { // [1] pid_t pid = fork(); // [2] if (pid == 0) { // 子进程处理连接 exit(0); // [3] } } // 父进程等待子进程结束 }
num_processes
:需要创建的子进程数量,用于处理并发连接。i
:循环变量,用于迭代创建指定数量的子进程。pid
:进程ID,由fork()
函数返回,用于区分父进程和子进程。pid
为0表示当前进程是子进程,正数表示父进程获得的子进程ID。exit(0)
:子进程完成任务后正常退出,返回0表示成功执行退出。
-
3.1.4.4 套接字超时设置(连接超时与操作超时)
设置超时可以防止程序在某些操作上无限期等待,常用的选项有 SO_RCVTIMEO
和 SO_SNDTIMEO
。
-
设置接收超时
cstruct timeval tv; tv.tv_sec = 5; // 5秒超时 tv.tv_usec = 0; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
-
设置发送超时
cstruct timeval tv; tv.tv_sec = 5; // 5秒超时 tv.tv_usec = 0; setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
通过上述技术,您可以显著提高基于C语言的网络应用程序的性能和稳定性,确保其在高并发场景下的健壮性。