套接字(Socket)是网络编程的基础,它提供了一种进程间通信的机制。根据传输特点,套接字主要分为以下两种类型:
sockaddr:通用套接字地址结构,通常作为其他具体地址结构的基类使用。sockaddr_in:用于IPv4地址,包括如下成员:struct 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地址,包括如下成员:struct 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
};
在网络编程中,数据的字节序(即在内存中存储多字节数据的方式)是一个重要概念。网络协议通常使用"大端字节序"(Big-endian),而不同主机可能使用"小端字节序"(Little-endian)。
字节序是什么?:
字节序(Byte Order,又称字节顺序)是指计算机系统中存储多字节数据(如整数、浮点数等)时的字节排列顺序。字节序影响了如何在内存中组织更高阶数据类型。它在计算机系统之间传输数据以及数据的正确处理和解释上至关重要。
为什么字节序重要?
大端字节序和小端字节序
大端字节序(Big-endian):
0x12345678 将按以下顺序存储:12 34 56 78。小端字节序(Little-endian):
0x12345678 将按以下顺序存储:78 56 34 12。为什么需要转换?
在网络编程中,数据通常需要从一个系统传输到另一个系统。双方计算机可能不使用相同的字节序。为了在不同使用字节序的系统之间正确地通信,我们需要一个统一的标准字节序。为此:
网络字节序:通常使用大端字节序作为标准,因为许多早期网络协议(如IP、TCP、UDP)便采用这种字节序,确保不同系统能够相互理解数据。
主机字节序:这是系统自身在内部计算和存储数据时使用的字节序,可能是大端或小端。
通过这些转换函数,如htonl、ntohl、htons和ntohs,程序开发者可以在主机字节序和网络字节序之间无误地转换数据。这些转换确保程序能在处理网络通信时正确解释和构造数据,使得不同架构的客户端和服务器可以流畅地交换信息。
以下函数用于在主机字节序和网络字节序之间转换:
htonl(Host to Network Long):将32位整数从主机字节序转换为网络字节序。
uint32_t htonl(uint32_t hostlong);
ntohl(Network to Host Long):将32位整数从网络字节序转换为主机字节序。
uint32_t ntohl(uint32_t netlong);
htons(Host to Network Short):将16位整数从主机字节序转换为网络字节序。
uint16_t htons(uint16_t hostshort);
ntohs(Network to Host Short):将16位整数从网络字节序转换为主机字节序。
uint16_t ntohs(uint16_t netshort);
这些转换函数在网络编程中至关重要,因为它们确保在不同架构的系统之间的通信中数据能被正确解释。
数据类型:
uint32_t 和 uint16_t 是在 C 标准库中定义的固定宽度整数类型,分别代表 32 位无符号整数和 16 位无符号整数。它们定义在头文件 <stdint.h> 中。使用这些类型的主要原因是为了保证跨平台的一致性。
在 C 语言中,诸如 int、short、unsigned int 和 unsigned short 等类型,其具体的位宽(也就是占用的字节数)并没有被标准严格定义,而是依赖于具体的实现和机器架构。例如,在某些平台上,int 可能是32位,但在其他平台上可能是16位或其他大小。
使用 uint32_t 和 uint16_t 等固定宽度的整数类型,可以保证程序在不同平台上的行为一致,因为这些类型在所有支持 C 语言标准的系统上都具有明确的位宽保证:
uint32_t 总是占据32位。uint16_t 总是占据16位。在需要处理网络协议、二进制文件格式或者需要跨平台的一致行为时,使用这些固定宽度的整数类型会更加安全和可靠。
#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。
[4] 端口号转换:htons(8080) 将端口号 8080 转换为网络字节序,htons 表示 “host to network short”(主机到网络短整数)。这一操作确保了字节序的正确性,因为网络的字节序通常不同于计算机的字节序。
[7] 关闭套接字:close(sockfd) 关闭套接字套接字描述符,释放系统资源。
socket 函数概述socket 函数是用于创建一个新的套接字(socket),它是网络通信的基本资源。其函数原型通常如下:
int socket(int domain, int type, int protocol);
domain: 定义了使用的地址族。常见的值有 AF_INET(IPv4)和 AF_INET6(IPv6)。type: 指定套接字的类型,常用的有 SOCK_STREAM(TCP,面向连接的流套接字)和 SOCK_DGRAM(UDP,数据报套接字)。protocol: 通常设置为 0 以选择默认协议。不同的套接字类型和地址族可能有多个协议可用,不过多数情况下系统会根据前两个参数自行选择合适的协议。返回值
socket 函数返回新创建的套接字描述符,它是一个非负整数。-1,并设置 errno 以指示错误类型。为什么返回值是 int 类型的 sockfd?
通用性和效率:
int 类型。网络通信的套接字、文件 I/O 和其他系统资源(如管道)都用int类型的文件描述符来识别。这提供了统一的接口,可以更高效地管理和调用不同类型的资源。简化资源管理:
read、write、close)进行操作。系统实现的便利性:
int 类型的文件描述符设计提供了一种简单、高效的方式将每个打开的文件或套接字映射到相应的内核数据结构。文件描述符是一个索引,指向内核维护的一个文件表,表中包含与每个打开的文件或套接字相关的信息。扩展性:
int 作为描述符可以轻松扩展到不同类型的 I/O 设备和其他资源,而不需要为每种资源类型设计特殊的识别符号或对象。总体来说,这种设计让程序员能够以一种一致的方式与不同的I/O资源进行交互,让操作系统以一种高效的方式管理这些资源。
connect 函数的参数connect 函数的原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket 函数返回的套接字文件描述符,标识一个唯一的套接字。addr:
sockaddr 结构的指针,描述要连接的目标地址。在 IPv4 协议中,This is typically cast to struct sockaddr_in* for IPv4 use but passed as a generic struct sockaddr*.server_addr 所指向的内存块在调用时被强制转换为通用的 sockaddr。addrlen:
addr 指向的结构的大小。对于 sockaddr_in 结构,它通常是 sizeof(struct sockaddr_in)。connect 函数的作用connect 开始 TCP 三次握手过程以确保通信。如果连接成功,connect 会返回0,否则返回-1,并设置 errno 表示错误类型。
数据通信
connect 成功后,你可以使用以下函数进行数据通信:
发送数据:
send 或 write 函数将数据发送到服务器。sockfd 发送数据。示例:
char *message = "Hello, Server!";
if(send(sockfd, message, strlen(message), 0) < 0) {
perror("send");
// handle error
}
接收数据:
recv 或 read 函数从服务器接收数据。buf 的数据。示例:
char buffer[1024];
int bytes_received;
if((bytes_received = recv(sockfd, buffer, sizeof(buffer), 0)) < 0) {
perror("recv");
// handle error
}
buffer[bytes_received] = '\0'; // Null-terminate received data
printf("Received: %s\n", buffer);
关闭连接:
close 关闭该套接字以清理资源,结束连接。close(sockfd);
通过这些步骤,你可以在连接建立后,与服务器进行通信并实现数据的发送和接收。确保在实际编程中适当地处理错误和异常情况,以提高代码的健壮性和正确性。
#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端口。连接成功后,程序会打印一条消息并关闭套接字。
[4] 端口号转换:htons(8080) 将端口号 8080 转换到网络字节序。
[6] 流量信息:sin6_flowinfo 设置成 0,通常用于服务质量 (QoS) 和流标签。
[8] 连接到服务器:与 IPv4 中的 connect() 类似,用于请求与指定 IPv6 服务器的连接。
[9] 关闭套接字:释放系统资源,关闭套接字描述符。
getaddrinfo() 函数优点:
用步骤:
包含头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
调用 getaddrinfo():
struct addrinfo 的 hint 结构体。ai_family 为 AF_UNSPEC 以允许IPv4和IPv6。getaddrinfo() 并传递主机名或IP地址和服务名或端口号。遍历结果列表:
getaddrinfo() 会返回一个链表,你可以遍历这个链表并尝试创建socket。释放结果列表:
freeaddrinfo() 函数释放由 getaddrinfo() 动态分配的内存。#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
int main() {
struct addrinfo hints, *res, *ai;
int sockfd;
int status;
// 清空 hints 结构体
memset(&hints, 0, sizeof hints);
// 我们想要一个可以用来连接的 socket
hints.ai_family = AF_UNSPEC; // 允许使用 IPv4 或 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP 流套接字
// 获取目标服务器的地址信息
status = getaddrinfo("example.com", "http", &hints, &res);
if (status != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return 1;
}
// 遍历所有的结果,并连接到第一个我们可以连接的
for (ai = res; ai != NULL; ai = ai->ai_next) {
// 尝试创建 socket
sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (sockfd == -1) {
perror("socket");
continue;
}
// 尝试连接
if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) == -1) {
close(sockfd);
perror("connect");
continue;
}
// 如果程序执行到这里,说明连接成功
break;
}
if (ai == NULL) {
fprintf(stderr, "连接失败\n");
return 2;
}
// 使用该 socket(例如,发送/接收数据)
// 清理资源
freeaddrinfo(res);
close(sockfd);
return 0;
}
getaddrinfo() 返回的结构链表中可能包含多个 addrinfo 结构体,每一个结构体可能代表一种可行的地址和协议组合。socket() 和 connect(),当产生错误时,你可以继续尝试链表中的下一个元素。在进行网络编程时,TCP(传输控制协议)是一种常见的选择,因其提供了可靠的、有序的、基于连接的数据传输服务。以下内容将详细介绍如何使用C语言进行TCP编程。
socket, setsockopt)套接字创建
socket 函数创建TCP套接字。socket 函数通常使用如下形式:int socket(int domain, int type, int protocol);
domain:协议族,比如 AF_INET 表示IPv4协议。type:套接字类型,比如 SOCK_STREAM 表示流套接字。protocol:一般设为 0,默认值代表TCP。int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
套接字配置
setsockopt 函数配置套接字选项以提高通信效率、解决地址复用问题等:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:套接字描述符。
level:选项所在的协议层,比如 SOL_SOCKET 表示套接字层。
optname:指定需要设置的选项名,如 SO_REUSEADDR。
optval:选项对应的值。
optlen:optval 的长度。
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
bind, listen, accept)绑定(bind)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addrlen:addr 的长度。
示例代码:
struct 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 函数将套接字设为被动模式,用于接收客户端连接:int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:等待连接队列的最大长度。指明了内核为此套接字排队的最大连接数。
示例代码:
if (listen(sockfd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
接受连接(accept)
accept 函数提取待处理连接请求,为每个连接分配一个新的套接字:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:监听套接字的描述符。
示例代码:
int 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");
connect)connect 函数用于客户端尝试连接服务器:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct 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");
send, recv)发送数据(send)
send 函数用于将数据从客户端/服务器发送到另一端:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符,标识要发送数据的套接字。
buf:指向包含要发送数据的缓冲区。
len:要发送的数据长度,以字节为单位。
flags:发送数据的标志,可以是 0 或使用位掩码组合的其他标志(如 MSG_DONTWAIT)。
示例代码:
char *message = "Hello, World!";
send(new_socket, message, strlen(message), 0);
printf("Message sent.\n");
接收数据(recv)
recv 函数用于从连接中接收数据:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符,从中接收数据。
buf:指向用于存储接收数据的缓冲区。
len:缓冲区的大小,以字节为单位。
flags:接收操作的标志,操作的修改行为,如 MSG_WAITALL,MSG_PEEK。
示例代码:
char buffer[1024] = {0};
int valread = recv(new_socket, buffer, 1024, 0);
printf("Received: %s\n", buffer);
close, shutdown)连接关闭(close)
close 函数用于关闭套接字及其创建的连接:int close(int fd);
fd:需要关闭的文件描述符,通常表示一个打开的套接字。
示例代码:
close(new_socket);
关闭连接(shutdown)
shutdown 函数用于关闭部分连接,即停止进一步发送或接收数据:int shutdown(int sockfd, int how);
how 参数:
SHUT_RD:关闭读但继续写。SHUT_WR:关闭写但继续读。SHUT_RDWR:关闭读写。shutdown(sockfd, SHUT_RDWR);
UDP(User Datagram Protocol)是一种无连接的协议,与面向连接的TCP不同,UDP更轻量、无需建立连接,因此常用于对时延要求高但不需要可靠传输的场景。下面是关于UDP编程的详细讲解:
socket, setsockopt)套接字的创建:
#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;
}
socket(AF_INET, SOCK_DGRAM, 0) 创建了一个IPv4的UDP套接字。AF_INET 表示IPv4地址族,SOCK_DGRAM表示数据报套接字,0 表示默认协议。套接字选项配置:
// 省略必要的#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");
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) 设置套接字的选项。SO_REUSEADDR 允许在套接字关闭后立即重新使用该端口。bind)// 省略必要的#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);
bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) 将IP地址和端口绑定到套接字上。INADDR_ANY表示绑定到所有可用接口。sendto, recvfrom)数据发送:
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");
}
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 的大小,让函数正确理解地址结构的长度。数据接收:
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);
}
recvfrom(sockfd, buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len) 从指定的地址和端口接收数据。
sockfd:套接字描述符,用于标识接收数据的套接字。buffer:用于存储接收到的数据的缓冲区。MAXLINE:接收缓冲区的最大字节数,buffer 的大小。MSG_WAITALL:接收选项标志。此标志表示在完整接收到请求字节的数据之前,调用不会返回。可以根据需要选择其他标志,如 0。(struct sockaddr *) &cliaddr:指向存储来源地址信息的指针。cliaddr 是一个用于保存发送端地址信息的结构体。&len:指向一个变量的指针,存储 cliaddr 结构体的大小,并在接收函数返回时,包含发送端地址的实际长度。UDP是无连接的,即在发送数据前不需要与对方建立连接。每个数据包(数据报)发送,可能会乱序到达或丢失,因此需要应用层实现可靠性保证。
在UDP中,由于无连接特性,数据包可能丢失、重复或乱序到达。实际应用中通常需要在应用层实现:
综上所述,UDP编程的核心在于通过创建和配置套接字、实现数据的发送与接收,并结合应用层机制来应对数据丢失及乱序问题。希望以上讲解对你的项目开发有所帮助。
select, poll, epoll)在高级网络编程中,非阻塞I/O与多路复用技术是处理高并发连接的关键。非阻塞I/O使套接字在I/O操作时不会阻塞进程,多路复用则允许程序同时监视多个套接字,提升效率。
非阻塞套接字
fcntl 函数将套接字设置为非阻塞模式。#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
fd_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, 但处理的文件描述符数量更大,且性能更好。struct 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
int 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 返回值,表示已触发事件的数量。SO_REUSEADDR, SO_KEEPALIVE, SO_LINGER等)套接字选项用于控制套接字的行为,可以通过 setsockopt 函数设置不同选项。
SO_REUSEADDR
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
SO_KEEPALIVE
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
SO_LINGER
struct 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));
为了提升服务器的性能和并发处理能力,常采用多线程或多进程模型,每个线程或进程处理不同的连接。
多线程模型
void *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创建子进程处理新连接,或使用预创建的进程池。void 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表示成功执行退出。设置超时可以防止程序在某些操作上无限期等待,常用的选项有 SO_RCVTIMEO 和 SO_SNDTIMEO。
设置接收超时
struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
设置发送超时
struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
通过上述技术,您可以显著提高基于C语言的网络应用程序的性能和稳定性,确保其在高并发场景下的健壮性。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- 99spj.com 版权所有 湘ICP备2022005869号-5
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务