【C语言系统编程】【第三部分:网络编程】3.3 实践与案例分析
3.3 实践与案例分析
在本章节中,我们将通过一些具体的案例来展示如何在实际项目中进行网络编程。这些案例不仅能帮助你理解各个概念,还能提升你的实践技能,并为你将来的项目提供参考。
3.3.1 案例分析:简单的聊天室
聊天室是网络编程中的经典案例,通过这一案例,您将学习如何构建一个基于TCP协议的服务器/客户端模型,实现消息广播机制,并处理多线程/进程并发问题。
3.3.1.1 TCP 服务器/客户端模型
在这个部分,我们将介绍如何使用TCP协议建立服务器和客户端,管理连接,并传输数据。
- 服务器端代码示例
多线程TCP服务器示例
此示例程序展示了如何使用多线程通过TCP协议实现简单的服务器。服务器能够接受多个客户端连接并为每个连接创建一个线程来处理通信。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h> // 用于线程 [1]#define PORT 8080
#define MAX_CLIENTS 10void *handle_client(void *arg); // 处理客户端连接的函数声明int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);pthread_t tid; // 线程标识符 [2]// 创建套接字 [3]if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项,允许端口重用 [4]if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");close(server_fd);exit(EXIT_FAILURE);}// 填写服务器地址信息 [5]address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY; // 允许来自任何IP的连接address.sin_port = htons(PORT); // 转换端口为网络字节顺序// 将套接字绑定到地址 [6]if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 监听传入的连接 [7]if (listen(server_fd, 3) < 0) {perror("listen");close(server_fd);exit(EXIT_FAILURE);}// 接受客户连接并创建线程处理 [8]while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {if (pthread_create(&tid, NULL, handle_client, (void *)&new_socket) != 0) {perror("pthread_create");close(new_socket);}}if (new_socket < 0) {perror("accept");close(server_fd);exit(EXIT_FAILURE);}return 0;
}void *handle_client(void *arg) {int sock = *(int *)arg; // 客户端的套接字描述符 [9]char buffer[1024] = {0};int valread;// 读取并处理来自客户端的数据 [10]while ((valread = read(sock, buffer, 1024)) > 0) {buffer[valread] = '\0';// 目前简单地打印接收到的消息printf("Received: %s\n", buffer);}// 关闭客户端套接字并退出线程close(sock);printf("Client disconnected\n");pthread_exit(NULL); // 退出线程 [11]
}
- [1] 用于线程:
#include <pthread.h>
引入了多线程编程的头文件,允许使用pthread库来处理并发任务。 - [2] 线程标识符:
pthread_t tid
是一个线程标识符,用于引用新创建的线程。 - [3] 创建套接字:调用
socket()
函数创建一个套接字,通过标记AF_INET
和SOCK_STREAM
来创建基于TCP的网络连接。 - [4] 设置套接字选项:通过
setsockopt()
函数允许端口重用,以便在程序重启时能立即重新绑定现有端口。 - [5] 填写服务器地址信息:通过
struct sockaddr_in
结构填入IP和端口信息。 - [6] 套接字绑定:通过
bind()
函数将套接字与服务器地址绑定,使套接字与网络地址相关联。 - [7] 监听传入的连接:通过
listen()
函数使套接字进入监听状态,准备接受传入的客户端连接。 - [8] 创建线程处理连接:使用
accept()
接受连接,然后通过pthread_create()
创建一个新线程为该连接处理通信。 - [9] 客户端的套接字描述符:
int sock = *(int *)arg
将传递过来的套接字描述符参数转换回整型引用。 - [10] 读取和处理数据:在
handle_client()
函数中使用read()
循环读取客户端发送过来的数据,并显示接收到的内容。 - [11] 退出线程:通过
pthread_exit()
使处理完的线程正常退出和释放资源。
- 客户端代码示例
- 示例代码解析:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080 // 定义服务器端口号 [1]int main() {int sock = 0;struct sockaddr_in serv_addr; // 定义服务器地址结构 [2]char *message = "Hello from client"; // 客户端发送的消息 [3]char buffer[1024] = {0}; // 缓冲区,用于接收服务器响应 [4]// 创建套接字并检查是否成功if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("\n Socket creation error \n");return -1;}serv_addr.sin_family = AF_INET; // 设置地址族为IPv4 [5]serv_addr.sin_port = htons(PORT); // 设置端口号,并进行网络字节序转换 [6]// 将IP地址从点分十进制转换为二进制格式if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {printf("\nInvalid address/ Address not supported \n");return -1;}// 建立与服务器的连接if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {printf("\nConnection Failed \n");return -1;}// 发送数据到服务器send(sock, message, strlen(message), 0);printf("Message sent\n");// 从服务器接收数据int valread = read(sock, buffer, 1024); // 读取服务器回应存入缓冲区 [7]printf("%s\n", buffer); // 打印服务器响应 [8]// 关闭套接字close(sock);return 0;
}
- [1] 服务器端口号:这里定义的
PORT 8080
是客户端希望连接的服务器的端口号。 - [2] 服务器地址结构:
struct sockaddr_in
是一个包含地址信息的结构体,用于存储服务器的IP地址和端口。 - [3] 客户端消息:
"Hello from client"
是客户端发送给服务器的消息内容。 - [4] 缓冲区:
char buffer[1024]
用于存储从服务器接收到的数据。 - [5] 地址族:
AF_INET
表示使用IPv4协议族。 - [6] 端口设置:
htons(PORT)
将主机字节序转换为网络字节序,这是网络通讯的标准格式。 - [7] 读取响应:
read()
用于从服务器读取数据,并存入buffer
中,buffer
的最大大小为1024字节。 - [8] 打印服务器响应:
printf("%s\n", buffer)
输出来自服务器的响应内容。
这段代码示例展示了一个简单的客户端程序,它通过TCP协议与一个本地服务器(假设运行在127.0.0.1
端口8080
)通信,发送消息并接收服务器的响应。
3.3.1.2 消息广播机制
消息广播是聊天室设计中的一个重要部分,需要服务器将接收到的消息转发给所有已连接的客户端。
- 示例代码
要实现消息广播,可以修改handle_client
函数,使其能够将接收到的消息发送给所有客户端:
- 示例代码解析:
#include <pthread.h>#define MAX_CLIENTS 10int client_sockets[MAX_CLIENTS]; // 保存客户端套接字 [1]
pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁初始化 [2]// 处理客户端连接的线程函数
void *handle_client(void *arg) {int sock = *(int *)arg; // 客户端套接字 [3]char buffer[1024] = {0}; // 缓冲区 [4]int valread;pthread_mutex_lock(&clients_mutex); // 加锁:修改共享资源 [5]for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = sock; // 存储新连接的客户端 [6]break;}}pthread_mutex_unlock(&clients_mutex); // 解锁:完成修改 [7]// 接收和广播数据while ((valread = read(sock, buffer, 1024)) > 0) {buffer[valread] = '\0'; // 添加字符串结束符 [8]printf("Received: %s\n", buffer);pthread_mutex_lock(&clients_mutex); // 加锁:广播消息 [9]for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] != 0) {send(client_sockets[i], buffer, strlen(buffer), 0); // 向所有客户端发送数据 [10]}}pthread_mutex_unlock(&clients_mutex); // 解锁:广播完成 [11]}close(sock); // 关闭客户端套接字 [12]pthread_mutex_lock(&clients_mutex); // 加锁:移除客户端 [13]for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == sock) {client_sockets[i] = 0; // 清除套接字记录 [14]break;}}pthread_mutex_unlock(&clients_mutex); // 解锁:移除完成 [15]printf("Client disconnected\n");pthread_exit(NULL); // 线程退出 [16]
}
- [1] 保存客户端套接字:
int client_sockets[MAX_CLIENTS]
是一个数组,用来保存连接的客户端的套接字。 - [2] 互斥锁初始化:
pthread_mutex_t clients_mutex
定义了一个互斥锁,用于线程同步访问共享资源。 - [3] 客户端套接字:通过传入的参数
arg
提取客户端的套接字描述符。 - [4] 缓冲区:
char buffer[1024]
用于存储从客户端接收到的数据。 - [5] 加锁:修改共享资源:在添加新客户端记录之前加锁,防止其他线程同时修改
client_sockets
。 - [6] 存储新连接的客户端:在共享数组
client_sockets
中记录当前客户端的套接字。 - [7] 解锁:完成修改:解锁以允许其他线程访问该共享资源。
- [8] 添加字符串结束符:在缓冲区的结尾加上字符串结束符,确保其是个有效的C字符串。
- [9] 加锁:广播消息:在向所有客户端发送数据前加锁,确保数据一致性。
- [10] 向所有客户端发送数据:利用
send()
函数广播消息到每个连接的客户端。 - [11] 解锁:广播完成:解锁以让其他线程可以访问该共享资源。
- [12] 关闭客户端套接字:在完成数据读取或连接断开后,关闭当前客户端的网络连接。
- [13] 加锁:移除客户端:在从
client_sockets
移除客户端套接字前加锁。 - [14] 清除套接字记录:移除已断开连接的客户端记录。
- [15] 解锁:移除完成:解锁以允许其他线程相继访问。
- [16] 线程退出:使用
pthread_exit()
结束处理客户端连接的线程。
3.3.1.3 多线程/进程并发处理
为了支持多个客户端的并发连接,可以使用多线程或多进程模型。在上面的服务器代码中,我们已经使用了多线程模型(pthread_create
)来处理并发连接。
- 关于多线程的注意事项
在使用多线程时,需要注意同步问题。例如在广播消息时,访问客户端数组是一个临界区,需要使用pthread_mutex
进行同步。
- 多进程模型的替换
多进程模型的实现也类似,可以在接受到新连接时创建新的子进程来处理。用fork
替换pthread_create
即可:
- 示例代码解析:
while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {if (fork() == 0) { // 子进程执行 [1]close(server_fd); // 关闭服务器文件描述符 [2]handle_client((void *)&new_socket); // 处理客户端请求 [3]exit(0); // 子进程退出 [4]} else { // 父进程继续监听 [5]close(new_socket); // 关闭新连接的 socket [6]}
}
- [1] 子进程执行:
fork()
函数创建一个新的子进程。比较fork()
的返回值,如果为0
,则意味着在子进程中执行后续代码。 - [2] 关闭服务器文件描述符: 在子进程中,通过
close(server_fd);
关闭父进程的服务器文件描述符server_fd
,因为子进程不需要监听新的连接。 - [3] 处理客户端请求: 调用
handle_client()
函数处理新接入的客户端请求,传入new_socket
的地址作为参数。 - [4] 子进程退出: 处理完客户端请求后,通过
exit(0);
退出子进程,防止子进程执行不必要的父进程代码。 - [5] 父进程继续监听: 如果
fork()
返回值大于0
,则表示处于父进程逻辑中,父进程将继续监听其他即将到来的客户端连接。 - [6] 关闭新连接的 socket: 在父进程中,通过
close(new_socket);
关闭为当前客户端生成的 socket,因为此连接将由子进程处理,父进程无需保留该连接。
这样子进程会处理每一个新的客户端连接,而父进程将继续监听新的连接请求。
通过以上案例分析和代码示例,相信你对如何建立一个简单的聊天室有了一定的了解。这包括了如何使用TCP协议创建服务器和客户端、如何实现消息广播机制,以及如何处理多线程和多进程并发问题。在理解这些基础上,你可以进一步扩展和完善这个项目,例如添加用户认证、改进消息格式等。
3.3.2 案例分析:文件传输程序
3.3.2.1 TCP 连接管理
在文件传输程序中,首先需要保证客户端和服务器之间的连接。TCP是面向连接的协议,它允许建立可靠的双向通信链路。下面展示了一个基本的客户端和服务器的TCP连接管理代码:
- 服务器端代码解析:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080int main() {int server_fd, new_socket;struct sockaddr_in address;int addrlen = sizeof(address);// 创建套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 配置地址和端口address.sin_family = AF_INET; // IPv4协议 [1]address.sin_addr.s_addr = INADDR_ANY; // 监听所有接口 [2]address.sin_port = htons(PORT); // 端口设置 [3]// 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 监听if (listen(server_fd, 3) < 0) { // 监听队列长度为3 [4]perror("listen");close(server_fd);exit(EXIT_FAILURE);}// 接受连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");close(server_fd);exit(EXIT_FAILURE);}printf("连接成功\n");close(new_socket); // 关闭客户端套接字 [5]close(server_fd); // 关闭服务器套接字 [6]return 0;
}
-
[1] IPv4协议:
address.sin_family = AF_INET
表示使用IPv4地址族。 -
[2] 监听所有接口:
INADDR_ANY
让服务器监听所有可用的网络接口(例如,eth0、lo等)。 -
[3] 端口设置:
htons(PORT)
将端口号转换为网络字节序,这是网络协议所需的字节顺序。 -
[4] 监听队列长度:
listen(server_fd, 3)
设置了服务器的监听队列长度为3,表示最多可积压3个未处理的连接请求。 -
[5] 关闭客户端套接字:用
close(new_socket)
关闭为客户端创建的套接字。 -
[6] 关闭服务器套接字:服务器不再需要监听其他连接时,调用
close(server_fd)
关闭服务器套接字。 -
示例代码解析:
// 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080 // 定义端口号 [1]int main() {int sock = 0;struct sockaddr_in serv_addr;// 创建套接字if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { // [2]printf("\nSocket creation error \n");return -1;}serv_addr.sin_family = AF_INET; // 使用IPv4协议 [3]serv_addr.sin_port = htons(PORT); // 设置端口号并转换字节序 [4]// 转换IP地址if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) { // [5]printf("\nInvalid address/ Address not supported \n");return -1;}// 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { // [6]printf("\nConnection Failed \n");return -1;}printf("连接成功\n"); // 输出连接成功信息 [7]close(sock); // 关闭套接字 [8]return 0;
}
- [1] 定义端口号:
#define PORT 8080
定义了用于连接的端口号,这里是与服务器通信的重要参数。 - [2] 创建套接字:
socket()
函数用于创建一个套接字,AF_INET
指定使用IPv4,SOCK_STREAM
表示使用面向连接的TCP协议。 - [3] 使用IPv4协议:
serv_addr.sin_family = AF_INET;
设置地址族为AF_INET
,表示使用IPv4协议。 - [4] 设置端口号并转换字节序:
htons()
函数用于将主机字节序转换为网络字节序,确保端口号的安全传输。 - [5] 转换IP地址:
inet_pton()
函数将点分十进制的IP地址转换为网络字节序的形式,若转换失败返回错误信息。 - [6] 连接到服务器:
connect()
函数用于将客户端的套接字连接到指定的服务器地址。 - [7] 输出连接成功信息:如果连接成功,客户端输出“连接成功”这行为标识。
- [8] 关闭套接字:使用
close(sock);
关闭客户端的套接字释放资源。
3.3.2.2 数据封包与文件分块传输
在传输大文件时,可以将文件分成若干小块进行传输。为了确保数据的完整性和有效性,可以使用自定义协议对数据进行封包与拆包:
- 示例代码解析:
#define CHUNK_SIZE 1024 // 定义数据块大小为1024字节 [1]void send_file(FILE *file, int sock) {char buffer[CHUNK_SIZE]; // 用于存储文件数据的缓冲区 [2]int bytes_read;// 读取文件并通过套接字发送while ((bytes_read = fread(buffer, sizeof(char), CHUNK_SIZE, file)) > 0) { // 从文件中读取数据 [3]if (send(sock, buffer, bytes_read, 0) < 0) { // 通过套接字发送数据 [4]perror("send"); // 发送失败,输出错误信息 [5]break; // 终止循环}}
}void receive_file(FILE *file, int sock) {char buffer[CHUNK_SIZE]; // 用于接收套接字数据的缓冲区 [6]int bytes_received;// 从套接字接收数据并写入文件while ((bytes_received = recv(sock, buffer, CHUNK_SIZE, 0)) > 0) { // 从套接字接收数据 [7]fwrite(buffer, sizeof(char), bytes_received, file); // 将接收到的数据写入文件 [8]}
}
- [1] 数据块大小:
#define CHUNK_SIZE 1024
定义一个常量CHUNK_SIZE
,用来指定每次传输的数据块大小为1024字节。这个大小可以根据网络和文件情况调整,以达到更优的传输效率。 - [2] 发送缓冲区:在
send_file
函数中,char buffer[CHUNK_SIZE]
用来暂存从文件读取的数据,准备发送。 - [3] 从文件中读取数据:
fread()
函数以块的方式读取数据,其中每次最多读取CHUNK_SIZE
字节的内容。 - [4] 通过套接字发送数据:
send()
函数用于将读取到的文件数据通过网络套接字进行发送,数据长度为bytes_read
。 - [5] 发送失败处理:若
send()
返回值小于0,表示发送失败,使用perror()
打印错误信息,并中断传输循环。 - [6] 接收缓冲区:在
receive_file
函数中,char buffer[CHUNK_SIZE]
用来暂存从网络接收到的数据。 - [7] 从套接字接收数据:
recv()
函数用于通过网络套接字接收数据,每次接收至多CHUNK_SIZE
字节。 - [8] 将接收到的数据写入文件:
fwrite()
将从套接字接收到的数据写入至指定文件,长度为bytes_received
。
总体而言,这段代码实现了从文件读取至缓冲区,再通过套接字发送文件数据的功能,以及从套接字读取数据并写入文件的接收功能。这是一个简单的文件传输机制。
3.3.2.3 进度显示与传输校验
在文件传输过程中,实时显示传输进度可以提高用户体验。同时,通过简单的校验(如校验和)来确保数据的完整性:
代码解析
此代码片段展示了两个主要的功能:一个是通过网络发送文件内容,并实时显示其传输进度,另一个是计算文件的简单校验和。
函数:send_file_with_progress
void send_file_with_progress(FILE *file, int sock) {char buffer[CHUNK_SIZE];int bytes_read;int total_bytes_sent = 0;fseek(file, 0, SEEK_END);int file_size = ftell(file);fseek(file, 0, SEEK_SET);while ((bytes_read = fread(buffer, sizeof(char), CHUNK_SIZE, file)) > 0) {if (send(sock, buffer, bytes_read, 0) < 0) {perror("send");break;}total_bytes_sent += bytes_read;printf("进度: %.2f%%\n", (total_bytes_sent / (float)file_size) * 100);}
}
- 作用:发送文件内容,并显示发送进度。
- 参数:
file
是待发送的文件指针,sock
是用于发送数据的套接字描述符。
关键知识点讲解:
- [1] 缓冲区初始化:
char buffer[CHUNK_SIZE];
定义了一个缓冲区,用来暂存每次从文件读取的部分数据。 - [2] 获取文件大小:
- 使用
fseek(file, 0, SEEK_END);
将文件指针移动到文件末尾。 int file_size = ftell(file);
利用ftell()
获取当前文件指针的位置,即文件大小。fseek(file, 0, SEEK_SET);
将文件指针复位到文件开头位置。
- 使用
- [3] 循环读取与发送:
- 使用
fread(buffer, sizeof(char), CHUNK_SIZE, file)
循环读取文件数据到缓冲区。 - 使用
send(sock, buffer, bytes_read, 0)
发送读取到的字节。 - 记录并计算已发送的数据量,以展示传输进度。
- 使用
- [4] 显示进度:
printf("进度: %.2f%%\n", (total_bytes_sent / (float)file_size) * 100);
利用总数据量与文件大小之比,实时显示传输进度。
函数:calculate_checksum
unsigned long calculate_checksum(FILE *file) {unsigned long checksum = 0;char buffer[CHUNK_SIZE];int bytes_read;rewind(file);while ((bytes_read = fread(buffer, sizeof(char), CHUNK_SIZE, file)) > 0) {for (int i = 0; i < bytes_read; ++i) {checksum += buffer[i];}}rewind(file);return checksum;
}
- 作用:计算文件数据的简单校验和。
- 参数:
file
是一个指向待计算校验和的文件的指针。
关键知识点讲解:
- [5] 初始化与重置:
unsigned long checksum = 0;
初始化校验和变量,rewind(file);
确保从文件开始位置进行计算。 - [6] 校验和计算逻辑:
- 在
fread()
读取的数据块上执行循环,累加字节到checksum
。 - 这是一个简单的字节加总算法,不太适合高安全需求场景,但用于简单验证或错误检测是足够的。
- 在
- [7] 文件指针复位:在计算完成后再次
rewind(file);
,使得文件指针回到开头,以方便后续其他操作。
在使用这些函数时,务必确保文件已正确打开,套接字已正常连接,以避免潜在错误。
在文件传输前后,计算文件的校验和并进行比对,可确保数据无误传输:
FILE *file = fopen("example_file", "rb"); // 打开文件以二进制只读模式 [1]
if (!file) {perror("fopen"); // 打印错误信息 [2]return;
}unsigned long checksum = calculate_checksum(file); // 计算文件校验和 [3]
send(sock, &checksum, sizeof(checksum), 0); // 发送校验和 [4]send_file_with_progress(file, sock); // 发送文件并显示进度 [5]fclose(file); // 关闭文件 [6]
- [1] 打开文件以二进制只读模式:使用
fopen()
函数以二进制模式 ("rb"
) 打开名为 “example_file” 的文件。函数返回一个FILE*
指针。如果文件成功打开,返回指向该文件的指针;否则返回NULL
。 - [2] 打印错误信息:如果
fopen()
返回NULL
,意味着文件打开失败,perror()
将打印出对应的错误信息到标准错误输出,并返回函数。 - [3] 计算文件校验和:调用
calculate_checksum(file)
函数计算文件的校验和,这通常用于验证文件内容的完整性。 - [4] 发送校验和:使用
send()
函数通过sock
套接字发送计算出的校验和数据。第三个参数表示要发送的数据的大小。 - [5] 发送文件并显示进度:
send_file_with_progress(file, sock)
函数用于通过sock
套接字发送文件内容,并在发送过程中显示进度。具体的实现细节取决于该函数的定义。 - [6] 关闭文件:使用
fclose()
函数关闭先前打开的文件,释放相关资源。
在接收端也计算和验证:
unsigned long received_checksum;
recv(sock, &received_checksum, sizeof(received_checksum), 0); // 接收校验和 [1]FILE *file = fopen("received_file", "wb"); // 打开文件以写入模式 [2]
if (!file) {perror("fopen");return;
}receive_file(file, sock); // 接收文件数据 [3]unsigned long calculated_checksum = calculate_checksum(file); // 计算接收数据的校验和 [4]
if (received_checksum == calculated_checksum) {printf("文件传输成功,校验和匹配。\n"); // 校验和匹配 [5]
} else {printf("文件校验和不匹配,传输可能损坏。\n"); // 校验和不匹配 [6]
}fclose(file); // 关闭文件 [7]
-
[1] 接收校验和:使用
recv()
函数从套接字sock
接收一个unsigned long
类型的校验和,并将其存储在received_checksum
中。 -
[2] 打开文件以写入模式:
fopen()
函数以二进制写入模式 (“wb”) 打开(或创建)文件"received_file"
。如果文件无法打开或创建,使用perror("fopen")
打印错误信息,并返回以终止函数。 -
[3] 接收文件数据:调用
receive_file()
函数,从套接字sock
接收文件数据并写入到打开的file
指针所指向的文件中。该函数具体实现不在此示例中,但它的职责是在传输过程中将数据流接收并保存。 -
[4] 计算接收数据的校验和:通过调用
calculate_checksum(file)
函数来计算所接收的文件数据的校验和。该函数应该能够重新从头读取文件并得出与发送方协议一致的校验和。 -
[5] 校验和匹配:如果接收的校验和
received_checksum
与计算出的校验和calculated_checksum
相等,则打印确认消息表明文件传输成功且完整。 -
[6] 校验和不匹配:如果校验和不匹配,意味着文件数据有可能在传输中损坏或丢失,打印警告消息。
-
[7] 关闭文件:
fclose()
函数用于关闭打开的文件释放系统资源。
此代码块涉及数据接收和完整性验证的基本过程,在实现文件传输时,校验和机制可以有效检测传输错误以确保数据完整性。
这样一个简易的文件传输程序就基本完成了。通过实践,读者可以更好地理解网络编程的概念和技巧。
3.3.3 案例分析:HTTP客户端
3.3.3.1 基于TCP的HTTP请求与响应
在构建一个HTTP客户端时,首先要了解HTTP协议是基于TCP协议建立连接的。因此,我们需要先创建一个TCP套接字,并通过这个套接字与服务器进行通信。
- 步骤介绍:
- 创建TCP套接字:使用
socket
函数创建一个流套接字。 - 连接服务器:使用
connect
连接到指定的服务器和端口。 - 发送HTTP请求:通过
send
函数发送HTTP请求。 - 接收HTTP响应:使用
recv
函数接收服务器的响应。
- 创建TCP套接字:使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>int main() {int sock;struct sockaddr_in server_addr;char request[] = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";char response[4096];// 创建套接字if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("Socket creation failed");exit(EXIT_FAILURE);}server_addr.sin_family = AF_INET;server_addr.sin_port = htons(80); // HTTP默认端口 [1]// 将IP地址转换为二进制形式if (inet_pton(AF_INET, "93.184.216.34", &server_addr.sin_addr) <= 0) { // example.com的IP地址 [2]perror("Invalid address/ Address not supported");exit(EXIT_FAILURE);}// 连接服务器if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("Connection failed");close(sock);exit(EXIT_FAILURE);}// 发送HTTP请求send(sock, request, strlen(request), 0); // 发送请求数据 [3]// 接收HTTP响应recv(sock, response, sizeof(response), 0); // 接收响应数据 [4]// 输出响应内容printf("HTTP Response:\n%s\n", response);// 关闭套接字close(sock);return 0;
}
- [1] HTTP默认端口:
htons(80)
将端口号80转换为网络字节序,这个端口号是HTTP协议的默认端口。 - [2] IP地址转换:
inet_pton()
函数将IP地址(“93.184.216.34”)从文本形式转换为二进制形式并存储在server_addr.sin_addr
中。 - [3] 发送请求数据:
send()
函数用于将HTTP请求的字符串通过套接字发送到服务器。 - [4] 接收响应数据:
recv()
函数用于接收服务器的响应数据并存储在response
缓冲中。
该程序演示了一个简单的HTTP客户端,使用套接字连接到指定的服务器地址并发送GET请求。收到响应后,程序输出响应内容并关闭套接字。
3.3.3.2 URL解析与请求构造
URL解析是HTTP客户端的重要一步。我们需要将URL分解为主机名和路径,以便在构造HTTP请求时使用。
- 步骤介绍:
- 解析URL:将URL拆解为协议、主机、端口和路径。
- 构造HTTP请求:根据解析出的信息生成完整的HTTP请求字符串。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 入门级URL解析函数示例
void parse_url(const char *url, char *host, char *path) {// 查找 "://"const char *host_start = strstr(url, "://");if (host_start) {host_start += 3; // 跳过协议部分 "://" [1]} else {host_start = url; // 如果没有找到,则从url开头解析 [2]}// 查找路径开始位置const char *path_start = strchr(host_start, '/');if (path_start) {strncpy(host, host_start, path_start - host_start); // 提取主机名 [3]strcpy(path, path_start); // 提取路径 [4]} else {strcpy(host, host_start); // 如果没有路径,默认为"/" [5]strcpy(path, "/");}
}// 构造HTTP GET请求
void construct_http_request(const char *host, const char *path, char *request) {// 使用sprintf构建HTTP请求字符串sprintf(request, "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", path, host); // 格式化为HTTP请求 [6]
}int main() {// 定义URL以及存储主机名、路径和请求的缓冲区char url[] = "http://example.com/path/to/resource";char host[256], path[256], request[512];// 调用URL解析parse_url(url, host, path);// 构造HTTP GET请求construct_http_request(host, path, request);// 输出解析结果和HTTP请求printf("Host: %s\nPath: %s\nRequest:\n%s\n", host, path, request);return 0;
}
- [1] 跳过协议部分:如果
url
中存在协议(如 “http://”),strstr
函数用于找到"://"
的位置,然后指针host_start
移动过这三个字符,以便定位到主机名的开始位置。 - [2] 如果没有找到协议:如果
url
中不包含协议部分,host_start
将指向url
的开头,假设整个url
都是主机部分。 - [3] 提取主机名:使用
strncpy
复制从host_start
到path_start
位置的字符串,这些字符组成主机名。 - [4] 提取路径:用
strcpy
将路径部分从path_start
开始的字符复制到path
,形成路径字符串。 - [5] 主机名和默认路径:当找不到路径(不存在
/
)时,将整个字符串作为主机名,并将路径设定为默认值 “/”。 - [6] 格式化为HTTP请求:
sprintf
用于构建 HTTP GET 请求字符串,将解析出的path
和host
置入适当位置,构成一个完整的 HTTP 1.1 请求格式。
这段代码展示了如何简单解析一个 URL 并使用解析结果生成一个 HTTP GET 请求。这在构建基本 HTTP 客户端或学习网络协议时非常有用。
3.3.3.3 响应解析与数据处理
接收到HTTP响应后,我们需要解析响应头和响应体,以便提取有用的信息,如状态码、内容类型和实际数据。
- 步骤介绍:
- 解析响应头:提取状态码、响应头字段和值。
- 处理响应体:根据内容类型进行适当的数据处理。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 分析HTTP响应示例
void parse_http_response(const char *response) {// 查找响应头和响应体的分界点 [1]const char *header_end = strstr(response, "\r\n\r\n");if (header_end) {char headers[512]; // 用于存储HTTP头信息 [2]char body[4096]; // 用于存储HTTP响应体 [3]// 解析响应头strncpy(headers, response, header_end - response); // 复制头部信息到headers [4]headers[header_end - response] = '\0'; // 添加字符串结束符 [5]printf("HTTP Headers:\n%s\n", headers);// 提取响应体strcpy(body, header_end + 4); // 复制响应体到body [6]printf("HTTP Body:\n%s\n", body);} else {printf("Invalid HTTP response.\n"); // 如果没有找到分界点则说明响应无效 [7]}
}int main() {// 虚拟HTTP响应示例 [8]char dummy_response[] = "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Content-Length: 13\r\n""\r\n""<h1>Hello</h1>";parse_http_response(dummy_response); // 调用解析函数 [9]return 0;
}
- [1] 查找响应头和响应体的分界点:使用
strstr()
来查找相应的 CRLFCRLF 字符串\r\n\r\n
,表示HTTP头与主体之间的分界。 - [2] 用于存储HTTP头信息:
headers
数组用于暂存解析后的HTTP头。 - [3] 用于存储HTTP响应体:
body
数组用于保存解析后的HTTP响应体数据。 - [4] 复制头部信息到 headers:使用
strncpy()
函数从响应中提取头部信息到headers
中。 - [5] 添加字符串结束符:确保
headers
以空字符结尾,以便正确形成C风格字符串。 - [6] 复制响应体到 body:使用
strcpy()
从响应中抽取主体部分。 - [7] 如果没有找到分界点则说明响应无效:如果找不到
\r\n\r\n
,表明响应格式不正确,打印错误信息。 - [8] 虚拟HTTP响应示例:
dummy_response
是一个模拟的HTTP响应字符串,用于测试。 - [9] 调用解析函数:
parse_http_response()
函数进行解析,打印头信息和主体。
上述代码展示了如何解析HTTP响应,提取响应头和响应体,并进行基本的打印输出。
通过上述步骤,构建一个基本的HTTP客户端,能够完整地从URL解析、构造HTTP请求,并解析返回的HTTP响应,不仅可以深入理解HTTP协议,还能提升网络编程能力。
相关文章:
【C语言系统编程】【第三部分:网络编程】3.3 实践与案例分析
3.3 实践与案例分析 在本章节中,我们将通过一些具体的案例来展示如何在实际项目中进行网络编程。这些案例不仅能帮助你理解各个概念,还能提升你的实践技能,并为你将来的项目提供参考。 3.3.1 案例分析:简单的聊天室 聊天室是网…...
Unity3D播放GIF图片 插件播放
系列文章目录 unity工具 文章目录 系列文章目录👉前言👉一、效果图👉二、使用方法1.代码如下👉三、壁纸分享👉总结👉前言 unity播放gif图片,本身是不支持的,但是可以使用其他方法来实现,有一种使用System.Drawing来实现播放的,一种是让UI把图片导出成帧动画图片,…...
基于STM32的远程工业控制系统架构设计:MQTT通信、React界面与FreeRTOS优化的综合应用
一、项目概述 项目目标和用途 本项目旨在开发一个基于STM32单片机的远程工业控制系统。该系统能够通过互联网监控和控制工业设备,实时采集环境和设备状态数据,并将数据上传至云端以便进行数据分析和可视化。用户可以通过移动应用或网页界面远程操作设备…...
墙绘艺术在线交易平台:SpringBoot技术详解
4 系统设计 墙绘产品展示交易平台的设计方案比如功能框架的设计,比如数据库的设计的好坏也就决定了该系统在开发层面是否高效,以及在系统维护层面是否容易维护和升级,因为在系统实现阶段是需要考虑用户的所有需求,要是在设计阶段没…...
VMware中Ubuntu系统Docker正常运行但网络不通(已解决)
问题描述:在VMware中的Ubuntu系统下部署了Docker,当在docker容器中运行Eureka微服务时,发现Eureka启动正常,但无法通过网页访问该容器中Eureka。 解决办法如下: 1、创建桥接网络:test-net sudo docker n…...
【web安全】——文件包含漏洞
1. 文件包含基础 和SQL注入等攻击方式一样,文件包含漏洞也是一种注入型漏洞,其本质就是输入一段用户能够控制的脚本或者代码,并让服务端执行。 1.1. 文件包含简介 什么叫包含呢?以PHP为例,我们常常把可重复使用的函…...
游戏如何对抗改包
游戏改包是指通过逆向分析手段及修改工具,来篡改游戏包内正常的设定和规则的行为,游戏包被篡改后,会被植入/剔除模块进行重打包。 本期图文我们将通过实际案例分析游戏改包的原理,并分享游戏如何应对改包问题。 安卓平台常见的改…...
12.梯度下降法的具体解析——举足轻重的模型优化算法
引言 梯度下降法(Gradient Descent)是一种广泛应用于机器学习领域的基本优化算法,它通过迭代地调整模型参数,最小化损失函数以求得到模型最优解。 通过阅读本篇博客,你可以: 1.知晓梯度下降法的具体流程 2.掌握不同梯度下降法…...
GPT对话知识库——C、C++,还有Java,他们之间有什么区别
目录 1,问: 1,答: 1. 语言特性与设计理念 C 语言: C 语言: Java 语言: 2. 内存管理 3. 运行效率 C 和 C: Java: 4. 程序的执行方式 C 和 C: Jav…...
华为GaussDB数据库之Yukon安装与使用
一、Yukon简介 Yukon(禹贡),基于openGauss、PostgreSQL、GaussDB数据库扩展地理空间数据的存储和管理能力,提供专业的GIS(Geographic Information System)功能,赋能传统关系型数据库。 Yukon 支…...
Linux命令:用于显示 Linux 发行版信息的命令行工具lsb_release详解
目录 一、概述 二、用法 1、基本用法 2、选项 3、获取帮助 三、示例 1. 显示所有信息 2. 只显示发行版名称 3. 只显示发行版版本号 4. 只显示发行版代号 5. 只显示发行版描述 6. 只显示值,不显示标签 四、使用场景 1、自动化脚本 2、诊断问题 3、环…...
sbb-classes 元素
sbb-classes 元素 在 JAIN SLEE(服务级别事件扩展)中,sbb-classes 元素用于定义服务边界组件(SBB)的类结构及其相关配置。这是每个 SBB 的必备部分,包含多个子元素,负责描述 SBB 的抽象类、接口…...
(作业)第三期书生·浦语大模型实战营(十一卷王场)--书生入门岛通关第3关Git 基础知识
任务编号 任务名称 任务描述 1 破冰活动 提交一份自我介绍。 2 实践项目 创建并提交一个项目。 破冰活动 提交一份自我介绍。 每位参与者提交一份自我介绍。 提交地址:https://github.com/InternLM/Tutorial 的 camp3 分支~ 安装并设置git 克隆仓库并…...
12.数据结构和算法-栈和队列的定义和特点
栈和队列的定义和特点 栈的应用 队列的常见应用 栈的定义和特点 栈的相关概念 栈的示意图 栈与一般线性表有什么不同 队列的定义和特点 队列的相关概念...
15分钟学 Python 第34天 :小项目-个人博客网站
Day 34: 小项目-个人博客网站 1. 引言 随着互联网的普及,个人博客已成为分享知识、体验和见解的一个重要平台。在这一节中,我们将使用Python的Flask框架构建一个简单的个人博客网站。我们将通过实际的项目来学习如何搭建Web应用、处理用户输入以及管理…...
从零开始实现RPC框架---------项目介绍及环境准备
一,介绍 RPC(Remote Procedure Call)远程过程调⽤,是⼀种通过⽹络从远程计算机上请求服务,⽽不需要 了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信, 如HTTP、TCP、UDP等, 并且在 TCP/…...
论文阅读:PET/CT Cross-modal medical image fusion of lung tumors based on DCIF-GAN
摘要 背景: 基于GAN的融合方法存在训练不稳定,提取图像的局部和全局上下文语义信息能力不足,交互融合程度不够等问题 贡献: 提出双耦合交互式融合GAN(Dual-Coupled Interactive Fusion GAN,DCIF-GAN&…...
java基础 day1
学习视频链接 人机交互的小故事 微软和乔布斯借鉴了施乐实现了如今的图形化界面 图形化界面对于用户来说,操作更加容易上手,但是也存在一些问题。使用图形化界面需要加载许多图片,所以消耗内存;此外运行的速度没有命令行快 Wi…...
cpp,git,unity学习
c#中的? 1. 空值类型(Nullable Types) ? 可以用于值类型(例如 int、bool 等),使它们可以接受 null。通常,值类型不能为 null,但是通过 ? 可以表示它们是可空的。 int? number null; // …...
HTML增加文本复制模块(使用户快速复制内容到剪贴板)
增加复制模块主要是为了方便用户快速复制内容到剪贴板,通常在需要提供文本信息可以便捷复制的网页设计或应用程序中常见。以下是为文本内容添加复制按钮的一个简单实现步骤: HTML结构: 在文本旁边添加一个复制按钮,例如 <butto…...
Spring Cloud面试题收集
Spring Cloud Spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。…...
观测云对接 SkyWalking 最佳实践
简介 SkyWalking 是一个开源的 APM(应用性能监控)和可观测性分析平台,专为微服务、云原生架构和基于容器的架构设计。它提供了分布式追踪、服务网格遥测分析、度量聚合和可视化一体化的解决方案。如果您的应用中正在使用SkyWalking …...
AI少女/HS2甜心选择2 仿天刀人物卡全合集打包
内含AI少女/甜心选择2 仿天刀角色卡全合集打包共21张 下载地址:https://www.51888w.com/408.html 部分演示图:...
MISC - 第11天(练习)
前言 各位师傅大家好,我是qmx_07,今天继续讲解MISC的相关知识 john-in-the-middle 导出http数据文件里面logo.png 是旗帜图案,放到stegsolve查看 通过转换颜色,发现flag信息 flag{J0hn_th3_Sn1ff3r} [UTCTF2020]docx 附件信息…...
[3.4]【机器人运动学MATLAB实战分析】PUMA560机器人逆运动学MATLAB计算
PUMA560是六自由度关节型机器人,其6个关节都是转动副,属于6R型操作臂。各连杆坐标系如图1,连杆参数如表1所示。 图1 PUMA560机器人的各连杆坐标系 表1 PUMA560机器人的连杆参数 用代数法对其进行运动学反解。具体步骤如下: 1、求θ1 PMUMA56...
centos常用知识和命令
linux目录及结构 /etc #存配置文件 /var #存日志文件 /home #用户家目录 /root #root用户家目录 /bin #命令文件目录 /sbin #超级管理员命令目录 /dev #设备文件目录 /boot #系统启动核心目录 /lib #库文件目录 /mnt #挂载目录 /tmp #临时文件目录 /usr #用户程序存…...
基于yolov8调用本地摄像头并将读取的信息传入jsonl中
最近在做水面垃圾识别的智能船 用到了yolov8进行目标检测 修改并添加了SEAttention注意力机制 详情见其他大神 【保姆级教程|YOLOv8添加注意力机制】【1】添加SEAttention注意力机制步骤详解、训练及推理使用_yolov8添加se-CSDN博客 并且修改传统的iou方法改为添加了wise-io…...
Linux中的进程间通信之管道
管道 管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道” 匿名管道 #include <unistd.h> 功能:创建一无名管道 原型 int pipe(int fd[2]); 参数 fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端 …...
【Vue】vue2项目打包后部署刷新404,配置publicPath ./ 不生效问题
Vue Router mode,为 history 无效,建议使用默认值 hash;...
【PyTorch】生成对抗网络
生成对抗网络是什么 概念 Generative Adversarial Nets,简称GAN GAN:生成对抗网络 —— 一种可以生成特定分布数据的模型 《Generative Adversarial Nets》 Ian J Goodfellow-2014 GAN网络结构 Recent Progress on Generative Adversarial Networks …...
html5 图片网站模板/百度识图软件
转载于:https://www.cnblogs.com/Minstrel223/p/10966959.html...
学院网站建设项目范围变更申请表/百度推广费用多少
三层架构 web层 他主要就是放servlet和各种过滤等等,去控制前端 service层 主要就是吧业务集中在这层 dao层 主要就是负责持久化,和数据库接壤。 WEB:SpringMVC,Struts1,Struts2 业务逻辑层:Spring 数据持久层:Hiber…...
商务网站开发心得/百度地图导航
1,触发器(http://www.cnblogs.com/zzwlovegfj/archive/2012/07/04/2576989.html)1.MYSQL中触发器中不能对本表进行 insert ,update ,delete 操作,以免递归循环触发2. 对于update 只能用set 进行操作,insert与delete只能借助第二张表才能实现需要的目的3.…...
免费网站推广咱们做/长沙seo推广优化
最近和一个猎头聊天,说到现在前端供需脱节的境况。一方面用人方招不到想要的中高级前端,另一方面市场上有大量初级前端薪资要不上价。特别是用 Vue 框架的,因为好上手,所以很多人将 Vue 作为入门框架,但学得深的人并不…...
做电影网站会不会涉及版权问题/优化防控举措
1.angularJS ng-model"spaceInfor.level.id" name"spaceFloor" ng-options"item.id as item.name for item in levels"> many to one 绑定的是ID, 显示时根据ID显示出名字。 2.传参数一般传数字和ID,一般不去传中文。 …...
门户网站英文/搭建自己的网站
进程ID与进程句柄 全局句柄表 索引地址内核对象00x87654321EPROCESS1 20x87654400ETHREAD30x87654000EPROCESS40x87653400ETHREAD 进程的句柄表是私有的,仅当前进程有效,拿到其他进程中不是有效的。而系统拥有一张全局句柄表,整个系统下都…...